The popup variable stays undefined when onStart bails out early because
props.clientRect is missing, so popup[0] throws instead of no-oping.
Use optional chaining on the array itself.
* feat: add ability to move boards between workspaces
Implements the "Move to workspace" feature (#344) allowing users to
relocate a board and all its contents (lists, cards, labels, checklists,
comments, activity) to a different workspace.
Key design decisions:
- Card member assignments are cleared on move (they reference
workspace-scoped members that may not exist in the target workspace)
- Comments and activity history are preserved (they reference global
user IDs, not workspace members)
- Slug conflicts in the target workspace are auto-resolved by
appending a UID suffix
- Permission model: requires board:edit in source workspace and
board:create in target workspace
- Templates and archived boards cannot be moved
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: consolidate board queries in move mutation
Address review feedback:
- Consolidate 3 separate board queries into a single findFirst()
that fetches all needed fields (id, name, slug, type, isArchived,
workspaceId, createdBy)
- Fix slug fallback to use board.name instead of publicId for
human-readable URLs
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: filter guest workspaces from move board destination list
Guests typically lack board:create permission in the target workspace,
so showing them as destinations leads to a confusing server rejection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract getBoardForMove repo function
Moves the inline board query from the move mutation into the repo
layer, consistent with how every other board mutation fetches data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add unit tests for board.move mutation
10 test cases covering auth, validation, permissions, slug conflict
resolution, and the happy path. Follows webhook.test.ts patterns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: mark locale files as linguist-generated
GitHub will now auto-collapse compiled translation files (messages.json,
messages.ts, messages.po) in PR diffs and exclude them from language
stats. This makes PRs that touch i18n strings much easier to review.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove locale file changes from PR
Reverts locale file diffs and .gitattributes to match main, per review
feedback. The locale changes were unrelated translation updates that
inflated the PR diff.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove locale file changes from PR
Per @hjball's review: locale compilation/translations are handled
automatically on merge to main, so this PR shouldn't carry them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: align locale files with upstream/main
Previous removal commit used local main, which had drifted from
upstream. Re-syncing to upstream/main so the PR carries no locale diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(board-move): tighten deletedAt handling per review
Three changes addressing @hjball's review comments, all about the
schema treating deletedAt as optional metadata while the move-board
flow needs it as a load-bearing invariant.
1. getBoardForMove now filters isNull(deletedAt). Moving a tombstoned
board has no defensible semantics. Replaces the implicit
"the board exists in the table" check with an explicit
"the board is not soft-deleted" check.
2. Move-flow's clearing of cardToWorkspaceMembers now spans every
card under every list ever associated with this board, including
soft-deleted ones. If we leave member assignments on a deleted
card and that card is later restored, the assignments would
resurrect rogue references to workspace members from the OLD
workspace. Removed the isNull filters on both lists and cards in
that loop.
3. Move-flow now refuses to move into a soft-deleted target
workspace. workspaceRepo.getByPublicId did not previously project
deletedAt; extended its column selection so the call-site guard
in board.move can check it. (A wider fix to make the repo treat
deleted-as-not-found across all 14+ callers is left for a
separate PR — narrow scope here.)
Plus one regression test: throws NOT_FOUND when target workspace is
soft-deleted. All 11 board-move tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The rich-text Editor (used for card descriptions) creates its Tiptap
instance once via useEditor with an empty dependency array. This means
the initial value of `editable: !readOnly` is captured at mount time
and never updated, and the onChange/onBlur callbacks are frozen as the
first-render closures.
In CardPage, the description Editor mounts as soon as the card query
resolves but before the permissions query resolves, so `readOnly` is
`true` and onChange/onBlur are `undefined` at that moment. When
permissions resolve a moment later and `canEdit` flips to `true`,
the Editor never becomes editable — leaving the description stuck
read-only even for workspace admins.
Fix mirrors the existing pattern in PlainTextEditor.tsx:
- Use refs for onChange/onBlur so the editor reads the latest values
- Add a useEffect that calls editor.setEditable(!readOnly) when the
readOnly prop changes
Co-authored-by: Jay <CodeEngineering@pm.me>
* feat: kan mcp server initial attempt
* fix: card was missing options and added default fallbacks
* fix: label creating with better information for colors and presets
The tiptap-markdown extension was registered with default config, where
`transformPastedText` defaults to false. As a result, pasting markdown
text into card descriptions resulted in literal characters (e.g. '# heading'
shown as a paragraph of text) rather than rendered formatting.
Typing markdown shortcuts in the editor already worked because that goes
through a different path. Programmatic writes via the API also work
because they bypass the editor entirely. Only paste was affected.
Enabling `transformPastedText: true` makes pasted markdown behave as
users intuitively expect, matching the typed-markdown UX.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The AWS SDK throws "Region is missing" when S3Client is constructed
with an empty-string region, so any S3 feature (avatars, attachments,
presigned URL generation) is silently broken when S3_REGION is unset
— even though .env.example ships it unset and env.ts declares it
optional. The schema and the runtime disagreed about whether the var
was required.
Default to "us-east-1" in createS3Client. S3-compatible providers
(MinIO, Backblaze B2, R2, DigitalOcean Spaces, Wasabi) ignore the
region entirely; real AWS S3 users should set S3_REGION explicitly
to their bucket's actual region (clarified in .env.example).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each workspace package had its exports map pointing `types` at
`./dist/...d.ts` while `default` pointed at `./src/...ts`. The dist
directory is a build artifact only refreshed when someone runs
`pnpm build` in that package — meaning every other package in the
monorepo typechecks against last week's type signatures.
Concrete failure mode: contributor edits a function in
packages/shared/src/utils/foo.ts (changes a parameter type, adds a
new export, etc.), runs `pnpm typecheck` from root, sees green. The
check actually validated against the stale dist/.d.ts. Real type
errors stay invisible until CI builds shared first, by which point
the diff is already pushed.
Fix: point `types` at the same `src/` paths the runtime resolves to.
TypeScript reads .ts source as types fine when consumers share the
same TS version, which a monorepo guarantees. Verified end-to-end:
adding a new export to shared and immediately typechecking @kan/api
now picks it up without rebuilding shared, and breaking a return
type immediately fails the consumer's typecheck.
Applied to @kan/api, @kan/db, @kan/logger, @kan/shared.
@kan/email is intentionally left as-is because its source is .tsx
(JSX) — pointing types at .tsx would force every consumer to enable
--jsx in their tsconfig, which is a worse cascade than the stale
dist problem we're solving.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>