implement better hanlding of filter search suggestions

This commit is contained in:
marlon-erler
2026-09-21 21:23:49 +02:00
parent ea422b3de6
commit deaa784148
94 changed files with 778 additions and 3415 deletions
Binary file not shown.
-1
View File
@@ -162,7 +162,6 @@ export function BoardPage(
coreViewModel,
boardViewModel.searchViewModel,
coreViewModel.translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
boardViewModel.isPresentingFilterModal,
)}
<div children:set={taskSettingsModal}></div>
Binary file not shown.
+38 -6
View File
@@ -9,7 +9,6 @@ export function SearchModal<T>(
coreViewModel: CoreViewModel,
searchViewModel: SearchViewModel<T>,
headline: string,
converter: React.StateItemConverter<T>,
isOpen: React.State<boolean>,
) {
function close() {
@@ -31,6 +30,7 @@ export function SearchModal<T>(
<div class="flex-row">
<input
id="focused"
style="max-width: unset"
placeholder={
coreViewModel.translations.general.searchLabel
}
@@ -70,14 +70,15 @@ export function SearchModal<T>(
</button>
</div>
<hr></hr>
<hr></hr>
<h3>{coreViewModel.translations.general.searchSuggestionsLabel}</h3>
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
class="flex-column gap"
children:append={[
searchViewModel.matchingObjects,
converter,
searchViewModel.suggestions,
(suggestion: string) => SuggestionView(suggestion, searchViewModel, coreViewModel),
]}
></div>
</main>
@@ -89,3 +90,34 @@ export function SearchModal<T>(
</div>
);
}
function SuggestionView<T>(suggestion: string, searchViewModel: SearchViewModel<T>, coreViewModel: CoreViewModel) {
const isApplied = React.createProxyState([searchViewModel.appliedQuery], ()=>searchViewModel.appliedQuery.value == suggestion);
function deleteSuggestion() {
searchViewModel.deleteSuggestion(suggestion);
}
function applySuggestion() {
searchViewModel.search(suggestion);
}
return <div class="flex-row surface align-center">
<span class="width-100 flex-1 padding-h">{suggestion}</span>
<button
class="danger"
aria-label={coreViewModel.translations.general.deleteButton}
on:click={deleteSuggestion}
>
<span class="icon">delete</span>
</button>
<button
class="primary"
aria-label={coreViewModel.translations.general.applyButton}
on:click={applySuggestion}
toggle:disabled={isApplied}
>
<span class="icon">check</span>
</button>
</div>
}
+4 -1
View File
@@ -9,9 +9,11 @@ const englishTranslations = {
searchButtonClearAudioLabel: "clear search query",
abortButton: "Abort",
applyButton: "Apply",
backButton: "Back",
cancelButton: "Cancel",
closeButton: "Close",
backButton: "Back",
deleteButton: "Delete",
continueButton: "Continue",
confirmButton: "Confirm",
@@ -22,6 +24,7 @@ const englishTranslations = {
fileVersionLabel: "Version",
searchLabel: "Search",
searchSuggestionsLabel: "Recent searches",
waitingLabel: "Waiting...",
restoreConnection: "Restore connection",
Binary file not shown.
-3
View File
@@ -83,9 +83,6 @@ export default class CoreViewModel {
// SUGGESTIONS
// boards & tasks
boardFilterStringSuggestions: React.ListState<string> =
new React.ListState();
taskCategorySuggestions: React.ListState<string> = new React.ListState();
taskStatusSuggestions: React.ListState<string> = new React.ListState();
Binary file not shown.
+15 -5
View File
@@ -28,6 +28,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
isPresentingFilterModal: React.State<boolean> = new React.State(false);
searchViewModel: SearchViewModel<TaskViewModel>;
searchSuggestions = new React.ListState<string>();
filteredTaskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
isFilterActive: React.State<boolean>;
@@ -115,17 +116,25 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
];
this.coreViewModel.storageModel.write(suggestionPath, "");
if (
!this.coreViewModel.boardFilterStringSuggestions.value.has(
!this.searchSuggestions.value.has(
searchTerm,
)
) && searchTerm != ""
) {
this.coreViewModel.boardFilterStringSuggestions.add(searchTerm);
this.searchSuggestions.add(searchTerm);
}
const lastSearchPath: string[] = this.getLastSearchPath();
this.coreViewModel.storageModel.write(lastSearchPath, searchTerm);
};
handleSearchRemoved = (searchTerm: string): void => {
const suggestionPath: string[] = [
...this.getPreviousSearchesPath(),
searchTerm,
];
this.coreViewModel.storageModel.remove(suggestionPath);
}
// view
showTask = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.boardId != this.boardInfo.fileId) {
@@ -224,7 +233,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
const dirPath: string[] = this.getPreviousSearchesPath();
const searches: string[] =
this.coreViewModel.storageModel.list(dirPath);
this.coreViewModel.boardFilterStringSuggestions.add(...searches);
this.searchSuggestions.add(...searches.filter(x => x != ""));
};
restoreSearch = (): void => {
@@ -297,12 +306,13 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
this.taskViewModels,
this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.coreViewModel.boardFilterStringSuggestions,
this.searchSuggestions,
);
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
this.handleNewSearch(newQuery);
});
this.restoreSearch();
this.searchSuggestions.handleRemovals(this.handleSearchRemoved);
this.isFilterActive = React.createProxyState(
[this.searchViewModel.appliedQuery],
Binary file not shown.
+8 -16
View File
@@ -2,16 +2,9 @@ import * as React from "bloatless-react";
import { checkDoesObjectMatchSearch } from "../../Model/Utility/utility";
export default class SearchViewModel<T> {
// data
allObjects: React.ListState<T> | React.MapState<T>;
getStringsOfObject: (object: T) => string[];
// state
appliedQuery = new React.State<string>("");
searchInput = new React.State<string>("");
matchingObjects: React.ListState<T>;
suggestions: React.ListState<string>;
// guards
cannotApplySearch: React.State<boolean> = React.createProxyState(
@@ -45,18 +38,17 @@ export default class SearchViewModel<T> {
this.searchInput.value = "";
}
deleteSuggestion = (suggestion: string): void => {
this.suggestions.remove(suggestion);
}
// init
constructor(
allObjects: React.ListState<T> | React.MapState<T>,
matchingObjects: React.ListState<T>,
getStringsOfObject: (object: T) => string[],
suggestions: React.ListState<string>,
public allObjects: React.ListState<T> | React.MapState<T>,
public matchingObjects: React.ListState<T>,
public getStringsOfObject: (object: T) => string[],
public suggestions: React.ListState<string>,
) {
this.allObjects = allObjects;
this.matchingObjects = matchingObjects;
this.getStringsOfObject = getStringsOfObject;
this.suggestions = suggestions;
// handle new objects
this.allObjects.handleAddition((newObject: T) => {
const doesMatch: boolean = this.checkDoesMatchSearch(newObject);