@open-mercato/core 0.6.4-develop.3929.1.fcf7afece2 → 0.6.4-develop.3944.1.4100aa7fbe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/backend/customers/deals/create/page.js +3 -61
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -0
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js +233 -0
- package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsField.js +209 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsSection.js +67 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsSection.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCreateSidebar.js +73 -0
- package/dist/modules/customers/components/detail/create/DealCreateSidebar.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCurrencyField.js +92 -0
- package/dist/modules/customers/components/detail/create/DealCurrencyField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCustomAttributes.js +81 -0
- package/dist/modules/customers/components/detail/create/DealCustomAttributes.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealDetailsFields.js +171 -0
- package/dist/modules/customers/components/detail/create/DealDetailsFields.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealFormField.js +24 -0
- package/dist/modules/customers/components/detail/create/DealFormField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealSectionCard.js +29 -0
- package/dist/modules/customers/components/detail/create/DealSectionCard.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealTipsCard.js +19 -0
- package/dist/modules/customers/components/detail/create/DealTipsCard.js.map +7 -0
- package/dist/modules/customers/components/detail/create/PipelineSelect.js +41 -0
- package/dist/modules/customers/components/detail/create/PipelineSelect.js.map +7 -0
- package/dist/modules/customers/components/detail/create/PipelineStageSelect.js +49 -0
- package/dist/modules/customers/components/detail/create/PipelineStageSelect.js.map +7 -0
- package/dist/modules/customers/components/detail/create/SuffixInput.js +21 -0
- package/dist/modules/customers/components/detail/create/SuffixInput.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +270 -0
- package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealFormTypes.js +17 -0
- package/dist/modules/customers/components/detail/create/dealFormTypes.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealNumericInput.js +16 -0
- package/dist/modules/customers/components/detail/create/dealNumericInput.js.map +7 -0
- package/dist/modules/customers/components/detail/create/useDealCustomFields.js +93 -0
- package/dist/modules/customers/components/detail/create/useDealCustomFields.js.map +7 -0
- package/dist/modules/customers/components/detail/create/useDealPipelines.js +59 -0
- package/dist/modules/customers/components/detail/create/useDealPipelines.js.map +7 -0
- package/dist/modules/customers/components/formConfig.js +4 -2
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +5 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/backend/customers/deals/create/page.tsx +3 -64
- package/src/modules/customers/components/detail/DealForm.tsx +2 -0
- package/src/modules/customers/components/detail/create/CreateDealForm.tsx +254 -0
- package/src/modules/customers/components/detail/create/DealAssociationsField.tsx +253 -0
- package/src/modules/customers/components/detail/create/DealAssociationsSection.tsx +72 -0
- package/src/modules/customers/components/detail/create/DealCreateSidebar.tsx +79 -0
- package/src/modules/customers/components/detail/create/DealCurrencyField.tsx +108 -0
- package/src/modules/customers/components/detail/create/DealCustomAttributes.tsx +118 -0
- package/src/modules/customers/components/detail/create/DealDetailsFields.tsx +171 -0
- package/src/modules/customers/components/detail/create/DealFormField.tsx +39 -0
- package/src/modules/customers/components/detail/create/DealSectionCard.tsx +40 -0
- package/src/modules/customers/components/detail/create/DealTipsCard.tsx +26 -0
- package/src/modules/customers/components/detail/create/PipelineSelect.tsx +55 -0
- package/src/modules/customers/components/detail/create/PipelineStageSelect.tsx +70 -0
- package/src/modules/customers/components/detail/create/SuffixInput.tsx +20 -0
- package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +310 -0
- package/src/modules/customers/components/detail/create/dealFormTypes.ts +29 -0
- package/src/modules/customers/components/detail/create/dealNumericInput.ts +20 -0
- package/src/modules/customers/components/detail/create/useDealCustomFields.ts +118 -0
- package/src/modules/customers/components/detail/create/useDealPipelines.ts +80 -0
- package/src/modules/customers/components/formConfig.tsx +3 -0
- package/src/modules/customers/i18n/de.json +26 -0
- package/src/modules/customers/i18n/en.json +26 -0
- package/src/modules/customers/i18n/es.json +26 -0
- package/src/modules/customers/i18n/pl.json +26 -0
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +12 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dictionaries/components/DictionaryEntrySelect.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { Plus, Settings, Save } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from '@open-mercato/ui/primitives/dialog'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { buildHrefWithReturnTo } from '@open-mercato/shared/lib/navigation/returnTo'\nimport { DictionaryValue, renderDictionaryColor, renderDictionaryIcon } from './dictionaryAppearance'\nimport { AppearanceSelector, type AppearanceSelectorLabels, useAppearanceState } from './AppearanceSelector'\n\nconst DEFAULT_APPEARANCE_LABELS: AppearanceSelectorLabels = {\n colorLabel: 'Color',\n colorHelp: 'Pick a highlight color for this entry.',\n colorClearLabel: 'Remove color',\n iconLabel: 'Icon or emoji',\n iconPlaceholder: 'Type an emoji or icon token.',\n iconPickerTriggerLabel: 'Browse icons and emoji',\n iconSearchPlaceholder: 'Search icons or emojis\u2026',\n iconSearchEmptyLabel: 'No icons match your search.',\n iconSuggestionsLabel: 'Suggestions',\n iconClearLabel: 'Remove icon',\n previewEmptyLabel: 'No appearance selected',\n}\n\nexport type DictionaryOption = {\n value: string\n label: string\n color: string | null\n icon: string | null\n}\n\nexport type DictionarySelectLabels = {\n placeholder: string\n addLabel: string\n addPrompt?: string\n dialogTitle: string\n valueLabel: string\n valuePlaceholder: string\n labelLabel: string\n labelPlaceholder: string\n emptyError: string\n cancelLabel: string\n saveLabel: string\n saveShortcutHint?: string\n successCreateLabel?: string\n errorLoad: string\n errorSave: string\n loadingLabel: string\n manageTitle: string\n}\n\nexport type DictionaryEntrySelectProps = {\n value?: string\n onChange: (value: string | undefined) => void\n fetchOptions: () => Promise<DictionaryOption[]>\n createOption?: (input: { value: string; label?: string; color?: string | null; icon?: string | null }) => Promise<DictionaryOption | null>\n labels: DictionarySelectLabels\n manageHref?: string\n selectClassName?: string\n allowInlineCreate?: boolean\n allowAppearance?: boolean\n appearanceLabels?: AppearanceSelectorLabels\n disabled?: boolean\n showLabelInput?: boolean\n showManage?: boolean\n}\n\nexport function DictionaryEntrySelect({\n value,\n onChange,\n fetchOptions,\n createOption,\n labels,\n manageHref,\n selectClassName,\n allowInlineCreate = true,\n allowAppearance = false,\n appearanceLabels,\n disabled: disabledProp = false,\n showLabelInput = true,\n showManage = true,\n}: DictionaryEntrySelectProps) {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const [options, setOptions] = React.useState<DictionaryOption[]>([])\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [newValue, setNewValue] = React.useState('')\n const [newLabel, setNewLabel] = React.useState('')\n const [formError, setFormError] = React.useState<string | null>(null)\n const appearance = useAppearanceState(null, null)\n\n const loadOptions = React.useCallback(async () => {\n setLoading(true)\n try {\n const items = await fetchOptions()\n setOptions(items.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })))\n } catch (err) {\n console.error('DictionaryEntrySelect.fetchOptions failed', err)\n flash(labels.errorLoad, 'error')\n setOptions([])\n } finally {\n setLoading(false)\n }\n }, [fetchOptions, labels.errorLoad])\n\n React.useEffect(() => {\n loadOptions().catch(() => {})\n }, [loadOptions])\n\n const resetDialogState = React.useCallback(() => {\n setNewValue('')\n setNewLabel('')\n setFormError(null)\n appearance.setColor(null)\n appearance.setIcon(null)\n setSaving(false)\n }, [appearance])\n\n React.useEffect(() => {\n if (!dialogOpen) resetDialogState()\n }, [dialogOpen, resetDialogState])\n\n const activeOption = React.useMemo(\n () => options.find((option) => option.value === value) ?? null,\n [options, value],\n )\n\n const handleCreate = React.useCallback(async () => {\n if (!createOption) return\n const trimmedValue = newValue.trim()\n if (!trimmedValue.length) {\n setFormError(labels.emptyError)\n return\n }\n setSaving(true)\n try {\n const payload = await createOption({\n value: trimmedValue,\n label: showLabelInput ? newLabel.trim() || undefined : undefined,\n color: allowAppearance && appearance.color ? appearance.color : undefined,\n icon: allowAppearance && appearance.icon ? appearance.icon : undefined,\n })\n if (!payload) throw new Error('createOption did not return an entry')\n setOptions((previous) => {\n const map = new Map(previous.map((option) => [option.value, option]))\n map.set(payload.value, {\n value: payload.value,\n label: payload.label,\n color: payload.color ?? null,\n icon: payload.icon ?? null,\n })\n return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))\n })\n await loadOptions()\n onChange(payload.value)\n setDialogOpen(false)\n if (labels.successCreateLabel) {\n flash(labels.successCreateLabel, 'success')\n }\n } catch (err) {\n console.error('DictionaryEntrySelect.createOption failed', err)\n flash(labels.errorSave, 'error')\n } finally {\n setSaving(false)\n }\n }, [\n allowAppearance,\n appearance.color,\n appearance.icon,\n createOption,\n labels.emptyError,\n labels.errorSave,\n labels.successCreateLabel,\n loadOptions,\n newLabel,\n newValue,\n onChange,\n ])\n\n const handleDialogKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault()\n if (!saving) {\n setDialogOpen(false)\n }\n return\n }\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n if (!saving && newValue.trim().length) {\n handleCreate().catch(() => {})\n } else if (!saving && !newValue.trim().length) {\n setFormError(labels.emptyError)\n }\n }\n },\n [handleCreate, labels.emptyError, newValue, saving],\n )\n\n const shortcutHint = React.useMemo(() => {\n const provided = typeof labels.saveShortcutHint === 'string' ? labels.saveShortcutHint.trim() : ''\n if (provided.length) return provided\n return '\u2318/Ctrl + Enter'\n }, [labels.saveShortcutHint])\n\n const disabled = disabledProp || loading || saving\n const manageLink = manageHref ?? '/backend/config/dictionaries'\n const returnTo = React.useMemo(() => {\n const query = searchParams?.toString() ?? ''\n if (!pathname) return null\n return query.length ? `${pathname}?${query}` : pathname\n }, [pathname, searchParams])\n const manageLinkWithReturnTo = React.useMemo(\n () => buildHrefWithReturnTo(manageLink, returnTo),\n [manageLink, returnTo],\n )\n\n return (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <Select\n value={value || undefined}\n onValueChange={(next) => onChange(next || undefined)}\n disabled={disabled}\n >\n <SelectTrigger\n className={selectClassName}\n title={activeOption?.label ?? undefined}\n >\n <SelectValue placeholder={labels.placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <div className=\"flex items-center gap-1\">\n {allowInlineCreate && createOption ? (\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n disabled={disabled}\n title={labels.addLabel}\n aria-label={labels.addLabel}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-md\" onKeyDown={handleDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>{labels.dialogTitle}</DialogTitle>\n {labels.addPrompt ? <DialogDescription>{labels.addPrompt}</DialogDescription> : null}\n </DialogHeader>\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">{labels.valueLabel}</label>\n <Input\n type=\"text\"\n value={newValue}\n onChange={(event) => {\n setNewValue(event.target.value)\n if (formError) setFormError(null)\n }}\n placeholder={labels.valuePlaceholder}\n autoFocus\n disabled={saving}\n />\n </div>\n {showLabelInput ? (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">{labels.labelLabel}</label>\n <Input\n type=\"text\"\n value={newLabel}\n onChange={(event) => setNewLabel(event.target.value)}\n placeholder={labels.labelPlaceholder}\n disabled={saving}\n />\n </div>\n ) : null}\n {allowAppearance ? (\n <AppearanceSelector\n icon={appearance.icon}\n color={appearance.color}\n onIconChange={appearance.setIcon}\n onColorChange={appearance.setColor}\n labels={appearanceLabels ?? DEFAULT_APPEARANCE_LABELS}\n />\n ) : null}\n {formError ? <p className=\"text-sm text-red-600\">{formError}</p> : null}\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setDialogOpen(false)} disabled={saving}>\n {labels.cancelLabel}\n </Button>\n <Button type=\"button\" onClick={handleCreate} disabled={saving || !newValue.trim()}>\n {saving ? <Spinner className=\"mr-2 h-4 w-4\" /> : <Save className=\"mr-2 h-4 w-4\" />}\n <span className=\"flex items-center gap-2\">\n <span>{labels.saveLabel}</span>\n {!saving ? (\n <span className=\"text-xs text-muted-foreground\">{`(${shortcutHint})`}</span>\n ) : null}\n </span>\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n ) : null}\n {showManage ? (\n <Button asChild variant=\"ghost\" size=\"icon\" title={labels.manageTitle} aria-label={labels.manageTitle}>\n <Link href={manageLinkWithReturnTo}>\n <Settings className=\"h-4 w-4\" />\n <span className=\"sr-only\">{labels.manageTitle}</span>\n </Link>\n </Button>\n ) : null}\n </div>\n </div>\n {activeOption && (activeOption.icon || activeOption.color) ? (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex items-center gap-2 rounded border border-dashed px-2 py-1\">\n {activeOption.icon ? renderDictionaryIcon(activeOption.icon, 'h-4 w-4') : null}\n {activeOption.color ? renderDictionaryColor(activeOption.color, 'h-4 w-4 rounded-sm') : null}\n </span>\n {activeOption.color ? <span>{activeOption.color}</span> : null}\n </div>\n ) : null}\n {loading ? <div className=\"text-xs text-muted-foreground\">{labels.loadingLabel}</div> : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { Plus, Settings, Save } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from '@open-mercato/ui/primitives/dialog'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { buildHrefWithReturnTo } from '@open-mercato/shared/lib/navigation/returnTo'\nimport { DictionaryValue, renderDictionaryColor, renderDictionaryIcon } from './dictionaryAppearance'\nimport { AppearanceSelector, type AppearanceSelectorLabels, useAppearanceState } from './AppearanceSelector'\n\nconst DEFAULT_APPEARANCE_LABELS: AppearanceSelectorLabels = {\n colorLabel: 'Color',\n colorHelp: 'Pick a highlight color for this entry.',\n colorClearLabel: 'Remove color',\n iconLabel: 'Icon or emoji',\n iconPlaceholder: 'Type an emoji or icon token.',\n iconPickerTriggerLabel: 'Browse icons and emoji',\n iconSearchPlaceholder: 'Search icons or emojis\u2026',\n iconSearchEmptyLabel: 'No icons match your search.',\n iconSuggestionsLabel: 'Suggestions',\n iconClearLabel: 'Remove icon',\n previewEmptyLabel: 'No appearance selected',\n}\n\nexport type DictionaryOption = {\n value: string\n label: string\n color: string | null\n icon: string | null\n}\n\nexport type DictionarySelectLabels = {\n placeholder: string\n addLabel: string\n addPrompt?: string\n dialogTitle: string\n valueLabel: string\n valuePlaceholder: string\n labelLabel: string\n labelPlaceholder: string\n emptyError: string\n cancelLabel: string\n saveLabel: string\n saveShortcutHint?: string\n successCreateLabel?: string\n errorLoad: string\n errorSave: string\n loadingLabel: string\n manageTitle: string\n}\n\nexport type DictionaryEntrySelectProps = {\n id?: string\n value?: string\n onChange: (value: string | undefined) => void\n fetchOptions: () => Promise<DictionaryOption[]>\n createOption?: (input: { value: string; label?: string; color?: string | null; icon?: string | null }) => Promise<DictionaryOption | null>\n labels: DictionarySelectLabels\n manageHref?: string\n selectClassName?: string\n allowInlineCreate?: boolean\n allowAppearance?: boolean\n appearanceLabels?: AppearanceSelectorLabels\n disabled?: boolean\n showLabelInput?: boolean\n showManage?: boolean\n /**\n * When false, hides the read-only appearance preview (color swatch + icon + hex)\n * rendered below the trigger for the currently-selected entry. Defaults to true to\n * preserve existing behavior; set false where the host only wants a plain select\n * (e.g. a create form that shouldn't surface dictionary styling).\n */\n showActiveAppearance?: boolean\n}\n\nexport function DictionaryEntrySelect({\n id,\n value,\n onChange,\n fetchOptions,\n createOption,\n labels,\n manageHref,\n selectClassName,\n allowInlineCreate = true,\n allowAppearance = false,\n appearanceLabels,\n disabled: disabledProp = false,\n showLabelInput = true,\n showManage = true,\n showActiveAppearance = true,\n}: DictionaryEntrySelectProps) {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const [options, setOptions] = React.useState<DictionaryOption[]>([])\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [newValue, setNewValue] = React.useState('')\n const [newLabel, setNewLabel] = React.useState('')\n const [formError, setFormError] = React.useState<string | null>(null)\n const appearance = useAppearanceState(null, null)\n\n const loadOptions = React.useCallback(async () => {\n setLoading(true)\n try {\n const items = await fetchOptions()\n setOptions(items.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })))\n } catch (err) {\n console.error('DictionaryEntrySelect.fetchOptions failed', err)\n flash(labels.errorLoad, 'error')\n setOptions([])\n } finally {\n setLoading(false)\n }\n }, [fetchOptions, labels.errorLoad])\n\n React.useEffect(() => {\n loadOptions().catch(() => {})\n }, [loadOptions])\n\n const resetDialogState = React.useCallback(() => {\n setNewValue('')\n setNewLabel('')\n setFormError(null)\n appearance.setColor(null)\n appearance.setIcon(null)\n setSaving(false)\n }, [appearance])\n\n React.useEffect(() => {\n if (!dialogOpen) resetDialogState()\n }, [dialogOpen, resetDialogState])\n\n const activeOption = React.useMemo(\n () => options.find((option) => option.value === value) ?? null,\n [options, value],\n )\n\n const handleCreate = React.useCallback(async () => {\n if (!createOption) return\n const trimmedValue = newValue.trim()\n if (!trimmedValue.length) {\n setFormError(labels.emptyError)\n return\n }\n setSaving(true)\n try {\n const payload = await createOption({\n value: trimmedValue,\n label: showLabelInput ? newLabel.trim() || undefined : undefined,\n color: allowAppearance && appearance.color ? appearance.color : undefined,\n icon: allowAppearance && appearance.icon ? appearance.icon : undefined,\n })\n if (!payload) throw new Error('createOption did not return an entry')\n setOptions((previous) => {\n const map = new Map(previous.map((option) => [option.value, option]))\n map.set(payload.value, {\n value: payload.value,\n label: payload.label,\n color: payload.color ?? null,\n icon: payload.icon ?? null,\n })\n return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))\n })\n await loadOptions()\n onChange(payload.value)\n setDialogOpen(false)\n if (labels.successCreateLabel) {\n flash(labels.successCreateLabel, 'success')\n }\n } catch (err) {\n console.error('DictionaryEntrySelect.createOption failed', err)\n flash(labels.errorSave, 'error')\n } finally {\n setSaving(false)\n }\n }, [\n allowAppearance,\n appearance.color,\n appearance.icon,\n createOption,\n labels.emptyError,\n labels.errorSave,\n labels.successCreateLabel,\n loadOptions,\n newLabel,\n newValue,\n onChange,\n ])\n\n const handleDialogKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault()\n if (!saving) {\n setDialogOpen(false)\n }\n return\n }\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n if (!saving && newValue.trim().length) {\n handleCreate().catch(() => {})\n } else if (!saving && !newValue.trim().length) {\n setFormError(labels.emptyError)\n }\n }\n },\n [handleCreate, labels.emptyError, newValue, saving],\n )\n\n const shortcutHint = React.useMemo(() => {\n const provided = typeof labels.saveShortcutHint === 'string' ? labels.saveShortcutHint.trim() : ''\n if (provided.length) return provided\n return '\u2318/Ctrl + Enter'\n }, [labels.saveShortcutHint])\n\n const disabled = disabledProp || loading || saving\n const manageLink = manageHref ?? '/backend/config/dictionaries'\n const returnTo = React.useMemo(() => {\n const query = searchParams?.toString() ?? ''\n if (!pathname) return null\n return query.length ? `${pathname}?${query}` : pathname\n }, [pathname, searchParams])\n const manageLinkWithReturnTo = React.useMemo(\n () => buildHrefWithReturnTo(manageLink, returnTo),\n [manageLink, returnTo],\n )\n\n return (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <Select\n value={value || undefined}\n onValueChange={(next) => onChange(next || undefined)}\n disabled={disabled}\n >\n <SelectTrigger\n id={id}\n className={selectClassName}\n title={activeOption?.label ?? undefined}\n >\n <SelectValue placeholder={labels.placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <div className=\"flex items-center gap-1\">\n {allowInlineCreate && createOption ? (\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n disabled={disabled}\n title={labels.addLabel}\n aria-label={labels.addLabel}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-md\" onKeyDown={handleDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>{labels.dialogTitle}</DialogTitle>\n {labels.addPrompt ? <DialogDescription>{labels.addPrompt}</DialogDescription> : null}\n </DialogHeader>\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">{labels.valueLabel}</label>\n <Input\n type=\"text\"\n value={newValue}\n onChange={(event) => {\n setNewValue(event.target.value)\n if (formError) setFormError(null)\n }}\n placeholder={labels.valuePlaceholder}\n autoFocus\n disabled={saving}\n />\n </div>\n {showLabelInput ? (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">{labels.labelLabel}</label>\n <Input\n type=\"text\"\n value={newLabel}\n onChange={(event) => setNewLabel(event.target.value)}\n placeholder={labels.labelPlaceholder}\n disabled={saving}\n />\n </div>\n ) : null}\n {allowAppearance ? (\n <AppearanceSelector\n icon={appearance.icon}\n color={appearance.color}\n onIconChange={appearance.setIcon}\n onColorChange={appearance.setColor}\n labels={appearanceLabels ?? DEFAULT_APPEARANCE_LABELS}\n />\n ) : null}\n {formError ? <p className=\"text-sm text-red-600\">{formError}</p> : null}\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setDialogOpen(false)} disabled={saving}>\n {labels.cancelLabel}\n </Button>\n <Button type=\"button\" onClick={handleCreate} disabled={saving || !newValue.trim()}>\n {saving ? <Spinner className=\"mr-2 h-4 w-4\" /> : <Save className=\"mr-2 h-4 w-4\" />}\n <span className=\"flex items-center gap-2\">\n <span>{labels.saveLabel}</span>\n {!saving ? (\n <span className=\"text-xs text-muted-foreground\">{`(${shortcutHint})`}</span>\n ) : null}\n </span>\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n ) : null}\n {showManage ? (\n <Button asChild variant=\"ghost\" size=\"icon\" title={labels.manageTitle} aria-label={labels.manageTitle}>\n <Link href={manageLinkWithReturnTo}>\n <Settings className=\"h-4 w-4\" />\n <span className=\"sr-only\">{labels.manageTitle}</span>\n </Link>\n </Button>\n ) : null}\n </div>\n </div>\n {showActiveAppearance && activeOption && (activeOption.icon || activeOption.color) ? (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex items-center gap-2 rounded border border-dashed px-2 py-1\">\n {activeOption.icon ? renderDictionaryIcon(activeOption.icon, 'h-4 w-4') : null}\n {activeOption.color ? renderDictionaryColor(activeOption.color, 'h-4 w-4 rounded-sm') : null}\n </span>\n {activeOption.color ? <span>{activeOption.color}</span> : null}\n </div>\n ) : null}\n {loading ? <div className=\"text-xs text-muted-foreground\">{labels.loadingLabel}</div> : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA6PQ,SAUI,KAVJ;AA3PR,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,aAAa,uBAAuB;AAC7C,SAAS,MAAM,UAAU,YAAY;AACrC,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,6BAA6B;AACtC,SAA0B,uBAAuB,4BAA4B;AAC7E,SAAS,oBAAmD,0BAA0B;AAEtF,MAAM,4BAAsD;AAAA,EAC1D,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA,EACtB,gBAAgB;AAAA,EAChB,mBAAmB;AACrB;AAqDO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,kBAAkB;AAAA,EAClB;AAAA,EACA,UAAU,eAAe;AAAA,EACzB,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,uBAAuB;AACzB,GAA+B;AAC7B,QAAM,WAAW,YAAY;AAC7B,QAAM,eAAe,gBAAgB;AACrC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAA6B,CAAC,CAAC;AACnE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,EAAE;AACjD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,EAAE;AACjD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,aAAa,mBAAmB,MAAM,IAAI;AAEhD,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,eAAW,IAAI;AACf,QAAI;AACF,YAAM,QAAQ,MAAM,aAAa;AACjC,iBAAW,MAAM,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,OAAO,QAAW,EAAE,aAAa,OAAO,CAAC,CAAC,CAAC;AAAA,IACrG,SAAS,KAAK;AACZ,cAAQ,MAAM,6CAA6C,GAAG;AAC9D,YAAM,OAAO,WAAW,OAAO;AAC/B,iBAAW,CAAC,CAAC;AAAA,IACf,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,cAAc,OAAO,SAAS,CAAC;AAEnC,QAAM,UAAU,MAAM;AACpB,gBAAY,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC9B,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,mBAAmB,MAAM,YAAY,MAAM;AAC/C,gBAAY,EAAE;AACd,gBAAY,EAAE;AACd,iBAAa,IAAI;AACjB,eAAW,SAAS,IAAI;AACxB,eAAW,QAAQ,IAAI;AACvB,cAAU,KAAK;AAAA,EACjB,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,WAAY,kBAAiB;AAAA,EACpC,GAAG,CAAC,YAAY,gBAAgB,CAAC;AAEjC,QAAM,eAAe,MAAM;AAAA,IACzB,MAAM,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK,KAAK;AAAA,IAC1D,CAAC,SAAS,KAAK;AAAA,EACjB;AAEA,QAAM,eAAe,MAAM,YAAY,YAAY;AACjD,QAAI,CAAC,aAAc;AACnB,UAAM,eAAe,SAAS,KAAK;AACnC,QAAI,CAAC,aAAa,QAAQ;AACxB,mBAAa,OAAO,UAAU;AAC9B;AAAA,IACF;AACA,cAAU,IAAI;AACd,QAAI;AACF,YAAM,UAAU,MAAM,aAAa;AAAA,QACjC,OAAO;AAAA,QACP,OAAO,iBAAiB,SAAS,KAAK,KAAK,SAAY;AAAA,QACvD,OAAO,mBAAmB,WAAW,QAAQ,WAAW,QAAQ;AAAA,QAChE,MAAM,mBAAmB,WAAW,OAAO,WAAW,OAAO;AAAA,MAC/D,CAAC;AACD,UAAI,CAAC,QAAS,OAAM,IAAI,MAAM,sCAAsC;AACpE,iBAAW,CAAC,aAAa;AACvB,cAAM,MAAM,IAAI,IAAI,SAAS,IAAI,CAAC,WAAW,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC;AACpE,YAAI,IAAI,QAAQ,OAAO;AAAA,UACrB,OAAO,QAAQ;AAAA,UACf,OAAO,QAAQ;AAAA,UACf,OAAO,QAAQ,SAAS;AAAA,UACxB,MAAM,QAAQ,QAAQ;AAAA,QACxB,CAAC;AACD,eAAO,MAAM,KAAK,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,OAAO,QAAW,EAAE,aAAa,OAAO,CAAC,CAAC;AAAA,MACnH,CAAC;AACD,YAAM,YAAY;AAClB,eAAS,QAAQ,KAAK;AACtB,oBAAc,KAAK;AACnB,UAAI,OAAO,oBAAoB;AAC7B,cAAM,OAAO,oBAAoB,SAAS;AAAA,MAC5C;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,6CAA6C,GAAG;AAC9D,YAAM,OAAO,WAAW,OAAO;AAAA,IACjC,UAAE;AACA,gBAAU,KAAK;AAAA,IACjB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA,WAAW;AAAA,IACX,WAAW;AAAA,IACX;AAAA,IACA,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,sBAAsB,MAAM;AAAA,IAChC,CAAC,UAA+B;AAC9B,UAAI,MAAM,QAAQ,UAAU;AAC1B,cAAM,eAAe;AACrB,YAAI,CAAC,QAAQ;AACX,wBAAc,KAAK;AAAA,QACrB;AACA;AAAA,MACF;AACA,UAAI,MAAM,QAAQ,YAAY,MAAM,WAAW,MAAM,UAAU;AAC7D,cAAM,eAAe;AACrB,YAAI,CAAC,UAAU,SAAS,KAAK,EAAE,QAAQ;AACrC,uBAAa,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC/B,WAAW,CAAC,UAAU,CAAC,SAAS,KAAK,EAAE,QAAQ;AAC7C,uBAAa,OAAO,UAAU;AAAA,QAChC;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,cAAc,OAAO,YAAY,UAAU,MAAM;AAAA,EACpD;AAEA,QAAM,eAAe,MAAM,QAAQ,MAAM;AACvC,UAAM,WAAW,OAAO,OAAO,qBAAqB,WAAW,OAAO,iBAAiB,KAAK,IAAI;AAChG,QAAI,SAAS,OAAQ,QAAO;AAC5B,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,gBAAgB,CAAC;AAE5B,QAAM,WAAW,gBAAgB,WAAW;AAC5C,QAAM,aAAa,cAAc;AACjC,QAAM,WAAW,MAAM,QAAQ,MAAM;AACnC,UAAM,QAAQ,cAAc,SAAS,KAAK;AAC1C,QAAI,CAAC,SAAU,QAAO;AACtB,WAAO,MAAM,SAAS,GAAG,QAAQ,IAAI,KAAK,KAAK;AAAA,EACjD,GAAG,CAAC,UAAU,YAAY,CAAC;AAC3B,QAAM,yBAAyB,MAAM;AAAA,IACnC,MAAM,sBAAsB,YAAY,QAAQ;AAAA,IAChD,CAAC,YAAY,QAAQ;AAAA,EACvB;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,SAAS;AAAA,UAChB,eAAe,CAAC,SAAS,SAAS,QAAQ,MAAS;AAAA,UACnD;AAAA,UAEA;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,WAAW;AAAA,gBACX,OAAO,cAAc,SAAS;AAAA,gBAE9B,8BAAC,eAAY,aAAa,OAAO,aAAa;AAAA;AAAA,YAChD;AAAA,YACA,oBAAC,iBACE,kBAAQ,IAAI,CAAC,WACZ,oBAAC,cAA8B,OAAO,OAAO,OAC1C,iBAAO,SADO,OAAO,KAExB,CACD,GACH;AAAA;AAAA;AAAA,MACF;AAAA,MACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,6BAAqB,eACpB,qBAAC,UAAO,MAAM,YAAY,cAAc,eACtC;AAAA,8BAAC,iBAAc,SAAO,MACpB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL;AAAA,cACA,OAAO,OAAO;AAAA,cACd,cAAY,OAAO;AAAA,cAEnB,8BAAC,QAAK,WAAU,WAAU;AAAA;AAAA,UAC5B,GACF;AAAA,UACA,qBAAC,iBAAc,WAAU,eAAc,WAAW,qBAChD;AAAA,iCAAC,gBACC;AAAA,kCAAC,eAAa,iBAAO,aAAY;AAAA,cAChC,OAAO,YAAY,oBAAC,qBAAmB,iBAAO,WAAU,IAAuB;AAAA,eAClF;AAAA,YACA,qBAAC,SAAI,WAAU,aACb;AAAA,mCAAC,SAAI,WAAU,aACb;AAAA,oCAAC,WAAM,WAAU,uBAAuB,iBAAO,YAAW;AAAA,gBAC1D;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,OAAO;AAAA,oBACP,UAAU,CAAC,UAAU;AACnB,kCAAY,MAAM,OAAO,KAAK;AAC9B,0BAAI,UAAW,cAAa,IAAI;AAAA,oBAClC;AAAA,oBACA,aAAa,OAAO;AAAA,oBACpB,WAAS;AAAA,oBACT,UAAU;AAAA;AAAA,gBACZ;AAAA,iBACF;AAAA,cACC,iBACC,qBAAC,SAAI,WAAU,aACb;AAAA,oCAAC,WAAM,WAAU,uBAAuB,iBAAO,YAAW;AAAA,gBAC1D;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,OAAO;AAAA,oBACP,UAAU,CAAC,UAAU,YAAY,MAAM,OAAO,KAAK;AAAA,oBACnD,aAAa,OAAO;AAAA,oBACpB,UAAU;AAAA;AAAA,gBACZ;AAAA,iBACF,IACE;AAAA,cACH,kBACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,WAAW;AAAA,kBACjB,OAAO,WAAW;AAAA,kBAClB,cAAc,WAAW;AAAA,kBACzB,eAAe,WAAW;AAAA,kBAC1B,QAAQ,oBAAoB;AAAA;AAAA,cAC9B,IACE;AAAA,cACH,YAAY,oBAAC,OAAE,WAAU,wBAAwB,qBAAU,IAAO;AAAA,eACrE;AAAA,YACA,qBAAC,gBACC;AAAA,kCAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,MAAM,cAAc,KAAK,GAAG,UAAU,QACpF,iBAAO,aACV;AAAA,cACA,qBAAC,UAAO,MAAK,UAAS,SAAS,cAAc,UAAU,UAAU,CAAC,SAAS,KAAK,GAC7E;AAAA,yBAAS,oBAAC,WAAQ,WAAU,gBAAe,IAAK,oBAAC,QAAK,WAAU,gBAAe;AAAA,gBAChF,qBAAC,UAAK,WAAU,2BACd;AAAA,sCAAC,UAAM,iBAAO,WAAU;AAAA,kBACvB,CAAC,SACA,oBAAC,UAAK,WAAU,iCAAiC,cAAI,YAAY,KAAI,IACnE;AAAA,mBACN;AAAA,iBACF;AAAA,eACF;AAAA,aACF;AAAA,WACF,IACE;AAAA,QACH,aACC,oBAAC,UAAO,SAAO,MAAC,SAAQ,SAAQ,MAAK,QAAO,OAAO,OAAO,aAAa,cAAY,OAAO,aACxF,+BAAC,QAAK,MAAM,wBACV;AAAA,8BAAC,YAAS,WAAU,WAAU;AAAA,UAC9B,oBAAC,UAAK,WAAU,WAAW,iBAAO,aAAY;AAAA,WAChD,GACF,IACE;AAAA,SACN;AAAA,OACF;AAAA,IACC,wBAAwB,iBAAiB,aAAa,QAAQ,aAAa,SAC1E,qBAAC,SAAI,WAAU,yDACb;AAAA,2BAAC,UAAK,WAAU,yEACb;AAAA,qBAAa,OAAO,qBAAqB,aAAa,MAAM,SAAS,IAAI;AAAA,QACzE,aAAa,QAAQ,sBAAsB,aAAa,OAAO,oBAAoB,IAAI;AAAA,SAC1F;AAAA,MACC,aAAa,QAAQ,oBAAC,UAAM,uBAAa,OAAM,IAAU;AAAA,OAC5D,IACE;AAAA,IACH,UAAU,oBAAC,SAAI,WAAU,iCAAiC,iBAAO,cAAa,IAAS;AAAA,KAC1F;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import {
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
|
-
import {
|
|
7
|
-
import { createCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
8
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
|
-
import { DealForm, type DealFormSubmitPayload } from '../../../../components/detail/DealForm'
|
|
10
|
-
import { useCurrencyDictionary } from '../../../../components/detail/hooks/useCurrencyDictionary'
|
|
6
|
+
import { CreateDealForm } from '../../../../components/detail/create/CreateDealForm'
|
|
11
7
|
|
|
12
8
|
const DEFAULT_RETURN_TO = '/backend/customers/deals'
|
|
13
9
|
|
|
@@ -24,73 +20,16 @@ function resolveReturnTo(value: string | null | undefined): string {
|
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
export default function CreateDealPage() {
|
|
27
|
-
const t = useT()
|
|
28
|
-
const router = useRouter()
|
|
29
23
|
const searchParams = useSearchParams()
|
|
30
24
|
const returnTo = React.useMemo(
|
|
31
25
|
() => resolveReturnTo(searchParams?.get('returnTo') ?? null),
|
|
32
26
|
[searchParams],
|
|
33
27
|
)
|
|
34
|
-
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
35
|
-
useCurrencyDictionary()
|
|
36
|
-
|
|
37
|
-
const handleCancel = React.useCallback(() => {
|
|
38
|
-
router.push(returnTo)
|
|
39
|
-
}, [router, returnTo])
|
|
40
|
-
|
|
41
|
-
const handleSubmit = React.useCallback(
|
|
42
|
-
async ({ base, custom }: DealFormSubmitPayload) => {
|
|
43
|
-
if (isSubmitting) return
|
|
44
|
-
setIsSubmitting(true)
|
|
45
|
-
try {
|
|
46
|
-
const payload: Record<string, unknown> = {
|
|
47
|
-
title: base.title,
|
|
48
|
-
status: base.status ?? undefined,
|
|
49
|
-
pipelineStage: base.pipelineStage ?? undefined,
|
|
50
|
-
pipelineId: base.pipelineId ?? undefined,
|
|
51
|
-
pipelineStageId: base.pipelineStageId ?? undefined,
|
|
52
|
-
valueAmount: typeof base.valueAmount === 'number' ? base.valueAmount : undefined,
|
|
53
|
-
valueCurrency: base.valueCurrency ?? undefined,
|
|
54
|
-
probability: typeof base.probability === 'number' ? base.probability : undefined,
|
|
55
|
-
expectedCloseAt: base.expectedCloseAt ?? undefined,
|
|
56
|
-
description: base.description ?? undefined,
|
|
57
|
-
personIds: Array.isArray(base.personIds) && base.personIds.length ? base.personIds : undefined,
|
|
58
|
-
companyIds: Array.isArray(base.companyIds) && base.companyIds.length ? base.companyIds : undefined,
|
|
59
|
-
}
|
|
60
|
-
if (Object.keys(custom).length) payload.customFields = custom
|
|
61
|
-
|
|
62
|
-
await createCrud('customers/deals', payload, {
|
|
63
|
-
errorMessage: t('customers.deals.create.error', 'Failed to create deal.'),
|
|
64
|
-
})
|
|
65
|
-
flash(t('customers.people.detail.deals.success', 'Deal created.'), 'success')
|
|
66
|
-
router.push(returnTo)
|
|
67
|
-
} catch (err) {
|
|
68
|
-
const message =
|
|
69
|
-
err instanceof Error
|
|
70
|
-
? err.message
|
|
71
|
-
: t('customers.deals.create.error', 'Failed to create deal.')
|
|
72
|
-
flash(message, 'error')
|
|
73
|
-
throw err instanceof Error ? err : new Error(message)
|
|
74
|
-
} finally {
|
|
75
|
-
setIsSubmitting(false)
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
[isSubmitting, returnTo, router, t],
|
|
79
|
-
)
|
|
80
28
|
|
|
81
29
|
return (
|
|
82
30
|
<Page>
|
|
83
31
|
<PageBody>
|
|
84
|
-
<
|
|
85
|
-
mode="create"
|
|
86
|
-
onSubmit={handleSubmit}
|
|
87
|
-
onCancel={handleCancel}
|
|
88
|
-
isSubmitting={isSubmitting}
|
|
89
|
-
submitLabel={t('customers.deals.create.submit', 'Create deal')}
|
|
90
|
-
embedded={false}
|
|
91
|
-
title={t('customers.deals.create.title', 'Create deal')}
|
|
92
|
-
backHref={returnTo}
|
|
93
|
-
/>
|
|
32
|
+
<CreateDealForm returnTo={returnTo} />
|
|
94
33
|
</PageBody>
|
|
95
34
|
</Page>
|
|
96
35
|
)
|
|
@@ -177,6 +177,8 @@ const schema = z.object({
|
|
|
177
177
|
companyIds: z.array(z.string().trim().min(1)).optional(),
|
|
178
178
|
}).passthrough()
|
|
179
179
|
|
|
180
|
+
export const dealFormSchema = schema
|
|
181
|
+
|
|
180
182
|
import { toDateInputValue as toDateInputValueOrNull } from '@open-mercato/shared/lib/date/format'
|
|
181
183
|
|
|
182
184
|
function toDateInputValue(value: string | null | undefined): string {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { Briefcase, Save } from 'lucide-react'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
8
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
|
+
import { createCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
10
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
11
|
+
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
12
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
13
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
14
|
+
import { dealFormSchema } from '../DealForm'
|
|
15
|
+
import { createDictionarySelectLabels } from '../utils'
|
|
16
|
+
import { DealSectionCard } from './DealSectionCard'
|
|
17
|
+
import { DealDetailsFields } from './DealDetailsFields'
|
|
18
|
+
import { DealAssociationsSection } from './DealAssociationsSection'
|
|
19
|
+
import { DealCreateSidebar } from './DealCreateSidebar'
|
|
20
|
+
import { useDealPipelines } from './useDealPipelines'
|
|
21
|
+
import { useDealCustomFields } from './useDealCustomFields'
|
|
22
|
+
import { EMPTY_VALUES, type BaseValues } from './dealFormTypes'
|
|
23
|
+
|
|
24
|
+
const CONTEXT_ID = 'customers.deals.create'
|
|
25
|
+
const DEAL_ENTITY_ID = 'customers:customer_deal'
|
|
26
|
+
const CUSTOM_FIELDS_MANAGE_HREF = `/backend/entities/system/${encodeURIComponent(DEAL_ENTITY_ID)}`
|
|
27
|
+
|
|
28
|
+
export type CreateDealFormProps = {
|
|
29
|
+
returnTo: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CreateDealForm({ returnTo }: CreateDealFormProps) {
|
|
33
|
+
const t = useT()
|
|
34
|
+
const router = useRouter()
|
|
35
|
+
const tr = React.useCallback(
|
|
36
|
+
(key: string, fallback: string, params?: Record<string, string | number>) =>
|
|
37
|
+
translateWithFallback(t, key, fallback, params),
|
|
38
|
+
[t],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const [values, setValues] = React.useState<BaseValues>(EMPTY_VALUES)
|
|
42
|
+
const [errors, setErrors] = React.useState<Record<string, string>>({})
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
44
|
+
|
|
45
|
+
const { pipelines, stages, loadStages } = useDealPipelines()
|
|
46
|
+
const {
|
|
47
|
+
customValues,
|
|
48
|
+
customFieldsLoaded,
|
|
49
|
+
customCount,
|
|
50
|
+
handleCustomChange,
|
|
51
|
+
handleCustomAttributesLoaded,
|
|
52
|
+
validateCustomFields,
|
|
53
|
+
collectNormalizedCustomValues,
|
|
54
|
+
} = useDealCustomFields(tr)
|
|
55
|
+
|
|
56
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
57
|
+
formId: string
|
|
58
|
+
resourceKind: string
|
|
59
|
+
retryLastMutation: () => Promise<boolean>
|
|
60
|
+
}>({
|
|
61
|
+
contextId: CONTEXT_ID,
|
|
62
|
+
blockedMessage: tr('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const statusLabels = React.useMemo(
|
|
66
|
+
() => createDictionarySelectLabels('deal-statuses', (key, fallback) => tr(key, fallback ?? key)),
|
|
67
|
+
[tr],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const patch = React.useCallback((partial: Partial<BaseValues>) => {
|
|
71
|
+
setValues((current) => ({ ...current, ...partial }))
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
const handlePipelineChange = React.useCallback(
|
|
75
|
+
(id: string) => {
|
|
76
|
+
patch({ pipelineId: id, pipelineStageId: '' })
|
|
77
|
+
// loadStages resets stages to [] on failure; the rejection is intentionally ignored here.
|
|
78
|
+
loadStages(id).catch(() => {})
|
|
79
|
+
},
|
|
80
|
+
[loadStages, patch],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const handleCancel = React.useCallback(() => {
|
|
84
|
+
router.push(returnTo)
|
|
85
|
+
}, [returnTo, router])
|
|
86
|
+
|
|
87
|
+
const handleSubmit = React.useCallback(async () => {
|
|
88
|
+
if (isSubmitting) return
|
|
89
|
+
if (!customFieldsLoaded) {
|
|
90
|
+
flash(tr('customers.deals.create.sections.custom.loading', 'Loading custom fields...'), 'error')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const merged = { ...values, ...customValues }
|
|
94
|
+
const parsed = dealFormSchema.safeParse(merged)
|
|
95
|
+
if (!parsed.success) {
|
|
96
|
+
const fieldErrors: Record<string, string> = {}
|
|
97
|
+
for (const issue of parsed.error.issues) {
|
|
98
|
+
const key = typeof issue.path[0] === 'string' ? issue.path[0] : undefined
|
|
99
|
+
if (key && !fieldErrors[key]) fieldErrors[key] = tr(issue.message, issue.message)
|
|
100
|
+
}
|
|
101
|
+
setErrors(fieldErrors)
|
|
102
|
+
const firstMessage = Object.values(fieldErrors)[0]
|
|
103
|
+
if (firstMessage) flash(firstMessage, 'error')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const customFieldErrors = validateCustomFields(merged)
|
|
108
|
+
if (Object.keys(customFieldErrors).length) {
|
|
109
|
+
setErrors(customFieldErrors)
|
|
110
|
+
const firstMessage = Object.values(customFieldErrors)[0]
|
|
111
|
+
if (firstMessage) flash(firstMessage, 'error')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setErrors({})
|
|
116
|
+
setIsSubmitting(true)
|
|
117
|
+
try {
|
|
118
|
+
const data = parsed.data
|
|
119
|
+
const expectedCloseAt =
|
|
120
|
+
data.expectedCloseAt && data.expectedCloseAt.length
|
|
121
|
+
? new Date(data.expectedCloseAt).toISOString()
|
|
122
|
+
: undefined
|
|
123
|
+
const payload: Record<string, unknown> = {
|
|
124
|
+
title: data.title,
|
|
125
|
+
status: data.status || undefined,
|
|
126
|
+
pipelineId: data.pipelineId || undefined,
|
|
127
|
+
pipelineStageId: data.pipelineStageId || undefined,
|
|
128
|
+
valueAmount: typeof data.valueAmount === 'number' ? data.valueAmount : undefined,
|
|
129
|
+
valueCurrency: data.valueCurrency || undefined,
|
|
130
|
+
probability: typeof data.probability === 'number' ? data.probability : undefined,
|
|
131
|
+
expectedCloseAt,
|
|
132
|
+
description: data.description && data.description.length ? data.description : undefined,
|
|
133
|
+
personIds: values.personIds.length ? values.personIds : undefined,
|
|
134
|
+
companyIds: values.companyIds.length ? values.companyIds : undefined,
|
|
135
|
+
}
|
|
136
|
+
const custom = collectNormalizedCustomValues(merged)
|
|
137
|
+
if (Object.keys(custom).length) payload.customFields = custom
|
|
138
|
+
|
|
139
|
+
await runMutation({
|
|
140
|
+
operation: () =>
|
|
141
|
+
createCrud('customers/deals', payload, {
|
|
142
|
+
errorMessage: tr('customers.deals.create.error', 'Failed to create deal.'),
|
|
143
|
+
}),
|
|
144
|
+
context: { formId: CONTEXT_ID, resourceKind: 'customers.deal', retryLastMutation },
|
|
145
|
+
mutationPayload: payload,
|
|
146
|
+
})
|
|
147
|
+
flash(tr('customers.people.detail.deals.success', 'Deal created.'), 'success')
|
|
148
|
+
router.push(returnTo)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : tr('customers.deals.create.error', 'Failed to create deal.')
|
|
151
|
+
flash(message, 'error')
|
|
152
|
+
} finally {
|
|
153
|
+
setIsSubmitting(false)
|
|
154
|
+
}
|
|
155
|
+
}, [
|
|
156
|
+
collectNormalizedCustomValues,
|
|
157
|
+
customFieldsLoaded,
|
|
158
|
+
customValues,
|
|
159
|
+
isSubmitting,
|
|
160
|
+
retryLastMutation,
|
|
161
|
+
returnTo,
|
|
162
|
+
router,
|
|
163
|
+
runMutation,
|
|
164
|
+
tr,
|
|
165
|
+
validateCustomFields,
|
|
166
|
+
values,
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
const onKeyDown = React.useCallback(
|
|
170
|
+
(event: React.KeyboardEvent<HTMLFormElement>) => {
|
|
171
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
handleSubmit()
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[handleSubmit],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const onFormSubmit = React.useCallback(
|
|
180
|
+
(event: React.FormEvent<HTMLFormElement>) => {
|
|
181
|
+
event.preventDefault()
|
|
182
|
+
handleSubmit()
|
|
183
|
+
},
|
|
184
|
+
[handleSubmit],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const cancelLabel = tr('customers.deals.create.cancel', 'Cancel')
|
|
188
|
+
const submitLabel = tr('customers.deals.create.submit', 'Create deal')
|
|
189
|
+
const submitDisabled = !customFieldsLoaded
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<form className="mx-auto max-w-screen-2xl" onKeyDown={onKeyDown} onSubmit={onFormSubmit} noValidate>
|
|
193
|
+
<FormHeader
|
|
194
|
+
backHref={returnTo}
|
|
195
|
+
backLabel={tr('customers.deals.create.back', 'Back to deals')}
|
|
196
|
+
/>
|
|
197
|
+
|
|
198
|
+
<div className="mt-6 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_330px]">
|
|
199
|
+
<div className="space-y-4">
|
|
200
|
+
<DealSectionCard
|
|
201
|
+
icon={Briefcase}
|
|
202
|
+
title={tr('customers.deals.create.title', 'Create deal')}
|
|
203
|
+
subtitle={tr('customers.deals.create.sections.details.subtitle', 'Core opportunity info')}
|
|
204
|
+
actions={
|
|
205
|
+
<>
|
|
206
|
+
<Button type="button" variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
|
207
|
+
{cancelLabel}
|
|
208
|
+
</Button>
|
|
209
|
+
<Button type="button" onClick={handleSubmit} disabled={isSubmitting || submitDisabled}>
|
|
210
|
+
{isSubmitting ? <Spinner className="size-4" /> : <Save className="size-4" />}
|
|
211
|
+
{submitLabel}
|
|
212
|
+
</Button>
|
|
213
|
+
</>
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
<DealDetailsFields
|
|
217
|
+
values={values}
|
|
218
|
+
errors={errors}
|
|
219
|
+
isSubmitting={isSubmitting}
|
|
220
|
+
patch={patch}
|
|
221
|
+
onPipelineChange={handlePipelineChange}
|
|
222
|
+
pipelines={pipelines}
|
|
223
|
+
stages={stages}
|
|
224
|
+
statusLabels={statusLabels}
|
|
225
|
+
tr={tr}
|
|
226
|
+
/>
|
|
227
|
+
</DealSectionCard>
|
|
228
|
+
|
|
229
|
+
<DealAssociationsSection
|
|
230
|
+
tr={tr}
|
|
231
|
+
personIds={values.personIds}
|
|
232
|
+
companyIds={values.companyIds}
|
|
233
|
+
onPeopleChange={(next) => patch({ personIds: next })}
|
|
234
|
+
onCompaniesChange={(next) => patch({ companyIds: next })}
|
|
235
|
+
disabled={isSubmitting}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<DealCreateSidebar
|
|
240
|
+
tr={tr}
|
|
241
|
+
customValues={customValues}
|
|
242
|
+
onCustomChange={handleCustomChange}
|
|
243
|
+
errors={errors}
|
|
244
|
+
disabled={isSubmitting}
|
|
245
|
+
customCount={customCount}
|
|
246
|
+
manageHref={CUSTOM_FIELDS_MANAGE_HREF}
|
|
247
|
+
onCustomLoaded={handleCustomAttributesLoaded}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
</form>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default CreateDealForm
|