Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements basic search frontend #79

Merged
merged 105 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
644c6fa
Make views a package
jacobtylerwalls Aug 20, 2024
0278b76
Small view cleanups
jacobtylerwalls Aug 20, 2024
b8809b6
Remove .all() cruft
jacobtylerwalls Aug 21, 2024
826b3e6
Add cursor_tuple_fraction optimization for queryset iterators
jacobtylerwalls Aug 21, 2024
23319f8
Initial commit of search backend #67
jacobtylerwalls Aug 21, 2024
c1b211a
Add caching
jacobtylerwalls Aug 21, 2024
ccedae9
Handle all language values
jacobtylerwalls Aug 22, 2024
5021c69
Add fuzzy searching
jacobtylerwalls Aug 22, 2024
8783cdc
Make chosen parentage deterministic
jacobtylerwalls Aug 22, 2024
760e742
Add polyhierarchical flag
jacobtylerwalls Aug 22, 2024
66ccf68
Shorten the serialization of parent path (just show labels)
jacobtylerwalls Aug 22, 2024
880fc19
Add pagination
jacobtylerwalls Aug 22, 2024
18c5de6
Rename search term param
jacobtylerwalls Aug 22, 2024
fe86962
Avoid empty narrower keys
jacobtylerwalls Aug 22, 2024
53e0105
Fix typo that broke cache
jacobtylerwalls Aug 23, 2024
f43f913
fixup! Add fuzzy
jacobtylerwalls Aug 23, 2024
818b51f
rough out search bar #19
chrabyrd Aug 23, 2024
60a551f
nti #19
chrabyrd Aug 26, 2024
e5be36a
Return all results when term is empty
jacobtylerwalls Aug 26, 2024
b848642
fixup! Add fuzzy
jacobtylerwalls Aug 26, 2024
0a18d49
nit #19
chrabyrd Aug 26, 2024
11f5d6d
Merge branch 'jtw/search-backend' of https://github.com/archesproject…
chrabyrd Aug 26, 2024
bcb29d3
nit #19
chrabyrd Aug 27, 2024
ef9d291
nit #19
chrabyrd Aug 27, 2024
bf65286
Merge branch 'main' of https://github.com/archesproject/arches-lingo …
chrabyrd Aug 27, 2024
1709d49
nit #19
chrabyrd Aug 27, 2024
579b99e
Make views a module
jacobtylerwalls Aug 20, 2024
c338dc5
Small view cleanups
jacobtylerwalls Aug 20, 2024
75a897f
Remove .all() cruft
jacobtylerwalls Aug 21, 2024
311b439
Add cursor_tuple_fraction optimization for queryset iterators
jacobtylerwalls Aug 21, 2024
c05e7aa
Initial commit of search backend #67
jacobtylerwalls Aug 21, 2024
da209d0
Add caching
jacobtylerwalls Aug 21, 2024
0ec9e8f
Handle all language values
jacobtylerwalls Aug 22, 2024
fc081dd
Add fuzzy searching
jacobtylerwalls Aug 22, 2024
91fb56b
Make chosen parentage deterministic
jacobtylerwalls Aug 22, 2024
ae6fd63
Add polyhierarchical flag
jacobtylerwalls Aug 22, 2024
87ac1a6
Shorten the serialization of parent path (just show labels)
jacobtylerwalls Aug 22, 2024
6a5f68b
Add pagination
jacobtylerwalls Aug 22, 2024
c1aa03a
Rename search term param
jacobtylerwalls Aug 22, 2024
0b43d2d
Avoid empty narrower keys
jacobtylerwalls Aug 22, 2024
89214ac
Return all results when term is empty
jacobtylerwalls Aug 26, 2024
27f2770
Merge branch 'jtw/search-backend' of https://github.com/archesproject…
chrabyrd Aug 27, 2024
6b64f06
Set TEST_RUNNER
jacobtylerwalls Aug 27, 2024
b29b9c0
fixup! Handle all language
jacobtylerwalls Aug 27, 2024
639efe5
adds typing #19
chrabyrd Aug 27, 2024
fae465e
remove mock call #19
chrabyrd Aug 27, 2024
ecab8f8
nit #19
chrabyrd Aug 27, 2024
5c48d38
Avoid looking up languages against the Languages model
jacobtylerwalls Aug 27, 2024
2308eef
Test basic search API params
jacobtylerwalls Aug 27, 2024
aff792c
adds pagination #19
chrabyrd Aug 27, 2024
85cca11
nit #19
chrabyrd Aug 27, 2024
d08c429
Merge branch 'jtw/search-backend' of https://github.com/archesproject…
chrabyrd Aug 27, 2024
539147e
Make views a module
jacobtylerwalls Aug 20, 2024
e54f706
Small view cleanups
jacobtylerwalls Aug 20, 2024
ed16aa0
Remove .all() cruft
jacobtylerwalls Aug 21, 2024
8d75b68
Add cursor_tuple_fraction optimization for queryset iterators
jacobtylerwalls Aug 21, 2024
55f6eda
Initial commit of search backend #67
jacobtylerwalls Aug 21, 2024
af32bc5
Add caching
jacobtylerwalls Aug 21, 2024
823b9d4
Handle all language values
jacobtylerwalls Aug 22, 2024
8cb74a5
Add fuzzy searching
jacobtylerwalls Aug 22, 2024
8a3991c
Make chosen parentage deterministic
jacobtylerwalls Aug 22, 2024
4dcd7f3
Add polyhierarchical flag
jacobtylerwalls Aug 22, 2024
bf868db
Shorten the serialization of parent path (just show labels)
jacobtylerwalls Aug 22, 2024
744880a
Add pagination
jacobtylerwalls Aug 22, 2024
ab3d427
Rename search term param
jacobtylerwalls Aug 22, 2024
c166e83
Avoid empty narrower keys
jacobtylerwalls Aug 22, 2024
d160f0d
Return all results when term is empty
jacobtylerwalls Aug 26, 2024
1ddb5a4
Avoid looking up languages against the Languages model
jacobtylerwalls Aug 27, 2024
7df77dc
Test basic search API params
jacobtylerwalls Aug 27, 2024
890e029
Import views from submodules
jacobtylerwalls Aug 28, 2024
9abcb33
Factor out api views
jacobtylerwalls Aug 28, 2024
1aae8a2
Factor out ConceptBuilder
jacobtylerwalls Aug 28, 2024
3b2a7f8
fixup! Add fuzzy
jacobtylerwalls Aug 28, 2024
a3b6040
fixup! Make chosen parentage deterministic
jacobtylerwalls Aug 28, 2024
2db979e
fixup! Factor out ConceptBuilder
jacobtylerwalls Aug 28, 2024
2c7bee7
fixup! Add fuzzy
jacobtylerwalls Aug 28, 2024
f8f6ae7
fixup! Add fuzzy
jacobtylerwalls Aug 28, 2024
cd1b7c9
Enable clickjacking protection middleware
jacobtylerwalls Aug 28, 2024
38dd2b2
move to dialog component #19
chrabyrd Aug 28, 2024
3c039e5
merge #19
chrabyrd Aug 28, 2024
eb08e4c
nit #19
chrabyrd Aug 28, 2024
0bd47c2
Add containment matching
jacobtylerwalls Aug 28, 2024
4c717a1
nit #19
chrabyrd Aug 28, 2024
68735d8
Merge branch 'jtw/search-backend' of https://github.com/archesproject…
chrabyrd Aug 28, 2024
1cf3755
Remove Python 3.10 from test matrix, update classifiers
jacobtylerwalls Aug 29, 2024
10855c0
nit #19
chrabyrd Aug 29, 2024
482099d
nit #19
chrabyrd Aug 29, 2024
e8b3079
nit #19
chrabyrd Aug 30, 2024
1613cc6
Merge branch 'jtw/search-backend' of https://github.com/archesproject…
chrabyrd Aug 30, 2024
3c0565a
nit #19
chrabyrd Aug 30, 2024
452affa
nit #19
chrabyrd Aug 30, 2024
4ffa6d4
nit #19
chrabyrd Sep 2, 2024
e2d723c
nit #19
chrabyrd Sep 2, 2024
9a702ae
nit #19
chrabyrd Sep 2, 2024
093d808
merge #19
chrabyrd Oct 28, 2024
7866d33
nit #19
chrabyrd Oct 28, 2024
bedbb80
nit #19
chrabyrd Oct 28, 2024
a0214d6
Merge branch 'cherry-pick-backend-fixes' of https://github.com/arches…
chrabyrd Oct 29, 2024
c7ec5df
merge #19
chrabyrd Oct 29, 2024
e9de73d
Merge branch 'cherry-pick-backend-fixes' of https://github.com/arches…
chrabyrd Oct 29, 2024
2635a98
Merge branch 'main' of https://github.com/archesproject/arches-lingo …
chrabyrd Oct 29, 2024
14de455
fixes frontend #19
chrabyrd Oct 29, 2024
6fe8434
addresses PR comments #19
chrabyrd Oct 29, 2024
cede13e
nit #19
chrabyrd Oct 29, 2024
aa1a902
nit #19
chrabyrd Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions arches_lingo/src/arches_lingo/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ main {
font-family: sans-serif;
height: 100vh;
width: 100vw;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
Expand Down
25 changes: 25 additions & 0 deletions arches_lingo/src/arches_lingo/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,28 @@ export const fetchUser = async () => {
throw new Error((error as Error).message || response.statusText);
}
};

