diff options
author | Marco <marcoambrosini@pm.me> | 2024-08-01 08:35:08 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-01 08:35:08 +0200 |
commit | 7221019151d8b6774a605c227c1e8e6c135c32c0 (patch) | |
tree | d9b8cdeac8acb88d369d4ef44474828b810dec15 /apps | |
parent | 9855485b0dbe292315068ca90b3193fcbdbb5df7 (diff) | |
parent | 75f80cbdcbfee506d3e3741d98b1decfbb4da3c1 (diff) | |
download | nextcloud-server-7221019151d8b6774a605c227c1e8e6c135c32c0.tar.gz nextcloud-server-7221019151d8b6774a605c227c1e8e6c135c32c0.zip |
Merge branch 'master' into feat/add-small-font-size-variable
Diffstat (limited to 'apps')
30 files changed, 214 insertions, 181 deletions
diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 93d342ed391..579040d8d60 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -722,7 +722,7 @@ }); } - if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) { + if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent) && !!window.oc_current_user) { this.registerAction({ name: 'EditLocally', displayName: function(context) { diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 53b5690d4dd..63335db194e 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -11,7 +11,7 @@ import CloseSvg from '@mdi/svg/svg/close.svg?raw' import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw' import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw' -import logger from '../logger.js' +import logger from '../logger.ts' import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, isTrashbinEnabled } from './deleteUtils' const queue = new PQueue({ concurrency: 5 }) diff --git a/apps/files/src/actions/downloadAction.spec.ts b/apps/files/src/actions/downloadAction.spec.ts index 8aca20eb4d4..59e533441be 100644 --- a/apps/files/src/actions/downloadAction.spec.ts +++ b/apps/files/src/actions/downloadAction.spec.ts @@ -4,7 +4,7 @@ */ import { action } from './downloadAction' import { expect } from '@jest/globals' -import { File, Folder, Permission, View, FileAction } from '@nextcloud/files' +import { File, Folder, Permission, View, FileAction, DefaultType } from '@nextcloud/files' const view = { id: 'files', @@ -23,7 +23,7 @@ describe('Download action conditions tests', () => { expect(action.id).toBe('download') expect(action.displayName([], view)).toBe('Download') expect(action.iconSvgInline([], view)).toBe('<svg>SvgMock</svg>') - expect(action.default).toBeUndefined() + expect(action.default).toBe(DefaultType.DEFAULT) expect(action.order).toBe(30) }) }) diff --git a/apps/files/src/actions/downloadAction.ts b/apps/files/src/actions/downloadAction.ts index 3ba4e23f0cf..cce1e48579a 100644 --- a/apps/files/src/actions/downloadAction.ts +++ b/apps/files/src/actions/downloadAction.ts @@ -4,9 +4,9 @@ */ import type { ShareAttribute } from '../../../files_sharing/src/sharing' -import { FileAction, Permission, Node, FileType, View } from '@nextcloud/files' +import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' -import { translate as t } from '@nextcloud/l10n' import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw' @@ -46,6 +46,8 @@ const isDownloadable = function(node: Node) { export const action = new FileAction({ id: 'download', + default: DefaultType.DEFAULT, + displayName: () => t('files', 'Download'), iconSvgInline: () => ArrowDownSvg, diff --git a/apps/files/src/actions/favoriteAction.ts b/apps/files/src/actions/favoriteAction.ts index 3815fc1a9a3..bb7d82ba2cd 100644 --- a/apps/files/src/actions/favoriteAction.ts +++ b/apps/files/src/actions/favoriteAction.ts @@ -12,7 +12,7 @@ import Vue from 'vue' import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw' import StarSvg from '@mdi/svg/svg/star.svg?raw' -import logger from '../logger.js' +import logger from '../logger.ts' import { encodePath } from '@nextcloud/paths' // If any of the nodes is not favorited, we display the favorite action. diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts index 21b5dfc2878..f00088f8d0c 100644 --- a/apps/files/src/actions/sidebarAction.ts +++ b/apps/files/src/actions/sidebarAction.ts @@ -6,7 +6,7 @@ import { Permission, type Node, View, FileAction } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw' -import logger from '../logger.js' +import logger from '../logger.ts' export const ACTION_DETAILS = 'details' diff --git a/apps/files/src/components/DragAndDropNotice.vue b/apps/files/src/components/DragAndDropNotice.vue index 0483e71f43b..cc0dc9104b2 100644 --- a/apps/files/src/components/DragAndDropNotice.vue +++ b/apps/files/src/components/DragAndDropNotice.vue @@ -37,7 +37,7 @@ import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue' import { useNavigation } from '../composables/useNavigation' import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService' -import logger from '../logger.js' +import logger from '../logger.ts' export default defineComponent({ name: 'DragAndDropNotice', diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue index 3df4289b1a0..2c3444a19fd 100644 --- a/apps/files/src/components/FileEntry/FileEntryActions.vue +++ b/apps/files/src/components/FileEntry/FileEntryActions.vue @@ -79,10 +79,10 @@ import type { PropType, ShallowRef } from 'vue' import type { FileAction, Node, View } from '@nextcloud/files' -import { DefaultType, NodeStatus, getFileActions } from '@nextcloud/files' +import { DefaultType, NodeStatus } from '@nextcloud/files' import { showError, showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' -import { defineComponent } from 'vue' +import { defineComponent, inject } from 'vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' @@ -93,10 +93,7 @@ import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue' import CustomElementRender from '../CustomElementRender.vue' import { useNavigation } from '../../composables/useNavigation' -import logger from '../../logger.js' - -// The registered actions list -const actions = getFileActions() +import logger from '../../logger.ts' export default defineComponent({ name: 'FileEntryActions', @@ -136,10 +133,12 @@ export default defineComponent({ setup() { const { currentView } = useNavigation() + const enabledFileActions = inject<FileAction[]>('enabledFileActions', []) return { // The file list is guaranteed to be only shown with active view currentView: currentView as ShallowRef<View>, + enabledFileActions, } }, @@ -158,23 +157,12 @@ export default defineComponent({ return this.source.status === NodeStatus.LOADING }, - // Sorted actions that are enabled for this node - enabledActions() { - if (this.source.status === NodeStatus.FAILED) { - return [] - } - - return actions - .filter(action => !action.enabled || action.enabled([this.source], this.currentView)) - .sort((a, b) => (a.order || 0) - (b.order || 0)) - }, - // Enabled action that are displayed inline enabledInlineActions() { if (this.filesListWidth < 768 || this.gridMode) { return [] } - return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView)) + return this.enabledFileActions.filter(action => action?.inline?.(this.source, this.currentView)) }, // Enabled action that are displayed inline with a custom render function @@ -182,12 +170,7 @@ export default defineComponent({ if (this.gridMode) { return [] } - return this.enabledActions.filter(action => typeof action.renderInline === 'function') - }, - - // Default actions - enabledDefaultActions() { - return this.enabledActions.filter(action => !!action?.default) + return this.enabledFileActions.filter(action => typeof action.renderInline === 'function') }, // Actions shown in the menu @@ -202,7 +185,7 @@ export default defineComponent({ // Showing inline first for the NcActions inline prop ...this.enabledInlineActions, // Then the rest - ...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'), + ...this.enabledFileActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'), ].filter((value, index, self) => { // Then we filter duplicates to prevent inline actions to be shown twice return index === self.findIndex(action => action.id === value.id) @@ -216,7 +199,7 @@ export default defineComponent({ }, enabledSubmenuActions() { - return this.enabledActions + return this.enabledFileActions .filter(action => action.parent) .reduce((arr, action) => { if (!arr[action.parent!]) { @@ -305,14 +288,6 @@ export default defineComponent({ } } }, - execDefaultAction(event) { - 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.currentDir) - } - }, isMenu(id: string) { return this.enabledSubmenuActions[id]?.length > 0 diff --git a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue index 987b48ef8ae..059e2f89f5f 100644 --- a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue +++ b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue @@ -14,17 +14,20 @@ </template> <script lang="ts"> -import { Node, FileType } from '@nextcloud/files' +import type { Node } from '@nextcloud/files' +import type { PropType } from 'vue' +import type { FileSource } from '../../types.ts' + +import { FileType } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import { type PropType, defineComponent } from 'vue' +import { defineComponent } from 'vue' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import { useKeyboardStore } from '../../store/keyboard.ts' import { useSelectionStore } from '../../store/selection.ts' -import logger from '../../logger.js' -import type { FileSource } from '../../types.ts' +import logger from '../../logger.ts' export default defineComponent({ name: 'FileEntryCheckbox', diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue index 7a6ad2a1051..1d45f7de17e 100644 --- a/apps/files/src/components/FileEntry/FileEntryName.vue +++ b/apps/files/src/components/FileEntry/FileEntryName.vue @@ -37,22 +37,23 @@ </template> <script lang="ts"> -import type { Node } from '@nextcloud/files' +import type { FileAction, Node } from '@nextcloud/files' import type { PropType } from 'vue' import axios, { isAxiosError } from '@nextcloud/axios' import { showError, showSuccess } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' -import { FileType, NodeStatus, Permission } from '@nextcloud/files' +import { FileType, NodeStatus } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import { defineComponent } from 'vue' +import { defineComponent, inject } from 'vue' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import { useNavigation } from '../../composables/useNavigation' +import { useRouteParameters } from '../../composables/useRouteParameters.ts' import { useRenamingStore } from '../../store/renaming.ts' import { getFilenameValidity } from '../../utils/filenameValidity.ts' -import logger from '../../logger.js' +import logger from '../../logger.ts' export default defineComponent({ name: 'FileEntryName', @@ -96,10 +97,15 @@ export default defineComponent({ setup() { const { currentView } = useNavigation() + const { directory } = useRouteParameters() const renamingStore = useRenamingStore() + const defaultFileAction = inject<FileAction | undefined>('defaultFileAction') + return { currentView, + defaultFileAction, + directory, renamingStore, } @@ -139,32 +145,20 @@ export default defineComponent({ } } - const enabledDefaultActions = this.$parent?.$refs?.actions?.enabledDefaultActions - if (enabledDefaultActions?.length > 0) { - const action = enabledDefaultActions[0] - const displayName = action.displayName([this.source], this.currentView) + if (this.defaultFileAction && this.currentView) { + const displayName = this.defaultFileAction.displayName([this.source], this.currentView) return { - is: 'a', + is: 'button', params: { + 'aria-label': displayName, title: displayName, - role: 'button', - tabindex: '0', - }, - } - } - - if (this.source?.permissions & Permission.READ) { - return { - is: 'a', - params: { - download: this.source.basename, - href: this.source.source, - title: t('files', 'Download file {name}', { name: `${this.basename}${this.extension}` }), tabindex: '0', }, } } + // nothing interactive here, there is no default action + // so if not even the download action works we only can show the list entry return { is: 'span', } @@ -280,12 +274,15 @@ export default defineComponent({ // Reset the renaming store this.stopRenaming() this.$nextTick(() => { - this.$refs.basename?.focus() + const nameContainter = this.$refs.basename as HTMLElement | undefined + nameContainter?.focus() }) } catch (error) { logger.error('Error while renaming file', { error }) + // Rename back as it failed this.source.rename(oldName) - this.$refs.renameInput?.focus() + // And ensure we reset to the renaming state + this.startRenaming() if (isAxiosError(error)) { // TODO: 409 means current folder does not exist, redirect ? @@ -293,7 +290,7 @@ export default defineComponent({ showError(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName })) return } else if (error?.response?.status === 412) { - showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir })) + showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.directory })) return } } @@ -309,3 +306,16 @@ export default defineComponent({ }, }) </script> + +<style scoped lang="scss"> +button.files-list__row-name-link { + background-color: unset; + border: none; + font-weight: normal; + + &:active { + // No active styles - handled by the row entry + background-color: unset !important; + } +} +</style> diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts index d9117053dd8..8681e9c3cb2 100644 --- a/apps/files/src/components/FileEntryMixin.ts +++ b/apps/files/src/components/FileEntryMixin.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { ComponentPublicInstance, PropType } from 'vue' +import type { PropType } from 'vue' import type { FileSource } from '../types.ts' import { showError } from '@nextcloud/dialogs' -import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node } from '@nextcloud/files' +import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, getFileActions } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' import { vOnClickOutside } from '@vueuse/components' @@ -18,11 +18,12 @@ import { action as sidebarAction } from '../actions/sidebarAction.ts' import { getDragAndDropPreview } from '../utils/dragUtils.ts' import { hashCode } from '../utils/hashUtils.ts' import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts' -import logger from '../logger.js' -import FileEntryActions from '../components/FileEntry/FileEntryActions.vue' +import logger from '../logger.ts' Vue.directive('onClickOutside', vOnClickOutside) +const actions = getFileActions() + export default defineComponent({ props: { source: { @@ -47,6 +48,13 @@ export default defineComponent({ }, }, + provide() { + return { + defaultFileAction: this.defaultFileAction, + enabledFileActions: this.enabledFileActions, + } + }, + data() { return { loading: '', @@ -178,6 +186,23 @@ export default defineComponent({ color: `color-mix(in srgb, var(--color-main-text) ${ratio}%, var(--color-text-maxcontrast))`, } }, + + /** + * Sorted actions that are enabled for this node + */ + enabledFileActions() { + if (this.source.status === NodeStatus.FAILED) { + return [] + } + + return actions + .filter(action => !action.enabled || action.enabled([this.source], this.currentView)) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }, + + defaultFileAction() { + return this.enabledFileActions.find((action) => action.default !== undefined) + }, }, watch: { @@ -261,8 +286,15 @@ export default defineComponent({ return false } - const actions = this.$refs.actions as ComponentPublicInstance<typeof FileEntryActions> - actions.execDefaultAction(event) + if (this.defaultFileAction) { + event.preventDefault() + event.stopPropagation() + // Execute the first default action if any + this.defaultFileAction.exec(this.source, this.currentView, this.currentDir) + } else { + // fallback to open in current tab + window.open(generateUrl('/f/{fileId}', { fileId: this.fileid }), '_self') + } }, openDetailsIfAvailable(event) { diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue index 723dd574da8..e91cfa055c1 100644 --- a/apps/files/src/components/FilesListTableHeader.vue +++ b/apps/files/src/components/FilesListTableHeader.vue @@ -68,7 +68,7 @@ import { useNavigation } from '../composables/useNavigation' import { useFilesStore } from '../store/files.ts' import { useSelectionStore } from '../store/selection.ts' import filesSortingMixin from '../mixins/filesSorting.ts' -import logger from '../logger.js' +import logger from '../logger.ts' export default defineComponent({ name: 'FilesListTableHeader', diff --git a/apps/files/src/components/FilesListTableHeaderActions.vue b/apps/files/src/components/FilesListTableHeaderActions.vue index 2916ce55e67..5e32a07eae1 100644 --- a/apps/files/src/components/FilesListTableHeaderActions.vue +++ b/apps/files/src/components/FilesListTableHeaderActions.vue @@ -26,21 +26,25 @@ </template> <script lang="ts"> -import { Node, NodeStatus, View, getFileActions } from '@nextcloud/files' +import type { Node, View } from '@nextcloud/files' +import type { PropType } from 'vue' +import type { FileSource } from '../types' + +import { NodeStatus, getFileActions } from '@nextcloud/files' import { showError, showSuccess } from '@nextcloud/dialogs' import { translate } from '@nextcloud/l10n' +import { defineComponent } from 'vue' + import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import Vue, { defineComponent, type PropType } from 'vue' import { useActionsMenuStore } from '../store/actionsmenu.ts' import { useFilesStore } from '../store/files.ts' import { useSelectionStore } from '../store/selection.ts' import filesListWidthMixin from '../mixins/filesListWidth.ts' -import logger from '../logger.js' -import type { FileSource } from '../types' +import logger from '../logger.ts' // The registered actions list const actions = getFileActions() @@ -136,11 +140,10 @@ export default defineComponent({ /** * Get a cached note from the store * - * @param {number} fileId the file id to get - * @return {Folder|File} + * @param source The source of the node to get */ - getNode(fileId) { - return this.filesStore.getNode(fileId) + getNode(source: string): Node|undefined { + return this.filesStore.getNode(source) }, async onActionClick(action) { @@ -150,7 +153,7 @@ export default defineComponent({ // Set loading markers this.loading = action.id this.nodes.forEach(node => { - Vue.set(node, 'status', NodeStatus.LOADING) + this.$set(node, 'status', NodeStatus.LOADING) }) // Dispatch action execution @@ -190,7 +193,7 @@ export default defineComponent({ // Remove loading markers this.loading = null this.nodes.forEach(node => { - Vue.set(node, 'status', undefined) + this.$set(node, 'status', undefined) }) } }, diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 1b9475333ab..4c9cea00f9c 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -81,7 +81,7 @@ import FilesListTableFooter from './FilesListTableFooter.vue' import FilesListTableHeader from './FilesListTableHeader.vue' import filesListWidthMixin from '../mixins/filesListWidth.ts' import VirtualList from './VirtualList.vue' -import logger from '../logger.js' +import logger from '../logger.ts' import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue' import FileListFilters from './FileListFilters.vue' @@ -600,24 +600,26 @@ export default defineComponent({ // Take as much space as possible flex: 1 1 auto; - a { + button.files-list__row-name-link { display: flex; align-items: center; + text-align: start; // Fill cell height and width width: 100%; height: 100%; // Necessary for flex grow to work min-width: 0; + margin: 0; // Already added to the inner text, see rule below &:focus-visible { - outline: none; + outline: none !important; } // Keyboard indicator a11y &:focus .files-list__row-name-text { - outline: 2px solid var(--color-main-text) !important; - border-radius: 20px; + outline: var(--border-width-input-focused) solid var(--color-main-text) !important; + border-radius: var(--border-radius-element); } &:focus:not(:focus-visible) .files-list__row-name-text { outline: none !important; @@ -627,7 +629,7 @@ export default defineComponent({ .files-list__row-name-text { color: var(--color-main-text); // Make some space for the outline - padding: 5px 10px; + padding: var(--default-grid-baseline) calc(2 * var(--default-grid-baseline)); margin-left: -10px; // Align two name and ext display: inline-flex; @@ -791,10 +793,6 @@ tbody.files-list__tbody.files-list__tbody--grid { height: var(--icon-preview-size); } - a.files-list__row-name-link { - height: var(--name-height); - } - .files-list__row-name-text { margin: 0; // Ensure that the outline is not too close to the text. diff --git a/apps/files/src/components/NavigationQuota.vue b/apps/files/src/components/NavigationQuota.vue index 0619f6bc3fd..0e5ef510342 100644 --- a/apps/files/src/components/NavigationQuota.vue +++ b/apps/files/src/components/NavigationQuota.vue @@ -37,7 +37,7 @@ import ChartPie from 'vue-material-design-icons/ChartPie.vue' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' import NcProgressBar from '@nextcloud/vue/dist/Components/NcProgressBar.js' -import logger from '../logger.js' +import logger from '../logger.ts' export default { name: 'NavigationQuota', diff --git a/apps/files/src/components/TransferOwnershipDialogue.vue b/apps/files/src/components/TransferOwnershipDialogue.vue index 346bc3bbeb8..f13e6655354 100644 --- a/apps/files/src/components/TransferOwnershipDialogue.vue +++ b/apps/files/src/components/TransferOwnershipDialogue.vue @@ -52,7 +52,7 @@ import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' import Vue from 'vue' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import logger from '../logger.js' +import logger from '../logger.ts' const picker = getFilePickerBuilder(t('files', 'Choose a file or folder to transfer')) .setMultiSelect(false) diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue index 6206f86cda5..859aba6c834 100644 --- a/apps/files/src/components/VirtualList.vue +++ b/apps/files/src/components/VirtualList.vue @@ -59,7 +59,7 @@ import debounce from 'debounce' import Vue from 'vue' import filesListWidthMixin from '../mixins/filesListWidth.ts' -import logger from '../logger.js' +import logger from '../logger.ts' interface RecycledPoolItem { key: string, diff --git a/apps/files/src/logger.js b/apps/files/src/logger.ts index 33f87b424e0..33f87b424e0 100644 --- a/apps/files/src/logger.js +++ b/apps/files/src/logger.ts diff --git a/apps/files/src/newMenu/newTemplatesFolder.ts b/apps/files/src/newMenu/newTemplatesFolder.ts index e2c27ce067f..43a6f08b525 100644 --- a/apps/files/src/newMenu/newTemplatesFolder.ts +++ b/apps/files/src/newMenu/newTemplatesFolder.ts @@ -15,7 +15,7 @@ import { newNodeName } from '../utils/newNodeDialog' import PlusSvg from '@mdi/svg/svg/plus.svg?raw' import axios from '@nextcloud/axios' -import logger from '../logger.js' +import logger from '../logger.ts' let templatesPath = loadState<string|false>('files', 'templates_path', false) logger.debug('Initial templates folder', { templatesPath }) @@ -53,9 +53,9 @@ const initTemplatesFolder = async function(directory: Folder, name: string) { export const entry = { id: 'template-picker', - displayName: t('files', 'Create new templates folder'), + displayName: t('files', 'Create templates folder'), iconSvgInline: PlusSvg, - order: 10, + order: 30, enabled(context: Folder): boolean { // Templates folder already initialized if (templatesPath) { diff --git a/apps/files/src/services/DropService.ts b/apps/files/src/services/DropService.ts index b947c612949..63e7c213f4f 100644 --- a/apps/files/src/services/DropService.ts +++ b/apps/files/src/services/DropService.ts @@ -17,7 +17,7 @@ import Vue from 'vue' import { Directory, traverseTree, resolveConflict, createDirectoryIfNotExists } from './DropServiceUtils' import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction' import { MoveCopyAction } from '../actions/moveOrCopyActionUtils' -import logger from '../logger.js' +import logger from '../logger.ts' /** * This function converts a list of DataTransferItems to a file tree. diff --git a/apps/files/src/services/DropServiceUtils.ts b/apps/files/src/services/DropServiceUtils.ts index 27478dd956a..f10a09cfe27 100644 --- a/apps/files/src/services/DropServiceUtils.ts +++ b/apps/files/src/services/DropServiceUtils.ts @@ -10,7 +10,7 @@ import { openConflictPicker } from '@nextcloud/upload' import { showError, showInfo } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' -import logger from '../logger.js' +import logger from '../logger.ts' /** * This represents a Directory in the file tree diff --git a/apps/files/src/services/Files.ts b/apps/files/src/services/Files.ts index 10e553592fe..86d9f9b8e80 100644 --- a/apps/files/src/services/Files.ts +++ b/apps/files/src/services/Files.ts @@ -8,7 +8,7 @@ import type { FileStat, ResponseDataDetailed } from 'webdav' import { CancelablePromise } from 'cancelable-promise' import { File, Folder, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' import { client } from './WebdavClient.ts' -import logger from '../logger.js' +import logger from '../logger.ts' /** * Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map` diff --git a/apps/files/src/services/ServiceWorker.js b/apps/files/src/services/ServiceWorker.js index 477354d1c36..7d03af65e44 100644 --- a/apps/files/src/services/ServiceWorker.js +++ b/apps/files/src/services/ServiceWorker.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { generateUrl } from '@nextcloud/router' -import logger from '../logger.js' +import logger from '../logger.ts' export default () => { if ('serviceWorker' in navigator) { diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index ca868b5d526..d486b6f1bae 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -74,32 +74,37 @@ :name="t('files', 'Loading current folder')" /> <!-- Empty content placeholder --> - <NcEmptyContent v-else-if="!loading && isEmptyDir" - :name="currentView?.emptyTitle || t('files', 'No files in here')" - :description="currentView?.emptyCaption || t('files', 'Upload some content or sync with your devices!')" - data-cy-files-content-empty> - <template v-if="directory !== '/'" #action> - <!-- Uploader --> - <UploadPicker v-if="currentFolder && canUpload && !isQuotaExceeded" - allow-folders - class="files-list__header-upload-button" - :content="getContent" - :destination="currentFolder" - :forbidden-characters="forbiddenCharacters" - multiple - @failed="onUploadFail" - @uploaded="onUpload" /> - <NcButton v-else - :aria-label="t('files', 'Go to the previous folder')" - :to="toPreviousDir" - type="primary"> - {{ t('files', 'Go back') }} - </NcButton> - </template> - <template #icon> - <NcIconSvgWrapper :svg="currentView.icon" /> - </template> - </NcEmptyContent> + <template v-else-if="!loading && isEmptyDir"> + <div v-if="currentView?.emptyView" class="files-list__empty-view-wrapper"> + <div ref="customEmptyView" /> + </div> + <NcEmptyContent v-else + :name="currentView?.emptyTitle || t('files', 'No files in here')" + :description="currentView?.emptyCaption || t('files', 'Upload some content or sync with your devices!')" + data-cy-files-content-empty> + <template v-if="directory !== '/'" #action> + <!-- Uploader --> + <UploadPicker v-if="currentFolder && canUpload && !isQuotaExceeded" + allow-folders + class="files-list__header-upload-button" + :content="getContent" + :destination="currentFolder" + :forbidden-characters="forbiddenCharacters" + multiple + @failed="onUploadFail" + @uploaded="onUpload" /> + <NcButton v-else + :aria-label="t('files', 'Go to the previous folder')" + :to="toPreviousDir" + type="primary"> + {{ t('files', 'Go back') }} + </NcButton> + </template> + <template #icon> + <NcIconSvgWrapper :svg="currentView.icon" /> + </template> + </NcEmptyContent> + </template> <!-- File list --> <FilesListVirtual v-else @@ -154,7 +159,7 @@ import BreadCrumbs from '../components/BreadCrumbs.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' import filesListWidthMixin from '../mixins/filesListWidth.ts' import filesSortingMixin from '../mixins/filesSorting.ts' -import logger from '../logger.js' +import logger from '../logger.ts' import DragAndDropNotice from '../components/DragAndDropNotice.vue' const isSharingEnabled = (getCapabilities() as { files_sharing?: boolean })?.files_sharing !== undefined @@ -391,9 +396,27 @@ export default defineComponent({ filtersChanged() { return this.filtersStore.filtersChanged }, + + showCustomEmptyView() { + return !this.loading && this.isEmptyDir && this.currentView?.emptyView !== undefined + } }, watch: { + /** + * Handle rendering the custom empty view + * @param show The current state if the custom empty view should be rendered + */ + showCustomEmptyView(show: boolean) { + if (show) { + this.$nextTick(() => { + const el = this.$refs.customEmptyView as HTMLDivElement + // We can cast here because "showCustomEmptyView" assets that current view is set + this.currentView!.emptyView!(el) + }) + } + }, + currentView(newView, oldView) { if (newView?.id === oldView?.id) { return @@ -679,6 +702,11 @@ export default defineComponent({ } } + &__empty-view-wrapper { + display: flex; + height: 100%; + } + &__refresh-icon { flex: 0 0 44px; width: 44px; diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index f431ca62b5f..b0588863f5d 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -81,7 +81,7 @@ import { useNavigation } from '../composables/useNavigation' import { useFilenameFilter } from '../composables/useFilenameFilter' import { useFiltersStore } from '../store/filters.ts' import { useViewConfigStore } from '../store/viewConfig.ts' -import logger from '../logger.js' +import logger from '../logger.ts' export default defineComponent({ name: 'Navigation', diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index 8c4af5fd80f..efa090112de 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -108,7 +108,7 @@ import FileInfo from '../services/FileInfo.js' import LegacyView from '../components/LegacyView.vue' import SidebarTab from '../components/SidebarTab.vue' import SystemTags from '../../../systemtags/src/components/SystemTags.vue' -import logger from '../logger.js' +import logger from '../logger.ts' export default { name: 'Sidebar', diff --git a/apps/files/src/views/TemplatePicker.vue b/apps/files/src/views/TemplatePicker.vue index 2a346ae139f..0d5b3cbaac9 100644 --- a/apps/files/src/views/TemplatePicker.vue +++ b/apps/files/src/views/TemplatePicker.vue @@ -60,7 +60,7 @@ import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' import TemplatePreview from '../components/TemplatePreview.vue' import TemplateFiller from '../components/TemplateFiller.vue' -import logger from '../logger.js' +import logger from '../logger.ts' const border = 2 const margin = 8 diff --git a/apps/files_sharing/src/new/newFileRequest.ts b/apps/files_sharing/src/new/newFileRequest.ts index c069bf5cdea..55b7f534610 100644 --- a/apps/files_sharing/src/new/newFileRequest.ts +++ b/apps/files_sharing/src/new/newFileRequest.ts @@ -4,50 +4,31 @@ */ import type { Entry, Folder, Node } from '@nextcloud/files' +import { defineAsyncComponent } from 'vue' +import { spawnDialog } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import FileUploadSvg from '@mdi/svg/svg/file-upload.svg?raw' -import Vue, { defineAsyncComponent } from 'vue' + import Config from '../services/ConfigService' +const sharingConfig = new Config() const NewFileRequestDialogVue = defineAsyncComponent(() => import('../components/NewFileRequestDialog.vue')) -const sharingConfig = new Config() - export const EntryId = 'file-request' export const entry = { id: EntryId, displayName: t('files_sharing', 'Create file request'), iconSvgInline: FileUploadSvg, - order: 30, + order: 10, enabled(): boolean { // We will check for the folder permission on the dialog return sharingConfig.isPublicShareAllowed }, async handler(context: Folder, content: Node[]) { - // Create document root - const mountingPoint = document.createElement('div') - mountingPoint.id = 'file-request-dialog' - document.body.appendChild(mountingPoint) - - // Init vue app - const NewFileRequestDialog = new Vue({ - name: 'NewFileRequestDialogRoot', - render: (h) => h( - NewFileRequestDialogVue, - { - props: { - context, - content, - }, - on: { - close: () => { - NewFileRequestDialog.$destroy() - }, - }, - }, - ), - el: mountingPoint, + spawnDialog(NewFileRequestDialogVue, { + context, + content, }) }, } as Entry diff --git a/apps/files_trashbin/src/actions/restoreAction.ts b/apps/files_trashbin/src/actions/restoreAction.ts index cda82fda466..31160183d83 100644 --- a/apps/files_trashbin/src/actions/restoreAction.ts +++ b/apps/files_trashbin/src/actions/restoreAction.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { emit } from '@nextcloud/event-bus' +import { encodePath } from '@nextcloud/paths' import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' import { Permission, Node, View, registerFileAction, FileAction } from '@nextcloud/files' @@ -10,8 +11,7 @@ import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import History from '@mdi/svg/svg/history.svg?raw' -import logger from '../../../files/src/logger.js' -import { encodePath } from '@nextcloud/paths' +import logger from '../../../files/src/logger.ts' registerFileAction(new FileAction({ id: 'restore', diff --git a/apps/systemtags/src/services/systemtags.ts b/apps/systemtags/src/services/systemtags.ts index 8e318f13adc..f29bb83984b 100644 --- a/apps/systemtags/src/services/systemtags.ts +++ b/apps/systemtags/src/services/systemtags.ts @@ -6,12 +6,12 @@ import type { ContentsWithRoot } from '@nextcloud/files' import type { FileStat, ResponseDataDetailed } from 'webdav' import type { TagWithId } from '../types' -import { Folder, Permission, getDavNameSpaces, getDavProperties, davGetClient, davResultToNode } from '@nextcloud/files' -import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' - +import { Folder, Permission, getDavNameSpaces, getDavProperties, davGetClient, davResultToNode, davRemoteURL, davRootPath } from '@nextcloud/files' import { fetchTags } from './api' +const rootPath = '/systemtags' + const client = davGetClient() const resultToNode = (node: FileStat) => davResultToNode(node) @@ -20,17 +20,18 @@ const formatReportPayload = (tagId: number) => `<?xml version="1.0"?> <d:prop> ${getDavProperties()} </d:prop> - <oc:filter-rules> - <oc:systemtag>${tagId}</oc:systemtag> - </oc:filter-rules> + <oc:filter-rules> + <oc:systemtag>${tagId}</oc:systemtag> + </oc:filter-rules> </oc:filter-files>` const tagToNode = function(tag: TagWithId): Folder { return new Folder({ id: tag.id, - source: generateRemoteUrl('dav/systemtags/' + tag.id), - owner: getCurrentUser()?.uid as string, - root: '/systemtags', + source: `${davRemoteURL}${rootPath}/${tag.id}`, + owner: String(getCurrentUser()?.uid ?? 'anonymous'), + root: rootPath, + displayname: tag.displayName, permissions: Permission.READ, attributes: { ...tag, @@ -47,16 +48,16 @@ export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { return { folder: new Folder({ id: 0, - source: generateRemoteUrl('dav/systemtags'), + source: `${davRemoteURL}${rootPath}`, owner: getCurrentUser()?.uid as string, - root: '/systemtags', + root: rootPath, permissions: Permission.NONE, }), contents: tagsCache.map(tagToNode), } } - const tagId = parseInt(path.replace('/', ''), 10) + const tagId = parseInt(path.split('/', 2)[0]) const tag = tagsCache.find(tag => tag.id === tagId) if (!tag) { @@ -64,7 +65,7 @@ export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { } const folder = tagToNode(tag) - const contentsResponse = await client.getDirectoryContents('/', { + const contentsResponse = await client.getDirectoryContents(davRootPath, { details: true, // Only filter favorites if we're at the root data: formatReportPayload(tagId), |