mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Add quick filter flyout [SCI-10248]
This commit is contained in:
parent
968b34d74c
commit
57c5140267
|
@ -42,6 +42,22 @@ class SearchController < ApplicationController
|
||||||
def new
|
def new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quick
|
||||||
|
results = [
|
||||||
|
Project.first,
|
||||||
|
Experiment.first,
|
||||||
|
MyModule.first,
|
||||||
|
Protocol.first,
|
||||||
|
RepositoryRow.first,
|
||||||
|
Result.first,
|
||||||
|
Step.first,
|
||||||
|
Report.first,
|
||||||
|
LabelTemplate.first
|
||||||
|
].compact
|
||||||
|
|
||||||
|
render json: results, each_serializer: QuickSearchSerializer
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_vars
|
def load_vars
|
||||||
|
|
228
app/javascript/vue/navigation/quick_search.vue
Normal file
228
app/javascript/vue/navigation/quick_search.vue
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
<template>
|
||||||
|
<GeneralDropdown ref="container" :canOpen="canOpen" :fieldOnlyOpen="true">
|
||||||
|
<template v-slot:field>
|
||||||
|
<div class="sci--navigation--top-menu-search left-icon sci-input-container-v2" :class="{'disabled' : !currentTeam}" :title="i18n.t('nav.search')">
|
||||||
|
<input ref="searchField" type="text" class="!pr-9" v-model="searchQuery" :placeholder="i18n.t('nav.search')" @keyup.enter="saveQuery"/>
|
||||||
|
<i class="sn-icon sn-icon-search"></i>
|
||||||
|
<i v-if="this.searchQuery.length > 0" class="sn-icon sn-icon-close absolute right-1 top-0.5" @click="this.searchQuery = ''"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:flyout >
|
||||||
|
<div v-if="showHistory" class="max-w-[600px]">
|
||||||
|
<div v-for="(query, i) in previousQueries.reverse()" @click="setQuery(query)" :key="i"
|
||||||
|
class="flex px-3 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="max-w-[600px]">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="btn btn-secondary btn-xs" @click="setQuickFilter('experiments')">
|
||||||
|
{{ i18n.t('search.quick_search.experiments') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-xs" @click="setQuickFilter('my_modules')">
|
||||||
|
{{ i18n.t('search.quick_search.tasks') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-xs" @click="setQuickFilter('results')">
|
||||||
|
{{ i18n.t('search.quick_search.results') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-xs" @click="setQuickFilter('repository_rows')">
|
||||||
|
{{ i18n.t('search.quick_search.inventory_items') }}
|
||||||
|
</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
|
||||||
|
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)"></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">
|
||||||
|
<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-6 w-6 bg-sn-light-grey rounded shrink-0"></div>
|
||||||
|
<div class="h-6 grow max-w-[200px] bg-sn-light-grey rounded shrink-0"></div>
|
||||||
|
<div class="h-6 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') }}</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">
|
||||||
|
<div class="btn btn-light" @click="searchValue">
|
||||||
|
{{ i18n.t('search.quick_search.all_results', {query: searchQuery}) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</GeneralDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GeneralDropdown from '../shared/general_dropdown.vue';
|
||||||
|
import StringWithEllipsis from '../shared/string_with_ellipsis.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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
GeneralDropdown,
|
||||||
|
StringWithEllipsis
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canOpen() {
|
||||||
|
return this.previousQueries.length > 0 || this.searchQuery.length > 1;
|
||||||
|
},
|
||||||
|
showHistory() {
|
||||||
|
return this.searchQuery.length < 2;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
searchQuery() {
|
||||||
|
this.$refs.container.isOpen = this.canOpen;
|
||||||
|
|
||||||
|
if (this.searchQuery.length > 1) {
|
||||||
|
this.fetchQuickSearchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchQuery: '',
|
||||||
|
previousQueries: [],
|
||||||
|
quickFilter: null,
|
||||||
|
results: [],
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.previousQueries = JSON.parse(localStorage.getItem('quickSearchHistory') || '[]');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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 'label-templates':
|
||||||
|
return 'sn-icon-label-templates';
|
||||||
|
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();
|
||||||
|
breadcrumbs.push(`ID: ${attributes.code}`);
|
||||||
|
return breadcrumbs;
|
||||||
|
},
|
||||||
|
setQuery(query) {
|
||||||
|
this.searchQuery = query;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.searchField.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveQuery() {
|
||||||
|
if (this.searchQuery.length > 0) {
|
||||||
|
this.previousQueries.push(this.searchQuery);
|
||||||
|
|
||||||
|
if (this.previousQueries.length > 5) {
|
||||||
|
this.previousQueries.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('quickSearchHistory', JSON.stringify(this.previousQueries));
|
||||||
|
|
||||||
|
this.searchValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setQuickFilter(filter) {
|
||||||
|
this.quickFilter = this.quickFilter === filter ? null : filter;
|
||||||
|
this.fetchQuickSearchResults();
|
||||||
|
},
|
||||||
|
fetchQuickSearchResults() {
|
||||||
|
if (this.loading) 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() {
|
||||||
|
window.open(`${this.searchUrl}?q=${this.searchQuery}`, '_self');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,9 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sci--navigation--top-menu-container">
|
<div class="sci--navigation--top-menu-container">
|
||||||
<div v-if="user" class="sci--navigation--top-menu-search left-icon sci-input-container-v2" :class="{'disabled' : !currentTeam}" :title="i18n.t('nav.search')">
|
<QuickSearch v-if="user" :quickSearchUrl="quickSearchUrl" :searchUrl="searchUrl" :currentTeam="currentTeam"></QuickSearch>
|
||||||
<input type="text" :placeholder="i18n.t('nav.search')" @change="searchValue"/>
|
|
||||||
<i class="sn-icon sn-icon-search"></i>
|
|
||||||
</div>
|
|
||||||
<div v-if="currentTeam" class="w-64">
|
<div v-if="currentTeam" class="w-64">
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
:value="currentTeam"
|
:value="currentTeam"
|
||||||
|
@ -70,6 +67,7 @@ import DropdownSelector from '../shared/legacy/dropdown_selector.vue';
|
||||||
import SelectDropdown from '../shared/select_dropdown.vue';
|
import SelectDropdown from '../shared/select_dropdown.vue';
|
||||||
import MenuDropdown from '../shared/menu_dropdown.vue';
|
import MenuDropdown from '../shared/menu_dropdown.vue';
|
||||||
import GeneralDropdown from '../shared/general_dropdown.vue';
|
import GeneralDropdown from '../shared/general_dropdown.vue';
|
||||||
|
import QuickSearch from './quick_search.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TopMenuContainer',
|
name: 'TopMenuContainer',
|
||||||
|
@ -78,12 +76,14 @@ export default {
|
||||||
NotificationsFlyout,
|
NotificationsFlyout,
|
||||||
SelectDropdown,
|
SelectDropdown,
|
||||||
MenuDropdown,
|
MenuDropdown,
|
||||||
GeneralDropdown
|
GeneralDropdown,
|
||||||
|
QuickSearch
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
url: String,
|
url: String,
|
||||||
notificationsUrl: String,
|
notificationsUrl: String,
|
||||||
unseenNotificationsUrl: String
|
unseenNotificationsUrl: String,
|
||||||
|
quickSearchUrl: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative" v-click-outside="closeMenu" >
|
<div class="relative" v-click-outside="closeMenu" >
|
||||||
<div ref="field" class="cursor-pointer" @click.stop="isOpen = (!isOpen || fieldOnlyOpen)">
|
<div ref="field" class="cursor-pointer" @click.stop="toggleMenu">
|
||||||
<slot name="field"></slot>
|
<slot name="field"></slot>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="isOpen">
|
<template v-if="isOpen">
|
||||||
|
@ -36,7 +36,8 @@ export default {
|
||||||
caret: { type: Boolean, default: false },
|
caret: { type: Boolean, default: false },
|
||||||
alwaysShow: { type: Boolean, default: false },
|
alwaysShow: { type: Boolean, default: false },
|
||||||
closeDropdown: { type: Boolean, default: false },
|
closeDropdown: { type: Boolean, default: false },
|
||||||
fieldOnlyOpen: { type: Boolean, default: false }
|
fieldOnlyOpen: { type: Boolean, default: false },
|
||||||
|
canOpen: { type: Boolean, default: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -60,6 +61,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleMenu() {
|
||||||
|
if (this.canOpen && (!this.isOpen || this.fieldOnlyOpen)) {
|
||||||
|
this.isOpen = true;
|
||||||
|
} else if (this.isOpen && !this.fieldOnlyOpen) {
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
closeMenu(e) {
|
closeMenu(e) {
|
||||||
if (e && e.target.closest('.sn-dropdown, .sn-select-dropdown, .sn-menu-dropdown, .dp__instance_calendar')) return;
|
if (e && e.target.closest('.sn-dropdown, .sn-select-dropdown, .sn-menu-dropdown, .dp__instance_calendar')) return;
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
|
|
26
app/javascript/vue/shared/string_with_ellipsis.vue
Normal file
26
app/javascript/vue/shared/string_with_ellipsis.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div :title="text" class="flex items-center">
|
||||||
|
<div class="truncate">
|
||||||
|
{{ text.slice(0, endCharacters * -1) }}
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0">
|
||||||
|
{{ text.slice(text.length - endCharacters) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'StringWithEllipsis',
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
endCharacters: {
|
||||||
|
type: Number,
|
||||||
|
default: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
67
app/serializers/concerns/breadcrumbs_helper.rb
Normal file
67
app/serializers/concerns/breadcrumbs_helper.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module BreadcrumbsHelper
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_breadcrumbs(subject, breadcrumbs)
|
||||||
|
return [] if subject.is_a?(NonExistantRecord)
|
||||||
|
|
||||||
|
case subject
|
||||||
|
when Project
|
||||||
|
parent = subject.team
|
||||||
|
url = project_path(subject)
|
||||||
|
when Experiment
|
||||||
|
parent = subject.project
|
||||||
|
url = my_modules_experiment_path(subject)
|
||||||
|
when MyModule
|
||||||
|
parent = subject.experiment
|
||||||
|
url = protocols_my_module_path(subject)
|
||||||
|
when Protocol
|
||||||
|
if subject.in_repository?
|
||||||
|
parent = subject.team
|
||||||
|
url = protocol_path(subject)
|
||||||
|
else
|
||||||
|
parent = subject.my_module
|
||||||
|
url = protocols_my_module_path(subject.my_module)
|
||||||
|
end
|
||||||
|
when Result
|
||||||
|
parent = subject.my_module
|
||||||
|
url = my_module_results_path(subject.my_module)
|
||||||
|
when ProjectFolder
|
||||||
|
parent = subject.team
|
||||||
|
url = project_folder_path(subject)
|
||||||
|
when RepositoryBase
|
||||||
|
parent = subject.team
|
||||||
|
url = repository_path(subject)
|
||||||
|
when RepositoryRow
|
||||||
|
parent = subject.team
|
||||||
|
url = repository_path(subject.repository)
|
||||||
|
when Report
|
||||||
|
parent = subject.team
|
||||||
|
|
||||||
|
url = if object.instance_of?(::Notification)
|
||||||
|
reports_path(
|
||||||
|
preview_report_id: subject.id,
|
||||||
|
preview_type: object.params[:report_type],
|
||||||
|
team_id: subject.team.id
|
||||||
|
)
|
||||||
|
else
|
||||||
|
reports_path(team_id: subject.team.id)
|
||||||
|
end
|
||||||
|
when LabelTemplate
|
||||||
|
parent = subject.team
|
||||||
|
url = label_template_path(subject)
|
||||||
|
when Team
|
||||||
|
parent = nil
|
||||||
|
url = projects_path(team: subject.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
breadcrumbs << { name: subject.name, url: url } if subject.name.present?
|
||||||
|
|
||||||
|
if parent
|
||||||
|
generate_breadcrumbs(parent, breadcrumbs)
|
||||||
|
else
|
||||||
|
breadcrumbs.reverse
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class NotificationSerializer < ActiveModel::Serializer
|
class NotificationSerializer < ActiveModel::Serializer
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
include BreadcrumbsHelper
|
||||||
|
|
||||||
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today
|
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today
|
||||||
|
|
||||||
|
@ -30,62 +31,4 @@ class NotificationSerializer < ActiveModel::Serializer
|
||||||
object.read_at.present?
|
object.read_at.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def generate_breadcrumbs(subject, breadcrumbs)
|
|
||||||
return [] if subject.is_a?(NonExistantRecord)
|
|
||||||
|
|
||||||
case subject
|
|
||||||
when Project
|
|
||||||
parent = subject.team
|
|
||||||
url = project_path(subject)
|
|
||||||
when Experiment
|
|
||||||
parent = subject.project
|
|
||||||
url = my_modules_experiment_path(subject)
|
|
||||||
when MyModule
|
|
||||||
parent = subject.experiment
|
|
||||||
url = protocols_my_module_path(subject)
|
|
||||||
when Protocol
|
|
||||||
if subject.in_repository?
|
|
||||||
parent = subject.team
|
|
||||||
url = protocol_path(subject)
|
|
||||||
else
|
|
||||||
parent = subject.my_module
|
|
||||||
url = protocols_my_module_path(subject.my_module)
|
|
||||||
end
|
|
||||||
when Result
|
|
||||||
parent = subject.my_module
|
|
||||||
url = my_module_results_path(subject.my_module)
|
|
||||||
when ProjectFolder
|
|
||||||
parent = subject.team
|
|
||||||
url = project_folder_path(subject)
|
|
||||||
when RepositoryBase
|
|
||||||
parent = subject.team
|
|
||||||
url = repository_path(subject)
|
|
||||||
when RepositoryRow
|
|
||||||
parent = subject.team
|
|
||||||
url = repository_path(subject.repository)
|
|
||||||
when Report
|
|
||||||
parent = subject.team
|
|
||||||
url = reports_path(
|
|
||||||
preview_report_id: subject.id,
|
|
||||||
preview_type: object.params[:report_type],
|
|
||||||
team_id: subject.team.id
|
|
||||||
)
|
|
||||||
when LabelTemplate
|
|
||||||
parent = subject.team
|
|
||||||
url = label_template_path(subject)
|
|
||||||
when Team
|
|
||||||
parent = nil
|
|
||||||
url = projects_path(team: subject.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
breadcrumbs << { name: subject.name, url: url } if subject.name.present?
|
|
||||||
|
|
||||||
if parent
|
|
||||||
generate_breadcrumbs(parent, breadcrumbs)
|
|
||||||
else
|
|
||||||
breadcrumbs.reverse
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
28
app/serializers/quick_search_serializer.rb
Normal file
28
app/serializers/quick_search_serializer.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class QuickSearchSerializer < ActiveModel::Serializer
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
include BreadcrumbsHelper
|
||||||
|
|
||||||
|
attributes :updated_at, :archived, :breadcrumbs, :code
|
||||||
|
|
||||||
|
def archived
|
||||||
|
@object.archived?
|
||||||
|
rescue StandardError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def code
|
||||||
|
@object.code
|
||||||
|
rescue StandardError
|
||||||
|
@object.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def updated_at
|
||||||
|
I18n.l(@object.updated_at, format: :full_date)
|
||||||
|
end
|
||||||
|
|
||||||
|
def breadcrumbs
|
||||||
|
generate_breadcrumbs(@object, [])
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,6 +3,7 @@
|
||||||
<top-menu-container
|
<top-menu-container
|
||||||
url="<%= top_menu_navigations_path %>"
|
url="<%= top_menu_navigations_path %>"
|
||||||
notifications-url="<%= user_notifications_path %>"
|
notifications-url="<%= user_notifications_path %>"
|
||||||
|
quick-search-url="<%= quick_search_path %>"
|
||||||
unseen-notifications-url="<%= unseen_counter_user_notifications_path %>" />
|
unseen-notifications-url="<%= unseen_counter_user_notifications_path %>" />
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
|
@ -426,6 +426,14 @@ en:
|
||||||
list_html: "List"
|
list_html: "List"
|
||||||
|
|
||||||
search:
|
search:
|
||||||
|
quick_search:
|
||||||
|
experiments: "Experiments"
|
||||||
|
tasks: "Tasks"
|
||||||
|
results: "Results"
|
||||||
|
inventory_items: "Inventory items"
|
||||||
|
all_results: "All search results for \"%{query}\""
|
||||||
|
empty_title: "No quick search results found."
|
||||||
|
empty_description: "Quick search only checks titles. Press enter or click \"All search results for '%{query}'\" to find all available matches."
|
||||||
whole_word: "Match any whole word"
|
whole_word: "Match any whole word"
|
||||||
whole_phrase: "Match whole phrase"
|
whole_phrase: "Match whole phrase"
|
||||||
match_case: "Match case sensitive"
|
match_case: "Match case sensitive"
|
||||||
|
|
|
@ -802,6 +802,11 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
get 'search' => 'search#index'
|
get 'search' => 'search#index'
|
||||||
get 'search/new' => 'search#new', as: :new_search
|
get 'search/new' => 'search#new', as: :new_search
|
||||||
|
resource :search, only: [], controller: :search do
|
||||||
|
collection do
|
||||||
|
get :quick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# We cannot use 'resources :assets' because assets is a reserved route
|
# We cannot use 'resources :assets' because assets is a reserved route
|
||||||
# in Rails (assets pipeline) and causes funky behavior
|
# in Rails (assets pipeline) and causes funky behavior
|
||||||
|
|
Loading…
Reference in a new issue