490 lines
49 KiB
Plaintext
490 lines
49 KiB
Plaintext
## Codebase Patterns
|
|
- Project uses Next.js 16 with App Router, TypeScript, and TailwindCSS 4
|
|
- Source files are in `src/` directory (app, components, lib, types)
|
|
- Supabase is configured with @supabase/supabase-js and @supabase/ssr packages
|
|
- Environment variables follow NEXT_PUBLIC_* convention for client-side access
|
|
- Use `npm run typecheck` to run TypeScript type checking (tsc --noEmit)
|
|
- Flowchart types exported from `src/types/flowchart.ts`
|
|
- Supabase migrations go in `supabase/migrations/` with timestamp prefix (YYYYMMDDHHMMSS_*.sql)
|
|
- Database has profiles table (linked to auth.users) and projects table (with flowchart_data JSONB)
|
|
- RLS policies enforce user_id = auth.uid() for project access
|
|
- Supabase client utilities in `src/lib/supabase/`: client.ts (browser), server.ts (App Router), middleware.ts (route protection)
|
|
- Next.js middleware.ts at project root handles route protection using updateSession helper
|
|
- Public auth routes: /login, /signup, /forgot-password, /reset-password
|
|
- Protected routes: /dashboard, /editor/* (redirect to /login if unauthenticated)
|
|
- Auth pages use 'use client' with useState, createClient() from lib/supabase/client.ts, and useRouter for redirects
|
|
- For lists with client-side updates (delete/add), use wrapper client component that receives initialData from server component
|
|
- Toast component in `src/components/Toast.tsx` for success/error notifications (auto-dismiss after 3s)
|
|
- Admin operations use SUPABASE_SERVICE_ROLE_KEY (server-side only via server actions)
|
|
- Admin users have is_admin=true in profiles table; check via .select('is_admin').eq('id', user.id).single()
|
|
- React Flow editor is in `src/app/editor/[projectId]/` with page.tsx (server) and FlowchartEditor.tsx (client)
|
|
- React Flow requires 'use client' and importing 'reactflow/dist/style.css'
|
|
- Use toReactFlowNodes/toReactFlowEdges helpers to convert app types to React Flow types
|
|
- Custom node components go in `src/components/editor/nodes/` with NodeProps<T> typing and useReactFlow() for updates
|
|
- Register custom node types in nodeTypes object (memoized with useMemo) and pass to ReactFlow component
|
|
- FlowchartEditor uses ReactFlowProvider wrapper + inner component pattern for useReactFlow() hook access
|
|
- Use nanoid for generating unique node IDs (import from 'nanoid')
|
|
- Reusable LoadingSpinner component in `src/components/LoadingSpinner.tsx` with size ('sm'|'md'|'lg') and optional message
|
|
- Toast component supports an optional `action` prop: `{ label: string; onClick: () => void }` for retry/undo buttons
|
|
- Settings page at `/dashboard/settings` reuses dashboard layout; re-auth via signInWithPassword before updateUser
|
|
- Character/Variable types (`Character`, `Variable`) and extracted node data types (`DialogueNodeData`, `VariableNodeData`) are in `src/types/flowchart.ts`
|
|
- `EditorContext` at `src/components/editor/EditorContext.tsx` provides shared state (characters, onAddCharacter) to all custom node components via React context
|
|
- Use `useEditorContext()` in node components to access project-level characters and variables without prop drilling through React Flow node data
|
|
- New JSONB fields (characters, variables) must be defaulted to `[]` when reading from DB in page.tsx to handle pre-existing data
|
|
- Reusable `Combobox` component at `src/components/editor/Combobox.tsx` - use for all character/variable dropdowns. Props: items (ComboboxItem[]), value, onChange, placeholder, onAddNew
|
|
- `ProjectSettingsModal` at `src/components/editor/ProjectSettingsModal.tsx` manages characters/variables. Receives state + callbacks from FlowchartEditor
|
|
- Characters and variables state is managed in `FlowchartEditorInner` with `useState` hooks, passed down to the modal
|
|
- For settings-style modals, use `max-w-2xl h-[80vh]` with overflow-y-auto content area and fixed header/tabs
|
|
- `EditorContext` provides both characters (onAddCharacter) and variables (onAddVariable) to node components. Use `useEditorContext()` to access them.
|
|
- In FlowchartEditor, `handleAddVariable` adds a variable *node* to the canvas; `handleAddVariableDefinition` creates a variable *definition* in project data. Avoid naming collisions between "add node" and "add definition" callbacks.
|
|
- Edge interactions use `onEdgeClick` on ReactFlow component. ConditionEditor opens as a modal overlay since React Flow edges don't support inline panels.
|
|
- `Condition.value` supports `number | string | boolean` — always check variable type before rendering value inputs for edge conditions.
|
|
- `OptionConditionEditor` at `src/components/editor/OptionConditionEditor.tsx` handles choice option conditions. Same pattern as `ConditionEditor` but with simpler props (no edgeId).
|
|
- `ChoiceOption` type includes optional `condition?: Condition`. When counting variable usage, check variable nodes + edge conditions + choice option conditions.
|
|
- React Compiler lint forbids `setState` in effects and reading `useRef().current` during render. Use `useState(() => computeValue())` lazy initializer pattern for one-time initialization logic.
|
|
- For detecting legacy data shape (pre-migration), pass a flag from the server component (page.tsx) to the client component, since only the server reads raw DB data.
|
|
- Collaboration tables: `project_collaborators` (roles), `collaboration_sessions` (presence), `audit_trail` (history) — all with RLS scoped by project ownership or collaborator membership
|
|
- RLS pattern for shared resources: check `projects.user_id = auth.uid()` OR `project_collaborators.user_id = auth.uid()` to cover both owners and collaborators
|
|
- `RealtimeConnection` class at `src/lib/collaboration/realtime.ts` manages Supabase Realtime channel lifecycle (connect, heartbeat, reconnect, disconnect). Instantiate with (projectId, userId, callbacks).
|
|
- FlowchartEditor receives `userId` prop from page.tsx server component for collaboration features
|
|
- Toolbar accepts optional `connectionState` prop to show green/yellow/red connection indicator
|
|
- `collaboration_sessions` table has UNIQUE(project_id, user_id) constraint to support upsert-based session management
|
|
- Server actions for project-specific operations go in `src/app/editor/[projectId]/actions.ts` — use `'use server'` directive and return `{ success: boolean; error?: string }` pattern
|
|
- Editor page.tsx supports both owner and collaborator access: first checks ownership, then falls back to `project_collaborators` lookup. Pass `isOwner` prop to client component.
|
|
- `ShareModal` at `src/components/editor/ShareModal.tsx` manages collaborator invites/roles/removal via server actions. Only owners see invite form.
|
|
- Dashboard shared projects use Supabase join query: `project_collaborators.select('role, projects(id, name, updated_at)')` to fetch projects shared with the user
|
|
- `ProjectCard` supports optional `shared` and `sharedRole` props — when `shared=true`, hide edit/delete buttons and show role badge instead
|
|
- `PresenceAvatars` at `src/components/editor/PresenceAvatars.tsx` renders connected collaborator avatars. Receives `PresenceUser[]` from `RealtimeConnection.onPresenceSync`.
|
|
- `RealtimeConnection` constructor takes `(projectId, userId, displayName, callbacks)` — `displayName` is broadcast via Supabase Realtime presence tracking
|
|
- User color for presence is derived from a hash of their userId, ensuring consistency across sessions. Use `getUserColor(userId)` pattern from PresenceAvatars.
|
|
- `CRDTManager` at `src/lib/collaboration/crdt.ts` wraps a Yjs Y.Doc with Y.Map<string> for nodes and edges. Connects to Supabase Realtime channel for broadcasting updates.
|
|
- CRDT sync pattern: local React Flow changes → `updateNodes`/`updateEdges` on CRDTManager → Yjs broadcasts to channel; remote broadcasts → Yjs applies update → callbacks set React Flow state. Use `isRemoteUpdateRef` to prevent echo loops.
|
|
- For Supabase Realtime broadcast of binary data (Yjs updates), convert `Uint8Array` → `Array.from()` for JSON payload, and `new Uint8Array()` on receive.
|
|
- For ephemeral real-time data (cursors, typing indicators), use Supabase Realtime broadcast (`channel.send({ type: 'broadcast', event, payload })`) + `.on('broadcast', { event }, callback)` — not persistence-backed
|
|
- `RemoteCursors` at `src/components/editor/RemoteCursors.tsx` renders collaborator cursors on canvas. Uses `useViewport()` to transform flow→screen coordinates. Throttle broadcasts to 50ms via timestamp ref.
|
|
- Supabase Realtime presence events: `sync` (full state), `join` (arrivals with `newPresences` array), `leave` (departures with `leftPresences` array). Filter `this.userId` to skip own events.
|
|
- `CollaborationToast` at `src/components/editor/CollaborationToast.tsx` shows join/leave notifications (bottom-left, auto-dismiss 3s). Uses `getUserColor(userId)` for accent color dot.
|
|
- Node lock indicators use `EditorContext` (`nodeLocks` Map, `onNodeFocus`, `onNodeBlur`). Each node component checks `nodeLocks.get(id)` for lock state and renders `NodeLockIndicator` + overlay if locked by another user.
|
|
- For ephemeral lock state (node editing locks), broadcast via `node-lock` event with `{ nodeId, userId, displayName, lockedAt }`. Send `nodeId: null` to release.
|
|
- `AuditTrailRecorder` at `src/lib/collaboration/auditTrail.ts` records node/edge changes to `audit_trail` table. Uses state diffing (previous vs current Maps), 1-second per-entity debounce, and fire-and-forget Supabase inserts. Only records local changes (guarded by `isRemoteUpdateRef` in FlowchartEditor).
|
|
- `ActivityHistorySidebar` at `src/components/editor/ActivityHistorySidebar.tsx` displays audit trail entries in a right sidebar. Rendered inside the canvas `relative flex-1` container. Toggle via `showHistory` state in FlowchartEditor.
|
|
- For async data fetching in components with React Compiler, use a pure fetch function returning `{ data, error, hasMore }` result object, then handle setState in the `.then()` callback with an abort/mount guard — never call setState-containing functions directly inside useEffect.
|
|
|
|
---
|
|
|
|
## 2026-01-23 - US-054
|
|
- What was implemented: Character and Variable TypeScript types added to `src/types/flowchart.ts`
|
|
- Files changed:
|
|
- `src/types/flowchart.ts` - Added `Character`, `Variable`, `DialogueNodeData`, `VariableNodeData` types; updated `FlowchartData`, `DialogueNode`, `VariableNode`, `Condition` types
|
|
- `src/app/editor/[projectId]/page.tsx` - Updated FlowchartData initialization to include `characters: []` and `variables: []` defaults
|
|
- **Learnings for future iterations:**
|
|
- The node components (`DialogueNode.tsx`, `VariableNode.tsx`, `ChoiceNode.tsx`) define their own local data types that mirror the global types. When adding fields, both the global type and local component type may need updating in later stories.
|
|
- `flowchart_data` is a JSONB column in Supabase, so it comes as `any` type. Always provide defaults for new fields when reading from DB to handle existing data without those fields.
|
|
- The new `characterId` and `variableId` fields are optional alongside existing `speaker`/`variableName` fields to support migration from free-text to referenced-entity pattern.
|
|
---
|
|
|
|
## 2026-01-23 - US-055
|
|
- What was implemented: Database migration to update flowchart_data JSONB default to include `characters: []` and `variables: []`
|
|
- Files changed:
|
|
- `supabase/migrations/20260123000000_add_characters_variables_to_flowchart_data.sql` - New migration that alters the default value for the flowchart_data column and documents the expected JSONB structure
|
|
- **Learnings for future iterations:**
|
|
- Since characters and variables are stored within the existing flowchart_data JSONB column (not as separate tables), schema changes are minimal - just updating the column default. The real data integrity is handled at the application layer.
|
|
- The app-side defaults in page.tsx (from US-054) already handle existing projects gracefully, so no data migration of existing rows is needed.
|
|
- For JSONB-embedded arrays, the pattern is: update the DB default for new rows + handle missing fields in app code for old rows.
|
|
---
|
|
|
|
## 2026-01-23 - US-065
|
|
- What was implemented: Reusable searchable combobox component at `src/components/editor/Combobox.tsx`
|
|
- Files changed:
|
|
- `src/components/editor/Combobox.tsx` - New component with searchable dropdown, keyboard navigation, color swatches, badges, "Add new..." option, and auto-positioning
|
|
- **Learnings for future iterations:**
|
|
- The Combobox exports both the default component and the `ComboboxItem` type for consumers to use
|
|
- Props: `items` (ComboboxItem[]), `value` (string | undefined), `onChange` (id: string) => void, `placeholder` (string), `onAddNew` (() => void, optional)
|
|
- ComboboxItem shape: `{ id: string, label: string, color?: string, badge?: string }`
|
|
- The component uses neutral zinc colors for borders/backgrounds (not blue/green/orange) so it can be reused across different node types
|
|
- Dropdown auto-positions above or below based on available viewport space (200px threshold)
|
|
- Keyboard: ArrowDown/Up navigate, Enter selects, Escape closes
|
|
- The component is designed to be a drop-in replacement for text inputs in node components (same `w-full` and `text-sm` sizing)
|
|
---
|
|
|
|
## 2026-01-23 - US-056
|
|
- What was implemented: Character management UI in the project settings modal
|
|
- Files changed:
|
|
- `src/components/editor/ProjectSettingsModal.tsx` - New modal component with Characters and Variables tabs; Characters tab has full CRUD (add, edit, delete with usage warnings), name uniqueness validation, color picker, inline forms
|
|
- `src/components/editor/Toolbar.tsx` - Added `onProjectSettings` prop and "Project Settings" button to the right side of the toolbar
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `characters` and `variables` state management, `showSettings` modal state, usage count helpers (`getCharacterUsageCount`, `getVariableUsageCount`), and ProjectSettingsModal rendering
|
|
- **Learnings for future iterations:**
|
|
- The ProjectSettingsModal receives `onCharactersChange` and `onVariablesChange` callbacks that directly set state in FlowchartEditor. When save is implemented, it should read from this state.
|
|
- The Variables tab is a read-only placeholder in US-056; US-057 will implement the full CRUD for variables using the same patterns (inline forms, validation, delete warnings).
|
|
- Modal pattern: fixed inset-0 z-50 with backdrop click to close, max-w-2xl for settings modals (larger than max-w-md used for simple dialogs).
|
|
- Character usage count checks dialogue nodes for `data.characterId`; variable usage count checks both variable nodes and edge conditions.
|
|
- The `randomHexColor()` utility picks from a curated list of 12 vibrant colors for character defaults.
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-057
|
|
- What was implemented: Variable management UI with full CRUD in the project settings modal Variables tab
|
|
- Files changed:
|
|
- `src/components/editor/ProjectSettingsModal.tsx` - Replaced placeholder VariablesTab with full implementation: add/edit/delete with inline forms, type dropdown (numeric/string/boolean), type-adaptive initial value input (number input for numeric, text for string, select for boolean), name uniqueness validation, delete warnings with usage count, colored type badges
|
|
- **Learnings for future iterations:**
|
|
- The VariableForm uses a `handleTypeChange` helper that resets the initial value to the type's default when the type changes, preventing invalid state (e.g., "hello" as a numeric value)
|
|
- Initial values are stored as strings in form state and parsed to the correct type (number/string/boolean) on save via `parseInitialValue()`
|
|
- Type badges use distinct colors: blue for numeric, green for string, purple for boolean - making variable types instantly recognizable in the list
|
|
- The same form patterns from CharactersTab apply: inline form within the list for editing, appended form below the list for adding
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-059
|
|
- What was implemented: Variable node variable dropdown using Combobox component, replacing the free-text input
|
|
- Files changed:
|
|
- `src/components/editor/nodes/VariableNode.tsx` - Replaced text input with Combobox for variable selection, added inline "Add new variable" form with name + type, added orange warning border for invalid references, filtered operation options (add/subtract only for numeric type)
|
|
- `src/components/editor/EditorContext.tsx` - Extended context to include `variables: Variable[]` and `onAddVariable` callback
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `handleAddVariableDefinition` callback and passed variables + onAddVariable through EditorContext
|
|
- **Learnings for future iterations:**
|
|
- The existing `handleAddVariable` in FlowchartEditor adds a variable *node* to the canvas (toolbar action). The new `handleAddVariableDefinition` creates a variable *definition* in the project's data. Name carefully to avoid collisions.
|
|
- EditorContext is the shared context for node components to access project-level characters and variables. Extend it when new entity types need to be accessible from custom node components.
|
|
- The VariableNode follows the same pattern as DialogueNode for Combobox integration: items derived via useMemo, handleSelect sets both variableId and variableName, inline add form for quick creation, hasInvalidReference for warning state.
|
|
- Operations filtering uses `isNumeric` flag: if no variable is selected (undefined) or type is 'numeric', all operations are shown; otherwise only 'set' is available. When selecting a non-numeric variable, operation is auto-reset to 'set' if it was 'add' or 'subtract'.
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-060
|
|
- What was implemented: Edge condition variable dropdown using Combobox component, replacing free-text input with a type-aware condition editor modal
|
|
- Files changed:
|
|
- `src/types/flowchart.ts` - Updated `Condition.value` type from `number` to `number | string | boolean` to support all variable types
|
|
- `src/components/editor/ConditionEditor.tsx` - New component: modal-based condition editor with Combobox for variable selection, type-aware operator filtering, type-adaptive value inputs, inline "Add new variable" form, orange warning for invalid references, and "Remove condition" action
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `onEdgeClick` handler to open ConditionEditor, `handleConditionChange` to update edge condition data, `selectedEdgeId` state, and ConditionEditor rendering
|
|
- **Learnings for future iterations:**
|
|
- Edge interactions in React Flow use `onEdgeClick` prop on the ReactFlow component (not on individual edges). The handler receives `(event: React.MouseEvent, edge: Edge)`.
|
|
- The ConditionEditor is rendered as a modal overlay (fixed z-50), not as part of the edge itself — since edges don't have built-in panel/popover support in React Flow.
|
|
- `Condition.value` was originally typed as just `number` but needed broadening to `number | string | boolean` to support string/boolean variables in conditions. This change didn't break existing code since the VariableNode's `value` field is a separate type.
|
|
- Operator filtering for non-numeric types: only `==` and `!=` are available for string/boolean variables. When switching from a numeric variable to a string/boolean, the operator auto-resets to `==` if it was a comparison operator.
|
|
- Value input adapts to type: number input for numeric, text input for string, boolean dropdown for boolean.
|
|
- The `selectedEdge` is derived via `useMemo` from `edges` state and `selectedEdgeId`, so it always reflects the latest condition data.
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-061
|
|
- What was implemented: Choice option condition variable dropdown using OptionConditionEditor component with Combobox
|
|
- Files changed:
|
|
- `src/types/flowchart.ts` - Added `condition?: Condition` to `ChoiceOption` type; moved `Condition` type definition before `ChoiceOption` for correct reference order
|
|
- `src/components/editor/OptionConditionEditor.tsx` - New component: modal-based condition editor for choice options with Combobox variable selection, type-aware operators, type-adaptive value inputs, inline "Add new variable" form, orange warning for invalid references
|
|
- `src/components/editor/nodes/ChoiceNode.tsx` - Added condition button per option (clipboard icon), condition summary text below options, OptionConditionEditor integration, EditorContext usage for variables, invalid reference detection with orange warning styling
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Extended `getVariableUsageCount` to also count variable references in choice option conditions
|
|
- **Learnings for future iterations:**
|
|
- The `OptionConditionEditor` follows the same pattern as `ConditionEditor` but with a simpler API: it doesn't need an edgeId since it works with a single option's condition via `onChange(condition | undefined)` callback
|
|
- The `ChoiceOption` type in `flowchart.ts` now references `Condition`, which required reordering type definitions (Condition must be defined before ChoiceOption)
|
|
- Each choice option shows a small clipboard icon button that turns blue when a condition is set, or orange when the referenced variable is invalid/deleted
|
|
- A condition summary line (e.g., "if score > 10") appears below each option label when a condition is active
|
|
- The `getVariableUsageCount` in FlowchartEditor now counts three sources: variable nodes, edge conditions, and choice option conditions
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-062
|
|
- What was implemented: Auto-migration of existing free-text speaker/variable values to character/variable definitions on project load
|
|
- Files changed:
|
|
- `src/app/editor/[projectId]/page.tsx` - Added `needsMigration` flag that detects whether raw DB data has characters/variables arrays
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `computeMigration()` helper function and `needsMigration` prop; migration result initializes state directly via lazy `useState` to avoid React Compiler lint issues
|
|
- `src/components/editor/nodes/DialogueNode.tsx` - Included pre-existing US-058 changes (speaker dropdown with Combobox) that were not previously committed
|
|
- **Learnings for future iterations:**
|
|
- React Compiler lint (`react-hooks/set-state-in-effect`) forbids calling `setState` synchronously within `useEffect`. For one-time initialization logic, compute the result and use it directly in state initializers instead.
|
|
- React Compiler lint (`react-hooks/refs`) forbids reading `useRef().current` during render. Use `useState(() => ...)` lazy initializer pattern instead of `useRef` for values computed once at mount.
|
|
- The migration detection relies on `rawData.characters` being `undefined` (old projects) vs `[]` (migrated projects). The `page.tsx` server component passes `needsMigration` flag to the client component since only the server has access to the raw DB shape.
|
|
- `computeMigration` is a pure function called outside the component render cycle (via lazy useState). It uses `nanoid()` for IDs, so it must only be called once — lazy `useState` ensures this.
|
|
- The toast message for migration is set as initial state, so it shows immediately on first render without needing an effect.
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-063
|
|
- What was implemented: Import characters/variables from another project via modal in project settings
|
|
- Files changed:
|
|
- `src/components/editor/ImportFromProjectModal.tsx` - New component: project list modal with checkbox selection for characters or variables, duplicate-by-name skipping with warnings, select all/none controls
|
|
- `src/components/editor/ProjectSettingsModal.tsx` - Added `projectId` prop, `ImportFromProjectModal` integration, and "Import from project" buttons in both Characters and Variables tabs
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Passed `projectId` through to `ProjectSettingsModal`
|
|
- **Learnings for future iterations:**
|
|
- The `ImportFromProjectModal` uses `z-[60]` to layer above the `ProjectSettingsModal` (which uses `z-50`), since it's rendered as a child of that modal
|
|
- Imported characters/variables get new IDs via `nanoid()` to avoid ID collisions between projects. The original colors, types, and initial values are preserved.
|
|
- Duplicate detection is case-insensitive by name. Duplicates are skipped (not overwritten) with a warning message shown to the user.
|
|
- The `LoadingSpinner` component mentioned in Codebase Patterns doesn't exist; used inline text loading indicators instead.
|
|
- Supabase client-side fetching from `createClient()` (browser) automatically scopes by the logged-in user's RLS policies, so fetching other projects just uses `.neq('id', currentProjectId)` and RLS handles ownership filtering.
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-22 - US-030
|
|
- What was implemented: Right-click context menu for canvas, nodes, and edges
|
|
- Files changed:
|
|
- src/components/editor/ContextMenu.tsx - new component with menu items for different contexts (canvas/node/edge)
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - integrated context menu with handlers for all actions
|
|
- **Learnings for future iterations:**
|
|
- Use `onPaneContextMenu`, `onNodeContextMenu`, and `onEdgeContextMenu` React Flow callbacks for context menus
|
|
- `screenToFlowPosition()` converts screen coordinates to flow coordinates for placing nodes at click position
|
|
- Context menu state includes type ('canvas'|'node'|'edge') and optional nodeId/edgeId for targeted actions
|
|
- Use `document.addEventListener('click', handler)` and `e.stopPropagation()` on menu to close on outside click
|
|
- Escape key listener via `document.addEventListener('keydown', handler)` for menu close
|
|
- NodeMouseHandler and EdgeMouseHandler types from reactflow provide proper typing for context menu callbacks
|
|
---
|
|
|
|
## 2026-01-22 - US-031
|
|
- What was implemented: Condition editor modal for adding/editing/removing conditions on edges
|
|
- Files changed:
|
|
- src/components/editor/ConditionEditor.tsx - new modal component with form for variable name, operator, and value
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - integrated condition editor with double-click and context menu triggers
|
|
- **Learnings for future iterations:**
|
|
- Use `onEdgeDoubleClick` React Flow callback for double-click on edges
|
|
- Store condition editor state separately from context menu state (`conditionEditor` vs `contextMenu`)
|
|
- Use `edge.data.condition` to access condition object on edges
|
|
- When removing properties from edge data, use `delete` operator instead of destructuring to avoid lint warnings about unused variables
|
|
- Condition type has operators: '>' | '<' | '==' | '>=' | '<=' | '!='
|
|
- Preview condition in modal using template string: `${variableName} ${operator} ${value}`
|
|
---
|
|
|
|
## 2026-01-22 - US-032
|
|
- What was implemented: Display conditions on edges with dashed styling and labels
|
|
- Files changed:
|
|
- src/components/editor/edges/ConditionalEdge.tsx - new custom edge component with condition display
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - integrated custom edge type, added EdgeTypes import and edgeTypes definition
|
|
- **Learnings for future iterations:**
|
|
- Custom React Flow edges use EdgeProps<T> typing where T is the data shape
|
|
- Use `BaseEdge` component for rendering the edge path, and `EdgeLabelRenderer` for positioning labels
|
|
- `getSmoothStepPath` returns [edgePath, labelX, labelY] - labelX/labelY are center coordinates for labels
|
|
- Custom edge types are registered in edgeTypes object (similar to nodeTypes) and passed to ReactFlow
|
|
- Style edges with conditions using strokeDasharray: '5 5' for dashed lines
|
|
- Custom edges go in `src/components/editor/edges/` directory
|
|
- Use amber color scheme for conditional edges to distinguish from regular edges
|
|
---
|
|
|
|
## 2026-01-22 - US-033
|
|
- What was implemented: Auto-save to LocalStorage with debounced saves and draft restoration prompt
|
|
- Files changed:
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - added LocalStorage auto-save functionality, draft check on load, and restoration prompt UI
|
|
- **Learnings for future iterations:**
|
|
- Use lazy useState initializer for draft check to avoid ESLint "setState in effect" warning
|
|
- LocalStorage key format: `vnwrite-draft-{projectId}` for project-specific drafts
|
|
- Debounce saves with 1 second delay using useRef for timer tracking
|
|
- Convert React Flow Node/Edge types back to app types using helper functions (fromReactFlowNodes, fromReactFlowEdges)
|
|
- React Flow Edge has `sourceHandle: string | null | undefined` but app types use `string | undefined` - use nullish coalescing (`?? undefined`)
|
|
- Check `typeof window === 'undefined'` in lazy initializer for SSR safety
|
|
- clearDraft is exported for use in save functionality (US-034) to clear draft after successful database save
|
|
- JSON.stringify comparison works for flowchart data equality check
|
|
---
|
|
|
|
## 2026-01-22 - US-034
|
|
- What was implemented: Save project to database functionality
|
|
- Files changed:
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleSave with Supabase update, added isSaving state and Toast notifications
|
|
- src/components/editor/Toolbar.tsx - added isSaving prop with loading spinner indicator
|
|
- **Learnings for future iterations:**
|
|
- Use createClient() from lib/supabase/client.ts for browser-side database operations
|
|
- Supabase update returns { error } object for error handling
|
|
- Use async/await with try/catch for async save operations
|
|
- Set updated_at manually with new Date().toISOString() for Supabase JSONB updates
|
|
- Clear LocalStorage draft after successful save to avoid stale drafts
|
|
- Toast state uses object with message and type for flexibility
|
|
- Loading spinner SVG with animate-spin class for visual feedback during save
|
|
---
|
|
|
|
## 2026-01-22 - US-035
|
|
- What was implemented: Export project as .vnflow file functionality
|
|
- Files changed:
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleExport with blob creation and download trigger, added projectName prop
|
|
- src/app/editor/[projectId]/page.tsx - passed projectName prop to FlowchartEditor
|
|
- **Learnings for future iterations:**
|
|
- Use Blob with type 'application/json' for JSON file downloads
|
|
- JSON.stringify(data, null, 2) creates pretty-printed JSON with 2-space indentation
|
|
- URL.createObjectURL creates a temporary URL for the blob
|
|
- Create temporary anchor element with download attribute to trigger file download
|
|
- Remember to cleanup: remove the anchor from DOM and revoke the object URL
|
|
- Props needed for export: pass data down from server components (e.g., projectName) to client components that need them
|
|
---
|
|
|
|
## 2026-01-22 - US-036
|
|
- What was implemented: Import project from .vnflow file functionality
|
|
- Files changed:
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleImport, handleFileSelect, validation, and confirmation dialog for unsaved changes
|
|
- **Learnings for future iterations:**
|
|
- Use hidden `<input type="file">` element with ref to trigger file picker programmatically via `ref.current?.click()`
|
|
- Accept multiple file extensions using comma-separated values in `accept` attribute: `accept=".vnflow,.json"`
|
|
- Reset file input value after selection (`event.target.value = ''`) to allow re-selecting the same file
|
|
- Use FileReader API with `readAsText()` for reading file contents, handle onload and onerror callbacks
|
|
- Type guard function `isValidFlowchartData()` validates imported JSON structure before loading
|
|
- Track unsaved changes by comparing current state to initialData using JSON.stringify comparison
|
|
- Show confirmation dialog before import if there are unsaved changes to prevent accidental data loss
|
|
---
|
|
|
|
## 2026-01-22 - US-037
|
|
- What was implemented: Export to Ren'Py JSON format functionality
|
|
- Files changed:
|
|
- src/components/editor/Toolbar.tsx - added 'Export to Ren'Py' button with purple styling
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented Ren'Py export types, conversion functions, and handleExportRenpy callback
|
|
- **Learnings for future iterations:**
|
|
- Ren'Py export uses typed interfaces for different node types: RenpyDialogueNode, RenpyMenuNode, RenpyVariableNode
|
|
- Find first node by identifying nodes with no incoming edges (not in any edge's target set)
|
|
- Use graph traversal (DFS) to organize nodes into labeled sections based on flow
|
|
- Choice nodes create branching sections - save current section before processing each branch
|
|
- Track visited nodes to detect cycles and create proper labels for jump references
|
|
- Labels are generated based on speaker name or incremental counter for uniqueness
|
|
- Replace node IDs with proper labels in a second pass after traversal completes
|
|
- Include metadata (projectName, exportedAt) at the top level of the export
|
|
- Validate JSON output with JSON.parse before download to ensure validity
|
|
- Use purple color scheme for Ren'Py-specific button to distinguish from generic export
|
|
---
|
|
|
|
## 2026-01-22 - US-038
|
|
- What was implemented: Unsaved changes warning with dirty state tracking, beforeunload, and navigation confirmation modal
|
|
- Files changed:
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - added isDirty tracking via useMemo comparing current state to lastSavedDataRef, beforeunload event handler, navigation warning modal, back button with handleBackClick, moved header from page.tsx into this component
|
|
- src/app/editor/[projectId]/page.tsx - simplified to only render FlowchartEditor (header moved to client component for dirty state access)
|
|
- **Learnings for future iterations:**
|
|
- Dirty state tracking uses useMemo comparing JSON.stringify of current flowchart data to a lastSavedDataRef
|
|
- lastSavedDataRef is a useRef initialized with initialData and updated after successful save
|
|
- Browser beforeunload requires both event.preventDefault() and setting event.returnValue = '' for modern browsers
|
|
- Header with back navigation was moved from server component (page.tsx) to client component (FlowchartEditor.tsx) so it can access isDirty state
|
|
- Back button uses handleBackClick which checks isDirty before navigating or showing confirmation modal
|
|
- Navigation warning modal shows "Leave Page" (red) and "Stay" buttons for clear user action
|
|
- "(unsaved changes)" indicator shown next to project name when isDirty is true
|
|
---
|
|
|
|
## 2026-01-22 - US-039
|
|
- What was implemented: Loading and error states with reusable spinner, error page, and toast retry
|
|
- Files changed:
|
|
- src/components/LoadingSpinner.tsx - new reusable loading spinner component with size variants and optional message
|
|
- src/app/editor/[projectId]/loading.tsx - updated to use LoadingSpinner component
|
|
- src/app/editor/[projectId]/page.tsx - replaced notFound() with custom error UI showing "Project Not Found" with back to dashboard link
|
|
- src/components/Toast.tsx - added optional action prop for action buttons (e.g., retry)
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - updated toast state type to include action, save error now shows retry button via handleSaveRef pattern
|
|
- **Learnings for future iterations:**
|
|
- Use a ref (handleSaveRef) to break circular dependency when a useCallback needs to reference itself for retry logic
|
|
- Toast action prop uses `{ label: string; onClick: () => void }` for flexible action buttons
|
|
- Don't auto-dismiss toasts that have action buttons (users need time to click them)
|
|
- Replace `notFound()` with inline error UI when you need custom styling and navigation links
|
|
- LoadingSpinner uses size prop ('sm' | 'md' | 'lg') for flexibility across different contexts
|
|
- Link component from next/link is needed in server components for navigation (no useRouter in server components)
|
|
---
|
|
|
|
## 2026-01-22 - US-040
|
|
- What was implemented: Conditionals on choice options - per-option visibility conditions
|
|
- Files changed:
|
|
- src/types/flowchart.ts - moved Condition type before ChoiceOption, added optional condition field to ChoiceOption
|
|
- src/components/editor/nodes/ChoiceNode.tsx - added condition button per option, condition badge display, condition editing state management
|
|
- src/components/editor/OptionConditionEditor.tsx - new modal component for editing per-option conditions (variable name, operator, value)
|
|
- src/app/editor/[projectId]/FlowchartEditor.tsx - updated Ren'Py export to include per-option conditions (option condition takes priority over edge condition)
|
|
- **Learnings for future iterations:**
|
|
- Per-option conditions use the same Condition type as edge conditions
|
|
- Condition type needed to be moved above ChoiceOption in types file since ChoiceOption now references it
|
|
- Use `delete obj.property` pattern instead of destructuring with unused variable to avoid lint warnings
|
|
- OptionConditionEditor is separate from ConditionEditor because it operates on option IDs vs edge IDs
|
|
- In Ren'Py export, option-level condition takes priority over edge condition since it represents visibility
|
|
- Condition button uses amber color scheme (bg-amber-100) when condition is set, neutral when not
|
|
- Condition badge below option shows "if variableName operator value" text in compact format
|
|
---
|
|
|
|
## 2026-01-22 - US-041
|
|
- What was implemented: Change password for logged-in user from settings page
|
|
- Files changed:
|
|
- src/app/dashboard/settings/page.tsx - new client component with password change form (current, new, confirm fields)
|
|
- src/components/Navbar.tsx - added "Settings" link to navbar
|
|
- **Learnings for future iterations:**
|
|
- Settings page lives under /dashboard/settings to reuse the dashboard layout (navbar, auth check)
|
|
- Re-authentication uses signInWithPassword with current password before allowing updateUser
|
|
- Supabase getUser() returns current user email needed for re-auth signInWithPassword call
|
|
- Password validation: check match and minimum length (6 chars) before making API calls
|
|
- Clear form fields after successful password update for security
|
|
- Settings link in navbar uses neutral zinc colors to distinguish from admin/action links
|
|
---
|
|
|
|
## 2026-01-22 - US-042
|
|
- What was implemented: Password reset modal that automatically appears when a recovery token is detected in the URL
|
|
- Files changed:
|
|
- src/components/PasswordResetModal.tsx - new client component with modal that detects recovery tokens from URL hash, sets session, and provides password reset form
|
|
- src/app/login/page.tsx - integrated PasswordResetModal component on the login page
|
|
- **Learnings for future iterations:**
|
|
- PasswordResetModal is a standalone component that can be placed on any page to detect recovery tokens
|
|
- Use window.history.replaceState to clean the URL hash after extracting the token (prevents re-triggering on refresh)
|
|
- Separate tokenError state from form error state to show different UI (expired link vs. form validation)
|
|
- Modal uses fixed positioning with z-50 to overlay above page content
|
|
- After successful password update, sign out the user and redirect to login with success message (same as reset-password page)
|
|
- The modal coexists with the existing /reset-password page - both handle recovery tokens but in different UX patterns
|
|
---
|
|
|
|
## 2026-01-23 - US-047
|
|
- What was implemented: Live cursor positions on canvas showing collaborators' mouse positions in real-time
|
|
- Files changed:
|
|
- `src/lib/collaboration/realtime.ts` - Added `CursorPosition`, `RemoteCursor` types, `onCursorUpdate` callback to `RealtimeCallbacks`, broadcast listener for 'cursor' events, and `broadcastCursor()` method
|
|
- `src/components/editor/RemoteCursors.tsx` - New component: renders colored arrow cursors with user name labels, smooth position interpolation via CSS transition, 5-second fade-out for inactive cursors, flow-to-screen coordinate transformation using React Flow viewport
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `remoteCursors` state, `cursorThrottleRef` for 50ms throttling, `handleMouseMove` that converts screen→flow coordinates and broadcasts via RealtimeConnection, cleanup of cursors for disconnected users, rendering of RemoteCursors overlay
|
|
- `src/app/editor/[projectId]/page.tsx` - Fixed broken JSX structure (malformed HTML nesting and dead code after return)
|
|
- **Learnings for future iterations:**
|
|
- `screenToFlowPosition` from `useReactFlow()` converts screen-relative mouse coordinates to flow coordinates; for the reverse (rendering cursors), multiply by viewport.zoom and add viewport offset
|
|
- Cursor broadcast uses Supabase Realtime broadcast (not presence) for efficiency: `channel.send({ type: 'broadcast', event: 'cursor', payload })`. Broadcast is fire-and-forget (no persistence).
|
|
- React Compiler lint treats `Date.now()` as an impure function call — use `useState(() => Date.now())` lazy initializer pattern instead of `useState(Date.now())`
|
|
- Throttling mouse events uses a ref storing the last broadcast timestamp (`cursorThrottleRef`), checked at the start of the handler before computing flow position
|
|
- Remote cursors are removed when their user disconnects (filtered by `presenceUsers` list changes)
|
|
- CSS `transition: transform 80ms linear` provides smooth interpolation between position updates without needing requestAnimationFrame
|
|
- The `page.tsx` had a corrupted structure with unclosed tags and dead code — likely from a failed merge. Fixed by restructuring the error/not-found case into a proper early return
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-23 - US-050
|
|
- What was implemented: Join/leave toast notifications when collaborators connect or disconnect from the editing session
|
|
- Files changed:
|
|
- `src/lib/collaboration/realtime.ts` - Added `onPresenceJoin` and `onPresenceLeave` callbacks to `RealtimeCallbacks` type; added Supabase Realtime `presence.join` and `presence.leave` event listeners that filter out own user and invoke callbacks
|
|
- `src/components/editor/CollaborationToast.tsx` - New component: renders a compact toast notification with user's presence color dot, "[Name] joined" or "[Name] left" message, auto-dismisses after 3 seconds
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `getUserColor` helper (same hash logic as PresenceAvatars), `collaborationNotifications` state, `onPresenceJoin`/`onPresenceLeave` handlers on RealtimeConnection, `handleDismissNotification` callback, and rendering of CollaborationToast list in bottom-left corner
|
|
- **Learnings for future iterations:**
|
|
- Supabase Realtime presence has three event types: `sync` (full state), `join` (new arrivals), and `leave` (departures). Each provides an array of presences (`newPresences`/`leftPresences`). Use all three for different purposes.
|
|
- The `join` event fires for each newly tracked presence. It includes the presence payload (userId, displayName) that was passed to `channel.track()`.
|
|
- Collaboration notifications are positioned `bottom-left` (`left-4`) to avoid overlapping with the existing Toast component which is `bottom-right` (`right-4`).
|
|
- The `getUserColor` function is duplicated from PresenceAvatars to avoid circular imports. Both use the same hash-to-color-index algorithm with the same RANDOM_COLORS palette for consistency.
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-24 - US-049
|
|
- What was implemented: Node editing lock indicators that show when another collaborator is editing a node
|
|
- Files changed:
|
|
- `src/lib/collaboration/realtime.ts` - Added `NodeLock` type, `onNodeLockUpdate` callback to `RealtimeCallbacks`, `node-lock` broadcast listener, and `broadcastNodeLock()` method
|
|
- `src/components/editor/NodeLockIndicator.tsx` - New component: renders a colored border and name label overlay on locked nodes
|
|
- `src/components/editor/EditorContext.tsx` - Extended context with `NodeLockInfo` type, `nodeLocks` (Map), `onNodeFocus`, and `onNodeBlur` callbacks
|
|
- `src/components/editor/nodes/DialogueNode.tsx` - Added lock detection, `NodeLockIndicator` rendering, "Being edited by [name]" overlay, `onFocus`/`onBlur` handlers
|
|
- `src/components/editor/nodes/VariableNode.tsx` - Same lock indicator pattern as DialogueNode
|
|
- `src/components/editor/nodes/ChoiceNode.tsx` - Same lock indicator pattern as DialogueNode
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `nodeLocks` state (Map<string, NodeLockInfo>), `localLockRef` for tracking own lock, `handleNodeFocus`/`handleNodeBlur` callbacks, `onNodeLockUpdate` handler in RealtimeConnection, lock expiry timer (60s check every 5s), lock cleanup on user leave and component unmount, extended `editorContextValue` with lock state
|
|
- **Learnings for future iterations:**
|
|
- Node lock uses Supabase Realtime broadcast (like cursors) — ephemeral, not persisted to DB. Event name: `node-lock`.
|
|
- Lock broadcasting uses `nodeId: string | null` pattern: non-null to acquire lock, `null` to release. The receiving side maps userId to their current lock.
|
|
- Lock expiry uses a 60-second timeout checked every 5 seconds via `setInterval`. The `lockedAt` timestamp is broadcast with the lock payload.
|
|
- Each node component accesses lock state via `EditorContext` (`nodeLocks` Map). The `NodeLockInfo` type extends `NodeLock` with a `color` field derived from `getUserColor()`.
|
|
- `onFocus`/`onBlur` on the node container div fires when any child input gains/loses focus (focus event bubbles), which naturally maps to "user is editing this node".
|
|
- Lock release on disconnect: broadcast `null` lock before calling `connection.disconnect()` in the cleanup return of the mount effect.
|
|
- Locks from disconnected users are cleaned up in the same effect that removes cursors (filtered by `presenceUsers` list).
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|
|
|
|
## 2026-01-24 - US-051
|
|
- What was implemented: Audit trail recording that writes all node/edge add/update/delete operations to the `audit_trail` table with debouncing and fire-and-forget semantics
|
|
- Files changed:
|
|
- `src/lib/collaboration/auditTrail.ts` - New `AuditTrailRecorder` class: tracks previous node/edge state, diffs against current state to detect add/update/delete operations, debounces writes per entity (1 second), merges rapid sequential actions (e.g., add+update=add, add+delete=no-op), fire-and-forget Supabase inserts with error logging, flush on destroy
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `auditRef` (AuditTrailRecorder), initialized in mount effect alongside CRDT manager, records node/edge changes in the CRDT sync effects (only for local changes, skipped for remote updates), destroyed on unmount
|
|
- **Learnings for future iterations:**
|
|
- The audit recorder piggybacks on the same CRDT sync effects that already compute `nodesForCRDT`/`edgesForCRDT` — this avoids duplicating the React Flow → FlowchartNode/Edge conversion.
|
|
- The `isRemoteUpdateRef` guard in the sync effects ensures audit entries are only created for local user actions, not for changes received from other collaborators (those users' own recorders will handle their audit entries).
|
|
- Debouncing per entity (1 second) prevents rapid edits (e.g., typing in a text field) from flooding the audit table. The merge logic handles transient states (add+delete within 1s = skip).
|
|
- The `destroy()` method flushes pending entries synchronously on unmount, ensuring in-flight edits aren't lost when navigating away.
|
|
- Supabase `.insert().then()` pattern provides fire-and-forget writes with error logging — the async operation doesn't block the editing flow.
|
|
- No browser testing needed — this is a developer/infrastructure story with no UI changes.
|
|
---
|
|
|
|
## 2026-01-24 - US-052
|
|
- What was implemented: Activity history sidebar that displays audit trail entries, grouped by time period, with entity selection on click
|
|
- Files changed:
|
|
- `src/components/editor/ActivityHistorySidebar.tsx` - New component: right sidebar panel showing chronological audit trail entries, grouped by Today/Yesterday/Earlier, with user color accents, entity descriptions, paginated loading (20 per page), and click-to-select entity on canvas
|
|
- `src/components/editor/Toolbar.tsx` - Added `onHistory` prop and "History" button in the right toolbar section
|
|
- `src/app/editor/[projectId]/FlowchartEditor.tsx` - Added `showHistory` state, `handleHistorySelectEntity` callback (selects nodes/edges on canvas), `ActivityHistorySidebar` import and rendering inside the canvas area, `onHistory` toggle prop on Toolbar
|
|
- **Learnings for future iterations:**
|
|
- React Compiler lint (`react-hooks/set-state-in-effect`) treats any function that calls setState as problematic when invoked inside useEffect — even if the setState is in an async `.then()` callback. To avoid this, extract data fetching into a pure function that returns a result object, then handle setState only in the `.then()` callback after checking a mounted/aborted guard.
|
|
- For right sidebar panels overlaying the canvas, use `absolute right-0 top-0 z-40 h-full w-80` inside the `relative flex-1` canvas container. This keeps the sidebar within the canvas area without affecting the toolbar.
|
|
- The `audit_trail` table has an index on `(project_id, created_at DESC)` which makes paginated queries efficient. Use `.range(offset, offset + PAGE_SIZE - 1)` for Supabase pagination.
|
|
- Entity descriptions are derived from `new_state` (for adds/updates) or `previous_state` (for deletes). The state contains the full node/edge data including `type`, `data.speaker`, `data.question`, `data.variableName`.
|
|
- Deleted entities (`action_type.endsWith('_delete')`) cannot be selected on canvas since they no longer exist — render those entries as disabled (no click handler, reduced opacity).
|
|
- No browser testing tools are available; manual verification is needed.
|
|
---
|