mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-15 21:56:12 +08:00
369 lines
13 KiB
Vue
369 lines
13 KiB
Vue
<template>
|
|
<GeneralDropdown ref="container" :canOpen="canOpen" :fieldOnlyOpen="true" @close="filtersOpened = false; flyoutOpened = false" @open="flyoutOpened = true">
|
|
<template v-slot:field>
|
|
<div class="sci--navigation--top-menu-search left-icon sci-input-container-v2"
|
|
:class="{'disabled' : !currentTeam, 'error': invalidQuery}" :title="i18n.t('nav.search')"
|
|
:data-e2e="'e2e-IF-topMenu-search'">
|
|
<input ref="searchField" type="text" class="!pr-20" v-model="searchQuery"
|
|
:class="{'active': flyoutOpened}"
|
|
@keydown="focusHistoryItem"
|
|
@keydown.down="focusQuickSearchResults"
|
|
@keydown.escape="closeFlyout"
|
|
@focus="openHistory" :placeholder="i18n.t('nav.search')" @keydown.enter="saveQuery"/>
|
|
<i class="sn-icon sn-icon-search"></i>
|
|
<div v-if="this.searchQuery.length > 1" class="flex items-center gap-1 absolute right-2 top-1.5">
|
|
<button class="btn btn-light icon-btn btn-xs" @click="this.searchQuery = ''; $refs.searchField.focus()" :data-e2e="'e2e-BT-topMenu-searchClear'">
|
|
<i class="sn-icon sn-icon-close !m-0" :title="i18n.t('nav.clear')"></i>
|
|
</button>
|
|
<button class="btn btn-light icon-btn btn-xs" :title="i18n.t('search.quick_search.search_options')"
|
|
:class="{'active': filtersOpened}" @click="filtersOpened = !filtersOpened" :data-e2e="'e2e-BT-topMenu-searchFilters'">
|
|
<i class="sn-icon sn-icon-search-options !m-0"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-slot:flyout >
|
|
<SearchFilters
|
|
class="px-3.5"
|
|
ref="filters"
|
|
v-if="filtersOpened"
|
|
:teamsUrl="teamsUrl"
|
|
:usersUrl="usersUrl"
|
|
:currentTeam="currentTeam"
|
|
:searchUrl="searchUrl"
|
|
:searchQuery="searchQuery"
|
|
@cancel="filtersOpened = false"
|
|
></SearchFilters>
|
|
<div v-else-if="showHistory" class="max-w-[600px]" data-e2e="e2e-DD-topMenu-searchHistory">
|
|
<div v-for="(query, i) in reversedPreviousQueries" @click="setQuery(query)" :key="i"
|
|
ref="historyItems"
|
|
tabindex="1"
|
|
@keydown="focusHistoryItem"
|
|
@keydown.enter="setQuery(query)"
|
|
class="flex px-3 min-h-11 items-center gap-2 hover:bg-sn-super-light-grey cursor-pointer">
|
|
<i class="sn-icon sn-icon-history-search"></i>
|
|
{{ query }}
|
|
</div>
|
|
</div>
|
|
<div v-else class="w-[600px]" data-e2e="e2e-FO-topMenu-quickSearch">
|
|
<div class="flex items-center gap-2">
|
|
<button class="btn btn-secondary btn-xs"
|
|
ref="experimentGroup"
|
|
:class="{'active': quickFilter === 'experiments'}"
|
|
@click="setQuickFilter('experiments')"
|
|
:data-e2e="'e2e-BT-topMenu-quickSearch-experiments'">
|
|
{{ i18n.t('search.quick_search.experiments') }}
|
|
</button>
|
|
<button class="btn btn-secondary btn-xs"
|
|
:class="{'active': quickFilter === 'my_modules'}"
|
|
@click="setQuickFilter('my_modules')"
|
|
:data-e2e="'e2e-BT-topMenu-quickSearch-tasks'">
|
|
{{ i18n.t('search.quick_search.tasks') }}
|
|
</button>
|
|
<button class="btn btn-secondary btn-xs"
|
|
:class="{'active': quickFilter === 'results'}"
|
|
@click="setQuickFilter('results')"
|
|
:data-e2e="'e2e-BT-topMenu-quickSearch-taskResults'">
|
|
{{ i18n.t('search.quick_search.results') }}
|
|
</button>
|
|
</div>
|
|
<hr class="my-2">
|
|
<a v-if="!loading" v-for="(result, i) in results" :key="i"
|
|
:href="getUrl(result.attributes)"
|
|
class="px-3 py-2 hover:bg-sn-super-light-grey cursor-pointer focus:no-underline
|
|
text-sn-black hover:no-underline active:no-underline hover:text-black block"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<i class="sn-icon shrink-0" :class="getIcon(result.type)" :title="getTitle(result.type)"></i>
|
|
<span v-if="result.attributes.archived">(A)</span>
|
|
<StringWithEllipsis class="grow max-w-[400px]" :text="getName(result.attributes)"></StringWithEllipsis>
|
|
<div class="ml-auto pl-4 text-sn-grey text-xs shrink-0">
|
|
{{ result.attributes.updated_at }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="text-sn-grey text-xs flex items-center gap-1 pl-8"
|
|
:class="{'opacity-0': result.type.includes('label_templates')}"
|
|
>
|
|
<div v-for="(breadcrumb, i) in getBreadcrumb(result.attributes)" :key="i"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<span v="if" v-if="i !== 0">/</span>
|
|
<span :title="breadcrumb" class="truncate max-w-[130px]">{{ breadcrumb }}</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
<div v-else v-for="i in Array(5).fill(5)" class="px-3 py-2">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<div class="h-5 w-5 bg-sn-light-grey rounded shrink-0"></div>
|
|
<div class="h-5 grow max-w-[200px] bg-sn-light-grey rounded shrink-0"></div>
|
|
<div class="h-5 w-12 bg-sn-light-grey rounded shrink-0 ml-auto"></div>
|
|
</div>
|
|
<div class="flex items-center gap-2 pl-8">
|
|
<div class="h-3 grow max-w-[200px] bg-sn-light-grey rounded shrink-0"></div>
|
|
</div>
|
|
</div>
|
|
<div v-if="!loading && results.length === 0" class="p-2 flex items-center gap-6">
|
|
<i class="sn-icon sn-icon-search text-sn-sleepy-grey" style="font-size: 64px !important;"></i>
|
|
<div>
|
|
<b>{{ i18n.t('search.quick_search.empty_title', {team: currentTeamName}) }}</b>
|
|
<div class="text-xs text-sn-dark-grey">
|
|
{{ i18n.t('search.quick_search.empty_description', {query: searchQuery}) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="my-2">
|
|
<button class="btn btn-light truncate !block leading-10 max-w-[600px]" @click="searchValue" :data-e2e="'e2e-BT-topMenu-quickSearch-allSearchResults'">
|
|
{{ i18n.t('search.quick_search.all_results', {query: searchQuery}) }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</GeneralDropdown>
|
|
</template>
|
|
|
|
<script>
|
|
/* global HelperModule GLOBAL_CONSTANTS */
|
|
|
|
import GeneralDropdown from '../shared/general_dropdown.vue';
|
|
import StringWithEllipsis from '../shared/string_with_ellipsis.vue';
|
|
import SearchFilters from '../global_search/filters.vue';
|
|
import axios from '../../packs/custom_axios.js';
|
|
|
|
export default {
|
|
name: 'QuickSearch',
|
|
props: {
|
|
quickSearchUrl: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
currentTeam: {
|
|
type: Number
|
|
},
|
|
searchUrl: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
teamsUrl: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
usersUrl: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
},
|
|
components: {
|
|
GeneralDropdown,
|
|
StringWithEllipsis,
|
|
SearchFilters
|
|
},
|
|
computed: {
|
|
reversedPreviousQueries() {
|
|
return [...this.previousQueries].reverse();
|
|
},
|
|
canOpen() {
|
|
return this.previousQueries.length > 0 || this.searchQuery.length > 1;
|
|
},
|
|
showHistory() {
|
|
return this.searchQuery.length < 2;
|
|
},
|
|
currentTeamName() {
|
|
return document.querySelector('body').dataset.currentTeamName;
|
|
},
|
|
invalidQuery() {
|
|
return this.searchQuery.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH;
|
|
}
|
|
},
|
|
watch: {
|
|
searchQuery() {
|
|
this.openHistory();
|
|
|
|
if (this.searchQuery.length > 1) {
|
|
this.fetchQuickSearchResults();
|
|
}
|
|
},
|
|
filtersOpened() {
|
|
if (this.filtersOpened) this.$nextTick(() => { this.focusFilters() });
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
searchQuery: '',
|
|
previousQueries: [],
|
|
quickFilter: null,
|
|
results: [],
|
|
loading: false,
|
|
filtersOpened: false,
|
|
focusedHistoryItem: null,
|
|
flyoutOpened: false
|
|
};
|
|
},
|
|
mounted() {
|
|
this.previousQueries = JSON.parse(localStorage.getItem('quickSearchHistory') || '[]');
|
|
},
|
|
methods: {
|
|
openHistory() {
|
|
this.$refs.container.isOpen = this.canOpen;
|
|
},
|
|
getIcon(type) {
|
|
switch (type) {
|
|
case 'projects':
|
|
return 'sn-icon-projects';
|
|
case 'experiments':
|
|
return 'sn-icon-experiment';
|
|
case 'my_modules':
|
|
return 'sn-icon-task';
|
|
case 'project_folders':
|
|
return 'sn-icon-folder';
|
|
case 'protocols':
|
|
return 'sn-icon-protocols-templates';
|
|
case 'results':
|
|
return 'sn-icon-results';
|
|
case 'repository_rows':
|
|
return 'sn-icon-inventory';
|
|
case 'reports':
|
|
return 'sn-icon-reports';
|
|
case 'steps':
|
|
return 'sn-icon-steps';
|
|
case 'zebra_label_templates':
|
|
return 'sn-icon-label-templates';
|
|
case 'fluics_label_templates':
|
|
return 'sn-icon-label-templates';
|
|
default:
|
|
return null;
|
|
}
|
|
},
|
|
getTitle(type) {
|
|
switch (type) {
|
|
case 'projects':
|
|
return this.i18n.t('search.quick_search.project');
|
|
case 'experiments':
|
|
return this.i18n.t('search.quick_search.experiment');
|
|
case 'my_modules':
|
|
return this.i18n.t('search.quick_search.task');
|
|
case 'project_folders':
|
|
return this.i18n.t('search.quick_search.folder');
|
|
case 'protocols':
|
|
return this.i18n.t('search.quick_search.protocol');
|
|
case 'results':
|
|
return this.i18n.t('search.quick_search.result');
|
|
case 'repository_rows':
|
|
return this.i18n.t('search.quick_search.inventory_item');
|
|
case 'reports':
|
|
return this.i18n.t('search.quick_search.report');
|
|
case 'steps':
|
|
return this.i18n.t('search.quick_search.step');
|
|
case 'zebra_label_templates':
|
|
return this.i18n.t('search.quick_search.label_template');
|
|
case 'fluics_label_templates':
|
|
return this.i18n.t('search.quick_search.label_template');
|
|
default:
|
|
return null;
|
|
}
|
|
},
|
|
getName(attributes) {
|
|
return attributes.breadcrumbs[attributes.breadcrumbs.length - 1].name;
|
|
},
|
|
getUrl(attributes) {
|
|
return attributes.breadcrumbs[attributes.breadcrumbs.length - 1].url;
|
|
},
|
|
getBreadcrumb(attributes) {
|
|
const breadcrumbs = attributes.breadcrumbs.map((breadcrumb) => breadcrumb.name);
|
|
breadcrumbs.pop();
|
|
breadcrumbs.shift();
|
|
if (attributes.code) {
|
|
breadcrumbs.push(`ID: ${attributes.code}`);
|
|
}
|
|
return breadcrumbs;
|
|
},
|
|
setQuery(query) {
|
|
this.searchQuery = query;
|
|
this.$nextTick(() => {
|
|
this.$refs.searchField.focus();
|
|
});
|
|
},
|
|
saveQuery() {
|
|
if (this.searchQuery.length > 1) {
|
|
this.previousQueries.push(this.searchQuery);
|
|
|
|
if (this.previousQueries.length > 5) {
|
|
this.previousQueries = this.previousQueries.slice(1);
|
|
}
|
|
|
|
localStorage.setItem('quickSearchHistory', JSON.stringify(this.previousQueries));
|
|
|
|
this.searchValue();
|
|
}
|
|
},
|
|
setQuickFilter(filter) {
|
|
this.quickFilter = this.quickFilter === filter ? null : filter;
|
|
this.fetchQuickSearchResults();
|
|
},
|
|
fetchQuickSearchResults() {
|
|
if (this.loading || this.invalidQuery) return;
|
|
|
|
this.loading = true;
|
|
|
|
const params = {
|
|
query: this.searchQuery,
|
|
filter: this.quickFilter
|
|
};
|
|
|
|
axios.get(this.quickSearchUrl, { params })
|
|
.then((response) => {
|
|
this.results = response.data.data;
|
|
this.loading = false;
|
|
if (params.query !== this.searchQuery) {
|
|
this.fetchQuickSearchResults();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.results = [];
|
|
this.loading = false;
|
|
});
|
|
},
|
|
searchValue() {
|
|
if (this.searchQuery.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH) {
|
|
HelperModule.flashAlertMsg(this.i18n.t('general.query.length_too_long', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH }), 'danger');
|
|
return;
|
|
}
|
|
|
|
window.open(`${this.searchUrl}?q=${this.searchQuery}&teams[]=${this.currentTeam}&include_archived=true`, '_self');
|
|
},
|
|
focusHistoryItem(event) {
|
|
if (this.focusedHistoryItem === null && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
this.focusedHistoryItem = 0;
|
|
this.$refs.historyItems[this.focusedHistoryItem]?.focus();
|
|
} else if (event.key === 'ArrowDown') {
|
|
event.preventDefault();
|
|
this.focusedHistoryItem += 1;
|
|
if (this.focusedHistoryItem >= this.$refs.historyItems.length) {
|
|
this.focusedHistoryItem = 0;
|
|
}
|
|
this.$refs.historyItems[this.focusedHistoryItem]?.focus();
|
|
} else if (event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
this.focusedHistoryItem -= 1;
|
|
if (this.focusedHistoryItem < 0) {
|
|
this.focusedHistoryItem = this.$refs.historyItems.length - 1;
|
|
}
|
|
this.$refs.historyItems[this.focusedHistoryItem]?.focus();
|
|
}
|
|
},
|
|
focusQuickSearchResults(e) {
|
|
this.$refs.experimentGroup?.focus();
|
|
e.preventDefault();
|
|
},
|
|
focusFilters() {
|
|
const { filters } = this.$refs;
|
|
|
|
if (filters) {
|
|
filters.$refs.groupButtons[0].focus();
|
|
}
|
|
},
|
|
closeFlyout() {
|
|
this.$refs.container.isOpen = false;
|
|
}
|
|
}
|
|
};
|
|
</script>
|