mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
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:
commit
e5c08cca40
156 changed files with 1592 additions and 1315 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
139
apps/files/src/components/FileListFilter/FileListFilters.vue
Normal file
139
apps/files/src/components/FileListFilter/FileListFilters.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 || []
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup() {
|
||||
const fileListWidth = useFileListWidth()
|
||||
const { width: fileListWidth } = useFileListWidth()
|
||||
|
||||
return {
|
||||
fileListWidth,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
9
apps/files/src/shims.d.ts
vendored
Normal 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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
7
build/frontend-legacy/package-lock.json
generated
7
build/frontend-legacy/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
2
dist/1546-1546.js
vendored
File diff suppressed because one or more lines are too long
8
dist/1546-1546.js.license
vendored
8
dist/1546-1546.js.license
vendored
|
|
@ -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
|
||||
1
dist/1546-1546.js.map
vendored
1
dist/1546-1546.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/1546-1546.js.map.license
vendored
1
dist/1546-1546.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
1546-1546.js.license
|
||||
2
dist/2251-2251.js
vendored
2
dist/2251-2251.js
vendored
File diff suppressed because one or more lines are too long
1
dist/2251-2251.js.map
vendored
1
dist/2251-2251.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/2251-2251.js.map.license
vendored
1
dist/2251-2251.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
2251-2251.js.license
|
||||
2
dist/23-23.js
vendored
2
dist/23-23.js
vendored
File diff suppressed because one or more lines are too long
37
dist/23-23.js.license
vendored
37
dist/23-23.js.license
vendored
|
|
@ -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
1
dist/23-23.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/23-23.js.map.license
vendored
1
dist/23-23.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
23-23.js.license
|
||||
2
dist/7257-7257.js
vendored
2
dist/7257-7257.js
vendored
File diff suppressed because one or more lines are too long
1
dist/7257-7257.js.map
vendored
1
dist/7257-7257.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/7257-7257.js.map.license
vendored
1
dist/7257-7257.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
7257-7257.js.license
|
||||
2
dist/8127-8127.js
vendored
Normal file
2
dist/8127-8127.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
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
1
dist/8127-8127.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
8127-8127.js.license
|
||||
2
dist/8309-8309.js
vendored
Normal file
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
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
1
dist/8309-8309.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
8309-8309.js.license
|
||||
2
dist/9165-9165.js
vendored
2
dist/9165-9165.js
vendored
|
|
@ -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
|
||||
8
dist/9165-9165.js.license
vendored
8
dist/9165-9165.js.license
vendored
|
|
@ -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
|
||||
1
dist/9165-9165.js.map
vendored
1
dist/9165-9165.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/9165-9165.js.map.license
vendored
1
dist/9165-9165.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
9165-9165.js.license
|
||||
4
dist/9281-9281.js
vendored
4
dist/9281-9281.js
vendored
|
|
@ -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
|
||||
2
dist/9281-9281.js.map
vendored
2
dist/9281-9281.js.map
vendored
|
|
@ -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":""}
|
||||
4
dist/comments-comments-app.js
vendored
4
dist/comments-comments-app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-app.js.map
vendored
2
dist/comments-comments-app.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/comments-comments-tab.js
vendored
4
dist/comments-comments-tab.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-tab.js.map
vendored
2
dist/comments-comments-tab.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
3
dist/core-common.js.license
vendored
3
dist/core-common.js.license
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-legacy-unified-search.js
vendored
4
dist/core-legacy-unified-search.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-legacy-unified-search.js.map
vendored
2
dist/core-legacy-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login.js
vendored
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-public-page-menu.js
vendored
4
dist/core-public-page-menu.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-public-page-menu.js.map
vendored
2
dist/core-public-page-menu.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-public-page-user-menu.js
vendored
4
dist/core-public-page-user-menu.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-public-page-user-menu.js.map
vendored
2
dist/core-public-page-user-menu.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unified-search.js
vendored
4
dist/core-unified-search.js
vendored
File diff suppressed because one or more lines are too long
5
dist/core-unified-search.js.license
vendored
5
dist/core-unified-search.js.license
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
dist/core-unified-search.js.map
vendored
2
dist/core-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-update.js
vendored
4
dist/core-update.js
vendored
|
|
@ -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
|
||||
2
dist/core-update.js.map
vendored
2
dist/core-update.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
286
dist/files-init.js.license
vendored
286
dist/files-init.js.license
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-reference-files.js
vendored
4
dist/files-reference-files.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-reference-files.js.map
vendored
2
dist/files-reference-files.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-search.js
vendored
4
dist/files-search.js
vendored
|
|
@ -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
|
||||
2
dist/files-search.js.map
vendored
2
dist/files-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-settings-admin.js
vendored
4
dist/files-settings-admin.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-settings-admin.js.map
vendored
2
dist/files-settings-admin.js.map
vendored
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
Loading…
Reference in a new issue