add history of searches in board

This commit is contained in:
marlon-erler
2024-07-27 03:41:47 +02:00
parent e9425b95fa
commit 845a9d2f35
6 changed files with 165 additions and 87 deletions
+102 -67
View File
@@ -2006,6 +2006,65 @@
}; };
}; };
// src/ViewModel/Utility/searchViewModel.ts
var SearchViewModel = class {
// data
allObjects;
getStringsOfObject;
// state
appliedQuery = new State("");
searchInput = new State("");
matchingObjects;
suggestions;
// guards
cannotApplySearch = createProxyState(
[this.searchInput, this.appliedQuery],
() => this.searchInput.value == this.appliedQuery.value
);
// methods
search = (searchTerm) => {
this.searchInput.value = searchTerm;
this.applySearch();
};
applySearch = () => {
this.appliedQuery.value = this.searchInput.value;
console.log("applying search");
this.matchingObjects.clear();
for (const object of this.allObjects.value.values()) {
const doesMatch = this.checkDoesMatchSearch(object);
if (doesMatch == false) continue;
this.matchingObjects.add(object);
}
};
// init
constructor(allObjects, matchingObjects, getStringsOfObject, suggestions) {
this.allObjects = allObjects;
this.matchingObjects = matchingObjects;
this.getStringsOfObject = getStringsOfObject;
this.suggestions = suggestions;
this.allObjects.handleAddition((newObject) => {
const doesMatch = this.checkDoesMatchSearch(newObject);
if (doesMatch == false) {
this.matchingObjects.remove(newObject);
} else {
if (this.matchingObjects.value.has(newObject)) return;
this.matchingObjects.add(newObject);
this.allObjects.handleRemoval(newObject, () => {
this.matchingObjects.remove(newObject);
});
}
});
}
// utility
checkDoesMatchSearch = (object) => {
return checkDoesObjectMatchSearch(
this.appliedQuery.value,
this.getStringsOfObject,
object
);
};
};
// src/ViewModel/Pages/boardViewModel.ts // src/ViewModel/Pages/boardViewModel.ts
var BoardViewModel = class extends TaskContainingPageViewModel { var BoardViewModel = class extends TaskContainingPageViewModel {
// init // init
@@ -2035,6 +2094,15 @@
this.updateTaskIndices(); this.updateTaskIndices();
} }
); );
this.searchViewModel = new SearchViewModel(
this.taskViewModels,
this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.searchSuggestions
);
this.searchViewModel.appliedQuery.subscribe((newQuery) => {
this.storeSearchTerm(newQuery);
});
} }
storageModel; storageModel;
boardsAndTasksModel; boardsAndTasksModel;
@@ -2051,7 +2119,9 @@
isSelected; isSelected;
isPresentingSettingsModal = new State(false); isPresentingSettingsModal = new State(false);
isPresentingFilterModal = new State(false); isPresentingFilterModal = new State(false);
searchViewModel;
filteredTaskViewModels = new ListState(); filteredTaskViewModels = new ListState();
searchSuggestions = new ListState();
// paths // paths
getBasePath = () => { getBasePath = () => {
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)]; return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)];
@@ -2059,6 +2129,9 @@
getLastUsedBoardPath = () => { getLastUsedBoardPath = () => {
return [...this.getBasePath(), "last-used-view" /* LastUsedView */]; return [...this.getBasePath(), "last-used-view" /* LastUsedView */];
}; };
getPreviousSearchesPath = () => {
return [...this.getBasePath(), "previous-searches" /* PreviousSearches */];
};
// settings // settings
saveSettings = () => { saveSettings = () => {
const newBoardInfoFileContent = BoardsAndTasksModel.createBoardInfoFileContent( const newBoardInfoFileContent = BoardsAndTasksModel.createBoardInfoFileContent(
@@ -2101,6 +2174,13 @@
if (lastUsedView == null) return; if (lastUsedView == null) return;
this.selectedPage.value = lastUsedView; this.selectedPage.value = lastUsedView;
}; };
storeSearchTerm = (searchTerm) => {
const path = [...this.getPreviousSearchesPath(), searchTerm];
this.storageModel.write(path, "");
if (!this.searchSuggestions.value.has(searchTerm)) {
this.searchSuggestions.add(searchTerm);
}
};
// view // view
showTask = (taskFileContent) => { showTask = (taskFileContent) => {
if (taskFileContent.boardId != this.boardInfo.fileId) { if (taskFileContent.boardId != this.boardInfo.fileId) {
@@ -2170,9 +2250,15 @@
} }
this.updateTaskIndices(); this.updateTaskIndices();
}; };
loadSearchSuggestions = () => {
const dirPath = this.getPreviousSearchesPath();
const searches = this.storageModel.list(dirPath);
this.searchSuggestions.add(...searches);
};
loadData = () => { loadData = () => {
this.restoreLastUsedView(); this.restoreLastUsedView();
this.loadTasks(); this.loadTasks();
this.loadSearchSuggestions();
}; };
}; };
@@ -3521,83 +3607,34 @@
return RibbonButton(label, icon, isSelected, select); return RibbonButton(label, icon, isSelected, select);
} }
// src/ViewModel/Utility/searchViewModel.ts
var SearchViewModel = class {
// data
allObjects;
getStringsOfObject;
// state
appliedQuery = new State("");
searchInput = new State("");
matchingObjects;
// guards
cannotApplySearch = createProxyState(
[this.searchInput, this.appliedQuery],
() => this.searchInput.value == this.appliedQuery.value
);
// methods
applySearch = () => {
this.appliedQuery.value = this.searchInput.value;
console.log("applying search");
this.matchingObjects.clear();
for (const object of this.allObjects.value.values()) {
const doesMatch = this.checkDoesMatchSearch(object);
if (doesMatch == false) continue;
this.matchingObjects.add(object);
}
};
// init
constructor(allObjects, matchingObjects, getStringsOfObject) {
this.allObjects = allObjects;
this.matchingObjects = matchingObjects;
this.getStringsOfObject = getStringsOfObject;
this.allObjects.handleAddition((newObject) => {
const doesMatch = this.checkDoesMatchSearch(newObject);
if (doesMatch == false) {
this.matchingObjects.remove(newObject);
} else {
if (this.matchingObjects.value.has(newObject)) return;
this.matchingObjects.add(newObject);
this.allObjects.handleRemoval(newObject, () => {
this.matchingObjects.remove(newObject);
});
}
});
}
// utility
checkDoesMatchSearch = (object) => {
return checkDoesObjectMatchSearch(
this.appliedQuery.value,
this.getStringsOfObject,
object
);
};
};
// src/View/Modals/searchModal.tsx // src/View/Modals/searchModal.tsx
function SearchModal(headline, allObjects, filteredObjects, converter, getStringsOfObject, isOpen) { function SearchModal(searchViewModel, headline, converter, isOpen) {
const viewModel = new SearchViewModel(
allObjects,
filteredObjects,
getStringsOfObject
);
function close() { function close() {
isOpen.value = false; isOpen.value = false;
} }
const id = v4_default();
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isOpen }, /* @__PURE__ */ createElement("div", { style: "max-width: 64rem" }, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, headline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isOpen }, /* @__PURE__ */ createElement("div", { style: "max-width: 64rem" }, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, headline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input", "input",
{ {
placeholder: translations.general.searchLabel, placeholder: translations.general.searchLabel,
"bind:value": viewModel.searchInput, "bind:value": searchViewModel.searchInput,
"on:enter": viewModel.applySearch "on:enter": searchViewModel.applySearch,
list: id
}
), /* @__PURE__ */ createElement(
"datalist",
{
hidden: true,
id,
"children:append": [searchViewModel.suggestions, StringToOption]
} }
), /* @__PURE__ */ createElement( ), /* @__PURE__ */ createElement(
"button", "button",
{ {
class: "primary", class: "primary",
"aria-label": translations.general.searchButtonAudioLabel, "aria-label": translations.general.searchButtonAudioLabel,
"on:click": viewModel.applySearch, "on:click": searchViewModel.applySearch,
"toggle:disabled": viewModel.cannotApplySearch "toggle:disabled": searchViewModel.cannotApplySearch
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "search") /* @__PURE__ */ createElement("span", { class: "icon" }, "search")
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement( )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
@@ -3605,7 +3642,7 @@
{ {
class: "grid gap", class: "grid gap",
style: "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))", style: "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))",
"children:append": [filteredObjects, converter] "children:append": [searchViewModel.matchingObjects, converter]
} }
)), /* @__PURE__ */ createElement("button", { "on:click": close }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); )), /* @__PURE__ */ createElement("button", { "on:click": close }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
} }
@@ -3696,11 +3733,9 @@
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "add") /* @__PURE__ */ createElement("span", { class: "icon" }, "add")
))), /* @__PURE__ */ createElement("div", { class: "content main-content", "children:set": mainContent }), BoardSettingsModal(boardViewModel), SearchModal( ))), /* @__PURE__ */ createElement("div", { class: "content main-content", "children:set": mainContent }), BoardSettingsModal(boardViewModel), SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline, translations.chatPage.task.filterTasksHeadline,
boardViewModel.taskViewModels,
boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry, TaskViewModelToEntry,
TaskViewModel.getStringsForFilter,
boardViewModel.isPresentingFilterModal boardViewModel.isPresentingFilterModal
), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal })); ), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal }));
} }
+1 -1
View File
@@ -38,7 +38,7 @@ COLORS
/* definition */ /* definition */
:root { :root {
--background: hsla(0, 0%, 100%, 100%); --background: hsla(0, 0%, 100%, 100%);
--background-glass: hsla(0, 0%, 100%, 40%); --background-glass: hsla(0, 0%, 100%, 5%);
--background-transparent: hsla(0, 0%, 0%, 5%); --background-transparent: hsla(0, 0%, 0%, 5%);
--background-hover: hsla(0, 0%, 55%, 12%); --background-hover: hsla(0, 0%, 55%, 12%);
--background-active: hsla(0, 0%, 55%, 12%); --background-active: hsla(0, 0%, 55%, 12%);
+1 -3
View File
@@ -115,11 +115,9 @@ export function BoardPage(boardViewModel: BoardViewModel) {
{BoardSettingsModal(boardViewModel)} {BoardSettingsModal(boardViewModel)}
{SearchModal( {SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline, translations.chatPage.task.filterTasksHeadline,
boardViewModel.taskViewModels,
boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry, TaskViewModelToEntry,
TaskViewModel.getStringsForFilter,
boardViewModel.isPresentingFilterModal boardViewModel.isPresentingFilterModal
)} )}
<div children:set={taskSettingsModal}></div> <div children:set={taskSettingsModal}></div>
+16 -14
View File
@@ -1,26 +1,22 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import SearchViewModel from "../../ViewModel/Utility/searchViewModel"; import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
import { StringToOption } from "../Components/option";
import { translations } from "../translations"; import { translations } from "../translations";
import { v4 } from "uuid";
export function SearchModal<T>( export function SearchModal<T>(
searchViewModel: SearchViewModel<T>,
headline: string, headline: string,
allObjects: React.ListState<T> | React.MapState<T>,
filteredObjects: React.ListState<T>,
converter: React.StateItemConverter<T>, converter: React.StateItemConverter<T>,
getStringsOfObject: (object: T) => string[],
isOpen: React.State<boolean> isOpen: React.State<boolean>
) { ) {
const viewModel: SearchViewModel<T> = new SearchViewModel(
allObjects,
filteredObjects,
getStringsOfObject
);
function close() { function close() {
isOpen.value = false; isOpen.value = false;
} }
const id = v4();
return ( return (
<div class="modal" toggle:open={isOpen}> <div class="modal" toggle:open={isOpen}>
<div style="max-width: 64rem"> <div style="max-width: 64rem">
@@ -29,14 +25,20 @@ export function SearchModal<T>(
<div class="flex-row width-input"> <div class="flex-row width-input">
<input <input
placeholder={translations.general.searchLabel} placeholder={translations.general.searchLabel}
bind:value={viewModel.searchInput} bind:value={searchViewModel.searchInput}
on:enter={viewModel.applySearch} on:enter={searchViewModel.applySearch}
list={id}
></input> ></input>
<datalist
hidden
id={id}
children:append={[searchViewModel.suggestions, StringToOption]}
></datalist>
<button <button
class="primary" class="primary"
aria-label={translations.general.searchButtonAudioLabel} aria-label={translations.general.searchButtonAudioLabel}
on:click={viewModel.applySearch} on:click={searchViewModel.applySearch}
toggle:disabled={viewModel.cannotApplySearch} toggle:disabled={searchViewModel.cannotApplySearch}
> >
<span class="icon">search</span> <span class="icon">search</span>
</button> </button>
@@ -47,7 +49,7 @@ export function SearchModal<T>(
<div <div
class="grid gap" class="grid gap"
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))" style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
children:append={[filteredObjects, converter]} children:append={[searchViewModel.matchingObjects, converter]}
></div> ></div>
</main> </main>
<button on:click={close}> <button on:click={close}>
+35 -1
View File
@@ -7,6 +7,7 @@ import BoardsAndTasksModel, {
import { Color } from "../../colors"; import { Color } from "../../colors";
import CoreViewModel from "../Global/coreViewModel"; import CoreViewModel from "../Global/coreViewModel";
import SearchViewModel from "../Utility/searchViewModel";
import StorageModel from "../../Model/Global/storageModel"; import StorageModel from "../../Model/Global/storageModel";
import TaskContainingPageViewModel from "./taskContainingPageViewModel"; import TaskContainingPageViewModel from "./taskContainingPageViewModel";
import TaskPageViewModel from "./taskPageViewModel"; import TaskPageViewModel from "./taskPageViewModel";
@@ -35,8 +36,10 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
isPresentingSettingsModal: React.State<boolean> = new React.State(false); isPresentingSettingsModal: React.State<boolean> = new React.State(false);
isPresentingFilterModal: React.State<boolean> = new React.State(false); isPresentingFilterModal: React.State<boolean> = new React.State(false);
searchViewModel: SearchViewModel<TaskViewModel>;
filteredTaskViewModels: React.ListState<TaskViewModel> = filteredTaskViewModels: React.ListState<TaskViewModel> =
new React.ListState(); new React.ListState();
searchSuggestions: React.ListState<string> = new React.ListState();
// paths // paths
getBasePath = (): string[] => { getBasePath = (): string[] => {
@@ -47,6 +50,10 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
return [...this.getBasePath(), BoardViewModelSubPath.LastUsedView]; return [...this.getBasePath(), BoardViewModelSubPath.LastUsedView];
}; };
getPreviousSearchesPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.PreviousSearches];
};
// settings // settings
saveSettings = (): void => { saveSettings = (): void => {
const newBoardInfoFileContent: BoardInfoFileContent = const newBoardInfoFileContent: BoardInfoFileContent =
@@ -70,7 +77,7 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
// methods // methods
createTask = (): void => { createTask = (): void => {
this.createTaskFromBoardId(this.boardInfo.fileId); this.createTaskFromBoardId(this.boardInfo.fileId);
} };
handleDropWithinBoard = (category?: string, status?: string): void => { handleDropWithinBoard = (category?: string, status?: string): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value; const draggedObject: any = this.coreViewModel.draggedObject.value;
@@ -99,6 +106,14 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
this.selectedPage.value = lastUsedView as BoardPageType; this.selectedPage.value = lastUsedView as BoardPageType;
}; };
storeSearchTerm = (searchTerm: string): void => {
const path: string[] = [...this.getPreviousSearchesPath(), searchTerm];
this.storageModel.write(path, "");
if (!this.searchSuggestions.value.has(searchTerm)) {
this.searchSuggestions.add(searchTerm);
}
};
// view // view
showTask = (taskFileContent: TaskFileContent): void => { showTask = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.boardId != this.boardInfo.fileId) { if (taskFileContent.boardId != this.boardInfo.fileId) {
@@ -186,9 +201,16 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
this.updateTaskIndices(); this.updateTaskIndices();
}; };
loadSearchSuggestions = (): void => {
const dirPath: string[] = this.getPreviousSearchesPath();
const searches: string[] = this.storageModel.list(dirPath);
this.searchSuggestions.add(...searches);
};
loadData = (): void => { loadData = (): void => {
this.restoreLastUsedView(); this.restoreLastUsedView();
this.loadTasks(); this.loadTasks();
this.loadSearchSuggestions();
}; };
// init // init
@@ -234,11 +256,23 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
this.updateTaskIndices(); this.updateTaskIndices();
} }
); );
// search
this.searchViewModel = new SearchViewModel(
this.taskViewModels,
this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.searchSuggestions
);
this.searchViewModel.appliedQuery.subscribe((newQuery) => {
this.storeSearchTerm(newQuery);
});
} }
} }
export enum BoardViewModelSubPath { export enum BoardViewModelSubPath {
LastUsedView = "last-used-view", LastUsedView = "last-used-view",
PreviousSearches = "previous-searches",
} }
// types // types
+10 -1
View File
@@ -12,6 +12,8 @@ export default class SearchViewModel<T> {
searchInput: React.State<string> = new React.State(""); searchInput: React.State<string> = new React.State("");
matchingObjects: React.ListState<T>; matchingObjects: React.ListState<T>;
suggestions: React.ListState<string>;
// guards // guards
cannotApplySearch: React.State<boolean> = React.createProxyState( cannotApplySearch: React.State<boolean> = React.createProxyState(
[this.searchInput, this.appliedQuery], [this.searchInput, this.appliedQuery],
@@ -19,6 +21,11 @@ export default class SearchViewModel<T> {
); );
// methods // methods
search = (searchTerm: string): void => {
this.searchInput.value = searchTerm;
this.applySearch();
};
applySearch = (): void => { applySearch = (): void => {
this.appliedQuery.value = this.searchInput.value; this.appliedQuery.value = this.searchInput.value;
console.log("applying search"); console.log("applying search");
@@ -36,11 +43,13 @@ export default class SearchViewModel<T> {
constructor( constructor(
allObjects: React.ListState<T> | React.MapState<T>, allObjects: React.ListState<T> | React.MapState<T>,
matchingObjects: React.ListState<T>, matchingObjects: React.ListState<T>,
getStringsOfObject: (object: T) => string[] getStringsOfObject: (object: T) => string[],
suggestions: React.ListState<string>
) { ) {
this.allObjects = allObjects; this.allObjects = allObjects;
this.matchingObjects = matchingObjects; this.matchingObjects = matchingObjects;
this.getStringsOfObject = getStringsOfObject; this.getStringsOfObject = getStringsOfObject;
this.suggestions = suggestions;
// handle new objects // handle new objects
this.allObjects.handleAddition((newObject: T) => { this.allObjects.handleAddition((newObject: T) => {