Add manufacturer catalog, category/manufacturer library management, and built-in cable flag
Normalizes manufacturer as a shared catalog entity (like device categories) instead of free text on each device, giving the admin duplicate-detection nudge a reliable signal. Adds full CRUD (including Admin direct-publish, bypassing the submission queue) for categories, manufacturers, port types, and cable types, plus a Categories & Manufacturers library modal and a browse-by-manufacturer/search view in the device palette. Adds Port.builtInCable so a captive/permanently-attached cable (a keyboard's USB lead, a budget AVR's power cord) can be flagged and excluded from the BOM. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
+153
-2
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
import type { CableType, Catalog, DeviceTemplate, PortType } from '../domain/types'
|
||||
import type { CatalogRepository } from '../data/CatalogRepository'
|
||||
import { cableTypeToRow, deviceTemplateToRow, portTypeToRow } from '../data/catalogRowMapping'
|
||||
import { cableTypeToRow, deviceCategoryToRow, deviceTemplateToRow, manufacturerToRow, portTypeToRow } from '../data/catalogRowMapping'
|
||||
import { SupabaseCatalogRepository } from '../data/SupabaseCatalogRepository'
|
||||
import type { CatalogEntityType } from '../data/SubmissionRepository'
|
||||
import { useSubmissionStore } from './submissionStore'
|
||||
@@ -29,7 +29,7 @@ function saveHiddenPublicIds(ids: Set<string>): void {
|
||||
}
|
||||
}
|
||||
|
||||
const emptyCatalog: Catalog = { portTypes: [], cableTypes: [], deviceCategories: [], deviceTemplates: [] }
|
||||
const emptyCatalog: Catalog = { portTypes: [], cableTypes: [], deviceCategories: [], manufacturers: [], deviceTemplates: [] }
|
||||
|
||||
interface CatalogStoreState {
|
||||
catalog: Catalog
|
||||
@@ -39,12 +39,23 @@ interface CatalogStoreState {
|
||||
loadCatalog: () => Promise<void>
|
||||
|
||||
addCustomDeviceCategory: (name: string) => Promise<string>
|
||||
updateCustomDeviceCategory: (id: string, name: string) => Promise<void>
|
||||
removeCustomDeviceCategory: (id: string) => Promise<void>
|
||||
|
||||
addCustomManufacturer: (name: string) => Promise<string>
|
||||
updateCustomManufacturer: (id: string, name: string) => Promise<void>
|
||||
removeCustomManufacturer: (id: string) => Promise<void>
|
||||
|
||||
addCustomPortType: (portType: Omit<PortType, 'id' | 'custom'>) => Promise<void>
|
||||
updateCustomPortType: (id: string, patch: Partial<Omit<PortType, 'id' | 'custom'>>) => Promise<void>
|
||||
/** Throws (like removeCustomDeviceCategory) if the delete is rejected —
|
||||
* most commonly a foreign-key violation because a device template's port
|
||||
* still references it. */
|
||||
removeCustomPortType: (id: string) => Promise<void>
|
||||
|
||||
addCustomCableType: (cableType: Omit<CableType, 'id' | 'custom'>) => Promise<void>
|
||||
updateCustomCableType: (id: string, patch: Partial<Omit<CableType, 'id' | 'custom'>>) => Promise<void>
|
||||
removeCustomCableType: (id: string) => Promise<void>
|
||||
|
||||
addCustomDeviceTemplate: (template: Omit<DeviceTemplate, 'id' | 'custom'>) => Promise<void>
|
||||
updateCustomDeviceTemplate: (id: string, patch: Partial<Omit<DeviceTemplate, 'id' | 'custom'>>) => Promise<void>
|
||||
@@ -53,15 +64,35 @@ interface CatalogStoreState {
|
||||
hidePublicDeviceTemplate: (id: string) => void
|
||||
restorePublicDeviceTemplate: (id: string) => void
|
||||
|
||||
/** Admin/Super-Admin direct-to-public creation — skips the submission/
|
||||
* review workflow, for authoring the shared library yourself (see
|
||||
* CatalogRepository.adminAddDeviceCategory's own comment). */
|
||||
adminAddDeviceCategory: (name: string) => Promise<string>
|
||||
adminAddManufacturer: (name: string) => Promise<string>
|
||||
adminAddPortType: (portType: Omit<PortType, 'id' | 'custom'>) => Promise<void>
|
||||
adminAddCableType: (cableType: Omit<CableType, 'id' | 'custom'>) => Promise<void>
|
||||
adminAddDeviceTemplate: (template: Omit<DeviceTemplate, 'id' | 'custom'>) => Promise<void>
|
||||
|
||||
/** Admin/Super-Admin direct edit of an already-public entry, per
|
||||
* organized-ideas.md §6 — bypasses the submission/review workflow
|
||||
* entirely (RLS's is_admin() clause is what actually permits it). */
|
||||
adminUpdatePortType: (id: string, patch: Partial<Omit<PortType, 'id' | 'custom'>>) => Promise<void>
|
||||
adminUpdateCableType: (id: string, patch: Partial<Omit<CableType, 'id' | 'custom'>>) => Promise<void>
|
||||
adminUpdateDeviceTemplate: (id: string, patch: Partial<Omit<DeviceTemplate, 'id' | 'custom'>>) => Promise<void>
|
||||
adminUpdateDeviceCategory: (id: string, name: string) => Promise<void>
|
||||
adminUpdateManufacturer: (id: string, name: string) => Promise<void>
|
||||
/** Unpublishes (is_public -> false) rather than deletes — see
|
||||
* CatalogRepository.adminUnpublish. */
|
||||
adminUnpublish: (entityType: CatalogEntityType, id: string) => Promise<void>
|
||||
|
||||
/** Permanent delete of a built-in entry — see
|
||||
* CatalogRepository.adminRemoveDeviceCategory's own comment for the
|
||||
* zero-usage contract the caller is expected to have already checked. */
|
||||
adminRemoveDeviceCategory: (id: string) => Promise<void>
|
||||
adminRemoveManufacturer: (id: string) => Promise<void>
|
||||
adminRemovePortType: (id: string) => Promise<void>
|
||||
adminRemoveCableType: (id: string) => Promise<void>
|
||||
adminRemoveDeviceTemplate: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const useCatalogStore = create<CatalogStoreState>((set, get) => ({
|
||||
@@ -80,11 +111,70 @@ export const useCatalogStore = create<CatalogStoreState>((set, get) => ({
|
||||
return category.id
|
||||
},
|
||||
|
||||
updateCustomDeviceCategory: async (id, name) => {
|
||||
await repository.updateDeviceCategory(id, name)
|
||||
set((state) => ({
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
deviceCategories: state.catalog.deviceCategories.map((c) => (c.id === id ? { ...c, name } : c)),
|
||||
},
|
||||
}))
|
||||
useSubmissionStore.getState().syncProposedData('device_category', id, deviceCategoryToRow(name))
|
||||
},
|
||||
|
||||
removeCustomDeviceCategory: async (id) => {
|
||||
await repository.removeDeviceCategory(id)
|
||||
set((state) => ({
|
||||
catalog: { ...state.catalog, deviceCategories: state.catalog.deviceCategories.filter((c) => c.id !== id) },
|
||||
}))
|
||||
},
|
||||
|
||||
adminAddDeviceCategory: async (name) => {
|
||||
const category = await repository.adminAddDeviceCategory(name)
|
||||
set((state) => ({ catalog: { ...state.catalog, deviceCategories: [...state.catalog.deviceCategories, category] } }))
|
||||
return category.id
|
||||
},
|
||||
|
||||
addCustomManufacturer: async (name) => {
|
||||
const manufacturer = await repository.addManufacturer(name)
|
||||
set((state) => ({ catalog: { ...state.catalog, manufacturers: [...state.catalog.manufacturers, manufacturer] } }))
|
||||
return manufacturer.id
|
||||
},
|
||||
|
||||
updateCustomManufacturer: async (id, name) => {
|
||||
await repository.updateManufacturer(id, name)
|
||||
set((state) => ({
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
manufacturers: state.catalog.manufacturers.map((m) => (m.id === id ? { ...m, name } : m)),
|
||||
},
|
||||
}))
|
||||
useSubmissionStore.getState().syncProposedData('manufacturer', id, manufacturerToRow(name))
|
||||
},
|
||||
|
||||
removeCustomManufacturer: async (id) => {
|
||||
await repository.removeManufacturer(id)
|
||||
set((state) => ({
|
||||
catalog: { ...state.catalog, manufacturers: state.catalog.manufacturers.filter((m) => m.id !== id) },
|
||||
}))
|
||||
},
|
||||
|
||||
adminAddManufacturer: async (name) => {
|
||||
const manufacturer = await repository.adminAddManufacturer(name)
|
||||
set((state) => ({ catalog: { ...state.catalog, manufacturers: [...state.catalog.manufacturers, manufacturer] } }))
|
||||
return manufacturer.id
|
||||
},
|
||||
|
||||
addCustomPortType: async (portType) => {
|
||||
const created = await repository.addPortType(portType)
|
||||
set((state) => ({ catalog: { ...state.catalog, portTypes: [...state.catalog.portTypes, created] } }))
|
||||
},
|
||||
|
||||
adminAddPortType: async (portType) => {
|
||||
const created = await repository.adminAddPortType(portType)
|
||||
set((state) => ({ catalog: { ...state.catalog, portTypes: [...state.catalog.portTypes, created] } }))
|
||||
},
|
||||
|
||||
updateCustomPortType: async (id, patch) => {
|
||||
await repository.updatePortType(id, patch)
|
||||
set((state) => ({
|
||||
@@ -97,11 +187,21 @@ export const useCatalogStore = create<CatalogStoreState>((set, get) => ({
|
||||
if (updated) useSubmissionStore.getState().syncProposedData('port_type', id, portTypeToRow(updated))
|
||||
},
|
||||
|
||||
removeCustomPortType: async (id) => {
|
||||
await repository.removePortType(id)
|
||||
set((state) => ({ catalog: { ...state.catalog, portTypes: state.catalog.portTypes.filter((pt) => pt.id !== id) } }))
|
||||
},
|
||||
|
||||
addCustomCableType: async (cableType) => {
|
||||
const created = await repository.addCableType(cableType)
|
||||
set((state) => ({ catalog: { ...state.catalog, cableTypes: [...state.catalog.cableTypes, created] } }))
|
||||
},
|
||||
|
||||
adminAddCableType: async (cableType) => {
|
||||
const created = await repository.adminAddCableType(cableType)
|
||||
set((state) => ({ catalog: { ...state.catalog, cableTypes: [...state.catalog.cableTypes, created] } }))
|
||||
},
|
||||
|
||||
updateCustomCableType: async (id, patch) => {
|
||||
await repository.updateCableType(id, patch)
|
||||
set((state) => ({
|
||||
@@ -114,11 +214,21 @@ export const useCatalogStore = create<CatalogStoreState>((set, get) => ({
|
||||
if (updated) useSubmissionStore.getState().syncProposedData('cable_type', id, cableTypeToRow(updated))
|
||||
},
|
||||
|
||||
removeCustomCableType: async (id) => {
|
||||
await repository.removeCableType(id)
|
||||
set((state) => ({ catalog: { ...state.catalog, cableTypes: state.catalog.cableTypes.filter((ct) => ct.id !== id) } }))
|
||||
},
|
||||
|
||||
addCustomDeviceTemplate: async (template) => {
|
||||
const created = await repository.addDeviceTemplate(template)
|
||||
set((state) => ({ catalog: { ...state.catalog, deviceTemplates: [...state.catalog.deviceTemplates, created] } }))
|
||||
},
|
||||
|
||||
adminAddDeviceTemplate: async (template) => {
|
||||
const created = await repository.adminAddDeviceTemplate(template)
|
||||
set((state) => ({ catalog: { ...state.catalog, deviceTemplates: [...state.catalog.deviceTemplates, created] } }))
|
||||
},
|
||||
|
||||
updateCustomDeviceTemplate: async (id, patch) => {
|
||||
await repository.updateDeviceTemplate(id, patch)
|
||||
set((state) => ({
|
||||
@@ -174,8 +284,49 @@ export const useCatalogStore = create<CatalogStoreState>((set, get) => ({
|
||||
await get().loadCatalog()
|
||||
},
|
||||
|
||||
adminUpdateDeviceCategory: async (id, name) => {
|
||||
await repository.adminUpdateDeviceCategory(id, name)
|
||||
await get().loadCatalog()
|
||||
},
|
||||
|
||||
adminUpdateManufacturer: async (id, name) => {
|
||||
await repository.adminUpdateManufacturer(id, name)
|
||||
await get().loadCatalog()
|
||||
},
|
||||
|
||||
adminUnpublish: async (entityType, id) => {
|
||||
await repository.adminUnpublish(entityType, id)
|
||||
await get().loadCatalog()
|
||||
},
|
||||
|
||||
adminRemoveDeviceCategory: async (id) => {
|
||||
await repository.adminRemoveDeviceCategory(id)
|
||||
set((state) => ({
|
||||
catalog: { ...state.catalog, deviceCategories: state.catalog.deviceCategories.filter((c) => c.id !== id) },
|
||||
}))
|
||||
},
|
||||
|
||||
adminRemoveManufacturer: async (id) => {
|
||||
await repository.adminRemoveManufacturer(id)
|
||||
set((state) => ({
|
||||
catalog: { ...state.catalog, manufacturers: state.catalog.manufacturers.filter((m) => m.id !== id) },
|
||||
}))
|
||||
},
|
||||
|
||||
adminRemovePortType: async (id) => {
|
||||
await repository.adminRemovePortType(id)
|
||||
set((state) => ({ catalog: { ...state.catalog, portTypes: state.catalog.portTypes.filter((pt) => pt.id !== id) } }))
|
||||
},
|
||||
|
||||
adminRemoveCableType: async (id) => {
|
||||
await repository.adminRemoveCableType(id)
|
||||
set((state) => ({ catalog: { ...state.catalog, cableTypes: state.catalog.cableTypes.filter((ct) => ct.id !== id) } }))
|
||||
},
|
||||
|
||||
adminRemoveDeviceTemplate: async (id) => {
|
||||
await repository.adminRemoveDeviceTemplate(id)
|
||||
set((state) => ({
|
||||
catalog: { ...state.catalog, deviceTemplates: state.catalog.deviceTemplates.filter((t) => t.id !== id) },
|
||||
}))
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user