Merge pull request #53826 from nextcloud/feat/search-while-filtering

This commit is contained in:
John Molakvoæ 2025-07-07 11:40:24 +02:00 committed by GitHub
commit 77939fad04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 281 additions and 234 deletions

View file

@ -281,7 +281,7 @@ export default defineComponent({
}
// Make sure we set the node as active
this.activeStore.setActiveNode(this.source)
this.activeStore.activeNode = this.source
// Execute the action
await executeAction(action)

View file

@ -315,7 +315,7 @@ export default defineComponent({
delete query.openfile
delete query.opendetails
this.activeStore.clearActiveNode()
this.activeStore.activeNode = undefined
window.OCP.Files.Router.goToRoute(
null,
{ ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') },
@ -449,7 +449,7 @@ export default defineComponent({
delete query.openfile
delete query.opendetails
this.activeStore.setActiveNode(node)
this.activeStore.activeNode = node
// Silent update of the URL
window.OCP.Files.Router.goToRoute(

View file

@ -13,15 +13,10 @@ import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSear
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { onBeforeNavigation } from '../composables/useBeforeNavigation.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useFilesStore } from '../store/files.ts'
import { useSearchStore } from '../store/search.ts'
import { VIEW_ID } from '../views/search.ts'
const { currentView } = useNavigation(true)
const { directory } = useRouteParameters()
const filesStore = useFilesStore()
const searchStore = useSearchStore()
/**
@ -55,44 +50,19 @@ onBeforeNavigation((to, from, next) => {
*/
const isSearchView = computed(() => currentView.value.id === VIEW_ID)
/**
* Local search is only possible on real DAV resources within the files root
*/
const canSearchLocally = computed(() => {
if (searchStore.base) {
return true
}
const folder = filesStore.getDirectoryByPath(currentView.value.id, directory.value)
return folder?.isDavResource && folder?.root?.startsWith('/files/')
})
/**
* Different searchbox label depending if filtering or searching
*/
const searchLabel = computed(() => {
if (searchStore.scope === 'globally') {
return t('files', 'Search globally by filename …')
} else if (searchStore.scope === 'locally') {
return t('files', 'Search here by filename …')
}
return t('files', 'Filter file names …')
return t('files', 'Search here by filename …')
})
/**
* Update the search value and set the base if needed
* @param value - The new value
*/
function onUpdateSearch(value: string) {
if (searchStore.scope === 'locally' && currentView.value.id !== VIEW_ID) {
searchStore.base = filesStore.getDirectoryByPath(currentView.value.id, directory.value)
}
searchStore.query = value
}
</script>
<template>
<NcAppNavigationSearch :label="searchLabel" :model-value="searchStore.query" @update:modelValue="onUpdateSearch">
<NcAppNavigationSearch v-model="searchStore.query" :label="searchLabel">
<template #actions>
<NcActions :aria-label="t('files', 'Search scope options')" :disabled="isSearchView">
<template #icon>
@ -102,13 +72,7 @@ function onUpdateSearch(value: string) {
<template #icon>
<NcIconSvgWrapper :path="mdiMagnify" />
</template>
{{ t('files', 'Filter in current view') }}
</NcActionButton>
<NcActionButton v-if="canSearchLocally" close-after-click @click="searchStore.scope = 'locally'">
<template #icon>
<NcIconSvgWrapper :path="mdiMagnify" />
</template>
{{ t('files', 'Search from this location') }}
{{ t('files', 'Filter and search from this location') }}
</NcActionButton>
<NcActionButton close-after-click @click="searchStore.scope = 'globally'">
<template #icon>

View file

@ -2,25 +2,55 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
import type { ContentsWithRoot, File, Folder, Node } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import { davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { defaultRootPath, getDefaultPropfind, resultToNode as davResultToNode } from '@nextcloud/files/dav'
import { CancelablePromise } from 'cancelable-promise'
import { join } from 'path'
import { client } from './WebdavClient.ts'
import { searchNodes } from './WebDavSearch.ts'
import { getPinia } from '../store/index.ts'
import { useFilesStore } from '../store/files.ts'
import { useSearchStore } from '../store/search.ts'
import logger from '../logger.ts'
/**
* Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map`
* @param stat The result returned by the webdav library
*/
export const resultToNode = (stat: FileStat): File | Folder => davResultToNode(stat)
export const resultToNode = (stat: FileStat): Node => davResultToNode(stat)
export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
path = join(davRootPath, path)
/**
* Get contents implementation for the files view.
* This also allows to fetch local search results when the user is currently filtering.
*
* @param path - The path to query
*/
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
const controller = new AbortController()
const propfindPayload = davGetDefaultPropfind()
const searchStore = useSearchStore(getPinia())
if (searchStore.query.length >= 3) {
return new CancelablePromise((resolve, reject, cancel) => {
cancel(() => controller.abort())
getLocalSearch(path, searchStore.query, controller.signal)
.then(resolve)
.catch(reject)
})
} else {
return defaultGetContents(path)
}
}
/**
* Generic `getContents` implementation for the users files.
*
* @param path - The path to get the contents
*/
export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> {
path = join(defaultRootPath, path)
const controller = new AbortController()
const propfindPayload = getDefaultPropfind()
return new CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort())
@ -56,3 +86,25 @@ export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> =>
}
})
}
/**
* Get the local search results for the current folder.
*
* @param path - The path
* @param query - The current search query
* @param signal - The aboort signal
*/
async function getLocalSearch(path: string, query: string, signal: AbortSignal): Promise<ContentsWithRoot> {
const filesStore = useFilesStore(getPinia())
let folder = filesStore.getDirectoryByPath('files', path)
if (!folder) {
const rootPath = join(defaultRootPath, path)
const stat = await client.stat(rootPath, { details: true }) as ResponseDataDetailed<FileStat>
folder = resultToNode(stat.data) as Folder
}
const contents = await searchNodes(query, { dir: path, signal })
return {
folder,
contents,
}
}

View file

@ -57,8 +57,8 @@ describe('HotKeysService testing', () => {
})
// Setting the view first as it reset the active node
activeStore.onChangedView(view)
activeStore.setActiveNode(file)
activeStore.activeView = view
activeStore.activeNode = file
window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
// We only mock what needed, we do not need Files.Router.goTo or Files.Navigation

View file

@ -21,12 +21,11 @@ export function getContents(): CancelablePromise<ContentsWithRoot> {
const controller = new AbortController()
const searchStore = useSearchStore(getPinia())
const dir = searchStore.base?.path
return new CancelablePromise<ContentsWithRoot>(async (resolve, reject, cancel) => {
cancel(() => controller.abort())
try {
const contents = await searchNodes(searchStore.query, { dir, signal: controller.signal })
const contents = await searchNodes(searchStore.query, { signal: controller.signal })
resolve({
contents,
folder: new Folder({

View file

@ -3,74 +3,84 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ActiveStore } from '../types.ts'
import type { FileAction, Node, View } from '@nextcloud/files'
import type { FileAction, View, Node, Folder } from '@nextcloud/files'
import { defineStore } from 'pinia'
import { getNavigation } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import { getNavigation } from '@nextcloud/files'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import logger from '../logger.ts'
export const useActiveStore = function(...args) {
const store = defineStore('active', {
state: () => ({
_initialized: false,
activeNode: null,
activeView: null,
activeAction: null,
} as ActiveStore),
export const useActiveStore = defineStore('active', () => {
/**
* The currently active action
*/
const activeAction = ref<FileAction>()
actions: {
setActiveNode(node: Node) {
if (!node) {
throw new Error('Use clearActiveNode to clear the active node')
}
logger.debug('Setting active node', { node })
this.activeNode = node
},
/**
* The currently active folder
*/
const activeFolder = ref<Folder>()
clearActiveNode() {
this.activeNode = null
},
/**
* The current active node within the folder
*/
const activeNode = ref<Node>()
onDeletedNode(node: Node) {
if (this.activeNode && this.activeNode.source === node.source) {
this.clearActiveNode()
}
},
/**
* The current active view
*/
const activeView = ref<View>()
setActiveAction(action: FileAction) {
this.activeAction = action
},
initialize()
clearActiveAction() {
this.activeAction = null
},
/**
* Unset the active node if deleted
*
* @param node - The node thats deleted
* @private
*/
function onDeletedNode(node: Node) {
if (activeNode.value && activeNode.value.source === node.source) {
activeNode.value = undefined
}
}
onChangedView(view: View|null = null) {
logger.debug('Setting active view', { view })
this.activeView = view
this.clearActiveNode()
},
},
})
/**
* Callback to update the current active view
*
* @param view - The new active view
* @private
*/
function onChangedView(view: View|null = null) {
logger.debug('Setting active view', { view })
activeView.value = view ?? undefined
activeNode.value = undefined
}
const activeStore = store(...args)
const navigation = getNavigation()
/**
* Initalize the store - connect all event listeners.
* @private
*/
function initialize() {
const navigation = getNavigation()
// Make sure we only register the listeners once
if (!activeStore._initialized) {
subscribe('files:node:deleted', activeStore.onDeletedNode)
// Make sure we only register the listeners once
subscribe('files:node:deleted', onDeletedNode)
activeStore._initialized = true
activeStore.onChangedView(navigation.active)
onChangedView(navigation.active)
// Or you can react to changes of the current active view
navigation.addEventListener('updateActive', (event) => {
activeStore.onChangedView(event.detail)
onChangedView(event.detail)
})
}
return activeStore
}
return {
activeAction,
activeFolder,
activeNode,
activeView,
}
})

View file

@ -154,7 +154,7 @@ export const useFilesStore = function(...args) {
}
// If we have only one node with the file ID, we can update it directly
if (node.source === nodes[0].source) {
if (nodes.length === 1 && node.source === nodes[0].source) {
this.updateNodes([node])
return
}

View file

@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { INode, View } from '@nextcloud/files'
import type RouterService from '../services/RouterService'
import type { SearchScope } from '../types'
import type { View } from '@nextcloud/files'
import type RouterService from '../services/RouterService.ts'
import type { SearchScope } from '../types.ts'
import { emit, subscribe } from '@nextcloud/event-bus'
import debounce from 'debounce'
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { VIEW_ID } from '../views/search'
import logger from '../logger'
import debounce from 'debounce'
import { VIEW_ID } from '../views/search.ts'
import logger from '../logger.ts'
export const useSearchStore = defineStore('search', () => {
/**
@ -20,28 +20,16 @@ export const useSearchStore = defineStore('search', () => {
*/
const query = ref('')
/**
* Where to start the search
*/
const base = ref<INode>()
/**
* Scope of the search.
* Scopes:
* - filter: only filter current file list
* - locally: search from current location recursivly
* - globally: search everywhere
*/
const scope = ref<SearchScope>('filter')
// reset the base if query is cleared
watch(scope, () => {
if (scope.value !== 'locally') {
base.value = undefined
}
updateSearch()
})
watch(scope, updateSearch)
watch(query, (old, current) => {
// skip if only whitespaces changed
@ -59,13 +47,12 @@ export const useSearchStore = defineStore('search', () => {
* Debounced update of the current route
* @private
*/
const updateRouter = debounce((isSearch: boolean, fileid?: number) => {
const updateRouter = debounce((isSearch: boolean) => {
const router = window.OCP.Files.Router as RouterService
router.goToRoute(
undefined,
{
view: VIEW_ID,
...(fileid === undefined ? {} : { fileid: String(fileid) }),
},
{
query: query.value,
@ -106,12 +93,10 @@ export const useSearchStore = defineStore('search', () => {
return
}
// we only use the directory if we search locally
const fileid = scope.value === 'locally' ? base.value?.fileid : undefined
const isSearch = router.params.view === VIEW_ID
logger.debug('Update route for updated search query', { query: query.value, fileid, isSearch })
updateRouter(isSearch, fileid)
logger.debug('Update route for updated search query', { query: query.value, isSearch })
updateRouter(isSearch)
}
/**
@ -162,7 +147,6 @@ export const useSearchStore = defineStore('search', () => {
}
return {
base,
query,
scope,
}

View file

@ -107,16 +107,16 @@ export interface DragAndDropStore {
// Active node store
export interface ActiveStore {
_initialized: boolean
activeAction: FileAction|null
activeFolder: Folder|null
activeNode: Node|null
activeView: View|null
activeAction: FileAction|null
}
/**
* Search scope for the in-files-search
*/
export type SearchScope = 'filter'|'locally'|'globally'
export type SearchScope = 'filter'|'globally'
export interface TemplateFile {
app: string

View file

@ -49,7 +49,7 @@ export const executeAction = async (action: FileAction) => {
try {
// Set the loading marker
Vue.set(currentNode, 'status', NodeStatus.LOADING)
activeStore.setActiveAction(action)
activeStore.activeAction = action
const success = await action.exec(currentNode, currentView, currentDir)
@ -69,6 +69,6 @@ export const executeAction = async (action: FileAction) => {
} finally {
// Reset the loading marker
Vue.set(currentNode, 'status', undefined)
activeStore.clearActiveAction()
activeStore.activeAction = undefined
}
}

View file

@ -155,7 +155,7 @@ import { getCapabilities } from '@nextcloud/capabilities'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Node, Permission, sortNodes, getFileListActions } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { join, dirname, normalize } from 'path'
import { join, dirname, normalize, relative } from 'path'
import { showError, showSuccess, showWarning } from '@nextcloud/dialogs'
import { ShareType } from '@nextcloud/sharing'
import { UploadPicker, UploadStatus } from '@nextcloud/upload'
@ -188,6 +188,7 @@ import { useFiltersStore } from '../store/filters.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { usePathsStore } from '../store/paths.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useActiveStore } from '../store/active.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
@ -240,6 +241,8 @@ export default defineComponent({
const { currentView } = useNavigation()
const { directory, fileId } = useRouteParameters()
const fileListWidth = useFileListWidth()
const activeStore = useActiveStore()
const filesStore = useFilesStore()
const filtersStore = useFiltersStore()
const pathsStore = usePathsStore()
@ -259,6 +262,7 @@ export default defineComponent({
headers: useFileListHeaders(),
t,
activeStore,
filesStore,
filtersStore,
pathsStore,
@ -322,7 +326,7 @@ export default defineComponent({
* The current folder.
*/
currentFolder(): Folder | undefined {
if (!this.currentView?.id) {
if (!this.currentView) {
return
}
@ -352,12 +356,28 @@ export default defineComponent({
return this.isAscSorting ? results : results.reverse()
}
return sortNodes(this.dirContentsFiltered, {
const nodes = sortNodes(this.dirContentsFiltered, {
sortFavoritesFirst: this.userConfig.sort_favorites_first,
sortFoldersFirst: this.userConfig.sort_folders_first,
sortingMode: this.sortingMode,
sortingOrder: this.isAscSorting ? 'asc' : 'desc',
})
// TODO upstream this
if (this.currentView.id === 'files') {
nodes.sort((a, b) => {
const aa = relative(a.source, this.currentFolder!.source) === '..'
const bb = relative(b.source, this.currentFolder!.source) === '..'
if (aa && bb) {
return 0
} else if (aa) {
return -1
}
return 1
})
}
return nodes
},
/**
@ -492,6 +512,10 @@ export default defineComponent({
}
},
currentFolder() {
this.activeStore.activeFolder = this.currentFolder
},
currentView(newView, oldView) {
if (newView?.id === oldView?.id) {
return

View file

@ -7,7 +7,6 @@
import { mdiMagnifyClose } from '@mdi/js'
import { t } from '@nextcloud/l10n'
import debounce from 'debounce'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcInputField from '@nextcloud/vue/components/NcInputField'
@ -32,9 +31,6 @@ const debouncedUpdate = debounce((value: string) => {
:model-value="searchStore.query"
type="search"
@update:model-value="debouncedUpdate" />
<NcButton v-if="searchStore.scope === 'locally'" @click="searchStore.scope = 'globally'">
{{ t('files', 'Search globally') }}
</NcButton>
</div>
</template>
</NcEmptyContent>

View file

@ -3,9 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { emit, subscribe } from '@nextcloud/event-bus'
import { View, getNavigation } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { getContents } from '../services/Files.ts'
import { useActiveStore } from '../store/active.ts'
import { defaultView } from '../utils/filesViews.ts'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
@ -16,6 +18,9 @@ export const VIEW_ID = 'files'
* Register the files view to the navigation
*/
export function registerFilesView() {
// we cache the query to allow more performant search (see below in event listener)
let oldQuery = ''
const Navigation = getNavigation()
Navigation.register(new View({
id: VIEW_ID,
@ -28,4 +33,33 @@ export function registerFilesView() {
getContents,
}))
// when the search is updated
// and we are in the files view
// and there is already a folder fetched
// then we "update" it to trigger a new `getContents` call to search for the query while the filelist is filtered
subscribe('files:search:updated', ({ scope, query }) => {
if (scope === 'globally') {
return
}
if (Navigation.active?.id !== VIEW_ID) {
return
}
// If neither the old query nor the new query is longer than the search minimum
// then we do not need to trigger a new PROPFIND / SEARCH
// so we skip unneccessary requests here
if (oldQuery.length < 3 && query.length < 3) {
return
}
const store = useActiveStore()
if (!store.activeFolder) {
return
}
oldQuery = query
emit('files:node:updated', store.activeFolder)
})
}

View file

@ -37,8 +37,8 @@ describe('HotKeysService testing', () => {
})
// Setting the view first as it reset the active node
activeStore.onChangedView(view)
activeStore.setActiveNode(file)
activeStore.activeView = view
activeStore.activeNode = file
})
it('Pressing t should open the tag management dialog', () => {

View file

@ -17,11 +17,13 @@ describe('files: search', () => {
cy.createRandomUser().then(($user) => {
user = $user
cy.mkdir(user, '/some folder')
cy.mkdir(user, '/some folder/nested folder')
cy.mkdir(user, '/other folder')
cy.mkdir(user, '/12345')
cy.uploadContent(user, new Blob(['content']), 'text/plain', '/file.txt')
cy.uploadContent(user, new Blob(['content']), 'text/plain', '/some folder/a file.txt')
cy.uploadContent(user, new Blob(['content']), 'text/plain', '/some folder/a second file.txt')
cy.uploadContent(user, new Blob(['content']), 'text/plain', '/some folder/nested folder/deep file.txt')
cy.uploadContent(user, new Blob(['content']), 'text/plain', '/other folder/another file.txt')
cy.login(user)
})
@ -58,21 +60,16 @@ describe('files: search', () => {
getRowForFile('another file.txt').should('be.visible')
})
it('can search locally', () => {
it('filter does also search locally', () => {
navigateToFolder('some folder')
getRowForFile('a file.txt').should('be.visible')
navigation.searchScopeTrigger().click()
navigation.searchScopeMenu()
.should('be.visible')
.findByRole('menuitem', { name: /search from this location/i })
.should('be.visible')
.click()
navigation.searchInput().type('file')
getRowForFile('a file.txt').should('be.visible')
getRowForFile('a second file.txt').should('be.visible')
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2)
getRowForFile('deep file.txt').should('be.visible')
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 3)
})
it('See "search everywhere" button', () => {
@ -107,7 +104,8 @@ describe('files: search', () => {
// see local results
getRowForFile('a file.txt').should('be.visible')
getRowForFile('a second file.txt').should('be.visible')
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2)
getRowForFile('deep file.txt').should('be.visible')
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 3)
// toggle global search
cy.get('[data-cy-files-filters]')
@ -118,6 +116,7 @@ describe('files: search', () => {
// see global results
getRowForFile('file.txt').should('be.visible')
getRowForFile('a file.txt').should('be.visible')
getRowForFile('deep file.txt').should('be.visible')
getRowForFile('a second file.txt').should('be.visible')
getRowForFile('another file.txt').should('be.visible')
})
@ -129,49 +128,22 @@ describe('files: search', () => {
navigation.searchScopeTrigger().click()
navigation.searchScopeMenu()
.should('be.visible')
.findByRole('menuitem', { name: /search from this location/i })
.findByRole('menuitem', { name: /search globally/i })
.should('be.visible')
.click()
navigation.searchInput().type('folder')
navigation.searchInput().type('xyz')
// see the empty content message
cy.contains('[role="note"]', /No search results for .folder./)
cy.contains('[role="note"]', /No search results for .xyz./)
.should('be.visible')
.within(() => {
// see within there is a search box with the same value
cy.findByRole('searchbox', { name: /search for files/i })
.should('be.visible')
.and('have.value', 'folder')
// and we can switch from local to global search
cy.findByRole('button', { name: 'Search globally' })
.should('be.visible')
.and('have.value', 'xyz')
})
})
it('can turn local search into global search', () => {
navigateToFolder('some folder')
getRowForFile('a file.txt').should('be.visible')
navigation.searchScopeTrigger().click()
navigation.searchScopeMenu()
.should('be.visible')
.findByRole('menuitem', { name: /search from this location/i })
.should('be.visible')
.click()
navigation.searchInput().type('folder')
// see the empty content message and turn into global search
cy.contains('[role="note"]', /No search results for .folder./)
.should('be.visible')
.findByRole('button', { name: 'Search globally' })
.should('be.visible')
.click()
getRowForFile('some folder').should('be.visible')
getRowForFile('other folder').should('be.visible')
cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2)
})
it('can alter search', () => {
navigation.searchScopeTrigger().click()
navigation.searchScopeMenu()

2
dist/175-175.js vendored
View file

@ -1,2 +0,0 @@
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[175],{50540:(e,t,n)=>{n.d(t,{A:()=>l});var s=n(71354),a=n.n(s),r=n(76314),c=n.n(r)()(a());c.push([e.id,".search-empty-view__input[data-v-61d86e6e]{flex:0 1;min-width:min(400px,50vw)}.search-empty-view__wrapper[data-v-61d86e6e]{display:flex;flex-wrap:wrap;gap:10px;align-items:baseline}","",{version:3,sources:["webpack://./apps/files/src/views/SearchEmptyView.vue"],names:[],mappings:"AAEC,2CACC,QAAA,CACA,yBAAA,CAGD,6CACC,YAAA,CACA,cAAA,CACA,QAAA,CACA,oBAAA",sourcesContent:["\n.search-empty-view {\n\t&__input {\n\t\tflex: 0 1;\n\t\tmin-width: min(400px, 50vw);\n\t}\n\n\t&__wrapper {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tgap: 10px;\n\t\talign-items: baseline;\n\t}\n}\n"],sourceRoot:""}]);const l=c},60175:(e,t,n)=>{n.r(t),n.d(t,{default:()=>q});var s=n(85471),a=n(9165),r=n(53334),c=n(17334),l=n.n(c),p=n(97012),o=n(32190),i=n(6695),u=n(16879),A=n(4114),d=n(82736);const y=(0,s.pM)({__name:"SearchEmptyView",setup(e){const t=(0,d.j)((0,A.u)()),n=l()((e=>{t.query=e}),500);return{__sfc:!0,searchStore:t,debouncedUpdate:n,mdiMagnifyClose:a.WBH,t:r.t,NcButton:p.A,NcEmptyContent:o.A,NcIconSvgWrapper:i.A,NcInputField:u.A}}});var f=n(85072),m=n.n(f),h=n(97825),C=n.n(h),_=n(77659),v=n.n(_),w=n(55056),x=n.n(w),S=n(10540),b=n.n(S),g=n(41113),N=n.n(g),k=n(50540),E={};E.styleTagTransform=N(),E.setAttributes=x(),E.insert=v().bind(null,"head"),E.domAPI=C(),E.insertStyleElement=b(),m()(k.A,E),k.A&&k.A.locals&&k.A.locals;const q=(0,n(14486).A)(y,(function(){var e=this,t=e._self._c,n=e._self._setupProxy;return t(n.NcEmptyContent,{attrs:{name:n.t("files","No search results for “{query}”",{query:n.searchStore.query})},scopedSlots:e._u([{key:"icon",fn:function(){return[t(n.NcIconSvgWrapper,{attrs:{path:n.mdiMagnifyClose}})]},proxy:!0},{key:"action",fn:function(){return[t("div",{staticClass:"search-empty-view__wrapper"},[t(n.NcInputField,{staticClass:"search-empty-view__input",attrs:{label:n.t("files","Search for files"),"model-value":n.searchStore.query,type:"search"},on:{"update:model-value":n.debouncedUpdate}}),e._v(" "),"locally"===n.searchStore.scope?t(n.NcButton,{on:{click:function(e){n.searchStore.scope="globally"}}},[e._v("\n\t\t\t\t"+e._s(n.t("files","Search globally"))+"\n\t\t\t")]):e._e()],1)]},proxy:!0}])})}),[],!1,null,"61d86e6e",null).exports}}]);
//# sourceMappingURL=175-175.js.map?v=a61155e5a3ee1e107813

1
dist/175-175.js.map vendored

File diff suppressed because one or more lines are too long

View file

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

2
dist/7457-7457.js vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,7 @@ SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: string_decoder developers
SPDX-FileCopyrightText: inherits developers
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: debounce developers
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Sindre Sorhus
@ -16,9 +17,11 @@ SPDX-FileCopyrightText: Roeland Jago Douma
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Joyent
SPDX-FileCopyrightText: Jonas Schade <derzade@gmail.com>
SPDX-FileCopyrightText: Guillaume Chau
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
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
@ -65,6 +68,9 @@ This file is generated from multiple sources. Included packages:
- @nextcloud/sharing
- version: 0.2.4
- license: GPL-3.0-or-later
- @vue/devtools-api
- version: 6.6.3
- license: MIT
- base64-js
- version: 1.5.1
- license: MIT
@ -74,6 +80,9 @@ This file is generated from multiple sources. Included packages:
- cancelable-promise
- version: 4.3.1
- license: MIT
- debounce
- version: 2.2.0
- license: MIT
- dompurify
- version: 3.2.6
- license: (MPL-2.0 OR Apache-2.0)
@ -98,6 +107,9 @@ This file is generated from multiple sources. Included packages:
- path
- version: 0.12.7
- license: MIT
- pinia
- version: 2.3.1
- license: MIT
- process
- version: 0.11.10
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,t,r,o={3360:(e,t,r)=>{var o=r(85168),n=r(32981),a=r(53334);window.addEventListener("DOMContentLoaded",(function(){const{updateLink:e,updateVersion:t}=(0,n.C)("updatenotification","updateState"),r=(0,a.t)("core","{version} is available. Get more information on how to update.",{version:t});(0,o.cf)(r,{onClick:()=>window.open(e,"_blank")})}))}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=o,e=[],a.O=(t,r,o,n)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){r=e[u][0],o=e[u][1],n=e[u][2];for(var d=!0,l=0;l<r.length;l++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](r[l])))?r.splice(l--,1):(d=!1,n<i&&(i=n));if(d){e.splice(u--,1);var c=o();void 0!==c&&(t=c)}}return t}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[r,o,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,r)=>(a.f[r](e,t),t)),[])),a.u=e=>e+"-"+e+".js?v="+{640:"b2fa23a809053c6305c5",5771:"a4e2a98efcfb7393c5bd",5810:"f63f10359069f886ce52",7432:"bf576075b1d8131aa273"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,o,n,i)=>{if(t[e])t[e].push(o);else{var d,l;if(void 0!==n)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")==r+n){d=s;break}}d||(l=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,a.nc&&d.setAttribute("nonce",a.nc),d.setAttribute("data-webpack",r+n),d.src=e),t[e]=[o];var p=(r,o)=>{d.onerror=d.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],d.parentNode&&d.parentNode.removeChild(d),n&&n.forEach((e=>e(o))),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=p.bind(null,d.onerror),d.onload=p.bind(null,d.onload),l&&document.head.appendChild(d)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=5169,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={5169:0};a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise(((r,n)=>o=e[t]=[r,n]));r.push(o[2]=n);var i=a.p+a.u(t),d=new Error;a.l(i,(r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;d.message="Loading chunk "+t+" failed.\n("+n+": "+i+")",d.name="ChunkLoadError",d.type=n,d.request=i,o[1](d)}}),"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,i=r[0],d=r[1],l=r[2],c=0;if(i.some((t=>0!==e[t]))){for(o in d)a.o(d,o)&&(a.m[o]=d[o]);if(l)var u=l(a)}for(t&&t(r);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(3360)));i=a.O(i)})();
//# sourceMappingURL=updatenotification-update-notification-legacy.js.map?v=76c12c6b7f3201af55ae
(()=>{"use strict";var e,t,r,o={3360:(e,t,r)=>{var o=r(85168),n=r(32981),a=r(53334);window.addEventListener("DOMContentLoaded",(function(){const{updateLink:e,updateVersion:t}=(0,n.C)("updatenotification","updateState"),r=(0,a.t)("core","{version} is available. Get more information on how to update.",{version:t});(0,o.cf)(r,{onClick:()=>window.open(e,"_blank")})}))}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=o,e=[],a.O=(t,r,o,n)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){r=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,c=0;c<r.length;c++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](r[c])))?r.splice(c--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var d=o();void 0!==d&&(t=d)}}return t}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[r,o,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,r)=>(a.f[r](e,t),t)),[])),a.u=e=>e+"-"+e+".js?v="+{640:"b2fa23a809053c6305c5",5771:"a4e2a98efcfb7393c5bd",5810:"f63f10359069f886ce52",7432:"bf576075b1d8131aa273"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,o,n,i)=>{if(t[e])t[e].push(o);else{var l,c;if(void 0!==n)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==r+n){l=s;break}}l||(c=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",r+n),l.src=e),t[e]=[o];var p=(r,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),r)return r(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),c&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=5169,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={5169:0};a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise(((r,n)=>o=e[t]=[r,n]));r.push(o[2]=n);var i=a.p+a.u(t),l=new Error;a.l(i,(r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;l.message="Loading chunk "+t+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,i=r[0],l=r[1],c=r[2],d=0;if(i.some((t=>0!==e[t]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(c)var u=c(a)}for(t&&t(r);d<i.length;d++)n=i[d],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(3360)));i=a.O(i)})();
//# sourceMappingURL=updatenotification-update-notification-legacy.js.map?v=4c377cdebe46c2279c41

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long