aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src')
-rw-r--r--apps/files/src/actions/deleteAction.ts7
-rw-r--r--apps/files/src/actions/sidebarAction.ts54
-rw-r--r--apps/files/src/components/FileEntry.vue112
-rw-r--r--apps/files/src/components/FilesListHeader.vue2
-rw-r--r--apps/files/src/components/FilesListHeaderActions.vue17
-rw-r--r--apps/files/src/components/FilesListVirtual.vue17
-rw-r--r--apps/files/src/main.ts (renamed from apps/files/src/main.js)32
-rw-r--r--apps/files/src/mixins/filesSorting.ts20
-rw-r--r--apps/files/src/router/router.js4
-rw-r--r--apps/files/src/services/FileAction.ts10
-rw-r--r--apps/files/src/services/Navigation.ts7
-rw-r--r--apps/files/src/services/RouterService.ts71
-rw-r--r--apps/files/src/store/files.ts12
-rw-r--r--apps/files/src/store/keyboard.ts4
-rw-r--r--apps/files/src/store/paths.ts22
-rw-r--r--apps/files/src/store/userconfig.ts4
-rw-r--r--apps/files/src/store/viewConfig.ts12
-rw-r--r--apps/files/src/types.ts10
-rw-r--r--apps/files/src/utils/hashUtils.ts28
-rw-r--r--apps/files/src/views/FilesList.vue10
-rw-r--r--apps/files/src/views/Navigation.vue3
-rw-r--r--apps/files/src/views/Sidebar.vue38
22 files changed, 384 insertions, 112 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts
index 087884b3362..a633e477b1f 100644
--- a/apps/files/src/actions/deleteAction.ts
+++ b/apps/files/src/actions/deleteAction.ts
@@ -27,10 +27,11 @@ import TrashCan from '@mdi/svg/svg/trash-can.svg?raw'
import { registerFileAction, FileAction } from '../services/FileAction.ts'
import logger from '../logger.js'
+import type { Navigation } from '../services/Navigation.ts'
registerFileAction(new FileAction({
id: 'delete',
- displayName(nodes: Node[], view) {
+ displayName(nodes: Node[], view: Navigation) {
return view.id === 'trashbin'
? t('files_trashbin', 'Delete permanently')
: t('files', 'Delete')
@@ -57,8 +58,8 @@ registerFileAction(new FileAction({
return false
}
},
- async execBatch(nodes: Node[], view) {
- return Promise.all(nodes.map(node => this.exec(node, view)))
+ async execBatch(nodes: Node[], view: Navigation, dir: string) {
+ return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
order: 100,
diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts
new file mode 100644
index 00000000000..f56d3a9475f
--- /dev/null
+++ b/apps/files/src/actions/sidebarAction.ts
@@ -0,0 +1,54 @@
+/**
+ * @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 { translate as t } from '@nextcloud/l10n'
+import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw'
+import type { Node } from '@nextcloud/files'
+
+import { registerFileAction, FileAction } from '../services/FileAction.ts'
+import logger from '../logger.js'
+
+export const ACTION_DETAILS = 'details'
+
+registerFileAction(new FileAction({
+ id: ACTION_DETAILS,
+ displayName: () => t('files', 'Details'),
+ iconSvgInline: () => InformationSvg,
+
+ // Sidebar currently supports user folder only, /files/USER
+ enabled: (files: Node[]) => !!window?.OCA?.Files?.Sidebar
+ && files.some(node => node.root?.startsWith('/files/')),
+
+ async exec(node: Node) {
+ try {
+ // TODO: migrate Sidebar to use a Node instead
+ window?.OCA?.Files?.Sidebar?.open?.(node.path)
+
+ return null
+ } catch (error) {
+ logger.error('Error while opening sidebar', { error })
+ return false
+ }
+ },
+
+ default: true,
+ order: -50,
+}))
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index 7db22482220..8dc067a407d 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -33,7 +33,7 @@
<!-- Link to file -->
<td class="files-list__row-name">
- <a ref="name" v-bind="linkTo">
+ <a ref="name" v-bind="linkTo" @click="execDefaultAction">
<!-- Icon or preview -->
<span class="files-list__row-icon">
<FolderIcon v-if="source.type === 'folder'" />
@@ -49,6 +49,13 @@
:style="{ backgroundImage: mimeIconUrl }" />
<FileIcon v-else />
+
+ <!-- Favorite icon -->
+ <span v-if="isFavorite"
+ class="files-list__row-icon-favorite"
+ :aria-label="t('files', 'Favorite')">
+ <StarIcon aria-hidden="true" :size="20" />
+ </span>
</span>
<!-- File name -->
@@ -64,8 +71,11 @@
<!-- Menu actions -->
<NcActions v-if="active"
ref="actionsMenu"
+ :boundaries-element="boundariesElement"
+ :container="boundariesElement"
:disabled="source._loading"
:force-title="true"
+ :force-menu="true"
:inline="enabledInlineActions.length"
:open.sync="openedMenu">
<NcActionButton v-for="action in enabledMenuActions"
@@ -84,7 +94,8 @@
<!-- Size -->
<td v-if="isSizeAvailable"
:style="{ opacity: sizeOpacity }"
- class="files-list__row-size">
+ class="files-list__row-size"
+ @click="openDetailsIfAvailable">
<span>{{ size }}</span>
</td>
@@ -92,7 +103,8 @@
<td v-for="column in columns"
:key="column.id"
:class="`files-list__row-${currentView?.id}-${column.id}`"
- class="files-list__row-column-custom">
+ class="files-list__row-column-custom"
+ @click="openDetailsIfAvailable">
<CustomElementRender v-if="active"
:current-view="currentView"
:render="column.render"
@@ -115,9 +127,12 @@ import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+import StarIcon from 'vue-material-design-icons/Star.vue'
import Vue from 'vue'
+import { ACTION_DETAILS } from '../actions/sidebarAction.ts'
import { getFileActions } from '../services/FileAction.ts'
+import { hashCode } from '../utils/hashUtils.ts'
import { isCachedPreview } from '../services/PreviewService.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useFilesStore } from '../store/files.ts'
@@ -144,6 +159,7 @@ export default Vue.extend({
NcActions,
NcCheckboxRadioSwitch,
NcLoadingIcon,
+ StarIcon,
},
props: {
@@ -192,6 +208,7 @@ export default Vue.extend({
return {
backgroundFailed: false,
backgroundImage: '',
+ boundariesElement: document.querySelector('.app-content > .files-list'),
loading: '',
}
},
@@ -204,7 +221,6 @@ export default Vue.extend({
currentView() {
return this.$navigation.active
},
-
columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512) {
@@ -217,7 +233,6 @@ export default Vue.extend({
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
},
-
fileid() {
return this.source?.fileid?.toString?.()
},
@@ -225,6 +240,7 @@ export default Vue.extend({
return this.source.attributes.displayName
|| this.source.basename
},
+
size() {
const size = parseInt(this.source.size, 10) || 0
if (typeof size !== 'number' || size < 0) {
@@ -232,7 +248,6 @@ export default Vue.extend({
}
return formatFileSize(size, true)
},
-
sizeOpacity() {
const size = parseInt(this.source.size, 10) || 0
if (!size || size < 0) {
@@ -255,6 +270,16 @@ export default Vue.extend({
to,
}
}
+
+ if (this.enabledDefaultActions.length > 0) {
+ const action = this.enabledDefaultActions[0]
+ const displayName = action.displayName([this.source], this.currentView)
+ return {
+ title: displayName,
+ role: 'button',
+ }
+ }
+
return {
href: this.source.source,
// TODO: Use first action title ?
@@ -272,7 +297,6 @@ export default Vue.extend({
cropPreviews() {
return this.userConfig.crop_image_previews
},
-
previewUrl() {
try {
const url = new URL(window.location.origin + this.source.attributes.previewUrl)
@@ -280,13 +304,12 @@ export default Vue.extend({
url.searchParams.set('x', '32')
url.searchParams.set('y', '32')
// Handle cropping
- url.searchParams.set('a', this.cropPreviews === true ? '1' : '0')
+ url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
return url.href
} catch (e) {
return null
}
},
-
mimeIconUrl() {
const mimeType = this.source.mime || 'application/octet-stream'
const mimeIconUrl = window.OC?.MimeType?.getIconUrl?.(mimeType)
@@ -301,29 +324,38 @@ export default Vue.extend({
.filter(action => !action.enabled || action.enabled([this.source], this.currentView))
.sort((a, b) => (a.order || 0) - (b.order || 0))
},
-
enabledInlineActions() {
if (this.filesListWidth < 768) {
return []
}
return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
},
-
enabledMenuActions() {
if (this.filesListWidth < 768) {
+ // If we have a default action, do not render the first one
+ if (this.enabledDefaultActions.length > 0) {
+ return this.enabledActions.slice(1)
+ }
return this.enabledActions
}
- return [
+ const actions = [
...this.enabledInlineActions,
...this.enabledActions.filter(action => !action.inline),
]
- },
- uniqueId() {
- return this.hashCode(this.source.source)
- },
+ // If we have a default action, do not render the first one
+ if (this.enabledDefaultActions.length > 0) {
+ return actions.slice(1)
+ }
+ return actions
+ },
+ enabledDefaultActions() {
+ return [
+ ...this.enabledActions.filter(action => action.default),
+ ]
+ },
openedMenu: {
get() {
return this.actionsMenuStore.opened === this.uniqueId
@@ -332,6 +364,14 @@ export default Vue.extend({
this.actionsMenuStore.opened = opened ? this.uniqueId : null
},
},
+
+ uniqueId() {
+ return hashCode(this.source.source)
+ },
+
+ isFavorite() {
+ return this.source.attributes.favorite === 1
+ },
},
watch: {
@@ -457,16 +497,6 @@ export default Vue.extend({
}
},
- hashCode(str) {
- let hash = 0
- for (let i = 0, len = str.length; i < len; i++) {
- const chr = str.charCodeAt(i)
- hash = (hash << 5) - hash + chr
- hash |= 0 // Convert to 32bit integer
- }
- return hash
- },
-
async onActionClick(action) {
const displayName = action.displayName([this.source], this.currentView)
try {
@@ -474,7 +504,13 @@ export default Vue.extend({
this.loading = action.id
Vue.set(this.source, '_loading', true)
- const success = await action.exec(this.source, this.currentView)
+ const success = await action.exec(this.source, this.currentView, this.dir)
+
+ // If the action returns null, we stay silent
+ if (success === null) {
+ return
+ }
+
if (success) {
showSuccess(this.t('files', '"{displayName}" action executed successfully', { displayName }))
return
@@ -489,6 +525,28 @@ export default Vue.extend({
Vue.set(this.source, '_loading', false)
}
},
+ execDefaultAction(event) {
+ // Do not execute the default action on the folder, navigate instead
+ if (this.source.type === 'folder') {
+ return
+ }
+
+ if (this.enabledDefaultActions.length > 0) {
+ event.preventDefault()
+ event.stopPropagation()
+ // Execute the first default action if any
+ this.enabledDefaultActions[0].exec(this.source, this.currentView, this.dir)
+ }
+ },
+
+ openDetailsIfAvailable(event) {
+ const detailsAction = this.enabledDefaultActions.find(action => action.id === ACTION_DETAILS)
+ if (detailsAction) {
+ event.preventDefault()
+ event.stopPropagation()
+ detailsAction.exec(this.source, this.currentView)
+ }
+ },
onSelectionChange(selection) {
const newSelectedIndex = this.index
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue
index 9e3fe0d46de..2d848d0eefe 100644
--- a/apps/files/src/components/FilesListHeader.vue
+++ b/apps/files/src/components/FilesListHeader.vue
@@ -173,7 +173,7 @@ export default Vue.extend({
onToggleAll(selected) {
if (selected) {
- const selection = this.nodes.map(node => node.attributes.fileid.toString())
+ const selection = this.nodes.map(node => node.fileid.toString())
logger.debug('Added all nodes to selection', { selection })
this.selectionStore.setLastIndex(null)
this.selectionStore.set(selection)
diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue
index c9f0c66be03..f8c60a5cd1b 100644
--- a/apps/files/src/components/FilesListHeaderActions.vue
+++ b/apps/files/src/components/FilesListHeaderActions.vue
@@ -103,6 +103,10 @@ export default Vue.extend({
},
computed: {
+ dir() {
+ // Remove any trailing slash but leave root slash
+ return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
+ },
enabledActions() {
return actions
.filter(action => action.execBatch)
@@ -165,13 +169,20 @@ export default Vue.extend({
})
// Dispatch action execution
- const results = await action.execBatch(this.nodes, this.currentView)
+ const results = await action.execBatch(this.nodes, this.currentView, this.dir)
+
+ // Check if all actions returned null
+ if (!results.some(result => result !== null)) {
+ // If the actions returned null, we stay silent
+ this.selectionStore.reset()
+ return
+ }
// Handle potential failures
- if (results.some(result => result !== true)) {
+ if (results.some(result => result === false)) {
// Remove the failed ids from the selection
const failedIds = selectionIds
- .filter((fileid, index) => results[index] !== true)
+ .filter((fileid, index) => results[index] === false)
this.selectionStore.set(failedIds)
showError(this.t('files', '"{displayName}" failed on some elements ', { displayName }))
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index ad0ba2069ff..866fc6da00d 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -139,7 +139,7 @@ export default Vue.extend({
methods: {
getFileId(node) {
- return node.attributes.fileid
+ return node.fileid
},
t: translate,
@@ -233,22 +233,24 @@ export default Vue.extend({
}
.files-list__row-icon {
+ position: relative;
display: flex;
+ overflow: visible;
align-items: center;
+ // No shrinking or growing allowed
+ flex: 0 0 var(--icon-preview-size);
justify-content: center;
width: var(--icon-preview-size);
height: 100%;
// Show same padding as the checkbox right padding for visual balance
margin-right: var(--checkbox-padding);
color: var(--color-primary-element);
- // No shrinking or growing allowed
- flex: 0 0 var(--icon-preview-size);
& > span {
justify-content: flex-start;
}
- svg {
+ &> span:not(.files-list__row-icon-favorite) svg {
width: var(--icon-preview-size);
height: var(--icon-preview-size);
}
@@ -263,6 +265,13 @@ export default Vue.extend({
background-position: center;
background-size: contain;
}
+
+ &-favorite {
+ position: absolute;
+ top: 4px;
+ right: -8px;
+ color: #ffcc00;
+ }
}
.files-list__row-name {
diff --git a/apps/files/src/main.js b/apps/files/src/main.ts
index a8464f0ee0d..195357d0e0a 100644
--- a/apps/files/src/main.js
+++ b/apps/files/src/main.ts
@@ -1,27 +1,37 @@
import './templates.js'
import './legacy/filelistSearch.js'
-import './actions/deleteAction.ts'
-
-import processLegacyFilesViews from './legacy/navigationMapper.js'
+import './actions/deleteAction'
+import './actions/sidebarAction'
import Vue from 'vue'
import { createPinia, PiniaVuePlugin } from 'pinia'
-import NavigationService from './services/Navigation.ts'
-import registerPreviewServiceWorker from './services/ServiceWorker.js'
-
-import NavigationView from './views/Navigation.vue'
import FilesListView from './views/FilesList.vue'
-
-import SettingsService from './services/Settings.js'
+import NavigationService from './services/Navigation'
+import NavigationView from './views/Navigation.vue'
+import processLegacyFilesViews from './legacy/navigationMapper.js'
+import registerPreviewServiceWorker from './services/ServiceWorker.js'
+import router from './router/router.js'
+import RouterService from './services/RouterService'
import SettingsModel from './models/Setting.js'
+import SettingsService from './services/Settings.js'
-import router from './router/router.js'
+declare global {
+ interface Window {
+ OC: any;
+ OCA: any;
+ OCP: any;
+ }
+}
// Init private and public Files namespace
window.OCA.Files = window.OCA.Files ?? {}
window.OCP.Files = window.OCP.Files ?? {}
+// Expose router
+const Router = new RouterService(router)
+Object.assign(window.OCP.Files, { Router })
+
// Init Pinia store
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
@@ -57,7 +67,7 @@ const FilesList = new ListView({
})
FilesList.$mount('#app-content-vue')
-// Init legacy files views
+// Init legacy and new files views
processLegacyFilesViews()
// Register preview service worker
diff --git a/apps/files/src/mixins/filesSorting.ts b/apps/files/src/mixins/filesSorting.ts
index 8930587ffab..2f79a3eb171 100644
--- a/apps/files/src/mixins/filesSorting.ts
+++ b/apps/files/src/mixins/filesSorting.ts
@@ -21,18 +21,14 @@
*/
import Vue from 'vue'
+import { mapState } from 'pinia'
import { useViewConfigStore } from '../store/viewConfig'
-import type { Navigation } from '../services/Navigation'
+import type { Navigation } from '../services/Navigation'
export default Vue.extend({
- setup() {
- const viewConfigStore = useViewConfigStore()
- return {
- viewConfigStore,
- }
- },
-
computed: {
+ ...mapState(useViewConfigStore, ['getConfig', 'setSortingBy', 'toggleSortingDirection']),
+
currentView(): Navigation {
return this.$navigation.active
},
@@ -41,7 +37,7 @@ export default Vue.extend({
* Get the sorting mode for the current view
*/
sortingMode(): string {
- return this.viewConfigStore.getConfig(this.currentView.id)?.sorting_mode
+ return this.getConfig(this.currentView.id)?.sorting_mode as string
|| this.currentView?.defaultSortKey
|| 'basename'
},
@@ -50,7 +46,7 @@ export default Vue.extend({
* Get the sorting direction for the current view
*/
isAscSorting(): boolean {
- const sortingDirection = this.viewConfigStore.getConfig(this.currentView.id)?.sorting_direction
+ const sortingDirection = this.getConfig(this.currentView.id)?.sorting_direction
return sortingDirection === 'asc'
},
},
@@ -59,11 +55,11 @@ export default Vue.extend({
toggleSortBy(key: string) {
// If we're already sorting by this key, flip the direction
if (this.sortingMode === key) {
- this.viewConfigStore.toggleSortingDirection(this.currentView.id)
+ this.toggleSortingDirection(this.currentView.id)
return
}
// else sort ASC by this new key
- this.viewConfigStore.setSortingBy(key, this.currentView.id)
+ this.setSortingBy(key, this.currentView.id)
},
},
})
diff --git a/apps/files/src/router/router.js b/apps/files/src/router/router.js
index cf5e5ec5ea8..0d833cd6464 100644
--- a/apps/files/src/router/router.js
+++ b/apps/files/src/router/router.js
@@ -22,7 +22,7 @@
import Vue from 'vue'
import Router from 'vue-router'
import { generateUrl } from '@nextcloud/router'
-import { stringify } from 'query-string'
+import queryString from 'query-string'
Vue.use(Router)
@@ -49,7 +49,7 @@ const router = new Router({
// Custom stringifyQuery to prevent encoding of slashes in the url
stringifyQuery(query) {
- const result = stringify(query).replace(/%2F/gmi, '/')
+ const result = queryString.stringify(query).replace(/%2F/gmi, '/')
return result ? ('?' + result) : ''
},
})
diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts
index 8c1d325e645..70d6405c804 100644
--- a/apps/files/src/services/FileAction.ts
+++ b/apps/files/src/services/FileAction.ts
@@ -20,8 +20,9 @@
*
*/
-import { Node } from '@nextcloud/files'
+import type { Node } from '@nextcloud/files'
import logger from '../logger'
+import type { Navigation } from './Navigation'
declare global {
interface Window {
@@ -48,13 +49,14 @@ interface FileActionData {
* @returns true if the action was executed, false otherwise
* @throws Error if the action failed
*/
- exec: (file: Node, view) => Promise<boolean>,
+ exec: (file: Node, view: Navigation, dir: string) => Promise<boolean|null>,
/**
* Function executed on multiple files action
- * @returns true if the action was executed, false otherwise
+ * @returns true if the action was executed successfully,
+ * false otherwise and null if the action is silent/undefined.
* @throws Error if the action failed
*/
- execBatch?: (files: Node[], view) => Promise<boolean[]>
+ execBatch?: (files: Node[], view: Navigation, dir: string) => Promise<(boolean|null)[]>
/** This action order in the list */
order?: number,
/** Make this action the default */
diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts
index e86266013d7..b2ae3b0b973 100644
--- a/apps/files/src/services/Navigation.ts
+++ b/apps/files/src/services/Navigation.ts
@@ -126,6 +126,13 @@ export default class {
this._views.push(view)
}
+ remove(id: string) {
+ const index = this._views.findIndex(view => view.id === id)
+ if (index !== -1) {
+ this._views.splice(index, 1)
+ }
+ }
+
get views(): Navigation[] {
return this._views
}
diff --git a/apps/files/src/services/RouterService.ts b/apps/files/src/services/RouterService.ts
new file mode 100644
index 00000000000..978e009514e
--- /dev/null
+++ b/apps/files/src/services/RouterService.ts
@@ -0,0 +1,71 @@
+/**
+ * @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 type { Route } from 'vue-router';
+import type VueRouter from 'vue-router';
+import type { Dictionary } from 'vue-router/types/router';
+import type { Location } from 'vue-router/types/router';
+
+export default class RouterService {
+
+ private _router: VueRouter;
+
+ constructor(router: VueRouter) {
+ this._router = router
+ }
+
+ /**
+ * Trigger a route change on the files app
+ *
+ * @param path the url path, eg: '/trashbin?dir=/Deleted'
+ * @param replace replace the current history
+ * @see https://router.vuejs.org/guide/essentials/navigation.html#navigate-to-a-different-location
+ */
+ goTo(path: string, replace: boolean = false): Promise<Route> {
+ return this._router.push({
+ path,
+ replace,
+ })
+ }
+
+ /**
+ * Trigger a route change on the files App
+ *
+ * @param name the route name
+ * @param params the route parameters
+ * @param query the url query parameters
+ * @param replace replace the current history
+ * @see https://router.vuejs.org/guide/essentials/navigation.html#navigate-to-a-different-location
+ */
+ goToRoute(
+ name?: string,
+ params?: Dictionary<string>,
+ query?: Dictionary<string | (string | null)[] | null | undefined>,
+ replace?: boolean,
+ ): Promise<Route> {
+ return this._router.push({
+ name,
+ query,
+ params,
+ replace,
+ } as Location)
+ }
+}
diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts
index 11e4fc970a4..bd7d3202dd9 100644
--- a/apps/files/src/store/files.ts
+++ b/apps/files/src/store/files.ts
@@ -25,11 +25,11 @@ import type { FilesStore, RootsStore, RootOptions, Service, FilesState } from '.
import { defineStore } from 'pinia'
import { subscribe } from '@nextcloud/event-bus'
-import Vue from 'vue'
import logger from '../logger'
-import { FileId } from '../types'
+import type { FileId } from '../types'
+import Vue from 'vue'
-export const useFilesStore = () => {
+export const useFilesStore = function() {
const store = defineStore('files', {
state: (): FilesState => ({
files: {} as FilesStore,
@@ -59,11 +59,11 @@ export const useFilesStore = () => {
updateNodes(nodes: Node[]) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
- if (!node.attributes.fileid) {
+ if (!node.fileid) {
logger.warn('Trying to update/set a node without fileid', node)
return acc
}
- acc[node.attributes.fileid] = node
+ acc[node.fileid] = node
return acc
}, {} as FilesStore)
@@ -88,7 +88,7 @@ export const useFilesStore = () => {
}
})
- const fileStore = store()
+ const fileStore = store(...arguments)
// Make sure we only register the listeners once
if (!fileStore._initialized) {
// subscribe('files:node:created', fileStore.onCreatedNode)
diff --git a/apps/files/src/store/keyboard.ts b/apps/files/src/store/keyboard.ts
index 1ba8285b960..bdce7d55075 100644
--- a/apps/files/src/store/keyboard.ts
+++ b/apps/files/src/store/keyboard.ts
@@ -28,7 +28,7 @@ import Vue from 'vue'
* special keys states. Useful for checking the
* current status of a key when executing a method.
*/
-export const useKeyboardStore = () => {
+export const useKeyboardStore = function() {
const store = defineStore('keyboard', {
state: () => ({
altKey: false,
@@ -50,7 +50,7 @@ export const useKeyboardStore = () => {
}
})
- const keyboardStore = store()
+ const keyboardStore = store(...arguments)
// Make sure we only register the listeners once
if (!keyboardStore._initialized) {
window.addEventListener('keydown', keyboardStore.onEvent)
diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts
index bcd7375518c..ecff97bf00c 100644
--- a/apps/files/src/store/paths.ts
+++ b/apps/files/src/store/paths.ts
@@ -23,21 +23,23 @@
import type { PathOptions, ServicesState } from '../types.ts'
import { defineStore } from 'pinia'
-import Vue from 'vue'
import { subscribe } from '@nextcloud/event-bus'
-import { FileId } from '../types'
+import type { FileId, PathsStore } from '../types'
+import Vue from 'vue'
-export const usePathsStore = () => {
+export const usePathsStore = function() {
const store = defineStore('paths', {
- state: (): ServicesState => ({}),
+ state: () => ({
+ paths: {} as ServicesState
+ } as PathsStore),
getters: {
getPath: (state) => {
return (service: string, path: string): FileId|undefined => {
- if (!state[service]) {
+ if (!state.paths[service]) {
return undefined
}
- return state[service][path]
+ return state.paths[service][path]
}
},
},
@@ -45,17 +47,17 @@ export const usePathsStore = () => {
actions: {
addPath(payload: PathOptions) {
// If it doesn't exists, init the service state
- if (!this[payload.service]) {
- Vue.set(this, payload.service, {})
+ if (!this.paths[payload.service]) {
+ Vue.set(this.paths, payload.service, {})
}
// Now we can set the provided path
- Vue.set(this[payload.service], payload.path, payload.fileid)
+ Vue.set(this.paths[payload.service], payload.path, payload.fileid)
},
}
})
- const pathsStore = store()
+ const pathsStore = store(...arguments)
// Make sure we only register the listeners once
if (!pathsStore._initialized) {
// TODO: watch folders to update paths?
diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts
index c81b7b4d77f..42821951dbf 100644
--- a/apps/files/src/store/userconfig.ts
+++ b/apps/files/src/store/userconfig.ts
@@ -33,7 +33,7 @@ const userConfig = loadState('files', 'config', {
crop_image_previews: true,
}) as UserConfig
-export const useUserConfigStore = () => {
+export const useUserConfigStore = function() {
const store = defineStore('userconfig', {
state: () => ({
userConfig,
@@ -60,7 +60,7 @@ export const useUserConfigStore = () => {
}
})
- const userConfigStore = store()
+ const userConfigStore = store(...arguments)
// Make sure we only register the listeners once
if (!userConfigStore._initialized) {
diff --git a/apps/files/src/store/viewConfig.ts b/apps/files/src/store/viewConfig.ts
index d7a5ab1daa6..607596dfd68 100644
--- a/apps/files/src/store/viewConfig.ts
+++ b/apps/files/src/store/viewConfig.ts
@@ -27,12 +27,12 @@ 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'
+import type { ViewConfigs, ViewConfigStore, ViewId } from '../types'
+import type { ViewConfig } from '../types'
const viewConfig = loadState('files', 'viewConfigs', {}) as ViewConfigs
-export const useViewConfigStore = () => {
+export const useViewConfigStore = function() {
const store = defineStore('viewconfig', {
state: () => ({
viewConfig,
@@ -46,7 +46,7 @@ export const useViewConfigStore = () => {
/**
* Update the view config local store
*/
- onUpdate(view: ViewId, key: string, value: boolean) {
+ onUpdate(view: ViewId, key: string, value: string | number | boolean) {
if (!this.viewConfig[view]) {
Vue.set(this.viewConfig, view, {})
}
@@ -56,7 +56,7 @@ export const useViewConfigStore = () => {
/**
* Update the view config local store AND on server side
*/
- async update(view: ViewId, key: string, value: boolean) {
+ async update(view: ViewId, key: string, value: string | number | boolean) {
axios.put(generateUrl(`/apps/files/api/v1/views/${view}/${key}`), {
value,
})
@@ -88,7 +88,7 @@ export const useViewConfigStore = () => {
}
})
- const viewConfigStore = store()
+ const viewConfigStore = store(...arguments)
// Make sure we only register the listeners once
if (!viewConfigStore._initialized) {
diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts
index cca6fb9111f..c04a9538827 100644
--- a/apps/files/src/types.ts
+++ b/apps/files/src/types.ts
@@ -49,13 +49,17 @@ export interface RootOptions {
// Paths store
export type ServicesState = {
- [service: Service]: PathsStore
+ [service: Service]: PathConfig
}
-export type PathsStore = {
+export type PathConfig = {
[path: string]: number
}
+export type PathsStore = {
+ paths: ServicesState
+}
+
export interface PathOptions {
service: Service
path: string
@@ -91,4 +95,4 @@ export interface ViewConfigs {
}
export interface ViewConfigStore {
viewConfig: ViewConfigs
-} \ No newline at end of file
+}
diff --git a/apps/files/src/utils/hashUtils.ts b/apps/files/src/utils/hashUtils.ts
new file mode 100644
index 00000000000..55cf8b9f51a
--- /dev/null
+++ b/apps/files/src/utils/hashUtils.ts
@@ -0,0 +1,28 @@
+/**
+ * @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/>.
+ *
+ */
+
+export const hashCode = function(str: string): number {
+ return str.split('').reduce(function(a, b) {
+ a = ((a << 5) - a) + b.charCodeAt(0)
+ return a & a
+ }, 0)
+}
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index c11b5820308..f2a20c18f28 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -166,7 +166,7 @@ export default Vue.extend({
return []
}
- const customColumn = this.currentView.columns
+ const customColumn = (this.currentView?.columns || [])
.find(column => column.id === this.sortingMode)
// Custom column must provide their own sorting methods
@@ -268,16 +268,16 @@ export default Vue.extend({
this.filesStore.updateNodes(contents)
// Define current directory children
- folder._children = contents.map(node => node.attributes.fileid)
+ folder._children = contents.map(node => node.fileid)
// If we're in the root dir, define the root
if (dir === '/') {
this.filesStore.setRoot({ service: currentView.id, root: folder })
} else
// Otherwise, add the folder to the store
- if (folder.attributes.fileid) {
+ if (folder.fileid) {
this.filesStore.updateNodes([folder])
- this.pathsStore.addPath({ service: currentView.id, fileid: folder.attributes.fileid, path: dir })
+ this.pathsStore.addPath({ service: currentView.id, fileid: folder.fileid, path: dir })
} else {
// If we're here, the view API messed up
logger.error('Invalid root folder returned', { dir, folder, currentView })
@@ -286,7 +286,7 @@ export default Vue.extend({
// Update paths store
const folders = contents.filter(node => node.type === 'folder')
folders.forEach(node => {
- this.pathsStore.addPath({ service: currentView.id, fileid: node.attributes.fileid, path: join(dir, node.basename) })
+ this.pathsStore.addPath({ service: currentView.id, fileid: node.fileid, path: join(dir, node.basename) })
})
} catch (error) {
logger.error('Error while fetching content', { error })
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue
index cc714964c9b..e164880003a 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -44,7 +44,7 @@
:title="child.name"
:to="generateToNavigation(child)">
<!-- Sanitized icon as svg if provided -->
- <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />
+ <NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" />
</NcAppNavigationItem>
</NcAppNavigationItem>
</template>
@@ -175,7 +175,6 @@ export default {
this.Navigation.setActive(view)
logger.debug('Navigation changed', { id: view.id, view })
- // debugger
this.showView(view, oldView)
},
},
diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue
index 5c3967b1c93..9b43570e345 100644
--- a/apps/files/src/views/Sidebar.vue
+++ b/apps/files/src/views/Sidebar.vue
@@ -36,10 +36,16 @@
@closed="handleClosed">
<!-- TODO: create a standard to allow multiple elements here? -->
<template v-if="fileInfo" #description>
- <LegacyView v-for="view in views"
- :key="view.cid"
- :component="view"
- :file-info="fileInfo" />
+ <div class="sidebar__description">
+ <SystemTags v-if="isSystemTagsEnabled"
+ v-show="showTags"
+ :file-id="fileInfo.id"
+ @has-tags="value => showTags = value" />
+ <LegacyView v-for="view in views"
+ :key="view.cid"
+ :component="view"
+ :file-info="fileInfo" />
+ </div>
</template>
<!-- Actions menu -->
@@ -96,22 +102,25 @@ import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import FileInfo from '../services/FileInfo.js'
import SidebarTab from '../components/SidebarTab.vue'
import LegacyView from '../components/LegacyView.vue'
+import SystemTags from '../../../systemtags/src/components/SystemTags.vue'
export default {
name: 'Sidebar',
components: {
+ LegacyView,
NcActionButton,
NcAppSidebar,
NcEmptyContent,
- LegacyView,
SidebarTab,
+ SystemTags,
},
data() {
return {
// reactive state
Sidebar: OCA.Files.Sidebar.state,
+ showTags: false,
error: null,
loading: true,
fileInfo: null,
@@ -410,9 +419,7 @@ export default {
* Toggle the tags selector
*/
toggleTags() {
- if (OCA.SystemTags && OCA.SystemTags.View) {
- OCA.SystemTags.View.toggle()
- }
+ this.showTags = !this.showTags
},
/**
@@ -505,7 +512,7 @@ export default {
</script>
<style lang="scss" scoped>
.app-sidebar {
- &--has-preview::v-deep {
+ &--has-preview:deep {
.app-sidebar-header__figure {
background-size: cover;
}
@@ -525,6 +532,12 @@ export default {
height: 100% !important;
}
+ :deep {
+ .app-sidebar-header__description {
+ margin: 0 16px 4px 16px !important;
+ }
+ }
+
.svg-icon {
::v-deep svg {
width: 20px;
@@ -533,4 +546,11 @@ export default {
}
}
}
+
+.sidebar__description {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 8px 0;
+}
</style>