Merge pull request #57664 from nextcloud/feat/filters

feat(files): render file list filters in top bar and allow to collapse into overflow menu
This commit is contained in:
Ferdinand Thiessen 2026-01-22 21:15:55 +01:00 committed by GitHub
commit e5c08cca40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 1592 additions and 1315 deletions

View file

@ -16,10 +16,11 @@
v-bind="section"
dir="auto"
:to="section.to"
:force-icon-text="index === 0 && fileListWidth >= 486"
:force-icon-text="index === 0 && !isNarrow"
force-menu
:open.sync="isMenuOpen"
:title="titleForSection(index, section)"
:aria-description="ariaForSection(section)"
@click.native="onClick(section.to)"
@dragover.native="onDragOver($event, section.dir)"
@drop="onDrop($event, section.dir)">
<template v-if="index === 0" #icon>
@ -27,6 +28,26 @@
:size="20"
:svg="viewIcon" />
</template>
<template v-if="index === sections.length - 1" #menu-icon>
<NcIconSvgWrapper :path="isMenuOpen ? mdiChevronUp : mdiChevronDown" />
</template>
<template v-if="index === sections.length - 1" #default>
<!-- Sharing button -->
<NcActionButton v-if="canShare" close-after-click @click="openSharingSidebar">
<template #icon>
<NcIconSvgWrapper :path="mdiAccountPlus" />
</template>
{{ t('files', 'Share') }}
</NcActionButton>
<!-- Reload button -->
<NcActionButton close-after-click @click="$emit('reload')">
<template #icon>
<NcIconSvgWrapper :path="mdiReload" />
</template>
{{ t('files', 'Reload content') }}
</NcActionButton>
</template>
</NcBreadcrumb>
<!-- Forward the actions slot -->
@ -40,12 +61,16 @@
import type { Node } from '@nextcloud/files'
import type { FileSource } from '../types.ts'
import { mdiAccountPlus, mdiChevronDown, mdiChevronUp, mdiReload } from '@mdi/js'
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
import { getCapabilities } from '@nextcloud/capabilities'
import { showError } from '@nextcloud/dialogs'
import { Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { getSidebar, Permission } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { isPublicShare } from '@nextcloud/sharing/public'
import { basename } from 'path'
import { defineComponent } from 'vue'
import { computed, defineComponent, ref, watch } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcBreadcrumb from '@nextcloud/vue/components/NcBreadcrumb'
import NcBreadcrumbs from '@nextcloud/vue/components/NcBreadcrumbs'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
@ -64,6 +89,7 @@ export default defineComponent({
name: 'BreadCrumbs',
components: {
NcActionButton,
NcBreadcrumbs,
NcBreadcrumb,
NcIconSvgWrapper,
@ -76,6 +102,8 @@ export default defineComponent({
},
},
emits: ['reload'],
setup() {
const activeStore = useActiveStore()
const filesStore = useFilesStore()
@ -84,9 +112,23 @@ export default defineComponent({
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
const fileListWidth = useFileListWidth()
const { isNarrow } = useFileListWidth()
const views = useViews()
const isMenuOpen = ref(false)
watch(() => activeStore.activeFolder, () => {
isMenuOpen.value = false
})
const isSharingEnabled = (getCapabilities() as { files_sharing?: boolean })?.files_sharing !== undefined
const isPublic = isPublicShare()
const canShare = computed(() => {
return isSharingEnabled
&& !isPublic
&& activeStore.activeFolder
&& (activeStore.activeFolder.permissions & Permission.SHARE) !== 0
})
return {
activeStore,
draggingStore,
@ -95,8 +137,23 @@ export default defineComponent({
selectionStore,
uploaderStore,
fileListWidth,
canShare,
isMenuOpen,
isNarrow,
views,
openSharingSidebar,
mdiAccountPlus,
mdiChevronDown,
mdiChevronUp,
mdiReload,
}
/**
* Open the sharing sidebar for the current folder
*/
function openSharingSidebar() {
getSidebar().open(activeStore.activeFolder!, 'sharing')
}
},
@ -132,7 +189,7 @@ export default defineComponent({
wrapUploadProgressBar(): boolean {
// if an upload is ongoing, and on small screens / mobile, then
// show the progress bar for the upload below breadcrumbs
return this.isUploadInProgress && this.fileListWidth < 512
return this.isUploadInProgress && this.isNarrow
},
// used to show the views icon for the first breadcrumb
@ -191,12 +248,6 @@ export default defineComponent({
}
},
onClick(to) {
if (to?.query?.dir === this.$route.query.dir) {
this.$emit('reload')
}
},
onDragOver(event: DragEvent, path: string) {
if (!event.dataTransfer) {
return
@ -298,8 +349,7 @@ export default defineComponent({
flex: 1 1 100% !important;
width: 100%;
height: 100%;
margin-block: 0;
margin-inline: 10px;
margin: 0;
min-width: 0;
:deep() {

View file

@ -160,7 +160,7 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
const filesListWidth = useFileListWidth()
const { isNarrow } = useFileListWidth()
const {
fileId: currentRouteFileId,
} = useRouteParameters()
@ -178,7 +178,7 @@ export default defineComponent({
activeView,
currentRouteFileId,
draggingStore,
filesListWidth,
isNarrow,
filesStore,
renamingStore,
selectionStore,
@ -209,10 +209,10 @@ export default defineComponent({
columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512 || this.compact) {
if (this.isNarrow || this.compact) {
return []
}
return this.activeView.columns || []
return this.activeView?.columns || []
},
mime() {

View file

@ -173,15 +173,17 @@ export default defineComponent({
},
},
emits: ['update:opened'],
setup() {
// The file list is guaranteed to be shown with active view - thus we can set the `loaded` flag
const activeStore = useActiveStore()
const filesListWidth = useFileListWidth()
const { isNarrow } = useFileListWidth()
const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
return {
activeStore,
enabledFileActions,
filesListWidth,
isNarrow,
t,
}
},
@ -206,7 +208,7 @@ export default defineComponent({
// Enabled action that are displayed inline
enabledInlineActions() {
if (this.filesListWidth < 768 || this.gridMode) {
if (this.isNarrow || this.gridMode) {
return []
}
return this.enabledFileActions.filter((action) => {
@ -302,7 +304,7 @@ export default defineComponent({
methods: {
actionDisplayName(action: FileAction) {
try {
if ((this.gridMode || (this.filesListWidth < 768 && action.inline)) && typeof action.title === 'function') {
if ((this.gridMode || (this.isNarrow && action.inline)) && typeof action.title === 'function') {
// if an inline action is rendered in the menu for
// lack of space we use the title first if defined
const title = action.title(this.actionContext)

View file

@ -39,7 +39,7 @@
</template>
<script lang="ts">
import type { FileAction, Node } from '@nextcloud/files'
import type { FileAction, Node, TFileType } from '@nextcloud/files'
import type { PropType } from 'vue'
import { showError } from '@nextcloud/dialogs'
@ -96,7 +96,7 @@ export default defineComponent({
setup() {
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const filesListWidth = useFileListWidth()
const { isNarrow } = useFileListWidth()
const renamingStore = useRenamingStore()
const userConfigStore = useUserConfigStore()
const { activeFolder, activeView } = useActiveStore()
@ -107,7 +107,7 @@ export default defineComponent({
activeFolder,
activeView,
defaultFileAction,
filesListWidth,
isNarrow,
renamingStore,
userConfigStore,
}
@ -119,7 +119,7 @@ export default defineComponent({
},
isRenamingSmallScreen() {
return this.isRenaming && this.filesListWidth < 512
return this.isRenaming && this.isNarrow
},
newName: {
@ -133,7 +133,7 @@ export default defineComponent({
},
renameLabel() {
const matchLabel: Record<FileType, string> = {
const matchLabel: Record<TFileType, string> = {
[FileType.File]: t('files', 'Filename'),
[FileType.Folder]: t('files', 'Folder name'),
}

View file

@ -113,7 +113,7 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
const filesListWidth = useFileListWidth()
const { isNarrow } = useFileListWidth()
const {
fileId: currentRouteFileId,
} = useRouteParameters()
@ -131,7 +131,7 @@ export default defineComponent({
activeView,
currentRouteFileId,
draggingStore,
filesListWidth,
isNarrow,
filesStore,
renamingStore,
selectionStore,

View file

@ -36,10 +36,6 @@ export default defineComponent({
type: Array as PropType<Node[]>,
required: true,
},
filesListWidth: {
type: Number,
default: 0,
},
isMtimeAvailable: {
type: Boolean,
default: false,
@ -119,7 +115,7 @@ export default defineComponent({
return this.renamingStore.renamingNode === this.source
},
isRenamingSmallScreen() {
return this.isRenaming && this.filesListWidth < 512
return this.isRenaming && this.isNarrow
},
isActive() {

View file

@ -1,55 +0,0 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcActions
force-menu
:variant="isActive ? 'secondary' : 'tertiary'"
:menu-name="filterName">
<template #icon>
<slot name="icon" />
</template>
<slot />
<template v-if="isActive">
<NcActionSeparator />
<NcActionButton
class="files-list-filter__clear-button"
close-after-click
@click="$emit('reset-filter')">
{{ t('files', 'Clear filter') }}
</NcActionButton>
</template>
</NcActions>
</template>
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
defineProps<{
isActive: boolean
filterName: string
}>()
defineEmits<{
(event: 'reset-filter'): void
}>()
</script>
<style scoped>
.files-list-filter__clear-button :deep(.action-button__text) {
color: var(--color-text-error, var(--color-error-text));
}
:deep(.button-vue) {
font-weight: normal !important;
* {
font-weight: normal !important;
}
}
</style>

View file

@ -0,0 +1,45 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import { computed } from 'vue'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcChip from '@nextcloud/vue/components/NcChip'
import { useFiltersStore } from '../../store/filters.ts'
const filterStore = useFiltersStore()
const activeChips = computed(() => filterStore.activeChips)
</script>
<template>
<ul
v-if="activeChips.length > 0"
:class="$style.fileListFilterChips"
:aria-label="t('files', 'Active filters')">
<li v-for="(chip, index) of activeChips" :key="index">
<NcChip
:aria-label-close="t('files', 'Remove filter')"
:icon-svg="chip.icon"
:text="chip.text"
@close="chip.onclick">
<template v-if="chip.user" #icon>
<NcAvatar
disable-menu
hide-status
:size="24"
:user="chip.user" />
</template>
</NcChip>
</li>
</ul>
</template>
<style module>
.fileListFilterChips {
display: flex;
gap: var(--default-grid-baseline);
}
</style>

View file

@ -3,100 +3,122 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<FileListFilter
:is-active="isActive"
:filter-name="t('files', 'Modified')"
@reset-filter="resetFilter">
<template #icon>
<NcIconSvgWrapper :path="mdiCalendarRangeOutline" />
</template>
<NcActionButton
<div>
<NcButton
v-for="preset of timePresets"
:key="preset.id"
type="radio"
close-after-click
:model-value.sync="selectedOption"
:value="preset.id">
alignment="start"
:pressed="preset === selectedOption"
variant="tertiary"
wide
@update:pressed="$event ? (selectedOption = preset) : onReset()">
{{ preset.label }}
</NcActionButton>
<!-- TODO: Custom time range -->
</FileListFilter>
</NcButton>
<NcDateTimePicker
v-if="selectedOption?.id === 'custom'"
v-model="timeRange"
append-to-body
:aria-label="t('files', 'Custom date range')"
type="date-range" />
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue'
import type { ITimePreset } from '../../filters/ModifiedFilter.ts'
<script setup lang="ts">
import type { ITimePreset, ModifiedFilter } from '../../filters/ModifiedFilter.ts'
import { mdiCalendarRangeOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import FileListFilter from './FileListFilter.vue'
import { t } from '@nextcloud/l10n'
import { NcDateTimePicker } from '@nextcloud/vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
export default defineComponent({
components: {
FileListFilter,
NcActionButton,
NcIconSvgWrapper,
},
const props = defineProps<{
filter: ModifiedFilter
}>()
props: {
timePresets: {
type: Array as PropType<ITimePreset[]>,
required: true,
},
},
setup() {
return {
// icons used in template
mdiCalendarRangeOutline,
const selectedOption = ref<typeof timePresets[number]>()
watch(selectedOption, (preset) => {
if (selectedOption.value) {
if (selectedOption.value.id === 'custom' && !timeRange.value) {
timeRange.value = [new Date(startOfLastWeek()), new Date(startOfToday())]
selectedOption.value.timeRange = [...timeRange.value]
}
},
data() {
return {
selectedOption: null as string | null,
timeRangeEnd: null as number | null,
timeRangeStart: null as number | null,
}
},
computed: {
/**
* Is the filter currently active
*/
isActive() {
return this.selectedOption !== null
},
currentPreset() {
return this.timePresets.find(({ id }) => id === this.selectedOption) ?? null
},
},
watch: {
selectedOption() {
if (this.selectedOption === null) {
this.$emit('update:preset')
} else {
const preset = this.currentPreset
this.$emit('update:preset', preset)
}
},
},
methods: {
t,
resetFilter() {
this.selectedOption = null
this.timeRangeEnd = null
this.timeRangeStart = null
},
},
props.filter.setPreset(selectedOption.value)
} else {
props.filter.setPreset()
}
})
const timeRange = ref<[Date, Date]>()
watch(timeRange, () => {
if (timeRange.value) {
selectedOption.value!.timeRange = [...timeRange.value]
props.filter.setPreset(selectedOption.value)
}
})
onMounted(() => {
selectedOption.value = props.filter.preset && timePresets.find((f) => f.id === props.filter.preset!.id)
props.filter.addEventListener('reset', onReset)
})
onUnmounted(() => {
props.filter.removeEventListener('reset', onReset)
})
/**
* Handler for resetting the filter
*/
function onReset() {
selectedOption.value = undefined
timeRange.value = undefined
}
</script>
<script lang="ts">
const startOfToday = () => (new Date()).setHours(0, 0, 0, 0)
const startOfLastWeek = () => startOfToday() - (7 * 24 * 60 * 60 * 1000)
/**
* Available presets
*/
const timePresets = [
{
id: 'today',
label: t('files', 'Today'),
filter: (time: number) => time > startOfToday(),
} satisfies ITimePreset,
{
id: 'last-7',
label: t('files', 'Last 7 days'),
filter: (time: number) => time > startOfLastWeek(),
} satisfies ITimePreset,
{
id: 'last-30',
label: t('files', 'Last 30 days'),
filter: (time: number) => time > (startOfToday() - (30 * 24 * 60 * 60 * 1000)),
} satisfies ITimePreset,
{
id: 'this-year',
label: t('files', 'This year ({year})', { year: (new Date()).getFullYear() }),
filter: (time: number) => time > (new Date(startOfToday())).setMonth(0, 1),
} satisfies ITimePreset,
{
id: 'last-year',
label: t('files', 'Last year ({year})', { year: (new Date()).getFullYear() - 1 }),
filter: (time: number) => (time > (new Date(startOfToday())).setFullYear((new Date()).getFullYear() - 1, 0, 1)) && (time < (new Date(startOfToday())).setMonth(0, 1)),
} satisfies ITimePreset,
{
id: 'custom',
label: t('files', 'Custom range'),
timeRange: [new Date(startOfLastWeek()), new Date(startOfToday())],
filter(time: number) {
if (!this.timeRange) {
return true
}
const timeValue = new Date(time).getTime()
return timeValue >= this.timeRange[0].getTime() && timeValue <= this.timeRange[1].getTime()
},
} satisfies ITimePreset & Record<string, unknown>,
]
</script>
<style scoped lang="scss">

View file

@ -4,44 +4,26 @@
-->
<template>
<NcButton v-show="isVisible" @click="onClick">
<NcButton v-if="isVisible" size="small" @click="onClick">
{{ t('files', 'Search everywhere') }}
</NcButton>
</template>
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import { ref } from 'vue'
import { computed } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import { getPinia } from '../../store/index.ts'
import { useSearchStore } from '../../store/search.ts'
const isVisible = ref(false)
const searchStore = useSearchStore(getPinia())
defineExpose({
hideButton,
showButton,
})
/**
* Hide the button - called by the filter class
*/
function hideButton() {
isVisible.value = false
}
/**
* Show the button - called by the filter class
*/
function showButton() {
isVisible.value = true
}
const isVisible = computed(() => searchStore.query.length >= 3 && searchStore.scope === 'filter')
/**
* Button click handler to make the filtering a global search.
*/
function onClick() {
const searchStore = useSearchStore(getPinia())
searchStore.scope = 'globally'
}
</script>

View file

@ -3,124 +3,164 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<FileListFilter
class="file-list-filter-type"
:is-active="isActive"
:filter-name="t('files', 'Type')"
@reset-filter="resetFilter">
<template #icon>
<NcIconSvgWrapper :path="mdiFileOutline" />
</template>
<NcActionButton
<div :class="$style.fileListFilterType">
<NcButton
v-for="fileType of typePresets"
:key="fileType.id"
type="checkbox"
:model-value="selectedOptions.includes(fileType)"
@click="toggleOption(fileType)">
:pressed="selectedOptions.includes(fileType)"
variant="tertiary"
alignment="start"
wide
@update:pressed="toggleOption(fileType, $event)">
<template #icon>
<NcIconSvgWrapper :svg="fileType.icon" />
</template>
{{ fileType.label }}
</NcActionButton>
</FileListFilter>
</NcButton>
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue'
import type { ITypePreset } from '../../filters/TypeFilter.ts'
<script setup lang="ts">
import type { ITypePreset, TypeFilter } from '../../filters/TypeFilter.ts'
import { mdiFileOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import svgDocument from '@mdi/svg/svg/file-document.svg?raw'
import svgPDF from '@mdi/svg/svg/file-pdf-box.svg?raw'
import svgPresentation from '@mdi/svg/svg/file-presentation-box.svg?raw'
import svgSpreadsheet from '@mdi/svg/svg/file-table-box.svg?raw'
import svgFolder from '@mdi/svg/svg/folder.svg?raw'
import svgImage from '@mdi/svg/svg/image.svg?raw'
import svgMovie from '@mdi/svg/svg/movie.svg?raw'
import svgAudio from '@mdi/svg/svg/music.svg?raw'
import { t } from '@nextcloud/l10n'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import FileListFilter from './FileListFilter.vue'
export default defineComponent({
name: 'FileListFilterType',
const props = defineProps<{
filter: TypeFilter
}>()
components: {
FileListFilter,
NcActionButton,
NcIconSvgWrapper,
},
props: {
presets: {
type: Array as PropType<ITypePreset[]>,
default: () => [],
},
typePresets: {
type: Array as PropType<ITypePreset[]>,
required: true,
},
},
setup() {
return {
mdiFileOutline,
t,
}
},
data() {
return {
selectedOptions: [] as ITypePreset[],
}
},
computed: {
isActive() {
return this.selectedOptions.length > 0
},
},
watch: {
/** Reset selected options if property is changed */
presets() {
this.selectedOptions = this.presets ?? []
},
selectedOptions(newValue, oldValue) {
if (this.selectedOptions.length === 0) {
if (oldValue.length !== 0) {
this.$emit('update:presets')
}
} else {
this.$emit('update:presets', this.selectedOptions)
}
},
},
mounted() {
this.selectedOptions = this.presets ?? []
},
methods: {
resetFilter() {
this.selectedOptions = []
},
/**
* Toggle option from selected option
*
* @param option The option to toggle
*/
toggleOption(option: ITypePreset) {
const idx = this.selectedOptions.indexOf(option)
if (idx !== -1) {
this.selectedOptions.splice(idx, 1)
} else {
this.selectedOptions.push(option)
}
},
},
const selectedOptions = ref<ITypePreset[]>([])
watch(selectedOptions, () => {
props.filter.setPresets([...selectedOptions.value])
})
onMounted(() => {
props.filter.addEventListener('reset', resetFilter)
props.filter.addEventListener('deselect', onDeselect)
selectedOptions.value = typePresets.filter(({ id }) => props.filter.presets.some((preset) => preset.id === id))
})
onUnmounted(() => {
props.filter.removeEventListener('reset', resetFilter)
props.filter.removeEventListener('deselect', onDeselect)
})
/**
* Handler for reset event from filter
*/
function resetFilter() {
selectedOptions.value = []
}
/**
* Handle deselect event from filter
*
* @param event - The custom event
*/
function onDeselect(event: CustomEvent<string>) {
const option = typePresets.find((preset) => preset.id === event.detail)
if (option) {
toggleOption(option, false)
}
}
/**
* Toggle option from selected option
*
* @param option The option to toggle
* @param selected Whether the option is selected or not
*/
function toggleOption(option: ITypePreset, selected: boolean) {
selectedOptions.value = selectedOptions.value.filter((o) => o.id !== option.id)
if (selected) {
selectedOptions.value.push(option)
}
}
</script>
<style>
.file-list-filter-type {
max-width: 220px;
<script lang="ts">
/**
* Available presets
*/
const typePresets = [
{
id: 'document',
label: t('files', 'Documents'),
icon: colorize(svgDocument, '#49abea'),
mime: ['x-office/document'],
},
{
id: 'spreadsheet',
label: t('files', 'Spreadsheets'),
icon: colorize(svgSpreadsheet, '#9abd4e'),
mime: ['x-office/spreadsheet'],
},
{
id: 'presentation',
label: t('files', 'Presentations'),
icon: colorize(svgPresentation, '#f0965f'),
mime: ['x-office/presentation'],
},
{
id: 'pdf',
label: t('files', 'PDFs'),
icon: colorize(svgPDF, '#dc5047'),
mime: ['application/pdf'],
},
{
id: 'folder',
label: t('files', 'Folders'),
icon: colorize(svgFolder, window.getComputedStyle(document.body).getPropertyValue('--color-primary-element')),
mime: ['httpd/unix-directory'],
},
{
id: 'audio',
label: t('files', 'Audio'),
icon: svgAudio,
mime: ['audio'],
},
{
id: 'image',
// TRANSLATORS: This is for filtering files, e.g. PNG or JPEG, so photos, drawings, or images in general
label: t('files', 'Images'),
icon: svgImage,
mime: ['image'],
},
{
id: 'video',
label: t('files', 'Videos'),
icon: svgMovie,
mime: ['video'],
},
] as ITypePreset[]
/**
* Helper to colorize an svg icon
*
* @param svg - the svg content
* @param color - the color to apply
*/
function colorize(svg: string, color: string) {
return svg.replace('<path ', `<path fill="${color}" `)
}
</script>
<style module>
.fileListFilterType {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
width: 100%;
}
</style>

View file

@ -0,0 +1,139 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { IFileListFilterWithUi } from '@nextcloud/files'
import { mdiArrowLeft, mdiFilterVariant } from '@mdi/js'
import { t } from '@nextcloud/l10n'
import { computed, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcPopover from '@nextcloud/vue/components/NcPopover'
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
import { useFiltersStore } from '../../store/filters.ts'
const filterStore = useFiltersStore()
const visualFilters = computed(() => filterStore.filtersWithUI)
const hasActiveFilters = computed(() => filterStore.activeChips.length > 0)
const selectedFilter = ref<IFileListFilterWithUi>()
const { isWide } = useFileListWidth()
const menuTriggerId = 'file-list-filters-menu-trigger'
const boundary = document.getElementById('app-content-vue')!
</script>
<template>
<div :class="$style.fileListFilters" data-test-id="files-list-filters">
<template v-if="isWide">
<NcPopover v-for="filter of visualFilters" :key="filter.id" :boundary="boundary">
<template #trigger>
<NcButton variant="tertiary">
<template #icon>
<NcIconSvgWrapper :svg="filter.iconSvgInline" />
</template>
{{ filter.displayName }}
</NcButton>
</template>
<template #default>
<div :class="$style.fileListFilters__popoverContainer">
<component :is="filter.tagName" :filter.prop="filter" />
</div>
</template>
</NcPopover>
</template>
<NcPopover
v-else
:boundary="boundary"
:popup-role="selectedFilter ? 'dialog' : 'menu'"
@update:shown="selectedFilter = undefined">
<template #trigger>
<NcButton
:id="menuTriggerId"
:aria-label="t('files', 'Filters')"
:pressed="hasActiveFilters"
variant="tertiary">
<template #icon>
<NcIconSvgWrapper :path="mdiFilterVariant" />
</template>
</NcButton>
</template>
<template #default>
<div v-if="selectedFilter" :class="$style.fileListFilters__popoverFilterView">
<NcButton wide variant="tertiary" @click="selectedFilter = undefined">
<template #icon>
<NcIconSvgWrapper directional :path="mdiArrowLeft" />
</template>
{{ t('files', 'Back to filters') }}
</NcButton>
<component :is="selectedFilter.tagName" :filter.prop="selectedFilter" />
</div>
<template v-else>
<ul :class="$style.fileListFilters__popoverContainer" :aria-labelledby="menuTriggerId" role="menu">
<li v-for="filter of visualFilters" :key="filter.id" role="presentation">
<NcButton
role="menuitem"
alignment="start"
variant="tertiary"
wide
@click="selectedFilter = filter">
<template #icon>
<NcIconSvgWrapper :svg="filter.iconSvgInline" />
</template>
{{ filter.displayName }}
</NcButton>
</li>
</ul>
</template>
</template>
</NcPopover>
</div>
</template>
<style module>
.fileListFilters {
display: flex;
flex-direction: row;
gap: var(--default-grid-baseline);
margin-inline-end: var(--default-grid-baseline);
height: 100%;
width: 100%;
}
.fileListFilters__popoverFilterView {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: calc(2 * var(--default-grid-baseline));
padding: calc(var(--default-grid-baseline) / 2);
min-width: calc(7 * var(--default-clickable-area));
}
.fileListFilters__popoverContainer {
box-sizing: border-box;
padding: calc(var(--default-grid-baseline) / 2);
min-width: calc(7 * var(--default-clickable-area));
}
.fileListFilters__filter {
display: flex;
align-items: start;
justify-content: start;
gap: calc(var(--default-grid-baseline, 4px) * 2);
> * {
flex: 0 1 fit-content;
}
}
.fileListFilters__active {
display: flex;
flex-direction: row;
gap: calc(var(--default-grid-baseline, 4px) * 2);
}
</style>

View file

@ -1,76 +0,0 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="file-list-filters">
<div class="file-list-filters__filter" data-cy-files-filters>
<span
v-for="filter of visualFilters"
:key="filter.id"
ref="filterElements" />
</div>
<ul v-if="activeChips.length > 0" class="file-list-filters__active" :aria-label="t('files', 'Active filters')">
<li v-for="(chip, index) of activeChips" :key="index">
<NcChip
:aria-label-close="t('files', 'Remove filter')"
:icon-svg="chip.icon"
:text="chip.text"
@close="chip.onclick">
<template v-if="chip.user" #icon>
<NcAvatar
disable-menu
hide-status
:size="24"
:user="chip.user" />
</template>
</NcChip>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import { computed, ref, watchEffect } from 'vue'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcChip from '@nextcloud/vue/components/NcChip'
import { useFiltersStore } from '../store/filters.ts'
const filterStore = useFiltersStore()
const visualFilters = computed(() => filterStore.filtersWithUI)
const activeChips = computed(() => filterStore.activeChips)
const filterElements = ref<HTMLElement[]>([])
watchEffect(() => {
filterElements.value
.forEach((el, index) => visualFilters.value[index].mount(el))
})
</script>
<style scoped lang="scss">
.file-list-filters {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
height: 100%;
width: 100%;
&__filter {
display: flex;
align-items: start;
justify-content: start;
gap: calc(var(--default-grid-baseline, 4px) * 2);
> * {
flex: 0 1 fit-content;
}
}
&__active {
display: flex;
flex-direction: row;
gap: calc(var(--default-grid-baseline, 4px) * 2);
}
}
</style>

View file

@ -48,111 +48,69 @@
</tr>
</template>
<script lang="ts">
import type { Node } from '@nextcloud/files'
import type { PropType } from 'vue'
<script setup lang="ts">
import type { IColumn, INode, IView } from '@nextcloud/files'
import { formatFileSize, View } from '@nextcloud/files'
import { translate } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { formatFileSize } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { computed } from 'vue'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useActiveStore } from '../store/active.ts'
export default defineComponent({
name: 'FilesListTableFooter',
const props = defineProps<{
/** The current view */
currentView: IView
props: {
currentView: {
type: View,
required: true,
},
/** Whether the mime column is available */
isMimeAvailable: boolean
isMimeAvailable: {
type: Boolean,
default: false,
},
/** Whether the mtime column is available */
isMtimeAvailable: boolean
isMtimeAvailable: {
type: Boolean,
default: false,
},
/** Whether the size column is available */
isSizeAvailable: boolean
isSizeAvailable: {
type: Boolean,
default: false,
},
/** The nodes to summarize */
nodes: INode[]
nodes: {
type: Array as PropType<Node[]>,
required: true,
},
/** Summary text */
summary: string
}>()
summary: {
type: String,
default: '',
},
const activeStore = useActiveStore()
const { isNarrow } = useFileListWidth()
filesListWidth: {
type: Number,
default: 0,
},
},
const currentFolder = computed(() => activeStore.activeFolder)
setup() {
const pathsStore = usePathsStore()
const filesStore = useFilesStore()
const { directory } = useRouteParameters()
return {
filesStore,
pathsStore,
directory,
}
},
computed: {
currentFolder() {
if (!this.currentView?.id) {
return
}
if (this.directory === '/') {
return this.filesStore.getRoot(this.currentView.id)
}
const fileId = this.pathsStore.getPath(this.currentView.id, this.directory)!
return this.filesStore.getNode(fileId)
},
columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512) {
return []
}
return this.currentView?.columns || []
},
totalSize() {
// If we have the size already, let's use it
if (this.currentFolder?.size) {
return formatFileSize(this.currentFolder.size, true)
}
// Otherwise let's compute it
return formatFileSize(this.nodes.reduce((total, node) => total + (node.size ?? 0), 0), true)
},
},
methods: {
classForColumn(column) {
return {
'files-list__row-column-custom': true,
[`files-list__row-${this.currentView.id}-${column.id}`]: true,
}
},
t: translate,
},
const columns = computed(() => {
// Hide columns if the list is too small
if (isNarrow.value) {
return []
}
return props.currentView?.columns || []
})
const totalSize = computed(() => {
// If we have the size already, let's use it
if (currentFolder.value?.size) {
return formatFileSize(currentFolder.value.size, true)
}
// Otherwise let's compute it
return formatFileSize(props.nodes.reduce((total, node) => total + (node.size ?? 0), 0), true)
})
/**
* Get the CSS classes for a custom column
*
* @param column - The column
*/
function classForColumn(column: IColumn) {
return {
'files-list__row-column-custom': true,
[`files-list__row-${props.currentView.id}-${column.id}`]: true,
}
}
</script>
<style scoped lang="scss">

View file

@ -10,7 +10,7 @@
<NcCheckboxRadioSwitch
v-bind="selectAllBind"
data-cy-files-list-selection-checkbox
@update:modelValue="onToggleAll" />
@update:model-value="onToggleAll" />
</th>
<!-- Columns display -->
@ -80,6 +80,7 @@ import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { defineComponent } from 'vue'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import logger from '../logger.ts'
import filesSortingMixin from '../mixins/filesSorting.ts'
@ -119,11 +120,6 @@ export default defineComponent({
type: Array as PropType<Node[]>,
required: true,
},
filesListWidth: {
type: Number,
default: 0,
},
},
setup() {
@ -132,19 +128,22 @@ export default defineComponent({
const selectionStore = useSelectionStore()
const { directory } = useRouteParameters()
const { isNarrow } = useFileListWidth()
return {
activeStore,
filesStore,
selectionStore,
directory,
isNarrow,
}
},
computed: {
columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512) {
if (this.isNarrow) {
return []
}
return this.activeStore.activeView?.columns || []

View file

@ -77,7 +77,7 @@ import type { FileSource } from '../types.ts'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { DefaultType, getFileActions, NodeStatus } from '@nextcloud/files'
import { translate } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { computed, defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
@ -126,18 +126,28 @@ export default defineComponent({
const actionsMenuStore = useActionsMenuStore()
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
const fileListWidth = useFileListWidth()
const { isMedium, isNarrow } = useFileListWidth()
const boundariesElement = document.getElementById('app-content-vue')
const inlineActions = computed(() => {
if (isNarrow.value) {
return 0
}
if (isMedium.value) {
return 1
}
return 3
})
return {
actionsMenuStore,
activeFolder,
fileListWidth,
filesStore,
selectionStore,
boundariesElement,
inlineActions,
}
},
@ -256,19 +266,6 @@ export default defineComponent({
this.actionsMenuStore.opened = opened ? 'global' : null
},
},
inlineActions() {
if (this.fileListWidth < 512) {
return 0
}
if (this.fileListWidth < 768) {
return 1
}
if (this.fileListWidth < 1024) {
return 2
}
return 3
},
},
methods: {

View file

@ -17,9 +17,8 @@
}"
:scroll-to-index="scrollToIndex"
:caption="caption">
<template #filters>
<FileListFilters />
</template>
<!-- eslint-disable-next-line vue/singleline-html-element-content-newline -- no space allowed as otherwise `:empty` css selector does not trigger! -->
<template #filters><FileListFilterToSearch /><FileListFilterChips /></template>
<template v-if="!isNoneSelected" #header-overlay>
<span class="files-list__selected">
@ -45,7 +44,6 @@
<!-- Table header and sort buttons -->
<FilesListTableHeader
ref="thead"
:files-list-width="fileListWidth"
:is-mime-available="isMimeAvailable"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
@ -61,7 +59,6 @@
<template #footer>
<FilesListTableFooter
:current-view="currentView"
:files-list-width="fileListWidth"
:is-mime-available="isMimeAvailable"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
@ -72,7 +69,7 @@
</template>
<script lang="ts">
import type { Node as NcNode } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { ComponentPublicInstance, PropType } from 'vue'
import type { UserConfig } from '../types.ts'
@ -80,10 +77,11 @@ import { showError } from '@nextcloud/dialogs'
import { FileType, Folder, getFileActions, getSidebar, Permission, View } from '@nextcloud/files'
import { n, t } from '@nextcloud/l10n'
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { defineComponent } from 'vue'
import { computed, defineComponent } from 'vue'
import FileEntry from './FileEntry.vue'
import FileEntryGrid from './FileEntryGrid.vue'
import FileListFilters from './FileListFilters.vue'
import FileListFilterChips from './FileListFilter/FileListFilterChips.vue'
import FileListFilterToSearch from './FileListFilter/FileListFilterToSearch.vue'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
@ -94,14 +92,15 @@ import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import logger from '../logger.ts'
import { useActiveStore } from '../store/active.ts'
import { useSelectionStore } from '../store/selection.js'
import { useSelectionStore } from '../store/selection.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
export default defineComponent({
name: 'FilesListVirtual',
components: {
FileListFilters,
FileListFilterChips,
FileListFilterToSearch,
FilesListHeader,
FilesListTableFooter,
FilesListTableHeader,
@ -121,7 +120,7 @@ export default defineComponent({
},
nodes: {
type: Array as PropType<NcNode[]>,
type: Array as PropType<INode[]>,
required: true,
},
@ -131,19 +130,48 @@ export default defineComponent({
},
},
setup() {
setup(props) {
const sidebar = getSidebar()
const activeStore = useActiveStore()
const selectionStore = useSelectionStore()
const userConfigStore = useUserConfigStore()
const fileListWidth = useFileListWidth()
const { isNarrow, isWide } = useFileListWidth()
const { fileId, openDetails, openFile } = useRouteParameters()
const isMimeAvailable = computed(() => {
if (!userConfigStore.userConfig.show_mime_column) {
return false
}
if (!isWide.value) {
return false // only show on wide screens
}
return props.nodes
.some((node: INode) => node.mime !== undefined || node.mime !== 'application/octet-stream')
})
const isMtimeAvailable = computed(() => {
// Hide mtime column on narrow screens
if (isNarrow.value) {
return false // hide on narrow screens
}
return props.nodes.some((node: INode) => node.mtime !== undefined)
})
const isSizeAvailable = computed(() => {
// Hide size column on narrow screens
if (isNarrow.value) {
return false // hide on narrow screens
}
return props.nodes.some((node: INode) => node.size !== undefined)
})
return {
fileId,
fileListWidth,
headers: useFileListHeaders(),
isSizeAvailable,
isMtimeAvailable,
isMimeAvailable,
openDetails,
openFile,
@ -170,33 +198,6 @@ export default defineComponent({
return this.userConfigStore.userConfig
},
isMimeAvailable() {
if (!this.userConfig.show_mime_column) {
return false
}
// Hide mime column on narrow screens
if (this.fileListWidth < 1024) {
return false
}
return this.nodes.some((node) => node.mime !== undefined || node.mime !== 'application/octet-stream')
},
isMtimeAvailable() {
// Hide mtime column on narrow screens
if (this.fileListWidth < 768) {
return false
}
return this.nodes.some((node) => node.mtime !== undefined)
},
isSizeAvailable() {
// Hide size column on narrow screens
if (this.fileListWidth < 768) {
return false
}
return this.nodes.some((node) => node.size !== undefined)
},
cantUpload() {
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) === 0
},
@ -312,7 +313,7 @@ export default defineComponent({
openSidebarForFile(fileId) {
// Open the sidebar for the given URL fileid
// iif we just loaded the app.
const node = this.nodes.find((n) => n.fileid === fileId) as NcNode
const node = this.nodes.find((n) => n.fileid === fileId) as INode
if (node && this.sidebar.available) {
logger.debug('Opening sidebar on file ' + node.path, { node })
this.sidebar.open(node)
@ -355,7 +356,7 @@ export default defineComponent({
* @param fileId File to open
*/
async handleOpenFile(fileId: number) {
const node = this.nodes.find((n) => n.fileid === fileId) as NcNode
const node = this.nodes.find((n) => n.fileid === fileId) as INode
if (node === undefined) {
return
}
@ -445,7 +446,7 @@ export default defineComponent({
const index = event.key === 'ArrowUp' || event.key === 'ArrowLeft'
? this.nodes.length - 1
: 0
this.setActiveNode(this.nodes[index] as NcNode & { fileid: number })
this.setActiveNode(this.nodes[index] as INode & { fileid: number })
}
const index = this.nodes.findIndex((node) => node.fileid === this.fileId) ?? 0
@ -481,7 +482,7 @@ export default defineComponent({
}
},
async setActiveNode(node: NcNode & { fileid: number }) {
async setActiveNode(node: INode & { fileid: number }) {
logger.debug('Navigating to file ' + node.path, { node, fileid: node.fileid })
this.scrollToFile(node.fileid)
@ -511,15 +512,15 @@ export default defineComponent({
--clickable-area: var(--default-clickable-area);
--icon-preview-size: 24px;
--fixed-block-start-position: var(--default-clickable-area);
--fixed-block-start-position: calc(var(--clickable-area-small) + var(--default-grid-baseline, 4px));
display: flex;
flex-direction: column;
overflow: auto;
height: 100%;
will-change: scroll-position;
&:has(.file-list-filters__active) {
--fixed-block-start-position: calc(var(--default-clickable-area) + var(--default-grid-baseline) + var(--clickable-area-small));
&:has(&__filters:empty) {
--fixed-block-start-position: 0px;
}
& :deep() {
@ -572,6 +573,10 @@ export default defineComponent({
}
.files-list__filters {
display: flex;
gap: var(--default-grid-baseline);
box-sizing: border-box;
// Pinned on top when scrolling above table header
position: sticky;
top: 0;
@ -582,6 +587,10 @@ export default defineComponent({
padding-inline: var(--row-height) var(--default-grid-baseline, 4px);
height: var(--fixed-block-start-position);
width: 100%;
&:not(:empty) {
padding-block: calc(var(--default-grid-baseline, 4px) / 2);
}
}
.files-list__thead-overlay {

View file

@ -131,7 +131,7 @@ export default defineComponent({
},
setup() {
const fileListWidth = useFileListWidth()
const { width: fileListWidth } = useFileListWidth()
return {
fileListWidth,

View file

@ -51,7 +51,7 @@ async function getFileList() {
template: '<div data-testid="component" style="width: 100%;background: white;">{{ fileListWidth }}</div>',
setup() {
return {
fileListWidth: useFileListWidth(),
fileListWidth: useFileListWidth().width,
}
},
})

View file

@ -2,9 +2,8 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Ref } from 'vue'
import { onMounted, readonly, ref } from 'vue'
import { computed, onMounted, readonly, ref } from 'vue'
/** The element we observe */
let element: HTMLElement | undefined
@ -12,13 +11,22 @@ let element: HTMLElement | undefined
/** The current width of the element */
const width = ref(0)
const observer = new ResizeObserver((elements) => {
if (elements[0].contentBoxSize) {
const isWide = computed(() => width.value >= 1024)
const isMedium = computed(() => width.value >= 512 && width.value < 1024)
const isNarrow = computed(() => width.value < 512)
const observer = new ResizeObserver(([element]) => {
if (!element) {
return
}
const contentBoxSize = element.contentBoxSize?.[0]
if (contentBoxSize) {
// use the newer `contentBoxSize` property if available
width.value = elements[0].contentBoxSize[0].inlineSize
width.value = contentBoxSize.inlineSize
} else {
// fall back to `contentRect`
width.value = elements[0].contentRect.width
width.value = element.contentRect.width
}
})
@ -41,11 +49,17 @@ function updateObserver() {
/**
* Get the reactive width of the file list
*/
export function useFileListWidth(): Readonly<Ref<number>> {
export function useFileListWidth() {
// Update the observer when the component is mounted (e.g. because this is the files app)
onMounted(updateObserver)
// Update the observer also in setup context, so we already have an initial value
updateObserver()
return readonly(width)
return {
width: readonly(width),
isWide,
isMedium,
isNarrow,
}
}

View file

@ -2,11 +2,13 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileListFilterChip, INode } from '@nextcloud/files'
import calendarSvg from '@mdi/svg/svg/calendar.svg?raw'
import type { IFileListFilterChip, IFileListFilterWithUi, INode } from '@nextcloud/files'
import svgCalendarRangeOutline from '@mdi/svg/svg/calendar-range-outline.svg?raw'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import wrap from '@vue/web-component-wrapper'
import Vue from 'vue'
import FileListFilterModified from '../components/FileListFilter/FileListFilterModified.vue'
@ -16,63 +18,20 @@ export interface ITimePreset {
filter: (time: number) => boolean
}
const startOfToday = () => (new Date()).setHours(0, 0, 0, 0)
const tagName = 'files-file-list-filter-modified'
/**
* Available presets
*/
const timePresets: ITimePreset[] = [
{
id: 'today',
label: t('files', 'Today'),
filter: (time: number) => time > startOfToday(),
},
{
id: 'last-7',
label: t('files', 'Last 7 days'),
filter: (time: number) => time > (startOfToday() - (7 * 24 * 60 * 60 * 1000)),
},
{
id: 'last-30',
label: t('files', 'Last 30 days'),
filter: (time: number) => time > (startOfToday() - (30 * 24 * 60 * 60 * 1000)),
},
{
id: 'this-year',
label: t('files', 'This year ({year})', { year: (new Date()).getFullYear() }),
filter: (time: number) => time > (new Date(startOfToday())).setMonth(0, 1),
},
{
id: 'last-year',
label: t('files', 'Last year ({year})', { year: (new Date()).getFullYear() - 1 }),
filter: (time: number) => (time > (new Date(startOfToday())).setFullYear((new Date()).getFullYear() - 1, 0, 1)) && (time < (new Date(startOfToday())).setMonth(0, 1)),
},
] as const
class ModifiedFilter extends FileListFilter {
class ModifiedFilter extends FileListFilter implements IFileListFilterWithUi {
private currentInstance?: Vue
private currentPreset?: ITimePreset
public readonly displayName = t('files', 'Modified')
public readonly iconSvgInline = svgCalendarRangeOutline
public readonly tagName = tagName
constructor() {
super('files:modified', 50)
}
public mount(el: HTMLElement) {
if (this.currentInstance) {
this.currentInstance.$destroy()
}
const View = Vue.extend(FileListFilterModified as never)
this.currentInstance = new View({
propsData: {
timePresets,
},
el,
})
.$on('update:preset', this.setPreset.bind(this))
.$mount()
}
public filter(nodes: INode[]): INode[] {
if (!this.currentPreset) {
return nodes
@ -82,7 +41,11 @@ class ModifiedFilter extends FileListFilter {
}
public reset(): void {
this.setPreset()
this.dispatchEvent(new CustomEvent('reset'))
}
public get preset() {
return this.currentPreset
}
public setPreset(preset?: ITimePreset) {
@ -92,9 +55,9 @@ class ModifiedFilter extends FileListFilter {
const chips: IFileListFilterChip[] = []
if (preset) {
chips.push({
icon: calendarSvg,
icon: svgCalendarRangeOutline,
text: preset.label,
onclick: () => this.setPreset(),
onclick: () => this.reset(),
})
} else {
(this.currentInstance as { resetFilter: () => void } | undefined)?.resetFilter()
@ -103,9 +66,26 @@ class ModifiedFilter extends FileListFilter {
}
}
export type { ModifiedFilter }
/**
* Register the file list filter by modification date
*/
export function registerModifiedFilter() {
const WrappedComponent = wrap(Vue, FileListFilterModified)
// In Vue 2, wrap doesn't support disabling shadow :(
// Disable with a hack
Object.defineProperty(WrappedComponent.prototype, 'attachShadow', {
value() {
return this
},
})
Object.defineProperty(WrappedComponent.prototype, 'shadowRoot', {
get() {
return this
},
})
customElements.define(tagName, WrappedComponent)
registerFileListFilter(new ModifiedFilter())
}

View file

@ -1,47 +0,0 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { INode } from '@nextcloud/files'
import type { ComponentPublicInstance } from 'vue'
import { subscribe } from '@nextcloud/event-bus'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import Vue from 'vue'
import FileListFilterToSearch from '../components/FileListFilter/FileListFilterToSearch.vue'
class SearchFilter extends FileListFilter {
private currentInstance?: ComponentPublicInstance<typeof FileListFilterToSearch>
constructor() {
super('files:filter-to-search', 999)
subscribe('files:search:updated', ({ query, scope }) => {
if (query && scope === 'filter') {
this.currentInstance?.showButton()
} else {
this.currentInstance?.hideButton()
}
})
}
public mount(el: HTMLElement) {
if (this.currentInstance) {
this.currentInstance.$destroy()
}
const View = Vue.extend(FileListFilterToSearch)
this.currentInstance = new View().$mount(el) as unknown as ComponentPublicInstance<typeof FileListFilterToSearch>
}
public filter(nodes: INode[]): INode[] {
return nodes
}
}
/**
* Register a file list filter to only show hidden files if enabled by user config
*/
export function registerFilterToSearchToggle() {
registerFileListFilter(new SearchFilter())
}

View file

@ -2,21 +2,16 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileListFilterChip, INode } from '@nextcloud/files'
// TODO: Create a modern replacement for OC.MimeType...
import svgDocument from '@mdi/svg/svg/file-document.svg?raw'
import svgPDF from '@mdi/svg/svg/file-pdf-box.svg?raw'
import svgPresentation from '@mdi/svg/svg/file-presentation-box.svg?raw'
import svgSpreadsheet from '@mdi/svg/svg/file-table-box.svg?raw'
import svgFolder from '@mdi/svg/svg/folder.svg?raw'
import svgImage from '@mdi/svg/svg/image.svg?raw'
import svgMovie from '@mdi/svg/svg/movie.svg?raw'
import svgAudio from '@mdi/svg/svg/music.svg?raw'
import type { IFileListFilterChip, IFileListFilterWithUi, INode } from '@nextcloud/files'
import svgFileOutline from '@mdi/svg/svg/file-outline.svg?raw'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import wrap from '@vue/web-component-wrapper'
import Vue from 'vue'
import FileListFilterType from '../components/FileListFilter/FileListFilterType.vue'
import logger from '../logger.ts'
export interface ITypePreset {
id: string
@ -25,106 +20,21 @@ export interface ITypePreset {
mime: string[]
}
/**
*
* @param svg
* @param color
*/
function colorize(svg: string, color: string) {
return svg.replace('<path ', `<path fill="${color}" `)
}
const tagName = 'files-file-list-filter-type'
/**
* Available presets
*/
async function getTypePresets() {
return [
{
id: 'document',
label: t('files', 'Documents'),
icon: colorize(svgDocument, '#49abea'),
mime: ['x-office/document'],
},
{
id: 'spreadsheet',
label: t('files', 'Spreadsheets'),
icon: colorize(svgSpreadsheet, '#9abd4e'),
mime: ['x-office/spreadsheet'],
},
{
id: 'presentation',
label: t('files', 'Presentations'),
icon: colorize(svgPresentation, '#f0965f'),
mime: ['x-office/presentation'],
},
{
id: 'pdf',
label: t('files', 'PDFs'),
icon: colorize(svgPDF, '#dc5047'),
mime: ['application/pdf'],
},
{
id: 'folder',
label: t('files', 'Folders'),
icon: colorize(svgFolder, window.getComputedStyle(document.body).getPropertyValue('--color-primary-element')),
mime: ['httpd/unix-directory'],
},
{
id: 'audio',
label: t('files', 'Audio'),
icon: svgAudio,
mime: ['audio'],
},
{
id: 'image',
// TRANSLATORS: This is for filtering files, e.g. PNG or JPEG, so photos, drawings, or images in general
label: t('files', 'Images'),
icon: svgImage,
mime: ['image'],
},
{
id: 'video',
label: t('files', 'Videos'),
icon: svgMovie,
mime: ['video'],
},
] as ITypePreset[]
}
class TypeFilter extends FileListFilter {
class TypeFilter extends FileListFilter implements IFileListFilterWithUi {
private currentInstance?: Vue
private currentPresets: ITypePreset[]
private allPresets?: ITypePreset[]
public readonly displayName = t('files', 'Type')
public readonly iconSvgInline = svgFileOutline
public readonly tagName = tagName
constructor() {
super('files:type', 10)
this.currentPresets = []
}
public async mount(el: HTMLElement) {
// We need to defer this as on init script this is not available:
if (this.allPresets === undefined) {
this.allPresets = await getTypePresets()
}
// Already mounted
if (this.currentInstance) {
this.currentInstance.$destroy()
delete this.currentInstance
}
const View = Vue.extend(FileListFilterType as never)
this.currentInstance = new View({
propsData: {
presets: this.currentPresets,
typePresets: this.allPresets!,
},
el,
})
.$on('update:presets', this.setPresets.bind(this))
.$mount()
}
public filter(nodes: INode[]): INode[] {
if (!this.currentPresets || this.currentPresets.length === 0) {
return nodes
@ -149,10 +59,17 @@ class TypeFilter extends FileListFilter {
}
public reset(): void {
this.setPresets()
// to be listener by the component
this.dispatchEvent(new CustomEvent('reset'))
}
public get presets(): ITypePreset[] {
return this.currentPresets
}
public setPresets(presets?: ITypePreset[]) {
logger.debug('TypeFilter: setting presets', { presets })
this.currentPresets = presets ?? []
if (this.currentInstance !== undefined) {
// could be called before the instance was created
@ -185,13 +102,31 @@ class TypeFilter extends FileListFilter {
*/
private removeFilterPreset(presetId: string) {
const filtered = this.currentPresets.filter(({ id }) => id !== presetId)
this.dispatchEvent(new CustomEvent('deselect', { detail: presetId }))
this.setPresets(filtered)
}
}
export type { TypeFilter }
/**
* Register the file list filter by file type
*/
export function registerTypeFilter() {
const WrappedComponent = wrap(Vue, FileListFilterType)
// In Vue 2, wrap doesn't support disabling shadow :(
// Disable with a hack
Object.defineProperty(WrappedComponent.prototype, 'attachShadow', {
value() {
return this
},
})
Object.defineProperty(WrappedComponent.prototype, 'shadowRoot', {
get() {
return this
},
})
window.customElements.define(tagName, WrappedComponent)
registerFileListFilter(new TypeFilter())
}

View file

@ -21,7 +21,6 @@ import { action as viewInFolderAction } from './actions/viewInFolderAction.ts'
import { registerFilenameFilter } from './filters/FilenameFilter.ts'
import { registerHiddenFilesFilter } from './filters/HiddenFilesFilter.ts'
import { registerModifiedFilter } from './filters/ModifiedFilter.ts'
import { registerFilterToSearchToggle } from './filters/SearchFilter.ts'
import { registerTypeFilter } from './filters/TypeFilter.ts'
import { entry as newFolderEntry } from './newMenu/newFolder.ts'
import { registerTemplateEntries } from './newMenu/newFromTemplate.ts'
@ -68,7 +67,6 @@ registerHiddenFilesFilter()
registerTypeFilter()
registerModifiedFilter()
registerFilenameFilter()
registerFilterToSearchToggle()
// Register sidebar action
registerSidebarFavoriteAction()

9
apps/files/src/shims.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
declare module '*.svg?raw' {
const content: string
export default content
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FilterUpdateChipsEvent, IFileListFilter, IFileListFilterChip } from '@nextcloud/files'
import type { FilterUpdateChipsEvent, IFileListFilter, IFileListFilterChip, IFileListFilterWithUi } from '@nextcloud/files'
import { emit, subscribe } from '@nextcloud/event-bus'
import { getFileListFilters } from '@nextcloud/files'
@ -16,8 +16,8 @@ import logger from '../logger.ts'
*
* @param value The filter to check
*/
function isFileListFilterWithUi(value: IFileListFilter): value is Required<IFileListFilter> {
return 'mount' in value
function isFileListFilterWithUi(value: IFileListFilter): value is IFileListFilterWithUi {
return 'tagName' in value
}
export const useFiltersStore = defineStore('filters', () => {
@ -37,7 +37,7 @@ export const useFiltersStore = defineStore('filters', () => {
/**
* All filters that provide a UI for visual controlling the filter state
*/
const filtersWithUI = computed<Required<IFileListFilter>[]>(() => sortedFilters.value.filter(isFileListFilterWithUi))
const filtersWithUI = computed<IFileListFilterWithUi[]>(() => sortedFilters.value.filter(isFileListFilterWithUi))
/**
* Register a new filter on the store.

View file

@ -5,42 +5,33 @@
<template>
<NcAppContent :page-heading="pageHeading" data-cy-files-content>
<div class="files-list__header" :class="{ 'files-list__header--public': isPublic }">
<!-- Uploader -->
<component :is="isNarrow ? 'Teleport' : 'div'" :to="isNarrow ? 'body' : undefined">
<UploadPicker
v-if="canUpload && !isQuotaExceeded && currentFolder"
allow-folders
:no-label="isNarrow"
class="files-list__header-upload-button"
:class="{ 'files-list__header-upload-button--narrow': isNarrow }"
:content="getContent"
:destination="currentFolder"
:forbidden-characters="forbiddenCharacters"
multiple
primary
@failed="onUploadFail"
@uploaded="onUpload" />
</component>
<!-- Current folder breadcrumbs -->
<BreadCrumbs :path="directory" @reload="fetchContent">
<template #actions>
<!-- Sharing button -->
<NcButton
v-if="canShare && fileListWidth >= 512"
:aria-label="shareButtonLabel"
:class="{ 'files-list__header-share-button--shared': shareButtonType }"
:title="shareButtonLabel"
class="files-list__header-share-button"
variant="tertiary"
@click="openSharingSidebar">
<template #icon>
<LinkIcon v-if="shareButtonType === ShareType.Link" />
<AccountPlusIcon v-else :size="20" />
</template>
</NcButton>
<BreadCrumbs :path="directory" @reload="fetchContent" />
<!-- Uploader -->
<UploadPicker
v-if="canUpload && !isQuotaExceeded && currentFolder"
allow-folders
:no-label="fileListWidth <= 511"
class="files-list__header-upload-button"
:content="getContent"
:destination="currentFolder"
:forbidden-characters="forbiddenCharacters"
multiple
@failed="onUploadFail"
@uploaded="onUpload" />
</template>
</BreadCrumbs>
<!-- Secondary loading indicator -->
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" />
<!-- Loading indicator -->
<NcLoadingIcon
v-if="isRefreshing"
class="files-list__refresh-icon"
:name="t('files', 'File list is reloading')" />
<!-- File list actions (global actions like restore all files from trashbin) -->
<NcActions
class="files-list__header-actions"
:inline="1"
@ -63,6 +54,10 @@
</NcActionButton>
</NcActions>
<!-- Filters thats can be applied to the file list -->
<FileListFilters />
<!-- Grid view toggle -->
<NcButton
v-if="enableGridView"
:aria-label="gridViewButtonLabel"
@ -166,7 +161,6 @@ import type { Route } from 'vue-router'
import type { UserConfig } from '../types.ts'
import { getCurrentUser } from '@nextcloud/auth'
import { getCapabilities } from '@nextcloud/capabilities'
import { showError, showSuccess, showWarning } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Folder, getFileListActions, Permission, sortNodes } from '@nextcloud/files'
@ -179,6 +173,7 @@ import { UploadPicker, UploadStatus } from '@nextcloud/upload'
import { useThrottleFn } from '@vueuse/core'
import { normalize, relative } from 'path'
import { computed, defineComponent } from 'vue'
import Teleport from 'vue2-teleport' // TODO: replace with native Vue Teleport when we switch to Vue 3
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
@ -186,14 +181,13 @@ import NcButton from '@nextcloud/vue/components/NcButton'
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/AccountPlusOutline.vue'
import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
import ListViewIcon from 'vue-material-design-icons/FormatListBulletedSquare.vue'
import LinkIcon from 'vue-material-design-icons/Link.vue'
import IconReload from 'vue-material-design-icons/Reload.vue'
import ViewGridIcon from 'vue-material-design-icons/ViewGridOutline.vue'
import BreadCrumbs from '../components/BreadCrumbs.vue'
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
import FileListFilters from '../components/FileListFilter/FileListFilters.vue'
import FilesListVirtual from '../components/FilesListVirtual.vue'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
@ -212,16 +206,14 @@ import { humanizeWebDAVError } from '../utils/davUtils.ts'
import { defaultView } from '../utils/filesViews.ts'
import { getSummaryFor } from '../utils/fileUtils.ts'
const isSharingEnabled = (getCapabilities() as { files_sharing?: boolean })?.files_sharing !== undefined
export default defineComponent({
name: 'FilesList',
components: {
BreadCrumbs,
DragAndDropNotice,
FileListFilters,
FilesListVirtual,
LinkIcon,
ListViewIcon,
NcAppContent,
NcActions,
@ -230,7 +222,7 @@ export default defineComponent({
NcEmptyContent,
NcIconSvgWrapper,
NcLoadingIcon,
AccountPlusIcon,
Teleport,
UploadPicker,
ViewGridIcon,
IconAlertCircleOutline,
@ -259,7 +251,7 @@ export default defineComponent({
const userConfigStore = useUserConfigStore()
const viewConfigStore = useViewConfigStore()
const fileListWidth = useFileListWidth()
const { isNarrow } = useFileListWidth()
const { directory, fileId } = useRouteParameters()
const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true)
@ -271,7 +263,7 @@ export default defineComponent({
currentView,
directory,
fileId,
fileListWidth,
isNarrow,
t,
sidebar,
@ -432,37 +424,6 @@ export default defineComponent({
return { ...this.$route, query: { dir } }
},
shareTypesAttributes(): number[] | undefined {
if (!this.currentFolder?.attributes?.['share-types']) {
return undefined
}
return Object.values(this.currentFolder?.attributes?.['share-types'] || {}).flat() as number[]
},
shareButtonLabel() {
if (!this.shareTypesAttributes) {
return t('files', 'Share')
}
if (this.shareButtonType === ShareType.Link) {
return t('files', 'Shared by link')
}
return t('files', 'Shared')
},
shareButtonType(): ShareType | null {
if (!this.shareTypesAttributes) {
return null
}
// If all types are links, show the link icon
if (this.shareTypesAttributes.some((type) => type === ShareType.Link)) {
return ShareType.Link
}
return ShareType.User
},
gridViewButtonLabel() {
return this.userConfig.grid_view
? t('files', 'Switch to list view')
@ -480,14 +441,6 @@ export default defineComponent({
return this.currentFolder?.attributes?.['quota-available-bytes'] === 0
},
/**
* Check if current folder has share permissions
*/
canShare() {
return isSharingEnabled && !this.isPublic
&& this.currentFolder && (this.currentFolder.permissions & Permission.SHARE) !== 0
},
showCustomEmptyView() {
return !this.loading && this.isEmptyDir && this.currentView?.emptyView !== undefined
},
@ -759,15 +712,6 @@ export default defineComponent({
}
},
openSharingSidebar() {
if (!this.currentFolder) {
logger.debug('No current folder found for opening sharing sidebar')
return
}
this.sidebar.open(this.currentFolder, 'sharing')
},
toggleGridView() {
this.userConfigStore.update('grid_view', !this.userConfig.grid_view)
},
@ -842,13 +786,14 @@ export default defineComponent({
.files-list {
&__header {
display: flex;
gap: var(--default-grid-baseline);
align-items: center;
// Do not grow or shrink (vertically)
flex: 0 0;
max-width: 100%;
// Align with the navigation toggle icon
margin-block: var(--app-navigation-padding, 4px);
margin-inline: calc(var(--default-clickable-area, 44px) + 2 * var(--app-navigation-padding, 4px)) var(--app-navigation-padding, 4px);
margin-inline: calc(var(--default-clickable-area) + 2 * var(--app-navigation-padding, 4px)) var(--app-navigation-padding, 4px);
&--public {
// There is no navigation toggle on public shares
@ -861,20 +806,18 @@ export default defineComponent({
flex: 0 0;
}
&-share-button {
color: var(--color-text-maxcontrast) !important;
&--shared {
color: var(--color-main-text) !important;
}
}
&-actions {
min-width: fit-content !important;
margin-inline: calc(var(--default-grid-baseline) * 2);
}
}
&__header-upload-button--narrow {
// this is teleported to body on narrow screens
position: fixed;
inset-block-end: calc(1.5 * var(--default-grid-baseline));
inset-inline-end: calc(1.5 * var(--default-grid-baseline));
}
&__before {
display: flex;
flex-direction: column;

View file

@ -3,53 +3,45 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<FileListFilter
class="file-list-filter-accounts"
:is-active="selectedAccounts.length > 0"
:filter-name="t('files_sharing', 'People')"
@reset-filter="resetFilter">
<template #icon>
<NcIconSvgWrapper :path="mdiAccountMultipleOutline" />
</template>
<NcActionInput
<div :class="$style.fileListFilterAccount">
<NcTextField
v-if="availableAccounts.length > 1"
v-model="accountFilter"
:label="t('files_sharing', 'Filter accounts')"
:label-outside="false"
:show-trailing-button="false"
type="search" />
<NcActionButton
type="search"
:label="t('files_sharing', 'Filter accounts')" />
<NcButton
v-for="account of shownAccounts"
:key="account.id"
class="file-list-filter-accounts__item"
type="radio"
:model-value="selectedAccounts.includes(account)"
:value="account.id"
@click="toggleAccount(account.id)">
alignment="start"
:pressed="selectedAccounts.includes(account)"
variant="tertiary"
wide
@update:pressed="toggleAccount(account.id, $event)">
<template #icon>
<NcAvatar
class="file-list-filter-accounts__avatar"
:class="$style.fileListFilterAccount__avatar"
v-bind="account"
:size="24"
disable-menu
hide-status />
</template>
{{ account.displayName }}
</NcActionButton>
</FileListFilter>
<span v-if="account.id === currentUserId" :class="$style.fileListFilterAccount__currentUser">
({{ t('files', 'you') }})
</span>
</NcButton>
</div>
</template>
<script setup lang="ts">
import type { IAccountData } from '../files_filters/AccountFilter.ts'
import type { AccountFilter, IAccountData } from '../files_filters/AccountFilter.ts'
import { mdiAccountMultipleOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { computed, ref, watch } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
import { t } from '@nextcloud/l10n'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import FileListFilter from '../../../files/src/components/FileListFilter/FileListFilter.vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import { getCurrentUser } from '../../../../core/src/OC/currentuser.js'
interface IUserSelectData {
id: string
@ -57,48 +49,88 @@ interface IUserSelectData {
displayName: string
}
const emit = defineEmits<{
(event: 'update:accounts', value: IAccountData[]): void
const props = defineProps<{
filter: AccountFilter
}>()
const currentUserId = getCurrentUser()!.uid
const accountFilter = ref('')
const availableAccounts = ref<IUserSelectData[]>([])
const selectedAccounts = ref<IUserSelectData[]>([])
watch(selectedAccounts, () => {
const accounts = selectedAccounts.value.map(({ id: uid, displayName }) => ({ uid, displayName }))
props.filter.setAccounts(accounts.length > 0 ? accounts : undefined)
})
onMounted(() => {
setAvailableAccounts(props.filter.availableAccounts)
selectedAccounts.value = availableAccounts.value.filter(({ id }) => props.filter.filterAccounts?.some(({ uid }) => uid === id)) ?? []
props.filter.addEventListener('accounts-updated', setAvailableAccounts)
props.filter.addEventListener('reset', resetFilter)
props.filter.addEventListener('deselect', deselect)
})
onUnmounted(() => {
props.filter.removeEventListener('accounts-updated', setAvailableAccounts)
props.filter.removeEventListener('reset', resetFilter)
props.filter.removeEventListener('deselect', deselect)
})
/**
* Currently shown accounts (filtered)
*/
const shownAccounts = computed(() => {
if (!accountFilter.value) {
return availableAccounts.value
return [...availableAccounts.value].sort(sortAccounts)
}
const queryParts = accountFilter.value.toLocaleLowerCase().trim().split(' ')
return availableAccounts.value.filter((account) => queryParts.every((part) => account.user.toLocaleLowerCase().includes(part)
const accounts = availableAccounts.value.filter((account) => queryParts.every((part) => account.user.toLocaleLowerCase().includes(part)
|| account.displayName.toLocaleLowerCase().includes(part)))
return accounts.sort(sortAccounts)
})
/**
* Sort accounts, putting the current user at the begin
*
* @param a - First account
* @param b - Second account
*/
function sortAccounts(a: IUserSelectData, b: IUserSelectData) {
if (a.id === currentUserId) {
return -1
}
if (b.id === currentUserId) {
return 1
}
return a.displayName.localeCompare(b.displayName)
}
/**
* Toggle an account as selected
*
* @param accountId The account to toggle
* @param selected Whether to select or deselect the account
*/
function toggleAccount(accountId: string) {
const account = availableAccounts.value.find(({ id }) => id === accountId)
if (account && selectedAccounts.value.includes(account)) {
selectedAccounts.value = selectedAccounts.value.filter(({ id }) => id !== accountId)
} else {
function toggleAccount(accountId: string, selected: boolean) {
selectedAccounts.value = selectedAccounts.value.filter(({ id }) => id !== accountId)
if (selected) {
const account = availableAccounts.value.find(({ id }) => id === accountId)
if (account) {
selectedAccounts.value = [...selectedAccounts.value, account]
}
}
}
// Watch selected account, on change we emit the new account data to the filter instance
watch(selectedAccounts, () => {
// Emit selected accounts as account data
const accounts = selectedAccounts.value.map(({ id: uid, displayName }) => ({ uid, displayName }))
emit('update:accounts', accounts)
})
/**
* Deselect an account
*
* @param event - The custom event
*/
function deselect(event: CustomEvent) {
const accountId = event.detail as string
selectedAccounts.value = selectedAccounts.value.filter(({ id }) => id !== accountId)
}
/**
* Reset this filter
@ -113,26 +145,27 @@ function resetFilter() {
*
* @param accounts - Accounts to use
*/
function setAvailableAccounts(accounts: IAccountData[]): void {
function setAvailableAccounts(accounts: IAccountData[] | CustomEvent): void {
if (accounts instanceof CustomEvent) {
accounts = accounts.detail as IAccountData[]
}
availableAccounts.value = accounts.map(({ uid, displayName }) => ({ displayName, id: uid, user: uid }))
}
defineExpose({
resetFilter,
setAvailableAccounts,
toggleAccount,
})
</script>
<style scoped lang="scss">
.file-list-filter-accounts {
&__item {
min-width: 250px;
}
<style module>
.fileListFilterAccount {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
}
&__avatar {
// 24px is the avatar size
margin: calc((var(--default-clickable-area) - 24px) / 2)
}
.fileListFilterAccount__avatar {
/* 24px is the avatar size */
margin: calc((var(--default-clickable-area) - 24px) / 2);
}
.fileListFilterAccount__currentUser {
font-weight: normal !important;
}
</style>

View file

@ -3,12 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileListFilterChip, INode } from '@nextcloud/files'
import type { IFileListFilterChip, IFileListFilterWithUi, INode } from '@nextcloud/files'
import svgAccountMultipleOutline from '@mdi/svg/svg/account-multiple-outline.svg?raw'
import { subscribe } from '@nextcloud/event-bus'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { ShareType } from '@nextcloud/sharing'
import { isPublicShare } from '@nextcloud/sharing/public'
import wrap from '@vue/web-component-wrapper'
import Vue from 'vue'
import FileListFilterAccount from '../components/FileListFilterAccount.vue'
@ -21,48 +24,42 @@ export interface IAccountData {
displayName: string
}
type CurrentInstance = Vue & {
resetFilter: () => void
setAvailableAccounts: (accounts: IAccountData[]) => void
toggleAccount: (account: string) => void
}
const tagName = 'files_sharing-file-list-filter-account'
/**
* File list filter to filter by owner / sharee
*/
class AccountFilter extends FileListFilter {
private availableAccounts: IAccountData[]
private currentInstance?: CurrentInstance
private filterAccounts?: IAccountData[]
class AccountFilter extends FileListFilter implements IFileListFilterWithUi {
#availableAccounts: IAccountData[]
#filterAccounts?: IAccountData[]
public readonly displayName = t('files_sharing', 'People')
public readonly iconSvgInline = svgAccountMultipleOutline
public readonly tagName = tagName
constructor() {
super('files_sharing:account', 100)
this.availableAccounts = []
this.#availableAccounts = []
subscribe('files:list:updated', ({ contents }) => {
this.updateAvailableAccounts(contents)
})
}
public mount(el: HTMLElement) {
if (this.currentInstance) {
this.currentInstance.$destroy()
}
public get availableAccounts() {
return this.#availableAccounts
}
const View = Vue.extend(FileListFilterAccount as never)
this.currentInstance = new View({ el })
.$on('update:accounts', (accounts?: IAccountData[]) => this.setAccounts(accounts))
.$mount() as CurrentInstance
this.currentInstance
.setAvailableAccounts(this.availableAccounts)
public get filterAccounts() {
return this.#filterAccounts
}
public filter(nodes: INode[]): INode[] {
if (!this.filterAccounts || this.filterAccounts.length === 0) {
if (!this.#filterAccounts || this.#filterAccounts.length === 0) {
return nodes
}
const userIds = this.filterAccounts.map(({ uid }) => uid)
const userIds = this.#filterAccounts.map(({ uid }) => uid)
// Filter if the owner of the node is in the list of filtered accounts
return nodes.filter((node) => {
if (window.OCP.Files.Router.params.view === TRASHBIN_VIEW_ID) {
@ -95,7 +92,7 @@ class AccountFilter extends FileListFilter {
}
public reset(): void {
this.currentInstance?.resetFilter()
this.dispatchEvent(new CustomEvent('reset'))
}
/**
@ -104,13 +101,13 @@ class AccountFilter extends FileListFilter {
* @param accounts - Account to filter or undefined if inactive.
*/
public setAccounts(accounts?: IAccountData[]) {
this.filterAccounts = accounts
this.#filterAccounts = accounts
let chips: IFileListFilterChip[] = []
if (this.filterAccounts && this.filterAccounts.length > 0) {
chips = this.filterAccounts.map(({ displayName, uid }) => ({
if (this.#filterAccounts && this.#filterAccounts.length > 0) {
chips = this.#filterAccounts.map(({ displayName, uid }) => ({
text: displayName,
user: uid,
onclick: () => this.currentInstance?.toggleAccount(uid),
onclick: () => this.dispatchEvent(new CustomEvent('deselect', { detail: uid })),
}))
}
@ -164,13 +161,13 @@ class AccountFilter extends FileListFilter {
}
}
this.availableAccounts = [...available.values()]
if (this.currentInstance) {
this.currentInstance.setAvailableAccounts(this.availableAccounts)
}
this.#availableAccounts = [...available.values()]
this.dispatchEvent(new CustomEvent('accounts-updated'))
}
}
export type { AccountFilter }
/**
* Register the file list filter by owner or sharees
*/
@ -180,5 +177,20 @@ export function registerAccountFilter() {
return
}
const WrappedComponent = wrap(Vue, FileListFilterAccount)
// In Vue 2, wrap doesn't support disabling shadow :(
// Disable with a hack
Object.defineProperty(WrappedComponent.prototype, 'attachShadow', {
value() {
return this
},
})
Object.defineProperty(WrappedComponent.prototype, 'shadowRoot', {
get() {
return this
},
})
customElements.define(tagName, WrappedComponent)
registerFileListFilter(new AccountFilter())
}

View file

@ -81,6 +81,7 @@
"vue-localstorage": "^0.6.2",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^3.6.5",
"vue2-teleport": "^1.1.4",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
@ -15052,7 +15053,6 @@
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@ -17956,6 +17956,11 @@
"vue": "^2.5.0"
}
},
"node_modules/vue2-teleport": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/vue2-teleport/-/vue2-teleport-1.1.4.tgz",
"integrity": "sha512-mGTszyQP6k3sSSk7MBq+PZdVojHYLwg5772hl3UVpu5uaLBqWIZ5eNP6/TjkDrf1XUTTxybvpXC6inpjwO+i/Q=="
},
"node_modules/vuedraggable": {
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",

View file

@ -97,6 +97,7 @@
"vue-localstorage": "^0.6.2",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^3.6.5",
"vue2-teleport": "^1.1.4",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",

View file

@ -162,7 +162,7 @@ export function triggerSelectionAction(actionId: string) {
getSelectionActionButton().click({ force: true })
// the entry might already be a button or a button might its child
getSelectionActionEntry(actionId)
.then(($el) => $el.is('button') ? cy.wrap($el) : cy.wrap($el).findByRole('button').last())
.then(($el) => $el.is('button') ? cy.wrap($el) : cy.wrap($el).findByRole('menuitem').last())
.should('exist')
.click()
}
@ -384,12 +384,24 @@ export function triggerFileListAction(actionId: string) {
}
/**
* Reloads the current folder
*
* @param intercept if true this will wait for the PROPFIND to complete before it resolves
*/
export function reloadCurrentFolder() {
export function reloadCurrentFolder(intercept = true) {
cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
cy.get('[data-cy-files-content-breadcrumbs]').findByRole('button', { description: 'Reload current directory' }).click()
cy.wait('@propfind')
cy.findByRole('navigation', { name: 'Current directory path' })
.findAllByRole('button')
.filter('[aria-haspopup="menu"]')
.click()
cy.findByRole('menu')
.should('be.visible')
.findByRole('menuitem', { name: 'Reload content' })
.click()
if (intercept) {
cy.wait('@propfind')
}
}
/**

View file

@ -132,6 +132,14 @@ describe('files: Drag and Drop', { testIsolation: true }, () => {
cy.get('[data-cy-upload-picker] progress').should('not.be.visible')
cy.get('@uploadFile.all').should('have.length', 2)
// see the warning
cy.get('.toast-warning').should('exist')
// close all toasts
cy.get('.toastify')
.findAllByRole('button', { name: 'Close' })
.click({ multiple: true })
getRowForFile('first.txt').should('be.visible')
getRowForFile('second.txt').should('be.visible')
getRowForFile('Foo').should('not.exist')

View file

@ -69,17 +69,17 @@ describe('files: Filter in files list', { testIsolation: true }, () => {
getRowForFile('file.txt').should('be.visible')
getRowForFile('spreadsheet.csv').should('be.visible')
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
.should('be.visible')
.click()
cy.findByRole('menuitemcheckbox', { name: 'Spreadsheets' })
.should('be.visible')
.click()
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Spreadsheets' })
.should('be.visible')
.and('have.attr', 'aria-pressed', 'false')
.as('spreadsheetsFilterButton')
.click()
cy.get('@spreadsheetsFilterButton')
.should('have.attr', 'aria-pressed', 'true')
filesFilters.closeFilterMenu()
// See that only the spreadsheet is visible
getRowForFile('spreadsheet.csv').should('be.visible')
@ -91,33 +91,32 @@ describe('files: Filter in files list', { testIsolation: true }, () => {
// All are visible by default
getRowForFile('folder').should('be.visible')
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
.should('be.visible')
.click()
cy.findByRole('menuitemcheckbox', { name: 'Spreadsheets' })
.should('be.visible')
.click()
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Spreadsheets' })
.should('be.visible')
.as('spreadsheetsFilterButton')
.click()
cy.get('@spreadsheetsFilterButton')
.should('have.attr', 'aria-pressed', 'true')
filesFilters.closeFilterMenu()
// See folder is not visible
getRowForFile('folder').should('not.exist')
// clear filter
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: /clear filter/i })
.should('be.visible')
.click()
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Spreadsheets' })
.should('be.visible')
.and('have.attr', 'aria-pressed', 'true')
.as('spreadsheetsFilterButton')
.click()
cy.get('@spreadsheetsFilterButton')
.should('have.attr', 'aria-pressed', 'false')
filesFilters.closeFilterMenu()
// See folder is visible again
getRowForFile('folder').should('be.visible')
@ -127,17 +126,16 @@ describe('files: Filter in files list', { testIsolation: true }, () => {
// All are visible by default
getRowForFile('folder').should('be.visible')
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
.should('be.visible')
.click()
cy.findByRole('menuitemcheckbox', { name: 'Spreadsheets' })
.should('be.visible')
.click()
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Spreadsheets' })
.should('be.visible')
.as('spreadsheetsFilterButton')
.click()
cy.get('@spreadsheetsFilterButton')
.should('have.attr', 'aria-pressed', 'true')
filesFilters.closeFilterMenu()
// See folder is not visible
getRowForFile('folder').should('not.exist')
@ -154,16 +152,16 @@ describe('files: Filter in files list', { testIsolation: true }, () => {
getRowForFile('folder').should('be.visible')
getRowForFile('file.txt').should('be.visible')
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Folders' })
.should('be.visible')
.as('spreadsheetsFilterButton')
.click()
cy.findByRole('menuitemcheckbox', { name: 'Folders' })
.should('be.visible')
.click()
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
.click()
cy.get('@spreadsheetsFilterButton')
.should('have.attr', 'aria-pressed', 'true')
filesFilters.closeFilterMenu()
// See that only the folder is visible
getRowForFile('folder').should('be.visible')
@ -189,20 +187,16 @@ describe('files: Filter in files list', { testIsolation: true }, () => {
getRowForFile('file.txt').should('be.visible')
// enable type filter for folders
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Folders' })
.should('be.visible')
.as('spreadsheetsFilterButton')
.click()
cy.findByRole('menuitemcheckbox', { name: 'Folders' })
.should('be.visible')
.click()
// assert the button is checked
cy.findByRole('menuitemcheckbox', { name: 'Folders' })
.should('have.attr', 'aria-checked', 'true')
// close the menu
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
.click()
cy.get('@spreadsheetsFilterButton')
.should('have.attr', 'aria-pressed', 'true')
filesFilters.closeFilterMenu()
// See the chips are active
filesFilters.activeFilters()
@ -222,13 +216,13 @@ describe('files: Filter in files list', { testIsolation: true }, () => {
.should('have.length', 1)
.contains(/Folder/).should('be.visible')
// And also the button should be active
filesFilters.filterContainter()
.findByRole('button', { name: 'Type' })
filesFilters.triggerFilter('Type')
cy.findByRole('button', { name: 'Folders' })
.should('be.visible')
.click()
cy.findByRole('menuitemcheckbox', { name: 'Folders' })
.should('be.visible')
.and('have.attr', 'aria-checked', 'true')
.should('have.attr', 'aria-pressed', 'true')
filesFilters.closeFilterMenu()
})
/** Regression test of https://github.com/nextcloud/server/issues/53038 */

View file

@ -19,11 +19,10 @@ describe('files: Set default view', { testIsolation: true }, () => {
// See URL and current view
cy.url().should('match', /\/apps\/files\/files/)
cy.get('[data-cy-files-content-breadcrumbs]')
.findByRole('button', {
name: 'All files',
description: 'Reload current directory',
})
cy.findByRole('navigation', { name: 'Current directory path' })
.findAllByRole('button')
.first()
.should('have.text', 'All files')
// See the option is also selected
// Open the files settings
@ -54,11 +53,10 @@ describe('files: Set default view', { testIsolation: true }, () => {
cy.visit('/apps/files')
cy.url().should('match', /\/apps\/files\/personal/)
cy.get('[data-cy-files-content-breadcrumbs]')
.findByRole('button', {
name: 'Personal files',
description: 'Reload current directory',
})
cy.findByRole('navigation', { name: 'Current directory path' })
.findAllByRole('button')
.first()
.should('have.text', 'Personal files')
})
})

View file

@ -6,13 +6,13 @@
import type { User } from '@nextcloud/e2e-test-server/cypress'
import {
clickOnBreadcrumbs,
copyFile,
createFolder,
getRowForFile,
getRowForFileId,
moveFile,
navigateToFolder,
reloadCurrentFolder,
renameFile,
triggerActionForFile,
triggerInlineActionForFileId,
@ -52,7 +52,7 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Copies both files when copying the .jpg', () => {
copyFile(`${randomFileName}.jpg`, '.')
clickOnBreadcrumbs('All files')
reloadCurrentFolder()
getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
@ -62,7 +62,7 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Copies both files when copying the .mov', () => {
copyFile(`${randomFileName}.mov`, '.')
clickOnBreadcrumbs('All files')
reloadCurrentFolder()
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
getRowForFile(`${randomFileName} (copy).jpg`).should('have.length', 1)
@ -100,7 +100,7 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Moves files when moving the .jpg', () => {
renameFile(`${randomFileName}.jpg`, `${randomFileName}_moved.jpg`)
clickOnBreadcrumbs('All files')
reloadCurrentFolder()
getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`)
getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`)
@ -108,7 +108,7 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Moves files when moving the .mov', () => {
renameFile(`${randomFileName}.mov`, `${randomFileName}_moved.mov`)
clickOnBreadcrumbs('All files')
reloadCurrentFolder()
getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`)
getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`)
@ -116,7 +116,7 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Deletes files when deleting the .jpg', () => {
triggerActionForFile(`${randomFileName}.jpg`, 'delete')
clickOnBreadcrumbs('All files')
reloadCurrentFolder()
getRowForFile(`${randomFileName}.jpg`).should('have.length', 0)
getRowForFile(`${randomFileName}.mov`).should('have.length', 0)
@ -129,7 +129,7 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Block deletion when deleting the .mov', () => {
triggerActionForFile(`${randomFileName}.mov`, 'delete')
clickOnBreadcrumbs('All files')
reloadCurrentFolder()
getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
@ -143,8 +143,9 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Restores files when restoring the .jpg', () => {
triggerActionForFile(`${randomFileName}.jpg`, 'delete')
cy.visit('/apps/files/trashbin')
triggerInlineActionForFileId(jpgFileId, 'restore')
clickOnBreadcrumbs('Deleted files')
reloadCurrentFolder()
getRowForFile(`${randomFileName}.jpg`).should('have.length', 0)
getRowForFile(`${randomFileName}.mov`).should('have.length', 0)
@ -158,8 +159,9 @@ describe('Files: Live photos', { testIsolation: true }, () => {
it('Blocks restoration when restoring the .mov', () => {
triggerActionForFile(`${randomFileName}.jpg`, 'delete')
cy.visit('/apps/files/trashbin')
triggerInlineActionForFileId(movFileId, 'restore')
clickOnBreadcrumbs('Deleted files')
reloadCurrentFolder()
getRowForFileId(jpgFileId).should('have.length', 1)
getRowForFileId(movFileId).should('have.length', 1)

View file

@ -6,7 +6,7 @@
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { FilesNavigationPage } from '../../pages/FilesNavigation.ts'
import { getRowForFile, navigateToFolder } from './FilesUtils.ts'
import { getRowForFile, navigateToFolder, reloadCurrentFolder } from './FilesUtils.ts'
describe('files: search', () => {
let user: User
@ -74,7 +74,7 @@ describe('files: search', () => {
it('See "search everywhere" button', () => {
// Not visible initially
cy.get('[data-cy-files-filters]')
cy.get('.files-list__filters')
.findByRole('button', { name: /Search everywhere/i })
.should('not.to.exist')
@ -82,7 +82,7 @@ describe('files: search', () => {
navigation.searchInput().type('file')
// see its visible
cy.get('[data-cy-files-filters]')
cy.get('.files-list__filters')
.findByRole('button', { name: /Search everywhere/i })
.should('be.visible')
@ -90,7 +90,7 @@ describe('files: search', () => {
navigation.searchClearButton().click()
// see its not visible again
cy.get('[data-cy-files-filters]')
cy.get('.files-list__filters')
.findByRole('button', { name: /Search everywhere/i })
.should('not.to.exist')
})
@ -108,7 +108,7 @@ describe('files: search', () => {
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 3)
// toggle global search
cy.get('[data-cy-files-filters]')
cy.get('.files-list__filters')
.findByRole('button', { name: /Search everywhere/i })
.should('be.visible')
.click()
@ -206,7 +206,7 @@ describe('files: search', () => {
cy.intercept('SEARCH', '**/remote.php/dav/').as('search')
// refresh the view
cy.findByRole('button', { description: /reload current directory/i }).click()
reloadCurrentFolder(false) // no PROPFIND intercept here as we want to wait for SEARCH
// wait for the request
cy.wait('@search')
// see that the search view is reloaded

View file

@ -18,45 +18,6 @@ const files = [
'file5.txt',
]
function resetTags() {
tags = {}
for (let i = 0; i < 5; i++) {
tags[randomBytes(8).toString('base64').slice(0, 6)] = 0
}
// delete any existing tags
cy.runOccCommand('tag:list --output=json').then((output) => {
Object.keys(JSON.parse(output.stdout)).forEach((id) => {
cy.runOccCommand(`tag:delete ${id}`)
})
})
// create tags
Object.keys(tags).forEach((tag) => {
cy.runOccCommand(`tag:add ${tag} public --output=json`).then((output) => {
tags[tag] = JSON.parse(output.stdout).id as number
})
})
cy.log('Using tags', tags)
}
function expectInlineTagForFile(file: string, tags: string[]) {
getRowForFile(file)
.find('[data-systemtags-fileid]')
.findAllByRole('listitem')
.should('have.length', tags.length)
.each((tag) => {
expect(tag.text()).to.be.oneOf(tags)
})
}
function triggerTagManagementDialogAction() {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/').as('getTagsList')
triggerSelectionAction('systemtags:bulk')
cy.wait('@getTagsList')
cy.get('[data-cy-systemtags-picker]').should('be.visible')
}
describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
let user1: User
let user2: User
@ -98,7 +59,7 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const tag = Object.keys(tags)[3]
const tag = Object.keys(tags)[3]!
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
@ -127,9 +88,9 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const prevTag = Object.keys(tags)[3]
const tag1 = Object.keys(tags)[1]
const tag2 = Object.keys(tags)[2]
const prevTag = Object.keys(tags)[3]!
const tag1 = Object.keys(tags)[1]!
const tag2 = Object.keys(tags)[2]!
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
@ -166,9 +127,9 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const firstTag = Object.keys(tags)[3]
const tag1 = Object.keys(tags)[1]
const tag2 = Object.keys(tags)[2]
const firstTag = Object.keys(tags)[3]!
const tag1 = Object.keys(tags)[1]!
const tag2 = Object.keys(tags)[2]!
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
@ -247,8 +208,8 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData1')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData1')
const tag1 = Object.keys(tags)[0]
const tag2 = Object.keys(tags)[3]
const tag1 = Object.keys(tags)[0]!
const tag2 = Object.keys(tags)[3]!
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
@ -466,3 +427,42 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
})
})
})
function resetTags() {
tags = {}
for (let i = 0; i < 5; i++) {
tags[randomBytes(8).toString('base64').slice(0, 6)] = 0
}
// delete any existing tags
cy.runOccCommand('tag:list --output=json').then((output) => {
Object.keys(JSON.parse(output.stdout)).forEach((id) => {
cy.runOccCommand(`tag:delete ${id}`)
})
})
// create tags
Object.keys(tags).forEach((tag) => {
cy.runOccCommand(`tag:add ${tag} public --output=json`).then((output) => {
tags[tag] = JSON.parse(output.stdout).id as number
})
})
cy.log('Using tags', tags)
}
function expectInlineTagForFile(file: string, tags: string[]) {
getRowForFile(file)
.find('[data-systemtags-fileid]')
.findAllByRole('listitem')
.should('have.length', tags.length)
.each((tag) => {
expect(tag.text()).to.be.oneOf(tags)
})
}
function triggerTagManagementDialogAction() {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/').as('getTagsList')
triggerSelectionAction('systemtags:bulk')
cy.wait('@getTagsList')
cy.get('[data-cy-systemtags-picker]').should('be.visible')
}

View file

@ -7,8 +7,62 @@
* Page object model for the files filters
*/
export class FilesFilterPage {
filterContainter() {
return cy.get('[data-cy-files-filters]')
/**
* Get the filters menu button (only on narrow and medium widths)
*/
getFiltersMenuToggle() {
return cy.get('[data-test-id="files-list-filters"]')
.findByRole('button', { name: 'Filters' })
}
/**
* Get and trigger the filter within the menu (only on narrow and medium widths)
*
* @param name - The name of the filter button
*/
triggerFilterMenu(name: string | RegExp) {
cy.get('[data-test-id="files-list-filters"]')
.findByRole('button', { name: 'Filters' })
.should('be.visible')
.as('filtersMenuToggle')
.click()
cy.get('@filtersMenuToggle')
.should('have.attr', 'aria-expanded', 'true')
cy.findByRole('menu')
.should('be.visible')
.findByRole('menuitem', { name })
.should('be.visible')
.click()
}
/**
* Get and trigger the filter button if the files list is wide enough to show all filters
*
* @param name - The name of the filter button
*/
triggerFilterButton(name: string | RegExp) {
cy.get('[data-test-id="files-list-filters"]')
.findByRole('button', { name })
.should('be.visible')
.click()
}
triggerFilter(name: string | RegExp) {
cy.get('[data-cy-files-list]')
.should('be.visible')
.if(($el) => expect($el.get(0).clientWidth).to.be.gte(1024))
.then(() => this.triggerFilterButton(name))
.else()
.then(() => this.triggerFilterMenu(name))
}
closeFilterMenu() {
cy.get('[data-test-id="files-list-filters"]')
.findAllByRole('button')
.filter('[aria-expanded="true"]')
.click({ multiple: true })
}
activeFiltersList() {

2
dist/1546-1546.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: Austin Andrews
This file is generated from multiple sources. Included packages:
- @mdi/js
- version: 7.4.47
- license: Apache-2.0

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
1546-1546.js.license

2
dist/2251-2251.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
2251-2251.js.license

2
dist/23-23.js vendored

File diff suppressed because one or more lines are too long

37
dist/23-23.js.license vendored
View file

@ -1,37 +0,0 @@
SPDX-License-Identifier: MIT
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: Austin Andrews
This file is generated from multiple sources. Included packages:
- @mdi/js
- version: 7.4.47
- license: Apache-2.0
- @nextcloud/l10n
- version: 3.4.1
- license: GPL-3.0-or-later
- @nextcloud/router
- version: 3.1.0
- license: GPL-3.0-or-later
- @nextcloud/vue
- version: 8.35.2
- license: AGPL-3.0-or-later
- css-loader
- version: 7.1.2
- license: MIT
- dompurify
- version: 3.3.1
- license: (MPL-2.0 OR Apache-2.0)
- escape-html
- version: 1.0.3
- license: MIT
- style-loader
- version: 4.0.0
- license: MIT

1
dist/23-23.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
23-23.js.license

2
dist/7257-7257.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
7257-7257.js.license

2
dist/8127-8127.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,6 @@ SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: string_decoder developers
@ -22,16 +21,12 @@ SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Eduardo San Martin Morote
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Austin Andrews
SPDX-FileCopyrightText: Arnout Kazemier
SPDX-FileCopyrightText: Alkemics
SPDX-FileCopyrightText: @nextcloud/dialogs developers
This file is generated from multiple sources. Included packages:
- @mdi/js
- version: 7.4.47
- license: Apache-2.0
- @nextcloud/auth
- version: 2.5.3
- license: GPL-3.0-or-later

1
dist/8127-8127.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/8127-8127.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
8127-8127.js.license

2
dist/8309-8309.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/8309-8309.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/8309-8309.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
8309-8309.js.license

2
dist/9165-9165.js vendored
View file

@ -1,2 +0,0 @@
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[1546,9165],{9165(L,C,M){M.d(C,{Fb5:()=>H,VX1:()=>A,W5x:()=>u,nO4:()=>a,rZW:()=>V,rvk:()=>l});var A="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z",l="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z",u="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z",H="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z",V="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z",a="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"}}]);
//# sourceMappingURL=9165-9165.js.map?v=6967acc1237f55abe6a9

View file

@ -1,8 +0,0 @@
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: Austin Andrews
This file is generated from multiple sources. Included packages:
- @mdi/js
- version: 7.4.47
- license: Apache-2.0

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
9165-9165.js.license

4
dist/9281-9281.js vendored
View file

@ -1,2 +1,2 @@
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[9281],{9281(e,l,c){c.d(l,{FilePickerVue:()=>i});const i=(0,c(85471).$V)(()=>Promise.all([c.e(4208),c.e(1546),c.e(5402)]).then(c.bind(c,65402)))}}]);
//# sourceMappingURL=9281-9281.js.map?v=ae4ef00aff18bfc08a08
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[9281],{9281(e,l,c){c.d(l,{FilePickerVue:()=>i});const i=(0,c(85471).$V)(()=>Promise.all([c.e(4208),c.e(5402)]).then(c.bind(c,65402)))}}]);
//# sourceMappingURL=9281-9281.js.map?v=feab11896533ab67dcd8

View file

@ -1 +1 @@
{"version":3,"file":"9281-9281.js?v=ae4ef00aff18bfc08a08","mappings":"gKACA,MAAMA,GAAgB,E,SAAA,IAAqB,IAAM,mE","sources":["webpack:///nextcloud/node_modules/@nextcloud/upload/node_modules/@nextcloud/dialogs/dist/chunks/index-BMbtc3xh.mjs"],"sourcesContent":["import { defineAsyncComponent } from \"vue\";\nconst FilePickerVue = defineAsyncComponent(() => import(\"./FilePicker-JKNLPCbR.mjs\"));\nexport {\n FilePickerVue\n};\n//# sourceMappingURL=index-BMbtc3xh.mjs.map\n"],"names":["FilePickerVue"],"ignoreList":[],"sourceRoot":""}
{"version":3,"file":"9281-9281.js?v=feab11896533ab67dcd8","mappings":"gKACA,MAAMA,GAAgB,E,SAAA,IAAqB,IAAM,yD","sources":["webpack:///nextcloud/node_modules/@nextcloud/upload/node_modules/@nextcloud/dialogs/dist/chunks/index-BMbtc3xh.mjs"],"sourcesContent":["import { defineAsyncComponent } from \"vue\";\nconst FilePickerVue = defineAsyncComponent(() => import(\"./FilePicker-JKNLPCbR.mjs\"));\nexport {\n FilePickerVue\n};\n//# sourceMappingURL=index-BMbtc3xh.mjs.map\n"],"names":["FilePickerVue"],"ignoreList":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

View file

@ -122,6 +122,9 @@ This file is generated from multiple sources. Included packages:
- @mapbox/hast-util-table-cell-style
- version: 0.2.1
- license: BSD-2-Clause
- @mdi/js
- version: 7.4.47
- license: Apache-2.0
- @mdi/svg
- version: 7.4.47
- license: Apache-2.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,7 @@ SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: escape-html developers
@ -27,11 +28,15 @@ SPDX-FileCopyrightText: Eduardo San Martin Morote
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Austin Andrews
SPDX-FileCopyrightText: Anthony Fu <https://github.com/antfu>
SPDX-FileCopyrightText: Anthony Fu <anthonyfu117@hotmail.com>
This file is generated from multiple sources. Included packages:
- @mdi/js
- version: 7.4.47
- license: Apache-2.0
- @nextcloud/auth
- version: 2.5.3
- license: GPL-3.0-or-later

File diff suppressed because one or more lines are too long

4
dist/core-update.js vendored
View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,r,t,o={42716(e,r,t){var o=t(21777),a=t(81222),n=t(85471);t.nc=(0,o.aV)();const i=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9165),t.e(9396)]).then(t.bind(t,31098))),l=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9165),t.e(428)]).then(t.bind(t,428))),d=(0,a.C)("core","updaterView");new n.Ay({name:"NextcloudUpdater",render:e=>e("adminCli"===d?l:i)}).$mount("#core-updater")}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){for(var[t,o,a]=e[u],l=!0,d=0;d<t.length;d++)(!1&a||i>=a)&&Object.keys(n.O).every(e=>n.O[e](t[d]))?t.splice(d--,1):(l=!1,a<i&&(i=a));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"-"+e+".js?v="+{428:"499a9f39f8f9c316fbf5",5862:"580b9c2e231a9169a12f",6798:"b6c47bd4c707c3e5af5b",7471:"9ee6c1057cda0339f62c",9165:"6967acc1237f55abe6a9",9396:"da8f3e497d259ffaf9d7"}[e],n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",n.l=(e,o,a,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==a)for(var c=document.getElementsByTagName("script"),u=0;u<c.length;u++){var s=c[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+a){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",n.nc&&l.setAttribute("nonce",n.nc),l.setAttribute("data-webpack",t+a),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var a=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),a&&a.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=6344,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})(),(()=>{n.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={6344:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var i=n.p+n.u(r),l=new Error;n.l(i,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+a+": "+i+")",l.name="ChunkLoadError",l.type=a,l.request=i,o[1](l)}},"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[i,l,d]=t,c=0;if(i.some(r=>0!==e[r])){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(d)var u=d(n)}for(r&&r(t);c<i.length;c++)a=i[c],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),n.nc=void 0;var i=n.O(void 0,[4208],()=>n(42716));i=n.O(i)})();
//# sourceMappingURL=core-update.js.map?v=80c7479dc6d111b1e687
(()=>{"use strict";var e,r,t,o={42716(e,r,t){var o=t(21777),a=t(81222),n=t(85471);t.nc=(0,o.aV)();const i=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9396)]).then(t.bind(t,31098))),l=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(428)]).then(t.bind(t,428))),d=(0,a.C)("core","updaterView");new n.Ay({name:"NextcloudUpdater",render:e=>e("adminCli"===d?l:i)}).$mount("#core-updater")}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){for(var[t,o,a]=e[u],l=!0,d=0;d<t.length;d++)(!1&a||i>=a)&&Object.keys(n.O).every(e=>n.O[e](t[d]))?t.splice(d--,1):(l=!1,a<i&&(i=a));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"-"+e+".js?v="+{428:"499a9f39f8f9c316fbf5",5862:"580b9c2e231a9169a12f",6798:"b6c47bd4c707c3e5af5b",7471:"9ee6c1057cda0339f62c",9396:"da8f3e497d259ffaf9d7"}[e],n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",n.l=(e,o,a,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==a)for(var c=document.getElementsByTagName("script"),u=0;u<c.length;u++){var s=c[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+a){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",n.nc&&l.setAttribute("nonce",n.nc),l.setAttribute("data-webpack",t+a),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var a=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),a&&a.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=6344,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})(),(()=>{n.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={6344:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var i=n.p+n.u(r),l=new Error;n.l(i,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+a+": "+i+")",l.name="ChunkLoadError",l.type=a,l.request=i,o[1](l)}},"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[i,l,d]=t,c=0;if(i.some(r=>0!==e[r])){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(d)var u=d(n)}for(r&&r(t);c<i.length;c++)a=i[c],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),n.nc=void 0;var i=n.O(void 0,[4208],()=>n(42716));i=n.O(i)})();
//# sourceMappingURL=core-update.js.map?v=19a1665d4b7b7d4f516e

File diff suppressed because one or more lines are too long

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

View file

@ -2,44 +2,68 @@ SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: BSD-2-Clause
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: xiemengxiong
SPDX-FileCopyrightText: xiaokai <kexiaokai@gmail.com>
SPDX-FileCopyrightText: string_decoder developers
SPDX-FileCopyrightText: rhysd <lin90162@yahoo.co.jp>
SPDX-FileCopyrightText: readable-stream developers
SPDX-FileCopyrightText: p-queue developers
SPDX-FileCopyrightText: omahlama
SPDX-FileCopyrightText: inline-style-parser developers
SPDX-FileCopyrightText: inherits developers
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: debounce developers
SPDX-FileCopyrightText: atomiks
SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)
SPDX-FileCopyrightText: Thorsten Lünborg
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Stefan Thomas <justmoon@members.fsf.org> (http://www.justmoon.net)
SPDX-FileCopyrightText: Sindre Sorhus
SPDX-FileCopyrightText: Rubén Norte <ruben.norte@softonic.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Richie Bendall
SPDX-FileCopyrightText: Perry Mitchell <perry@perrymitchell.net>
SPDX-FileCopyrightText: Paul Vorbach <paul@vorba.ch> (http://paul.vorba.ch)
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
SPDX-FileCopyrightText: OpenJS Foundation and other contributors
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)
SPDX-FileCopyrightText: Max <max@nextcloud.com>
SPDX-FileCopyrightText: Matt Zabriskie
SPDX-FileCopyrightText: Mark <mark@remarkablemark.org>
SPDX-FileCopyrightText: Mapbox
SPDX-FileCopyrightText: Joyent
SPDX-FileCopyrightText: Jonas Schade <derzade@gmail.com>
SPDX-FileCopyrightText: Jeff Sagal <sagalbot@gmail.com>
SPDX-FileCopyrightText: James Halliday
SPDX-FileCopyrightText: Jacob Clevenger<https://github.com/wheatjs>
SPDX-FileCopyrightText: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)
SPDX-FileCopyrightText: Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
SPDX-FileCopyrightText: Hiroki Osame
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: Guillaume Chau
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Eugene Sharygin <eush77@gmail.com>
SPDX-FileCopyrightText: Eric Norris (https://github.com/ericnorris)
SPDX-FileCopyrightText: Eduardo San Martin Morote
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Borys Serebrov
SPDX-FileCopyrightText: Austin Andrews
SPDX-FileCopyrightText: Arnout Kazemier
SPDX-FileCopyrightText: Antoni Andre <antoniandre.web@gmail.com>
SPDX-FileCopyrightText: Anthony Fu <https://github.com/antfu>
SPDX-FileCopyrightText: Anthony Fu <anthonyfu117@hotmail.com>
SPDX-FileCopyrightText: Andrea Giammarchi
SPDX-FileCopyrightText: Alkemics
SPDX-FileCopyrightText: @nextcloud/dialogs developers
@ -48,12 +72,27 @@ This file is generated from multiple sources. Included packages:
- @floating-ui/core
- version: 1.7.3
- license: MIT
- @floating-ui/dom
- version: 1.7.4
- license: MIT
- @floating-ui/utils
- version: 0.2.10
- license: MIT
- @mdi/js
- version: 7.4.47
- @linusborg/vue-simple-portal
- version: 0.1.5
- license: Apache-2.0
- unist-util-is
- version: 3.0.0
- license: MIT
- unist-util-visit-parents
- version: 2.1.2
- license: MIT
- unist-util-visit
- version: 1.4.1
- license: MIT
- @mapbox/hast-util-table-cell-style
- version: 0.2.1
- license: BSD-2-Clause
- @mdi/svg
- version: 7.4.47
- license: Apache-2.0
@ -147,9 +186,27 @@ This file is generated from multiple sources. Included packages:
- @nextcloud/upload
- version: 1.11.0
- license: AGPL-3.0-or-later
- @nextcloud/vue-select
- version: 3.26.0
- license: MIT
- @nextcloud/initial-state
- version: 2.2.0
- license: GPL-3.0-or-later
- debounce
- version: 2.2.0
- license: MIT
- eventemitter3
- version: 5.0.1
- license: MIT
- p-queue
- version: 8.1.1
- license: MIT
- @nextcloud/vue
- version: 8.35.2
- license: AGPL-3.0-or-later
- @ungap/structured-clone
- version: 1.3.0
- license: ISC
- @vue/devtools-api
- version: 6.6.4
- license: MIT
@ -165,6 +222,12 @@ This file is generated from multiple sources. Included packages:
- @vue/shared
- version: 3.5.26
- license: MIT
- @vue/web-component-wrapper
- version: 1.3.0
- license: MIT
- @vueuse/components
- version: 11.3.0
- license: MIT
- @vueuse/core
- version: 11.3.0
- license: MIT
@ -177,45 +240,183 @@ This file is generated from multiple sources. Included packages:
- axios
- version: 1.12.2
- license: MIT
- bail
- version: 2.0.2
- license: MIT
- base64-js
- version: 1.5.1
- license: MIT
- blurhash
- version: 2.0.5
- license: MIT
- cancelable-promise
- version: 4.3.1
- license: MIT
- char-regex
- version: 2.0.2
- license: MIT
- charenc
- version: 0.0.2
- license: BSD-3-Clause
- comma-separated-tokens
- version: 2.0.3
- license: MIT
- crypt
- version: 0.0.2
- license: BSD-3-Clause
- css-loader
- version: 7.1.2
- license: MIT
- date-format-parse
- version: 0.2.7
- license: MIT
- debounce
- version: 3.0.0
- license: MIT
- decode-named-character-reference
- version: 1.2.0
- license: MIT
- devlop
- version: 1.1.0
- license: MIT
- dompurify
- version: 3.3.1
- license: (MPL-2.0 OR Apache-2.0)
- emoji-mart-vue-fast
- version: 15.0.5
- license: BSD-3-Clause
- escape-html
- version: 1.0.3
- license: MIT
- events
- version: 3.3.0
- license: MIT
- extend
- version: 3.0.2
- license: MIT
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 7.6.6
- license: MIT
- hast-to-hyperscript
- version: 10.0.3
- license: MIT
- hast-util-is-element
- version: 3.0.0
- license: MIT
- hast-util-whitespace
- version: 2.0.1
- license: MIT
- ieee754
- version: 1.2.1
- license: BSD-3-Clause
- inherits
- version: 2.0.4
- license: ISC
- inline-style-parser
- version: 0.1.1
- license: MIT
- is-absolute-url
- version: 4.0.1
- license: MIT
- is-buffer
- version: 1.1.6
- license: MIT
- is-plain-obj
- version: 4.1.0
- license: MIT
- is-retry-allowed
- version: 2.2.0
- license: MIT
- is-svg
- version: 6.1.0
- license: MIT
- jquery
- version: 3.7.1
- license: MIT
- md5
- version: 2.3.0
- license: BSD-3-Clause
- mdast-squeeze-paragraphs
- version: 6.0.0
- license: MIT
- escape-string-regexp
- version: 5.0.0
- license: MIT
- mdast-util-find-and-replace
- version: 3.0.2
- license: MIT
- mdast-util-from-markdown
- version: 2.0.2
- license: MIT
- mdast-util-newline-to-break
- version: 2.0.0
- license: MIT
- mdast-util-to-hast
- version: 13.2.1
- license: MIT
- mdast-util-to-string
- version: 4.0.0
- license: MIT
- micromark-core-commonmark
- version: 2.0.3
- license: MIT
- micromark-factory-destination
- version: 2.0.1
- license: MIT
- micromark-factory-label
- version: 2.0.1
- license: MIT
- micromark-factory-space
- version: 2.0.1
- license: MIT
- micromark-factory-title
- version: 2.0.1
- license: MIT
- micromark-factory-whitespace
- version: 2.0.1
- license: MIT
- micromark-util-character
- version: 2.1.1
- license: MIT
- micromark-util-chunked
- version: 2.0.1
- license: MIT
- micromark-util-classify-character
- version: 2.0.1
- license: MIT
- micromark-util-combine-extensions
- version: 2.0.1
- license: MIT
- micromark-util-decode-numeric-character-reference
- version: 2.0.2
- license: MIT
- micromark-util-decode-string
- version: 2.0.1
- license: MIT
- micromark-util-encode
- version: 2.0.1
- license: MIT
- micromark-util-html-tag-name
- version: 2.0.1
- license: MIT
- micromark-util-normalize-identifier
- version: 2.0.1
- license: MIT
- micromark-util-resolve-all
- version: 2.0.1
- license: MIT
- micromark-util-sanitize-uri
- version: 2.0.1
- license: MIT
- micromark-util-subtokenize
- version: 2.1.0
- license: MIT
- micromark
- version: 4.0.2
- license: MIT
- buffer
- version: 6.0.3
- license: MIT
@ -246,45 +447,126 @@ This file is generated from multiple sources. Included packages:
- process
- version: 0.11.10
- license: MIT
- property-information
- version: 6.5.0
- license: MIT
- rehype-external-links
- version: 3.0.0
- license: MIT
- rehype-react
- version: 7.2.0
- license: MIT
- remark-breaks
- version: 4.0.0
- license: MIT
- remark-parse
- version: 11.0.0
- license: MIT
- remark-rehype
- version: 11.1.2
- license: MIT
- remark-unlink-protocols
- version: 1.0.0
- license: MIT
- safe-buffer
- version: 5.2.1
- license: MIT
- sax
- version: 1.4.1
- license: ISC
- space-separated-tokens
- version: 2.0.2
- license: MIT
- splitpanes
- version: 2.4.1
- license: MIT
- readable-stream
- version: 3.6.2
- license: MIT
- stream-browserify
- version: 3.0.0
- license: MIT
- strip-ansi
- version: 7.1.2
- license: MIT
- string_decoder
- version: 1.3.0
- license: MIT
- striptags
- version: 3.2.0
- license: MIT
- style-loader
- version: 4.0.0
- license: MIT
- style-to-object
- version: 0.4.4
- license: MIT
- tabbable
- version: 6.4.0
- license: MIT
- toastify-js
- version: 1.12.0
- license: MIT
- trim-lines
- version: 3.0.1
- license: MIT
- trough
- version: 2.2.0
- license: MIT
- typescript-event-target
- version: 1.1.2
- license: MIT
- unified
- version: 11.0.5
- license: MIT
- unist-builder
- version: 4.0.0
- license: MIT
- unist-util-is
- version: 6.0.0
- license: MIT
- unist-util-position
- version: 5.0.0
- license: MIT
- unist-util-stringify-position
- version: 4.0.0
- license: MIT
- unist-util-visit-parents
- version: 6.0.1
- license: MIT
- unist-util-visit
- version: 5.0.0
- license: MIT
- util-deprecate
- version: 1.0.2
- license: MIT
- vfile-message
- version: 4.0.3
- license: MIT
- vfile
- version: 6.0.3
- license: MIT
- vue-color
- version: 2.8.2
- license: MIT
- vue-demi
- version: 0.14.10
- license: MIT
- vue-frag
- version: 1.4.3
- license: MIT
- vue-loader
- version: 15.11.1
- license: MIT
- vue-router
- version: 3.6.5
- license: MIT
- vue
- version: 2.7.16
- license: MIT
- web-namespaces
- version: 2.0.1
- license: MIT
- webdav
- version: 5.8.0
- license: MIT

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,r,t,a={48246(e,r,t){var a=t(85168),i=t(61338),o=t(53334),l=t(63814);const n=(0,t(35947).YK)().setApp("files").detectUser().build();document.addEventListener("DOMContentLoaded",function(){const e=window.OCA;e.UnifiedSearch&&(n.info("Initializing unified search plugin: folder search from files app"),e.UnifiedSearch.registerFilterAction({id:"in-folder",appId:"files",searchFrom:"files",label:(0,o.Tl)("files","In folder"),icon:(0,l.d0)("files","app.svg"),callback:(e=!0)=>{e?(0,a.a1)((0,o.Tl)("files","Pick folder to search in")).setNoMenu(!0).addMimeTypeFilter("httpd/unix-directory").allowDirectories(!0).addButton({label:"Pick",callback:e=>{n.info("Folder picked",{folder:e[0]});const r=e[0],t=r.root==="/files/"+r.basename?(0,o.Tl)("files","Search in all files"):(0,o.Tl)("files","Search in folder: {folder}",{folder:r.basename});(0,i.Ic)("nextcloud:unified-search:add-filter",{id:"in-folder",appId:"files",searchFrom:"files",payload:r,filterUpdateText:t,filterParams:{path:r.path}})}}).build().pick():n.debug("Folder search callback was handled without showing the file picker, it might already be open")}}))})}},i={};function o(e){var r=i[e];if(void 0!==r)return r.exports;var t=i[e]={id:e,loaded:!1,exports:{}};return a[e].call(t.exports,t,t.exports,o),t.loaded=!0,t.exports}o.m=a,e=[],o.O=(r,t,a,i)=>{if(!t){var l=1/0;for(s=0;s<e.length;s++){for(var[t,a,i]=e[s],n=!0,d=0;d<t.length;d++)(!1&i||l>=i)&&Object.keys(o.O).every(e=>o.O[e](t[d]))?t.splice(d--,1):(n=!1,i<l&&(l=i));if(n){e.splice(s--,1);var c=a();void 0!==c&&(r=c)}}return r}i=i||0;for(var s=e.length;s>0&&e[s-1][2]>i;s--)e[s]=e[s-1];e[s]=[t,a,i]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((r,t)=>(o.f[t](e,r),r),[])),o.u=e=>e+"-"+e+".js?v="+{2251:"4257477cac9387ca3d84",2710:"0c2e26891ac1c05900e0",4471:"9b3c8620f038b7593241",7004:"da5a822695a273d4d2eb",7394:"5b773f16893ed80e0246",7859:"cd6f48c919ca307639eb",8453:"0ad2c9a35eee895d5980"}[e],o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",o.l=(e,a,i,l)=>{if(r[e])r[e].push(a);else{var n,d;if(void 0!==i)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==t+i){n=f;break}}n||(d=!0,(n=document.createElement("script")).charset="utf-8",o.nc&&n.setAttribute("nonce",o.nc),n.setAttribute("data-webpack",t+i),n.src=e),r[e]=[a];var u=(t,a)=>{n.onerror=n.onload=null,clearTimeout(p);var i=r[e];if(delete r[e],n.parentNode&&n.parentNode.removeChild(n),i&&i.forEach(e=>e(a)),t)return t(a)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:n}),12e4);n.onerror=u.bind(null,n.onerror),n.onload=u.bind(null,n.onload),d&&document.head.appendChild(n)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.j=2277,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var a=t.length-1;a>-1&&(!e||!/^http(s?):/.test(e));)e=t[a--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{o.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={2277:0};o.f.j=(r,t)=>{var a=o.o(e,r)?e[r]:void 0;if(0!==a)if(a)t.push(a[2]);else{var i=new Promise((t,i)=>a=e[r]=[t,i]);t.push(a[2]=i);var l=o.p+o.u(r),n=new Error;o.l(l,t=>{if(o.o(e,r)&&(0!==(a=e[r])&&(e[r]=void 0),a)){var i=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;n.message="Loading chunk "+r+" failed.\n("+i+": "+l+")",n.name="ChunkLoadError",n.type=i,n.request=l,a[1](n)}},"chunk-"+r,r)}},o.O.j=r=>0===e[r];var r=(r,t)=>{var a,i,[l,n,d]=t,c=0;if(l.some(r=>0!==e[r])){for(a in n)o.o(n,a)&&(o.m[a]=n[a]);if(d)var s=d(o)}for(r&&r(t);c<l.length;c++)i=l[c],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(s)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),o.nc=void 0;var l=o.O(void 0,[4208],()=>o(48246));l=o.O(l)})();
//# sourceMappingURL=files-search.js.map?v=f843c4d2d6d7f46684dc
(()=>{"use strict";var e,r,t,a={48246(e,r,t){var a=t(85168),i=t(61338),o=t(53334),l=t(63814);const n=(0,t(35947).YK)().setApp("files").detectUser().build();document.addEventListener("DOMContentLoaded",function(){const e=window.OCA;e.UnifiedSearch&&(n.info("Initializing unified search plugin: folder search from files app"),e.UnifiedSearch.registerFilterAction({id:"in-folder",appId:"files",searchFrom:"files",label:(0,o.Tl)("files","In folder"),icon:(0,l.d0)("files","app.svg"),callback:(e=!0)=>{e?(0,a.a1)((0,o.Tl)("files","Pick folder to search in")).setNoMenu(!0).addMimeTypeFilter("httpd/unix-directory").allowDirectories(!0).addButton({label:"Pick",callback:e=>{n.info("Folder picked",{folder:e[0]});const r=e[0],t=r.root==="/files/"+r.basename?(0,o.Tl)("files","Search in all files"):(0,o.Tl)("files","Search in folder: {folder}",{folder:r.basename});(0,i.Ic)("nextcloud:unified-search:add-filter",{id:"in-folder",appId:"files",searchFrom:"files",payload:r,filterUpdateText:t,filterParams:{path:r.path}})}}).build().pick():n.debug("Folder search callback was handled without showing the file picker, it might already be open")}}))})}},i={};function o(e){var r=i[e];if(void 0!==r)return r.exports;var t=i[e]={id:e,loaded:!1,exports:{}};return a[e].call(t.exports,t,t.exports,o),t.loaded=!0,t.exports}o.m=a,e=[],o.O=(r,t,a,i)=>{if(!t){var l=1/0;for(s=0;s<e.length;s++){for(var[t,a,i]=e[s],n=!0,d=0;d<t.length;d++)(!1&i||l>=i)&&Object.keys(o.O).every(e=>o.O[e](t[d]))?t.splice(d--,1):(n=!1,i<l&&(l=i));if(n){e.splice(s--,1);var c=a();void 0!==c&&(r=c)}}return r}i=i||0;for(var s=e.length;s>0&&e[s-1][2]>i;s--)e[s]=e[s-1];e[s]=[t,a,i]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((r,t)=>(o.f[t](e,r),r),[])),o.u=e=>e+"-"+e+".js?v="+{2710:"0c2e26891ac1c05900e0",4471:"9b3c8620f038b7593241",7004:"da5a822695a273d4d2eb",7394:"5b773f16893ed80e0246",7859:"cd6f48c919ca307639eb",8127:"b62d5791b2d7256af4a8",8453:"0ad2c9a35eee895d5980"}[e],o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",o.l=(e,a,i,l)=>{if(r[e])r[e].push(a);else{var n,d;if(void 0!==i)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==t+i){n=f;break}}n||(d=!0,(n=document.createElement("script")).charset="utf-8",o.nc&&n.setAttribute("nonce",o.nc),n.setAttribute("data-webpack",t+i),n.src=e),r[e]=[a];var u=(t,a)=>{n.onerror=n.onload=null,clearTimeout(p);var i=r[e];if(delete r[e],n.parentNode&&n.parentNode.removeChild(n),i&&i.forEach(e=>e(a)),t)return t(a)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:n}),12e4);n.onerror=u.bind(null,n.onerror),n.onload=u.bind(null,n.onload),d&&document.head.appendChild(n)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.j=2277,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var a=t.length-1;a>-1&&(!e||!/^http(s?):/.test(e));)e=t[a--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{o.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={2277:0};o.f.j=(r,t)=>{var a=o.o(e,r)?e[r]:void 0;if(0!==a)if(a)t.push(a[2]);else{var i=new Promise((t,i)=>a=e[r]=[t,i]);t.push(a[2]=i);var l=o.p+o.u(r),n=new Error;o.l(l,t=>{if(o.o(e,r)&&(0!==(a=e[r])&&(e[r]=void 0),a)){var i=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;n.message="Loading chunk "+r+" failed.\n("+i+": "+l+")",n.name="ChunkLoadError",n.type=i,n.request=l,a[1](n)}},"chunk-"+r,r)}},o.O.j=r=>0===e[r];var r=(r,t)=>{var a,i,[l,n,d]=t,c=0;if(l.some(r=>0!==e[r])){for(a in n)o.o(n,a)&&(o.m[a]=n[a]);if(d)var s=d(o)}for(r&&r(t);c<l.length;c++)i=l[c],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(s)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),o.nc=void 0;var l=o.O(void 0,[4208],()=>o(48246));l=o.O(l)})();
//# sourceMappingURL=files-search.js.map?v=386c4bc97bbafabdb328

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more