aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/store
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/store')
-rw-r--r--apps/files/src/store/active.ts112
-rw-r--r--apps/files/src/store/files.ts29
-rw-r--r--apps/files/src/store/filters.ts8
-rw-r--r--apps/files/src/store/renaming.ts317
-rw-r--r--apps/files/src/store/search.ts153
-rw-r--r--apps/files/src/store/userconfig.ts88
-rw-r--r--apps/files/src/store/viewConfig.ts150
7 files changed, 510 insertions, 347 deletions
diff --git a/apps/files/src/store/active.ts b/apps/files/src/store/active.ts
index e261e817f3d..1303a157b08 100644
--- a/apps/files/src/store/active.ts
+++ b/apps/files/src/store/active.ts
@@ -3,74 +3,84 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { ActiveStore } from '../types.ts'
-import type { FileAction, Node, View } from '@nextcloud/files'
+import type { FileAction, View, Node, Folder } from '@nextcloud/files'
-import { defineStore } from 'pinia'
-import { getNavigation } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
+import { getNavigation } from '@nextcloud/files'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
import logger from '../logger.ts'
-export const useActiveStore = function(...args) {
- const store = defineStore('active', {
- state: () => ({
- _initialized: false,
- activeNode: null,
- activeView: null,
- activeAction: null,
- } as ActiveStore),
+export const useActiveStore = defineStore('active', () => {
+ /**
+ * The currently active action
+ */
+ const activeAction = ref<FileAction>()
- actions: {
- setActiveNode(node: Node) {
- if (!node) {
- throw new Error('Use clearActiveNode to clear the active node')
- }
- logger.debug('Setting active node', { node })
- this.activeNode = node
- },
+ /**
+ * The currently active folder
+ */
+ const activeFolder = ref<Folder>()
- clearActiveNode() {
- this.activeNode = null
- },
+ /**
+ * The current active node within the folder
+ */
+ const activeNode = ref<Node>()
- onDeletedNode(node: Node) {
- if (this.activeNode && this.activeNode.source === node.source) {
- this.clearActiveNode()
- }
- },
+ /**
+ * The current active view
+ */
+ const activeView = ref<View>()
- setActiveAction(action: FileAction) {
- this.activeAction = action
- },
+ initialize()
- clearActiveAction() {
- this.activeAction = null
- },
+ /**
+ * Unset the active node if deleted
+ *
+ * @param node - The node thats deleted
+ * @private
+ */
+ function onDeletedNode(node: Node) {
+ if (activeNode.value && activeNode.value.source === node.source) {
+ activeNode.value = undefined
+ }
+ }
- onChangedView(view: View|null = null) {
- logger.debug('Setting active view', { view })
- this.activeView = view
- this.clearActiveNode()
- },
- },
- })
+ /**
+ * Callback to update the current active view
+ *
+ * @param view - The new active view
+ * @private
+ */
+ function onChangedView(view: View|null = null) {
+ logger.debug('Setting active view', { view })
+ activeView.value = view ?? undefined
+ activeNode.value = undefined
+ }
- const activeStore = store(...args)
- const navigation = getNavigation()
+ /**
+ * Initalize the store - connect all event listeners.
+ * @private
+ */
+ function initialize() {
+ const navigation = getNavigation()
- // Make sure we only register the listeners once
- if (!activeStore._initialized) {
- subscribe('files:node:deleted', activeStore.onDeletedNode)
+ // Make sure we only register the listeners once
+ subscribe('files:node:deleted', onDeletedNode)
- activeStore._initialized = true
- activeStore.onChangedView(navigation.active)
+ onChangedView(navigation.active)
// Or you can react to changes of the current active view
navigation.addEventListener('updateActive', (event) => {
- activeStore.onChangedView(event.detail)
+ onChangedView(event.detail)
})
}
- return activeStore
-}
+ return {
+ activeAction,
+ activeFolder,
+ activeNode,
+ activeView,
+ }
+})
diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts
index a2413b5a1ba..0bcf4ce9350 100644
--- a/apps/files/src/store/files.ts
+++ b/apps/files/src/store/files.ts
@@ -54,13 +54,13 @@ export const useFilesStore = function(...args) {
actions: {
/**
- * Get cached child nodes within a given path
+ * Get cached directory matching a given path
*
- * @param service The service (files view)
- * @param path The path relative within the service
- * @return Array of cached nodes within the path
+ * @param service - The service (files view)
+ * @param path - The path relative within the service
+ * @return The folder if found
*/
- getNodesByPath(service: string, path?: string): Node[] {
+ getDirectoryByPath(service: string, path?: string): Folder | undefined {
const pathsStore = usePathsStore()
let folder: Folder | undefined
@@ -74,6 +74,19 @@ export const useFilesStore = function(...args) {
}
}
+ return folder
+ },
+
+ /**
+ * Get cached child nodes within a given path
+ *
+ * @param service - The service (files view)
+ * @param path - The path relative within the service
+ * @return Array of cached nodes within the path
+ */
+ getNodesByPath(service: string, path?: string): Node[] {
+ const folder = this.getDirectoryByPath(service, path)
+
// If we found a cache entry and the cache entry was already loaded (has children) then use it
return (folder?._children ?? [])
.map((source: string) => this.getNode(source))
@@ -135,19 +148,19 @@ export const useFilesStore = function(...args) {
// If we have multiple nodes with the same file ID, we need to update all of them
const nodes = this.getNodesById(node.fileid)
if (nodes.length > 1) {
- await Promise.all(nodes.map(fetchNode)).then(this.updateNodes)
+ await Promise.all(nodes.map(node => fetchNode(node.path))).then(this.updateNodes)
logger.debug(nodes.length + ' nodes updated in store', { fileid: node.fileid })
return
}
// If we have only one node with the file ID, we can update it directly
- if (node.source === nodes[0].source) {
+ if (nodes.length === 1 && node.source === nodes[0].source) {
this.updateNodes([node])
return
}
// Otherwise, it means we receive an event for a node that is not in the store
- fetchNode(node).then(n => this.updateNodes([n]))
+ fetchNode(node.path).then(n => this.updateNodes([n]))
},
// Handlers for legacy sidebar (no real nodes support)
diff --git a/apps/files/src/store/filters.ts b/apps/files/src/store/filters.ts
index bc57e25a1f8..fd16ec5dc84 100644
--- a/apps/files/src/store/filters.ts
+++ b/apps/files/src/store/filters.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FilterUpdateChipsEvent, IFileListFilter, IFileListFilterChip } from '@nextcloud/files'
-import { subscribe } from '@nextcloud/event-bus'
+import { emit, subscribe } from '@nextcloud/event-bus'
import { getFileListFilters } from '@nextcloud/files'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
@@ -20,7 +20,6 @@ function isFileListFilterWithUi(value: IFileListFilter): value is Required<IFile
export const useFiltersStore = defineStore('filters', () => {
const chips = ref<Record<string, IFileListFilterChip[]>>({})
const filters = ref<IFileListFilter[]>([])
- const filtersChanged = ref(false)
/**
* Currently active filter chips
@@ -40,7 +39,7 @@ export const useFiltersStore = defineStore('filters', () => {
* All filters that provide a UI for visual controlling the filter state
*/
const filtersWithUI = computed<Required<IFileListFilter>[]>(
- () => sortedFilters.value.filter(isFileListFilterWithUi)
+ () => sortedFilters.value.filter(isFileListFilterWithUi),
)
/**
@@ -77,7 +76,7 @@ export const useFiltersStore = defineStore('filters', () => {
* @private
*/
function onFilterUpdate() {
- filtersChanged.value = true
+ emit('files:filters:changed')
}
/**
@@ -122,7 +121,6 @@ export const useFiltersStore = defineStore('filters', () => {
chips,
filters,
filtersWithUI,
- filtersChanged,
// getters / computed
activeChips,
diff --git a/apps/files/src/store/renaming.ts b/apps/files/src/store/renaming.ts
index 2ec73837f82..fc61be3bd3b 100644
--- a/apps/files/src/store/renaming.ts
+++ b/apps/files/src/store/renaming.ts
@@ -3,184 +3,173 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
-import type { RenamingStore } from '../types'
import axios, { isAxiosError } from '@nextcloud/axios'
import { emit, subscribe } from '@nextcloud/event-bus'
-import { NodeStatus } from '@nextcloud/files'
-import { DialogBuilder } from '@nextcloud/dialogs'
+import { FileType, NodeStatus } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
+import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import { basename, dirname, extname } from 'path'
import { defineStore } from 'pinia'
import logger from '../logger'
-import Vue from 'vue'
-import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
-import IconCheck from '@mdi/svg/svg/check.svg?raw'
-
-let isDialogVisible = false
-
-const showWarningDialog = (oldExtension: string, newExtension: string): Promise<boolean> => {
- if (isDialogVisible) {
- return Promise.resolve(false)
- }
-
- isDialogVisible = true
-
- let message
-
- if (!oldExtension && newExtension) {
- message = t(
- 'files',
- 'Adding the file extension "{new}" may render the file unreadable.',
- { new: newExtension },
- )
- } else if (!newExtension) {
- message = t(
- 'files',
- 'Removing the file extension "{old}" may render the file unreadable.',
- { old: oldExtension },
- )
- } else {
- message = t(
- 'files',
- 'Changing the file extension from "{old}" to "{new}" may render the file unreadable.',
- { old: oldExtension, new: newExtension },
- )
- }
-
- return new Promise((resolve) => {
- const dialog = new DialogBuilder()
- .setName(t('files', 'Change file extension'))
- .setText(message)
- .setButtons([
- {
- label: t('files', 'Keep {oldextension}', { oldextension: oldExtension }),
- icon: IconCancel,
- type: 'secondary',
- callback: () => {
- isDialogVisible = false
- resolve(false)
- },
+import Vue, { defineAsyncComponent, ref } from 'vue'
+import { useUserConfigStore } from './userconfig'
+import { fetchNode } from '../services/WebdavClient'
+
+export const useRenamingStore = defineStore('renaming', () => {
+ /**
+ * The currently renamed node
+ */
+ const renamingNode = ref<Node>()
+ /**
+ * The new name of the currently renamed node
+ */
+ const newNodeName = ref('')
+
+ /**
+ * Internal flag to only allow calling `rename` once.
+ */
+ const isRenaming = ref(false)
+
+ /**
+ * Execute the renaming.
+ * This will rename the node set as `renamingNode` to the configured new name `newName`.
+ *
+ * @return true if success, false if skipped (e.g. new and old name are the same)
+ * @throws Error if renaming fails, details are set in the error message
+ */
+ async function rename(): Promise<boolean> {
+ if (renamingNode.value === undefined) {
+ throw new Error('No node is currently being renamed')
+ }
+
+ // Only rename once so we use this as some kind of mutex
+ if (isRenaming.value) {
+ return false
+ }
+ isRenaming.value = true
+
+ let node = renamingNode.value
+ Vue.set(node, 'status', NodeStatus.LOADING)
+
+ const userConfig = useUserConfigStore()
+
+ let newName = newNodeName.value.trim()
+ const oldName = node.basename
+ const oldExtension = extname(oldName)
+ const newExtension = extname(newName)
+ // Check for extension change for files
+ if (node.type === FileType.File
+ && oldExtension !== newExtension
+ && userConfig.userConfig.show_dialog_file_extension
+ && !(await showFileExtensionDialog(oldExtension, newExtension))
+ ) {
+ // user selected to use the old extension
+ newName = basename(newName, newExtension) + oldExtension
+ }
+
+ const oldEncodedSource = node.encodedSource
+ try {
+ if (oldName === newName) {
+ return false
+ }
+
+ // rename the node
+ node.rename(newName)
+ logger.debug('Moving file to', { destination: node.encodedSource, oldEncodedSource })
+ // create MOVE request
+ await axios({
+ method: 'MOVE',
+ url: oldEncodedSource,
+ headers: {
+ Destination: node.encodedSource,
+ Overwrite: 'F',
},
- {
- label: newExtension.length ? t('files', 'Use {newextension}', { newextension: newExtension }) : t('files', 'Remove extension'),
- icon: IconCheck,
- type: 'primary',
- callback: () => {
- isDialogVisible = false
- resolve(true)
- },
- },
- ])
- .build()
-
- dialog.show().then(() => {
- dialog.hide()
- })
- })
-}
-
-export const useRenamingStore = function(...args) {
- const store = defineStore('renaming', {
- state: () => ({
- renamingNode: undefined,
- newName: '',
- } as RenamingStore),
-
- actions: {
- /**
- * Execute the renaming.
- * This will rename the node set as `renamingNode` to the configured new name `newName`.
- * @return true if success, false if skipped (e.g. new and old name are the same)
- * @throws Error if renaming fails, details are set in the error message
- */
- async rename(): Promise<boolean> {
- if (this.renamingNode === undefined) {
- throw new Error('No node is currently being renamed')
- }
-
- const newName = this.newName.trim?.() || ''
- const oldName = this.renamingNode.basename
- const oldEncodedSource = this.renamingNode.encodedSource
-
- // Check for extension change
- const oldExtension = extname(oldName)
- const newExtension = extname(newName)
- if (oldExtension !== newExtension) {
- const proceed = await showWarningDialog(oldExtension, newExtension)
- if (!proceed) {
- return false
- }
+ })
+
+ // Update mime type if extension changed
+ // as other related informations might have changed
+ // on the backend but it is really hard to know on the front
+ if (oldExtension !== newExtension) {
+ node = await fetchNode(node.path)
+ }
+
+ // Success 🎉
+ emit('files:node:updated', node)
+ emit('files:node:renamed', node)
+ emit('files:node:moved', {
+ node,
+ oldSource: `${dirname(node.source)}/${oldName}`,
+ })
+
+ // Reset the state not changed
+ if (renamingNode.value === node) {
+ $reset()
+ }
+
+ return true
+ } catch (error) {
+ logger.error('Error while renaming file', { error })
+ // Rename back as it failed
+ node.rename(oldName)
+ if (isAxiosError(error)) {
+ // TODO: 409 means current folder does not exist, redirect ?
+ if (error?.response?.status === 404) {
+ throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
+ } else if (error?.response?.status === 412) {
+ throw new Error(t(
+ 'files',
+ 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
+ {
+ newName,
+ dir: basename(renamingNode.value!.dirname),
+ },
+ ))
}
+ }
+ // Unknown error
+ throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
+ } finally {
+ Vue.set(node, 'status', undefined)
+ isRenaming.value = false
+ }
+ }
- if (oldName === newName) {
- return false
- }
+ /**
+ * Reset the store state
+ */
+ function $reset(): void {
+ newNodeName.value = ''
+ renamingNode.value = undefined
+ }
- const node = this.renamingNode
- Vue.set(node, 'status', NodeStatus.LOADING)
-
- try {
- // rename the node
- this.renamingNode.rename(newName)
- logger.debug('Moving file to', { destination: this.renamingNode.encodedSource, oldEncodedSource })
- // create MOVE request
- await axios({
- method: 'MOVE',
- url: oldEncodedSource,
- headers: {
- Destination: this.renamingNode.encodedSource,
- Overwrite: 'F',
- },
- })
-
- // Success 🎉
- emit('files:node:updated', this.renamingNode as Node)
- emit('files:node:renamed', this.renamingNode as Node)
- emit('files:node:moved', {
- node: this.renamingNode as Node,
- oldSource: `${dirname(this.renamingNode.source)}/${oldName}`,
- })
- this.$reset()
- return true
- } catch (error) {
- logger.error('Error while renaming file', { error })
- // Rename back as it failed
- this.renamingNode.rename(oldName)
- if (isAxiosError(error)) {
- // TODO: 409 means current folder does not exist, redirect ?
- if (error?.response?.status === 404) {
- throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
- } else if (error?.response?.status === 412) {
- throw new Error(t(
- 'files',
- 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
- {
- newName,
- dir: basename(this.renamingNode.dirname),
- },
- ))
- }
- }
- // Unknown error
- throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
- } finally {
- Vue.set(node, 'status', undefined)
- }
- },
- },
+ // Make sure we only register the listeners once
+ subscribe('files:node:rename', (node: Node) => {
+ renamingNode.value = node
+ newNodeName.value = node.basename
})
- const renamingStore = store(...args)
+ return {
+ $reset,
- // Make sure we only register the listeners once
- if (!renamingStore._initialized) {
- subscribe('files:node:rename', function(node: Node) {
- renamingStore.renamingNode = node
- renamingStore.newName = node.basename
- })
- renamingStore._initialized = true
+ newNodeName,
+ rename,
+ renamingNode,
}
+})
- return renamingStore
+/**
+ * Show a dialog asking user for confirmation about changing the file extension.
+ *
+ * @param oldExtension the old file name extension
+ * @param newExtension the new file name extension
+ */
+async function showFileExtensionDialog(oldExtension: string, newExtension: string): Promise<boolean> {
+ const { promise, resolve } = Promise.withResolvers<boolean>()
+ spawnDialog(
+ defineAsyncComponent(() => import('../views/DialogConfirmFileExtension.vue')),
+ { oldExtension, newExtension },
+ (useNewExtension: unknown) => resolve(Boolean(useNewExtension)),
+ )
+ return await promise
}
diff --git a/apps/files/src/store/search.ts b/apps/files/src/store/search.ts
new file mode 100644
index 00000000000..43e01f35b92
--- /dev/null
+++ b/apps/files/src/store/search.ts
@@ -0,0 +1,153 @@
+/*!
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { View } from '@nextcloud/files'
+import type RouterService from '../services/RouterService.ts'
+import type { SearchScope } from '../types.ts'
+
+import { emit, subscribe } from '@nextcloud/event-bus'
+import debounce from 'debounce'
+import { defineStore } from 'pinia'
+import { ref, watch } from 'vue'
+import { VIEW_ID } from '../views/search.ts'
+import logger from '../logger.ts'
+
+export const useSearchStore = defineStore('search', () => {
+ /**
+ * The current search query
+ */
+ const query = ref('')
+
+ /**
+ * Scope of the search.
+ * Scopes:
+ * - filter: only filter current file list
+ * - globally: search everywhere
+ */
+ const scope = ref<SearchScope>('filter')
+
+ // reset the base if query is cleared
+ watch(scope, updateSearch)
+
+ watch(query, (old, current) => {
+ // skip if only whitespaces changed
+ if (old.trim() === current.trim()) {
+ return
+ }
+
+ updateSearch()
+ })
+
+ // initialize the search store
+ initialize()
+
+ /**
+ * Debounced update of the current route
+ * @private
+ */
+ const updateRouter = debounce((isSearch: boolean) => {
+ const router = window.OCP.Files.Router as RouterService
+ router.goToRoute(
+ undefined,
+ {
+ view: VIEW_ID,
+ },
+ {
+ query: query.value,
+ },
+ isSearch,
+ )
+ })
+
+ /**
+ * Handle updating the filter if needed.
+ * Also update the search view by updating the current route if needed.
+ *
+ * @private
+ */
+ function updateSearch() {
+ // emit the search event to update the filter
+ emit('files:search:updated', { query: query.value, scope: scope.value })
+ const router = window.OCP.Files.Router as RouterService
+
+ // if we are on the search view and the query was unset or scope was set to 'filter' we need to move back to the files view
+ if (router.params.view === VIEW_ID && (query.value === '' || scope.value === 'filter')) {
+ scope.value = 'filter'
+ return router.goToRoute(
+ undefined,
+ {
+ view: 'files',
+ },
+ {
+ ...router.query,
+ query: undefined,
+ },
+ )
+ }
+
+ // for the filter scope we do not need to adjust the current route anymore
+ // also if the query is empty we do not need to do anything
+ if (scope.value === 'filter' || !query.value) {
+ return
+ }
+
+ const isSearch = router.params.view === VIEW_ID
+
+ logger.debug('Update route for updated search query', { query: query.value, isSearch })
+ updateRouter(isSearch)
+ }
+
+ /**
+ * Event handler that resets the store if the file list view was changed.
+ *
+ * @param view - The new view that is active
+ * @private
+ */
+ function onViewChanged(view: View) {
+ if (view.id !== VIEW_ID) {
+ query.value = ''
+ scope.value = 'filter'
+ }
+ }
+
+ /**
+ * Initialize the store from the router if needed
+ */
+ function initialize() {
+ subscribe('files:navigation:changed', onViewChanged)
+
+ const router = window.OCP.Files.Router as RouterService
+ // if we initially load the search view (e.g. hard page refresh)
+ // then we need to initialize the store from the router
+ if (router.params.view === VIEW_ID) {
+ query.value = [router.query.query].flat()[0] ?? ''
+
+ if (query.value) {
+ scope.value = 'globally'
+ logger.debug('Directly navigated to search view', { query: query.value })
+ } else {
+ // we do not have any query so we need to move to the files list
+ logger.info('Directly navigated to search view without any query, redirect to files view.')
+ router.goToRoute(
+ undefined,
+ {
+ ...router.params,
+ view: 'files',
+ },
+ {
+ ...router.query,
+ query: undefined,
+ },
+ true,
+ )
+ }
+ }
+ }
+
+ return {
+ query,
+ scope,
+ }
+})
diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts
index ffe07a91bab..48fe01d5134 100644
--- a/apps/files/src/store/userconfig.ts
+++ b/apps/files/src/store/userconfig.ts
@@ -2,61 +2,61 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { UserConfig, UserConfigStore } from '../types'
-import { defineStore } from 'pinia'
+import type { UserConfig } from '../types'
+import { getCurrentUser } from '@nextcloud/auth'
import { emit, subscribe } from '@nextcloud/event-bus'
-import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+import { defineStore } from 'pinia'
+import { ref, set } from 'vue'
import axios from '@nextcloud/axios'
-import Vue from 'vue'
-const userConfig = loadState<UserConfig>('files', 'config', {
- show_hidden: false,
+const initialUserConfig = loadState<UserConfig>('files', 'config', {
crop_image_previews: true,
+ default_view: 'files',
+ grid_view: false,
+ show_files_extensions: true,
+ show_hidden: false,
+ show_mime_column: true,
sort_favorites_first: true,
sort_folders_first: true,
- grid_view: false,
+
+ show_dialog_deletion: false,
+ show_dialog_file_extension: true,
})
-export const useUserConfigStore = function(...args) {
- const store = defineStore('userconfig', {
- state: () => ({
- userConfig,
- } as UserConfigStore),
+export const useUserConfigStore = defineStore('userconfig', () => {
+ const userConfig = ref<UserConfig>({ ...initialUserConfig })
- actions: {
- /**
- * Update the user config local store
- * @param key
- * @param value
- */
- onUpdate(key: string, value: boolean) {
- Vue.set(this.userConfig, key, value)
- },
+ /**
+ * Update the user config local store
+ * @param key The config key
+ * @param value The new value
+ */
+ function onUpdate(key: string, value: boolean): void {
+ set(userConfig.value, key, value)
+ }
- /**
- * Update the user config local store AND on server side
- * @param key
- * @param value
- */
- async update(key: string, value: boolean) {
- await axios.put(generateUrl('/apps/files/api/v1/config/' + key), {
- value,
- })
- emit('files:config:updated', { key, value })
- },
- },
- })
+ /**
+ * Update the user config local store AND on server side
+ * @param key The config key
+ * @param value The new value
+ */
+ async function update(key: string, value: boolean): Promise<void> {
+ // only update if a user is logged in (not the case for public shares)
+ if (getCurrentUser() !== null) {
+ await axios.put(generateUrl('/apps/files/api/v1/config/{key}', { key }), {
+ value,
+ })
+ }
+ emit('files:config:updated', { key, value })
+ }
- const userConfigStore = store(...args)
+ // Register the event listener
+ subscribe('files:config:updated', ({ key, value }) => onUpdate(key, value))
- // Make sure we only register the listeners once
- if (!userConfigStore._initialized) {
- subscribe('files:config:updated', function({ key, value }: { key: string, value: boolean }) {
- userConfigStore.onUpdate(key, value)
- })
- userConfigStore._initialized = true
+ return {
+ userConfig,
+ update,
}
-
- return userConfigStore
-}
+})
diff --git a/apps/files/src/store/viewConfig.ts b/apps/files/src/store/viewConfig.ts
index 76786306d60..a902cedb6fa 100644
--- a/apps/files/src/store/viewConfig.ts
+++ b/apps/files/src/store/viewConfig.ts
@@ -2,95 +2,95 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { defineStore } from 'pinia'
+import type { ViewConfigs, ViewId, ViewConfig } from '../types'
+
+import { getCurrentUser } from '@nextcloud/auth'
import { emit, subscribe } from '@nextcloud/event-bus'
-import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+import { defineStore } from 'pinia'
+import { ref, set } from 'vue'
import axios from '@nextcloud/axios'
-import Vue from 'vue'
-import type { ViewConfigs, ViewConfigStore, ViewId, ViewConfig } from '../types'
+const initialViewConfig = loadState('files', 'viewConfigs', {}) as ViewConfigs
-const viewConfig = loadState('files', 'viewConfigs', {}) as ViewConfigs
+export const useViewConfigStore = defineStore('viewconfig', () => {
-export const useViewConfigStore = function(...args) {
- const store = defineStore('viewconfig', {
- state: () => ({
- viewConfig,
- } as ViewConfigStore),
+ const viewConfigs = ref({ ...initialViewConfig })
- getters: {
- getConfig: (state) => (view: ViewId): ViewConfig => state.viewConfig[view] || {},
+ /**
+ * Get the config for a specific view
+ * @param viewid Id of the view to fet the config for
+ */
+ function getConfig(viewid: ViewId): ViewConfig {
+ return viewConfigs.value[viewid] || {}
+ }
- getConfigs: (state) => (): ViewConfigs => state.viewConfig,
- },
+ /**
+ * Update the view config local store
+ * @param viewId The id of the view to update
+ * @param key The config key to update
+ * @param value The new value
+ */
+ function onUpdate(viewId: ViewId, key: string, value: string | number | boolean): void {
+ if (!(viewId in viewConfigs.value)) {
+ set(viewConfigs.value, viewId, {})
+ }
+ set(viewConfigs.value[viewId], key, value)
+ }
- actions: {
- /**
- * Update the view config local store
- * @param view
- * @param key
- * @param value
- */
- onUpdate(view: ViewId, key: string, value: string | number | 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
+ * @param view Id of the view to update
+ * @param key Config key to update
+ * @param value New value
+ */
+ async function update(view: ViewId, key: string, value: string | number | boolean): Promise<void> {
+ if (getCurrentUser() !== null) {
+ await axios.put(generateUrl('/apps/files/api/v1/views'), {
+ value,
+ view,
+ key,
+ })
+ }
- /**
- * Update the view config local store AND on server side
- * @param view
- * @param key
- * @param value
- */
- async update(view: ViewId, key: string, value: string | number | boolean) {
- axios.put(generateUrl('/apps/files/api/v1/views'), {
- value,
- view,
- key,
- })
+ emit('files:view-config:updated', { 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
+ * @param key Key to sort by
+ * @param view View to set the sorting key for
+ */
+ function setSortingBy(key = 'basename', view = 'files'): void {
+ // Save new config
+ update(view, 'sorting_mode', key)
+ update(view, 'sorting_direction', 'asc')
+ }
- /**
- * 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
- * @param key Key to sort by
- * @param view View to set the sorting key for
- */
- setSortingBy(key = 'basename', view = 'files') {
- // Save new config
- this.update(view, 'sorting_mode', key)
- this.update(view, 'sorting_direction', 'asc')
- },
+ /**
+ * Toggle the sorting direction
+ * @param viewId id of the view to set the sorting order for
+ */
+ function toggleSortingDirection(viewId = 'files'): void {
+ const config = viewConfigs.value[viewId] || { sorting_direction: 'asc' }
+ const newDirection = config.sorting_direction === 'asc' ? 'desc' : 'asc'
- /**
- * Toggle the sorting direction
- * @param view view to set the sorting order for
- */
- toggleSortingDirection(view = 'files') {
- const config = this.getConfig(view) || { sorting_direction: 'asc' }
- const newDirection = config.sorting_direction === 'asc' ? 'desc' : 'asc'
+ // Save new config
+ update(viewId, 'sorting_direction', newDirection)
+ }
- // Save new config
- this.update(view, 'sorting_direction', newDirection)
- },
- },
- })
+ // Initialize event listener
+ subscribe('files:view-config:updated', ({ view, key, value }) => onUpdate(view, key, value))
- const viewConfigStore = store(...args)
+ return {
+ viewConfigs,
- // 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
+ getConfig,
+ setSortingBy,
+ toggleSortingDirection,
+ update,
}
-
- return viewConfigStore
-}
+})