From 1a28f924c015fcd1875f772190b2f9ebcbd18581 Mon Sep 17 00:00:00 2001 From: fenn-cs Date: Mon, 9 Oct 2023 07:58:14 +0100 Subject: [PATCH] Feat: New UI global search 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 --- .../GlobalSearch/CustomDateRangeModal.vue | 98 ++++ .../GlobalSearch/SearchFilterChip.vue | 72 +++ core/src/global-search.js | 55 ++ core/src/services/GlobalSearchService.js | 107 ++++ core/src/views/GlobalSearch.vue | 67 +++ core/src/views/GlobalSearchModal.vue | 500 ++++++++++++++++++ core/templates/layout.user.php | 1 + lib/private/TemplateLayout.php | 1 + webpack.modules.js | 1 + 9 files changed, 902 insertions(+) create mode 100644 core/src/components/GlobalSearch/CustomDateRangeModal.vue create mode 100644 core/src/components/GlobalSearch/SearchFilterChip.vue create mode 100644 core/src/global-search.js create mode 100644 core/src/services/GlobalSearchService.js create mode 100644 core/src/views/GlobalSearch.vue create mode 100644 core/src/views/GlobalSearchModal.vue diff --git a/core/src/components/GlobalSearch/CustomDateRangeModal.vue b/core/src/components/GlobalSearch/CustomDateRangeModal.vue new file mode 100644 index 0000000000000..aca37b3c71b7d --- /dev/null +++ b/core/src/components/GlobalSearch/CustomDateRangeModal.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/core/src/components/GlobalSearch/SearchFilterChip.vue b/core/src/components/GlobalSearch/SearchFilterChip.vue new file mode 100644 index 0000000000000..4353234f77124 --- /dev/null +++ b/core/src/components/GlobalSearch/SearchFilterChip.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/core/src/global-search.js b/core/src/global-search.js new file mode 100644 index 0000000000000..f0c47fa189501 --- /dev/null +++ b/core/src/global-search.js @@ -0,0 +1,55 @@ +/** + * @copyright Copyright (c) 2020 Fon E. Noel NFEBE + * + * @author Fon E. Noel NFEBE + * + * @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 . + * + */ + +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), +}) diff --git a/core/src/services/GlobalSearchService.js b/core/src/services/GlobalSearchService.js new file mode 100644 index 0000000000000..47fd97a535ed8 --- /dev/null +++ b/core/src/services/GlobalSearchService.js @@ -0,0 +1,107 @@ +/** + * @copyright 2023, Fon E. Noel NFEBE + * + * @author Fon E. Noel NFEBE + * + * @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 . + * + */ + +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} + */ +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 +} diff --git a/core/src/views/GlobalSearch.vue b/core/src/views/GlobalSearch.vue new file mode 100644 index 0000000000000..4d1692a4f4eec --- /dev/null +++ b/core/src/views/GlobalSearch.vue @@ -0,0 +1,67 @@ + + + + + + diff --git a/core/src/views/GlobalSearchModal.vue b/core/src/views/GlobalSearchModal.vue new file mode 100644 index 0000000000000..55e4763ab3130 --- /dev/null +++ b/core/src/views/GlobalSearchModal.vue @@ -0,0 +1,500 @@ + + + + + diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 9c23930f32491..9e04fed196f27 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -68,6 +68,7 @@
+
diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index e2504363257ee..90ca935974074 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -110,6 +110,7 @@ public function __construct($renderAs, $appId = '') { $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); Util::addScript('core', 'unified-search', 'core'); + Util::addScript('core', 'global-search', 'core'); // Set body data-theme $this->assign('enabledThemes', []); diff --git a/webpack.modules.js b/webpack.modules.js index 67be70c5fed64..b380a8385ed0b 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -38,6 +38,7 @@ module.exports = { profile: path.join(__dirname, 'core/src', 'profile.js'), recommendedapps: path.join(__dirname, 'core/src', 'recommendedapps.js'), systemtags: path.resolve(__dirname, 'core/src', 'systemtags/merged-systemtags.js'), + 'global-search': path.join(__dirname, 'core/src', 'global-search.js'), 'unified-search': path.join(__dirname, 'core/src', 'unified-search.js'), 'unsupported-browser': path.join(__dirname, 'core/src', 'unsupported-browser.js'), 'unsupported-browser-redirect': path.join(__dirname, 'core/src', 'unsupported-browser-redirect.js'),