Skip to content

Commit

Permalink
Feat: New UI global search
Browse files Browse the repository at this point in the history
We are introducing a new search UI that providers a lot more space
for users via a large centralized modal and providers various filters
which can by applied by adding various chips on the UI.

For example, users can now filter their search or scope it by limiting the results
to specific apps, time period and people by apply the appropriate filters on the
new UI, previously filters where applied using text in the search box by prefixing
with `::`.

Resolves: #39162

Signed-off-by: fenn-cs <fenn25.fn@gmail.com>
  • Loading branch information
nfebe committed Nov 3, 2023
1 parent 2acf304 commit 6b09c96
Show file tree
Hide file tree
Showing 9 changed files with 858 additions and 0 deletions.
98 changes: 98 additions & 0 deletions core/src/components/GlobalSearch/CustomDateRangeModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<NcModal v-if="isModalOpen"
id="global-search"
:name="t('core', 'Date range filter')"
:show.sync="isModalOpen"
:size="'small'"
:clear-view-delay="0"
:title="t('Date range filter')"
@close="closeModal">
<!-- Custom date range -->
<div class="global-search-custom-date-modal">
<h1>{{ t('core', 'Date range filter') }}</h1>
<div class="global-search-custom-date-modal__pickers">
<NcDateTimePicker :id="'globalsearch-custom-date-range-start'"
v-model="dateFilter.startFrom"
:max="new Date()"
:label="t('core', 'Pick start date')"
type="date" />
<NcDateTimePicker :id="'globalsearch-custom-date-range-end'"
v-model="dateFilter.endAt"
:max="new Date()"
:label="t('core', 'Pick end date')"
type="date" />
</div>
<NcButton @click="applyCustomRange">
{{ t('core', 'Apply range') }}
<template #icon>
<CalendarRangeIcon :size="20" />
</template>
</NcButton>
</div>
</NcModal>
</template>

<script>
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcDateTimePicker from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue'
export default {
name: 'CustomDateRangeModal',
components: {
NcButton,
NcModal,
CalendarRangeIcon,
NcDateTimePicker,
},
props: {
isOpen: {
type: Boolean,
required: true,
},
},
data() {
return {
dateFilter: { startFrom: null, endAt: null },
}
},
computed: {
isModalOpen: {
get() {
return this.isOpen
},
set(value) {
this.$emit('update:is-open', value)
},
},
},
methods: {
closeModal() {
this.isModalOpen = false
},
applyCustomRange() {
this.$emit('set:custom-date-range', this.dateFilter)
this.closeModal()
},
},
}
</script>

<style lang="scss" scoped>
.global-search-custom-date-modal {
padding: 10px 20px 10px 20px;
h1 {
font-size: 16px;
font-weight: bolder;
line-height: 2em;
}
&__pickers {
display: flex;
flex-direction: column;
}
}
</style>
72 changes: 72 additions & 0 deletions core/src/components/GlobalSearch/SearchFilterChip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="chip">
<span class="icon">
<slot name="icon" />
<span v-if="pretext.length"> {{ pretext }} : </span>
</span>
<span class="text">{{ text }}</span>
<span class="close-icon" @click="deleteChip">
<CloseIcon :size="16" />
</span>
</div>
</template>

<script>
import CloseIcon from 'vue-material-design-icons/CloseThick.vue'
export default {
name: 'SearchFilterChip',
components: {
CloseIcon,
},
props: {
text: String,
pretext: String,
},
methods: {
deleteChip() {
this.$emit('delete', this.filter)
},
},
}
</script>

<style lang="scss" scoped>
.chip {
display: flex;
align-items: center;
padding: 2px 4px;
border: 1px solid var(--color-primary-element-light);
border-radius: 20px;
background-color: var(--color-primary-element-light);
margin: 2px;
font-size: 10px;
font-weight: bolder;
.icon {
display: flex;
align-items: center;
padding-right: 5px;
img {
width: 20px;
padding: 2px;
background-color: var(--color-primary-element);
border-radius: 20px;
}
}
.text {
margin: 0 2px;
}
.close-icon {
cursor: pointer;
:hover {
border-radius: 4px;
padding: 1px;
}
}
}
</style>
55 changes: 55 additions & 0 deletions core/src/global-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com>
*
* @author Fon E. Noel NFEBE <fenn25.fn@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { getLoggerBuilder } from '@nextcloud/logger'
import { getRequestToken } from '@nextcloud/auth'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Vue from 'vue'

