Skip to content

Commit

Permalink
Implement spaces list (#6199)
Browse files Browse the repository at this point in the history
* Implement spaces list overview

Co-authored-by: Florian Schade <f.schade@icloud.com>
  • Loading branch information
JammingBen and fschade authored Jan 13, 2022
1 parent f71ae66 commit 34ffb3f
Show file tree
Hide file tree
Showing 25 changed files with 3,738 additions and 29 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-spaces-list
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Implement spaces list

We added a new route that lists all available spaces of type "project".

https://github.com/owncloud/web/pull/6199
https://github.com/owncloud/web/issues/6104
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packages/web-app-markdown-editor",
"packages/web-app-media-viewer",
"packages/web-app-search",
"packages/web-client",
"packages/web-pkg",
"packages/web-runtime"
],
Expand Down
5 changes: 4 additions & 1 deletion packages/web-app-files/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class="files-list-wrapper oc-width-expand"
@dragover="$_ocApp_dragOver"
>
<app-bar id="files-app-bar" />
<app-bar v-if="!hideAppBar" id="files-app-bar" />
<progress-bar v-show="$_uploadProgressVisible" id="files-upload-progress" class="oc-p-s" />
<router-view id="files-view" />
</div>
Expand Down Expand Up @@ -55,6 +55,9 @@ export default {
},
showSidebar() {
return !this.sidebarClosed
},
hideAppBar() {
return this.$route.meta.hideAppBar === true
}
},
watch: {
Expand Down
16 changes: 15 additions & 1 deletion packages/web-app-files/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Personal from './views/Personal.vue'
import SharedWithMe from './views/SharedWithMe.vue'
import SharedWithOthers from './views/SharedWithOthers.vue'
import SharedViaLink from './views/SharedViaLink.vue'
import SpaceProjects from './views/spaces/Projects.vue'
import Trashbin from './views/Trashbin.vue'
import translations from '../l10n/translations.json'
import quickActions from './quickActions'
Expand Down Expand Up @@ -39,7 +40,7 @@ const navItems = [
iconMaterial: appInfo.icon,
fillType: 'fill',
route: {
path: `/${appInfo.id}/spaces/`
path: `/${appInfo.id}/spaces/personal/home`
}
},
{
Expand Down Expand Up @@ -77,6 +78,16 @@ const navItems = [
path: `/${appInfo.id}/shares/via-link`
}
},
{
name: $gettext('Spaces'),
iconMaterial: 'layout-grid',
route: {
path: `/${appInfo.id}/spaces/projects`
},
enabled(capabilities) {
return capabilities.spaces && capabilities.spaces.enabled === true
}
},
{
name: $gettext('Deleted files'),
iconMaterial: 'delete-bin-5',
Expand Down Expand Up @@ -105,6 +116,9 @@ export default {
SharedViaLink,
SharedWithMe,
SharedWithOthers,
Spaces: {
Projects: SpaceProjects
},
Trashbin
}),
navItems,
Expand Down
29 changes: 17 additions & 12 deletions packages/web-app-files/src/router/router.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import Vue, { ComponentOptions } from 'vue'

/**
* we need to inject the vue files into the route builders,
* this is because we also import the provided helpers from other js|ts files
* like mixins, rollup seems to have a problem to import files which contain vue file imports
* into js files which then again get imported by other vue files...
*/
export interface RouteComponents {
App: any
Favorites: any
FilesDrop: any
LocationPicker: any
PrivateLink: any
PublicFiles: any
Personal: any
PublicLink: any
SharedWithMe: any
SharedWithOthers: any
SharedViaLink: any
Trashbin: any
App: ComponentOptions<Vue>
Favorites: ComponentOptions<Vue>
FilesDrop: ComponentOptions<Vue>
LocationPicker: ComponentOptions<Vue>
PrivateLink: ComponentOptions<Vue>
PublicFiles: ComponentOptions<Vue>
Personal: ComponentOptions<Vue>
PublicLink: ComponentOptions<Vue>
SharedWithMe: ComponentOptions<Vue>
SharedWithOthers: ComponentOptions<Vue>
SharedViaLink: ComponentOptions<Vue>
Spaces: {
Projects: ComponentOptions<Vue>
}
Trashbin: ComponentOptions<Vue>
}
36 changes: 23 additions & 13 deletions packages/web-app-files/src/router/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,48 @@ import { Location, RouteConfig } from 'vue-router'
import { RouteComponents } from './router'
import { createLocation, isLocationActiveDirector, $gettext } from './utils'

type shareTypes = 'files-spaces-personal-home'
type spaceTypes = 'files-spaces-personal-home' | 'files-spaces-projects'

export const createLocationSpaces = (name: shareTypes, location = {}): Location =>
export const createLocationSpaces = (name: spaceTypes, location = {}): Location =>
createLocation(
name,
{
params: {
storage: 'home',
namespace: 'personal'
...(name === 'files-spaces-personal-home' && { storage: 'home' })
}
},
location
)
export const isLocationSpacesActive = isLocationActiveDirector<shareTypes>(
createLocationSpaces('files-spaces-personal-home')

const locationSpacesProjects = createLocationSpaces('files-spaces-projects')
const locationSpacesPersonalHome = createLocationSpaces('files-spaces-personal-home')

export const isLocationSpacesActive = isLocationActiveDirector<spaceTypes>(
locationSpacesProjects,
locationSpacesPersonalHome
)

export const buildRoutes = (components: RouteComponents): RouteConfig[] => [
{
path: '/spaces',
redirect: (to) => createLocationSpaces('files-spaces-personal-home', to)
},
{
path: '/spaces/:namespace',
components: {
app: components.App
},
redirect: (to) => createLocationSpaces('files-spaces-personal-home', to),
children: [
{
name: createLocationSpaces('files-spaces-personal-home').name,
path: ':storage/:item*',
path: 'projects',
name: locationSpacesProjects.name,
component: components.Spaces?.Projects,
meta: {
hideFilelistActions: true,
hasBulkActions: true,
hideAppBar: true,
title: $gettext('Spaces')
}
},
{
path: 'personal/:storage/:item*',
name: locationSpacesPersonalHome.name,
component: components.Personal,
meta: {
hasBulkActions: true,
Expand Down
98 changes: 98 additions & 0 deletions packages/web-app-files/src/views/spaces/Projects.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<div class="oc-p-s">
<h2 v-text="$gettext('Spaces')" />
<span v-text="$gettext('Access all project related files in one place.')" />
<a href="#" v-text="$gettext('Learn more about spaces.')" />
<h3 v-text="$gettext('Your spaces')" />
<hr class="oc-mb-s" />
<list-loader v-if="loadSpacesTask.isRunning" />
<template v-else>
<no-content-message
v-if="!spaces.length"
id="files-spaces-empty"
class="files-empty"
icon="layout-grid"
>
<template #message>
<span v-translate>You don't have access to any spaces</span>
</template>
</no-content-message>
<div v-else class="spaces-list">
<div
class="
oc-grid
oc-grid-match
oc-grid-column-small
oc-grid-row-large
oc-text-center
oc-child-width-1-3@s
"
>
<a v-for="space in spaces" :key="space.id" href="#" class="oc-mb-m">
<span class="spaces-list-card oc-border oc-card oc-card-default">
<span class="oc-card-media-top oc-border-b">
<img v-if="space.image" :src="space.image" alt="" />
<oc-icon v-else name="layout-grid" size="xxlarge" class="oc-px-m oc-py-m" />
</span>
<span class="oc-card-body">
<span class="oc-card-title" v-text="space.name" />
</span>
</span>
</a>
</div>
</div>
</template>
</div>
</template>

<script>
import NoContentMessage from '../../components/FilesList/NoContentMessage.vue'
import ListLoader from '../../components/FilesList/ListLoader.vue'
import { client } from 'web-client'
import { ref } from '@vue/composition-api'
import { useStore } from '../../composables'
import { useTask } from 'vue-concurrency'
export default {
components: {
NoContentMessage,
ListLoader
},
setup() {
const store = useStore()
const spaces = ref([])
const { graph } = client(store.getters.configuration.server, store.getters.getToken)
const loadSpacesTask = useTask(function* () {
const response = yield graph.drives.listMyDrives()
spaces.value = (response.data?.value || []).filter((drive) => drive.driveType === 'project')
})
loadSpacesTask.perform()
return {
spaces,
loadSpacesTask
}
}
}
</script>
<style lang="scss">
#files-spaces-empty {
height: 50vh;
}
.spaces-list {
&-card {
box-shadow: none !important;
}
.oc-card-media-top {
display: inline-block;
width: 100%;
background-color: var(--oc-color-background-muted);
max-height: 150px;
}
}
</style>
63 changes: 63 additions & 0 deletions packages/web-app-files/tests/unit/views/spaces/Projects.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { mount } from '@vue/test-utils'
import { localVue } from '../views.setup'
import { createStore } from 'vuex-extensions'
import mockAxios from 'jest-mock-axios'
import SpaceProjects from '../../../../src/views/spaces/Projects.vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'

localVue.use(VueRouter)

const selectors = {
sharesNoContentMessage: '#files-spaces-empty',
spacesList: '.spaces-list'
}

beforeEach(mockAxios.reset)

describe('Spaces component', () => {
it('should show a "no content" message', async () => {
mockAxios.request.mockImplementationOnce(() => {
return Promise.resolve({
data: {
value: []
}
})
})

const wrapper = getMountedWrapper()
await wrapper.vm.loadSpacesTask.last

expect(wrapper.find(selectors.sharesNoContentMessage).exists()).toBeTruthy()
})

it('should only list drives of type "project"', async () => {
mockAxios.request.mockImplementationOnce(() => {
return Promise.resolve({
data: {
value: [{ driveType: 'project' }, { driveType: 'personal' }]
}
})
})

const wrapper = getMountedWrapper()
await wrapper.vm.loadSpacesTask.last

expect(wrapper.vm.spaces.length).toEqual(1)
expect(wrapper).toMatchSnapshot()
})
})

function getMountedWrapper() {
return mount(SpaceProjects, {
localVue,
router: new VueRouter(),
store: createStore(Vuex.Store, {
getters: {
configuration: () => ({
server: 'https://example.com/'
})
}
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Spaces component should only list drives of type "project" 1`] = `
<div class="oc-p-s">
<h2>Spaces</h2> <span>Access all project related files in one place.</span> <a href="#">Learn more about spaces.</a>
<h3>Your spaces</h3>
<hr class="oc-mb-s">
<div class="spaces-list">
<div class="
oc-grid
oc-grid-match
oc-grid-column-small
oc-grid-row-large
oc-text-center
oc-child-width-1-3@s
"><a href="#" class="oc-mb-m"><span class="spaces-list-card oc-border oc-card oc-card-default"><span class="oc-card-media-top oc-border-b"><span class="oc-px-m oc-py-m oc-icon oc-icon-xxl oc-icon-passive"><!----></span></span> <span class="oc-card-body"><span class="oc-card-title"></span></span>
</span></a>
</div>
</div>
</div>
`;
11 changes: 11 additions & 0 deletions packages/web-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "web-client",
"private": true,
"version": "0.0.0",
"description": "ownCloud web client",
"license": "AGPL-3.0",
"main": "src/index.ts",
"scripts": {
"generate-openapi": "rm -rf src/generated && docker run --rm -v \"${PWD}/src:/local\" openapitools/openapi-generator-cli generate -i https://github.com/raw/owncloud/libre-graph-api/main/api/openapi-spec/v0.0.yaml -g typescript-axios -o /local/generated"
}
}
4 changes: 4 additions & 0 deletions packages/web-client/src/generated/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
wwwroot/*.js
node_modules
typings
dist
1 change: 1 addition & 0 deletions packages/web-client/src/generated/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
Loading

0 comments on commit 34ffb3f

Please sign in to comment.