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'),