Skip to content

Commit

Permalink
Added taggable autocompletable query input
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianrudnik committed Oct 31, 2023
1 parent 75ef7e0 commit 75c3459
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ $scaleLG: 1.25 !default;
$focusOutlineColor: $primaryLightColor !default;
$focusOutline: 0 none !default;
$focusOutlineOffset: 0 !default;
$focusShadow: 0 0 0 0.2rem $focusOutlineColor !default;
$focusShadow: 0 0 0 0.05rem $focusOutlineColor !default;

//action icons
$actionIconWidth: 2rem !default;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/assets/styles/fonts.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@
font-style: normal;
font-weight: 700;
src: url('/fonts/inter-tight-v7-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
}
8 changes: 7 additions & 1 deletion frontend/src/assets/styles/main.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@import "fonts.css";
@import 'fonts.css';
@import '../../../node_modules/primevue/resources/primevue.min.css';
@import '../../../node_modules/primeflex/primeflex.css';
@import '../../../node_modules/primeicons/primeicons.css';

$errorColor: red;

@import '@/assets/primefaces/themes/ablegram/ablegram-light/blackwhite/theme.scss';

.content {
Expand All @@ -24,3 +26,7 @@
p {
line-height: 1.4;
}

body {
overflow-y: scroll;
}
233 changes: 188 additions & 45 deletions frontend/src/components/parts/QueryInput.vue
Original file line number Diff line number Diff line change
@@ -1,70 +1,213 @@
<template>
<div>
<div class="QueryInput">
<div class="p-inputgroup flex-1">
<span class="p-inputgroup-addon">
<i class="pi pi-search"></i>
</span>
<InputText
type="search"
v-model="query"
class="w-full"
:placeholder="t('query-input-component.placeholder')"
/>
<AutoComplete
v-model="currentSelection"
:forceSelection="false"
:panel-class="{ HidePanel: hidePanel }"
:pt="{ token: { class: 'SearchTagChip' } }"
:suggestions="suggestions"
:virtualScrollerOptions="{ itemSize: 50, scrollWidth: '100vw', scrollHeight: '300px' }"
multiple
placeholder="Search for files by string or tag"
@complete="aComplete"
@item-select="clearAfterSelect"
@clear="clearInput"
:class="{ 'p-invalid': !currentRequestValid }"
>
<template #option="slotProps">
<div class="flex align-options-center">
<SearchTag :tag="slotProps.option" />
</div>
</template>

<template #chip="slotProps">
<SearchTag :tag="slotProps.value" />
</template>

<template #removetokenicon="slotProps">
<div
@click="slotProps.onClick"
class="inline-flex bg-red-500 align-items-center px-1 text-white cursor-pointer"
>
<i class="pi pi-times"></i>
</div>
</template>
</AutoComplete>
</div>

<div class="my-2 text-sm" v-if="!isSearching && query !== ''">
{{ t('query-input-component.hits', { count: searchResultCount }) }}
<div class="Options flex justify-content-between gap-2 mt-2">
<div class="my-2 text flex gap-2">
<span>{{ t('query-input-component.hits', { count: currentResultCount }) }}</span>
<span class="text-red-500" v-if="!currentRequestValid">INVALID QUERY</span>
</div>

<div class="flex justify-content-end gap-2">
<RouterLink v-slot="{ navigate }" to="/x" custom>
<Button @click="navigate" text size="small" label="examples" />
</RouterLink>

<Button @click="clearInput" text size="small" label="clear" />
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import InputText from 'primevue/inputtext'
import { useI18n } from 'vue-i18n'
import { executeQuerySearch } from '@/plugins/search'
import type { Tag } from '@/stores/tags'
import { useTagStore } from '@/stores/tags'
import type { AutoCompleteCompleteEvent } from 'primevue/autocomplete'
import AutoComplete from 'primevue/autocomplete'
import SearchTag from '@/components/structure/SearchTag.vue'
import { useSearchResultStore } from '@/stores/results'
import { useStatStore } from '@/stores/stats'
import { useI18n } from 'vue-i18n'
import { watchDebounced } from '@vueuse/core'
import { executeQuerySearch } from '@/plugins/search'
import Button from 'primevue/button'
import { isEmpty } from 'lodash'
const { t } = useI18n()
const currentSelection = ref<Tag[]>([])
const suggestions = ref<Tag[]>([])
const currentPlainValue = ref('')
const hidePanel = ref(false)
const search = async (query: string) => {
useStatStore().isSearching = true
try {
const result = await executeQuerySearch({
size: 10,
query: {
query: query
},
fields: ['*']
})
useSearchResultStore().overwrite(
result.hits.map((h) => {
h.fields.id = h.id
return h.fields
})
)
} finally {
useStatStore().isSearching = false
}
const resultStore = useSearchResultStore()
const statStore = useStatStore()
const aComplete = (event: AutoCompleteCompleteEvent) => {
currentPlainValue.value = event.query
suggestions.value = useTagStore().entries.filter(
(entry) =>
entry.trans.plain?.toLowerCase().includes(event.query.toLowerCase()) ||
entry.id.includes(event.query.toLowerCase())
)
// Hide the panel if no suggestions have been found
hidePanel.value = suggestions.value.length === 0
}
const clearAfterSelect = () => {
currentPlainValue.value = ''
}
const clearInput = () => {
currentSelection.value = []
currentPlainValue.value = ''
currentResultCount.value = 0
currentRequestValid.value = true
resultStore.clear()
}
const query = ref('')
const currentResultCount = ref(0)
const currentRequestValid = ref(true)
const query = computed(() => {
const plain = currentPlainValue.value
const tags = currentSelection.value.map((tag) => 'tags:"' + tag.id + '"')
return (plain + ' ' + tags.join('')).trim()
})
watchDebounced(
query,
() => {
search(query.value)
async () => {
if (query.value.trim() === '') return
statStore.isSearching = true
try {
const result = await executeQuerySearch({
size: 10,
query: {
query: query.value
},
fields: ['*']
})
resultStore.overwrite(
result.hits.map((h) => {
h.fields.id = h.id
return h.fields
})
)
currentResultCount.value = result.total_hits
currentRequestValid.value = true
} catch {
currentRequestValid.value = false
} finally {
statStore.isSearching = false
}
},
{ debounce: 200, maxWait: 400 } // maxWait should be lower than the progress indicator "delay" in AppLayout.
{ debounce: 200, maxWait: 500 }
)
</script>

// setTimeout(() => {
// query.value = 'midi'
// }, 500)
<style lang="scss">
.SearchTagChip {
border: 0;
padding: 0;
margin: 0;
const isSearching = computed(() => useStatStore().isSearching)
const searchResultCount = computed(() => useStatStore().searchResultCount)
</script>
display: flex;
align-items: stretch;
justify-content: center;
border-top-width: 2px !important;
border-top-style: solid;
border-bottom-width: 2px !important;
border-bottom-style: solid;
border-left-width: 2px !important;
border-left-style: solid;
border-right-width: 2px !important;
border-right-style: solid;
border-radius: var(--border-radius);
.SearchTag {
margin: unset !important;
.Parts {
span {
border: unset !important;
}
span:first-child {
border-left-width: 2px !important;
border-left-style: solid;
border-top-left-radius: var(--border-radius) !important;
border-bottom-left-radius: var(--border-radius) !important;
}
span:last-child {
border-right-width: unset !important;
border-right-style: unset !important;
border-top-right-radius: unset !important;
border-bottom-right-radius: unset !important;
}
}
}
}
.p-autocomplete-panel.HidePanel {
visibility: hidden;
}
.QueryInput {
.Options {
a {
color: var(--gray-500);
text-decoration: underline;
&:hover {
color: var(--gray-700);
text-decoration: none;
}
}
}
}
</style>
5 changes: 3 additions & 2 deletions frontend/src/components/structure/SearchTag.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<span v-if="showCount" class="p-1 px-2 bg-gray-200 border-900">
{{ props.tag.count }}
</span>
<span v-if="$slots.action" class="p-1 px-2 bg-gray-200 border-900">
<slot name="action" />
</span>
</div>
</div>
</template>
Expand Down Expand Up @@ -48,8 +51,6 @@ if (props.tag.type === TagType.ColorValue) {

<style lang="scss">
.SearchTag {
cursor: default;
& > div > span {
overflow: hidden;
text-overflow: ellipsis;
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,12 @@ result-item-component:
live-set-result-item-component:
header: '@:common.label.live-set.s'


color:
# Colors picked from ableton track color picker.
# Names picked from gpick namification.

# Specials
"-1": No color
'-1': No color

# Column 1
0: Rose pink
Expand Down Expand Up @@ -436,4 +435,4 @@ color:
27: Light silver
41: Silver
55: Medium grey
69: Dark grey
69: Dark grey
2 changes: 1 addition & 1 deletion frontend/src/plugins/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const colorMap: { [key: number]: string } = {
27: '#CFCFCF',
41: '#A8A8A8',
55: '#7A7A7A',
69: '#3E3E3E',
69: '#3E3E3E'
}

export function resolveColorByIndex(index: number | undefined): string | null {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/stores/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface TagTranslation {
topic: string
detail: string
extra: string | undefined
plain?: string // contains the full tag in plain translated form for searches and copy/pastes
}

export interface Tag {
Expand Down Expand Up @@ -187,6 +188,7 @@ function classifyTag(tag: Tag): TagType {
}

function overrideTranslations(tag: Tag) {
// Special handling for mtime/btime fields with shared logic
if (tag.topic === 'file') {
const marker = tag.detail.substring(1)

Expand All @@ -211,6 +213,11 @@ function overrideTranslations(tag: Tag) {
break
}
}

// Finalize the plain text translated tag
tag.trans.plain = [tag.trans.topic, tag.trans.detail, tag.trans.extra]
.filter((v) => !!v)
.join(':')
}

function colorizeTag(tag: Tag): string | null {
Expand Down

0 comments on commit 75c3459

Please sign in to comment.