mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Merge branch 'react/collections' of https://github.com/TriliumNext/trilium into react/collections
This commit is contained in:
		| @@ -8,6 +8,7 @@ import GeoView from "./geomap"; | |||||||
| import ViewModeStorage from "../view_widgets/view_mode_storage"; | import ViewModeStorage from "../view_widgets/view_mode_storage"; | ||||||
| import CalendarView from "./calendar"; | import CalendarView from "./calendar"; | ||||||
| import TableView from "./table"; | import TableView from "./table"; | ||||||
|  | import BoardView from "./board"; | ||||||
|  |  | ||||||
| interface NoteListProps<T extends object> { | interface NoteListProps<T extends object> { | ||||||
|     note?: FNote | null; |     note?: FNote | null; | ||||||
| @@ -88,6 +89,8 @@ function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps< | |||||||
|             return <CalendarView {...props} /> |             return <CalendarView {...props} /> | ||||||
|         case "table": |         case "table": | ||||||
|             return <TableView {...props} /> |             return <TableView {...props} /> | ||||||
|  |         case "board": | ||||||
|  |             return <BoardView {...props} /> | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								apps/client/src/widgets/collections/board/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/client/src/widgets/collections/board/api.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import FNote from "../../../entities/fnote"; | ||||||
|  | import attributes from "../../../services/attributes"; | ||||||
|  | import note_create from "../../../services/note_create"; | ||||||
|  |  | ||||||
|  | export async function createNewItem(parentNote: FNote, column: string) { | ||||||
|  |     try { | ||||||
|  |         // Get the parent note path | ||||||
|  |         const parentNotePath = parentNote.noteId; | ||||||
|  |         const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status"; | ||||||
|  |  | ||||||
|  |         // Create a new note as a child of the parent note | ||||||
|  |         const { note: newNote } = await note_create.createNote(parentNotePath, { | ||||||
|  |             activate: false, | ||||||
|  |             title: "New item" | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (newNote) { | ||||||
|  |             // Set the status label to place it in the correct column | ||||||
|  |             await changeColumn(newNote.noteId, column, statusAttribute); | ||||||
|  |  | ||||||
|  |             // Start inline editing of the newly created card | ||||||
|  |             //this.startInlineEditingCard(newNote.noteId); | ||||||
|  |         } | ||||||
|  |     } catch (error) { | ||||||
|  |         console.error("Failed to create new item:", error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function changeColumn(noteId: string, newColumn: string, statusAttribute: string) { | ||||||
|  |     await attributes.setLabel(noteId, statusAttribute, newColumn); | ||||||
|  | } | ||||||
| @@ -1,13 +1,13 @@ | |||||||
| import FBranch from "../../../entities/fbranch"; | import FBranch from "../../../entities/fbranch"; | ||||||
| import FNote from "../../../entities/fnote"; | import FNote from "../../../entities/fnote"; | ||||||
| import { BoardData } from "./config"; | import { BoardViewData } from "./index"; | ||||||
| 
 | 
 | ||||||
| export type ColumnMap = Map<string, { | export type ColumnMap = Map<string, { | ||||||
|     branch: FBranch; |     branch: FBranch; | ||||||
|     note: FNote; |     note: FNote; | ||||||
| }[]>; | }[]>; | ||||||
| 
 | 
 | ||||||
| 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(); |     const byColumn: ColumnMap = new Map(); | ||||||
| 
 | 
 | ||||||
|     // First, scan all notes to find what columns actually exist
 |     // 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
 |     // Return updated persisted data only if there were changes
 | ||||||
|     let newPersistedData: BoardData | undefined; |     let newPersistedData: BoardViewData | undefined; | ||||||
|     const hasChanges = newColumnValues.length > 0 || |     const hasChanges = newColumnValues.length > 0 || | ||||||
|                       existingPersistedColumns.length !== deduplicatedColumns.length || |                       existingPersistedColumns.length !== deduplicatedColumns.length || | ||||||
|                       !existingPersistedColumns.every((col, idx) => deduplicatedColumns[idx]?.value === col.value); |                       !existingPersistedColumns.every((col, idx) => deduplicatedColumns[idx]?.value === col.value); | ||||||
| @@ -65,6 +65,7 @@ async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupB | |||||||
|     for (const branch of branches) { |     for (const branch of branches) { | ||||||
|         const note = await branch.getNote(); |         const note = await branch.getNote(); | ||||||
|         if (!note) { |         if (!note) { | ||||||
|  |             console.warn("Not note found"); | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
							
								
								
									
										276
									
								
								apps/client/src/widgets/collections/board/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								apps/client/src/widgets/collections/board/index.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | |||||||
|  | .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-add-column input { | ||||||
|  |   background: var(--main-background-color); | ||||||
|  |   border: 1px solid var(--main-text-color); | ||||||
|  |   border-radius: 4px; | ||||||
|  |   padding: 0.5em; | ||||||
|  |   color: var(--main-text-color); | ||||||
|  |   font-family: inherit; | ||||||
|  |   font-size: inherit; | ||||||
|  |   width: 100%; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .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; | ||||||
|  | } | ||||||
							
								
								
									
										177
									
								
								apps/client/src/widgets/collections/board/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								apps/client/src/widgets/collections/board/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | import { useCallback, useEffect, useRef, useState } from "preact/hooks"; | ||||||
|  | import { ViewModeProps } from "../interface"; | ||||||
|  | import "./index.css"; | ||||||
|  | import { ColumnMap, getBoardData } from "./data"; | ||||||
|  | import { useNoteLabel, useTriliumEvent } from "../../react/hooks"; | ||||||
|  | import FNote from "../../../entities/fnote"; | ||||||
|  | import FBranch from "../../../entities/fbranch"; | ||||||
|  | import Icon from "../../react/Icon"; | ||||||
|  | import { t } from "../../../services/i18n"; | ||||||
|  | import { createNewItem } from "./api"; | ||||||
|  | import FormTextBox from "../../react/FormTextBox"; | ||||||
|  |  | ||||||
|  | export interface BoardViewData { | ||||||
|  |     columns?: BoardColumnData[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface BoardColumnData { | ||||||
|  |     value: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps<BoardViewData>) { | ||||||
|  |     const [ statusAttribute ] = useNoteLabel(parentNote, "board:groupBy"); | ||||||
|  |     const [ byColumn, setByColumn ] = useState<ColumnMap>(); | ||||||
|  |     const [ columns, setColumns ] = useState<string[]>(); | ||||||
|  |  | ||||||
|  |     function refresh() { | ||||||
|  |         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]); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     useEffect(refresh, [ parentNote, noteIds ]); | ||||||
|  |  | ||||||
|  |     useTriliumEvent("entitiesReloaded", ({ loadResults }) => { | ||||||
|  |         // TODO: Re-enable | ||||||
|  |         return; | ||||||
|  |  | ||||||
|  |         // Check if any changes affect our board | ||||||
|  |         const hasRelevantChanges = | ||||||
|  |             // React to changes in status attribute for notes in this board | ||||||
|  |             loadResults.getAttributeRows().some(attr => attr.name === statusAttribute && noteIds.includes(attr.noteId!)) || | ||||||
|  |             // React to changes in note title | ||||||
|  |             loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) || | ||||||
|  |             // React to changes in branches for subchildren (e.g., moved, added, or removed notes) | ||||||
|  |             loadResults.getBranchRows().some(branch => noteIds.includes(branch.noteId!)) || | ||||||
|  |             // React to changes in note icon or color. | ||||||
|  |             loadResults.getAttributeRows().some(attr => [ "iconClass", "color" ].includes(attr.name ?? "") && noteIds.includes(attr.noteId ?? "")) || | ||||||
|  |             // React to attachment change | ||||||
|  |             loadResults.getAttachmentRows().some(att => att.ownerId === parentNote.noteId && att.title === "board.json") || | ||||||
|  |             // React to changes in "groupBy" | ||||||
|  |             loadResults.getAttributeRows().some(attr => attr.name === "board:groupBy" && attr.noteId === parentNote.noteId); | ||||||
|  |  | ||||||
|  |         if (hasRelevantChanges) { | ||||||
|  |             console.log("Trigger refresh"); | ||||||
|  |             refresh(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className="board-view"> | ||||||
|  |             <div className="board-view-container"> | ||||||
|  |                 {byColumn && columns?.map(column => ( | ||||||
|  |                     <Column | ||||||
|  |                         column={column} | ||||||
|  |                         columnItems={byColumn.get(column)} | ||||||
|  |                         parentNote={parentNote} | ||||||
|  |                     /> | ||||||
|  |                 ))} | ||||||
|  |  | ||||||
|  |                 <AddNewColumn viewConfig={viewConfig} saveConfig={saveConfig} /> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Column({ parentNote, column, columnItems }: { parentNote: FNote, column: string, columnItems?: { note: FNote, branch: FBranch }[] }) { | ||||||
|  |     return ( | ||||||
|  |         <div className="board-column"> | ||||||
|  |             <h3> | ||||||
|  |                 <span>{column}</span> | ||||||
|  |                 <span | ||||||
|  |                     className="edit-icon icon bx bx-edit-alt" | ||||||
|  |                     title="Click to edit column title" /> | ||||||
|  |             </h3> | ||||||
|  |  | ||||||
|  |             {(columnItems ?? []).map(({ note, branch }) => ( | ||||||
|  |                 <Card note={note} branch={branch} column={column} /> | ||||||
|  |             ))} | ||||||
|  |  | ||||||
|  |             <div className="board-new-item" onClick={() => createNewItem(parentNote, column)}> | ||||||
|  |                 <Icon icon="bx bx-plus" />{" "} | ||||||
|  |                 {t("board_view.new-item")} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Card({ note }: { note: FNote, branch: FBranch, column: string }) { | ||||||
|  |     const colorClass = note.getColorClass() || ''; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className={`board-note ${colorClass}`}> | ||||||
|  |             <span class={`icon ${note.getIcon()}`} /> | ||||||
|  |             {note.title} | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function AddNewColumn({ viewConfig, saveConfig }: { viewConfig?: BoardViewData, saveConfig: (data: BoardViewData) => void }) { | ||||||
|  |     const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false); | ||||||
|  |     const columnNameRef = useRef<HTMLInputElement>(null); | ||||||
|  |  | ||||||
|  |     const addColumnCallback = useCallback(() => { | ||||||
|  |         setIsCreatingNewColumn(true); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     const finishEdit = useCallback((save: boolean) => { | ||||||
|  |         const columnName = columnNameRef.current?.value; | ||||||
|  |         if (!columnName || !save) { | ||||||
|  |             setIsCreatingNewColumn(false); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add the new column to persisted data if it doesn't exist | ||||||
|  |         if (!viewConfig) { | ||||||
|  |             viewConfig = {}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!viewConfig.columns) { | ||||||
|  |             viewConfig.columns = []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const existingColumn = viewConfig.columns.find(col => col.value === columnName); | ||||||
|  |         if (!existingColumn) { | ||||||
|  |             viewConfig.columns.push({ value: columnName }); | ||||||
|  |             saveConfig(viewConfig); | ||||||
|  |         } | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className={`board-add-column ${isCreatingNewColumn ? "editing" : ""}`} onClick={addColumnCallback}> | ||||||
|  |             {!isCreatingNewColumn | ||||||
|  |             ? <> | ||||||
|  |                 <Icon icon="bx bx-plus" />{" "} | ||||||
|  |                 {t("board_view.add-column")} | ||||||
|  |             </> | ||||||
|  |             : <> | ||||||
|  |                 <FormTextBox | ||||||
|  |                     inputRef={columnNameRef} | ||||||
|  |                     type="text" | ||||||
|  |                     placeholder="Enter column name..." | ||||||
|  |                     onBlur={() => finishEdit(true)} | ||||||
|  |                     onKeyDown={(e: KeyboardEvent) => { | ||||||
|  |                         if (e.key === "Enter") { | ||||||
|  |                             e.preventDefault(); | ||||||
|  |                             finishEdit(true); | ||||||
|  |                         } else if (e.key === "Escape") { | ||||||
|  |                             e.preventDefault(); | ||||||
|  |                             finishEdit(false); | ||||||
|  |                         } | ||||||
|  |                     }} | ||||||
|  |                 /> | ||||||
|  |             </>} | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -29,10 +29,6 @@ export default class BoardApi { | |||||||
|         return this.byColumn.get(column); |         return this.byColumn.get(column); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async changeColumn(noteId: string, newColumn: string) { |  | ||||||
|         await attributes.setLabel(noteId, this._statusAttribute, newColumn); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     openNote(noteId: string) { |     openNote(noteId: string) { | ||||||
|         appContext.triggerCommand("openInPopup", { noteIdOrPath: noteId }); |         appContext.triggerCommand("openInPopup", { noteIdOrPath: noteId }); | ||||||
|     } |     } | ||||||
| @@ -95,21 +91,6 @@ export default class BoardApi { | |||||||
|         this.viewStorage.store(this.persistedData); |         this.viewStorage.store(this.persistedData); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async createColumn(columnValue: string) { |  | ||||||
|         // Add the new column to persisted data if it doesn't exist |  | ||||||
|         if (!this.persistedData.columns) { |  | ||||||
|             this.persistedData.columns = []; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const existingColumn = this.persistedData.columns.find(col => col.value === columnValue); |  | ||||||
|         if (!existingColumn) { |  | ||||||
|             this.persistedData.columns.push({ value: columnValue }); |  | ||||||
|             await this.viewStorage.store(this.persistedData); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return columnValue; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async reorderColumns(newColumnOrder: string[]) { |     async reorderColumns(newColumnOrder: string[]) { | ||||||
|         // Update the column order in persisted data |         // Update the column order in persisted data | ||||||
|         if (!this.persistedData.columns) { |         if (!this.persistedData.columns) { | ||||||
| @@ -135,12 +116,9 @@ export default class BoardApi { | |||||||
|  |  | ||||||
|     async refresh(parentNote: FNote) { |     async refresh(parentNote: FNote) { | ||||||
|         // Refresh the API data by re-fetching from the parent note |         // 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 |         // Use the current in-memory persisted data instead of restoring from storage | ||||||
|         // This ensures we don't lose recent updates like column renames |         // This ensures we don't lose recent updates like column renames | ||||||
|         const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, this.persistedData); |  | ||||||
|  |  | ||||||
|         // Update internal state |         // Update internal state | ||||||
|         this.byColumn = byColumn; |         this.byColumn = byColumn; | ||||||
| @@ -161,18 +139,6 @@ export default class BoardApi { | |||||||
|         const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status"; |         const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status"; | ||||||
|  |  | ||||||
|         let persistedData = await viewStorage.restore() ?? {}; |         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); |         return new BoardApi(columns, parentNote.noteId, viewStorage, byColumn, persistedData, statusAttribute); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,7 +0,0 @@ | |||||||
| export interface BoardColumnData { |  | ||||||
|     value: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface BoardData { |  | ||||||
|     columns?: BoardColumnData[]; |  | ||||||
| } |  | ||||||
| @@ -95,8 +95,6 @@ export class DifferentialBoardRenderer { | |||||||
|             const $columnEl = this.createColumn(column, columnItems); |             const $columnEl = this.createColumn(column, columnItems); | ||||||
|             this.$container.append($columnEl); |             this.$container.append($columnEl); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.addAddColumnButton(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async differentialRender(oldState: BoardState, newState: BoardState): Promise<void> { |     private async differentialRender(oldState: BoardState, newState: BoardState): Promise<void> { | ||||||
| @@ -329,24 +327,6 @@ export class DifferentialBoardRenderer { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private createColumn(column: string, columnItems: { note: any; branch: any }[]): JQuery<HTMLElement> { |     private createColumn(column: string, columnItems: { note: any; branch: any }[]): JQuery<HTMLElement> { | ||||||
|         const $columnEl = $("<div>") |  | ||||||
|             .addClass("board-column") |  | ||||||
|             .attr("data-column", column); |  | ||||||
|  |  | ||||||
|         // Create header |  | ||||||
|         const $titleEl = $("<h3>").attr("data-column-value", column); |  | ||||||
|  |  | ||||||
|         // Create title text |  | ||||||
|         const $titleText = $("<span>").text(column); |  | ||||||
|  |  | ||||||
|         // Create edit icon |  | ||||||
|         const $editIcon = $("<span>") |  | ||||||
|             .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 |         // Setup column dragging | ||||||
|         this.dragHandler.setupColumnDrag($columnEl, column); |         this.dragHandler.setupColumnDrag($columnEl, column); | ||||||
|  |  | ||||||
| @@ -363,47 +343,18 @@ export class DifferentialBoardRenderer { | |||||||
|         this.dragHandler.setupNoteDropZone($columnEl, column); |         this.dragHandler.setupNoteDropZone($columnEl, column); | ||||||
|         this.dragHandler.setupColumnDropZone($columnEl); |         this.dragHandler.setupColumnDropZone($columnEl); | ||||||
|  |  | ||||||
|         // Add cards |  | ||||||
|         for (const item of columnItems) { |  | ||||||
|             if (item.note) { |  | ||||||
|                 const $noteEl = this.createCard(item.note, item.branch, column); |  | ||||||
|                 $columnEl.append($noteEl); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Add "New item" button |         // Add "New item" button | ||||||
|         const $newItemEl = $("<div>") |         const $newItemEl = $("<div>") | ||||||
|             .addClass("board-new-item") |             .addClass("board-new-item") | ||||||
|             .attr("data-column", column) |             .attr("data-column", column) | ||||||
|             .html(`<span class="icon bx bx-plus"></span> ${t("board_view.new-item")}`); |             .html(`<span class="icon bx bx-plus"></span> ${}`); | ||||||
|  |  | ||||||
|         $newItemEl.on("click", () => this.onCreateNewItem(column)); |  | ||||||
|         $columnEl.append($newItemEl); |         $columnEl.append($newItemEl); | ||||||
|  |  | ||||||
|         return $columnEl; |         return $columnEl; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private createCard(note: any, branch: any, column: string): JQuery<HTMLElement> { |     private createCard(note: any, branch: any, column: string): JQuery<HTMLElement> { | ||||||
|         const $iconEl = $("<span>") |  | ||||||
|             .addClass("icon") |  | ||||||
|             .addClass(note.getIcon()); |  | ||||||
|  |  | ||||||
|         const colorClass = note.getColorClass() || ''; |  | ||||||
|  |  | ||||||
|         const $noteEl = $("<div>") |  | ||||||
|             .addClass("board-note") |  | ||||||
|             .attr("data-note-id", note.noteId) |  | ||||||
|             .attr("data-branch-id", branch.branchId) |  | ||||||
|             .attr("data-current-column", column) |  | ||||||
|             .attr("data-icon-class", note.getIcon()) |  | ||||||
|             .attr("data-color-class", colorClass) |  | ||||||
|             .text(note.title); |  | ||||||
|  |  | ||||||
|         // Add color class to the card if it exists |  | ||||||
|         if (colorClass) { |  | ||||||
|             $noteEl.addClass(colorClass); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $noteEl.prepend($iconEl); |         $noteEl.prepend($iconEl); | ||||||
|         $noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId })); |         $noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId })); | ||||||
|  |  | ||||||
| @@ -413,16 +364,6 @@ export class DifferentialBoardRenderer { | |||||||
|         return $noteEl; |         return $noteEl; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private addAddColumnButton(): void { |  | ||||||
|         if (this.$container.find('.board-add-column').length === 0) { |  | ||||||
|             const $addColumnEl = $("<div>") |  | ||||||
|                 .addClass("board-add-column") |  | ||||||
|                 .html(`<span class="icon bx bx-plus"></span> ${t("board_view.add-column")}`); |  | ||||||
|  |  | ||||||
|             this.$container.append($addColumnEl); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     forceFullRender(): void { |     forceFullRender(): void { | ||||||
|         this.lastState = null; |         this.lastState = null; | ||||||
|         if (this.updateTimeout) { |         if (this.updateTimeout) { | ||||||
|   | |||||||
| @@ -9,279 +9,6 @@ import BoardApi from "./api"; | |||||||
| import { BoardDragHandler, DragContext } from "./drag_handler"; | import { BoardDragHandler, DragContext } from "./drag_handler"; | ||||||
| import { DifferentialBoardRenderer } from "./differential_renderer"; | import { DifferentialBoardRenderer } from "./differential_renderer"; | ||||||
|  |  | ||||||
| const TPL = /*html*/` |  | ||||||
| <div class="board-view"> |  | ||||||
|     <style> |  | ||||||
|         .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; |  | ||||||
|         } |  | ||||||
|     </style> |  | ||||||
|  |  | ||||||
|     <div class="board-view-container"></div> |  | ||||||
| </div> |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| export default class BoardView extends ViewMode<BoardData> { | export default class BoardView extends ViewMode<BoardData> { | ||||||
|  |  | ||||||
|     private $root: JQuery<HTMLElement>; |     private $root: JQuery<HTMLElement>; | ||||||
| @@ -337,7 +64,6 @@ export default class BoardView extends ViewMode<BoardData> { | |||||||
|             this.$container, |             this.$container, | ||||||
|             this.api, |             this.api, | ||||||
|             this.dragHandler, |             this.dragHandler, | ||||||
|             (column: string) => this.createNewItem(column), |  | ||||||
|             this.parentNote, |             this.parentNote, | ||||||
|             this.viewStorage, |             this.viewStorage, | ||||||
|             () => this.refreshApi() |             () => this.refreshApi() | ||||||
| @@ -493,32 +219,6 @@ export default class BoardView extends ViewMode<BoardData> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async createNewItem(column: string) { |  | ||||||
|         try { |  | ||||||
|             // Get the parent note path |  | ||||||
|             const parentNotePath = this.parentNote.noteId; |  | ||||||
|  |  | ||||||
|             // Create a new note as a child of the parent note |  | ||||||
|             const { note: newNote } = await noteCreateService.createNote(parentNotePath, { |  | ||||||
|                 activate: false, |  | ||||||
|                 title: "New item" |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             if (newNote) { |  | ||||||
|                 // Set the status label to place it in the correct column |  | ||||||
|                 await this.api?.changeColumn(newNote.noteId, column); |  | ||||||
|  |  | ||||||
|                 // Refresh the board to show the new item |  | ||||||
|                 await this.renderList(); |  | ||||||
|  |  | ||||||
|                 // Start inline editing of the newly created card |  | ||||||
|                 this.startInlineEditingCard(newNote.noteId); |  | ||||||
|             } |  | ||||||
|         } catch (error) { |  | ||||||
|             console.error("Failed to create new item:", error); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async insertItemAtPosition(column: string, relativeToBranchId: string, direction: "before" | "after"): Promise<void> { |     async insertItemAtPosition(column: string, relativeToBranchId: string, direction: "before" | "after"): Promise<void> { | ||||||
|         try { |         try { | ||||||
|             // Create the note without opening it |             // Create the note without opening it | ||||||
| @@ -546,27 +246,6 @@ export default class BoardView extends ViewMode<BoardData> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private startCreatingNewColumn($addColumnEl: JQuery<HTMLElement>) { |     private startCreatingNewColumn($addColumnEl: JQuery<HTMLElement>) { | ||||||
|         if ($addColumnEl.hasClass("editing")) { |  | ||||||
|             return; // Already editing |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $addColumnEl.addClass("editing"); |  | ||||||
|  |  | ||||||
|         const $input = $("<input>") |  | ||||||
|             .attr("type", "text") |  | ||||||
|             .attr("placeholder", "Enter column name...") |  | ||||||
|             .css({ |  | ||||||
|                 background: "var(--main-background-color)", |  | ||||||
|                 border: "1px solid var(--main-text-color)", |  | ||||||
|                 borderRadius: "4px", |  | ||||||
|                 padding: "0.5em", |  | ||||||
|                 color: "var(--main-text-color)", |  | ||||||
|                 fontFamily: "inherit", |  | ||||||
|                 fontSize: "inherit", |  | ||||||
|                 width: "100%", |  | ||||||
|                 textAlign: "center" |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|         $addColumnEl.empty().append($input); |         $addColumnEl.empty().append($input); | ||||||
|         $input.focus(); |         $input.focus(); | ||||||
|  |  | ||||||
| @@ -583,21 +262,7 @@ export default class BoardView extends ViewMode<BoardData> { | |||||||
|                     await this.createNewColumn(columnName.trim()); |                     await this.createNewColumn(columnName.trim()); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Restore the add button |  | ||||||
|             $addColumnEl.html('<span class="icon bx bx-plus"></span>Add Column'); |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         $input.on("blur", () => finishEdit(true)); |  | ||||||
|         $input.on("keydown", (e) => { |  | ||||||
|             if (e.key === "Enter") { |  | ||||||
|                 e.preventDefault(); |  | ||||||
|                 finishEdit(true); |  | ||||||
|             } else if (e.key === "Escape") { |  | ||||||
|                 e.preventDefault(); |  | ||||||
|                 finishEdit(false); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async createNewColumn(columnName: string) { |     private async createNewColumn(columnName: string) { | ||||||
| @@ -618,31 +283,6 @@ export default class BoardView extends ViewMode<BoardData> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { |  | ||||||
|         // Check if any changes affect our board |  | ||||||
|         const hasRelevantChanges = |  | ||||||
|             // React to changes in status attribute for notes in this board |  | ||||||
|             loadResults.getAttributeRows().some(attr => attr.name === this.api?.statusAttribute && this.noteIds.includes(attr.noteId!)) || |  | ||||||
|             // React to changes in note title |  | ||||||
|             loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)) || |  | ||||||
|             // React to changes in branches for subchildren (e.g., moved, added, or removed notes) |  | ||||||
|             loadResults.getBranchRows().some(branch => this.noteIds.includes(branch.noteId!)) || |  | ||||||
|             // React to changes in note icon or color. |  | ||||||
|             loadResults.getAttributeRows().some(attr => [ "iconClass", "color" ].includes(attr.name ?? "") && this.noteIds.includes(attr.noteId ?? "")) || |  | ||||||
|             // React to attachment change |  | ||||||
|             loadResults.getAttachmentRows().some(att => att.ownerId === this.parentNote.noteId && att.title === "board.json") || |  | ||||||
|             // React to changes in "groupBy" |  | ||||||
|             loadResults.getAttributeRows().some(attr => attr.name === "board:groupBy" && attr.noteId === this.parentNote.noteId); |  | ||||||
|  |  | ||||||
|         if (hasRelevantChanges && this.renderer) { |  | ||||||
|             // Use differential rendering with API refresh |  | ||||||
|             await this.renderer.renderBoard(true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Don't trigger full view refresh - let differential renderer handle it |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private onSave() { |     private onSave() { | ||||||
|         this.viewStorage.store(this.persistentData); |         this.viewStorage.store(this.persistentData); | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user