Skip to content

feat(board): redesign card workflows and fix embeds - #63

Merged
cnjack merged 1 commit into
mainfrom
codex/card-actions-ui-package-fix
Jul 31, 2026
Merged

feat(board): redesign card workflows and fix embeds#63
cnjack merged 1 commit into
mainfrom
codex/card-actions-ui-package-fix

Conversation

@cnjack

@cnjack cnjack commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace inline card creation and the narrow side peek with focused Headless UI dialogs
  • make editable board-react embeds use the shared editor while preserving explicit read-only and host-intercepted paths
  • persist description, properties, relations, and parent links in the initial create write
  • use portable relative-path relation keys with safe legacy basename resolution
  • serialize per-card saves and fix the Web stale-metadata race for rapid consecutive edits
  • rebuild jtype-board-react as 0.1.1 with scoped portal CSS and a real Cloud-sized embed regression

Root cause

Cloud layout was bounded correctly. Package 0.1.0 routed every card open through its legacy read-only detail even though readOnly defaults to false. The shared editor was never mounted. Rapid Web field edits also reused a closed-over metadata snapshot before React rerendered.

Impact matrix

Area Decision Reason
Desktop Yes Shared local-vault board workflow and initial filesystem write
Web Yes Same card UI plus ref-backed cloud save metadata
Shared model/UI Yes Portable relation keys, quick create, focused detail
Web service/API No Existing save response already includes content; no endpoint or migration change
board-react Yes Public embed behavior, types, bundle, README, and example locks changed
CLI/MCP Yes Help/schema copy documents path-based parent references
Help/docs Yes English/Chinese workflow docs and implementation note updated
Release Yes Desktop/Web assets and checked-in package dist rebuilt; Cloud follow-up pins this commit

Acceptance criteria

  • New card captures title, Markdown description, status, priority, assignee, tags, and due date
  • Existing card opens a responsive focused detail dialog
  • Parent/sub-card relation is present in the first save
  • Failed create/sub-card operations preserve the draft and switching cards clears stale child state
  • Editable, explicit read-only, and host onCardOpen embed paths remain distinct
  • Portal styles stay scoped inside arbitrary hosts
  • Rapid Web edits compose on the newest content/hash
  • Legacy unique basename relations remain readable; ambiguous references are not guessed

Verification

  • npm run test:unit — 65 passed
  • npm run build — Desktop passed
  • cd services/jtype-web/frontend && npm run build — Web passed
  • cd packages/board-react && npm run build && npm test — 21 package unit + 3 embed E2E passed
  • npx playwright test --config playwright.board.config.ts --workers=1 — 16 passed
  • cargo check --manifest-path services/jtype-cli/Cargo.toml — passed
  • cargo check --manifest-path services/jtype-web/Cargo.toml — passed
  • Grok CLI final review — NO ACTIONABLE FINDINGS
  • Kimi CLI final review — NO ACTIONABLE FINDINGS

Documentation and release

  • No data migration or backend rollout is required.
  • packages/board-react/dist is rebuilt for 0.1.1.
  • Cloud will consume the exact reviewed git commit in a follow-up PR; npm publication remains a separate authorized release step.

Focused card detail

Summary by CodeRabbit

  • New Features

    • Added a focused card-creation dialog with workflow fields, Markdown notes, tags, and Command/Ctrl + Enter submission.
    • Improved card details, sub-card creation, relation handling, Markdown previews, and embedded board behavior.
    • Added read-only and custom card-open behavior for React embeds.
    • Improved dropdown styling and preservation of selected labels.
  • Bug Fixes

    • Improved save reliability during rapid card updates and prevented stale asynchronous results.
    • Improved path-based parent and dependency resolution, including legacy compatibility.
  • Documentation

    • Updated board, embed, CLI, and sub-card guidance.
  • Tests

    • Expanded end-to-end and unit coverage for embeds, card creation, persistence, and relations.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request redesigns board card creation and detail interactions across shared UI, desktop, web, and React embeds. It adds relation-aware card identity, queued persistence, markdown renderer injection, read-only/open interception behavior, path-based parent references, and expanded unit and E2E coverage.

Changes

Card actions redesign