export const fetchSearchResults = async (
searchTerm: string,
items: number,
page: number,
) => {
const params = new URLSearchParams({
term: searchTerm,
items: items.toString(),
page: page.toString(),
});

const url = `${arches.urls.api_search}?${params.toString()}`;

const response = await fetch(url);
try {
const responseJson = await response.json();
if (response.ok) {
return responseJson;
}
throw new Error(responseJson.message);
} catch (error) {
throw new Error((error as Error).message || response.statusText);
}
};
313 changes: 313 additions & 0 deletions arches_lingo/src/arches_lingo/components/basic-search/BasicSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
<script setup lang="ts">
import { nextTick, ref, watch, onMounted } from "vue";
import { useGettext } from "vue3-gettext";

import AutoComplete from "primevue/autocomplete";
import Button from "primevue/button";

import { useToast } from "primevue/usetoast";

import SortAndFilterControls from "@/arches_lingo/components/basic-search/SortAndFilterControls.vue";
import SearchResult from "@/arches_lingo/components/basic-search/SearchResult.vue";

import { fetchSearchResults } from "@/arches_lingo/api.ts";
import { DEFAULT_ERROR_TOAST_LIFE, ERROR } from "@/arches_lingo/constants.ts";

import type { VirtualScrollerLazyEvent } from "primevue/virtualscroller";

import type { Concept } from "@/arches_lingo/types.ts";

const { $gettext } = useGettext();
const toast = useToast();

const props = defineProps({
searchResultsPerPage: {
type: Number,
required: true,
},
searchResultItemSize: {
type: Number,
required: true,
},
});

const autoCompleteInstance = ref<InstanceType<typeof AutoComplete> | null>(
null,
);
const autoCompleteKey = ref(0);
const computedSearchResultsHeight = ref("");
const isLoading = ref(false);
const isLoadingAdditionalResults = ref(false);
const searchResults = ref<Concept[]>([]);
const searchResultsPage = ref(1);
const searchResultsTotalCount = ref(0);
const query = ref("");
const shouldShowClearInputButton = ref(false);

const clearInput = () => {
query.value = "";
shouldShowClearInputButton.value = false;
focusInput();
};

const fetchData = async (searchTerm: string, items: number, page: number) => {
isLoading.value = true;
shouldShowClearInputButton.value = Boolean(page !== 1);

try {
const parsedResponse = await fetchSearchResults(
searchTerm,
items,
page,
);

if (query.value) {
if (page === 1) {
searchResults.value = parsedResponse.data;
} else {
searchResults.value = [
...searchResults.value,
...parsedResponse.data,
];
}

searchResultsPage.value = parsedResponse.current_page;
searchResultsTotalCount.value = parsedResponse.total_results;
shouldShowClearInputButton.value = true;
}
} catch (error) {
toast.add({
severity: ERROR,
life: DEFAULT_ERROR_TOAST_LIFE,
summary: $gettext("Failed to fetch data."),
detail: error instanceof Error ? error.message : undefined,
});

searchResults.value = [];
searchResultsPage.value = 1;
searchResultsTotalCount.value = 0;
shouldShowClearInputButton.value = true;
} finally {
isLoading.value = false;
isLoadingAdditionalResults.value = false;
}
};

const focusInput = () => {
if (autoCompleteInstance.value) {
autoCompleteInstance.value.$el.querySelector("input").focus();
}
};

const keepOverlayVisible = () => {
if (
query.value &&
searchResults.value.length &&
isLoading.value === isLoadingAdditionalResults.value
) {
nextTick(() => autoCompleteInstance.value?.show());
}
};

const loadAdditionalSearchResults = (event: VirtualScrollerLazyEvent) => {
if (
event.last >= searchResultsPage.value * props.searchResultsPerPage &&
event.last <= searchResultsTotalCount.value
) {
isLoadingAdditionalResults.value = true;
searchResultsPage.value += 1;

fetchData(
query.value,
props.searchResultsPerPage,
searchResultsPage.value,
);
}
};

const navigateToReport = () => {};

onMounted(focusInput);

// handles the edge case of inputting a query then clearing the input before the data is fetched.
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
watch(query, (query) => {
if (!query) {
autoCompleteKey.value += 1;
nextTick(() => {
focusInput();
});
}
});

