Merge pull request #41609 from nextcloud/enh/in-app-search

feat: In app search
This commit is contained in:
Simon L 2024-06-27 13:34:05 +02:00 committed by GitHub
commit a184161978
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 870 additions and 293 deletions

View file

@ -464,7 +464,7 @@ export default defineComponent({
logger.debug('View changed', { newView, oldView })
this.selectionStore.reset()
this.resetSearch()
this.triggerResetSearch()
this.fetchContent()
},
@ -472,7 +472,7 @@ export default defineComponent({
logger.debug('Directory changed', { newDir, oldDir })
// TODO: preserve selection on browsing?
this.selectionStore.reset()
this.resetSearch()
this.triggerResetSearch()
this.fetchContent()
// Scroll to top, force virtual scroller to re-render
@ -493,8 +493,8 @@ export default defineComponent({
subscribe('files:node:deleted', this.onNodeDeleted)
subscribe('files:node:updated', this.onUpdatedNode)
subscribe('nextcloud:unified-search.search', this.onSearch)
subscribe('nextcloud:unified-search.reset', this.resetSearch)
subscribe('nextcloud:unified-search:search', this.onSearch)
subscribe('nextcloud:unified-search:reset', this.onResetSearch)
// reload on settings change
this.unsubscribeStoreCallback = this.userConfigStore.$subscribe(() => this.fetchContent(), { deep: true })
@ -503,8 +503,8 @@ export default defineComponent({
unmounted() {
unsubscribe('files:node:deleted', this.onNodeDeleted)
unsubscribe('files:node:updated', this.onUpdatedNode)
unsubscribe('nextcloud:unified-search.search', this.onSearch)
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
unsubscribe('nextcloud:unified-search:search', this.onSearch)
unsubscribe('nextcloud:unified-search:reset', this.onResetSearch)
this.unsubscribeStoreCallback()
},
@ -676,15 +676,23 @@ export default defineComponent({
},
/**
* Reset the search query
* Handle reset search query event
*/
resetSearch() {
onResetSearch() {
// Reset debounced calls to not set the query again
this.onSearch.clear()
// Reset filter query
this.filterText = ''
},
/**
* Trigger a reset of the local search (part of unified search)
* This is usful to reset the search on directory / view change
*/
triggerResetSearch() {
emit('nextcloud:unified-search:reset')
},
openSharingSidebar() {
if (!this.currentFolder) {
logger.debug('No current folder found for opening sharing sidebar')

View file

@ -5,4 +5,4 @@
*//*!
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/#header,#expanddiv{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#header a:not(.button):focus-visible,#header button:not(.button-vue):focus-visible,#header div[role=button]:focus-visible,#expanddiv a:not(.button):focus-visible,#expanddiv button:not(.button-vue):focus-visible,#expanddiv div[role=button]:focus-visible{outline:none}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#header div[role=button]:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after,#expanddiv div[role=button]:focus-visible::after{content:" ";position:absolute;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);left:50%;opacity:1}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after{bottom:2px}#header .header-right a:not(.button):focus-visible::after,#header .header-right div[role=button]:focus-visible::after,#expanddiv .header-right a:not(.button):focus-visible::after,#expanddiv .header-right div[role=button]:focus-visible::after{bottom:4px}#header .header-right #expand.menutoggle:focus-visible::after,#expanddiv .header-right #expand.menutoggle:focus-visible::after{left:40%}#body-user #header,#body-settings #header,#body-public #header{display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:50px;box-sizing:border-box;justify-content:space-between}#nextcloud{padding:5px 0;padding-left:86px;position:relative;height:calc(100% - 4px);box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#nextcloud:hover,#nextcloud:active{opacity:1}#header .header-right>div>.menu{background-color:var(--color-main-background);filter:drop-shadow(0 1px 5px var(--color-box-shadow));border-radius:var(--border-radius-large);box-sizing:border-box;z-index:2000;position:absolute;max-width:350px;min-height:66px;max-height:calc(100vh - 50px - 8px);right:8px;top:50px;margin:0;overflow-y:auto}#header .header-right>div>.menu:not(.popovermenu){display:none}#header .header-right>div>.menu:after{border:10px solid rgba(0,0,0,0);border-bottom-color:var(--color-main-background);bottom:100%;content:" ";height:0;width:0;position:absolute;pointer-events:none;right:10px}#header .header-right>div>.menu>div,#header .header-right>div>.menu>ul{-webkit-overflow-scrolling:touch;min-height:66px;max-height:calc(100vh - 50px - 8px)}#header .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;left:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header .header-appname-container{display:none;padding-right:10px;flex-shrink:0}#header #header-left,#header .header-left,#header #header-right,#header .header-right{display:inline-flex;align-items:center}#header #header-left,#header .header-left{flex:1 0;white-space:nowrap;min-width:0}#header #header-right,#header .header-right{justify-content:flex-end;flex-shrink:1}#header .header-right>.header-menu__trigger img{filter:var(--background-image-invert-if-bright)}#header .header-right>div,#header .header-right>form{height:100%;position:relative}#header .header-right>div>.menutoggle,#header .header-right>form>.menutoggle{display:flex;justify-content:center;align-items:center;width:50px;height:44px;cursor:pointer;opacity:.85;padding:0;margin:2px 0}#header .header-right>div>.menutoggle:focus,#header .header-right>form>.menutoggle:focus{opacity:1}#header .header-right>div>.menutoggle:focus-visible,#header .header-right>form>.menutoggle:focus-visible{outline:none}.header-appname-container .header-appname{opacity:.75}.header-appname{color:var(--color-primary-element-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-right:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}.header-info{display:flex;flex-direction:column;overflow:hidden}.header-title{overflow:hidden;text-overflow:ellipsis}.header-shared-by{color:var(--color-primary-element-text);position:relative;font-weight:300;font-size:11px;line-height:11px;overflow:hidden;text-overflow:ellipsis}#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;left:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:50px}header #emptycontent h2,header .emptycontent h2{font-weight:normal;font-size:16px}header #emptycontent [class^=icon-],header #emptycontent [class*=icon-],header .emptycontent [class^=icon-],header .emptycontent [class*=icon-]{background-size:48px;height:48px;width:48px}/*# sourceMappingURL=header.css.map */
*/#header,#expanddiv{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#header a:not(.button):focus-visible,#header button:not(.button-vue):focus-visible,#header div[role=button]:focus-visible,#expanddiv a:not(.button):focus-visible,#expanddiv button:not(.button-vue):focus-visible,#expanddiv div[role=button]:focus-visible{outline:none}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#header div[role=button]:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after,#expanddiv div[role=button]:focus-visible::after{content:" ";position:absolute;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);left:50%;opacity:1}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after{bottom:2px}#header .header-right,#expanddiv .header-right{margin-inline-end:calc(3*var(--default-grid-baseline))}#header .header-right a:not(.button):focus-visible::after,#header .header-right div[role=button]:focus-visible::after,#expanddiv .header-right a:not(.button):focus-visible::after,#expanddiv .header-right div[role=button]:focus-visible::after{bottom:4px}#header .header-right #expand.menutoggle:focus-visible::after,#expanddiv .header-right #expand.menutoggle:focus-visible::after{left:40%}#body-user #header,#body-settings #header,#body-public #header{display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:50px;box-sizing:border-box;justify-content:space-between}#nextcloud{padding:5px 0;padding-left:86px;position:relative;height:calc(100% - 4px);box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#nextcloud:hover,#nextcloud:active{opacity:1}#header .header-right>div>.menu{background-color:var(--color-main-background);filter:drop-shadow(0 1px 5px var(--color-box-shadow));border-radius:var(--border-radius-large);box-sizing:border-box;z-index:2000;position:absolute;max-width:350px;min-height:66px;max-height:calc(100vh - 50px - 8px);right:8px;top:50px;margin:0;overflow-y:auto}#header .header-right>div>.menu:not(.popovermenu){display:none}#header .header-right>div>.menu:after{border:10px solid rgba(0,0,0,0);border-bottom-color:var(--color-main-background);bottom:100%;content:" ";height:0;width:0;position:absolute;pointer-events:none;right:10px}#header .header-right>div>.menu>div,#header .header-right>div>.menu>ul{-webkit-overflow-scrolling:touch;min-height:66px;max-height:calc(100vh - 50px - 8px)}#header .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;left:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header .header-appname-container{display:none;padding-right:10px;flex-shrink:0}#header #header-left,#header .header-left,#header #header-right,#header .header-right{display:inline-flex;align-items:center}#header #header-left,#header .header-left{flex:1 0;white-space:nowrap;min-width:0}#header #header-right,#header .header-right{justify-content:flex-end;flex-shrink:1}#header .header-right>.header-menu__trigger img{filter:var(--background-image-invert-if-bright)}#header .header-right>div,#header .header-right>form{height:100%;position:relative}#header .header-right>div>.menutoggle,#header .header-right>form>.menutoggle{display:flex;justify-content:center;align-items:center;width:50px;height:44px;cursor:pointer;opacity:.85;padding:0;margin:2px 0}#header .header-right>div>.menutoggle:focus,#header .header-right>form>.menutoggle:focus{opacity:1}#header .header-right>div>.menutoggle:focus-visible,#header .header-right>form>.menutoggle:focus-visible{outline:none}.header-appname-container .header-appname{opacity:.75}.header-appname{color:var(--color-primary-element-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-right:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}.header-info{display:flex;flex-direction:column;overflow:hidden}.header-title{overflow:hidden;text-overflow:ellipsis}.header-shared-by{color:var(--color-primary-element-text);position:relative;font-weight:300;font-size:11px;line-height:11px;overflow:hidden;text-overflow:ellipsis}#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;left:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:50px}header #emptycontent h2,header .emptycontent h2{font-weight:normal;font-size:16px}header #emptycontent [class^=icon-],header #emptycontent [class*=icon-],header .emptycontent [class^=icon-],header .emptycontent [class*=icon-]{background-size:48px;height:48px;width:48px}/*# sourceMappingURL=header.css.map */

View file

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["header.scss","variables.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAQA,mBAEC,yBACA,sBACA,qBACA,6PACC,aAGD,+QACC,YACA,kBACA,2BACA,WACA,WACA,kBACA,oDACA,SACA,UAGD,gLACC,WAIA,kPACC,WAGD,+HACC,SAOH,+DAGC,oBACA,kBACA,MACA,WACA,aACA,OCmCe,KDlCf,sBACA,8BAID,WACC,cACA,kBACA,kBACA,wBACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mCACC,UAaD,gCACC,8CACA,sDACA,yCACA,sBACA,aACA,kBACA,gBAfD,gBACA,oCAgBC,UACA,ICRc,KDSd,SACA,gBAEA,kDACC,aAID,sCACC,gCACA,iDACA,YACA,YACA,SACA,QACA,kBACA,oBACA,WAGD,uEAEC,iCAzCF,gBACA,oCA4CA,cACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,UACA,QACA,WAEA,gFAGD,kCACC,aACA,mBACA,cAGD,sFAEC,oBACA,mBAGD,0CACC,SACA,mBACA,YAGD,4CACC,yBACA,cAKA,gDACC,gDAED,qDAEC,YACA,kBACA,6EACC,aACA,uBACA,mBACA,MCtFY,KDuFZ,YACA,eACA,YACA,UACA,aAEA,yFACC,UAGD,yGACC,aASL,0CACC,YAKD,gBACC,wCACA,eACA,iBACA,SACA,UACA,kBACA,gBACA,uBAEA,cAGD,aACC,aACA,sBACA,gBAGD,cACC,gBACA,uBAGD,kBACC,wCACA,kBACA,gBACA,eACA,iBACA,gBACA,uBAID,cACC,kBACA,gBACA,aACA,WACA,SACA,aACA,aACA,eACA,SAEA,2BACC,IChKc,KDuKf,gDACC,mBACA,eAED,gJAEC,qBACA,YACA","file":"header.css"}
{"version":3,"sourceRoot":"","sources":["header.scss","variables.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAQA,mBAEC,yBACA,sBACA,qBACA,6PACC,aAGD,+QACC,YACA,kBACA,2BACA,WACA,WACA,kBACA,oDACA,SACA,UAGD,gLACC,WAGD,+CAEC,uDAEA,kPACC,WAGD,+HACC,SAOH,+DAGC,oBACA,kBACA,MACA,WACA,aACA,OCgCe,KD/Bf,sBACA,8BAID,WACC,cACA,kBACA,kBACA,wBACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mCACC,UAaD,gCACC,8CACA,sDACA,yCACA,sBACA,aACA,kBACA,gBAfD,gBACA,oCAgBC,UACA,ICXc,KDYd,SACA,gBAEA,kDACC,aAID,sCACC,gCACA,iDACA,YACA,YACA,SACA,QACA,kBACA,oBACA,WAGD,uEAEC,iCAzCF,gBACA,oCA4CA,cACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,UACA,QACA,WAEA,gFAGD,kCACC,aACA,mBACA,cAGD,sFAEC,oBACA,mBAGD,0CACC,SACA,mBACA,YAGD,4CACC,yBACA,cAKA,gDACC,gDAED,qDAEC,YACA,kBACA,6EACC,aACA,uBACA,mBACA,MCzFY,KD0FZ,YACA,eACA,YACA,UACA,aAEA,yFACC,UAGD,yGACC,aASL,0CACC,YAKD,gBACC,wCACA,eACA,iBACA,SACA,UACA,kBACA,gBACA,uBAEA,cAGD,aACC,aACA,sBACA,gBAGD,cACC,gBACA,uBAGD,kBACC,wCACA,kBACA,gBACA,eACA,iBACA,gBACA,uBAID,cACC,kBACA,gBACA,aACA,WACA,SACA,aACA,aACA,eACA,SAEA,2BACC,ICnKc,KD0Kf,gDACC,mBACA,eAED,gJAEC,qBACA,YACA","file":"header.css"}

View file

@ -32,6 +32,9 @@
}
.header-right {
// Add some spacing so the last entry looks ok
margin-inline-end: calc(3 * var(--default-grid-baseline));
a:not(.button):focus-visible::after, div[role=button]:focus-visible::after {
bottom: 4px;
}

View file

@ -21,7 +21,7 @@
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/#header,#expanddiv{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#header a:not(.button):focus-visible,#header button:not(.button-vue):focus-visible,#header div[role=button]:focus-visible,#expanddiv a:not(.button):focus-visible,#expanddiv button:not(.button-vue):focus-visible,#expanddiv div[role=button]:focus-visible{outline:none}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#header div[role=button]:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after,#expanddiv div[role=button]:focus-visible::after{content:" ";position:absolute;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);left:50%;opacity:1}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after{bottom:2px}#header .header-right a:not(.button):focus-visible::after,#header .header-right div[role=button]:focus-visible::after,#expanddiv .header-right a:not(.button):focus-visible::after,#expanddiv .header-right div[role=button]:focus-visible::after{bottom:4px}#header .header-right #expand.menutoggle:focus-visible::after,#expanddiv .header-right #expand.menutoggle:focus-visible::after{left:40%}#body-user #header,#body-settings #header,#body-public #header{display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:50px;box-sizing:border-box;justify-content:space-between}#nextcloud{padding:5px 0;padding-left:86px;position:relative;height:calc(100% - 4px);box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#nextcloud:hover,#nextcloud:active{opacity:1}#header .header-right>div>.menu{background-color:var(--color-main-background);filter:drop-shadow(0 1px 5px var(--color-box-shadow));border-radius:var(--border-radius-large);box-sizing:border-box;z-index:2000;position:absolute;max-width:350px;min-height:66px;max-height:calc(100vh - 50px - 8px);right:8px;top:50px;margin:0;overflow-y:auto}#header .header-right>div>.menu:not(.popovermenu){display:none}#header .header-right>div>.menu:after{border:10px solid rgba(0,0,0,0);border-bottom-color:var(--color-main-background);bottom:100%;content:" ";height:0;width:0;position:absolute;pointer-events:none;right:10px}#header .header-right>div>.menu>div,#header .header-right>div>.menu>ul{-webkit-overflow-scrolling:touch;min-height:66px;max-height:calc(100vh - 50px - 8px)}#header .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;left:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header .header-appname-container{display:none;padding-right:10px;flex-shrink:0}#header #header-left,#header .header-left,#header #header-right,#header .header-right{display:inline-flex;align-items:center}#header #header-left,#header .header-left{flex:1 0;white-space:nowrap;min-width:0}#header #header-right,#header .header-right{justify-content:flex-end;flex-shrink:1}#header .header-right>.header-menu__trigger img{filter:var(--background-image-invert-if-bright)}#header .header-right>div,#header .header-right>form{height:100%;position:relative}#header .header-right>div>.menutoggle,#header .header-right>form>.menutoggle{display:flex;justify-content:center;align-items:center;width:50px;height:44px;cursor:pointer;opacity:.85;padding:0;margin:2px 0}#header .header-right>div>.menutoggle:focus,#header .header-right>form>.menutoggle:focus{opacity:1}#header .header-right>div>.menutoggle:focus-visible,#header .header-right>form>.menutoggle:focus-visible{outline:none}.header-appname-container .header-appname{opacity:.75}.header-appname{color:var(--color-primary-element-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-right:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}.header-info{display:flex;flex-direction:column;overflow:hidden}.header-title{overflow:hidden;text-overflow:ellipsis}.header-shared-by{color:var(--color-primary-element-text);position:relative;font-weight:300;font-size:11px;line-height:11px;overflow:hidden;text-overflow:ellipsis}#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;left:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:50px}header #emptycontent h2,header .emptycontent h2{font-weight:normal;font-size:16px}header #emptycontent [class^=icon-],header #emptycontent [class*=icon-],header .emptycontent [class^=icon-],header .emptycontent [class*=icon-]{background-size:48px;height:48px;width:48px}/*!
*/#header,#expanddiv{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#header a:not(.button):focus-visible,#header button:not(.button-vue):focus-visible,#header div[role=button]:focus-visible,#expanddiv a:not(.button):focus-visible,#expanddiv button:not(.button-vue):focus-visible,#expanddiv div[role=button]:focus-visible{outline:none}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#header div[role=button]:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after,#expanddiv div[role=button]:focus-visible::after{content:" ";position:absolute;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);left:50%;opacity:1}#header a:not(.button):focus-visible::after,#header .button-vue:focus-visible::after,#expanddiv a:not(.button):focus-visible::after,#expanddiv .button-vue:focus-visible::after{bottom:2px}#header .header-right,#expanddiv .header-right{margin-inline-end:calc(3*var(--default-grid-baseline))}#header .header-right a:not(.button):focus-visible::after,#header .header-right div[role=button]:focus-visible::after,#expanddiv .header-right a:not(.button):focus-visible::after,#expanddiv .header-right div[role=button]:focus-visible::after{bottom:4px}#header .header-right #expand.menutoggle:focus-visible::after,#expanddiv .header-right #expand.menutoggle:focus-visible::after{left:40%}#body-user #header,#body-settings #header,#body-public #header{display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:50px;box-sizing:border-box;justify-content:space-between}#nextcloud{padding:5px 0;padding-left:86px;position:relative;height:calc(100% - 4px);box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#nextcloud:hover,#nextcloud:active{opacity:1}#header .header-right>div>.menu{background-color:var(--color-main-background);filter:drop-shadow(0 1px 5px var(--color-box-shadow));border-radius:var(--border-radius-large);box-sizing:border-box;z-index:2000;position:absolute;max-width:350px;min-height:66px;max-height:calc(100vh - 50px - 8px);right:8px;top:50px;margin:0;overflow-y:auto}#header .header-right>div>.menu:not(.popovermenu){display:none}#header .header-right>div>.menu:after{border:10px solid rgba(0,0,0,0);border-bottom-color:var(--color-main-background);bottom:100%;content:" ";height:0;width:0;position:absolute;pointer-events:none;right:10px}#header .header-right>div>.menu>div,#header .header-right>div>.menu>ul{-webkit-overflow-scrolling:touch;min-height:66px;max-height:calc(100vh - 50px - 8px)}#header .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;left:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header .header-appname-container{display:none;padding-right:10px;flex-shrink:0}#header #header-left,#header .header-left,#header #header-right,#header .header-right{display:inline-flex;align-items:center}#header #header-left,#header .header-left{flex:1 0;white-space:nowrap;min-width:0}#header #header-right,#header .header-right{justify-content:flex-end;flex-shrink:1}#header .header-right>.header-menu__trigger img{filter:var(--background-image-invert-if-bright)}#header .header-right>div,#header .header-right>form{height:100%;position:relative}#header .header-right>div>.menutoggle,#header .header-right>form>.menutoggle{display:flex;justify-content:center;align-items:center;width:50px;height:44px;cursor:pointer;opacity:.85;padding:0;margin:2px 0}#header .header-right>div>.menutoggle:focus,#header .header-right>form>.menutoggle:focus{opacity:1}#header .header-right>div>.menutoggle:focus-visible,#header .header-right>form>.menutoggle:focus-visible{outline:none}.header-appname-container .header-appname{opacity:.75}.header-appname{color:var(--color-primary-element-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-right:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}.header-info{display:flex;flex-direction:column;overflow:hidden}.header-title{overflow:hidden;text-overflow:ellipsis}.header-shared-by{color:var(--color-primary-element-text);position:relative;font-weight:300;font-size:11px;line-height:11px;overflow:hidden;text-overflow:ellipsis}#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;left:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:50px}header #emptycontent h2,header .emptycontent h2{font-weight:normal;font-size:16px}header #emptycontent [class^=icon-],header #emptycontent [class*=icon-],header .emptycontent [class^=icon-],header .emptycontent [class*=icon-]{background-size:48px;height:48px;width:48px}/*!
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*//*!

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,167 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<Transition>
<div v-if="open"
class="local-unified-search animated-width"
:class="{ 'local-unified-search--open': open }">
<!-- We can not use labels as it breaks the header layout so only aria-label and placeholder -->
<NcInputField ref="searchInput"
class="local-unified-search__input animated-width"
:aria-label="t('core', 'Search in current app')"
:placeholder="t('core', 'Search in current app')"
show-trailing-button
:trailing-button-label="t('core', 'Clear search')"
:value="query"
@update:value="$emit('update:query', $event)"
@trailing-button-click="clearAndCloseSearch">
<template #trailing-button-icon>
<NcIconSvgWrapper :path="mdiClose" />
</template>
</NcInputField>
<NcButton ref="searchGlobalButton"
class="local-unified-search__global-search"
:aria-label="t('core', 'Search everywhere')"
:title="t('core', 'Search everywhere')"
type="tertiary-no-background"
@click="$emit('global-search')">
<template v-if="!isMobile" #default>
{{ t('core', 'Search everywhere') }}
</template>
<template #icon>
<NcIconSvgWrapper :path="mdiCloudSearch" />
</template>
</NcButton>
</div>
</Transition>
</template>
<script lang="ts" setup>
import type { ComponentPublicInstance } from 'vue'
import { mdiCloudSearch, mdiClose } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { useIsMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js'
import { computed, ref, watchEffect } from 'vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
import { useElementSize } from '@vueuse/core'
const props = defineProps<{
query: string,
open: boolean
}>()
const emit = defineEmits<{
(e: 'update:open', open: boolean): void
(e: 'update:query', query: string): void
(e: 'global-search'): void
}>()
// Hacky type until the library provides real Types
type FocusableComponent = ComponentPublicInstance<object, object, object, Record<string, never>, { focus: () => void }>
/** The input field component */
const searchInput = ref<FocusableComponent>()
/** When the search bar is opened we focus the input */
watchEffect(() => {
if (props.open && searchInput.value) {
searchInput.value.focus()
}
})
/** Current window size is below the "mobile" breakpoint (currently 1024px) */
const isMobile = useIsMobile()
const searchGlobalButton = ref<ComponentPublicInstance>()
/** Width of the search global button, used to resize the input field */
const { width: searchGlobalButtonWidth } = useElementSize(searchGlobalButton)
const searchGlobalButtonCSSWidth = computed(() => searchGlobalButtonWidth.value ? `${searchGlobalButtonWidth.value}px` : 'var(--default-clickable-area)')
/**
* Clear the search query and close the search bar
*/
function clearAndCloseSearch() {
emit('update:query', '')
emit('update:open', false)
}
</script>
<style scoped lang="scss">
.local-unified-search {
--local-search-width: min(calc(250px + v-bind('searchGlobalButtonCSSWidth')), 95vw);
box-sizing: border-box;
position: relative;
height: var(--header-height);
width: var(--local-search-width);
display: flex;
align-items: center;
// Ensure it overlays the other entries
z-index: 10;
// add some padding for the focus visible outline
padding-inline: var(--border-width-input-focused);
// hide the overflow - needed for the transition
overflow: hidden;
// Ensure the position is fixed also during "position: absolut" (transition)
inset-inline-end: 0;
#{&} &__global-search {
position: absolute;
inset-inline-end: var(--default-clickable-area);
}
#{&} &__input {
box-sizing: border-box;
// override some nextcloud-vue styles
margin: 0;
width: var(--local-search-width);
// Fixup the spacing so we can fit in the "search globally" button
// this can break at any time the component library changes
:deep(input) {
// search global width + close button width
padding-inline-end: calc(v-bind('searchGlobalButtonWidth') + var(--default-clickable-area));
}
}
}
.animated-width {
transition: width var(--animation-quick) linear;
}
// Make the position absolut during the transition
// this is needed to "hide" the button begind it
.v-leave-active {
position: absolute !important;
}
.v-enter,
.v-leave-to {
&.local-unified-search {
// Start with only the overlayed button
--local-search-width: var(--clickable-area-large);
}
}
@media screen and (max-width: 500px) {
.local-unified-search.local-unified-search--open {
// 100% but still show the menu toggle on the very right
--local-search-width: 100vw;
padding-inline: var(--default-grid-baseline);
}
// when open we need to position it absolut to allow overlay the full bar
:global(.unified-search-menu:has(.local-unified-search--open)) {
position: absolute !important;
inset-inline: 0;
}
// Hide all other entries, especially the user menu as it might leak pixels
:global(.header-right:has(.local-unified-search--open) > :not(.unified-search-menu)) {
display: none;
}
}
</style>

View file

@ -3,194 +3,229 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcModal id="unified-search"
<NcDialog id="unified-search"
ref="unifiedSearchModal"
:show.sync="internalIsVisible"
:clear-view-delay="0"
@close="closeModal">
content-classes="unified-search-modal__content"
dialog-classes="unified-search-modal"
:name="t('core', 'Unified search')"
:open="open"
@update:open="onUpdateOpen">
<!-- Modal for picking custom time range -->
<CustomDateRangeModal :is-open="showDateRangeModal"
class="unified-search__date-range"
@set:custom-date-range="setCustomDateRange"
@update:is-open="showDateRangeModal = $event" />
<!-- Unified search form -->
<div ref="unifiedSearch" class="unified-search-modal">
<div class="unified-search-modal__header">
<h2>{{ t('core', 'Unified search') }}</h2>
<NcInputField ref="searchInput"
data-cy-unified-search-input
:value.sync="searchQuery"
type="text"
:label="t('core', 'Search apps, files, tags, messages') + '...'"
@update:value="debouncedFind" />
<div class="unified-search-modal__filters" data-cy-unified-search-filters>
<NcActions :menu-name="t('core', 'Places')" :open.sync="providerActionMenuIsOpen" data-cy-unified-search-filter="places">
<div class="unified-search-modal__header">
<NcInputField ref="searchInput"
data-cy-unified-search-input
:value.sync="searchQuery"
type="text"
:label="t('core', 'Search apps, files, tags, messages') + '...'"
@update:value="debouncedFind" />
<div class="unified-search-modal__filters" data-cy-unified-search-filters>
<NcActions :menu-name="t('core', 'Places')" :open.sync="providerActionMenuIsOpen" data-cy-unified-search-filter="places">
<template #icon>
<IconListBox :size="20" />
</template>
<!-- Provider id's may be duplicated since, plugin filters could depend on a provider that is already in the defaults.
provider.id concatenated to provider.name is used to create the item id, if same then, there should be an issue. -->
<NcActionButton v-for="provider in providers"
:key="`${provider.id}-${provider.name.replace(/\s/g, '')}`"
@click="addProviderFilter(provider)">
<template #icon>
<ListBox :size="20" />
<img :src="provider.icon" class="filter-button__icon" alt="">
</template>
<!-- Provider id's may be duplicated since, plugin filters could depend on a provider that is already in the defaults.
provider.id concatenated to provider.name is used to create the item id, if same then, there should be an issue. -->
<NcActionButton v-for="provider in providers"
:key="`${provider.id}-${provider.name.replace(/\s/g, '')}`"
@click="addProviderFilter(provider)">
{{ provider.name }}
</NcActionButton>
</NcActions>
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen" data-cy-unified-search-filter="date">
<template #icon>
<IconCalendarRange :size="20" />
</template>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
{{ t('core', 'Today') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
{{ t('core', 'Last 7 days') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
{{ t('core', 'Last 30 days') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
{{ t('core', 'This year') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
{{ t('core', 'Last year') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
{{ t('core', 'Custom date range') }}
</NcActionButton>
</NcActions>
<SearchableList :label-text="t('core', 'Search people')"
:search-list="userContacts"
:empty-content-text="t('core', 'Not found')"
data-cy-unified-search-filter="people"
@search-term-change="debouncedFilterContacts"
@item-selected="applyPersonFilter">
<template #trigger>
<NcButton>
<template #icon>
<img :src="provider.icon" class="filter-button__icon" alt="">
<IconAccountGroup :size="20" />
</template>
{{ provider.name }}
</NcActionButton>
</NcActions>
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen" data-cy-unified-search-filter="date">
{{ t('core', 'People') }}
</NcButton>
</template>
</SearchableList>
<NcButton v-if="localSearch" data-cy-unified-search-filter="current-view" @click="searchLocally">
{{ t('core', 'Filter in current view') }}
<template #icon>
<IconFilter :size="20" />
</template>
</NcButton>
</div>
<div class="unified-search-modal__filters-applied">
<FilterChip v-for="filter in filters"
:key="filter.id"
:text="filter.name ?? filter.text"
:pretext="''"
@delete="removeFilter(filter)">
<template #icon>
<NcAvatar v-if="filter.type === 'person'"
:user="filter.user"
:size="24"
:disable-menu="true"
:show-user-status="false"
:hide-favorite="false" />
<IconCalendarRange v-else-if="filter.type === 'date'" />
<img v-else :src="filter.icon" alt="">
</template>
</FilterChip>
</div>
</div>
<div v-if="showEmptyContentInfo" class="unified-search-modal__no-content">
<NcEmptyContent :name="emptyContentMessage">
<template #icon>
<IconMagnify :size="64" />
</template>
</NcEmptyContent>
</div>
<div v-else class="unified-search-modal__results">
<h3 class="hidden-visually">{{ t('core', 'Results') }}</h3>
<div v-for="providerResult in results" :key="providerResult.id" class="result">
<h4 :id="`unified-search-result-${providerResult.id}`" class="result-title">
{{ providerResult.provider }}
</h4>
<ul class="result-items" :aria-labelledby="`unified-search-result-${providerResult.id}`">
<SearchResult v-for="(result, index) in providerResult.results"
:key="index"
v-bind="result" />
</ul>
<div class="result-footer">
<NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)">
{{ t('core', 'Load more results') }}
<template #icon>
<CalendarRangeIcon :size="20" />
<IconDotsHorizontal :size="20" />
</template>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
{{ t('core', 'Today') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
{{ t('core', 'Last 7 days') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
{{ t('core', 'Last 30 days') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
{{ t('core', 'This year') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
{{ t('core', 'Last year') }}
</NcActionButton>
<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
{{ t('core', 'Custom date range') }}
</NcActionButton>
</NcActions>
<SearchableList :label-text="t('core', 'Search people')"
:search-list="userContacts"
:empty-content-text="t('core', 'Not found')"
data-cy-unified-search-filter="people"
@search-term-change="debouncedFilterContacts"
@item-selected="applyPersonFilter">
<template #trigger>
<NcButton>
<template #icon>
<AccountGroup :size="20" />
</template>
{{ t('core', 'People') }}
</NcButton>
</template>
</SearchableList>
<NcButton v-if="supportFiltering" data-cy-unified-search-filter="current-view" @click="closeModal">
{{ t('core', 'Filter in current view') }}
</NcButton>
<NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background">
{{ t('core', 'Search in') }} {{ providerResult.provider }}
<template #icon>
<FilterIcon :size="20" />
<IconArrowRight :size="20" />
</template>
</NcButton>
</div>
<div class="unified-search-modal__filters-applied">
<FilterChip v-for="filter in filters"
:key="filter.id"
:text="filter.name ?? filter.text"
:pretext="''"
@delete="removeFilter(filter)">
<template #icon>
<NcAvatar v-if="filter.type === 'person'"
:user="filter.user"
:size="24"
:disable-menu="true"
:show-user-status="false"
:hide-favorite="false" />
<CalendarRangeIcon v-else-if="filter.type === 'date'" />
<img v-else :src="filter.icon" alt="">
</template>
</FilterChip>
</div>
</div>
<div v-if="noContentInfo.show" class="unified-search-modal__no-content">
<NcEmptyContent :name="noContentInfo.text">
<template #icon>
<component :is="noContentInfo.icon" />
</template>
</NcEmptyContent>
</div>
<div v-else class="unified-search-modal__results">
<div v-for="providerResult in results" :key="providerResult.id" class="result">
<div class="result-title">
<span>{{ providerResult.provider }}</span>
</div>
<ul class="result-items">
<SearchResult v-for="(result, index) in providerResult.results" :key="index" v-bind="result" />
</ul>
<div class="result-footer">
<NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)">
{{ t('core', 'Load more results') }}
<template #icon>
<DotsHorizontalIcon :size="20" />
</template>
</NcButton>
<NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background">
{{ t('core', 'Search in') }} {{ providerResult.provider }}
<template #icon>
<ArrowRight :size="20" />
</template>
</NcButton>
</div>
</div>
</div>
</div>
</NcModal>
</NcDialog>
</template>
<script>
import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue'
import CustomDateRangeModal from '../components/UnifiedSearch/CustomDateRangeModal.vue'
import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
import FilterIcon from 'vue-material-design-icons/Filter.vue'
import FilterChip from '../components/UnifiedSearch/SearchFilterChip.vue'
import ListBox from 'vue-material-design-icons/ListBox.vue'
<script lang="ts">
import { subscribe } from '@nextcloud/event-bus'
import { translate as t } from '@nextcloud/l10n'
import { useBrowserLocation } from '@vueuse/core'
import { defineComponent } from 'vue'
import { getProviders, search as unifiedSearch, getContacts } from '../../services/UnifiedSearchService.js'
import { useSearchStore } from '../../store/unified-search-external-filters.js'
import debounce from 'debounce'
import { unifiedSearchLogger } from '../../logger'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import IconAccountGroup from 'vue-material-design-icons/AccountGroup.vue'
import IconCalendarRange from 'vue-material-design-icons/CalendarRange.vue'
import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
import IconFilter from 'vue-material-design-icons/Filter.vue'
import IconListBox from 'vue-material-design-icons/ListBox.vue'
import IconMagnify from 'vue-material-design-icons/Magnify.vue'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import MagnifyIcon from 'vue-material-design-icons/Magnify.vue'
import SearchableList from '../components/UnifiedSearch/SearchableList.vue'
import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import debounce from 'debounce'
import { emit, subscribe } from '@nextcloud/event-bus'
import { useBrowserLocation } from '@vueuse/core'
import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
import { useSearchStore } from '../store/unified-search-external-filters.js'
import CustomDateRangeModal from './CustomDateRangeModal.vue'
import FilterChip from './SearchFilterChip.vue'
import SearchableList from './SearchableList.vue'
import SearchResult from './SearchResult.vue'
export default {
export default defineComponent({
name: 'UnifiedSearchModal',
components: {
ArrowRight,
AccountGroup,
CalendarRangeIcon,
IconArrowRight,
IconAccountGroup,
IconCalendarRange,
IconDotsHorizontal,
IconFilter,
IconListBox,
IconMagnify,
CustomDateRangeModal,
DotsHorizontalIcon,
FilterIcon,
FilterChip,
ListBox,
NcActions,
NcActionButton,
NcAvatar,
NcButton,
NcEmptyContent,
NcModal,
NcDialog,
NcInputField,
MagnifyIcon,
SearchableList,
SearchResult,
},
props: {
isVisible: {
/**
* Open state of the modal
*/
open: {
type: Boolean,
required: true,
},
/**
* The current query string
*/
query: {
type: String,
default: '',
},
/**
* If the current page / app supports local search
*/
localSearch: {
type: Boolean,
default: false,
},
},
emits: ['update:open', 'update:query'],
setup() {
/**
* Reactive version of window.location
@ -198,10 +233,13 @@ export default {
const currentLocation = useBrowserLocation()
const searchStore = useSearchStore()
return {
t,
currentLocation,
externalFilters: searchStore.externalFilters,
}
},
data() {
return {
providers: [],
@ -220,54 +258,63 @@ export default {
filters: [],
results: [],
contacts: [],
debouncedFind: debounce(this.find, 300),
debouncedFilterContacts: debounce(this.filterContacts, 300),
showDateRangeModal: false,
internalIsVisible: false,
internalIsVisible: this.open,
}
},
computed: {
isEmptySearch() {
return this.searchQuery.length === 0
},
hasNoResults() {
return !this.isEmptySearch && this.results.length === 0
},
showEmptyContentInfo() {
return this.isEmptySearch || this.hasNoResults
},
emptyContentMessage() {
if (this.searching && this.hasNoResults) {
return t('core', 'Searching …')
}
if (this.isEmptySearch) {
return t('core', 'Start typing to search')
}
return t('core', 'No matching results')
},
userContacts() {
return this.contacts
},
noContentInfo() {
const isEmptySearch = this.searchQuery.length === 0
const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
return {
show: isEmptySearch || hasNoResults,
text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing to search') : t('core', 'No matching results')),
icon: MagnifyIcon,
}
},
supportFiltering() {
/* Hard coded apps for the moment this would be improved in coming updates. */
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
},
},
watch: {
isVisible(value) {
if (value) {
/*
* Before setting the search UI to visible, reset previous search event emissions.
* This allows apps to restore defaults after "Filter in current view" if the user opens the search interface once more.
* Additionally, it's a new search, so it's better to reset all previous events emitted.
*/
emit('nextcloud:unified-search.reset', { query: '' })
}
this.internalIsVisible = value
},
internalIsVisible(value) {
this.$emit('update:isVisible', value)
this.$nextTick(() => {
if (value) {
this.focusInput()
}
})
debouncedFind() {
return debounce(this.find, 300)
},
debouncedFilterContacts() {
return debounce(this.filterContacts, 300)
},
},
watch: {
open() {
// Load results when opened with already filled query
if (this.open && this.searchQuery) {
this.find(this.searchQuery)
}
},
query: {
immediate: true,
handler() {
this.searchQuery = this.query.trim()
}
},
},
mounted() {
subscribe('nextcloud:unified-search:add-filter', this.handlePluginFilter)
getProviders().then((providers) => {
@ -276,23 +323,41 @@ export default {
this.providers.push(filter)
})
this.providers = this.groupProvidersByApp(this.providers)
console.debug('Search providers', this.providers)
unifiedSearchLogger.debug('Search providers', { providers: this.providers })
})
getContacts({ searchTerm: '' }).then((contacts) => {
this.contacts = this.mapContacts(contacts)
console.debug('Contacts', this.contacts)
unifiedSearchLogger.debug('Contacts', { contacts: this.contacts })
})
},
methods: {
find(query) {
this.searching = true
/**
* On close the modal is closed and the query is reset
* @param open The new open state
*/
onUpdateOpen(open: boolean) {
if (!open) {
this.$emit('update:open', false)
this.$emit('update:query', '')
}
},
/**
* Only close the modal but keep the query for in-app search
*/
searchLocally() {
this.$emit('update:query', this.searchQuery)
this.$emit('update:open', false)
},
find(query: string) {
if (query.length === 0) {
this.results = []
this.searching = false
emit('nextcloud:unified-search.reset', { query })
return
}
emit('nextcloud:unified-search.search', { query })
this.searching = true
const newResults = []
const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers
const searchProvider = (provider, filters) => {
@ -304,7 +369,7 @@ export default {
}
if (filters.dateFilterIsApplied) {
if (provider.filters.since && provider.filters.until) {
if (provider.filters?.since && provider.filters?.until) {
params.since = this.dateFilter.startFrom
params.until = this.dateFilter.endAt
} else {
@ -314,7 +379,7 @@ export default {
}
if (filters.personFilterIsApplied) {
if (provider.filters.person) {
if (provider.filters?.person) {
params.person = this.personFilter.user
} else {
// Person filter is applied but provider does not support it, no need to search provider
@ -336,8 +401,7 @@ export default {
results: response.data.ocs.data.entries,
})
console.debug('New results', newResults)
console.debug('Unified search results:', this.results)
unifiedSearchLogger.debug('Unified search results:', { results: this.results, newResults })
this.updateResults(newResults)
this.searching = false
@ -402,7 +466,7 @@ export default {
filterContacts(query) {
getContacts({ searchTerm: query }).then((contacts) => {
this.contacts = this.mapContacts(contacts)
console.debug(`Contacts filtered by ${query}`, this.contacts)
unifiedSearchLogger.debug(`Contacts filtered by ${query}`, { contacts: this.contacts })
})
},
applyPersonFilter(person) {
@ -420,7 +484,7 @@ export default {
}
this.debouncedFind(this.searchQuery)
console.debug('Person filter applied', person)
unifiedSearchLogger.debug('Person filter applied', { person })
},
loadMoreResultsForProvider(providerId) {
this.providerResultLimit += 5
@ -452,7 +516,7 @@ export default {
isPluginFilter: providerFilter.isPluginFilter || false,
})
this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
console.debug('Search filters (newly added)', this.filters)
unifiedSearchLogger.debug('Search filters (newly added)', { filters: this.filters })
this.debouncedFind(this.searchQuery)
},
removeFilter(filter) {
@ -464,7 +528,7 @@ export default {
}
}
this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
console.debug('Search filters (recently removed)', this.filters)
unifiedSearchLogger.debug('Search filters (recently removed)', { filters: this.filters })
} else {
for (let i = 0; i < this.filters.length; i++) {
@ -562,7 +626,7 @@ export default {
},
setCustomDateRange(event) {
console.debug('Custom date range', event)
unifiedSearchLogger.debug('Custom date range', { range: event })
this.dateFilter.startFrom = event.startFrom
this.dateFilter.endAt = event.endAt
this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
@ -603,41 +667,35 @@ export default {
return flattenedArray
},
focusInput() {
this.$refs.searchInput.$el.children[0].children[0].focus()
},
closeModal() {
this.internalIsVisible = false
this.searchQuery = ''
},
},
}
})
</script>
<style lang="scss" scoped>
.unified-search-modal {
:deep(.unified-search-modal .unified-search-modal__content) {
--dialog-height: min(80vh, 800px);
box-sizing: border-box;
height: 100%;
min-height: 80vh;
height: var(--dialog-height);
max-height: var(--dialog-height);
min-height: var(--dialog-height);
display: flex;
flex-direction: column;
padding-block: 10px 0;
// inline padding on direct children to make sure the scrollbar is on the modal container
>* {
padding-inline: 20px;
}
// No padding to prevent scrollbar misplacement
padding-inline: 0;
}
.unified-search-modal {
&__header {
padding-block-end: 8px;
}
&__heading {
font-size: 16px;
font-weight: bolder;
line-height: 2em;
margin-bottom: 0;
// Add background to prevent leaking scrolled content (because of sticky position)
background-color: var(--color-main-background);
// Fix padding to have the input centered
padding-inline-end: 12px;
// Some padding to make elements scrolled under sticky position look nicer
padding-block-end: 12px;
// Make it sticky with the input margin for the label
position: sticky;
top: 6px;
}
&__filters {
@ -662,15 +720,15 @@ export default {
&__results {
overflow: hidden scroll;
padding-block: 0 10px;
// Adjust padding to match container but keep the scrollbar on the very end
padding-inline: 0 12px;
padding-block: 0 12px;
.result {
&-title {
span {
color: var(--color-primary-element);
font-weight: bolder;
font-size: 16px;
}
color: var(--color-primary-element);
font-size: 16px;
margin-block: 8px 4px;
}
&-footer {

14
core/src/eventbus.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
declare module '@nextcloud/event-bus' {
export interface NextcloudEvents {
// mapping of 'event name' => 'event type'
'nextcloud:unified-search:reset': undefined
'nextcloud:unified-search:search': { query: string }
}
}
export {}

View file

@ -19,3 +19,8 @@ const getLogger = user => {
}
export default getLogger(getCurrentUser())
export const unifiedSearchLogger = getLoggerBuilder()
.setApp('unified-search')
.detectUser()
.build()

View file

@ -4,7 +4,8 @@
-->
<template>
<div class="header-menu unified-search-menu">
<NcButton class="header-menu__trigger"
<NcButton v-show="!showLocalSearch"
class="header-menu__trigger"
:aria-label="t('core', 'Unified search')"
type="tertiary-no-background"
@click="toggleUnifiedSearch">
@ -12,39 +13,161 @@
<Magnify class="header-menu__trigger-icon" :size="20" />
</template>
</NcButton>
<UnifiedSearchModal :is-visible="showUnifiedSearch" @update:isVisible="handleModalVisibilityChange" />
<UnifiedSearchLocalSearchBar v-if="supportsLocalSearch"
:open.sync="showLocalSearch"
:query.sync="queryText"
@global-search="openModal" />
<UnifiedSearchModal :local-search="supportsLocalSearch"
:query.sync="queryText"
:open.sync="showUnifiedSearch" />
</div>
</template>
<script>
<script lang="ts">
import { emit, subscribe } from '@nextcloud/event-bus'
import { translate } from '@nextcloud/l10n'
import { useBrowserLocation } from '@vueuse/core'
import { defineComponent } from 'vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import Magnify from 'vue-material-design-icons/Magnify.vue'
import UnifiedSearchModal from './UnifiedSearchModal.vue'
import UnifiedSearchModal from '../components/UnifiedSearch/UnifiedSearchModal.vue'
import UnifiedSearchLocalSearchBar from '../components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue'
export default {
import debounce from 'debounce'
import logger from '../logger'
export default defineComponent({
name: 'UnifiedSearch',
components: {
NcButton,
Magnify,
UnifiedSearchModal,
UnifiedSearchLocalSearchBar,
},
data() {
setup() {
const currentLocation = useBrowserLocation()
return {
showUnifiedSearch: false,
currentLocation,
t: translate,
}
},
data() {
return {
/** The current search query */
queryText: '',
/** Open state of the modal */
showUnifiedSearch: false,
/** Open state of the local search bar */
showLocalSearch: false,
}
},
computed: {
/**
* Debounce emitting the search query by 250ms
*/
debouncedQueryUpdate() {
return debounce(this.emitUpdatedQuery, 250)
},
/**
* Current page (app) supports local in-app search
*/
supportsLocalSearch() {
// TODO: Make this an API
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck', '/settings/apps']
return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
},
},
watch: {
/**
* Emit the updated query as eventbus events
* (This is debounced)
*/
queryText() {
this.debouncedQueryUpdate()
},
},
mounted() {
console.debug('Unified search initialized!')
// register keyboard listener for search shortcut
window.addEventListener('keydown', this.onKeyDown)
// Allow external reset of the search / close local search
subscribe('nextcloud:unified-search:reset', () => {
this.showLocalSearch = false
this.queryText = ''
})
// Deprecated events to be removed
subscribe('nextcloud:unified-search:reset', () => {
emit('nextcloud:unified-search.reset', { query: '' })
})
subscribe('nextcloud:unified-search:search', ({ query }) => {
emit('nextcloud:unified-search.search', { query })
})
// all done
logger.debug('Unified search initialized!')
},
beforeDestroy() {
// keep in mind to remove the event listener
window.removeEventListener('keydown', this.onKeyDown)
},
methods: {
toggleUnifiedSearch() {
this.showUnifiedSearch = !this.showUnifiedSearch
/**
* Handle the key down event to open search on `ctrl + F`
* @param event The keyboard event
*/
onKeyDown(event: KeyboardEvent) {
if (event.ctrlKey && event.code === 'KeyF') {
// only handle search if not already open - in this case the browser native search should be used
if (!this.showLocalSearch && !this.showUnifiedSearch) {
this.toggleUnifiedSearch()
event.preventDefault()
}
}
},
handleModalVisibilityChange(newVisibilityVal) {
this.showUnifiedSearch = newVisibilityVal
/**
* Toggle the local search if available - otherwise open the unified search modal
*/
toggleUnifiedSearch() {
if (this.supportsLocalSearch) {
this.showLocalSearch = true
} else {
this.openModal()
}
},
/**
* Open the unified search modal
*/
openModal() {
this.showUnifiedSearch = true
this.showLocalSearch = false
},
/**
* Emit the updated search query as eventbus events
*/
emitUpdatedQuery() {
if (this.queryText === '') {
emit('nextcloud:unified-search:reset')
} else {
emit('nextcloud:unified-search:search', { query: this.queryText })
}
},
},
}
})
</script>
<style lang="scss" scoped>

View file

@ -157,8 +157,6 @@ export default {
<style lang="scss" scoped>
.user-menu {
margin-right: 12px;
&:deep {
.header-menu {
&__trigger {

View file

@ -5,9 +5,10 @@
import type { User } from '@nextcloud/cypress'
import { getRowForFile, navigateToFolder } from './FilesUtils'
import { UnifiedSearchFilter, getUnifiedSearchFilter, getUnifiedSearchInput, getUnifiedSearchModal, openUnifiedSearch } from '../core-utils.ts'
import { UnifiedSearchPage } from '../../pages/UnifiedSearch.ts'
describe('files: Search and filter in files list', { testIsolation: true }, () => {
const unifiedSearch = new UnifiedSearchPage()
let user: User
beforeEach(() => cy.createRandomUser().then(($user) => {
@ -20,17 +21,21 @@ describe('files: Search and filter in files list', { testIsolation: true }, () =
cy.visit('/apps/files')
}))
it('files app supports local search', () => {
unifiedSearch.openLocalSearch()
unifiedSearch.localSearchInput()
.should('not.have.css', 'display', 'none')
.and('not.be.disabled')
})
it('filters current view', () => {
// All are visible by default
getRowForFile('a folder').should('be.visible')
getRowForFile('b file').should('be.visible')
// Set up a search query
openUnifiedSearch()
getUnifiedSearchInput().type('a folder')
getUnifiedSearchFilter(UnifiedSearchFilter.FilterCurrentView).click({ force: true })
// Wait for modal to close
getUnifiedSearchModal().should('not.be.visible')
unifiedSearch.openLocalSearch()
unifiedSearch.typeLocalSearch('a folder')
// See that only the folder is visible
getRowForFile('a folder').should('be.visible')
@ -43,11 +48,8 @@ describe('files: Search and filter in files list', { testIsolation: true }, () =
getRowForFile('b file').should('be.visible')
// Set up a search query
openUnifiedSearch()
getUnifiedSearchInput().type('a folder')
getUnifiedSearchFilter(UnifiedSearchFilter.FilterCurrentView).click({ force: true })
// Wait for modal to close
getUnifiedSearchModal().should('not.be.visible')
unifiedSearch.openLocalSearch()
unifiedSearch.typeLocalSearch('a folder')
// See that only the folder is visible
getRowForFile('a folder').should('be.visible')
@ -66,11 +68,8 @@ describe('files: Search and filter in files list', { testIsolation: true }, () =
getRowForFile('b file').should('be.visible')
// Set up a search query
openUnifiedSearch()
getUnifiedSearchInput().type('a folder')
getUnifiedSearchFilter(UnifiedSearchFilter.FilterCurrentView).click({ force: true })
// Wait for modal to close
getUnifiedSearchModal().should('not.be.visible')
unifiedSearch.openLocalSearch()
unifiedSearch.typeLocalSearch('a folder')
// See that only the folder is visible
getRowForFile('a folder').should('be.visible')
@ -84,5 +83,8 @@ describe('files: Search and filter in files list', { testIsolation: true }, () =
// see that the folder is not filtered
getRowForFile('a folder').should('be.visible')
getRowForFile('b file').should('be.visible')
// see the filter bar is gone
unifiedSearch.localSearchInput().should('not.exist')
})
})

View file

@ -0,0 +1,75 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
* Page object model for the UnifiedSearch
*/
export class UnifiedSearchPage {
toggleButton() {
return cy.findByRole('button', { name: 'Unified search' })
}
globalSearchButton() {
return cy.findByRole('button', { name: 'Search everywhere' })
}
localSearchInput() {
return cy.findByRole('textbox', { name: 'Search in current app' })
}
globalSearchInput() {
return cy.findByRole('textbox', { name: /Search apps, files/ })
}
globalSearchModal() {
// TODO: Broken in library
// return cy.findByRole('dialog', { name: 'Unified search' })
return cy.get('#unified-search')
}
// functions
openLocalSearch() {
this.toggleButton()
.if('visible')
.click()
this.localSearchInput().should('exist').and('not.have.css', 'display', 'none')
}
/**
* Type in the local search (must be open before)
* Helper because the input field is overlayed by the global-search button -> cypress thinks the input is not visible
*
* @param text The text to type
* @param options Options as for `cy.type()`
*/
typeLocalSearch(text: string, options?: Partial<Omit<Cypress.TypeOptions, 'force'>>) {
return this.localSearchInput()
.type(text, { ...options, force: true })
}
openGlobalSearch() {
this.toggleButton()
.if('visible').click()
this.globalSearchButton()
.if('visible').click()
}
closeGlobalSearch() {
this.globalSearchModal()
.findByRole('button', { name: 'Close' })
.click()
}
getResults(category: string | RegExp) {
return this.globalSearchModal()
.findByRole('list', { name: category })
.findAllByRole('listitem')
}
}

View file

@ -10,6 +10,7 @@ import { addCommands, User } from '@nextcloud/cypress'
import { basename } from 'path'
// Add custom commands
import '@testing-library/cypress/add-commands'
import 'cypress-if'
import 'cypress-wait-until'
addCommands()

View file

@ -2,6 +2,12 @@
"extends": "../tsconfig.json",
"include": ["./**/*.ts"],
"compilerOptions": {
"types": ["cypress", "cypress-axe", "cypress-wait-until", "dockerode"],
"types": [
"@testing-library/cypress",
"cypress",
"cypress-axe",
"cypress-wait-until",
"dockerode"
],
}
}

4
dist/7883-7883.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(21777);t.nc=btoa((0,n.do)()),window.TESTING||null!==(o=OC)&&void 0!==o&&null!==(o=o.config)&&void 0!==o&&o.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",(async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()}))}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){t=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[d])))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+"-"+e+".js?v=a1c70edb7f4fe1c85ea6",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,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;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")==t+n){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.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)}},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=3604,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&(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(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,i=t[0],l=t[1],d=t[2],c=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var u=d(a)}for(r&&r(t);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(47210)));i=a.O(i)})();
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=e3929d5074a4d50c2869
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(21777);t.nc=btoa((0,n.do)()),window.TESTING||null!==(o=OC)&&void 0!==o&&null!==(o=o.config)&&void 0!==o&&o.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",(async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()}))}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){t=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[d])))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+"-"+e+".js?v=b510a36434fab3e923cf",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,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;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")==t+n){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.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)}},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=3604,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&(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(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,i=t[0],l=t[1],d=t[2],c=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var u=d(a)}for(r&&r(t);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(47210)));i=a.O(i)})();
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=5a76f9739aaeb248f601

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/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

116
package-lock.json generated
View file

@ -101,6 +101,7 @@
"@nextcloud/webpack-vue-config": "^6.0.1",
"@pinia/testing": "^0.1.2",
"@simplewebauthn/types": "^10.0.0",
"@testing-library/cypress": "^10.0.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",
@ -5255,6 +5256,121 @@
"string.prototype.matchall": "^4.0.6"
}
},
"node_modules/@testing-library/cypress": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.0.2.tgz",
"integrity": "sha512-dKv95Bre5fDmNb9tOIuWedhGUryxGu1GWYWtXDqUsDPcr9Ekld0fiTb+pcBvSsFpYXAZSpmyEjhoXzLbhh06yQ==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.14.6",
"@testing-library/dom": "^10.1.0"
},
"engines": {
"node": ">=12",
"npm": ">=6"
},
"peerDependencies": {
"cypress": "^12.0.0 || ^13.0.0"
}
},
"node_modules/@testing-library/cypress/node_modules/@testing-library/dom": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.2.0.tgz",
"integrity": "sha512-CytIvb6tVOADRngTHGWNxH8LPgO/3hi/BdCEHOf7Qd2GvZVClhVP0Wo/QHzWhpki49Bk0b4VT6xpt3fx8HTSIw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/cypress/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/cypress/node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/@testing-library/cypress/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@testing-library/cypress/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/cypress/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/@testing-library/cypress/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/cypress/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",

View file

@ -129,6 +129,7 @@
"@nextcloud/webpack-vue-config": "^6.0.1",
"@pinia/testing": "^0.1.2",
"@simplewebauthn/types": "^10.0.0",
"@testing-library/cypress": "^10.0.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",