Layer / File(s) Summary
Board contracts, identity, and lane behavior
shared/lib/board.ts, shared/components/board/types.ts, src/components/BoardView.tsx, tests/unit/boardDependencies.spec.ts
Adds stable relation keys, collision-aware dependency resolution, nullable lane computation, and initial card-creation patches across shared and desktop board logic.
Dialog, detail modal, and board controls
shared/components/board/BoardSurface.tsx, shared/components/board/CardCreateDialog.tsx, shared/components/board/BoardPeek.tsx, shared/components/board/controls.tsx
Replaces inline creation and side peek UI with dialogs, queues per-card updates, supports injected markdown rendering, improves relation and child-card handling, and adds portal styling support.
Web persistence and React embed integration
services/jtype-web/frontend/src/pages/WebBoardView.tsx, services/jtype-web/frontend/src/lib/boardCardWrites.ts, packages/board-react/src/*, tests/e2e/board-react-embed.spec.ts
Uses current metadata references for sequential saves, updates embed creation and open behavior, supports read-only details and host interception, and adds embed regression coverage.
Board behavior validation
tests/e2e/board-swimlanes.spec.ts, tests/fixtures/board-swimlanes.tsx, tests/unit/webBoardSaveQueue.spec.ts
Validates quick creation, failed creation recovery, parent persistence, renderer wiring, and sequential patch bases.
Documentation and command contracts
docs/implementation-notes/*, services/jtype-web/frontend/src/help/content/*, services/jtype-cli/src/main.rs, services/jtype-web/src/mcp/kanban_tools.rs
Documents card creation/detail behavior and updates CLI/MCP and help text for relative-path parent references with legacy basename compatibility.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • cnjack/jtype#47: Shares the embeddable board, detail interception, portal styling, and card identity integration points.
  • cnjack/jtype#53: Both update the frontend SaveDocumentResponse contract.
  • cnjack/jtype#62: Both modify board card creation and swimlane placement logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main work: redesigning board card workflows and fixing embed behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/card-actions-ui-package-fix

Warning

Tools execution failed with the following error:

Failed to run tools: 14 UNAVAILABLE: Connection dropped


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/board-swimlanes.spec.ts (1)

11-21: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

HarnessState.cards is missing a parent field used by the new sub-card test.

The type was extended with columnKey, assignee, due, notes, tags, but the new "sub-card creation persists the parent" test (Line 206) asserts parent: "roadmap/offline" against this same shape. Add parent?: string | null alongside the other new optional fields for consistency and IDE/type support.

🩹 Proposed fix
   cards: Array<{
     id: string;
     title: string;
     priority?: string | null;
     columnKey?: string;
     assignee?: string | null;
     due?: string | null;
     notes?: string;
     tags?: Array<{ label: string }>;
     swimlaneKey?: string | null;
+    parent?: string | null;
   }>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/board-swimlanes.spec.ts` around lines 11 - 21, Add an optional
nullable parent field to the HarnessState.cards element type, alongside
columnKey, assignee, due, notes, tags, and swimlaneKey, so the sub-card
persistence assertion is type-safe.
🧹 Nitpick comments (2)
shared/components/board/controls.tsx (1)

184-184: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded hex swatch fallback in shared code.

"#d6d3d1" is a literal color in shared/; prefer a token from shared/styles/tokens.css (or a semantic class on the swatch with the inline style applied only when o.color is set).

As per coding guidelines, shared components "must use semantic Tailwind classes only ... and never hardcode hex values".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/components/board/controls.tsx` at line 184, Update the swatch in the
option-rendering component around the color style so it no longer hardcodes
"`#d6d3d1`". Use the appropriate semantic Tailwind class or shared token from
tokens.css as the fallback, while retaining the inline background color only
when o.color is provided.

Source: Coding guidelines

shared/components/board/BoardPeek.tsx (1)

261-293: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicate reference-resolution logic.

This mirrors cardReferenceResolver in shared/lib/board.ts (same basename/suffix maps, same uniqueness gating) over a different shape. If the resolution rules ever change, the peek's labels and blockedCounts/childCardsByParent will silently disagree. Consider exporting a generic resolver from shared/lib/board.ts parameterized by a key extractor and reusing it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/components/board/BoardPeek.tsx` around lines 261 - 293, Replace the
duplicated dependency reference-resolution logic in BoardPeek’s
dependencyByBasename, dependencyBySuffix, and resolveDependencyValue with the
shared cardReferenceResolver from shared/lib/board.ts. Export or generalize
cardReferenceResolver to accept the dependency card shape via key extractors,
then reuse it here so dependency labels and relationship calculations follow the
same uniqueness and suffix rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md`:
- Line 18: Update the sub-card navigation wording to use the current card detail
dialog terminology instead of the legacy peek-panel UI. Change the English
article at
services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md:18
and the Chinese article at
services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.zh.md:18,
replacing “peek panel” and “侧边详情” respectively while preserving the navigation
instructions.

In `@services/jtype-web/frontend/src/lib/boardCardWrites.ts`:
- Around line 36-63: Serialize saveBoardCardPatch operations per relativePath
inside the board-card write module, ensuring each invocation rereads the latest
metadata and uses the completed prior save before applying its patch. Implement
per-path promise chaining or an equivalent internal queue, preserve the existing
null and metadata-update behavior, and update
services/jtype-web/frontend/src/pages/WebBoardView.tsx lines 381-396 only as
needed to rely on the internal serialization rather than caller ordering.

In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx`:
- Line 5: Update WebBoardView’s markdown imports to use the local frontend
lib/markdown wrapper, including both renderMarkdownToHtml and renderToContainer
if the wrapper exports them; first verify that the wrapper exists and exposes
these helpers, and retain the direct shared import only if the wrapper is absent
or does not provide the required API.
- Around line 469-470: Update the remaining metadata lookups in the actions
object—deleteCards, duplicateCard, and deleteColumn—to read from
metaByPathRef.current instead of the stale metaByPath state. Preserve each
action’s existing behavior while ensuring rapid consecutive mutations use the
latest metadata.

In `@shared/components/board/BoardSurface.tsx`:
- Around line 1404-1436: Update the detail action handlers in BoardSurface’s
selected-card props so readOnly disables every mutation affordance: guard
onChange and onDelete the same way as onAddChild, while preserving their
existing behavior when readOnly is false. Ensure read-only details cannot edit
or delete cards even when rendered through peekComponent without onCardOpen.

In `@shared/components/board/CardCreateDialog.tsx`:
- Around line 60-73: Update the reset effect in CardCreateDialog so it runs only
on the dialog’s closed-to-open transition, not when initialStatus,
initialPriority, or initialAssignee changes while already open. Track the
previous open state or use an equivalent open-transition guard, while preserving
the existing field resets, submission-state cleanup, and title focus behavior.

---

Outside diff comments:
In `@tests/e2e/board-swimlanes.spec.ts`:
- Around line 11-21: Add an optional nullable parent field to the
HarnessState.cards element type, alongside columnKey, assignee, due, notes,
tags, and swimlaneKey, so the sub-card persistence assertion is type-safe.

---

Nitpick comments:
In `@shared/components/board/BoardPeek.tsx`:
- Around line 261-293: Replace the duplicated dependency reference-resolution
logic in BoardPeek’s dependencyByBasename, dependencyBySuffix, and
resolveDependencyValue with the shared cardReferenceResolver from
shared/lib/board.ts. Export or generalize cardReferenceResolver to accept the
dependency card shape via key extractors, then reuse it here so dependency
labels and relationship calculations follow the same uniqueness and suffix
rules.

In `@shared/components/board/controls.tsx`:
- Line 184: Update the swatch in the option-rendering component around the color
style so it no longer hardcodes "`#d6d3d1`". Use the appropriate semantic Tailwind
class or shared token from tokens.css as the fallback, while retaining the
inline background color only when o.color is provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 62bde8cd-7b05-420d-bb21-3fc898e91f7b

📥 Commits

Reviewing files that changed from the base of the PR and between 79835f5 and eff1d85.

⛔ Files ignored due to path filters (18)
  • docs/implementation-notes/card-actions-ui-redesign.png is excluded by !**/*.png
  • packages/board-react/dist/index.d.ts is excluded by !**/dist/**
  • packages/board-react/dist/index.js is excluded by !**/dist/**
  • packages/board-react/dist/style.css is excluded by !**/dist/**
  • packages/board-react/example/package-lock.json is excluded by !**/package-lock.json
  • packages/board-react/package-lock.json is excluded by !**/package-lock.json
  • services/jtype-web/frontend/src/i18n/locales/en/messages.po is excluded by !**/i18n/locales/**
  • services/jtype-web/frontend/src/i18n/locales/ja/messages.po is excluded by !**/i18n/locales/**
  • services/jtype-web/frontend/src/i18n/locales/ko/messages.po is excluded by !**/i18n/locales/**
  • services/jtype-web/frontend/src/i18n/locales/zh/messages.po is excluded by !**/i18n/locales/**
  • shared/i18n/locales/en/messages.mjs is excluded by !**/i18n/locales/**
  • shared/i18n/locales/en/messages.po is excluded by !**/i18n/locales/**
  • shared/i18n/locales/ja/messages.mjs is excluded by !**/i18n/locales/**
  • shared/i18n/locales/ja/messages.po is excluded by !**/i18n/locales/**
  • shared/i18n/locales/ko/messages.mjs is excluded by !**/i18n/locales/**
  • shared/i18n/locales/ko/messages.po is excluded by !**/i18n/locales/**
  • shared/i18n/locales/zh/messages.mjs is excluded by !**/i18n/locales/**
  • shared/i18n/locales/zh/messages.po is excluded by !**/i18n/locales/**
📒 Files selected for processing (33)
  • docs/implementation-notes/card-actions-ui-redesign.md
  • packages/board-react/README.md
  • packages/board-react/package.json
  • packages/board-react/src/CardDetail.tsx
  • packages/board-react/src/JTypeBoard.tsx
  • packages/board-react/src/boardData.ts
  • playwright.board.config.ts
  • services/jtype-cli/src/main.rs
  • services/jtype-web/frontend/src/api.ts
  • services/jtype-web/frontend/src/help/content/articles/kanban-boards-and-cards.en.md
  • services/jtype-web/frontend/src/help/content/articles/kanban-boards-and-cards.ts
  • services/jtype-web/frontend/src/help/content/articles/kanban-boards-and-cards.zh.md
  • services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md
  • services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.zh.md
  • services/jtype-web/frontend/src/lib/boardCardWrites.ts
  • services/jtype-web/frontend/src/pages/WebBoardView.tsx
  • services/jtype-web/src/mcp/kanban_tools.rs
  • shared/components/board/BoardPeek.tsx
  • shared/components/board/BoardSurface.tsx
  • shared/components/board/CardCreateDialog.tsx
  • shared/components/board/SwimlaneDeleteDialog.tsx
  • shared/components/board/controls.tsx
  • shared/components/board/index.ts
  • shared/components/board/types.ts
  • shared/lib/board.ts
  • src/components/BoardView.tsx
  • tests/e2e/board-react-embed.spec.ts
  • tests/e2e/board-swimlanes.spec.ts
  • tests/fixtures/board-react-embed.html
  • tests/fixtures/board-react-embed.tsx
  • tests/fixtures/board-swimlanes.tsx
  • tests/unit/boardDependencies.spec.ts
  • tests/unit/webBoardSaveQueue.spec.ts


You rarely type this by hand:

- In a card's peek panel, pick a **Parent** from the dropdown.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update sub-card navigation wording to match the dialog redesign.

The implementation note says existing cards open a wide detail dialog, but these localized articles still direct users to the old side-peek UI.

  • services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md#L18-L18: replace “peek panel” with “card detail dialog” or the current UI name.
  • services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.zh.md#L18-L18: replace 侧边详情 with the current card-detail-dialog terminology.
📍 Affects 2 files
  • services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md#L18-L18 (this comment)
  • services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.zh.md#L18-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md`
at line 18, Update the sub-card navigation wording to use the current card
detail dialog terminology instead of the legacy peek-panel UI. Change the
English article at
services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.en.md:18
and the Chinese article at
services/jtype-web/frontend/src/help/content/articles/kanban-sub-cards-and-batch-edits.zh.md:18,
replacing “peek panel” and “侧边详情” respectively while preserving the navigation
instructions.

Comment on lines +36 to +63
export async function saveBoardCardPatch(
metaRef: WebBoardCardMetaRef,
relativePath: string,
patch: Partial<BoardViewCard>,
saveDocument: SaveCardDocument,
): Promise<Map<string, WebBoardCardMeta> | null> {
const meta = metaRef.current.get(relativePath)
if (!meta) return null

const content = applyBoardCardPatch(meta.content, patch)
const saved = await saveDocument({
relativePath,
content,
baseContentHash: meta.contentHash,
baseContent: meta.content,
})
const currentMeta = metaRef.current.get(relativePath)
if (!currentMeta) return null

const nextMeta = new Map(metaRef.current)
nextMeta.set(relativePath, {
...currentMeta,
content: saved.content,
contentHash: saved.contentHash,
})
metaRef.current = nextMeta
return nextMeta
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'services/jtype-web/frontend/src/(lib/boardCardWrites\.ts|pages/WebBoardView\.tsx|.*CardDetail|.*BoardSurface)' || true

echo
echo "boardCardWrites outline/content:"
if [ -f services/jtype-web/frontend/src/lib/boardCardWrites.ts ]; then
  wc -l services/jtype-web/frontend/src/lib/boardCardWrites.ts
  cat -n services/jtype-web/frontend/src/lib/boardCardWrites.ts
fi

echo
echo "WebBoardView relevant section:"
if [ -f services/jtype-web/frontend/src/pages/WebBoardView.tsx ]; then
  wc -l services/jtype-web/frontend/src/pages/WebBoardView.tsx
  sed -n '340,420p' services/jtype-web/frontend/src/pages/WebBoardView.tsx | cat -n
fi

echo
echo "Search updateCard/saveBoardCardPatch usages:"
rg -n "updateCard|saveBoardCardPatch|setActions|actions\.updateCard|CardDetail|BoardSurface" services/jtype-web/frontend/src -S || true

Repository: cnjack/jtype

Length of output: 1913


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Current directory:"
pwd

echo "Files:"
git ls-files 2>/dev/null | rg 'services/jtype-web/frontend/src/(lib/boardCardWrites\.ts|pages/WebBoardView\.tsx|.*CardDetail|.*BoardSurface)' || true

echo
echo "boardCardWrites outline/content:"
if [ -f services/jtype-web/frontend/src/lib/boardCardWrites.ts ]; then
  wc -l services/jtype-web/frontend/src/lib/boardCardWrites.ts
  cat -n services/jtype-web/frontend/src/lib/boardCardWrites.ts
fi

echo
echo "WebBoardView relevant section:"
if [ -f services/jtype-web/frontend/src/pages/WebBoardView.tsx ]; then
  wc -l services/jtype-web/frontend/src/pages/WebBoardView.tsx
  sed -n '340,420p' services/jtype-web/frontend/src/pages/WebBoardView.tsx | cat -n
fi

echo
echo "Search updateCard/saveBoardCardPatch usages:"
rg -n "updateCard|saveBoardCardPatch|setActions|actions\.updateCard|CardDetail|BoardSurface" services/jtype-web/frontend/src -S || true

Repository: cnjack/jtype

Length of output: 1913


Avoid relying on callers to serialize per-card saves. saveBoardCardPatch optimistically reads meta.contentHash/meta.content, then writes with those stale base values if two patches for the same relativePath can start before the first save awaits. Serialize these saves inside saveBoardCardPatch per path so correctness doesn’t depend on BoardSurface/CardDetail callers always awaiting one update before firing the next.

📍 Affects 2 files
  • services/jtype-web/frontend/src/lib/boardCardWrites.ts#L36-L63 (this comment)
  • services/jtype-web/frontend/src/pages/WebBoardView.tsx#L381-L396
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/jtype-web/frontend/src/lib/boardCardWrites.ts` around lines 36 - 63,
Serialize saveBoardCardPatch operations per relativePath inside the board-card
write module, ensuring each invocation rereads the latest metadata and uses the
completed prior save before applying its patch. Implement per-path promise
chaining or an equivalent internal queue, preserve the existing null and
metadata-update behavior, and update
services/jtype-web/frontend/src/pages/WebBoardView.tsx lines 381-396 only as
needed to rely on the internal serialization rather than caller ordering.

import { BoardSettingsDialog } from '../components/BoardSettingsDialog'
import { useConfirm, usePrompt } from '@shared/components/PromptDialogContext'
import { parseFrontmatter, writeFrontmatter } from '@shared/lib/frontmatter'
import { renderMarkdownToHtml, renderToContainer } from '@shared/lib/markdown'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Markdown helpers imported directly from @shared/lib/markdown, bypassing the local wrapper.

Per coding guidelines, web-service markdown rendering must import renderMarkdownToHtml from the local services/jtype-web/frontend/src/lib/markdown.ts (kept synchronized with desktop's src/lib/markdown.ts), not directly from @shared/lib/markdown. Please confirm whether that local wrapper still exists/is the intended import path, or whether the guideline is stale now that markdown lives in shared/lib/.

#!/bin/bash
# Check whether the local web markdown wrapper still exists and what other web files import.
fd markdown.ts services/jtype-web/frontend/src/lib
rg -nP "from '(`@shared/lib/markdown`|\.\./lib/markdown|\./lib/markdown)'" services/jtype-web/frontend/src
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx` at line 5, Update
WebBoardView’s markdown imports to use the local frontend lib/markdown wrapper,
including both renderMarkdownToHtml and renderToContainer if the wrapper exports
them; first verify that the wrapper exists and exposes these helpers, and retain
the direct shared import only if the wrapper is absent or does not provide the
required API.

Comment on lines 469 to +470
deleteCard: async (card) => {
const meta = metaByPath.get(card.id)
const meta = metaByPathRef.current.get(card.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Sibling mutation paths still use stale metaByPath state instead of the ref.

deleteCard here now reads metaByPathRef.current for freshness, but deleteCards, duplicateCard, and deleteColumn (further below in this same actions object) still call metaByPath.get(...) — the very state staleness this migration was meant to fix. Rapid consecutive actions before React commits metaByPath can still read outdated content for these three paths.

♻️ Apply the same fix to the remaining sites
-      deleteCards: async (cardsToDelete) => {
-        ...
-          const meta = metaByPath.get(card.id)
+      deleteCards: async (cardsToDelete) => {
+        ...
+          const meta = metaByPathRef.current.get(card.id)

(same swap for duplicateCard and deleteColumn)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx` around lines 469 -
470, Update the remaining metadata lookups in the actions object—deleteCards,
duplicateCard, and deleteColumn—to read from metaByPathRef.current instead of
the stale metaByPath state. Preserve each action’s existing behavior while
ensuring rapid consecutive mutations use the latest metadata.

Comment on lines +1404 to +1436
onAddChild={
readOnly
? undefined
: async (title) => {
await cardUpdateQueues.current.get(selected.id);
// Status sub-cards enter the first workflow state; other
// dimensions keep the new card beside its parent.
const startLane =
groupKey === "status"
? config.columns[0]?.key ?? selected.columnKey
: boardLaneValueOf(selected, config);
await actions.createCard(startLane, title, {
parent: cardRelationKey(selected),
});
}
}
loadNotes={loadNotes}
onUploadAttachment={onUploadAttachment}
loadComments={loadComments}
addComment={addComment}
updateComment={updateComment}
deleteComment={deleteComment}
toggleReaction={toggleReaction}
resolveComment={resolveComment}
currentUser={currentUser}
loadActivity={loadActivity}
renderMarkdownToContainer={renderMarkdownToContainer}
renderMarkdownToHtml={renderMarkdownToHtml}
portalClassName={portalClassName}
onChange={(patch) => void queueCardUpdate(selected.id, patch)}
onClose={() => setSelectedId(null)}
onDelete={() => void actions.deleteCard(selected)}
onOpenFull={actions.openCardFull ? () => actions.openCardFull?.(selected) : undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

readOnly only gates onAddChild; edits and delete still reach the detail.

onAddChild is disabled under readOnly, but onChange and onDelete are wired unconditionally, so a read-only host that supplies peekComponent without onCardOpen gets a fully editable/deletable detail — contrary to the documented readOnly contract ("hides every mutation affordance").

🛡️ Suggested guard
-                onChange={(patch) => void queueCardUpdate(selected.id, patch)}
+                onChange={readOnly ? () => {} : (patch) => void queueCardUpdate(selected.id, patch)}
                 onClose={() => setSelectedId(null)}
-                onDelete={() => void actions.deleteCard(selected)}
+                onDelete={readOnly ? () => {} : () => void actions.deleteCard(selected)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onAddChild={
readOnly
? undefined
: async (title) => {
await cardUpdateQueues.current.get(selected.id);
// Status sub-cards enter the first workflow state; other
// dimensions keep the new card beside its parent.
const startLane =
groupKey === "status"
? config.columns[0]?.key ?? selected.columnKey
: boardLaneValueOf(selected, config);
await actions.createCard(startLane, title, {
parent: cardRelationKey(selected),
});
}
}
loadNotes={loadNotes}
onUploadAttachment={onUploadAttachment}
loadComments={loadComments}
addComment={addComment}
updateComment={updateComment}
deleteComment={deleteComment}
toggleReaction={toggleReaction}
resolveComment={resolveComment}
currentUser={currentUser}
loadActivity={loadActivity}
renderMarkdownToContainer={renderMarkdownToContainer}
renderMarkdownToHtml={renderMarkdownToHtml}
portalClassName={portalClassName}
onChange={(patch) => void queueCardUpdate(selected.id, patch)}
onClose={() => setSelectedId(null)}
onDelete={() => void actions.deleteCard(selected)}
onOpenFull={actions.openCardFull ? () => actions.openCardFull?.(selected) : undefined}
onAddChild={
readOnly
? undefined
: async (title) => {
await cardUpdateQueues.current.get(selected.id);
// Status sub-cards enter the first workflow state; other
// dimensions keep the new card beside its parent.
const startLane =
groupKey === "status"
? config.columns[0]?.key ?? selected.columnKey
: boardLaneValueOf(selected, config);
await actions.createCard(startLane, title, {
parent: cardRelationKey(selected),
});
}
}
loadNotes={loadNotes}
onUploadAttachment={onUploadAttachment}
loadComments={loadComments}
addComment={addComment}
updateComment={updateComment}
deleteComment={deleteComment}
toggleReaction={toggleReaction}
resolveComment={resolveComment}
currentUser={currentUser}
loadActivity={loadActivity}
renderMarkdownToContainer={renderMarkdownToContainer}
renderMarkdownToHtml={renderMarkdownToHtml}
portalClassName={portalClassName}
onChange={readOnly ? () => {} : (patch) => void queueCardUpdate(selected.id, patch)}
onClose={() => setSelectedId(null)}
onDelete={readOnly ? () => {} : () => void actions.deleteCard(selected)}
onOpenFull={actions.openCardFull ? () => actions.openCardFull?.(selected) : undefined}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/components/board/BoardSurface.tsx` around lines 1404 - 1436, Update
the detail action handlers in BoardSurface’s selected-card props so readOnly
disables every mutation affordance: guard onChange and onDelete the same way as
onAddChild, while preserving their existing behavior when readOnly is false.
Ensure read-only details cannot edit or delete cards even when rendered through
peekComponent without onCardOpen.

Comment on lines +60 to +73
useEffect(() => {
if (!open) return;
setTitle("");
setNotes("");
setStatus(initialStatus);
setPriority(initialPriority);
setAssignee(initialAssignee);
setDue("");
setTags([]);
setTagText("");
setSubmitting(false);
setSubmitError("");
window.setTimeout(() => titleRef.current?.focus(), 0);
}, [initialAssignee, initialPriority, initialStatus, open]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset also fires on initial* changes while the dialog is open.

The effect depends on initialStatus/initialPriority/initialAssignee, which in BoardSurface are derived from config.columns and addingIn. A background board reload that changes the first column key would clear a half-typed title/description. Gate the reset on the open transition instead.

♻️ Reset only when the dialog opens
+  const wasOpen = useRef(false);
   useEffect(() => {
-    if (!open) return;
+    if (!open) {
+      wasOpen.current = false;
+      return;
+    }
+    if (wasOpen.current) return;
+    wasOpen.current = true;
     setTitle("");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!open) return;
setTitle("");
setNotes("");
setStatus(initialStatus);
setPriority(initialPriority);
setAssignee(initialAssignee);
setDue("");
setTags([]);
setTagText("");
setSubmitting(false);
setSubmitError("");
window.setTimeout(() => titleRef.current?.focus(), 0);
}, [initialAssignee, initialPriority, initialStatus, open]);
const wasOpen = useRef(false);
useEffect(() => {
if (!open) {
wasOpen.current = false;
return;
}
if (wasOpen.current) return;
wasOpen.current = true;
setTitle("");
setNotes("");
setStatus(initialStatus);
setPriority(initialPriority);
setAssignee(initialAssignee);
setDue("");
setTags([]);
setTagText("");
setSubmitting(false);
setSubmitError("");
window.setTimeout(() => titleRef.current?.focus(), 0);
}, [initialAssignee, initialPriority, initialStatus, open]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/components/board/CardCreateDialog.tsx` around lines 60 - 73, Update
the reset effect in CardCreateDialog so it runs only on the dialog’s
closed-to-open transition, not when initialStatus, initialPriority, or
initialAssignee changes while already open. Track the previous open state or use
an equivalent open-transition guard, while preserving the existing field resets,
submission-state cleanup, and title focus behavior.

@cnjack
cnjack merged commit c2f9d48 into main Jul 31, 2026
3 checks passed
@cnjack
cnjack deleted the codex/card-actions-ui-package-fix branch July 31, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant