implement task filter

This commit is contained in:
marlon-erler
2024-07-25 15:31:27 +02:00
parent 8b47bb74e8
commit be6f609ea2
10 changed files with 321 additions and 20 deletions
+142 -7
View File
@@ -353,6 +353,29 @@
} }
return [...values.values()].sort(localeCompare); 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 { var HandlerManager = class {
handlers = /* @__PURE__ */ new Set(); handlers = /* @__PURE__ */ new Set();
// manage // manage
@@ -1613,7 +1636,6 @@
name = new State(""); name = new State("");
color = new State("standard" /* Standard */); color = new State("standard" /* Standard */);
index = new State(0); index = new State(0);
taskViewModels = new MapState();
selectedPage = new State( selectedPage = new State(
"list" /* List */ "list" /* List */
); );
@@ -1621,6 +1643,8 @@
isSelected; isSelected;
isPresentingSettingsModal = new State(false); isPresentingSettingsModal = new State(false);
isPresentingFilterModal = new State(false); isPresentingFilterModal = new State(false);
taskViewModels = new MapState();
filteredTaskViewModels = new ListState();
// paths // paths
getBasePath = () => { getBasePath = () => {
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)]; return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)];
@@ -1718,6 +1742,18 @@
boardViewModel.updateIndex(); 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 // load
loadListRelevantData = () => { loadListRelevantData = () => {
this.name.value = this.boardInfo.name; this.name.value = this.boardInfo.name;
@@ -2064,12 +2100,14 @@
var englishTranslations = { var englishTranslations = {
general: { general: {
deleteItemButtonAudioLabel: "delete item", deleteItemButtonAudioLabel: "delete item",
searchButtonAudioLabel: "search",
abortButton: "Abort", abortButton: "Abort",
closeButton: "Close", closeButton: "Close",
confirmButton: "Confirm", confirmButton: "Confirm",
saveButton: "Save", saveButton: "Save",
setButton: "Set", setButton: "Set",
fileVersionLabel: "Version" fileVersionLabel: "Version",
searchLabel: "Search"
}, },
regional: { regional: {
weekdays: { weekdays: {
@@ -2187,6 +2225,8 @@
taskTimeLabel: "Time", taskTimeLabel: "Time",
deleteTaskButton: "Delete task", deleteTaskButton: "Delete task",
/// ///
filterTasksHeadline: "Filter Tasks",
///
renameCategoryInputPlaceholder: "Rename category," renameCategoryInputPlaceholder: "Rename category,"
} }
} }
@@ -2481,7 +2521,7 @@
return viewBuilder(propertyValues); return viewBuilder(propertyValues);
} }
// src/ViewModel/Pages/taskPropertyBulkChangeViewModel.ts // src/ViewModel/Utility/taskPropertyBulkChangeViewModel.ts
var TaskPropertyBulkChangeViewModel = class { var TaskPropertyBulkChangeViewModel = class {
taskViewModels; taskViewModels;
// state // state
@@ -2598,7 +2638,7 @@
return FilteredList( return FilteredList(
{ category: categoryName }, { category: categoryName },
(taskViewModel) => taskViewModel.task, (taskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels, boardViewModel.filteredTaskViewModels,
(taskViewModels) => { (taskViewModels) => {
const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); 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( return /* @__PURE__ */ createElement("div", { class: "flex-column flex-no" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
@@ -2669,7 +2709,7 @@
), PropertyValueList( ), PropertyValueList(
"category", "category",
(taskViewModel) => taskViewModel.task, (taskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels, boardViewModel.filteredTaskViewModels,
(categories) => { (categories) => {
return /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement(
"div", "div",
@@ -2780,6 +2820,94 @@
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
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 // src/View/Components/option.tsx
function Option(text, value, selectedOnCreate) { function Option(text, value, selectedOnCreate) {
return /* @__PURE__ */ createElement("option", { value, "toggle:selected": selectedOnCreate }, text); return /* @__PURE__ */ createElement("option", { value, "toggle:selected": selectedOnCreate }, text);
@@ -2834,7 +2962,7 @@
{ {
class: "task-grid", class: "task-grid",
"children:append": [ "children:append": [
boardViewModel.taskViewModels, boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry TaskViewModelToEntry
] ]
} }
@@ -2900,7 +3028,14 @@
"on:click": boardViewModel.createTask "on:click": boardViewModel.createTask
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "add") /* @__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 // src/View/Components/boardEntry.tsx
+10 -4
View File
@@ -101,7 +101,7 @@ export function filterObjectsByWords<T>(
const matches: Set<T> = new Set(); const matches: Set<T> = new Set();
object_loop: for (const object of objects) { object_loop: for (const object of objects) {
const doesMatch: boolean = checkIfMatchesFilter( const doesMatch: boolean = checkDoesObjectMatchSearch(
query, query,
getStringsOfObject, getStringsOfObject,
object object
@@ -112,7 +112,7 @@ export function filterObjectsByWords<T>(
return matches; return matches;
} }
export function checkIfMatchesFilter<T>( export function checkDoesObjectMatchSearch<T>(
query: string, query: string,
getStringsOfObject: (object: T) => string[], getStringsOfObject: (object: T) => string[],
object: T object: T
@@ -122,11 +122,17 @@ export function checkIfMatchesFilter<T>(
const stringsInObject: string[] = getStringsOfObject(object); const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = []; const wordsInObject: string[] = [];
for (const string of stringsInObject) { for (const string of stringsInObject) {
const lowercaseWordsInString = string.toLocaleLowerCase().split(" "); const lowercaseWordsInString = string
.toLocaleLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString); wordsInObject.push(...lowercaseWordsInString);
} }
const lowercaseWordsInQuery = query.toLowerCase().split(" "); const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) { for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") { if (queryWord[0] == "-") {
// exclusion // exclusion
+2 -2
View File
@@ -3,7 +3,7 @@ import * as React from "bloatless-react";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { FilteredList } from "../Components/filteredList"; import { FilteredList } from "../Components/filteredList";
import { PropertyValueList } from "../Components/propertyList"; import { PropertyValueList } from "../Components/propertyList";
import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Pages/taskPropertyBulkChangeViewModel"; import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations"; import { translations } from "../translations";
@@ -34,7 +34,7 @@ function KanbanBoard(categoryName: string, boardViewModel: BoardViewModel) {
return FilteredList( return FilteredList(
{ category: categoryName }, { category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task, (taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels, boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => { (taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel = const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
+13 -1
View File
@@ -8,13 +8,16 @@ import { BoardKanbanPage } from "./boardKanbanPage";
import { BoardSettingsModal } from "../Modals/boardSettingsModal"; import { BoardSettingsModal } from "../Modals/boardSettingsModal";
import { BoardStatusGridPage } from "./boardStatusGridPage"; import { BoardStatusGridPage } from "./boardStatusGridPage";
import { BoardViewToggleButton } from "../Components/boardViewToggleButton"; import { BoardViewToggleButton } from "../Components/boardViewToggleButton";
import { SearchModal } from "../Modals/searchModal";
import { TaskSettingsModal } from "../Modals/taskSettingsModal"; import { TaskSettingsModal } from "../Modals/taskSettingsModal";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import { TaskViewModelToEntry } from "../Components/taskEntry"; import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations"; import { translations } from "../translations";
export function BoardPage(boardViewModel: BoardViewModel) { export function BoardPage(boardViewModel: BoardViewModel) {
boardViewModel.loadData(); boardViewModel.loadData();
// main content
const mainContent = React.createProxyState( const mainContent = React.createProxyState(
[boardViewModel.selectedPage], [boardViewModel.selectedPage],
() => { () => {
@@ -30,7 +33,7 @@ export function BoardPage(boardViewModel: BoardViewModel) {
<div <div
class="task-grid" class="task-grid"
children:append={[ children:append={[
boardViewModel.taskViewModels, boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry, TaskViewModelToEntry,
]} ]}
></div> ></div>
@@ -40,6 +43,7 @@ export function BoardPage(boardViewModel: BoardViewModel) {
} }
); );
// task modal
const taskSettingsModal = React.createProxyState( const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel], [boardViewModel.selectedTaskViewModel],
() => { () => {
@@ -110,6 +114,14 @@ export function BoardPage(boardViewModel: BoardViewModel) {
<div class="content main-content" children:set={mainContent}></div> <div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)} {BoardSettingsModal(boardViewModel)}
{SearchModal(
translations.chatPage.task.filterTasksHeadline,
boardViewModel.taskViewModels,
boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry,
boardViewModel.getStringsFromTaskViewModel,
boardViewModel.isPresentingFilterModal
)}
<div children:set={taskSettingsModal}></div> <div children:set={taskSettingsModal}></div>
</div> </div>
); );
+2 -2
View File
@@ -3,7 +3,7 @@ import * as React from "bloatless-react";
import { import {
TaskCategoryBulkChangeViewModel, TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel, TaskStatusBulkChangeViewModel,
} from "../../ViewModel/Pages/taskPropertyBulkChangeViewModel"; } from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { FilteredList } from "../Components/filteredList"; import { FilteredList } from "../Components/filteredList";
@@ -46,7 +46,7 @@ export function BoardStatusGridPage(boardViewModel: BoardViewModel) {
{PropertyValueList( {PropertyValueList(
"category", "category",
(taskViewModel: TaskViewModel) => taskViewModel.task, (taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels, boardViewModel.filteredTaskViewModels,
(categories: React.ListState<string>) => { (categories: React.ListState<string>) => {
return ( return (
<div <div
+59
View File
@@ -0,0 +1,59 @@
import * as React from "bloatless-react";
import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
import { translations } from "../translations";
export function SearchModal<T>(
headline: string,
allObjects: React.ListState<T> | React.MapState<T>,
filteredObjects: React.ListState<T>,
converter: React.StateItemConverter<T>,
getStringsOfObject: (object: T) => string[],
isOpen: React.State<boolean>
) {
const viewModel: SearchViewModel<T> = new SearchViewModel(
allObjects,
filteredObjects,
getStringsOfObject
);
function close() {
isOpen.value = false;
}
return (
<div class="modal" toggle:open={isOpen}>
<div style="max-width: 64rem">
<main>
<h2>{headline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.general.searchLabel}
bind:value={viewModel.searchInput}
on:enter={viewModel.applySearch}
></input>
<button
class="primary"
aria-label={translations.general.searchButtonAudioLabel}
on:click={viewModel.applySearch}
toggle:disabled={viewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
<hr></hr>
<div
class="grid gap"
children:append={[filteredObjects, converter]}
></div>
</main>
<button on:click={close}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+6
View File
@@ -1,6 +1,7 @@
const englishTranslations = { const englishTranslations = {
general: { general: {
deleteItemButtonAudioLabel: "delete item", deleteItemButtonAudioLabel: "delete item",
searchButtonAudioLabel: "search",
abortButton: "Abort", abortButton: "Abort",
closeButton: "Close", closeButton: "Close",
@@ -10,6 +11,7 @@ const englishTranslations = {
setButton: "Set", setButton: "Set",
fileVersionLabel: "Version", fileVersionLabel: "Version",
searchLabel: "Search",
}, },
regional: { regional: {
@@ -177,6 +179,10 @@ const englishTranslations = {
/// ///
filterTasksHeadline: "Filter Tasks",
///
renameCategoryInputPlaceholder: "Rename category," renameCategoryInputPlaceholder: "Rename category,"
}, },
}, },
+17 -2
View File
@@ -30,8 +30,6 @@ export default class BoardViewModel {
index: React.State<number> = new React.State(0); index: React.State<number> = new React.State(0);
taskViewModels: React.MapState<TaskViewModel> = new React.MapState();
selectedPage: React.State<BoardPageType> = new React.State<BoardPageType>( selectedPage: React.State<BoardPageType> = new React.State<BoardPageType>(
BoardPageType.List BoardPageType.List
); );
@@ -42,6 +40,10 @@ export default class BoardViewModel {
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);
taskViewModels: React.MapState<TaskViewModel> = new React.MapState();
filteredTaskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
// paths // paths
getBasePath = (): string[] => { getBasePath = (): string[] => {
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)]; 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 // load
loadListRelevantData = (): void => { loadListRelevantData = (): void => {
this.name.value = this.boardInfo.name; this.name.value = this.boardInfo.name;
+68
View File
@@ -0,0 +1,68 @@
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: React.State<string> = new React.State("");
searchInput: React.State<string> = new React.State("");
matchingObjects: React.ListState<T>;
// guards
cannotApplySearch: React.State<boolean> = 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<T> | React.MapState<T>,
matchingObjects: React.ListState<T>,
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
);
};
}
@@ -1,6 +1,6 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import TaskViewModel from "./taskViewModel"; import TaskViewModel from "../Pages/taskViewModel";
export default class TaskPropertyBulkChangeViewModel { export default class TaskPropertyBulkChangeViewModel {
taskViewModels: React.ListState<TaskViewModel>; taskViewModels: React.ListState<TaskViewModel>;