diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2023-04-14 12:40:08 +0200 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2023-04-18 09:02:01 +0200 |
commit | d7ab8da1ef7decb512d68b038fc7e92758fbb518 (patch) | |
tree | 302b14a5a8a5c3b07cabc3595caba53500eca238 /apps/files/src | |
parent | ff58cd52279cccfbda0cc4683f1194d6c7ee283b (diff) | |
download | nextcloud-server-d7ab8da1ef7decb512d68b038fc7e92758fbb518.tar.gz nextcloud-server-d7ab8da1ef7decb512d68b038fc7e92758fbb518.zip |
feat(files): add view config service to store user-config per view
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/FilesListHeader.vue | 34 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeaderButton.vue | 31 | ||||
-rw-r--r-- | apps/files/src/mixins/filesSorting.ts | 69 | ||||
-rw-r--r-- | apps/files/src/services/Navigation.ts | 4 | ||||
-rw-r--r-- | apps/files/src/store/sorting.ts | 80 | ||||
-rw-r--r-- | apps/files/src/store/userconfig.ts | 2 | ||||
-rw-r--r-- | apps/files/src/store/viewConfig.ts | 103 | ||||
-rw-r--r-- | apps/files/src/types.ts | 24 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 22 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.cy.ts | 18 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 33 |
11 files changed, 243 insertions, 177 deletions
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue index 2edfb4aa30e..9e3fe0d46de 100644 --- a/apps/files/src/components/FilesListHeader.vue +++ b/apps/files/src/components/FilesListHeader.vue @@ -66,16 +66,15 @@ </template> <script lang="ts"> -import { mapState } from 'pinia' import { translate } from '@nextcloud/l10n' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import Vue from 'vue' import { useFilesStore } from '../store/files.ts' import { useSelectionStore } from '../store/selection.ts' -import { useSortingStore } from '../store/sorting.ts' import FilesListHeaderActions from './FilesListHeaderActions.vue' import FilesListHeaderButton from './FilesListHeaderButton.vue' +import filesSortingMixin from '../mixins/filesSorting.ts' import logger from '../logger.js' export default Vue.extend({ @@ -87,11 +86,9 @@ export default Vue.extend({ FilesListHeaderActions, }, - provide() { - return { - toggleSortBy: this.toggleSortBy, - } - }, + mixins: [ + filesSortingMixin, + ], props: { isSizeAvailable: { @@ -111,17 +108,13 @@ export default Vue.extend({ setup() { const filesStore = useFilesStore() const selectionStore = useSelectionStore() - const sortingStore = useSortingStore() return { filesStore, selectionStore, - sortingStore, } }, computed: { - ...mapState(useSortingStore, ['filesSortingConfig']), - currentView() { return this.$navigation.active }, @@ -166,15 +159,6 @@ export default Vue.extend({ isSomeSelected() { return !this.isAllSelected && !this.isNoneSelected }, - - sortingMode() { - return this.sortingStore.getSortingMode(this.currentView.id) - || this.currentView.defaultSortKey - || 'basename' - }, - isAscSorting() { - return this.sortingStore.isAscSorting(this.currentView.id) === true - }, }, methods: { @@ -199,16 +183,6 @@ export default Vue.extend({ } }, - toggleSortBy(key) { - // If we're already sorting by this key, flip the direction - if (this.sortingMode === key) { - this.sortingStore.toggleSortingDirection(this.currentView.id) - return - } - // else sort ASC by this new key - this.sortingStore.setSortingBy(key, this.currentView.id) - }, - t: translate, }, }) diff --git a/apps/files/src/components/FilesListHeaderButton.vue b/apps/files/src/components/FilesListHeaderButton.vue index afa48465dab..9aac83a185d 100644 --- a/apps/files/src/components/FilesListHeaderButton.vue +++ b/apps/files/src/components/FilesListHeaderButton.vue @@ -33,14 +33,13 @@ </template> <script lang="ts"> -import { mapState } from 'pinia' import { translate } from '@nextcloud/l10n' import MenuDown from 'vue-material-design-icons/MenuDown.vue' import MenuUp from 'vue-material-design-icons/MenuUp.vue' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import Vue from 'vue' -import { useSortingStore } from '../store/sorting.ts' +import filesSortingMixin from '../mixins/filesSorting.ts' export default Vue.extend({ name: 'FilesListHeaderButton', @@ -51,7 +50,9 @@ export default Vue.extend({ NcButton, }, - inject: ['toggleSortBy'], + mixins: [ + filesSortingMixin, + ], props: { name: { @@ -64,30 +65,6 @@ export default Vue.extend({ }, }, - setup() { - const sortingStore = useSortingStore() - return { - sortingStore, - } - }, - - computed: { - ...mapState(useSortingStore, ['filesSortingConfig']), - - currentView() { - return this.$navigation.active - }, - - sortingMode() { - return this.sortingStore.getSortingMode(this.currentView.id) - || this.currentView.defaultSortKey - || 'basename' - }, - isAscSorting() { - return this.sortingStore.isAscSorting(this.currentView.id) === true - }, - }, - methods: { sortAriaLabel(column) { const direction = this.isAscSorting diff --git a/apps/files/src/mixins/filesSorting.ts b/apps/files/src/mixins/filesSorting.ts new file mode 100644 index 00000000000..8930587ffab --- /dev/null +++ b/apps/files/src/mixins/filesSorting.ts @@ -0,0 +1,69 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +import Vue from 'vue' + +import { useViewConfigStore } from '../store/viewConfig' +import type { Navigation } from '../services/Navigation' + +export default Vue.extend({ + setup() { + const viewConfigStore = useViewConfigStore() + return { + viewConfigStore, + } + }, + + computed: { + currentView(): Navigation { + return this.$navigation.active + }, + + /** + * Get the sorting mode for the current view + */ + sortingMode(): string { + return this.viewConfigStore.getConfig(this.currentView.id)?.sorting_mode + || this.currentView?.defaultSortKey + || 'basename' + }, + + /** + * Get the sorting direction for the current view + */ + isAscSorting(): boolean { + const sortingDirection = this.viewConfigStore.getConfig(this.currentView.id)?.sorting_direction + return sortingDirection === 'asc' + }, + }, + + methods: { + toggleSortBy(key: string) { + // If we're already sorting by this key, flip the direction + if (this.sortingMode === key) { + this.viewConfigStore.toggleSortingDirection(this.currentView.id) + return + } + // else sort ASC by this new key + this.viewConfigStore.setSortingBy(key, this.currentView.id) + }, + }, +}) diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts index a39b04b642a..e86266013d7 100644 --- a/apps/files/src/services/Navigation.ts +++ b/apps/files/src/services/Navigation.ts @@ -71,7 +71,9 @@ export interface Navigation { parent?: string /** This view is sticky (sent at the bottom) */ sticky?: boolean - /** This view has children and is expanded or not */ + /** This view has children and is expanded or not, + * will be overridden by user config. + */ expanded?: boolean /** diff --git a/apps/files/src/store/sorting.ts b/apps/files/src/store/sorting.ts deleted file mode 100644 index 6afb6fa97b6..00000000000 --- a/apps/files/src/store/sorting.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -/* eslint-disable */ -import { loadState } from '@nextcloud/initial-state' -import { generateUrl } from '@nextcloud/router' -import { defineStore } from 'pinia' -import Vue from 'vue' -import axios from '@nextcloud/axios' -import type { direction, SortingStore } from '../types.ts' - -const saveUserConfig = (mode: string, direction: direction, view: string) => { - return axios.post(generateUrl('/apps/files/api/v1/sorting'), { - mode, - direction, - view, - }) -} - -const filesSortingConfig = loadState('files', 'filesSortingConfig', {}) as SortingStore - -export const useSortingStore = defineStore('sorting', { - state: () => ({ - filesSortingConfig, - }), - - getters: { - isAscSorting: (state) => (view: string = 'files') => state.filesSortingConfig[view]?.direction !== 'desc', - getSortingMode: (state) => (view: string = 'files') => state.filesSortingConfig[view]?.mode, - }, - - actions: { - /** - * Set the sorting key AND sort by ASC - * The key param must be a valid key of a File object - * If not found, will be searched within the File attributes - */ - setSortingBy(key: string = 'basename', view: string = 'files') { - const config = this.filesSortingConfig[view] || {} - config.mode = key - config.direction = 'asc' - - // Save new config - Vue.set(this.filesSortingConfig, view, config) - saveUserConfig(config.mode, config.direction, view) - }, - - /** - * Toggle the sorting direction - */ - toggleSortingDirection(view: string = 'files') { - const config = this.filesSortingConfig[view] || { 'direction': 'asc' } - const newDirection = config.direction === 'asc' ? 'desc' : 'asc' - config.direction = newDirection - - // Save new config - Vue.set(this.filesSortingConfig, view, config) - saveUserConfig(config.mode, config.direction, view) - } - } -}) - diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts index 05d63c95424..c81b7b4d77f 100644 --- a/apps/files/src/store/userconfig.ts +++ b/apps/files/src/store/userconfig.ts @@ -51,7 +51,7 @@ export const useUserConfigStore = () => { * Update the user config local store AND on server side */ async update(key: string, value: boolean) { - await axios.post(generateUrl('/apps/files/api/v1/config/' + key), { + await axios.put(generateUrl('/apps/files/api/v1/config/' + key), { value, }) diff --git a/apps/files/src/store/viewConfig.ts b/apps/files/src/store/viewConfig.ts new file mode 100644 index 00000000000..d7a5ab1daa6 --- /dev/null +++ b/apps/files/src/store/viewConfig.ts @@ -0,0 +1,103 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +/* eslint-disable */ +import { defineStore } from 'pinia' +import { emit, subscribe } from '@nextcloud/event-bus' +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' +import axios from '@nextcloud/axios' +import Vue from 'vue' + +import { ViewConfigs, ViewConfigStore, ViewId } from '../types.ts' +import { ViewConfig } from '../types' + +const viewConfig = loadState('files', 'viewConfigs', {}) as ViewConfigs + +export const useViewConfigStore = () => { + const store = defineStore('viewconfig', { + state: () => ({ + viewConfig, + } as ViewConfigStore), + + getters: { + getConfig: (state) => (view: ViewId): ViewConfig => state.viewConfig[view] || {}, + }, + + actions: { + /** + * Update the view config local store + */ + onUpdate(view: ViewId, key: string, value: boolean) { + if (!this.viewConfig[view]) { + Vue.set(this.viewConfig, view, {}) + } + Vue.set(this.viewConfig[view], key, value) + }, + + /** + * Update the view config local store AND on server side + */ + async update(view: ViewId, key: string, value: boolean) { + axios.put(generateUrl(`/apps/files/api/v1/views/${view}/${key}`), { + value, + }) + + emit('files:viewconfig:updated', { view, key, value }) + }, + + /** + * Set the sorting key AND sort by ASC + * The key param must be a valid key of a File object + * If not found, will be searched within the File attributes + */ + setSortingBy(key: string = 'basename', view: string = 'files') { + // Save new config + this.update(view, 'sorting_mode', key) + this.update(view, 'sorting_direction', 'asc') + }, + + /** + * Toggle the sorting direction + */ + toggleSortingDirection(view: string = 'files') { + const config = this.getConfig(view) || { 'sorting_direction': 'asc' } + const newDirection = config.sorting_direction === 'asc' ? 'desc' : 'asc' + + // Save new config + this.update(view, 'sorting_direction', newDirection) + } + } + }) + + const viewConfigStore = store() + + // Make sure we only register the listeners once + if (!viewConfigStore._initialized) { + subscribe('files:viewconfig:updated', function({ view, key, value }: { view: ViewId, key: string, value: boolean }) { + viewConfigStore.onUpdate(view, key, value) + }) + viewConfigStore._initialized = true + } + + return viewConfigStore +} + diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 2e8358aa704..cca6fb9111f 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -26,6 +26,7 @@ import type { Node } from '@nextcloud/files' // Global definitions export type Service = string export type FileId = number +export type ViewId = string // Files store export type FilesState = { @@ -61,18 +62,6 @@ export interface PathOptions { fileid: FileId } -// Sorting store -export type direction = 'asc' | 'desc' - -export interface SortingConfig { - mode: string - direction: direction -} - -export interface SortingStore { - [key: string]: SortingConfig -} - // User config store export interface UserConfig { [key: string]: boolean @@ -92,3 +81,14 @@ export type GlobalActions = 'global' export interface ActionsMenuStore { opened: GlobalActions|string|null } + +// View config store +export interface ViewConfig { + [key: string]: string|boolean +} +export interface ViewConfigs { + [viewId: ViewId]: ViewConfig +} +export interface ViewConfigStore { + viewConfig: ViewConfigs +}
\ No newline at end of file diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 34006228f37..c11b5820308 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -75,14 +75,15 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import TrashCan from 'vue-material-design-icons/TrashCan.vue' import Vue from 'vue' -import Navigation, { ContentsWithRoot } from '../services/Navigation.ts' import { useFilesStore } from '../store/files.ts' import { usePathsStore } from '../store/paths.ts' import { useSelectionStore } from '../store/selection.ts' -import { useSortingStore } from '../store/sorting.ts' +import { useViewConfigStore } from '../store/viewConfig.ts' import BreadCrumbs from '../components/BreadCrumbs.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' +import filesSortingMixin from '../mixins/filesSorting.ts' import logger from '../logger.js' +import Navigation, { ContentsWithRoot } from '../services/Navigation.ts' export default Vue.extend({ name: 'FilesList', @@ -97,16 +98,20 @@ export default Vue.extend({ TrashCan, }, + mixins: [ + filesSortingMixin, + ], + setup() { const pathsStore = usePathsStore() const filesStore = useFilesStore() const selectionStore = useSelectionStore() - const sortingStore = useSortingStore() + const viewConfigStore = useViewConfigStore() return { filesStore, pathsStore, selectionStore, - sortingStore, + viewConfigStore, } }, @@ -151,15 +156,6 @@ export default Vue.extend({ return this.filesStore.getNode(fileId) }, - sortingMode() { - return this.sortingStore.getSortingMode(this.currentView.id) - || this.currentView.defaultSortKey - || 'basename' - }, - isAscSorting() { - return this.sortingStore.isAscSorting(this.currentView.id) === true - }, - /** * The current directory contents. * diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts index 3d5307e6800..c9a7ca98ee1 100644 --- a/apps/files/src/views/Navigation.cy.ts +++ b/apps/files/src/views/Navigation.cy.ts @@ -7,6 +7,7 @@ import { createTestingPinia } from '@pinia/testing' import NavigationService from '../services/Navigation.ts' import NavigationView from './Navigation.vue' import router from '../router/router.js' +import { useViewConfigStore } from '../store/viewConfig' describe('Navigation renders', () => { const Navigation = new NavigationService() as NavigationService @@ -116,23 +117,28 @@ describe('Navigation API', () => { router, }) + cy.wrap(useViewConfigStore()).as('viewConfigStore') + cy.get('[data-cy-files-navigation]').should('be.visible') cy.get('[data-cy-files-navigation-item]').should('have.length', 3) - // Intercept collapse preference request - cy.intercept('POST', '*/apps/files/api/v1/toggleShowFolder/*', { - statusCode: 200, - }).as('toggleShowFolder') - // Toggle the sharing entry children cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').should('exist') cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').click({ force: true }) - cy.wait('@toggleShowFolder') + + // Expect store update to be called + cy.get('@viewConfigStore').its('update').should('have.been.calledWith', 'sharing', 'expanded', true) // Validate children cy.get('[data-cy-files-navigation-item="sharingin"]').should('be.visible') cy.get('[data-cy-files-navigation-item="sharingin"]').should('contain.text', 'Shared with me') + // Toggle the sharing entry children 🇦again + cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').click({ force: true }) + cy.get('[data-cy-files-navigation-item="sharingin"]').should('not.be.visible') + + // Expect store update to be called + cy.get('@viewConfigStore').its('update').should('have.been.calledWith', 'sharing', 'expanded', false) }) it('Throws when adding a duplicate entry', () => { diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 26ac99c15d3..cc714964c9b 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -27,7 +27,7 @@ :allow-collapse="true" :data-cy-files-navigation-item="view.id" :icon="view.iconClass" - :open="view.expanded" + :open="isExpanded(view)" :pinned="view.sticky" :title="view.name" :to="generateToNavigation(view)" @@ -74,20 +74,18 @@ <script> import { emit, subscribe } from '@nextcloud/event-bus' -import { generateUrl } from '@nextcloud/router' import { translate } from '@nextcloud/l10n' - -import axios from '@nextcloud/axios' import Cog from 'vue-material-design-icons/Cog.vue' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' +import { setPageHeading } from '../../../../core/src/OCP/accessibility.js' +import { useViewConfigStore } from '../store/viewConfig.ts' import logger from '../logger.js' import Navigation from '../services/Navigation.ts' import NavigationQuota from '../components/NavigationQuota.vue' import SettingsModal from './Settings.vue' -import { setPageHeading } from '../../../../core/src/OCP/accessibility.js' export default { name: 'Navigation', @@ -109,6 +107,13 @@ export default { }, }, + setup() { + const viewConfigStore = useViewConfigStore() + return { + viewConfigStore, + } + }, + data() { return { settingsOpened: false, @@ -245,8 +250,22 @@ export default { */ onToggleExpand(view) { // Invert state - view.expanded = !view.expanded - axios.post(generateUrl(`/apps/files/api/v1/toggleShowFolder/${view.id}`), { show: view.expanded }) + const isExpanded = this.isExpanded(view) + // Update the view expanded state, might not be necessary + view.expanded = !isExpanded + this.viewConfigStore.update(view.id, 'expanded', !isExpanded) + }, + + /** + * Check if a view is expanded by user config + * or fallback to the default value. + * + * @param {Navigation} view the view to check + */ + isExpanded(view) { + return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean' + ? this.viewConfigStore.getConfig(view.id).expanded === true + : view.expanded === true }, /** |