add filter modal, implement search

This commit is contained in:
marlon-erler
2024-07-13 17:13:59 +02:00
parent 61ddb4e32c
commit af67144a6c
7 changed files with 237 additions and 40 deletions
+100 -20
View File
@@ -429,6 +429,8 @@
// general // general
set: "Set", set: "Set",
save: "Save", save: "Save",
search: "Search",
reset: "Reset",
back: "Back", back: "Back",
undoChanges: "Undo changes", undoChanges: "Undo changes",
close: "Close", close: "Close",
@@ -501,6 +503,10 @@
showObjects: "show objects", showObjects: "show objects",
createObject: "create object", createObject: "create object",
untitledObject: "Untitled Object", untitledObject: "Untitled Object",
filterObjects: "Filter Objects",
searchByTitle: "Search by title",
searchByTitlePlaceholder: "My Object",
searchTitleText: (term, resultCount) => term == "" ? `Objects in total: ${resultCount}` : `Objects containing "${term}": ${resultCount}`,
viewAll: "All", viewAll: "All",
viewNotes: "Notes", viewNotes: "Notes",
viewKanban: "Kanban", viewKanban: "Kanban",
@@ -528,6 +534,8 @@
// general // general
set: "Guardar", set: "Guardar",
save: "Guardar", save: "Guardar",
search: "Buscar",
reset: "Restablecer",
back: "Atr\xE1s", back: "Atr\xE1s",
undoChanges: "Deshacer", undoChanges: "Deshacer",
close: "Cerrar", close: "Cerrar",
@@ -600,6 +608,7 @@
showObjects: "mostrar objetos", showObjects: "mostrar objetos",
createObject: "a\xF1adir nuevo objeto", createObject: "a\xF1adir nuevo objeto",
untitledObject: "Sin T\xEDtulo", untitledObject: "Sin T\xEDtulo",
filterObjects: "filtrar objetos",
viewAll: "Todos", viewAll: "Todos",
viewNotes: "Notas", viewNotes: "Notas",
viewKanban: "Kanban", viewKanban: "Kanban",
@@ -625,6 +634,8 @@
// general // general
set: "OK", set: "OK",
save: "Sichern", save: "Sichern",
search: "Suchen",
reset: "Zur\xFCcksetzen",
back: "Zur\xFCck", back: "Zur\xFCck",
undoChanges: "\xC4nderungen verwerfen", undoChanges: "\xC4nderungen verwerfen",
close: "Schlie\xDFen", close: "Schlie\xDFen",
@@ -697,6 +708,7 @@
showObjects: "Objekte anzeigen", showObjects: "Objekte anzeigen",
createObject: "Neues Objekt erstellen", createObject: "Neues Objekt erstellen",
untitledObject: "Unbenannt", untitledObject: "Unbenannt",
filterObjects: "Objekte filtern",
viewAll: "Alle", viewAll: "Alle",
viewNotes: "Notizen", viewNotes: "Notizen",
viewKanban: "Kanban", viewKanban: "Kanban",
@@ -1579,12 +1591,17 @@
} }
// src/Views/Objects/allObjectsView.tsx // src/Views/Objects/allObjectsView.tsx
function AllObjectsView(chat, selectedObject, isShowingObjectModal) { function AllObjectsView(chat, messageObjects, selectedObject, isShowingObjectModal) {
const objectCount = createProxyState( const objectCount = createProxyState(
[chat.objects], [chat.objects],
() => chat.objects.value.size () => chat.objects.value.size
); );
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100 scroll-v padding-top" }, /* @__PURE__ */ createElement("b", { class: "secondary padding" }, translation.objectsInTotal, " ", /* @__PURE__ */ createElement("span", { "subscribe:innerText": objectCount })), ObjectGridView(chat, chat.objects, selectedObject, isShowingObjectModal)); return /* @__PURE__ */ createElement("div", { class: "width-100 height-100 scroll-v padding-top" }, /* @__PURE__ */ createElement("b", { class: "secondary padding" }, translation.objectsInTotal, " ", /* @__PURE__ */ createElement("span", { "subscribe:innerText": objectCount })), ObjectGridView(
chat,
messageObjects,
selectedObject,
isShowingObjectModal
));
} }
// src/Views/renameView.tsx // src/Views/renameView.tsx
@@ -1606,13 +1623,13 @@
} }
// src/Views/Objects/kanbanView.tsx // src/Views/Objects/kanbanView.tsx
function KanbanView(chat, selectedObject, isShowingObjectModal) { function KanbanView(chat, messageObjects, selectedObject, isShowingObjectModal) {
const boards = new MapState(); const boards = new MapState();
const sortedBoardNames = createProxyState( const sortedBoardNames = createProxyState(
[boards], [boards],
() => [...boards.value.values()].map((board) => board.category).sort((a, b) => a.localeCompare(b)) () => [...boards.value.values()].map((board) => board.category).sort((a, b) => a.localeCompare(b))
); );
chat.objects.handleAddition((messageObject) => { messageObjects.handleAddition((messageObject) => {
const latest = chat.getMostRecentContent(messageObject); const latest = chat.getMostRecentContent(messageObject);
if (!latest) return; if (!latest) return;
if (!latest.categoryName) return; if (!latest.categoryName) return;
@@ -1633,7 +1650,7 @@
messageObject messageObject
}; };
boards.value.get(categoryName)?.items.add(boardItem); boards.value.get(categoryName)?.items.add(boardItem);
chat.objects.handleRemoval(messageObject, () => { messageObjects.handleRemoval(messageObject, () => {
boards.value.get(categoryName)?.items.remove(boardItem); boards.value.get(categoryName)?.items.remove(boardItem);
}); });
}); });
@@ -1711,14 +1728,14 @@
} }
// src/Views/Objects/noteObjectsView.tsx // src/Views/Objects/noteObjectsView.tsx
function NoteObjectsView(chat, selectedObject, isShowingObjectModal) { function NoteObjectsView(chat, messageObjects, selectedObject, isShowingObjectModal) {
const notes = new ListState(); const notes = new ListState();
chat.objects.handleAddition((messageObject) => { messageObjects.handleAddition((messageObject) => {
const latest = chat.getMostRecentContent(messageObject); const latest = chat.getMostRecentContent(messageObject);
if (!latest) return; if (!latest) return;
if (!latest.noteContent) return; if (!latest.noteContent) return;
notes.add(messageObject); notes.add(messageObject);
chat.objects.handleRemoval( messageObjects.handleRemoval(
messageObject, messageObject,
() => notes.remove(messageObject) () => notes.remove(messageObject)
); );
@@ -1862,7 +1879,7 @@
} }
// src/Views/Objects/statusView.tsx // src/Views/Objects/statusView.tsx
function StatusView(chat, selectedObject, isShowingObjectModal) { function StatusView(chat, messageObjects, selectedObject, isShowingObjectModal) {
const rows = new MapState(); const rows = new MapState();
const statuses = new MapState(); const statuses = new MapState();
const sortedStatuses = createProxyState( const sortedStatuses = createProxyState(
@@ -1873,10 +1890,10 @@
[rows], [rows],
() => [...rows.value.values()].map((row) => row.category).sort((a, b) => a.localeCompare(b)) () => [...rows.value.values()].map((row) => row.category).sort((a, b) => a.localeCompare(b))
); );
chat.objects.subscribe(() => { messageObjects.subscribe(() => {
rows.clear(); rows.clear();
statuses.clear(); statuses.clear();
chat.objects.value.forEach((messageObject) => { messageObjects.value.forEach((messageObject) => {
const latest = chat.getMostRecentContent(messageObject); const latest = chat.getMostRecentContent(messageObject);
if (!latest.categoryName || !latest.status) return; if (!latest.categoryName || !latest.status) return;
const category = latest.categoryName; const category = latest.categoryName;
@@ -2037,7 +2054,48 @@
}; };
function ObjectPane(chat) { function ObjectPane(chat) {
const isShowingObjectModal = new State(false); const isShowingObjectModal = new State(false);
const isShowingFilterModel = new State(false);
const selectedObject = new State(void 0); const selectedObject = new State(void 0);
function createObject() {
const newObject = chat.createObjectFromTitle("");
chat.addObjectAndSend(newObject);
selectedObject.value = newObject;
isShowingObjectModal.value = true;
}
function showFilters() {
isShowingFilterModel.value = true;
}
function closeFilters() {
isShowingFilterModel.value = false;
}
const filterInput = new State("");
const appliedFilter = new State("");
const resultCount = new State(0);
const resultText = createProxyState(
[appliedFilter, resultCount],
() => translation.searchTitleText(appliedFilter.value, resultCount.value)
);
const messageObjects = new MapState();
const cannotReset = createProxyState(
[appliedFilter],
() => appliedFilter.value == ""
);
function resetFilter() {
filterInput.value = "";
applyFilter();
}
function applyFilter() {
appliedFilter.value = filterInput.value;
}
appliedFilter.subscribe((filterTerm) => {
const allObjects = [...chat.objects.value.entries()];
const matchingObjects = allObjects.filter(
(entry) => entry[1].title.indexOf(filterTerm) != -1
);
resultCount.value = matchingObjects.length;
messageObjects.clear();
matchingObjects.forEach((entry) => messageObjects.set(...entry));
});
const objectModal = createProxyState( const objectModal = createProxyState(
[chat.objects, selectedObject], [chat.objects, selectedObject],
() => { () => {
@@ -2062,14 +2120,13 @@
return AllObjectsView; return AllObjectsView;
} }
} }
return getViewFunction()(chat, selectedObject, isShowingObjectModal); return getViewFunction()(
chat,
messageObjects,
selectedObject,
isShowingObjectModal
);
}); });
function createObject() {
const newObject = chat.createObjectFromTitle("");
chat.addObjectAndSend(newObject);
selectedObject.value = newObject;
isShowingObjectModal.value = true;
}
return /* @__PURE__ */ createElement("div", { class: "chat-object-view flex-column scroll-no padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-row align-center border-bottom" }, /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "chat-object-view flex-column scroll-no padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-row align-center border-bottom" }, /* @__PURE__ */ createElement(
"button", "button",
{ {
@@ -2080,13 +2137,36 @@
/* @__PURE__ */ createElement("span", { class: "icon" }, "add") /* @__PURE__ */ createElement("span", { class: "icon" }, "add")
), /* @__PURE__ */ createElement("div", { class: "padding-sm flex flex-row gap justify-center scroll-h width-100" }, ...Object.keys(viewTypes).map( ), /* @__PURE__ */ createElement("div", { class: "padding-sm flex flex-row gap justify-center scroll-h width-100" }, ...Object.keys(viewTypes).map(
(key) => ViewTypeToggle(key, chat.viewType) (key) => ViewTypeToggle(key, chat.viewType)
)), /* @__PURE__ */ createElement("button", { class: "height-100", disabled: true }, /* @__PURE__ */ createElement("span", { class: "icon" }, "visibility"))), /* @__PURE__ */ createElement( )), /* @__PURE__ */ createElement(
"button",
{
class: "height-100",
"aria-label": translation.filterObjects,
"on:click": showFilters
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "filter_list")
)), /* @__PURE__ */ createElement(
"div", "div",
{ {
class: "object-content width-100 height-100 flex scroll-no", class: "object-content width-100 height-100 flex scroll-no",
"children:set": mainView "children:set": mainView
} }
), /* @__PURE__ */ createElement("div", { "children:set": objectModal })); ), /* @__PURE__ */ createElement("div", { "children:set": objectModal }), /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isShowingFilterModel }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translation.filterObjects), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "search"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.searchByTitle), /* @__PURE__ */ createElement(
"input",
{
"bind:value": filterInput,
"on:enter": applyFilter,
placeholder: translation.searchByTitlePlaceholder
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"button",
{
class: "width-50 flex",
"on:click": resetFilter,
"toggle:disabled": cannotReset
},
translation.reset
), /* @__PURE__ */ createElement("button", { class: "width-50 flex primary", "on:click": applyFilter }, translation.search, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("span", { class: "secondary", "subscribe:innerText": resultText })), /* @__PURE__ */ createElement("button", { "on:click": closeFilters }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))));
} }
function ViewTypeToggle(key, selection) { function ViewTypeToggle(key, selection) {
const [label, icon] = viewTypes[key]; const [label, icon] = viewTypes[key];
+7 -1
View File
@@ -7,6 +7,7 @@ import { translation } from "../../translations";
export function AllObjectsView( export function AllObjectsView(
chat: Chat, chat: Chat,
messageObjects: React.MapState<MessageObject>,
selectedObject: React.State<MessageObject | undefined>, selectedObject: React.State<MessageObject | undefined>,
isShowingObjectModal: React.State<boolean> isShowingObjectModal: React.State<boolean>
) { ) {
@@ -21,7 +22,12 @@ export function AllObjectsView(
{translation.objectsInTotal}{" "} {translation.objectsInTotal}{" "}
<span subscribe:innerText={objectCount}></span> <span subscribe:innerText={objectCount}></span>
</b> </b>
{ObjectGridView(chat, chat.objects, selectedObject, isShowingObjectModal)} {ObjectGridView(
chat,
messageObjects,
selectedObject,
isShowingObjectModal
)}
</div> </div>
); );
} }
+3 -3
View File
@@ -3,7 +3,6 @@ import * as React from "bloatless-react";
import { Chat, MessageObject } from "../../Model/chatModel"; import { Chat, MessageObject } from "../../Model/chatModel";
import { ObjectEntryView } from "./objectEntryView"; import { ObjectEntryView } from "./objectEntryView";
import { PlaceholderView } from "./placeholderView";
import { RenameView } from "../renameView"; import { RenameView } from "../renameView";
interface KanbanBoard { interface KanbanBoard {
@@ -18,6 +17,7 @@ interface KanbanBoardItem {
export function KanbanView( export function KanbanView(
chat: Chat, chat: Chat,
messageObjects: React.MapState<MessageObject>,
selectedObject: React.State<MessageObject | undefined>, selectedObject: React.State<MessageObject | undefined>,
isShowingObjectModal: React.State<boolean> isShowingObjectModal: React.State<boolean>
) { ) {
@@ -28,7 +28,7 @@ export function KanbanView(
.sort((a, b) => a.localeCompare(b)) .sort((a, b) => a.localeCompare(b))
); );
chat.objects.handleAddition((messageObject) => { messageObjects.handleAddition((messageObject) => {
const latest = chat.getMostRecentContent(messageObject); const latest = chat.getMostRecentContent(messageObject);
if (!latest) return; if (!latest) return;
if (!latest.categoryName) return; if (!latest.categoryName) return;
@@ -53,7 +53,7 @@ export function KanbanView(
}; };
boards.value.get(categoryName)?.items.add(boardItem); boards.value.get(categoryName)?.items.add(boardItem);
chat.objects.handleRemoval(messageObject, () => { messageObjects.handleRemoval(messageObject, () => {
boards.value.get(categoryName)?.items.remove(boardItem); boards.value.get(categoryName)?.items.remove(boardItem);
}); });
}); });
+3 -2
View File
@@ -6,17 +6,18 @@ import { ObjectGridView } from "./objectGridView";
export function NoteObjectsView( export function NoteObjectsView(
chat: Chat, chat: Chat,
messageObjects: React.MapState<MessageObject>,
selectedObject: React.State<MessageObject | undefined>, selectedObject: React.State<MessageObject | undefined>,
isShowingObjectModal: React.State<boolean> isShowingObjectModal: React.State<boolean>
) { ) {
const notes = new React.ListState<MessageObject>(); const notes = new React.ListState<MessageObject>();
chat.objects.handleAddition((messageObject) => { messageObjects.handleAddition((messageObject) => {
const latest = chat.getMostRecentContent(messageObject); const latest = chat.getMostRecentContent(messageObject);
if (!latest) return; if (!latest) return;
if (!latest.noteContent) return; if (!latest.noteContent) return;
notes.add(messageObject); notes.add(messageObject);
chat.objects.handleRemoval(messageObject, () => messageObjects.handleRemoval(messageObject, () =>
notes.remove(messageObject) notes.remove(messageObject)
); );
}); });
+103 -12
View File
@@ -20,7 +20,58 @@ export const viewTypes = {
export function ObjectPane(chat: Chat) { export function ObjectPane(chat: Chat) {
// selection and modal // selection and modal
const isShowingObjectModal = new React.State(false); const isShowingObjectModal = new React.State(false);
const isShowingFilterModel = new React.State(false);
const selectedObject = new React.State<MessageObject | undefined>(undefined); const selectedObject = new React.State<MessageObject | undefined>(undefined);
// methods
function createObject() {
const newObject = chat.createObjectFromTitle("");
chat.addObjectAndSend(newObject);
selectedObject.value = newObject;
isShowingObjectModal.value = true;
}
function showFilters() {
isShowingFilterModel.value = true;
}
function closeFilters() {
isShowingFilterModel.value = false;
}
// filter
const filterInput = new React.State("");
const appliedFilter = new React.State("");
const resultCount = new React.State(0);
const resultText = React.createProxyState([appliedFilter, resultCount], () =>
translation.searchTitleText(appliedFilter.value, resultCount.value)
);
const messageObjects = new React.MapState<MessageObject>();
const cannotReset = React.createProxyState(
[appliedFilter],
() => appliedFilter.value == ""
);
function resetFilter() {
filterInput.value = "";
applyFilter();
}
function applyFilter() {
appliedFilter.value = filterInput.value;
}
appliedFilter.subscribe((filterTerm) => {
const allObjects = [...chat.objects.value.entries()];
const matchingObjects = allObjects.filter(
(entry) => entry[1].title.indexOf(filterTerm) != -1
);
resultCount.value = matchingObjects.length;
messageObjects.clear();
matchingObjects.forEach((entry) => messageObjects.set(...entry));
});
// view
const objectModal = React.createProxyState( const objectModal = React.createProxyState(
[chat.objects, selectedObject], [chat.objects, selectedObject],
() => { () => {
@@ -34,7 +85,6 @@ export function ObjectPane(chat: Chat) {
} }
); );
// view type
const mainView = React.createProxyState([chat.viewType], () => { const mainView = React.createProxyState([chat.viewType], () => {
function getViewFunction() { function getViewFunction() {
switch (chat.viewType.value) { switch (chat.viewType.value) {
@@ -49,17 +99,14 @@ export function ObjectPane(chat: Chat) {
} }
} }
return getViewFunction()(chat, selectedObject, isShowingObjectModal); return getViewFunction()(
chat,
messageObjects,
selectedObject,
isShowingObjectModal
);
}); });
// methods
function createObject() {
const newObject = chat.createObjectFromTitle("");
chat.addObjectAndSend(newObject);
selectedObject.value = newObject;
isShowingObjectModal.value = true;
}
return ( return (
<div class="chat-object-view flex-column scroll-no padding-0"> <div class="chat-object-view flex-column scroll-no padding-0">
<div class="flex-row align-center border-bottom"> <div class="flex-row align-center border-bottom">
@@ -77,8 +124,12 @@ export function ObjectPane(chat: Chat) {
)} )}
</div> </div>
<button class="height-100" disabled> <button
<span class="icon">visibility</span> class="height-100"
aria-label={translation.filterObjects}
on:click={showFilters}
>
<span class="icon">filter_list</span>
</button> </button>
</div> </div>
@@ -88,6 +139,46 @@ export function ObjectPane(chat: Chat) {
></div> ></div>
<div children:set={objectModal}></div> <div children:set={objectModal}></div>
<div class="modal" toggle:open={isShowingFilterModel}>
<div>
<main>
<h2>{translation.filterObjects}</h2>
<label class="tile">
<span class="icon">search</span>
<div>
<span>{translation.searchByTitle}</span>
<input
bind:value={filterInput}
on:enter={applyFilter}
placeholder={translation.searchByTitlePlaceholder}
></input>
</div>
</label>
<div class="flex-row width-input">
<button
class="width-50 flex"
on:click={resetFilter}
toggle:disabled={cannotReset}
>
{translation.reset}
</button>
<button class="width-50 flex primary" on:click={applyFilter}>
{translation.search}
<span class="icon">arrow_forward</span>
</button>
</div>
<hr></hr>
<span class="secondary" subscribe:innerText={resultText}></span>
</main>
<button on:click={closeFilters}>
{translation.close}
<span class="icon">close</span>
</button>
</div>
</div>
</div> </div>
); );
} }
+3 -2
View File
@@ -29,6 +29,7 @@ interface StatusData {
export function StatusView( export function StatusView(
chat: Chat, chat: Chat,
messageObjects: React.MapState<MessageObject>,
selectedObject: React.State<MessageObject | undefined>, selectedObject: React.State<MessageObject | undefined>,
isShowingObjectModal: React.State<boolean> isShowingObjectModal: React.State<boolean>
) { ) {
@@ -45,12 +46,12 @@ export function StatusView(
.sort((a, b) => a.localeCompare(b)) .sort((a, b) => a.localeCompare(b))
); );
chat.objects.subscribe(() => { messageObjects.subscribe(() => {
rows.clear(); rows.clear();
statuses.clear(); statuses.clear();
// add items // add items
chat.objects.value.forEach((messageObject) => { messageObjects.value.forEach((messageObject) => {
const latest = chat.getMostRecentContent(messageObject); const latest = chat.getMostRecentContent(messageObject);
if (!latest.categoryName || !latest.status) return; if (!latest.categoryName || !latest.status) return;
+18
View File
@@ -2,6 +2,8 @@ const englishTranslations = {
// general // general
set: "Set", set: "Set",
save: "Save", save: "Save",
search: "Search",
reset: "Reset",
back: "Back", back: "Back",
undoChanges: "Undo changes", undoChanges: "Undo changes",
close: "Close", close: "Close",
@@ -99,6 +101,14 @@ const englishTranslations = {
createObject: "create object", createObject: "create object",
untitledObject: "Untitled Object", untitledObject: "Untitled Object",
filterObjects: "Filter Objects",
searchByTitle: "Search by title",
searchByTitlePlaceholder: "My Object",
searchTitleText: (term: string, resultCount: number) =>
term == ""
? `Objects in total: ${resultCount}`
: `Objects containing "${term}": ${resultCount}`,
viewAll: "All", viewAll: "All",
viewNotes: "Notes", viewNotes: "Notes",
viewKanban: "Kanban", viewKanban: "Kanban",
@@ -131,6 +141,8 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
// general // general
set: "Guardar", set: "Guardar",
save: "Guardar", save: "Guardar",
search: "Buscar",
reset: "Restablecer",
back: "Atrás", back: "Atrás",
undoChanges: "Deshacer", undoChanges: "Deshacer",
close: "Cerrar", close: "Cerrar",
@@ -228,6 +240,8 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
createObject: "añadir nuevo objeto", createObject: "añadir nuevo objeto",
untitledObject: "Sin Título", untitledObject: "Sin Título",
filterObjects: "filtrar objetos",
viewAll: "Todos", viewAll: "Todos",
viewNotes: "Notas", viewNotes: "Notas",
viewKanban: "Kanban", viewKanban: "Kanban",
@@ -257,6 +271,8 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
// general // general
set: "OK", set: "OK",
save: "Sichern", save: "Sichern",
search: "Suchen",
reset: "Zurücksetzen",
back: "Zurück", back: "Zurück",
undoChanges: "Änderungen verwerfen", undoChanges: "Änderungen verwerfen",
close: "Schließen", close: "Schließen",
@@ -354,6 +370,8 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
createObject: "Neues Objekt erstellen", createObject: "Neues Objekt erstellen",
untitledObject: "Unbenannt", untitledObject: "Unbenannt",
filterObjects: "Objekte filtern",
viewAll: "Alle", viewAll: "Alle",
viewNotes: "Notizen", viewNotes: "Notizen",
viewKanban: "Kanban", viewKanban: "Kanban",