mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 01:30:50 -04:00
Merge pull request #41609 from nextcloud/enh/in-app-search
feat: In app search
This commit is contained in:
commit
a184161978
34 changed files with 870 additions and 293 deletions
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -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
14
core/src/eventbus.d.ts
vendored
Normal 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 {}
|
||||
|
|
@ -19,3 +19,8 @@ const getLogger = user => {
|
|||
}
|
||||
|
||||
export default getLogger(getCurrentUser())
|
||||
|
||||
export const unifiedSearchLogger = getLoggerBuilder()
|
||||
.setApp('unified-search')
|
||||
.detectUser()
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -157,8 +157,6 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.user-menu {
|
||||
margin-right: 12px;
|
||||
|
||||
&:deep {
|
||||
.header-menu {
|
||||
&__trigger {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
75
cypress/pages/UnifiedSearch.ts
Normal file
75
cypress/pages/UnifiedSearch.ts
Normal 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')
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
4
dist/7883-7883.js
vendored
File diff suppressed because one or more lines are too long
2
dist/7883-7883.js.map
vendored
2
dist/7883-7883.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login.js
vendored
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-recommendedapps.js
vendored
4
dist/core-recommendedapps.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-recommendedapps.js.map
vendored
2
dist/core-recommendedapps.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unified-search.js
vendored
4
dist/core-unified-search.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-unified-search.js.map
vendored
2
dist/core-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unsupported-browser-redirect.js
vendored
4
dist/core-unsupported-browser-redirect.js
vendored
|
|
@ -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
4
dist/core-unsupported-browser.js
vendored
4
dist/core-unsupported-browser.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-unsupported-browser.js.map
vendored
2
dist/core-unsupported-browser.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
116
package-lock.json
generated
116
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue