import { useMemo } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { Button, toast } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { HttpTypes } from "@medusajs/types" import { ColumnDef } from "@tanstack/react-table" import { RouteFocusModal, useRouteModal } from "../../../components/modals" import { KeyboundForm } from "../../../components/utilities/keybound-form" import { useBatchPropertyLabels } from "../../../hooks/api/property-labels" import { DataGrid, createDataGridHelper } from "../../../components/data-grid" import { VisuallyHidden } from "../../../components/utilities/visually-hidden" interface PropertyFieldRow { field: string } interface PropertyLabelsEditFormProps { entity: string columns: HttpTypes.AdminColumn[] propertyLabels: HttpTypes.AdminPropertyLabel[] } const isLeafNode = (obj: any): boolean => { if (typeof obj !== "object" || obj === null) return false const keys = Object.keys(obj) return ( keys.length > 0 && keys.every((key) => key === "label" || key === "description") ) } const propertyLabelValueSchema = z.object({ label: z.string().optional(), description: z.string().optional(), }) type PropertyLabelValue = z.infer interface PropertyLabelSchema { [key: string]: PropertyLabelValue | PropertyLabelSchema } const propertyLabelSchema: z.ZodType = z.lazy(() => z.record(z.string(), z.any()).superRefine((obj, ctx) => { Object.entries(obj).forEach(([key, value]) => { if (isLeafNode(value)) { const result = propertyLabelValueSchema.safeParse(value) if (!result.success) { result.error.issues.forEach((issue) => { ctx.addIssue({ ...issue, path: [key, ...issue.path], }) }) } const typedValue = value as PropertyLabelValue if (typedValue.description && typedValue.description.trim() !== "") { if (!typedValue.label || typedValue.label.trim() === "") { ctx.addIssue({ code: "custom", message: "Label is required when description is provided", path: [key, "label"], }) } } } else if (typeof value === "object" && value !== null) { const result = propertyLabelSchema.safeParse(value) if (!result.success) { result.error.issues.forEach((issue) => { ctx.addIssue({ ...issue, path: [key, ...issue.path], }) }) } } }) }) ) const columnHelper = createDataGridHelper< PropertyFieldRow, PropertyLabelSchema >() export const PropertyLabelsEditForm = ({ entity, columns, propertyLabels, }: PropertyLabelsEditFormProps) => { const { t } = useTranslation() const { handleSuccess } = useRouteModal() const batchMutation = useBatchPropertyLabels() const buildNestedInitialData = ( labels: HttpTypes.AdminPropertyLabel[] ): PropertyLabelSchema => { const result: any = {} labels.forEach((label) => { const parts = label.property.split(".") let current = result parts.forEach((part, index) => { if (index === parts.length - 1) { current[part] = { label: label.label, description: label.description || "", } } else { if (!current[part]) { current[part] = {} } current = current[part] } }) }) return result } const initialData = buildNestedInitialData(propertyLabels) const form = useForm({ resolver: zodResolver(propertyLabelSchema), defaultValues: initialData, }) const rows: PropertyFieldRow[] = useMemo(() => { return columns.map((column) => ({ field: column.field, })) }, [columns]) const gridColumns: ColumnDef[] = useMemo(() => { return [ columnHelper.column({ id: "field", name: "field", header: t("fields.field", "Field"), cell: (context) => { const row = context.row.original return ( {row.field} ) }, disableHiding: true, minSize: 250, }), columnHelper.column({ id: "label", name: "label", header: t("propertyLabels.customLabel", "Custom Label"), type: "text", field: (context) => { const row = context.row.original return `${row.field}.label` }, cell: (context) => { return }, disableHiding: true, minSize: 250, }), columnHelper.column({ id: "description", name: "description", header: t("fields.description", "Description"), type: "multiline-text", field: (context) => { const row = context.row.original return `${row.field}.description` }, cell: (context) => { return }, minSize: 400, }), ] }, [t]) const flattenData = ( obj: any, prefix = "" ): Array<[string, PropertyLabelValue]> => { const results: Array<[string, PropertyLabelValue]> = [] Object.entries(obj).forEach(([key, value]) => { const fullPath = prefix ? `${prefix}.${key}` : key if (value && typeof value === "object") { const keys = Object.keys(value) const isLeaf = keys.every((k) => k === "label" || k === "description") if (isLeaf) { results.push([fullPath, value as PropertyLabelValue]) } else { results.push(...flattenData(value, fullPath)) } } }) return results } const handleSubmit = form.handleSubmit(async (data) => { const create: HttpTypes.AdminCreatePropertyLabel[] = [] const update: NonNullable = [] const deleteIds: string[] = [] const labelMap = new Map() propertyLabels?.forEach((label) => { labelMap.set(label.property, label) }) const flatEntries = flattenData(data) flatEntries.forEach(([property, values]) => { const existingLabel = labelMap.get(property) const newLabel = values.label?.trim() const newDescription = values.description?.trim() if (newLabel) { if (existingLabel) { if ( existingLabel.label !== newLabel || (existingLabel.description || "") !== (newDescription || "") ) { update.push({ id: existingLabel.id, label: newLabel, description: newDescription ?? null, }) } } else { create.push({ entity, property, label: newLabel, description: newDescription || undefined, }) } } else if (existingLabel) { deleteIds.push(existingLabel.id) } }) if (!create.length && !update.length && !deleteIds.length) { return } await batchMutation.mutateAsync( { create: create.length > 0 ? create : undefined, update: update.length > 0 ? update : undefined, delete: deleteIds.length > 0 ? deleteIds : undefined, }, { onSuccess: () => { toast.success( t( "propertyLabels.updateSuccess", "Property labels updated successfully" ) ) handleSuccess() }, onError: (error) => { toast.error(error.message) }, } ) }) return ( {t("propertyLabels.editTitle", "Edit Property Labels")} {t( "propertyLabels.editDescription", "Customize how property names are displayed for {{entity}}", { entity } )}
) }