import GlobalSearch from './views/GlobalSearch.vue'

// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())

const logger = getLoggerBuilder()
.setApp('global-search')
.detectUser()
.build()

Vue.mixin({
data() {
return {
logger,
}
},
methods: {
t,
n,
},
})

export default new Vue({
el: '#global-search',
// eslint-disable-next-line vue/match-component-file-name
name: 'GlobalSearchRoot',
render: h => h(GlobalSearch),
})
93 changes: 93 additions & 0 deletions core/src/services/GlobalSearchService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @copyright 2023, Fon E. Noel NFEBE <fenn25.fn@gmail.com>
*
* @author Fon E. Noel NFEBE <fenn25.fn@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { generateOcsUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import axios from '@nextcloud/axios'

export const defaultLimit = loadState('unified-search', 'limit-default')
export const minSearchLength = loadState('unified-search', 'min-search-length', 1)
export const enableLiveSearch = loadState('unified-search', 'live-search', true)

export const regexFilterIn = /(^|\s)in:([a-z_-]+)/ig
export const regexFilterNot = /(^|\s)-in:([a-z_-]+)/ig

/**
* Create a cancel token
*
* @return {import('axios').CancelTokenSource}
*/
const createCancelToken = () => axios.CancelToken.source()

/**
* Get the list of available search providers
*
* @return {Promise<Array>}
*/
export async function getProviders() {
try {
const { data } = await axios.get(generateOcsUrl('search/providers'), {
params: {
// Sending which location we're currently at
from: window.location.pathname.replace('/index.php', '') + window.location.search,
},
})
if ('ocs' in data && 'data' in data.ocs && Array.isArray(data.ocs.data) && data.ocs.data.length > 0) {
// Providers are sorted by the api based on their order key
return data.ocs.data
}
} catch (error) {
console.error(error)
}
return []
}

/**
* Get the list of available search providers
*
* @param {object} options destructuring object
* @param {string} options.type the type to search
* @param {string} options.query the search
* @param {number|string|undefined} options.cursor the offset for paginated searches
* @return {object} {request: Promise, cancel: Promise}
*/
export function search({ type, query, cursor }) {
/**
* Generate an axios cancel token
*/
const cancelToken = createCancelToken()

const request = async () => axios.get(generateOcsUrl('search/providers/{type}/search', { type }), {
cancelToken: cancelToken.token,
params: {
term: query,
cursor,
// Sending which location we're currently at
from: window.location.pathname.replace('/index.php', '') + window.location.search,
},
})

return {
request,
cancel: cancelToken.cancel,
}
}
67 changes: 67 additions & 0 deletions core/src/views/GlobalSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!--
- @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com>
-
- @author Fon E. Noel NFEBE <fenn25.fn@gmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div>
<NcButton aria-label="Global search" @click="toggleGlobalSearch">
<template #icon>
<Magnify class="unified-search__trigger" :size="22" />
</template>
</NcButton>
<GlobalSearchModal :is-visible="showGlobalSearch" :class="'global-search-modal'" />
</div>
</template>

<script>
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import Magnify from 'vue-material-design-icons/Magnify.vue'
import GlobalSearchModal from './GlobalSearchModal.vue'

export default {
name: 'GlobalSearch',
components: {
NcButton,
Magnify,
GlobalSearchModal,
},
data() {
return {
showGlobalSearch: false,
}
},
mounted() {
console.debug('Global search initialized!')
},
methods: {
toggleGlobalSearch() {
this.showGlobalSearch = !this.showGlobalSearch
},
},
}
</script>

<style lang="scss" scoped>
.global-search-modal {
::v-deep .modal-container {
height: 80%;
}
}
</style>
Loading

0 comments on commit 6b09c96

Please sign in to comment.