diff --git a/apps/client/src/widgets/collections/NoteList.tsx b/apps/client/src/widgets/collections/NoteList.tsx index b0dd94622..8e462e44b 100644 --- a/apps/client/src/widgets/collections/NoteList.tsx +++ b/apps/client/src/widgets/collections/NoteList.tsx @@ -8,6 +8,7 @@ import GeoView from "./geomap"; import ViewModeStorage from "../view_widgets/view_mode_storage"; import CalendarView from "./calendar"; import TableView from "./table"; +import BoardView from "./board"; interface NoteListProps { note?: FNote | null; @@ -88,6 +89,8 @@ function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps< return case "table": return + case "board": + return } } diff --git a/apps/client/src/widgets/view_widgets/board_view/data.ts b/apps/client/src/widgets/collections/board/data.ts similarity index 95% rename from apps/client/src/widgets/view_widgets/board_view/data.ts rename to apps/client/src/widgets/collections/board/data.ts index f468f2292..2a59e82b7 100644 --- a/apps/client/src/widgets/view_widgets/board_view/data.ts +++ b/apps/client/src/widgets/collections/board/data.ts @@ -1,13 +1,13 @@ import FBranch from "../../../entities/fbranch"; import FNote from "../../../entities/fnote"; -import { BoardData } from "./config"; +import { BoardViewData } from "./index"; export type ColumnMap = Map; -export async function getBoardData(parentNote: FNote, groupByColumn: string, persistedData: BoardData) { +export async function getBoardData(parentNote: FNote, groupByColumn: string, persistedData: BoardViewData) { const byColumn: ColumnMap = new Map(); // First, scan all notes to find what columns actually exist @@ -43,7 +43,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per } // Return updated persisted data only if there were changes - let newPersistedData: BoardData | undefined; + let newPersistedData: BoardViewData | undefined; const hasChanges = newColumnValues.length > 0 || existingPersistedColumns.length !== deduplicatedColumns.length || !existingPersistedColumns.every((col, idx) => deduplicatedColumns[idx]?.value === col.value); diff --git a/apps/client/src/widgets/collections/board/index.css b/apps/client/src/widgets/collections/board/index.css new file mode 100644 index 000000000..bc941b54e --- /dev/null +++ b/apps/client/src/widgets/collections/board/index.css @@ -0,0 +1,264 @@ +.board-view { + overflow-x: auto; + position: relative; + height: 100%; + user-select: none; +} + +.board-view-container { + height: 100%; + display: flex; + gap: 1em; + padding: 1em; + padding-bottom: 0; + align-items: flex-start; +} + +.board-view-container .board-column { + width: 250px; + flex-shrink: 0; + border: 2px solid transparent; + border-radius: 8px; + padding: 0.5em; + background-color: var(--accented-background-color); + transition: border-color 0.2s ease; + overflow-y: auto; + max-height: 100%; +} + +.board-view-container .board-column.drag-over { + border-color: var(--main-text-color); + background-color: var(--hover-item-background-color); +} + +.board-view-container .board-column h3 { + font-size: 1em; + margin-bottom: 0.75em; + padding: 0.5em 0.5em 0.5em 0.5em; + border-bottom: 1px solid var(--main-border-color); + cursor: grab; + position: relative; + transition: background-color 0.2s ease, border-radius 0.2s ease; + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + background-color: transparent; +} + +.board-view-container .board-column h3:active { + cursor: grabbing; +} + +.board-view-container .board-column h3.editing { + cursor: default; +} + +.board-view-container .board-column h3:hover { + background-color: var(--hover-item-background-color); + border-radius: 4px; +} + +.board-view-container .board-column h3.editing { + background-color: var(--main-background-color); + border: 1px solid var(--main-text-color); + border-radius: 4px; +} + +.board-view-container .board-column.column-dragging { + opacity: 0.6; + transform: scale(0.98); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.board-view-container .board-column h3 input { + background: transparent; + border: none; + outline: none; + font-size: inherit; + font-weight: inherit; + color: inherit; + width: 100%; + font-family: inherit; +} + +.board-view-container .board-column h3 .edit-icon { + opacity: 0; + margin-left: 0.5em; + transition: opacity 0.2s ease; + color: var(--muted-text-color); +} + +.board-view-container .board-column h3:hover .edit-icon { + opacity: 1; +} + +.board-view-container .board-column h3.editing .edit-icon { + display: none; +} + +.board-view-container .board-note { + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.25); + margin: 0.65em 0; + padding: 0.5em; + border-radius: 5px; + cursor: move; + position: relative; + background-color: var(--main-background-color); + border: 1px solid var(--main-border-color); + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease; + opacity: 1; +} + +.board-view-container .board-note.fade-in { + animation: fadeIn 0.15s ease-in; +} + +.board-view-container .board-note.fade-out { + animation: fadeOut 0.15s ease-out forwards; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeOut { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(-10px); } +} + +.board-view-container .board-note.card-updated { + animation: cardUpdate 0.3s ease-in-out; +} + +@keyframes cardUpdate { + 0% { transform: scale(1); } + 50% { transform: scale(1.02); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } + 100% { transform: scale(1); } +} + +.board-view-container .board-note:hover { + transform: translateY(-2px); + box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35); +} + +.board-view-container .board-note.dragging { + opacity: 0.8; + transform: rotate(5deg); + z-index: 1000; + box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5); +} + +.board-view-container .board-note.editing { + box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35); + border-color: var(--main-text-color); +} + +.board-view-container .board-note.editing input { + background: transparent; + border: none; + outline: none; + font-family: inherit; + font-size: inherit; + color: inherit; + width: 100%; + padding: 0; +} + +.board-view-container .board-note .icon { + margin-right: 0.25em; +} + +.board-drop-indicator { + height: 3px; + background-color: var(--main-text-color); + border-radius: 2px; + margin: 0.25em 0; + opacity: 0; + transition: opacity 0.2s ease; +} + +.board-drop-indicator.show { + opacity: 1; +} + +.column-drop-indicator { + width: 4px; + background-color: var(--main-text-color); + border-radius: 2px; + opacity: 0; + transition: opacity 0.2s ease; + height: 100%; + z-index: 1000; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); + flex-shrink: 0; +} + +.column-drop-indicator.show { + opacity: 1; +} + +.board-new-item { + margin-top: 0.5em; + padding: 0.5em; + border-radius: 5px; + color: var(--muted-text-color); + cursor: pointer; + transition: all 0.2s ease; + background-color: transparent; +} + +.board-new-item:hover { + border-color: var(--main-text-color); + color: var(--main-text-color); + background-color: var(--hover-item-background-color); +} + +.board-new-item .icon { + margin-right: 0.25em; +} + +.board-add-column { + width: 180px; + flex-shrink: 0; + height: 60px; + border-radius: 8px; + padding: 0.5em; + background-color: var(--accented-background-color); + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--muted-text-color); + font-size: 0.9em; + align-self: flex-start; +} + +.board-add-column:hover { + border-color: var(--main-text-color); + color: var(--main-text-color); + background-color: var(--hover-item-background-color); +} + +.board-add-column .icon { + margin-right: 0.5em; + font-size: 1.2em; +} + +.board-drag-preview { + position: fixed; + z-index: 10000; + pointer-events: none; + opacity: 0.8; + transform: rotate(5deg); + box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5); + background-color: var(--main-background-color); + border: 1px solid var(--main-border-color); + border-radius: 5px; + padding: 0.5em; + font-size: 0.9em; + max-width: 200px; + word-wrap: break-word; +} \ No newline at end of file diff --git a/apps/client/src/widgets/collections/board/index.tsx b/apps/client/src/widgets/collections/board/index.tsx new file mode 100644 index 000000000..a84e934b8 --- /dev/null +++ b/apps/client/src/widgets/collections/board/index.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from "preact/hooks"; +import { ViewModeProps } from "../interface"; +import "./index.css"; +import { ColumnMap, getBoardData } from "./data"; +import { useNoteLabel } from "../../react/hooks"; + +export interface BoardViewData { + columns?: BoardColumnData[]; +} + +export interface BoardColumnData { + value: string; +} + +export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps) { + const [ statusAttribute ] = useNoteLabel(parentNote, "board:groupBy"); + const [ byColumn, setByColumn ] = useState(); + const [ columns, setColumns ] = useState(); + + useEffect(() => { + getBoardData(parentNote, statusAttribute ?? "status", viewConfig ?? {}).then(({ byColumn, newPersistedData }) => { + setByColumn(byColumn); + + if (newPersistedData) { + viewConfig = { ...newPersistedData }; + saveConfig(newPersistedData); + } + + // Use the order from persistedData.columns, then add any new columns found + const orderedColumns = viewConfig?.columns?.map(col => col.value) || []; + const allColumns = Array.from(byColumn.keys()); + const newColumns = allColumns.filter(col => !orderedColumns.includes(col)); + setColumns([...orderedColumns, ...newColumns]); + }); + }, [ parentNote ]); + + return ( +
+
+ {columns?.map(column => ( + + ))} +
+
+ ) +} + +function Column({ column }: { column: string }) { + return ( +
+

+ {column} + +

+
+ ) +} diff --git a/apps/client/src/widgets/view_widgets/board_view/api.ts b/apps/client/src/widgets/view_widgets/board_view/api.ts index 20c51141a..df354ace4 100644 --- a/apps/client/src/widgets/view_widgets/board_view/api.ts +++ b/apps/client/src/widgets/view_widgets/board_view/api.ts @@ -135,16 +135,13 @@ export default class BoardApi { async refresh(parentNote: FNote) { // Refresh the API data by re-fetching from the parent note - const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status"; - this._statusAttribute = statusAttribute; // Use the current in-memory persisted data instead of restoring from storage // This ensures we don't lose recent updates like column renames - const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, this.persistedData); - + // Update internal state this.byColumn = byColumn; - + if (newPersistedData) { this.persistedData = newPersistedData; this.viewStorage.store(this.persistedData); @@ -161,18 +158,6 @@ export default class BoardApi { const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status"; let persistedData = await viewStorage.restore() ?? {}; - const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, persistedData); - - // Use the order from persistedData.columns, then add any new columns found - const orderedColumns = persistedData.columns?.map(col => col.value) || []; - const allColumns = Array.from(byColumn.keys()); - const newColumns = allColumns.filter(col => !orderedColumns.includes(col)); - const columns = [...orderedColumns, ...newColumns]; - - if (newPersistedData) { - persistedData = newPersistedData; - viewStorage.store(persistedData); - } return new BoardApi(columns, parentNote.noteId, viewStorage, byColumn, persistedData, statusAttribute); } diff --git a/apps/client/src/widgets/view_widgets/board_view/config.ts b/apps/client/src/widgets/view_widgets/board_view/config.ts deleted file mode 100644 index 92dd99f5f..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface BoardColumnData { - value: string; -} - -export interface BoardData { - columns?: BoardColumnData[]; -} diff --git a/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts b/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts index 4f1cf64dc..e73f74a51 100644 --- a/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts +++ b/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts @@ -329,24 +329,6 @@ export class DifferentialBoardRenderer { } private createColumn(column: string, columnItems: { note: any; branch: any }[]): JQuery { - const $columnEl = $("
") - .addClass("board-column") - .attr("data-column", column); - - // Create header - const $titleEl = $("

").attr("data-column-value", column); - - // Create title text - const $titleText = $("").text(column); - - // Create edit icon - const $editIcon = $("") - .addClass("edit-icon icon bx bx-edit-alt") - .attr("title", "Click to edit column title"); - - $titleEl.append($titleText, $editIcon); - $columnEl.append($titleEl); - // Setup column dragging this.dragHandler.setupColumnDrag($columnEl, column); diff --git a/apps/client/src/widgets/view_widgets/board_view/index.ts b/apps/client/src/widgets/view_widgets/board_view/index.ts index 1a4a48bb2..ec203068f 100644 --- a/apps/client/src/widgets/view_widgets/board_view/index.ts +++ b/apps/client/src/widgets/view_widgets/board_view/index.ts @@ -9,279 +9,6 @@ import BoardApi from "./api"; import { BoardDragHandler, DragContext } from "./drag_handler"; import { DifferentialBoardRenderer } from "./differential_renderer"; -const TPL = /*html*/` -
- - -
-
-`; - export default class BoardView extends ViewMode { private $root: JQuery;