diff options
Diffstat (limited to 'apps/files/src')
30 files changed, 217 insertions, 78 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 1f0665ced8f..3e9e441a63c 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -10,7 +10,7 @@ import PQueue from 'p-queue' 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 TrashCanSvg from '@mdi/svg/svg/trash-can-outline.svg?raw' import { TRASHBIN_VIEW_ID } from '../../../files_trashbin/src/files_views/trashbinView.ts' import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, shouldAskForConfirmation } from './deleteUtils.ts' @@ -112,5 +112,6 @@ export const action = new FileAction({ return Promise.all(promises) }, + destructive: true, order: 100, }) diff --git a/apps/files/src/actions/moveOrCopyAction.ts b/apps/files/src/actions/moveOrCopyAction.ts index 724b65fa515..af68120bb1b 100644 --- a/apps/files/src/actions/moveOrCopyAction.ts +++ b/apps/files/src/actions/moveOrCopyAction.ts @@ -16,8 +16,8 @@ import { openConflictPicker, hasConflict } from '@nextcloud/upload' import { basename, join } from 'path' import Vue from 'vue' -import CopyIconSvg from '@mdi/svg/svg/folder-multiple.svg?raw' -import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw' +import CopyIconSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw' +import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw' import { MoveCopyAction, canCopy, canMove, getQueue } from './moveOrCopyActionUtils' import { getContents } from '../services/Files' diff --git a/apps/files/src/actions/renameAction.ts b/apps/files/src/actions/renameAction.ts index d421d18c473..715ecb7563e 100644 --- a/apps/files/src/actions/renameAction.ts +++ b/apps/files/src/actions/renameAction.ts @@ -5,7 +5,7 @@ import { emit } from '@nextcloud/event-bus' import { Permission, type Node, FileAction, View } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import PencilSvg from '@mdi/svg/svg/pencil.svg?raw' +import PencilSvg from '@mdi/svg/svg/pencil-outline.svg?raw' import { getPinia } from '../store' import { useFilesStore } from '../store/files' import { dirname } from 'path' diff --git a/apps/files/src/actions/viewInFolderAction.ts b/apps/files/src/actions/viewInFolderAction.ts index eb145dc409f..b22393c1152 100644 --- a/apps/files/src/actions/viewInFolderAction.ts +++ b/apps/files/src/actions/viewInFolderAction.ts @@ -2,10 +2,13 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { Node, FileType, Permission, View, FileAction } from '@nextcloud/files' -import { translate as t } from '@nextcloud/l10n' -import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw' +import type { Node, View } from '@nextcloud/files' + import { isPublicShare } from '@nextcloud/sharing/public' +import { FileAction, FileType, Permission } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' + +import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw' export const action = new FileAction({ id: 'view-in-folder', diff --git a/apps/files/src/components/DragAndDropNotice.vue b/apps/files/src/components/DragAndDropNotice.vue index 38d07c94d80..c7684d5c205 100644 --- a/apps/files/src/components/DragAndDropNotice.vue +++ b/apps/files/src/components/DragAndDropNotice.vue @@ -235,7 +235,7 @@ export default defineComponent({ justify-content: center; width: 100%; // Breadcrumbs height + row thead height - min-height: calc(58px + 55px); + min-height: calc(58px + 44px); margin: 0; user-select: none; color: var(--color-text-maxcontrast); diff --git a/apps/files/src/components/DragAndDropPreview.vue b/apps/files/src/components/DragAndDropPreview.vue index 7c9c6f4f1a7..72fd98d43fb 100644 --- a/apps/files/src/components/DragAndDropPreview.vue +++ b/apps/files/src/components/DragAndDropPreview.vue @@ -92,7 +92,7 @@ export default Vue.extend({ </script> <style lang="scss"> -$size: 32px; +$size: 28px; $stack-shift: 6px; .files-list-drag-image { @@ -102,24 +102,24 @@ $stack-shift: 6px; display: flex; overflow: hidden; align-items: center; - height: 44px; - padding: 6px 12px; + height: $size + $stack-shift; + padding: $stack-shift $stack-shift * 2; background: var(--color-main-background); &__icon, - .files-list__row-icon { + .files-list__row-icon-preview-container { display: flex; overflow: hidden; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: $size - $stack-shift; + height: $size - $stack-shift;; border-radius: var(--border-radius); } &__icon { overflow: visible; - margin-inline-end: 12px; + margin-inline-end: $stack-shift * 2; img { max-width: 100%; @@ -138,13 +138,15 @@ $stack-shift: 6px; display: flex; // Stack effect if more than one element - .files-list__row-icon + .files-list__row-icon { + // Max 3 elements + > .files-list__row-icon-preview-container + .files-list__row-icon-preview-container { margin-top: $stack-shift; - margin-inline-start: $stack-shift - $size; - & + .files-list__row-icon { + margin-inline-start: $stack-shift * 2 - $size; + & + .files-list__row-icon-preview-container { margin-top: $stack-shift * 2; } } + // If we have manually clone the preview, // let's hide any fallback icons &:not(:empty) + * { diff --git a/apps/files/src/components/FileEntry/FavoriteIcon.vue b/apps/files/src/components/FileEntry/FavoriteIcon.vue index 87684758b43..c66cb8fbd7f 100644 --- a/apps/files/src/components/FileEntry/FavoriteIcon.vue +++ b/apps/files/src/components/FileEntry/FavoriteIcon.vue @@ -56,8 +56,8 @@ export default defineComponent({ :deep() { svg { // We added a stroke for a11y so we must increase the size to include the stroke - width: 26px !important; - height: 26px !important; + width: 20px !important; + height: 20px !important; // Override NcIconSvgWrapper defaults of 20px max-width: unset !important; diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue index ec111a1235d..5c537d878fe 100644 --- a/apps/files/src/components/FileEntry/FileEntryActions.vue +++ b/apps/files/src/components/FileEntry/FileEntryActions.vue @@ -25,15 +25,16 @@ :open="openedMenu" @close="onMenuClose" @closed="onMenuClosed"> - <!-- Default actions list--> - <NcActionButton v-for="action, index in enabledMenuActions" + <!-- Non-destructive actions list --> + <!-- Please keep this block in sync with the destructive actions block below --> + <NcActionButton v-for="action, index in renderedNonDestructiveActions" :key="action.id" :ref="`action-${action.id}`" class="files-list__row-action" :class="{ [`files-list__row-action-${action.id}`]: true, 'files-list__row-action--inline': index < enabledInlineActions.length, - 'files-list__row-action--menu': isValidMenu(action) + 'files-list__row-action--menu': isValidMenu(action), }" :close-after-click="!isValidMenu(action)" :data-cy-files-list-row-action="action.id" @@ -50,6 +51,35 @@ {{ actionDisplayName(action) }} </NcActionButton> + <!-- Destructive actions list --> + <template v-if="renderedDestructiveActions.length > 0"> + <NcActionSeparator /> + <NcActionButton v-for="action, index in renderedDestructiveActions" + :key="action.id" + :ref="`action-${action.id}`" + class="files-list__row-action" + :class="{ + [`files-list__row-action-${action.id}`]: true, + 'files-list__row-action--inline': index < enabledInlineActions.length, + 'files-list__row-action--menu': isValidMenu(action), + 'files-list__row-action--destructive': true, + }" + :close-after-click="!isValidMenu(action)" + :data-cy-files-list-row-action="action.id" + :is-menu="isValidMenu(action)" + :aria-label="action.title?.([source], currentView)" + :title="action.title?.([source], currentView)" + @click="onActionClick(action)"> + <template #icon> + <NcLoadingIcon v-if="isLoadingAction(action)" /> + <NcIconSvgWrapper v-else + class="files-list__row-action-icon" + :svg="action.iconSvgInline([source], currentView)" /> + </template> + {{ actionDisplayName(action) }} + </NcActionButton> + </template> + <!-- Submenu actions list--> <template v-if="openedSubmenu && enabledSubmenuActions[openedSubmenu?.id]"> <!-- Back to top-level button --> @@ -68,10 +98,11 @@ class="files-list__row-action--submenu" close-after-click :data-cy-files-list-row-action="action.id" + :aria-label="action.title?.([source], currentView)" :title="action.title?.([source], currentView)" @click="onActionClick(action)"> <template #icon> - <NcLoadingIcon v-if="isLoadingAction(action)" :size="18" /> + <NcLoadingIcon v-if="isLoadingAction(action)" /> <NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" /> </template> {{ actionDisplayName(action) }} @@ -211,6 +242,14 @@ export default defineComponent({ return actions.filter(action => !(action.parent && topActionsIds.includes(action.parent))) }, + renderedNonDestructiveActions() { + return this.enabledMenuActions.filter(action => !action.destructive) + }, + + renderedDestructiveActions() { + return this.enabledMenuActions.filter(action => action.destructive) + }, + openedMenu: { get() { return this.opened @@ -349,5 +388,12 @@ main.app-content[style*="mouse-pos-x"] .v-popper__popper { max-height: var(--max-icon-size) !important; max-width: var(--max-icon-size) !important; } + + &.files-list__row-action--destructive { + ::deep(button) { + color: var(--color-error) !important; + } + } } + </style> diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue index 2fec9e5d556..418f9581eb6 100644 --- a/apps/files/src/components/FileEntry/FileEntryName.vue +++ b/apps/files/src/components/FileEntry/FileEntryName.vue @@ -30,7 +30,7 @@ <span class="files-list__row-name-text" dir="auto"> <!-- Keep the filename stuck to the extension to avoid whitespace rendering issues--> <span class="files-list__row-name-" v-text="basename" /> - <span class="files-list__row-name-ext" v-text="extension" /> + <span v-if="userConfigStore.userConfig.show_files_extensions" class="files-list__row-name-ext" v-text="extension" /> </span> </component> </template> @@ -46,11 +46,12 @@ import { defineComponent, inject } from 'vue' import NcTextField from '@nextcloud/vue/components/NcTextField' -import { useNavigation } from '../../composables/useNavigation' +import { getFilenameValidity } from '../../utils/filenameValidity.ts' import { useFileListWidth } from '../../composables/useFileListWidth.ts' -import { useRouteParameters } from '../../composables/useRouteParameters.ts' +import { useNavigation } from '../../composables/useNavigation.ts' import { useRenamingStore } from '../../store/renaming.ts' -import { getFilenameValidity } from '../../utils/filenameValidity.ts' +import { useRouteParameters } from '../../composables/useRouteParameters.ts' +import { useUserConfigStore } from '../../store/userconfig.ts' import logger from '../../logger.ts' export default defineComponent({ @@ -95,6 +96,7 @@ export default defineComponent({ const { directory } = useRouteParameters() const filesListWidth = useFileListWidth() const renamingStore = useRenamingStore() + const userConfigStore = useUserConfigStore() const defaultFileAction = inject<FileAction | undefined>('defaultFileAction') @@ -105,6 +107,7 @@ export default defineComponent({ filesListWidth, renamingStore, + userConfigStore, } }, diff --git a/apps/files/src/components/FileEntry/FileEntryPreview.vue b/apps/files/src/components/FileEntry/FileEntryPreview.vue index 506677b49af..3d0fffe7584 100644 --- a/apps/files/src/components/FileEntry/FileEntryPreview.vue +++ b/apps/files/src/components/FileEntry/FileEntryPreview.vue @@ -64,7 +64,7 @@ import FolderIcon from 'vue-material-design-icons/Folder.vue' import FolderOpenIcon from 'vue-material-design-icons/FolderOpen.vue' import KeyIcon from 'vue-material-design-icons/Key.vue' import LinkIcon from 'vue-material-design-icons/Link.vue' -import NetworkIcon from 'vue-material-design-icons/Network.vue' +import NetworkIcon from 'vue-material-design-icons/NetworkOutline.vue' import TagIcon from 'vue-material-design-icons/Tag.vue' import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue' diff --git a/apps/files/src/components/FileListFilter/FileListFilterModified.vue b/apps/files/src/components/FileListFilter/FileListFilterModified.vue index f3a968dd56e..3a843b2bc3e 100644 --- a/apps/files/src/components/FileListFilter/FileListFilterModified.vue +++ b/apps/files/src/components/FileListFilter/FileListFilterModified.vue @@ -7,7 +7,7 @@ :filter-name="t('files', 'Modified')" @reset-filter="resetFilter"> <template #icon> - <NcIconSvgWrapper :path="mdiCalendarRange" /> + <NcIconSvgWrapper :path="mdiCalendarRangeOutline" /> </template> <NcActionButton v-for="preset of timePresets" :key="preset.id" @@ -25,7 +25,7 @@ import type { PropType } from 'vue' import type { ITimePreset } from '../../filters/ModifiedFilter.ts' -import { mdiCalendarRange } from '@mdi/js' +import { mdiCalendarRangeOutline } from '@mdi/js' import { translate as t } from '@nextcloud/l10n' import { defineComponent } from 'vue' @@ -50,7 +50,7 @@ export default defineComponent({ setup() { return { // icons used in template - mdiCalendarRange, + mdiCalendarRangeOutline, } }, diff --git a/apps/files/src/components/FileListFilter/FileListFilterType.vue b/apps/files/src/components/FileListFilter/FileListFilterType.vue index 53e46b48f8c..d3ad791513f 100644 --- a/apps/files/src/components/FileListFilter/FileListFilterType.vue +++ b/apps/files/src/components/FileListFilter/FileListFilterType.vue @@ -8,7 +8,7 @@ :filter-name="t('files', 'Type')" @reset-filter="resetFilter"> <template #icon> - <NcIconSvgWrapper :path="mdiFile" /> + <NcIconSvgWrapper :path="mdiFileOutline" /> </template> <NcActionButton v-for="fileType of typePresets" :key="fileType.id" @@ -27,7 +27,7 @@ import type { PropType } from 'vue' import type { ITypePreset } from '../../filters/TypeFilter.ts' -import { mdiFile } from '@mdi/js' +import { mdiFileOutline } from '@mdi/js' import { translate as t } from '@nextcloud/l10n' import { defineComponent } from 'vue' @@ -57,7 +57,7 @@ export default defineComponent({ setup() { return { - mdiFile, + mdiFileOutline, t, } }, diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 04acbd302f5..47b8ef19b19 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -481,13 +481,13 @@ export default defineComponent({ <style scoped lang="scss"> .files-list { - --row-height: 55px; + --row-height: 44px; --cell-margin: 14px; --checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2); --checkbox-size: 24px; --clickable-area: var(--default-clickable-area); - --icon-preview-size: 32px; + --icon-preview-size: 24px; --fixed-block-start-position: var(--default-clickable-area); display: flex; @@ -775,8 +775,8 @@ export default defineComponent({ // File and folder overlay &-overlay { position: absolute; - max-height: calc(var(--icon-preview-size) * 0.5); - max-width: calc(var(--icon-preview-size) * 0.5); + max-height: calc(var(--icon-preview-size) * 0.6); + max-width: calc(var(--icon-preview-size) * 0.6); color: var(--color-primary-element-text); // better alignment with the folder icon margin-block-start: 2px; @@ -888,21 +888,21 @@ export default defineComponent({ } .files-list__row-size { - width: calc(var(--row-height) * 1.5); + width: calc(var(--row-height) * 2); // Right align content/text justify-content: flex-end; } .files-list__row-mtime { - width: calc(var(--row-height) * 2); + width: calc(var(--row-height) * 2.5); } .files-list__row-mime { - width: calc(var(--row-height) * 2.5); + width: calc(var(--row-height) * 3.5); } .files-list__row-column-custom { - width: calc(var(--row-height) * 2); + width: calc(var(--row-height) * 2.5); } } } diff --git a/apps/files/src/components/NavigationQuota.vue b/apps/files/src/components/NavigationQuota.vue index fd10af1c495..46c8e5c9af4 100644 --- a/apps/files/src/components/NavigationQuota.vue +++ b/apps/files/src/components/NavigationQuota.vue @@ -33,7 +33,7 @@ import { subscribe } from '@nextcloud/event-bus' import { translate } from '@nextcloud/l10n' import axios from '@nextcloud/axios' -import ChartPie from 'vue-material-design-icons/ChartPie.vue' +import ChartPie from 'vue-material-design-icons/ChartPieOutline.vue' import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem' import NcProgressBar from '@nextcloud/vue/components/NcProgressBar' diff --git a/apps/files/src/components/NewNodeDialog.vue b/apps/files/src/components/NewNodeDialog.vue index 76555db1536..ca10935940d 100644 --- a/apps/files/src/components/NewNodeDialog.vue +++ b/apps/files/src/components/NewNodeDialog.vue @@ -26,6 +26,11 @@ :helper-text="validity" :label="label" :value.sync="localDefaultName" /> + + <!-- Hidden file warning --> + <NcNoteCard v-if="isHiddenFileName" + type="warning" + :text="t('files', 'Files starting with a dot are hidden by default')" /> </form> </NcDialog> </template> @@ -35,12 +40,13 @@ import type { ComponentPublicInstance, PropType } from 'vue' import { getUniqueName } from '@nextcloud/files' import { t } from '@nextcloud/l10n' import { extname } from 'path' -import { nextTick, onMounted, ref, watch, watchEffect } from 'vue' +import { computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue' import { getFilenameValidity } from '../utils/filenameValidity.ts' import NcButton from '@nextcloud/vue/components/NcButton' import NcDialog from '@nextcloud/vue/components/NcDialog' import NcTextField from '@nextcloud/vue/components/NcTextField' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' const props = defineProps({ /** @@ -89,6 +95,11 @@ const nameInput = ref<ComponentPublicInstance>() const formElement = ref<HTMLFormElement>() const validity = ref('') +const isHiddenFileName = computed(() => { + // Check if the name starts with a dot, which indicates a hidden file + return localDefaultName.value.trim().startsWith('.') +}) + /** * Focus the filename input field */ diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue index 4f9d8096580..4746fedf863 100644 --- a/apps/files/src/components/VirtualList.vue +++ b/apps/files/src/components/VirtualList.vue @@ -157,7 +157,7 @@ export default defineComponent({ itemHeight() { // Align with css in FilesListVirtual // 166px + 32px (name) + 16px (mtime) + 16px (padding top and bottom) - return this.gridMode ? (166 + 32 + 16 + 16 + 16) : 55 + return this.gridMode ? (166 + 32 + 16 + 16 + 16) : 44 }, // Grid mode only diff --git a/apps/files/src/composables/useNavigation.spec.ts b/apps/files/src/composables/useNavigation.spec.ts index 569e61825e1..b9eb671a181 100644 --- a/apps/files/src/composables/useNavigation.spec.ts +++ b/apps/files/src/composables/useNavigation.spec.ts @@ -29,6 +29,7 @@ describe('Composables: useNavigation', () => { describe('currentView', () => { beforeEach(() => { + // eslint-disable-next-line import/namespace navigation = new nextcloudFiles.Navigation() spy.mockImplementation(() => navigation) }) @@ -39,6 +40,7 @@ describe('Composables: useNavigation', () => { }) it('should return already active navigation', async () => { + // eslint-disable-next-line import/namespace const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 }) navigation.register(view) navigation.setActive(view) @@ -48,6 +50,7 @@ describe('Composables: useNavigation', () => { }) it('should be reactive on updating active navigation', async () => { + // eslint-disable-next-line import/namespace const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 }) navigation.register(view) const wrapper = mount(TestComponent) @@ -63,6 +66,7 @@ describe('Composables: useNavigation', () => { describe('views', () => { beforeEach(() => { + // eslint-disable-next-line import/namespace navigation = new nextcloudFiles.Navigation() spy.mockImplementation(() => navigation) }) @@ -73,6 +77,7 @@ describe('Composables: useNavigation', () => { }) it('should return already registered views', () => { + // eslint-disable-next-line import/namespace const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 }) // register before mount navigation.register(view) @@ -82,7 +87,9 @@ describe('Composables: useNavigation', () => { }) it('should be reactive on registering new views', () => { + // eslint-disable-next-line import/namespace const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 }) + // eslint-disable-next-line import/namespace const view2 = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-2', name: 'My View 2', order: 1 }) // register before mount diff --git a/apps/files/src/models/Setting.js b/apps/files/src/models/Setting.js index 66173b7df93..1db1d818e69 100644 --- a/apps/files/src/models/Setting.js +++ b/apps/files/src/models/Setting.js @@ -9,6 +9,7 @@ export default class Setting { _el _name _open + _order /** * Create a new files app setting @@ -19,12 +20,14 @@ export default class Setting { * @param {Function} component.el function that returns an unmounted dom element to be added * @param {Function} [component.open] callback for when setting is added * @param {Function} [component.close] callback for when setting is closed + * @param {number} [component.order] the order of this setting, lower numbers are shown first */ - constructor(name, { el, open, close }) { + constructor(name, { el, open, close, order }) { this._name = name this._el = el this._open = open this._close = close + this._order = order || 0 if (typeof this._open !== 'function') { this._open = () => {} @@ -33,6 +36,18 @@ export default class Setting { if (typeof this._close !== 'function') { this._close = () => {} } + + if (typeof this._el !== 'function') { + throw new Error('Setting must have an `el` function that returns a DOM element') + } + + if (typeof this._name !== 'string') { + throw new Error('Setting must have a `name` string') + } + + if (typeof this._order !== 'number') { + throw new Error('Setting must have an `order` number') + } } get name() { @@ -51,4 +66,8 @@ export default class Setting { return this._close } + get order() { + return this._order + } + } diff --git a/apps/files/src/newMenu/newFolder.ts b/apps/files/src/newMenu/newFolder.ts index 9a8badb4eb7..bc82d389e54 100644 --- a/apps/files/src/newMenu/newFolder.ts +++ b/apps/files/src/newMenu/newFolder.ts @@ -12,7 +12,7 @@ import { showError, showInfo, showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' -import FolderPlusSvg from '@mdi/svg/svg/folder-plus.svg?raw' +import FolderPlusSvg from '@mdi/svg/svg/folder-plus-outline.svg?raw' import { newNodeName } from '../utils/newNodeDialog' import logger from '../logger' @@ -43,8 +43,11 @@ export const entry = { id: 'newFolder', displayName: t('files', 'New folder'), enabled: (context: Folder) => Boolean(context.permissions & Permission.CREATE) && Boolean(context.permissions & Permission.READ), - iconSvgInline: FolderPlusSvg, + + // Make the svg icon color match the primary element color + iconSvgInline: FolderPlusSvg.replace(/viewBox/gi, 'style="color: var(--color-primary-element)" viewBox'), order: 0, + async handler(context: Folder, content: Node[]) { const name = await newNodeName(t('files', 'New folder'), content) if (name === null) { diff --git a/apps/files/src/plugins/search/folderSearch.ts b/apps/files/src/plugins/search/folderSearch.ts index 626b1daa72b..6aabefbfc9d 100644 --- a/apps/files/src/plugins/search/folderSearch.ts +++ b/apps/files/src/plugins/search/folderSearch.ts @@ -36,12 +36,15 @@ function init() { callback: (nodes: Node[]) => { logger.info('Folder picked', { folder: nodes[0] }) const folder = nodes[0] + const filterUpdateText = (folder.root === '/files/' + folder.basename) + ? t('files', 'Search in all files') + : t('files', 'Search in folder: {folder}', { folder: folder.basename }) emit('nextcloud:unified-search:add-filter', { id: 'in-folder', appId: 'files', searchFrom: 'files', payload: folder, - filterUpdateText: t('files', 'Search in folder: {folder}', { folder: folder.basename }), + filterUpdateText, filterParams: { path: folder.path }, }) }, diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts index 54e9a75eb8b..48fe01d5134 100644 --- a/apps/files/src/store/userconfig.ts +++ b/apps/files/src/store/userconfig.ts @@ -15,6 +15,7 @@ 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, diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 6757b7f1f45..0096ecc0fdb 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -55,6 +55,7 @@ export interface UserConfig { crop_image_previews: boolean default_view: 'files' | 'personal' grid_view: boolean + show_files_extensions: boolean show_hidden: boolean show_mime_column: boolean sort_favorites_first: boolean diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 15a7f93ddf0..3f993e24958 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -176,12 +176,12 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' -import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue' +import AccountPlusIcon from 'vue-material-design-icons/AccountPlusOutline.vue' import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' import IconReload from 'vue-material-design-icons/Reload.vue' import LinkIcon from 'vue-material-design-icons/Link.vue' import ListViewIcon from 'vue-material-design-icons/FormatListBulletedSquare.vue' -import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue' +import ViewGridIcon from 'vue-material-design-icons/ViewGridOutline.vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' import { useFileListWidth } from '../composables/useFileListWidth.ts' diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index c424a0d74b8..0f3c3647c6e 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -47,7 +47,7 @@ import { getNavigation } from '@nextcloud/files' import { t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' import { defineComponent } from 'vue' -import IconCog from 'vue-material-design-icons/Cog.vue' +import IconCog from 'vue-material-design-icons/CogOutline.vue' import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation' import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem' import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList' diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue index 50376bcc578..0838d308af9 100644 --- a/apps/files/src/views/Settings.vue +++ b/apps/files/src/views/Settings.vue @@ -29,7 +29,6 @@ {{ t('files', 'Personal files') }} </NcCheckboxRadioSwitch> </fieldset> - <NcCheckboxRadioSwitch data-cy-files-settings-setting="sort_favorites_first" :checked="userConfig.sort_favorites_first" @update:checked="setConfig('sort_favorites_first', $event)"> @@ -40,6 +39,15 @@ @update:checked="setConfig('sort_folders_first', $event)"> {{ t('files', 'Sort folders before files') }} </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch data-cy-files-settings-setting="folder_tree" + :checked="userConfig.folder_tree" + @update:checked="setConfig('folder_tree', $event)"> + {{ t('files', 'Enable folder tree') }} + </NcCheckboxRadioSwitch> + </NcAppSettingsSection> + + <!-- Visual settings --> + <NcAppSettingsSection id="settings" :name="t('files', 'Visual settings')"> <NcCheckboxRadioSwitch data-cy-files-settings-setting="show_hidden" :checked="userConfig.show_hidden" @update:checked="setConfig('show_hidden', $event)"> @@ -55,10 +63,10 @@ @update:checked="setConfig('crop_image_previews', $event)"> {{ t('files', 'Crop image previews') }} </NcCheckboxRadioSwitch> - <NcCheckboxRadioSwitch data-cy-files-settings-setting="folder_tree" - :checked="userConfig.folder_tree" - @update:checked="setConfig('folder_tree', $event)"> - {{ t('files', 'Enable folder tree') }} + <NcCheckboxRadioSwitch data-cy-files-settings-setting="show_files_extensions" + :checked="userConfig.show_files_extensions" + @update:checked="setConfig('show_files_extensions', $event)"> + {{ t('files', 'Show files extensions') }} </NcCheckboxRadioSwitch> </NcAppSettingsSection> @@ -79,6 +87,7 @@ :success="webdavUrlCopied" :trailing-button-label="t('files', 'Copy to clipboard')" :value="webdavUrl" + class="webdav-url-input" readonly="readonly" type="url" @focus="$event.target.select()" @@ -92,13 +101,13 @@ :href="webdavDocs" target="_blank" rel="noreferrer noopener"> - {{ t('files', 'Use this address to access your Files via WebDAV') }} ↗ + {{ t('files', 'Use this address to access your Files via WebDAV.') }} ↗ </a> </em> <br> - <em> + <em v-if="isTwoFactorEnabled"> <a class="setting-link" :href="appPasswordUrl"> - {{ t('files', 'If you have enabled 2FA, you must create and use a new app password by clicking here.') }} ↗ + {{ t('files', 'Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client.') }} ↗ </a> </em> </NcAppSettingsSection> @@ -333,6 +342,7 @@ export default { appPasswordUrl: generateUrl('/settings/user/security#generate-app-token-section'), webdavUrlCopied: false, enableGridView: (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true), + isTwoFactorEnabled: (loadState('files', 'isTwoFactorEnabled', false)), } }, @@ -340,6 +350,16 @@ export default { userConfig() { return this.userConfigStore.userConfig }, + + sortedSettings() { + // Sort settings by name + return [...this.settings].sort((a, b) => { + if (a.order && b.order) { + return a.order - b.order + } + return a.name.localeCompare(b.name) + }) + }, }, created() { @@ -419,4 +439,8 @@ export default { white-space: nowrap; } } + +.webdav-url-input { + margin-block-end: 0.5rem; +} </style> diff --git a/apps/files/src/views/favorites.spec.ts b/apps/files/src/views/favorites.spec.ts index 64c3df0500a..f793eb9f54c 100644 --- a/apps/files/src/views/favorites.spec.ts +++ b/apps/files/src/views/favorites.spec.ts @@ -7,6 +7,7 @@ import type { Folder as CFolder, Navigation } from '@nextcloud/files' import * as filesUtils from '@nextcloud/files' +import * as filesDavUtils from '@nextcloud/files/dav' import { CancelablePromise } from 'cancelable-promise' import { basename } from 'path' import { beforeEach, describe, expect, test, vi } from 'vitest' @@ -16,6 +17,7 @@ import { action } from '../actions/favoriteAction' import * as favoritesService from '../services/Favorites' import { registerFavoritesView } from './favorites' +// eslint-disable-next-line import/namespace const { Folder, getNavigation } = filesUtils vi.mock('@nextcloud/axios') @@ -43,7 +45,7 @@ describe('Favorites view definition', () => { test('Default empty favorite view', async () => { vi.spyOn(eventBus, 'subscribe') - vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([])) + vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([])) vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] })) await registerFavoritesView() @@ -89,8 +91,14 @@ describe('Favorites view definition', () => { source: 'http://nextcloud.local/remote.php/dav/files/admin/foo/bar', owner: 'admin', }), + new Folder({ + id: 4, + root: '/files/admin', + source: 'http://nextcloud.local/remote.php/dav/files/admin/foo/bar/yabadaba', + owner: 'admin', + }), ] - vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve(favoriteFolders)) + vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve(favoriteFolders)) vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] })) await registerFavoritesView() @@ -98,9 +106,12 @@ describe('Favorites view definition', () => { const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites') // one main view and 3 children - expect(Navigation.views.length).toBe(4) + expect(Navigation.views.length).toBe(5) expect(favoritesView).toBeDefined() - expect(favoriteFoldersViews.length).toBe(3) + expect(favoriteFoldersViews.length).toBe(4) + + // Sorted by basename: bar, bar, foo + const expectedOrder = [2, 0, 1, 3] favoriteFolders.forEach((folder, index) => { const favoriteView = favoriteFoldersViews[index] @@ -108,7 +119,7 @@ describe('Favorites view definition', () => { expect(favoriteView?.id).toBeDefined() expect(favoriteView?.name).toBe(basename(folder.path)) expect(favoriteView?.icon).toMatch(/<svg.+<\/svg>/) - expect(favoriteView?.order).toBe(index) + expect(favoriteView?.order).toBe(expectedOrder[index]) expect(favoriteView?.params).toStrictEqual({ dir: folder.path, fileid: String(folder.fileid), @@ -132,7 +143,7 @@ describe('Dynamic update of favorite folders', () => { test('Add a favorite folder creates a new entry in the navigation', async () => { vi.spyOn(eventBus, 'emit') - vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([])) + vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([])) vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] })) await registerFavoritesView() @@ -160,7 +171,7 @@ describe('Dynamic update of favorite folders', () => { test('Remove a favorite folder remove the entry from the navigation column', async () => { vi.spyOn(eventBus, 'emit') - vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([ + vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([ new Folder({ id: 42, root: '/files/admin', @@ -211,7 +222,7 @@ describe('Dynamic update of favorite folders', () => { test('Renaming a favorite folder updates the navigation', async () => { vi.spyOn(eventBus, 'emit') - vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([])) + vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([])) vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] })) await registerFavoritesView() diff --git a/apps/files/src/views/favorites.ts b/apps/files/src/views/favorites.ts index cadc7704e14..cac776507ef 100644 --- a/apps/files/src/views/favorites.ts +++ b/apps/files/src/views/favorites.ts @@ -4,13 +4,15 @@ */ import type { Folder, Node } from '@nextcloud/files' +import { FileType, View, getNavigation } from '@nextcloud/files' +import { getCanonicalLocale, getLanguage, t } from '@nextcloud/l10n' +import { getFavoriteNodes } from '@nextcloud/files/dav' import { subscribe } from '@nextcloud/event-bus' -import { FileType, View, getFavoriteNodes, getNavigation } from '@nextcloud/files' -import { getLanguage, translate as t } from '@nextcloud/l10n' -import { client } from '../services/WebdavClient.ts' + import FolderSvg from '@mdi/svg/svg/folder.svg?raw' -import StarSvg from '@mdi/svg/svg/star.svg?raw' +import StarSvg from '@mdi/svg/svg/star-outline.svg?raw' +import { client } from '../services/WebdavClient.ts' import { getContents } from '../services/Favorites' import { hashCode } from '../utils/hashUtils' import logger from '../logger' @@ -118,7 +120,7 @@ export const registerFavoritesView = async () => { * update the order property of the existing views */ const updateAndSortViews = function() { - favoriteFolders.sort((a, b) => a.path.localeCompare(b.path, getLanguage(), { ignorePunctuation: true })) + favoriteFolders.sort((a, b) => a.basename.localeCompare(b.basename, [getLanguage(), getCanonicalLocale()], { ignorePunctuation: true, numeric: true, usage: 'sort' })) favoriteFolders.forEach((folder, index) => { const view = favoriteFoldersViews.find((view) => view.id === generateIdFromPath(folder.path)) if (view) { @@ -176,4 +178,6 @@ export const registerFavoritesView = async () => { removePathFromFavorites(favoriteFolder.path) addToFavorites(node) } + + updateAndSortViews() } diff --git a/apps/files/src/views/files.ts b/apps/files/src/views/files.ts index 95450f0d71a..a94aab0f14b 100644 --- a/apps/files/src/views/files.ts +++ b/apps/files/src/views/files.ts @@ -10,7 +10,7 @@ import { getContents } from '../services/Files.ts' import { useActiveStore } from '../store/active.ts' import { defaultView } from '../utils/filesViews.ts' -import FolderSvg from '@mdi/svg/svg/folder.svg?raw' +import FolderSvg from '@mdi/svg/svg/folder-outline.svg?raw' export const VIEW_ID = 'files' diff --git a/apps/files/src/views/folderTree.ts b/apps/files/src/views/folderTree.ts index c38e4721316..2ce4e501e6f 100644 --- a/apps/files/src/views/folderTree.ts +++ b/apps/files/src/views/folderTree.ts @@ -13,7 +13,7 @@ import { isSamePath } from '@nextcloud/paths' import { loadState } from '@nextcloud/initial-state' import FolderSvg from '@mdi/svg/svg/folder.svg?raw' -import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple.svg?raw' +import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw' import { folderTreeId, @@ -151,7 +151,7 @@ const registerTreeRoot = () => { Navigation.register(new View({ id: folderTreeId, - name: t('files', 'All folders'), + name: t('files', 'Folder tree'), caption: t('files', 'List of your files and folders.'), icon: FolderMultipleSvg, diff --git a/apps/files/src/views/personal-files.ts b/apps/files/src/views/personal-files.ts index 36888eb7ee0..241582057d1 100644 --- a/apps/files/src/views/personal-files.ts +++ b/apps/files/src/views/personal-files.ts @@ -8,7 +8,7 @@ import { View, getNavigation } from '@nextcloud/files' import { getContents } from '../services/PersonalFiles.ts' import { defaultView, hasPersonalFilesView } from '../utils/filesViews.ts' -import AccountIcon from '@mdi/svg/svg/account.svg?raw' +import AccountIcon from '@mdi/svg/svg/account-outline.svg?raw' export const VIEW_ID = 'personal' |