-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
9 changed files
with
902 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/** | ||
* @copyright Copyright (c) 2020 Fon E. Noel NFEBE <[email protected]> | ||
* | ||
* @author Fon E. Noel NFEBE <[email protected]> | ||
* | ||
* @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), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @copyright 2023, Fon E. Noel NFEBE <[email protected]> | ||
* | ||
* @author Fon E. Noel NFEBE <[email protected]> | ||
* | ||
* @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, generateUrl } 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, | ||
} | ||
} | ||
|
||
/** | ||
* Get the list of active contacts | ||
* | ||
* @param {object} filter filter contacts by string | ||
* @param filter.searchTerm | ||
* @return {object} {request: Promise} | ||
*/ | ||
export async function getContacts({ searchTerm }) { | ||
const { data: { contacts } } = await axios.post(generateUrl('/contactsmenu/contacts'), { | ||
filter: searchTerm, | ||
}) | ||
return contacts | ||
} |
Oops, something went wrong.