feat: manage canvases with custom name and icon

Make designs (canvases) fully user-manageable: create with a chosen name
and icon, rename, change icon, and delete. Replaces the hardcoded
"New Electrical Design" button with a generic "New Canvas" flow.

- Add Design.icon column + migration that backfills legacy rows
  (electrical -> zap, others -> dashboard)
- DesignModal: name input + curated lucide icon picker (create + edit)
- Sidebar switcher gains per-canvas edit/delete; delete guards the last
  canvas and confirms
- designStore: addDesign/updateDesign/removeDesign with active reassignment
- Fix data loss on design switch: abort load when the save fails and keep
  unsaved edits; skip the save-old step when the previous canvas was deleted
- designsApi create/update carry icon; design_type kept for back-compat

Tests: backend design CRUD (icon + cascade + last-canvas guard), designStore
actions, designIcons resolver, DesignModal create/edit/validation.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-02 15:47:57 +02:00
parent eb7b0c6d38
commit 12d527aad6
15 changed files with 693 additions and 42 deletions
+30
View File
@@ -9,6 +9,12 @@ interface DesignState {
setDesigns: (designs: Design[]) => void
setActiveDesign: (id: string) => void
getActiveDesign: () => Design | null
/** Append a new design and make it active. */
addDesign: (design: Design) => void
/** Patch an existing design in place (name/icon edits). */
updateDesign: (id: string, patch: Partial<Pick<Design, 'name' | 'icon'>>) => void
/** Remove a design; if it was active, fall back to the first remaining one. */
removeDesign: (id: string) => void
}
export const useDesignStore = create<DesignState>((set, get) => ({
@@ -39,4 +45,28 @@ export const useDesignStore = create<DesignState>((set, get) => ({
const { designs, activeDesignId } = get()
return designs.find((d) => d.id === activeDesignId) ?? null
},
addDesign: (design) =>
set((state) => ({
designs: [...state.designs, design],
activeDesignId: design.id,
activeDesignType: design.design_type,
})),
updateDesign: (id, patch) =>
set((state) => ({
designs: state.designs.map((d) => (d.id === id ? { ...d, ...patch } : d)),
})),
removeDesign: (id) =>
set((state) => {
const designs = state.designs.filter((d) => d.id !== id)
if (state.activeDesignId !== id) return { designs }
const next = designs[0] ?? null
return {
designs,
activeDesignId: next?.id ?? null,
activeDesignType: next?.design_type ?? null,
}
}),
}))