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.ts3
-rw-r--r--apps/files/src/actions/moveOrCopyAction.ts4
-rw-r--r--apps/files/src/actions/renameAction.ts2
-rw-r--r--apps/files/src/actions/viewInFolderAction.ts9
-rw-r--r--apps/files/src/components/DragAndDropNotice.vue2
-rw-r--r--apps/files/src/components/DragAndDropPreview.vue22
-rw-r--r--apps/files/src/components/FileEntry/FavoriteIcon.vue4
-rw-r--r--apps/files/src/components/FileEntry/FileEntryActions.vue54
-rw-r--r--apps/files/src/components/FileEntry/FileEntryName.vue11
-rw-r--r--apps/files/src/components/FileEntry/FileEntryPreview.vue2
-rw-r--r--apps/files/src/components/FileListFilter/FileListFilterModified.vue6
-rw-r--r--apps/files/src/components/FileListFilter/FileListFilterType.vue6
-rw-r--r--apps/files/src/components/FilesListVirtual.vue16
-rw-r--r--apps/files/src/components/NavigationQuota.vue2
-rw-r--r--apps/files/src/components/NewNodeDialog.vue13
-rw-r--r--apps/files/src/components/VirtualList.vue2
-rw-r--r--apps/files/src/composables/useNavigation.spec.ts7
-rw-r--r--apps/files/src/models/Setting.js21
-rw-r--r--apps/files/src/newMenu/newFolder.ts7
-rw-r--r--apps/files/src/plugins/search/folderSearch.ts5
-rw-r--r--apps/files/src/store/userconfig.ts1
-rw-r--r--apps/files/src/types.ts1
-rw-r--r--apps/files/src/views/FilesList.vue4
-rw-r--r--apps/files/src/views/Navigation.vue2
-rw-r--r--apps/files/src/views/Settings.vue40
-rw-r--r--apps/files/src/views/favorites.spec.ts27
-rw-r--r--apps/files/src/views/favorites.ts14
-rw-r--r--apps/files/src/views/files.ts2
-rw-r--r--apps/files/src/views/folderTree.ts4
-rw-r--r--apps/files/src/views/personal-files.ts2
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'