From 3a569499cbca84869c71717d19b44c8df887f3d6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 21 Jul 2025 11:26:38 +0300 Subject: [PATCH] feat(views/board): edit the note title inline on new --- .../board_view/differential_renderer.ts | 113 +++++++++++++++++- .../widgets/view_widgets/board_view/index.ts | 31 ++++- 2 files changed, 134 insertions(+), 10 deletions(-) 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 149574e1f..f79e5294b 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 @@ -183,6 +183,7 @@ export class DifferentialBoardRenderer { const item = newCards[i]; const noteId = item.note.noteId; let $existingCard = $cardContainer.find(`[data-note-id="${noteId}"]`); + const isNewCard = !oldCardIds.includes(noteId); if ($existingCard.length) { // Update existing card if title changed @@ -197,8 +198,8 @@ export class DifferentialBoardRenderer { // Ensure card is in correct position this.ensureCardPosition($existingCard, i, $cardContainer); } else { - // Create new card - const $newCard = this.createCard(item.note, item.branch, column); + // Create new card (pass isNewCard flag) + const $newCard = this.createCard(item.note, item.branch, column, isNewCard); $newCard.addClass('fade-in').css('opacity', '0'); // Insert at correct position @@ -264,7 +265,7 @@ export class DifferentialBoardRenderer { // Add cards for (const item of columnItems) { if (item.note) { - const $noteEl = this.createCard(item.note, item.branch, column); + const $noteEl = this.createCard(item.note, item.branch, column, false); // false = existing card $columnEl.append($noteEl); } } @@ -281,7 +282,7 @@ export class DifferentialBoardRenderer { return $columnEl; } - private createCard(note: any, branch: any, column: string): JQuery { + private createCard(note: any, branch: any, column: string, isNewCard: boolean = false): JQuery { const $iconEl = $("") .addClass("icon") .addClass(note.getIcon()); @@ -291,10 +292,15 @@ export class DifferentialBoardRenderer { .attr("data-note-id", note.noteId) .attr("data-branch-id", branch.branchId) .attr("data-current-column", column) + .attr("data-icon-class", note.getIcon()) .text(note.title); $noteEl.prepend($iconEl); - $noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId })); + + // Only add quick edit click handler for existing cards (not new ones) + if (!isNewCard) { + $noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId })); + } // Setup drag functionality this.dragHandler.setupNoteDrag($noteEl, note, branch); @@ -327,4 +333,101 @@ export class DifferentialBoardRenderer { await this.performUpdate(); } } + + startInlineEditing(noteId: string): void { + // Use setTimeout to ensure the card is rendered before trying to edit it + setTimeout(() => { + const $card = this.$container.find(`[data-note-id="${noteId}"]`); + if ($card.length) { + this.makeCardEditable($card, noteId); + } + }, 100); + } + + private makeCardEditable($card: JQuery, noteId: string): void { + if ($card.hasClass('editing')) { + return; // Already editing + } + + // Get the current title (get text without icon) + const $icon = $card.find('.icon'); + const currentTitle = $card.text().trim(); + + // Add editing class and store original click handler + $card.addClass('editing'); + $card.off('click'); // Remove any existing click handlers temporarily + + // Create input element + const $input = $('') + .attr('type', 'text') + .val(currentTitle) + .css({ + background: 'transparent', + border: 'none', + outline: 'none', + fontFamily: 'inherit', + fontSize: 'inherit', + color: 'inherit', + flex: '1', + minWidth: '0', + padding: '0', + marginLeft: '0.25em' + }); + + // Create a flex container to keep icon and input inline + const $editContainer = $('
') + .css({ + display: 'flex', + alignItems: 'center', + width: '100%' + }); + + // Replace content with icon + input in flex container + $editContainer.append($icon.clone(), $input); + $card.empty().append($editContainer); + $input.focus().select(); + + const finishEdit = async (save: boolean = true) => { + if (!$card.hasClass('editing')) { + return; // Already finished + } + + $card.removeClass('editing'); + + let finalTitle = currentTitle; + if (save) { + const newTitle = $input.val() as string; + if (newTitle.trim() && newTitle !== currentTitle) { + try { + // Update the note title using the board view's server call + import('../../../services/server').then(async ({ default: server }) => { + await server.put(`notes/${noteId}/title`, { title: newTitle.trim() }); + finalTitle = newTitle.trim(); + }); + } catch (error) { + console.error("Failed to update note title:", error); + } + } + } + + // Restore the card content + const iconClass = $card.attr('data-icon-class') || 'bx bx-file'; + const $newIcon = $('').addClass('icon').addClass(iconClass); + $card.empty().append($newIcon, finalTitle); + + // Re-attach click handler for quick edit (for existing cards) + $card.on('click', () => appContext.triggerCommand("openInPopup", { noteIdOrPath: noteId })); + }; + + $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); + } + }); + } } 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 3a7e5d885..60e347320 100644 --- a/apps/client/src/widgets/view_widgets/board_view/index.ts +++ b/apps/client/src/widgets/view_widgets/board_view/index.ts @@ -139,6 +139,22 @@ const TPL = /*html*/` 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; } @@ -270,7 +286,7 @@ export default class BoardView extends ViewMode { this.$container.empty(); await this.initializeRenderer(); } - + await this.renderer!.renderBoard(); return this.$root; } @@ -402,7 +418,8 @@ export default class BoardView extends ViewMode { // Create a new note as a child of the parent note const { note: newNote } = await noteCreateService.createNote(parentNotePath, { - activate: false + activate: false, + title: "New item" }); if (newNote) { @@ -412,14 +429,18 @@ export default class BoardView extends ViewMode { // Refresh the board to show the new item await this.renderList(); - // Optionally, open the new note for editing - appContext.triggerCommand("openInPopup", { noteIdOrPath: newNote.noteId }); + // Start inline editing of the newly created card + this.startInlineEditingCard(newNote.noteId); } } catch (error) { console.error("Failed to create new item:", error); } } + private startInlineEditingCard(noteId: string) { + this.renderer?.startInlineEditing(noteId); + } + forceFullRefresh() { this.renderer?.forceFullRender(); return this.renderList(); @@ -500,7 +521,7 @@ export default class BoardView extends ViewMode { async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { // Check if any changes affect our board - const hasRelevantChanges = + const hasRelevantChanges = // React to changes in "status" attribute for notes in this board loadResults.getAttributeRows().some(attr => attr.name === "status" && this.noteIds.includes(attr.noteId!)) || // React to changes in note title