add kanban view

This commit is contained in:
marlon-erler
2024-07-25 02:49:23 +02:00
parent ece8f21d2f
commit ae1520bd89
9 changed files with 374 additions and 53 deletions
+23 -6
View File
@@ -293,6 +293,7 @@ body[showing-chat] #chat-page-wrapper {
flex-direction: column; flex-direction: column;
scroll-snap-align: center; scroll-snap-align: center;
min-width: 100%; min-width: 100%;
overflow: hidden;
} }
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side), #chat-page > div > #main > div > .pane-wrapper > .pane:not(.side),
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side) .content { #chat-page > div > #main > div > .pane-wrapper > .pane:not(.side) .content {
@@ -303,6 +304,8 @@ body[showing-chat] #chat-page-wrapper {
min-width: unset; min-width: unset;
} }
#chat-page > div > #main > div > .pane-wrapper.side { #chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem;
min-width: 18rem;
max-width: 18rem; max-width: 18rem;
} }
} }
@@ -566,28 +569,42 @@ body[showing-chat] #chat-page-wrapper {
} }
/* src/View/ChatPages/taskPage.css */ /* src/View/ChatPages/taskPage.css */
#task-page .task-grid { #chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
width: 100%;
max-width: 100%;
padding: 0px;
}
#chat-page > div > #main > #task-page .task-grid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-between-tiles); gap: var(--gap-between-tiles);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper {
display: flex;
gap: var(--large-gap);
width: 100%;
height: 100%;
overflow: auto;
padding: var(--content-padding);
} }
@media (min-width: 640px) { @media (min-width: 640px) {
#task-page .task-grid { #chat-page > div > #main > #task-page .task-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
#task-page #board-list { #chat-page > div > #main > #task-page #board-list {
display: false; display: false;
} }
#task-page #board-content { #chat-page > div > #main > #task-page #board-content {
display: none; display: none;
} }
#task-page[isshowingboard] #board-list { #chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none; display: none;
} }
#task-page[isshowingboard] #board-content { #chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex; display: flex;
} }
} }
+201 -38
View File
@@ -330,6 +330,45 @@
} }
}; };
}; };
function checkDoesObjectMatchReference(reference, stringEntryObject) {
reference_entry_loop: for (const referenceEntry of Object.entries(
reference
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue = stringEntryObject[referenceKey];
if (referenceValue == void 0) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue = referenceValue.substring(1);
if (strippedReferenceValue == "" && stringEntryObjectValue != void 0 && stringEntryObjectValue != "") {
return false;
}
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
if (referenceValue == "" && (stringEntryObjectValue == void 0 || stringEntryObjectValue == "")) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
return true;
}
function collectObjectValuesForKey(key, converter, objects) {
const values = /* @__PURE__ */ new Set();
for (const object of objects) {
const stringEntryObject = converter(object);
const stringEntryObjectValue = stringEntryObject[key];
if (stringEntryObjectValue == void 0 || stringEntryObjectValue == "")
continue;
values.add(stringEntryObjectValue);
}
return [...values.values()].sort(localeCompare);
}
var IndexManager = class { var IndexManager = class {
itemToString; itemToString;
sortedStrings = []; sortedStrings = [];
@@ -2145,7 +2184,9 @@
taskDescriptionLabel: "Description", taskDescriptionLabel: "Description",
taskDateLabel: "Date", taskDateLabel: "Date",
taskTimeLabel: "Time", taskTimeLabel: "Time",
deleteTaskButton: "Delete task" deleteTaskButton: "Delete task",
///
renameCategoryInputPlaceholder: "Rename category,"
} }
} }
}; };
@@ -2404,9 +2445,166 @@
)))))); ))))));
} }
// src/View/Components/filteredList.tsx
function FilteredList(reference, stringEntryObjectConverter, objects, viewBuilder) {
const matchingObjects = new ListState();
objects.handleAddition((newObject) => {
const doesMatch = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject)
);
if (doesMatch == false) return;
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
});
});
return viewBuilder(matchingObjects);
}
// src/View/Components/propertyList.tsx
function PropertyValueList(propertyKey, stringEntryObjectConverter, objects, viewBuilder) {
const propertyValues = new ListState();
function collectValues() {
propertyValues.clear();
const values = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()]
);
propertyValues.add(...values);
}
objects.subscribe(() => {
collectValues();
});
return viewBuilder(propertyValues);
}
// src/ViewModel/Pages/taskPropertyBulkChangeViewModel.ts
var TaskPropertyBulkChangeViewModel = class {
taskViewModels;
// state
inputValue = new State("");
// guards
cannotSet;
// methods
set = () => {
if (this.cannotSet.value == true) return;
this.taskViewModels.value.forEach((taskViewModel) => {
this.setValue(this.inputValue.value, taskViewModel);
});
};
setValue;
// init
constructor(taskViewModels, valueSetter, initialValue) {
this.taskViewModels = taskViewModels;
this.inputValue.value = initialValue;
this.setValue = valueSetter;
this.cannotSet = createProxyState(
[this.inputValue],
() => this.inputValue.value == "" || this.inputValue.value == initialValue
);
}
};
var TaskCategoryBulkChangeViewModel = class extends TaskPropertyBulkChangeViewModel {
constructor(taskViewModels, initialValue) {
super(
taskViewModels,
(newCategory, taskViewModel) => {
taskViewModel.category.value = newCategory;
taskViewModel.save();
},
initialValue
);
}
};
// src/View/Components/taskEntry.tsx
function TaskEntry(taskViewModel) {
const details = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---"
};
const view = /* @__PURE__ */ createElement("button", { class: "tile flex-no", "on:click": taskViewModel.open }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { "subscribe:innerText": taskViewModel.name }), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
"div",
{
class: "grid secondary",
style: "grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
},
...Object.entries(details).map((entry) => /* @__PURE__ */ createElement(
"span",
{
class: "flex-row align-center width-100 flex-no clip",
style: "gap: 1rem"
},
/* @__PURE__ */ createElement("span", { class: "icon", style: "font-size: 1.1rem" }, entry[0]),
/* @__PURE__ */ createElement("span", { class: "ellipsis" }, entry[1])
))
)));
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
var TaskViewModelToEntry = (taskViewModel) => {
return TaskEntry(taskViewModel);
};
// src/View/ChatPages/boardKanbanPage.tsx // src/View/ChatPages/boardKanbanPage.tsx
function BoardKanbanPage(boardViewModel) { function BoardKanbanPage(boardViewModel) {
return /* @__PURE__ */ createElement("div", null, "Kanban"); const categoryNameConverter = (categoryName) => {
return KanbanBoard(categoryName, boardViewModel);
};
return PropertyValueList(
"category",
(taskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(categories) => {
return /* @__PURE__ */ createElement(
"div",
{
class: "kanban-board-wrapper",
"children:append": [categories, categoryNameConverter]
}
);
}
);
}
function KanbanBoard(categoryName, boardViewModel) {
return FilteredList(
{ category: categoryName },
(taskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(taskViewModels) => {
const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
return /* @__PURE__ */ createElement("div", { class: "flex-column flex-no", style: "width: 18rem; min-width: 18rem" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
placeholder: translations.chatPage.task.renameCategoryInputPlaceholder,
"bind:value": viewModel.inputValue,
"on:enter": viewModel.set
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"on:click": viewModel.set,
"toggle:disabled": viewModel.cannotSet
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
"div",
{
class: "flex-column gap",
"children:append": [taskViewModels, TaskViewModelToEntry]
}
));
}
);
} }
// src/View/Modals/boardSettingsModal.tsx // src/View/Modals/boardSettingsModal.tsx
@@ -2471,41 +2669,6 @@
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": taskViewModel.close }, translations.general.closeButton), /* @__PURE__ */ createElement("button", { class: "flex primary", "on:click": taskViewModel.closeAndSave }, translations.general.saveButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "save"))))); ))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": taskViewModel.close }, translations.general.closeButton), /* @__PURE__ */ createElement("button", { class: "flex primary", "on:click": taskViewModel.closeAndSave }, translations.general.saveButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "save")))));
} }
// src/View/Components/taskEntry.tsx
function TaskEntry(taskViewModel) {
const details = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---"
};
const view = /* @__PURE__ */ createElement("button", { class: "tile", "on:click": taskViewModel.open }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { "subscribe:innerText": taskViewModel.name }), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
"div",
{
class: "grid secondary",
style: "grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
},
...Object.entries(details).map((entry) => /* @__PURE__ */ createElement(
"span",
{
class: "flex-row align-center width-100 flex-no clip",
style: "gap: 1rem"
},
/* @__PURE__ */ createElement("span", { class: "icon", style: "font-size: 1.1rem" }, entry[0]),
/* @__PURE__ */ createElement("span", { class: "ellipsis" }, entry[1])
))
)));
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
var TaskViewModelToEntry = (taskViewModel) => {
return TaskEntry(taskViewModel);
};
// src/View/ChatPages/boardPage.tsx // src/View/ChatPages/boardPage.tsx
function BoardPage(boardViewModel) { function BoardPage(boardViewModel) {
boardViewModel.loadData(); boardViewModel.loadData();
@@ -2588,7 +2751,7 @@
"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", "children:set": mainContent }), BoardSettingsModal(boardViewModel), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal })); ))), /* @__PURE__ */ createElement("div", { class: "content main-content", "children:set": mainContent }), BoardSettingsModal(boardViewModel), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal }));
} }
// src/View/Components/boardEntry.tsx // src/View/Components/boardEntry.tsx
+58 -1
View File
@@ -1,14 +1,71 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel"; import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { FilteredList } from "../Components/filteredList";
import { PropertyValueList } from "../Components/propertyList"; import { PropertyValueList } from "../Components/propertyList";
import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Pages/taskPropertyBulkChangeViewModel";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel"; import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function BoardKanbanPage(boardViewModel: BoardViewModel) { export function BoardKanbanPage(boardViewModel: BoardViewModel) {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
return KanbanBoard(categoryName, boardViewModel);
};
return PropertyValueList( return PropertyValueList(
"category", "category",
(taskViewModel: TaskViewModel) => taskViewModel.task, (taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels, boardViewModel.taskViewModels,
() => <div></div> (categories: React.ListState<string>) => {
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
}
);
}
function KanbanBoard(categoryName: string, boardViewModel: BoardViewModel) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
return (
<div class="flex-column flex-no" style="width: 18rem; min-width: 18rem">
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<div
class="flex-column gap"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
}
); );
} }
+1 -1
View File
@@ -103,7 +103,7 @@ export function BoardPage(boardViewModel: BoardViewModel) {
</button> </button>
</span> </span>
</div> </div>
<div class="content" children:set={mainContent}></div> <div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)} {BoardSettingsModal(boardViewModel)}
<div children:set={taskSettingsModal}></div> <div children:set={taskSettingsModal}></div>
+27 -6
View File
@@ -1,30 +1,51 @@
#task-page .task-grid { #chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
width: 100%;
max-width: 100%;
padding: 0px;
}
#chat-page > div > #main > #task-page .task-grid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-between-tiles); gap: var(--gap-between-tiles);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper {
display: flex;
gap: var(--large-gap);
width: 100%;
height: 100%;
overflow: auto;
padding: var(--content-padding);
} }
@media (min-width: 640px) { @media (min-width: 640px) {
#task-page .task-grid { #chat-page > div > #main > #task-page .task-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
#task-page #board-list { #chat-page > div > #main > #task-page #board-list {
display: false; display: false;
} }
#task-page #board-content { #chat-page > div > #main > #task-page #board-content {
display: none; display: none;
} }
#task-page[isshowingboard] #board-list { #chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none; display: none;
} }
#task-page[isshowingboard] #board-content { #chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex; display: flex;
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
}; };
const view = ( const view = (
<button class="tile" on:click={taskViewModel.open}> <button class="tile flex-no" on:click={taskViewModel.open}>
<div> <div>
<b subscribe:innerText={taskViewModel.name}></b> <b subscribe:innerText={taskViewModel.name}></b>
<hr></hr> <hr></hr>
+3
View File
@@ -66,6 +66,7 @@ body[showing-chat] #chat-page-wrapper {
scroll-snap-align: center; scroll-snap-align: center;
min-width: 100%; min-width: 100%;
overflow: hidden;
} }
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side), #chat-page > div > #main > div > .pane-wrapper > .pane:not(.side),
@@ -79,6 +80,8 @@ body[showing-chat] #chat-page-wrapper {
} }
#chat-page > div > #main > div > .pane-wrapper.side { #chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem;
min-width: 18rem;
max-width: 18rem; max-width: 18rem;
} }
} }
+4
View File
@@ -174,6 +174,10 @@ const englishTranslations = {
taskTimeLabel: "Time", taskTimeLabel: "Time",
deleteTaskButton: "Delete task", deleteTaskButton: "Delete task",
///
renameCategoryInputPlaceholder: "Rename category,"
}, },
}, },
}; };
@@ -0,0 +1,56 @@
import * as React from "bloatless-react";
import TaskViewModel from "./taskViewModel";
export default class TaskPropertyBulkChangeViewModel {
taskViewModels: React.ListState<TaskViewModel>;
// state
inputValue: React.State<string> = new React.State("");
// guards
cannotSet: React.State<boolean>;
// methods
set = (): void => {
if (this.cannotSet.value == true) return;
this.taskViewModels.value.forEach((taskViewModel: TaskViewModel) => {
this.setValue(this.inputValue.value, taskViewModel);
});
};
setValue: (newValue: string, taskViewModel: TaskViewModel) => void;
// init
constructor(
taskViewModels: React.ListState<TaskViewModel>,
valueSetter: (newValue: string, taskViewModel: TaskViewModel) => void,
initialValue: string
) {
this.taskViewModels = taskViewModels;
this.inputValue.value = initialValue;
this.setValue = valueSetter;
this.cannotSet = React.createProxyState(
[this.inputValue],
() => this.inputValue.value == "" || this.inputValue.value == initialValue
);
}
}
export class TaskCategoryBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
constructor(
taskViewModels: React.ListState<TaskViewModel>,
initialValue: string
) {
super(
taskViewModels,
(newCategory: string, taskViewModel: TaskViewModel) => {
taskViewModel.category.value = newCategory;
taskViewModel.save();
},
initialValue
);
}
}