diff --git a/dist/index.css b/dist/index.css index 25f83a9d..35bfc4e9 100644 --- a/dist/index.css +++ b/dist/index.css @@ -583,7 +583,8 @@ body[showing-chat] #chat-page-wrapper { gap: var(--gap-between-tiles); padding: var(--content-padding); } -#chat-page > div > #main > #task-page .kanban-board-wrapper { +#chat-page > div > #main > #task-page .kanban-board-wrapper, +#chat-page > div > #main > #task-page .status-page-content { display: flex; gap: var(--large-gap); width: 100%; @@ -591,6 +592,38 @@ body[showing-chat] #chat-page-wrapper { overflow: auto; padding: var(--content-padding); } +#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column, +#chat-page > div > #main > #task-page .status-grid-wrapper .status-column { + display: flex; + flex-direction: column; + gap: var(--gap-between-tiles); +} +#chat-page > div > #main > #task-page .property-input-wrapper { + display: flex; + flex-direction: row; +} +#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column, +#chat-page > div > #main > #task-page .property-input-wrapper, +#chat-page > div > #main > #task-page .status-grid-wrapper .status-column, +#chat-page > div > #main > #task-page .status-name-row > *, +#chat-page > div > #main > #task-page .status-name-row::before { + width: 18rem; + min-width: 18rem; + max-width: 18rem; +} +#chat-page > div > #main > #task-page .status-name-row, +#chat-page > div > #main > #task-page .status-grid-wrapper { + display: flex; + gap: var(--large-gap); +} +#chat-page > div > #main > #task-page .status-page-content, +#chat-page > div > #main > #task-page .status-grid-wrapper { + flex-direction: column; +} +#chat-page > div > #main > #task-page .status-name-row::before { + content: ""; + display: flex; +} @media (min-width: 640px) { #chat-page > div > #main > #task-page .task-grid { display: grid; diff --git a/dist/index.js b/dist/index.js index 44907622..86ed536f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2519,6 +2519,18 @@ ); } }; + var TaskStatusBulkChangeViewModel = class extends TaskPropertyBulkChangeViewModel { + constructor(taskViewModels, initialValue) { + super( + taskViewModels, + (newStatus, taskViewModel) => { + taskViewModel.status.value = newStatus; + taskViewModel.save(); + }, + initialValue + ); + } + }; // src/View/Components/taskEntry.tsx function TaskEntry(taskViewModel) { @@ -2530,22 +2542,29 @@ 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", + const view = /* @__PURE__ */ createElement( + "button", { - class: "grid secondary", - style: "grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem" + class: "tile flex-no", + "on:click": taskViewModel.open }, - ...Object.entries(details).map((entry) => /* @__PURE__ */ createElement( - "span", + /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { "subscribe:innerText": taskViewModel.name }), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement( + "div", { - class: "flex-row align-center width-100 flex-no clip", - style: "gap: 1rem" + class: "grid secondary", + style: "grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem" }, - /* @__PURE__ */ createElement("span", { class: "icon", style: "font-size: 1.1rem" }, entry[0]), - /* @__PURE__ */ createElement("span", { class: "ellipsis" }, entry[1]) + ...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; }); @@ -2582,7 +2601,7 @@ 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( + return /* @__PURE__ */ createElement("div", { class: "flex-column flex-no" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement( "input", { placeholder: translations.chatPage.task.renameCategoryInputPlaceholder, @@ -2600,7 +2619,7 @@ )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement( "div", { - class: "flex-column gap", + class: "kanban-column", "children:append": [taskViewModels, TaskViewModelToEntry] } )); @@ -2623,6 +2642,132 @@ ))), /* @__PURE__ */ createElement("button", { "on:click": boardViewModel.hideSettings }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); } + // src/View/ChatPages/boardStatusGridPage.tsx + function BoardStatusGridPage(boardViewModel) { + const categoryRowConverter = (categoryName) => { + return CategoryRow(categoryName, statuses, boardViewModel); + }; + const statusNameCellConverter = (statusName) => { + return StatusNameCell(statusName, boardViewModel); + }; + const statuses = new ListState(); + boardViewModel.taskViewModels.subscribe(() => { + statuses.clear(); + const statusArray = collectObjectValuesForKey( + "status", + (taskViewModel) => taskViewModel.task, + [...boardViewModel.taskViewModels.value.values()] + ); + statuses.add(...statusArray); + }); + return /* @__PURE__ */ createElement("div", { class: "status-page-content" }, /* @__PURE__ */ createElement( + "div", + { + class: "status-name-row", + "children:append": [statuses, statusNameCellConverter] + } + ), PropertyValueList( + "category", + (taskViewModel) => taskViewModel.task, + boardViewModel.taskViewModels, + (categories) => { + return /* @__PURE__ */ createElement( + "div", + { + class: "status-grid-wrapper", + "children:append": [categories, categoryRowConverter] + } + ); + } + )); + } + function StatusNameCell(statusName, boardViewModel) { + const taskViewModelsWithMatchingStatus = new ListState(); + boardViewModel.taskViewModels.handleAddition( + (taskViewModel) => { + const doesMatchStatus = taskViewModel.task.status == statusName; + if (doesMatchStatus == false) return; + taskViewModelsWithMatchingStatus.add(taskViewModel); + boardViewModel.taskViewModels.handleRemoval(taskViewModel, () => { + taskViewModelsWithMatchingStatus.remove(taskViewModel); + }); + } + ); + const viewModel = new TaskStatusBulkChangeViewModel( + taskViewModelsWithMatchingStatus, + statusName + ); + return /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("div", { class: "property-input-wrapper" }, /* @__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") + ))); + } + function CategoryRow(categoryName, allStatuses, boardViewModel) { + return FilteredList( + { category: categoryName }, + (taskViewModel) => taskViewModel.task, + boardViewModel.taskViewModels, + (taskViewModels) => { + const statusNameConverter = (statusName) => { + return CategoryStatusColumn(statusName, taskViewModels); + }; + const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); + return /* @__PURE__ */ createElement("div", { class: "flex-row flex-no large-gap" }, /* @__PURE__ */ createElement("div", { class: "property-input-wrapper" }, /* @__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( + "div", + { + class: "flex-row large-gap padding-right", + "children:append": [allStatuses, statusNameConverter] + } + )); + } + ); + } + function CategoryStatusColumn(statusName, taskViewModelsWithMatchingCategory) { + const taskViewModels = new ListState(); + taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => { + const doesMatchStatus = taskViewModel.status.value == statusName; + if (doesMatchStatus == false) return; + taskViewModels.add(taskViewModel); + taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => { + taskViewModels.remove(taskViewModel); + }); + }); + return /* @__PURE__ */ createElement( + "div", + { + class: "status-column gap", + "children:append": [taskViewModels, TaskViewModelToEntry] + } + ); + } + // src/View/Components/boardViewToggleButton.tsx function BoardViewToggleButton(label, icon, page, boardViewModel) { function select() { @@ -2680,6 +2825,9 @@ case "kanban" /* Kanban */: { return BoardKanbanPage(boardViewModel); } + case "status-grid" /* StatusGrid */: { + return BoardStatusGridPage(boardViewModel); + } default: { return /* @__PURE__ */ createElement( "div", diff --git a/src/View/ChatPages/boardKanbanPage.tsx b/src/View/ChatPages/boardKanbanPage.tsx index f73e27c1..3b94c420 100644 --- a/src/View/ChatPages/boardKanbanPage.tsx +++ b/src/View/ChatPages/boardKanbanPage.tsx @@ -40,7 +40,7 @@ function KanbanBoard(categoryName: string, boardViewModel: BoardViewModel) { new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); return ( -
+
diff --git a/src/View/ChatPages/boardPage.tsx b/src/View/ChatPages/boardPage.tsx index c55bb12b..2d4feae4 100644 --- a/src/View/ChatPages/boardPage.tsx +++ b/src/View/ChatPages/boardPage.tsx @@ -6,6 +6,7 @@ import BoardViewModel, { import { BoardKanbanPage } from "./boardKanbanPage"; import { BoardSettingsModal } from "../Modals/boardSettingsModal"; +import { BoardStatusGridPage } from "./boardStatusGridPage"; import { BoardViewToggleButton } from "../Components/boardViewToggleButton"; import { TaskSettingsModal } from "../Modals/taskSettingsModal"; import { TaskViewModelToEntry } from "../Components/taskEntry"; @@ -21,6 +22,9 @@ export function BoardPage(boardViewModel: BoardViewModel) { case BoardPageType.Kanban: { return BoardKanbanPage(boardViewModel); } + case BoardPageType.StatusGrid: { + return BoardStatusGridPage(boardViewModel); + } default: { return (
= ( + categoryName: string + ) => { + return CategoryRow(categoryName, statuses, boardViewModel); + }; + + const statusNameCellConverter: React.StateItemConverter = ( + statusName: string + ) => { + return StatusNameCell(statusName, boardViewModel); + }; + + const statuses: React.ListState = new React.ListState(); + boardViewModel.taskViewModels.subscribe(() => { + statuses.clear(); + const statusArray: string[] = collectObjectValuesForKey( + "status", + (taskViewModel: TaskViewModel) => taskViewModel.task, + [...boardViewModel.taskViewModels.value.values()] + ); + statuses.add(...statusArray); + }); + + return ( +
+
+ {PropertyValueList( + "category", + (taskViewModel: TaskViewModel) => taskViewModel.task, + boardViewModel.taskViewModels, + (categories: React.ListState) => { + return ( +
+ ); + } + )} +
+ ); +} + +function StatusNameCell(statusName: string, boardViewModel: BoardViewModel) { + const taskViewModelsWithMatchingStatus: React.ListState = + new React.ListState(); + + boardViewModel.taskViewModels.handleAddition( + (taskViewModel: TaskViewModel) => { + const doesMatchStatus: boolean = taskViewModel.task.status == statusName; + if (doesMatchStatus == false) return; + + taskViewModelsWithMatchingStatus.add(taskViewModel); + boardViewModel.taskViewModels.handleRemoval(taskViewModel, () => { + taskViewModelsWithMatchingStatus.remove(taskViewModel); + }); + } + ); + + const viewModel: TaskStatusBulkChangeViewModel = + new TaskStatusBulkChangeViewModel( + taskViewModelsWithMatchingStatus, + statusName + ); + + return ( +
+
+ + +
+
+ ); +} + +function CategoryRow( + categoryName: string, + allStatuses: React.ListState, + boardViewModel: BoardViewModel +) { + return FilteredList( + { category: categoryName }, + (taskViewModel: TaskViewModel) => taskViewModel.task, + boardViewModel.taskViewModels, + (taskViewModels: React.ListState) => { + const statusNameConverter: React.StateItemConverter = ( + statusName: string + ) => { + return CategoryStatusColumn(statusName, taskViewModels); + }; + + const viewModel: TaskCategoryBulkChangeViewModel = + new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName); + + return ( +
+
+ + +
+ +
+
+ ); + } + ); +} + +function CategoryStatusColumn( + statusName: string, + taskViewModelsWithMatchingCategory: React.ListState +) { + const taskViewModels: React.ListState = new React.ListState(); + + taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => { + const doesMatchStatus: boolean = taskViewModel.status.value == statusName; + if (doesMatchStatus == false) return; + + taskViewModels.add(taskViewModel); + taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => { + taskViewModels.remove(taskViewModel); + }); + }); + + return ( +
+ ); +} diff --git a/src/View/ChatPages/taskPage.css b/src/View/ChatPages/taskPage.css index 2fb34e8f..5b201823 100644 --- a/src/View/ChatPages/taskPage.css +++ b/src/View/ChatPages/taskPage.css @@ -24,7 +24,8 @@ padding: var(--content-padding); } -#chat-page > div > #main > #task-page .kanban-board-wrapper { +#chat-page > div > #main > #task-page .kanban-board-wrapper, +#chat-page > div > #main > #task-page .status-page-content { display: flex; gap: var(--large-gap); @@ -36,6 +37,45 @@ padding: var(--content-padding); } +#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column, +#chat-page > div > #main > #task-page .status-grid-wrapper .status-column { + display: flex; + flex-direction: column; + + gap: var(--gap-between-tiles); +} + +#chat-page > div > #main > #task-page .property-input-wrapper { + display: flex; + flex-direction: row; +} + +#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column, +#chat-page > div > #main > #task-page .property-input-wrapper, +#chat-page > div > #main > #task-page .status-grid-wrapper .status-column, +#chat-page > div > #main > #task-page .status-name-row > *, +#chat-page > div > #main > #task-page .status-name-row::before { + width: 18rem; + min-width: 18rem; + max-width: 18rem; +} + +#chat-page > div > #main > #task-page .status-name-row, +#chat-page > div > #main > #task-page .status-grid-wrapper { + display: flex; + gap: var(--large-gap); +} + +#chat-page > div > #main > #task-page .status-page-content, +#chat-page > div > #main > #task-page .status-grid-wrapper { + flex-direction: column; +} + +#chat-page > div > #main > #task-page .status-name-row::before { + content: ""; + display: flex; +} + @media (min-width: 640px) { #chat-page > div > #main > #task-page .task-grid { display: grid; diff --git a/src/View/Components/propertyList.tsx b/src/View/Components/propertyList.tsx index 8966d63a..b5551d43 100644 --- a/src/View/Components/propertyList.tsx +++ b/src/View/Components/propertyList.tsx @@ -9,7 +9,7 @@ export function PropertyValueList( propertyKey: string, stringEntryObjectConverter: (object: T) => StringEntryObject, objects: React.ListState | React.MapState, - viewBuilder: (categories: React.ListState) => HTMLElement + viewBuilder: (keys: React.ListState) => HTMLElement ) { const propertyValues: React.ListState = new React.ListState(); diff --git a/src/View/Components/taskEntry.tsx b/src/View/Components/taskEntry.tsx index bc3a5316..c4f55a53 100644 --- a/src/View/Components/taskEntry.tsx +++ b/src/View/Components/taskEntry.tsx @@ -13,7 +13,10 @@ export function TaskEntry(taskViewModel: TaskViewModel) { }; const view = ( -