/**
* This isn't fantastic but it's the best way I can find to get around PrimeVue's lack of support for
* updating the height of a `VirtualScroller` overlay, much less updating the height dynamically.
*/
watch(searchResults, (searchResults) => {
if (searchResults?.length) {
const rootFontSize = parseFloat(
getComputedStyle(document.documentElement).fontSize,
);
const itemHeightInRem = props.searchResultItemSize / rootFontSize; // Convert to rem based on the root font size
const computedHeightInRem = searchResults.length * itemHeightInRem;

const viewHeightInPixels = window.innerHeight * 0.6;
const viewHeightInRem = viewHeightInPixels / rootFontSize; // Convert 60vh to rem

if (computedHeightInRem > viewHeightInRem) {
computedSearchResultsHeight.value = "60vh";
} else {
computedSearchResultsHeight.value = `${computedHeightInRem}rem`;
}
} else {
computedSearchResultsHeight.value = "2.25rem";
}
});
</script>

<template>
<div
id="basic-search-container"
style="width: 100%; font-family: sans-serif"
>
<div style="display: flex; align-items: center">
<i
class="pi pi-search search-icon"
aria-hidden="true"
/>

<AutoComplete
ref="autoCompleteInstance"
:key="autoCompleteKey"
v-model="query"
option-label="id"
append-to="#basic-search-container"
:loading="isLoading && !isLoadingAdditionalResults"
:placeholder="$gettext('Quick Search')"
:pt="{
option: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about doing this instead of the onMounted() in the script tag? This moves the functionality a little closer to what it pertains to.

Suggested change
option: {
hooks: { onMounted: focusInput },
option: {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This did not seem to work

style: {
padding: '0',
borderRadius: '0',
},
chrabyrd marked this conversation as resolved.
Show resolved Hide resolved
},
overlay: {
style: {
padding: '0',
borderRadius: '0',
},
},
list: {
style: {
padding: '0',
gap: '0',
},
},
}"
:suggestions="searchResults"
:virtual-scroller-options="{
itemSize: props.searchResultItemSize,
lazy: true,
onLazyLoad: loadAdditionalSearchResults,
scrollHeight: computedSearchResultsHeight,
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
style: {
minHeight: computedSearchResultsHeight,
maxHeight: computedSearchResultsHeight,
},
numToleratedItems: 1,
}"
@complete="
() => {
autoCompleteInstance?.hide();
fetchData(query, props.searchResultsPerPage, 1);
}
"
@option-select="navigateToReport"
@before-hide="keepOverlayVisible"
@update:model-value="
(value) => {
if (!value) {
shouldShowClearInputButton = false;
}
}
"
>
<template #empty>
<div style="font-family: sans-serif; text-align: center">
{{ $gettext("No search results found") }}
</div>
</template>
Comment on lines +235 to +239
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as I get used to PrimeVue, is this being used instead of :empty-selection-message=... because we're templating #option and thus need to template all slots? Or can you just template one slot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for a few reasons. basically it's 6-in-one-half-dozen-the-other for me, in that both solutions take about the same LoE and lines of code.

Using :empty-selection-message= still needs styling

Screenshot 2024-10-29 at 1 53 18 PM

which can be done, but then why not just slot out a div and make it more versatile for potential future adjustments. idk what if I wanna include a gif of a sad cat or something?

<template #option="slotProps">
<SearchResult :search-result="slotProps" />
</template>
<template
v-if="isLoadingAdditionalResults"
#footer
>
<div class="footer">
<i
class="pi pi-spin pi-spinner p-virtualscroller-loader"
chrabyrd marked this conversation as resolved.
Show resolved Hide resolved
:aria-label="
$gettext('Loading additional search results')
"
/>
</div>
</template>
</AutoComplete>

<Button
v-if="shouldShowClearInputButton"
class="p-button-text clear-button"
icon="pi pi-times"
:aria-label="$gettext('Clear Input')"
@click="clearInput"
/>
</div>

<SortAndFilterControls />
</div>
</template>

<style scoped>
.clear-button {
background-color: transparent !important;
position: absolute;
inset-inline-end: 0.2rem;
color: var(--p-input-color);
}

.search-icon {
position: absolute;
inset-inline-start: 1rem;
z-index: 1;
font-weight: bold;
}

.p-autocomplete {
width: 100%;
}

.footer {
text-align: center;
position: absolute;
bottom: 0;
inset-inline-end: 0;

i {
font-size: 2rem;
background-color: transparent;
padding: 1rem;
height: 4rem;
}
}

:deep(.p-autocomplete .p-autocomplete-input) {
width: 100%;
padding: 1rem 2.5rem;
border: none;
}

:deep(.p-autocomplete-overlay) {
position: static !important;
}
</style>
Loading
Loading