diff --git a/dist/index.js b/dist/index.js index 0aaff7ed..6095d4ac 100644 --- a/dist/index.js +++ b/dist/index.js @@ -353,6 +353,29 @@ } return [...values.values()].sort(localeCompare); } + function checkDoesObjectMatchSearch(query, getStringsOfObject, object) { + if (query == "") return true; + const stringsInObject = getStringsOfObject(object); + const wordsInObject = []; + for (const string of stringsInObject) { + const lowercaseWordsInString = string.toLocaleLowerCase().split(" ").filter((word) => word != ""); + wordsInObject.push(...lowercaseWordsInString); + } + const lowercaseWordsInQuery = query.toLowerCase().split(" ").filter((word) => word != ""); + for (const queryWord of lowercaseWordsInQuery) { + if (queryWord[0] == "-") { + const wordContent = queryWord.substring(1); + if (wordsInObject.includes(wordContent)) { + return false; + } + } else { + if (wordsInObject.includes(queryWord) == false) { + return false; + } + } + } + return true; + } var HandlerManager = class { handlers = /* @__PURE__ */ new Set(); // manage @@ -1613,7 +1636,6 @@ name = new State(""); color = new State("standard" /* Standard */); index = new State(0); - taskViewModels = new MapState(); selectedPage = new State( "list" /* List */ ); @@ -1621,6 +1643,8 @@ isSelected; isPresentingSettingsModal = new State(false); isPresentingFilterModal = new State(false); + taskViewModels = new MapState(); + filteredTaskViewModels = new ListState(); // paths getBasePath = () => { return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)]; @@ -1718,6 +1742,18 @@ boardViewModel.updateIndex(); } }; + // filter + getStringsFromTaskViewModel = (taskViewModel) => { + return [ + taskViewModel.task.name, + taskViewModel.task.description ?? "", + taskViewModel.task.category ?? "", + taskViewModel.task.status ?? "", + taskViewModel.task.priority ?? "", + taskViewModel.task.date ?? "", + taskViewModel.task.time ?? "" + ]; + }; // load loadListRelevantData = () => { this.name.value = this.boardInfo.name; @@ -2064,12 +2100,14 @@ var englishTranslations = { general: { deleteItemButtonAudioLabel: "delete item", + searchButtonAudioLabel: "search", abortButton: "Abort", closeButton: "Close", confirmButton: "Confirm", saveButton: "Save", setButton: "Set", - fileVersionLabel: "Version" + fileVersionLabel: "Version", + searchLabel: "Search" }, regional: { weekdays: { @@ -2187,6 +2225,8 @@ taskTimeLabel: "Time", deleteTaskButton: "Delete task", /// + filterTasksHeadline: "Filter Tasks", + /// renameCategoryInputPlaceholder: "Rename category," } } @@ -2481,7 +2521,7 @@ return viewBuilder(propertyValues); } - // src/ViewModel/Pages/taskPropertyBulkChangeViewModel.ts + // src/ViewModel/Utility/taskPropertyBulkChangeViewModel.ts var TaskPropertyBulkChangeViewModel = class { taskViewModels; // state @@ -2598,7 +2638,7 @@ return FilteredList( { category: categoryName }, (taskViewModel) => taskViewModel.task, - boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, (taskViewModels) => { const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); return /* @__PURE__ */ createElement("div", { class: "flex-column flex-no" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement( @@ -2669,7 +2709,7 @@ ), PropertyValueList( "category", (taskViewModel) => taskViewModel.task, - boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, (categories) => { return /* @__PURE__ */ createElement( "div", @@ -2780,6 +2820,94 @@ 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 + function SearchModal(headline, allObjects, filteredObjects, converter, getStringsOfObject, isOpen) { + const viewModel = new SearchViewModel( + allObjects, + filteredObjects, + getStringsOfObject + ); + function close() { + isOpen.value = false; + } + 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", + { + placeholder: translations.general.searchLabel, + "bind:value": viewModel.searchInput, + "on:enter": viewModel.applySearch + } + ), /* @__PURE__ */ createElement( + "button", + { + class: "primary", + "aria-label": translations.general.searchButtonAudioLabel, + "on:click": viewModel.applySearch, + "toggle:disabled": viewModel.cannotApplySearch + }, + /* @__PURE__ */ createElement("span", { class: "icon" }, "search") + )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement( + "div", + { + class: "grid gap", + "children:append": [filteredObjects, converter] + } + )), /* @__PURE__ */ createElement("button", { "on:click": close }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + } + // src/View/Components/option.tsx function Option(text, value, selectedOnCreate) { return /* @__PURE__ */ createElement("option", { value, "toggle:selected": selectedOnCreate }, text); @@ -2834,7 +2962,7 @@ { class: "task-grid", "children:append": [ - boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, TaskViewModelToEntry ] } @@ -2900,7 +3028,14 @@ "on:click": boardViewModel.createTask }, /* @__PURE__ */ createElement("span", { class: "icon" }, "add") - ))), /* @__PURE__ */ createElement("div", { class: "content main-content", "children:set": mainContent }), BoardSettingsModal(boardViewModel), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal })); + ))), /* @__PURE__ */ createElement("div", { class: "content main-content", "children:set": mainContent }), BoardSettingsModal(boardViewModel), SearchModal( + translations.chatPage.task.filterTasksHeadline, + boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, + TaskViewModelToEntry, + boardViewModel.getStringsFromTaskViewModel, + boardViewModel.isPresentingFilterModal + ), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal })); } // src/View/Components/boardEntry.tsx diff --git a/src/Model/Utility/utility.ts b/src/Model/Utility/utility.ts index 185d6c58..7d93be77 100644 --- a/src/Model/Utility/utility.ts +++ b/src/Model/Utility/utility.ts @@ -101,7 +101,7 @@ export function filterObjectsByWords( const matches: Set = new Set(); object_loop: for (const object of objects) { - const doesMatch: boolean = checkIfMatchesFilter( + const doesMatch: boolean = checkDoesObjectMatchSearch( query, getStringsOfObject, object @@ -112,7 +112,7 @@ export function filterObjectsByWords( return matches; } -export function checkIfMatchesFilter( +export function checkDoesObjectMatchSearch( query: string, getStringsOfObject: (object: T) => string[], object: T @@ -122,11 +122,17 @@ export function checkIfMatchesFilter( const stringsInObject: string[] = getStringsOfObject(object); const wordsInObject: string[] = []; for (const string of stringsInObject) { - const lowercaseWordsInString = string.toLocaleLowerCase().split(" "); + const lowercaseWordsInString = string + .toLocaleLowerCase() + .split(" ") + .filter((word) => word != ""); wordsInObject.push(...lowercaseWordsInString); } - const lowercaseWordsInQuery = query.toLowerCase().split(" "); + const lowercaseWordsInQuery = query + .toLowerCase() + .split(" ") + .filter((word) => word != ""); for (const queryWord of lowercaseWordsInQuery) { if (queryWord[0] == "-") { // exclusion diff --git a/src/View/ChatPages/boardKanbanPage.tsx b/src/View/ChatPages/boardKanbanPage.tsx index 3b94c420..9452f908 100644 --- a/src/View/ChatPages/boardKanbanPage.tsx +++ b/src/View/ChatPages/boardKanbanPage.tsx @@ -3,7 +3,7 @@ import * as React from "bloatless-react"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import { FilteredList } from "../Components/filteredList"; import { PropertyValueList } from "../Components/propertyList"; -import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Pages/taskPropertyBulkChangeViewModel"; +import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import { TaskViewModelToEntry } from "../Components/taskEntry"; import { translations } from "../translations"; @@ -34,7 +34,7 @@ function KanbanBoard(categoryName: string, boardViewModel: BoardViewModel) { return FilteredList( { category: categoryName }, (taskViewModel: TaskViewModel) => taskViewModel.task, - boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, (taskViewModels: React.ListState) => { const viewModel: TaskCategoryBulkChangeViewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); diff --git a/src/View/ChatPages/boardPage.tsx b/src/View/ChatPages/boardPage.tsx index 2d4feae4..ef2c82ae 100644 --- a/src/View/ChatPages/boardPage.tsx +++ b/src/View/ChatPages/boardPage.tsx @@ -8,13 +8,16 @@ import { BoardKanbanPage } from "./boardKanbanPage"; import { BoardSettingsModal } from "../Modals/boardSettingsModal"; import { BoardStatusGridPage } from "./boardStatusGridPage"; import { BoardViewToggleButton } from "../Components/boardViewToggleButton"; +import { SearchModal } from "../Modals/searchModal"; import { TaskSettingsModal } from "../Modals/taskSettingsModal"; +import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import { TaskViewModelToEntry } from "../Components/taskEntry"; import { translations } from "../translations"; export function BoardPage(boardViewModel: BoardViewModel) { boardViewModel.loadData(); + // main content const mainContent = React.createProxyState( [boardViewModel.selectedPage], () => { @@ -30,7 +33,7 @@ export function BoardPage(boardViewModel: BoardViewModel) {
@@ -40,6 +43,7 @@ export function BoardPage(boardViewModel: BoardViewModel) { } ); + // task modal const taskSettingsModal = React.createProxyState( [boardViewModel.selectedTaskViewModel], () => { @@ -110,6 +114,14 @@ export function BoardPage(boardViewModel: BoardViewModel) {
{BoardSettingsModal(boardViewModel)} + {SearchModal( + translations.chatPage.task.filterTasksHeadline, + boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, + TaskViewModelToEntry, + boardViewModel.getStringsFromTaskViewModel, + boardViewModel.isPresentingFilterModal + )}
); diff --git a/src/View/ChatPages/boardStatusGridPage.tsx b/src/View/ChatPages/boardStatusGridPage.tsx index 168281c7..99c20b14 100644 --- a/src/View/ChatPages/boardStatusGridPage.tsx +++ b/src/View/ChatPages/boardStatusGridPage.tsx @@ -3,7 +3,7 @@ import * as React from "bloatless-react"; import { TaskCategoryBulkChangeViewModel, TaskStatusBulkChangeViewModel, -} from "../../ViewModel/Pages/taskPropertyBulkChangeViewModel"; +} from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import { FilteredList } from "../Components/filteredList"; @@ -46,7 +46,7 @@ export function BoardStatusGridPage(boardViewModel: BoardViewModel) { {PropertyValueList( "category", (taskViewModel: TaskViewModel) => taskViewModel.task, - boardViewModel.taskViewModels, + boardViewModel.filteredTaskViewModels, (categories: React.ListState) => { return (
( + headline: string, + allObjects: React.ListState | React.MapState, + filteredObjects: React.ListState, + converter: React.StateItemConverter, + getStringsOfObject: (object: T) => string[], + isOpen: React.State +) { + const viewModel: SearchViewModel = new SearchViewModel( + allObjects, + filteredObjects, + getStringsOfObject + ); + + function close() { + isOpen.value = false; + } + + return ( +
+
+
+

{headline}

+
+ + +
+ +
+ +
+
+ +
+
+ ); +} diff --git a/src/View/translations.ts b/src/View/translations.ts index 285f72df..9d8833eb 100644 --- a/src/View/translations.ts +++ b/src/View/translations.ts @@ -1,6 +1,7 @@ const englishTranslations = { general: { deleteItemButtonAudioLabel: "delete item", + searchButtonAudioLabel: "search", abortButton: "Abort", closeButton: "Close", @@ -10,6 +11,7 @@ const englishTranslations = { setButton: "Set", fileVersionLabel: "Version", + searchLabel: "Search", }, regional: { @@ -177,6 +179,10 @@ const englishTranslations = { /// + filterTasksHeadline: "Filter Tasks", + + /// + renameCategoryInputPlaceholder: "Rename category," }, }, diff --git a/src/ViewModel/Pages/boardViewModel.ts b/src/ViewModel/Pages/boardViewModel.ts index 6fc644d7..f0a81860 100644 --- a/src/ViewModel/Pages/boardViewModel.ts +++ b/src/ViewModel/Pages/boardViewModel.ts @@ -30,8 +30,6 @@ export default class BoardViewModel { index: React.State = new React.State(0); - taskViewModels: React.MapState = new React.MapState(); - selectedPage: React.State = new React.State( BoardPageType.List ); @@ -42,6 +40,10 @@ export default class BoardViewModel { isPresentingSettingsModal: React.State = new React.State(false); isPresentingFilterModal: React.State = new React.State(false); + taskViewModels: React.MapState = new React.MapState(); + filteredTaskViewModels: React.ListState = + new React.ListState(); + // paths getBasePath = (): string[] => { return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)]; @@ -162,6 +164,19 @@ export default class BoardViewModel { } }; + // filter + getStringsFromTaskViewModel = (taskViewModel: TaskViewModel): string[] => { + return [ + taskViewModel.task.name, + taskViewModel.task.description ?? "", + taskViewModel.task.category ?? "", + taskViewModel.task.status ?? "", + taskViewModel.task.priority ?? "", + taskViewModel.task.date ?? "", + taskViewModel.task.time ?? "", + ]; + }; + // load loadListRelevantData = (): void => { this.name.value = this.boardInfo.name; @@ -174,7 +189,7 @@ export default class BoardViewModel { ); for (const taskId of taskIds) { if (this.taskViewModels.value.has(taskId)) return; - + const taskFileContent: TaskFileContent | null = this.boardModel.getLatestTaskFileContent(taskId); if (taskFileContent == null) continue; diff --git a/src/ViewModel/Utility/searchViewModel.ts b/src/ViewModel/Utility/searchViewModel.ts new file mode 100644 index 00000000..c9d21760 --- /dev/null +++ b/src/ViewModel/Utility/searchViewModel.ts @@ -0,0 +1,68 @@ +import * as React from "bloatless-react"; + +import { checkDoesObjectMatchSearch } from "../../Model/Utility/utility"; + +export default class SearchViewModel { + // data + allObjects: React.ListState | React.MapState; + getStringsOfObject: (object: T) => string[]; + + // state + appliedQuery: React.State = new React.State(""); + searchInput: React.State = new React.State(""); + matchingObjects: React.ListState; + + // guards + cannotApplySearch: React.State = React.createProxyState( + [this.searchInput, this.appliedQuery], + () => this.searchInput.value == this.appliedQuery.value + ); + + // methods + applySearch = (): void => { + this.appliedQuery.value = this.searchInput.value; + console.log("applying search"); + + this.matchingObjects.clear(); + for (const object of this.allObjects.value.values()) { + const doesMatch: boolean = this.checkDoesMatchSearch(object); + if (doesMatch == false) continue; + + this.matchingObjects.add(object); + } + }; + + // init + constructor( + allObjects: React.ListState | React.MapState, + matchingObjects: React.ListState, + getStringsOfObject: (object: T) => string[] + ) { + this.allObjects = allObjects; + this.matchingObjects = matchingObjects; + this.getStringsOfObject = getStringsOfObject; + + // handle new objects + this.allObjects.handleAddition((newObject: T) => { + const doesMatch: boolean = 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: T): boolean => { + return checkDoesObjectMatchSearch( + this.appliedQuery.value, + this.getStringsOfObject, + object + ); + }; +} diff --git a/src/ViewModel/Pages/taskPropertyBulkChangeViewModel.ts b/src/ViewModel/Utility/taskPropertyBulkChangeViewModel.ts similarity index 97% rename from src/ViewModel/Pages/taskPropertyBulkChangeViewModel.ts rename to src/ViewModel/Utility/taskPropertyBulkChangeViewModel.ts index 71dbf2d5..5e84fb88 100644 --- a/src/ViewModel/Pages/taskPropertyBulkChangeViewModel.ts +++ b/src/ViewModel/Utility/taskPropertyBulkChangeViewModel.ts @@ -1,6 +1,6 @@ import * as React from "bloatless-react"; -import TaskViewModel from "./taskViewModel"; +import TaskViewModel from "../Pages/taskViewModel"; export default class TaskPropertyBulkChangeViewModel { taskViewModels: React.ListState;