feat: Implement retrospective template system#40
Conversation
- Add DAKI template to existing template collection - Create database schema for custom templates with RLS policies - Build TemplateSelector UI component for template selection - Implement CustomTemplateCreator for creating custom templates - Add hooks for managing templates and preferences - Update TypeScript types from Supabase schema - Enable template preferences saving to user profile Fixes #13
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a template system: new client components for creating/selecting templates, Supabase-backed template hooks and preference APIs, a new DAKI board template, expanded Supabase types including Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Creator as CustomTemplateCreator (UI)
participant HooksCreate as useCreateCustomTemplate
participant DB as Supabase (custom_templates)
User->>Creator: Edit template (name, visibility, columns)
Creator->>User: validate (name & column titles)
User->>Creator: Click "Save"
Creator->>HooksCreate: createTemplate({ name, description, is_public, columns, team/org })
HooksCreate->>DB: INSERT into public.custom_templates
DB-->>HooksCreate: created record / error
HooksCreate-->>Creator: result
alt success
Creator-->>User: close dialog / confirm saved
else error
Creator-->>User: show error
end
sequenceDiagram
autonumber
actor User
participant Selector as TemplateSelector (UI)
participant HooksList as useTemplates
participant Prefs as useTemplatePreferences
participant Data as boardTemplates + custom_templates
User->>Selector: Open selector
Selector->>HooksList: fetch custom templates (team/org/public)
Selector->>Prefs: fetch user preferences
HooksList-->>Selector: customTemplates
Prefs-->>Selector: preferences
Selector->>Data: merge templates
Selector->>Selector: sort (preferred → recent → name)
Selector-->>User: render selectable list
User->>Selector: choose template
Selector-->>User: onTemplateSelect(templateId)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
Comment |
There was a problem hiding this comment.
Pull Request Overview
This PR implements a comprehensive retrospective template system with custom template support and user preferences. It adds database schema for custom templates, introduces the DAKI retrospective template, and creates UI components for template management.
- Added DAKI template and expanded icon support for all retrospective column types
- Implemented database schema with custom_templates table and user template preferences
- Created TemplateSelector and CustomTemplateCreator components for template management
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/20250927_add_custom_templates.sql | Database migration adding custom_templates table and template preferences to profiles |
| src/lib/supabase/types.ts | Updated TypeScript types with new database schema and improved type generation |
| src/lib/boards/templates.ts | Added DAKI template with Drop, Add, Keep, Improve columns |
| src/hooks/use-templates.ts | Custom hooks for template management and user preferences |
| src/components/TemplateSelector.tsx | Component for selecting built-in and custom templates with preference sorting |
| src/components/RetrospectiveBoard.tsx | Added icon mappings for new template column types |
| src/components/CustomTemplateCreator.tsx | Drag-and-drop interface for creating custom templates |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/RetrospectiveBoard.tsx (2)
84-112: Honor custom column.icon when present and fix icon mismatches (mad/sad/longed-for)Currently icons are chosen only by column_type, so custom template icons are ignored. Also, mappings for mad/sad/longed-for don’t match templates.ts. Use column.icon first, fall back to type.
Apply this diff:
-// Icon mapping for different column types -const getColumnIcon = (columnType: string): React.ReactNode => { - const iconMap: Record<string, React.ReactNode> = { +// Icon mapping for different column types, with override via column.icon +const getColumnIcon = (column: { column_type: string; icon?: string }): React.ReactNode => { + // map string icon names (used by CustomTemplateCreator) to components + const iconByName: Record<string, React.ReactNode> = { + ThumbsUp: <ThumbsUp className="h-5 w-5" />, + ThumbsDown: <ThumbsDown className="h-5 w-5" />, + Heart: <Heart className="h-5 w-5" />, + Star: <Star className="h-5 w-5" />, + Lightbulb: <Lightbulb className="h-5 w-5" />, + AlertTriangle: <AlertTriangle className="h-5 w-5" />, + Target: <Target className="h-5 w-5" />, + TrendingUp: <TrendingUp className="h-5 w-5" />, + TrendingDown: <TrendingDown className="h-5 w-5" />, + MessageSquare: <MessageSquare className="h-5 w-5" />, + Check: <Check className="h-5 w-5" />, + X: <X className="h-5 w-5" />, + Plus: <Plus className="h-5 w-5" />, + PlayCircle: <PlayCircle className="h-5 w-5" />, + PauseCircle: <PauseCircle className="h-5 w-5" />, + Smile: <Smile className="h-5 w-5" />, + Frown: <Frown className="h-5 w-5" />, + Meh: <Meh className="h-5 w-5" />, + }; + + // Use explicit icon if provided (custom templates) + if (column.icon && iconByName[column.icon]) { + return iconByName[column.icon]; + } + + const iconMap: Record<string, React.ReactNode> = { 'went-well': <ThumbsUp className="h-5 w-5" />, 'improve': <Lightbulb className="h-5 w-5" />, 'blockers': <AlertTriangle className="h-5 w-5" />, 'action-items': <Target className="h-5 w-5" />, 'glad': <Smile className="h-5 w-5" />, - 'sad': <Frown className="h-5 w-5" />, - 'mad': <AlertTriangle className="h-5 w-5" />, + 'sad': <Meh className="h-5 w-5" />, + 'mad': <Frown className="h-5 w-5" />, 'liked': <Heart className="h-5 w-5" />, 'learned': <Lightbulb className="h-5 w-5" />, 'lacked': <AlertTriangle className="h-5 w-5" />, - 'longed-for': <Target className="h-5 w-5" />, + 'longed-for': <Star className="h-5 w-5" />, 'drop': <X className="h-5 w-5" />, 'add': <Plus className="h-5 w-5" />, 'keep': <Check className="h-5 w-5" />, 'start': <PlayCircle className="h-5 w-5" />, 'stop': <PauseCircle className="h-5 w-5" />, 'continue': <TrendingUp className="h-5 w-5" />, 'wind': <TrendingUp className="h-5 w-5" />, 'anchor': <TrendingDown className="h-5 w-5" />, 'rocks': <AlertTriangle className="h-5 w-5" />, 'island': <Target className="h-5 w-5" />, 'plus': <ThumbsUp className="h-5 w-5" />, 'delta': <TrendingUp className="h-5 w-5" />, }; - return iconMap[columnType] || <MessageSquare className="h-5 w-5" />; + return iconMap[column.column_type] || <MessageSquare className="h-5 w-5" />; };As per coding guidelines.
646-647: Pass the full column to getColumnIcon after signature changeWire the new helper that honors column.icon.
Apply this diff:
- {getColumnIcon(column.column_type)} + {getColumnIcon(column)}
🧹 Nitpick comments (5)
src/components/RetrospectiveBoard.tsx (1)
405-410: Simplify intra-column insert index logic (comment vs. code mismatch)Both branches return overIndex; remove the dead conditional.
Apply this diff:
- // If dragging down, insert after; if dragging up, insert before - newPosition = activeIndex < overIndex ? overIndex : overIndex; + // Insert at the dropped index + newPosition = overIndex;supabase/migrations/20250927_add_custom_templates.sql (1)
2-13: Add minimal schema validations for columns JSON and consider NOT NULLsEnforce 1–6 columns and JSON array shape; optionally require created_by/organization_id.
Apply this diff:
CREATE TABLE IF NOT EXISTS public.custom_templates ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, name text NOT NULL, description text, - created_by uuid REFERENCES public.profiles(id) ON DELETE CASCADE, - organization_id uuid REFERENCES public.organizations(id) ON DELETE CASCADE, + created_by uuid REFERENCES public.profiles(id) ON DELETE CASCADE, + organization_id uuid REFERENCES public.organizations(id) ON DELETE CASCADE, team_id uuid REFERENCES public.teams(id) ON DELETE CASCADE, is_public boolean DEFAULT false, columns jsonb NOT NULL, created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now() ); + +-- Enforce array with 1..6 entries +ALTER TABLE public.custom_templates + ADD CONSTRAINT custom_templates_columns_is_array CHECK (jsonb_typeof(columns) = 'array'), + ADD CONSTRAINT custom_templates_columns_len CHECK (jsonb_array_length(columns) BETWEEN 1 AND 6); + +-- Optional hardening (uncomment if desired): +-- ALTER TABLE public.custom_templates ALTER COLUMN created_by SET NOT NULL; +-- ALTER TABLE public.custom_templates ALTER COLUMN organization_id SET NOT NULL;src/components/CustomTemplateCreator.tsx (1)
297-304: Guard against >6 columns on save (initialColumns can bypass UI limit)Enforce the cap at submission.
Apply this diff:
const handleSave = () => { - onSave({ + const capped = columns.slice(0, 6); + onSave({ name, description, - columns, + columns: capped, is_public: isPublic, }); };src/components/TemplateSelector.tsx (2)
62-80: Sorting mutates source array; memoize to avoid re-sorting every renderOptional: wrap in useMemo for stability (inputs: allTemplates, preferredTemplate, recentTemplates).
If desired, I can provide a small memoized snippet.
91-146: Wire “Create Custom Template” action or hide the buttonDialog currently closes with TODO. Either call onCreateCustomTemplate with a minimal payload or omit the button.
Proposed minimal call (pseudo, adjust to your creator contract):
- onClick={() => { - // TODO: Implement custom template creation - setIsCreateDialogOpen(false); - }} + onClick={() => { + onCreateCustomTemplate?.({ + name: customTemplateName.trim(), + description: customTemplateDescription.trim(), + is_public: false, + columns: [], // or current board columns if available in scope + created_by: null, + organization_id: null, + team_id: null, + } as any); + setIsCreateDialogOpen(false); + }}If you want, I can adapt this to your exact hook signature once you confirm where team/org ids come from in this flow.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/components/CustomTemplateCreator.tsx(1 hunks)src/components/RetrospectiveBoard.tsx(2 hunks)src/components/TemplateSelector.tsx(1 hunks)src/hooks/use-templates.ts(1 hunks)src/lib/boards/templates.ts(1 hunks)src/lib/supabase/types.ts(6 hunks)supabase/migrations/20250927_add_custom_templates.sql(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use the @/* path alias for imports that target ./src/*
Use generated database types from src/lib/supabase/types.ts for Supabase-related TypeScript typings
Files:
src/lib/boards/templates.tssrc/lib/supabase/types.tssrc/components/CustomTemplateCreator.tsxsrc/hooks/use-templates.tssrc/components/TemplateSelector.tsxsrc/components/RetrospectiveBoard.tsx
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.{ts,tsx}: In client-side code, import the Supabase browser client from src/lib/supabase/client.ts
In server-side code, import the Supabase server client from src/lib/supabase/server.ts
Files:
src/lib/boards/templates.tssrc/lib/supabase/types.tssrc/components/CustomTemplateCreator.tsxsrc/hooks/use-templates.tssrc/components/TemplateSelector.tsxsrc/components/RetrospectiveBoard.tsx
src/components/**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Add the "use client" directive to components that require client-side interactivity
Files:
src/components/CustomTemplateCreator.tsxsrc/components/TemplateSelector.tsxsrc/components/RetrospectiveBoard.tsx
src/components/RetrospectiveBoard.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Keep the main retrospective board component at src/components/RetrospectiveBoard.tsx implementing the four-column layout, real-time CRUD, voting, and author attribution
Files:
src/components/RetrospectiveBoard.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Applies to **/*.{ts,tsx} : Use generated database types from src/lib/supabase/types.ts for Supabase-related TypeScript typings
Applied to files:
src/lib/supabase/types.ts
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Applies to src/components/RetrospectiveBoard.tsx : Keep the main retrospective board component at src/components/RetrospectiveBoard.tsx implementing the four-column layout, real-time CRUD, voting, and author attribution
Applied to files:
src/components/CustomTemplateCreator.tsxsrc/components/TemplateSelector.tsxsrc/components/RetrospectiveBoard.tsx
🧬 Code graph analysis (3)
src/components/CustomTemplateCreator.tsx (1)
src/lib/boards/templates.ts (1)
BoardColumn(2-9)
src/hooks/use-templates.ts (2)
src/lib/supabase/types.ts (2)
Tables(549-576)TablesInsert(578-601)src/lib/boards/templates.ts (2)
BoardTemplate(11-16)BoardColumn(2-9)
src/components/TemplateSelector.tsx (2)
src/lib/supabase/types.ts (1)
Tables(549-576)src/lib/boards/templates.ts (2)
boardTemplates(18-260)BoardTemplate(11-16)
🪛 GitHub Check: Run Tests (20.x)
src/components/TemplateSelector.tsx
[failure] 159-159:
Unexpected any. Specify a different type
[failure] 158-158:
Unexpected any. Specify a different type
🪛 GitHub Actions: Tests
src/components/TemplateSelector.tsx
[error] 158-158: ESLint error: Unexpected any. Specify a different type. (no-explicit-any)
🔇 Additional comments (3)
src/lib/boards/templates.ts (1)
221-259: DAKI template addition looks goodFields align with BoardColumn/BoardTemplate. No structural issues spotted.
Please verify icon rendering stays consistent in RetrospectiveBoard for:
- drop → X
- add → Plus
- keep → Check
- improve → TrendingUp
src/components/TemplateSelector.tsx (2)
23-24: Import path OK and types usage aligns with guidelinesUsing Tables<> from generated Supabase types is correct here.
158-159: Install dependencies and verify ESLint passes
We couldn’t runeslintin the sandbox due to missingnode_modules. Please runpnpm install(ornpm install/yarn install) and thenpnpm lint(ornpm run lint/yarn lint) to confirm noUnexpected anyerrors remain.
- Fix loading state reset in use-templates hook to prevent stale errors - Implement proper template creation handler in TemplateSelector - Strengthen RLS policies to prevent cross-tenant data leaks - Ensure organization_id constraints in INSERT and UPDATE policies
- Replace inline template creation with informational dialog - Direct users to create templates from existing boards - Remove unused form fields and imports - Templates should be created with proper board context and user auth
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
src/components/TemplateSelector.tsx (2)
66-80: Avoid mutating the source when sortingSorting in place can cause subtle bugs if allTemplates is reused.
Apply:
- const sortedTemplates = allTemplates.sort((a, b) => { + const sortedTemplates = [...allTemplates].sort((a, b) => {
232-240: Prefer stable keys over index for columnsUse a semantic key to reduce reconciliation issues.
Apply:
- {template.columns.map((column, index) => ( + {template.columns.map((column, index) => ( <Badge - key={index} + key={column.column_type ?? String(index)} variant="secondary" className="text-xs" > {column.title} </Badge> ))}supabase/migrations/20250927_add_custom_templates.sql (1)
20-25: Indexing advice for common filtersLikely queries: by team_id, by organization_id with is_public, recents by created_at. Add composite indexes to support these.
Apply:
CREATE INDEX IF NOT EXISTS idx_custom_templates_created_by ON public.custom_templates(created_by); CREATE INDEX IF NOT EXISTS idx_custom_templates_organization_id ON public.custom_templates(organization_id); CREATE INDEX IF NOT EXISTS idx_custom_templates_team_id ON public.custom_templates(team_id); CREATE INDEX IF NOT EXISTS idx_custom_templates_is_public ON public.custom_templates(is_public); +CREATE INDEX IF NOT EXISTS idx_custom_templates_org_public ON public.custom_templates(organization_id, is_public); +CREATE INDEX IF NOT EXISTS idx_custom_templates_team_created_at ON public.custom_templates(team_id, created_at DESC);Also applies to: 33-51
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/components/TemplateSelector.tsx(1 hunks)src/hooks/use-templates.ts(1 hunks)supabase/migrations/20250927_add_custom_templates.sql(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/hooks/use-templates.ts
🧰 Additional context used
📓 Path-based instructions (3)
src/components/**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Add the "use client" directive to components that require client-side interactivity
Files:
src/components/TemplateSelector.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use the @/* path alias for imports that target ./src/*
Use generated database types from src/lib/supabase/types.ts for Supabase-related TypeScript typings
Files:
src/components/TemplateSelector.tsx
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.{ts,tsx}: In client-side code, import the Supabase browser client from src/lib/supabase/client.ts
In server-side code, import the Supabase server client from src/lib/supabase/server.ts
Files:
src/components/TemplateSelector.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Enable and rely on TypeScript strict mode
Applied to files:
src/components/TemplateSelector.tsx
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Applies to src/components/RetrospectiveBoard.tsx : Keep the main retrospective board component at src/components/RetrospectiveBoard.tsx implementing the four-column layout, real-time CRUD, voting, and author attribution
Applied to files:
src/components/TemplateSelector.tsx
🧬 Code graph analysis (1)
src/components/TemplateSelector.tsx (2)
src/lib/supabase/types.ts (1)
Tables(549-576)src/lib/boards/templates.ts (2)
boardTemplates(18-260)BoardTemplate(11-16)
🪛 GitHub Check: Run Tests (20.x)
src/components/TemplateSelector.tsx
[failure] 180-180:
Unexpected any. Specify a different type
[failure] 179-179:
Unexpected any. Specify a different type
[failure] 148-148:
Unexpected any. Specify a different type
🪛 GitHub Actions: Tests
src/components/TemplateSelector.tsx
[error] 148-148: Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
🔇 Additional comments (5)
src/components/TemplateSelector.tsx (3)
1-1: Good: client component directive present"use client" is correctly added for an interactive component.
49-60: Type the unified list and safely deserialize JSON columnsCasts from unknown to BoardTemplate["columns"] hide shape errors and caused downstream any usage. Define a small union and a deserializer to normalize DB JSON to the UI shape.
Apply:
+type UnifiedTemplate = BoardTemplate & { + isCustom?: boolean; + isPublic?: boolean; + createdBy?: string | null; +}; + +type DbBoardColumn = { + column_type: string; + title: string; + description?: string | null; + color?: string | null; + icon?: string | null; + display_order?: number | null; +}; + +const deserializeColumns = (val: unknown): BoardTemplate["columns"] => { + if (!Array.isArray(val)) return []; + return (val as DbBoardColumn[]).map((c, i) => ({ + column_type: c?.column_type ?? `column-${i}`, + title: c?.title ?? "", + description: c?.description ?? "", + color: c?.color ?? "bg-green-500/10 border-green-500/20", + icon: c?.icon ?? undefined, + display_order: typeof c?.display_order === "number" ? c.display_order : i, + })); +}; + - const allTemplates = [ + const allTemplates: UnifiedTemplate[] = [ ...boardTemplates, ...customTemplates.map((ct) => ({ id: ct.id, name: ct.name, description: ct.description || "", - columns: (ct.columns as unknown) as BoardTemplate["columns"], + columns: deserializeColumns(ct.columns as unknown), isCustom: true, - isPublic: ct.is_public, - createdBy: ct.created_by, + isPublic: !!ct.is_public, + createdBy: ct.created_by ?? null, })), ];As per coding guidelines.
Also applies to: 23-24
176-181: Fix ESLint “no-explicit-any”: remove any-casts in badgesTemplate is a UnifiedTemplate; no cast needed.
Apply:
- const isCustom = "isCustom" in template && (template as any).isCustom; - const isPublic = "isPublic" in template && (template as any).isPublic; + const isCustom = !!template.isCustom; + const isPublic = !!template.isPublic;supabase/migrations/20250927_add_custom_templates.sql (2)
30-32: Restrict “public” visibility to the viewer’s organization(s)Current policy leaks all is_public=true rows to every authenticated user; PR intent is org‑wide public, not global.
Apply:
-DROP POLICY IF EXISTS "Users can view public templates" ON public.custom_templates; -CREATE POLICY "Users can view public templates" ON public.custom_templates - FOR SELECT USING (is_public = true); +DROP POLICY IF EXISTS "Users can view public templates" ON public.custom_templates; +CREATE POLICY "Users can view org-public templates" ON public.custom_templates + FOR SELECT USING ( + is_public = true AND + organization_id IN ( + SELECT t.organization_id + FROM public.teams t + WHERE t.id IN ( + SELECT tm.team_id FROM public.team_members tm + WHERE tm.profile_id = auth.uid() + ) + ) + );
52-68: Enforce org/team consistency on INSERTRows can be written with mismatched organization_id vs team_id, or with NULL team_id (policy requires membership but NULL bypasses equality). Enforce consistency at policy time.
Apply:
-DROP POLICY IF EXISTS "Users can create templates for their teams" ON public.custom_templates; -CREATE POLICY "Users can create templates for their teams" ON public.custom_templates +DROP POLICY IF EXISTS "Users can create templates for their teams" ON public.custom_templates; +CREATE POLICY "Users can create templates for their teams" ON public.custom_templates FOR INSERT WITH CHECK ( created_by = auth.uid() AND - team_id IN ( + team_id IS NOT NULL AND + team_id IN ( SELECT team_id FROM public.team_members WHERE profile_id = auth.uid() - ) AND - ( - organization_id IS NULL OR - organization_id IN ( - SELECT t.organization_id - FROM public.teams t - JOIN public.team_members tm ON t.id = tm.team_id - WHERE tm.profile_id = auth.uid() - ) - ) + ) AND + organization_id = (SELECT t.organization_id FROM public.teams t WHERE t.id = team_id) );
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
src/components/TemplateSelector.tsx (5)
22-32: Narrow the create callback type to UI-collected fieldsPassing near‑DB row shape from UI invites nulls for RLS‑checked fields. Accept only name/description here; let the caller enrich with auth/team/org before insert.
interface TemplateSelectorProps { selectedTemplateId: string; onTemplateSelect: (templateId: string) => void; customTemplates?: Tables<"custom_templates">[]; showCustomTemplateButton?: boolean; - onCreateCustomTemplate?: (template: Omit<Tables<"custom_templates">, "id" | "created_at" | "updated_at">) => void; + // UI gathers only basic metadata; server/hook adds auth/team/org/columns. + onCreateCustomTemplate?: (template: Pick<Tables<"custom_templates">, "name" | "description">) => void; userPreferences?: { preferred_template_id?: string | null; recent_templates?: string[]; }; }As per coding guidelines (use generated Supabase types).
60-75: Avoid in-place sort (minor purity/readability)Sorting in place mutates the local array. Copy before sort for clarity.
- const sortedTemplates = allTemplates.sort((a, b) => { + const sortedTemplates = [...allTemplates].sort((a, b) => {
86-94: Button condition and label mismatch with behaviorThe button shows only an info dialog; it doesn’t use the callback. Don’t gate it on
onCreateCustomTemplate, and adjust the label to set expectations.- {showCustomTemplateButton && onCreateCustomTemplate && ( + {showCustomTemplateButton && ( <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> <DialogTrigger asChild> <Button variant="outline" size="sm"> - <Plus className="mr-2 h-4 w-4" /> - Create Custom Template + <Plus aria-hidden="true" className="mr-2 h-4 w-4" /> + How to create a custom template </Button> </DialogTrigger>
149-155: Accessibility: mark decorative icons aria-hiddenThese icons convey no additional meaning; hide them from screen readers.
- <Layout className="h-4 w-4 text-muted-foreground" /> + <Layout aria-hidden="true" className="h-4 w-4 text-muted-foreground" />- <Star className="mr-1 h-3 w-3" /> + <Star aria-hidden="true" className="mr-1 h-3 w-3" />- <Globe className="mr-1 h-3 w-3" /> + <Globe aria-hidden="true" className="mr-1 h-3 w-3" />- <Users className="mr-1 h-3 w-3" /> + <Users aria-hidden="true" className="mr-1 h-3 w-3" />- <Plus className="mr-2 h-4 w-4" /> + <Plus aria-hidden="true" className="mr-2 h-4 w-4" />As per accessibility best practices.
Also applies to: 168-174, 90-90
185-193: Use a stable key for columnsAvoid index keys to prevent remounts on reordering;
column_typeis stable.- {template.columns.map((column, index) => ( + {template.columns.map((column) => ( <Badge - key={index} + key={column.column_type} variant="secondary" className="text-xs" > {column.title} </Badge> ))}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/components/TemplateSelector.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/components/**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Add the "use client" directive to components that require client-side interactivity
Files:
src/components/TemplateSelector.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use the @/* path alias for imports that target ./src/*
Use generated database types from src/lib/supabase/types.ts for Supabase-related TypeScript typings
Files:
src/components/TemplateSelector.tsx
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.{ts,tsx}: In client-side code, import the Supabase browser client from src/lib/supabase/client.ts
In server-side code, import the Supabase server client from src/lib/supabase/server.ts
Files:
src/components/TemplateSelector.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Enable and rely on TypeScript strict mode
Applied to files:
src/components/TemplateSelector.tsx
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Applies to src/components/RetrospectiveBoard.tsx : Keep the main retrospective board component at src/components/RetrospectiveBoard.tsx implementing the four-column layout, real-time CRUD, voting, and author attribution
Applied to files:
src/components/TemplateSelector.tsx
🧬 Code graph analysis (1)
src/components/TemplateSelector.tsx (2)
src/lib/supabase/types.ts (1)
Tables(549-576)src/lib/boards/templates.ts (2)
boardTemplates(18-260)BoardTemplate(11-16)
🪛 GitHub Check: Run Tests (20.x)
src/components/TemplateSelector.tsx
[failure] 133-133:
Unexpected any. Specify a different type
[failure] 132-132:
Unexpected any. Specify a different type
[failure] 107-107:
" can be escaped with ", “, ", ”
[failure] 107-107:
" can be escaped with ", “, ", ”
🪛 GitHub Actions: Tests
src/components/TemplateSelector.tsx
[error] 107-107: " can be escaped with ", “, ", ” react/no-unescaped-entities
🔇 Additional comments (3)
src/components/TemplateSelector.tsx (3)
1-1: Good: client component directive present"use client" is correctly set for an interactive component.
44-55: Unify types and safely deserialize custom columns (remove unsafe casts)The
(ct.columns as unknown) as BoardTemplate["columns"]cast hides shape issues. Define a unified template type and a small deserializer for JSON columns.const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + type UnifiedTemplate = BoardTemplate & { + isCustom?: boolean; + isPublic?: boolean; + createdBy?: string | null; + }; + + type DbBoardColumn = { + column_type?: string; + title?: string; + description?: string | null; + color?: string | null; + icon?: string | null; + display_order?: number | null; + }; + + const deserializeColumns = (val: unknown): BoardTemplate["columns"] => { + if (!Array.isArray(val)) return []; + return (val as DbBoardColumn[]).map((c, i) => ({ + column_type: c?.column_type ?? `column-${i}`, + title: c?.title ?? "", + description: c?.description ?? "", + color: c?.color ?? "bg-green-500/10 border-green-500/20", + icon: c?.icon ?? undefined, + display_order: typeof c?.display_order === "number" ? c.display_order : i, + })); + }; + - const allTemplates = [ + const allTemplates: UnifiedTemplate[] = [ ...boardTemplates, ...customTemplates.map((ct) => ({ id: ct.id, name: ct.name, - description: ct.description || "", - columns: (ct.columns as unknown) as BoardTemplate["columns"], + description: ct.description ?? "", + columns: deserializeColumns(ct.columns as unknown), isCustom: true, isPublic: ct.is_public, createdBy: ct.created_by, })), ];Based on learnings.
132-134: Fix ESLint no-explicit-any by using the unified typeRemove
(template as any)and rely on the optional flags from UnifiedTemplate.- const isCustom = "isCustom" in template && (template as any).isCustom; - const isPublic = "isPublic" in template && (template as any).isPublic; + const isCustom = !!template.isCustom; + const isPublic = !!template.isPublic;#!/bin/bash # Verify no remaining any-casts in this file rg -n "as any" src/components/TemplateSelector.tsx || echo "No any-casts found"
- Fix unescaped quotes in TemplateSelector JSX - Replace any types with proper type annotations - Fix Json type usage in template preferences - Ensure proper type casting for custom template columns
- Fix mad template icon: AlertTriangle -> Frown - Fix sad template icon: Frown -> Meh - Fix longed-for template icon: Target -> Star - Add missing Meh and Star icon imports Icons now match the template definitions in templates.ts
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/components/TemplateSelector.tsx(1 hunks)src/hooks/use-templates.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/components/**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Add the "use client" directive to components that require client-side interactivity
Files:
src/components/TemplateSelector.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use the @/* path alias for imports that target ./src/*
Use generated database types from src/lib/supabase/types.ts for Supabase-related TypeScript typings
Files:
src/components/TemplateSelector.tsxsrc/hooks/use-templates.ts
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.{ts,tsx}: In client-side code, import the Supabase browser client from src/lib/supabase/client.ts
In server-side code, import the Supabase server client from src/lib/supabase/server.ts
Files:
src/components/TemplateSelector.tsxsrc/hooks/use-templates.ts
🧠 Learnings (3)
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Enable and rely on TypeScript strict mode
Applied to files:
src/components/TemplateSelector.tsx
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Applies to src/components/RetrospectiveBoard.tsx : Keep the main retrospective board component at src/components/RetrospectiveBoard.tsx implementing the four-column layout, real-time CRUD, voting, and author attribution
Applied to files:
src/components/TemplateSelector.tsx
📚 Learning: 2025-09-27T16:07:41.169Z
Learnt from: CR
PR: TheEagleByte/scrumkit#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-27T16:07:41.169Z
Learning: Applies to **/*.{ts,tsx} : Use generated database types from src/lib/supabase/types.ts for Supabase-related TypeScript typings
Applied to files:
src/hooks/use-templates.ts
🧬 Code graph analysis (2)
src/components/TemplateSelector.tsx (2)
src/lib/supabase/types.ts (1)
Tables(549-576)src/lib/boards/templates.ts (2)
boardTemplates(18-260)BoardTemplate(11-16)
src/hooks/use-templates.ts (2)
src/lib/supabase/types.ts (3)
Tables(549-576)TablesInsert(578-601)Json(1-7)src/lib/boards/templates.ts (2)
BoardTemplate(11-16)BoardColumn(2-9)
🔇 Additional comments (1)
src/components/TemplateSelector.tsx (1)
44-55: Guard against malformed custom template columns before rendering
ct.columnscomes back from Supabase as genericJson, yet we immediately coerce it intoBoardTemplate["columns"]and later call.map. If a row ever carriesnull, an object, or otherwise malformed JSON (legacy data, manual insert, failed serialization), this cast will blow up the selector at runtime. Please funnel the payload through a small deserializer that verifies the shape and supplies sane fallbacks before rendering.+const deserializeColumns = (value: Tables<'custom_templates'>['columns']): BoardTemplate['columns'] => { + if (!Array.isArray(value)) { + return []; + } + + return value.map((column, index) => ({ + column_type: typeof column?.column_type === "string" ? column.column_type : `column-${index}`, + title: typeof column?.title === "string" ? column.title : "", + description: typeof column?.description === "string" ? column.description ?? "" : "", + color: typeof column?.color === "string" ? column.color : "bg-green-500/10 border-green-500/20", + icon: typeof column?.icon === "string" ? column.icon : undefined, + display_order: typeof column?.display_order === "number" ? column.display_order : index, + })); +}; ... - columns: (ct.columns as unknown) as BoardTemplate["columns"], + columns: deserializeColumns(ct.columns),
- Add deserializeColumns function to safely handle malformed custom template data - Guard against null, non-array, or invalid column data from database - Add X, Check, and Plus icons to test's validIcons list for DAKI template - Prevents runtime errors when custom templates have corrupted column data Addresses CodeRabbit review feedback
Summary
What's Changed
Templates Added
Database Changes
custom_templatestable for user-created templatespreferred_template_idandtemplate_preferencescolumns to profiles tableUI Components
Features
Test Plan
Fixes #13
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Enhancements
Chores