@open-mercato/core 0.6.3-develop.3901.1.ddad60693a → 0.6.3
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/global.d.js +1 -0
- package/dist/global.d.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +11 -5
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- 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/dist/modules/feature_toggles/lib/feature-flag-check.js +13 -5
- package/dist/modules/feature_toggles/lib/feature-flag-check.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +6 -1
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraph.js +29 -186
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +196 -0
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +7 -0
- package/package.json +8 -9
- package/src/global.d.ts +9 -0
- package/src/modules/catalog/commands/variants.ts +14 -5
- 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
- package/src/modules/feature_toggles/lib/feature-flag-check.ts +14 -4
- package/src/modules/query_index/subscribers/coverage_refresh.ts +7 -1
- package/src/modules/resources/i18n/de.json +1 -0
- package/src/modules/resources/i18n/en.json +1 -0
- package/src/modules/resources/i18n/es.json +1 -0
- package/src/modules/resources/i18n/pl.json +1 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/workflows/components/WorkflowGraph.tsx +39 -235
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +233 -0
|
@@ -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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -16,13 +16,19 @@ const getCacheTags = (identifier, tenantId) => {
|
|
|
16
16
|
return [getIdentifierTag(identifier), getTenantTag(tenantId)];
|
|
17
17
|
};
|
|
18
18
|
class FeatureTogglesService {
|
|
19
|
-
// 1 minute
|
|
20
19
|
constructor(cache, em) {
|
|
21
20
|
this.cache = cache;
|
|
22
21
|
this.em = em;
|
|
23
22
|
this.cacheTtlMs = 1 * 60 * 1e3;
|
|
23
|
+
// 1 minute
|
|
24
|
+
// Resolution cache can be disabled via env (e.g. integration tests that flip
|
|
25
|
+
// overrides rapidly between cases). The 1-minute TTL is a production
|
|
26
|
+
// optimization; under fast flag churn it can serve a stale value across
|
|
27
|
+
// override set/clear despite invalidation, so tests opt out for determinism.
|
|
28
|
+
this.cacheDisabled = process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === "1" || process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === "true";
|
|
24
29
|
}
|
|
25
30
|
async saveCache(identifier, tenantId, result) {
|
|
31
|
+
if (this.cacheDisabled) return;
|
|
26
32
|
const key = getIsEnabledCacheKey(identifier, tenantId);
|
|
27
33
|
await runWithCacheTenant(
|
|
28
34
|
tenantId,
|
|
@@ -31,10 +37,12 @@ class FeatureTogglesService {
|
|
|
31
37
|
}
|
|
32
38
|
async resolveToggle(identifier, tenantId) {
|
|
33
39
|
const key = getIsEnabledCacheKey(identifier, tenantId);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
if (!this.cacheDisabled) {
|
|
41
|
+
const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key));
|
|
42
|
+
if (cached) {
|
|
43
|
+
const parsed = toCachedResolution(cached);
|
|
44
|
+
if (parsed) return parsed;
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
let toggle = null;
|
|
40
48
|
toggle = await this.em.findOne(FeatureToggle, { identifier, deletedAt: null });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/lib/feature-flag-check.ts"],
|
|
4
|
-
"sourcesContent": ["import { FeatureToggle, FeatureToggleOverride } from \"../data/entities\"\nimport { EntityManager } from \"@mikro-orm/core\"\nimport { CacheService, runWithCacheTenant } from \"@open-mercato/cache\"\n\ntype ToggleValueType = \"boolean\" | \"string\" | \"number\" | \"json\"\n\ntype ToggleResolutionSource = \"override\" | \"default\" | \"missing\"\n\ntype ToggleResolutionResult = {\n valueType: ToggleValueType\n value: boolean | string | number | unknown | null\n source: ToggleResolutionSource\n toggleId: string\n identifier: string\n tenantId: string\n}\n\ntype ToggleErrorCode = \"TYPE_MISMATCH\" | \"MISSING_TOGGLE\" | \"INVALID_VALUE\"\n\ntype ToggleError = {\n code: ToggleErrorCode\n message: string\n identifier: string\n expectedType: ToggleValueType\n actualType?: ToggleValueType\n source?: ToggleResolutionSource\n}\n\ntype ResultOk<T> = { ok: true; value: T; resolution: ToggleResolutionResult }\ntype ResultErr = { ok: false; error: ToggleError; resolution: ToggleResolutionResult }\nexport type Result<T> = ResultOk<T> | ResultErr\n\ntype ResolutionContext = {\n tenantId: string\n valueType: ToggleValueType\n}\n\nconst toCachedResolution = (value: unknown): ToggleResolutionResult | null => {\n if (typeof value !== \"object\" || value === null) return null\n const record = value as Partial<ToggleResolutionResult>\n if (\n !record.valueType ||\n typeof record.source !== \"string\" ||\n !record.toggleId ||\n !record.identifier ||\n !record.tenantId\n )\n return null\n return value as ToggleResolutionResult\n}\n\nexport const getIsEnabledCacheKey = (identifier: string, tenantId: string) => {\n return `feature_toggles:resolution:${identifier}:${tenantId}`\n}\n\nconst getIdentifierTag = (identifier: string) => `feature_toggles:identifier:${identifier}`\nconst getTenantTag = (tenantId: string) => `feature_toggles:tenant:${tenantId}`\n\nconst getCacheTags = (identifier: string, tenantId: string) => {\n return [getIdentifierTag(identifier), getTenantTag(tenantId)]\n}\n\nexport class FeatureTogglesService {\n private cacheTtlMs: number = 1 * 60 * 1000 // 1 minute\n constructor(\n private readonly cache: CacheService,\n private readonly em: EntityManager\n ) { }\n\n private async saveCache(\n identifier: string,\n tenantId: string,\n result: ToggleResolutionResult,\n ) {\n const key = getIsEnabledCacheKey(identifier, tenantId)\n await runWithCacheTenant(\n tenantId,\n () => this.cache.set(key, result, { ttl: this.cacheTtlMs, tags: getCacheTags(identifier, tenantId) }),\n )\n }\n\n private async resolveToggle(identifier: string, tenantId: string): Promise<ToggleResolutionResult> {\n const key = getIsEnabledCacheKey(identifier, tenantId)\n\n const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))\n
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe,6BAA6B;AAErD,SAAuB,0BAA0B;AAmCjD,MAAM,qBAAqB,CAAC,UAAkD;AAC5E,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,MACE,CAAC,OAAO,aACR,OAAO,OAAO,WAAW,YACzB,CAAC,OAAO,YACR,CAAC,OAAO,cACR,CAAC,OAAO;AAER,WAAO;AACT,SAAO;AACT;AAEO,MAAM,uBAAuB,CAAC,YAAoB,aAAqB;AAC5E,SAAO,8BAA8B,UAAU,IAAI,QAAQ;AAC7D;AAEA,MAAM,mBAAmB,CAAC,eAAuB,8BAA8B,UAAU;AACzF,MAAM,eAAe,CAAC,aAAqB,0BAA0B,QAAQ;AAE7E,MAAM,eAAe,CAAC,YAAoB,aAAqB;AAC7D,SAAO,CAAC,iBAAiB,UAAU,GAAG,aAAa,QAAQ,CAAC;AAC9D;AAEO,MAAM,sBAAsB;AAAA
|
|
4
|
+
"sourcesContent": ["import { FeatureToggle, FeatureToggleOverride } from \"../data/entities\"\nimport { EntityManager } from \"@mikro-orm/core\"\nimport { CacheService, runWithCacheTenant } from \"@open-mercato/cache\"\n\ntype ToggleValueType = \"boolean\" | \"string\" | \"number\" | \"json\"\n\ntype ToggleResolutionSource = \"override\" | \"default\" | \"missing\"\n\ntype ToggleResolutionResult = {\n valueType: ToggleValueType\n value: boolean | string | number | unknown | null\n source: ToggleResolutionSource\n toggleId: string\n identifier: string\n tenantId: string\n}\n\ntype ToggleErrorCode = \"TYPE_MISMATCH\" | \"MISSING_TOGGLE\" | \"INVALID_VALUE\"\n\ntype ToggleError = {\n code: ToggleErrorCode\n message: string\n identifier: string\n expectedType: ToggleValueType\n actualType?: ToggleValueType\n source?: ToggleResolutionSource\n}\n\ntype ResultOk<T> = { ok: true; value: T; resolution: ToggleResolutionResult }\ntype ResultErr = { ok: false; error: ToggleError; resolution: ToggleResolutionResult }\nexport type Result<T> = ResultOk<T> | ResultErr\n\ntype ResolutionContext = {\n tenantId: string\n valueType: ToggleValueType\n}\n\nconst toCachedResolution = (value: unknown): ToggleResolutionResult | null => {\n if (typeof value !== \"object\" || value === null) return null\n const record = value as Partial<ToggleResolutionResult>\n if (\n !record.valueType ||\n typeof record.source !== \"string\" ||\n !record.toggleId ||\n !record.identifier ||\n !record.tenantId\n )\n return null\n return value as ToggleResolutionResult\n}\n\nexport const getIsEnabledCacheKey = (identifier: string, tenantId: string) => {\n return `feature_toggles:resolution:${identifier}:${tenantId}`\n}\n\nconst getIdentifierTag = (identifier: string) => `feature_toggles:identifier:${identifier}`\nconst getTenantTag = (tenantId: string) => `feature_toggles:tenant:${tenantId}`\n\nconst getCacheTags = (identifier: string, tenantId: string) => {\n return [getIdentifierTag(identifier), getTenantTag(tenantId)]\n}\n\nexport class FeatureTogglesService {\n private cacheTtlMs: number = 1 * 60 * 1000 // 1 minute\n // Resolution cache can be disabled via env (e.g. integration tests that flip\n // overrides rapidly between cases). The 1-minute TTL is a production\n // optimization; under fast flag churn it can serve a stale value across\n // override set/clear despite invalidation, so tests opt out for determinism.\n private readonly cacheDisabled: boolean =\n process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === '1' ||\n process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === 'true'\n constructor(\n private readonly cache: CacheService,\n private readonly em: EntityManager\n ) { }\n\n private async saveCache(\n identifier: string,\n tenantId: string,\n result: ToggleResolutionResult,\n ) {\n if (this.cacheDisabled) return\n const key = getIsEnabledCacheKey(identifier, tenantId)\n await runWithCacheTenant(\n tenantId,\n () => this.cache.set(key, result, { ttl: this.cacheTtlMs, tags: getCacheTags(identifier, tenantId) }),\n )\n }\n\n private async resolveToggle(identifier: string, tenantId: string): Promise<ToggleResolutionResult> {\n const key = getIsEnabledCacheKey(identifier, tenantId)\n\n if (!this.cacheDisabled) {\n const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))\n if (cached) {\n const parsed = toCachedResolution(cached)\n if (parsed) return parsed\n }\n }\n\n let toggle: FeatureToggle | null = null\n toggle = await this.em.findOne(FeatureToggle, { identifier, deletedAt: null })\n\n if (!toggle) {\n const result: ToggleResolutionResult = {\n valueType: \"boolean\",\n value: null,\n source: \"missing\",\n toggleId: \"\",\n identifier,\n tenantId,\n }\n return result\n }\n\n let override: FeatureToggleOverride | null = null\n override = await this.em.findOne(FeatureToggleOverride, { toggle: toggle.id, tenantId })\n\n\n const result: ToggleResolutionResult = {\n valueType: toggle.type,\n value: override ? override.value : toggle.defaultValue,\n source: override ? \"override\" : \"default\",\n toggleId: toggle.id,\n identifier: toggle.identifier,\n tenantId,\n }\n\n await this.saveCache(identifier, tenantId, result)\n return result\n }\n\n public async invalidateIsEnabledCacheByIdentifierTag(identifier: string) {\n await this.cache.deleteByTags([getIdentifierTag(identifier)])\n }\n\n public async invalidateIsEnabledCacheByKey(identifier: string, tenantId: string) {\n await runWithCacheTenant(tenantId, () => this.cache.delete(getIsEnabledCacheKey(identifier, tenantId)))\n }\n\n public async getFeatureToggleValue<T>(\n identifier: string,\n ctx: ResolutionContext\n ): Promise<Result<T>> {\n const resolution = await this.resolveToggle(identifier, ctx.tenantId)\n\n if (resolution.source === \"missing\") {\n console.warn(`[feature_toggles] Toggle \"${identifier}\" not found (missing).`)\n return {\n ok: false,\n error: {\n code: \"MISSING_TOGGLE\",\n message: `Toggle \"${identifier}\" not found (missing).`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n\n\n if (resolution.valueType !== ctx.valueType) {\n console.error(\n `[feature_toggles] Toggle \"${identifier}\" has type \"${resolution.valueType}\" but \"${ctx.valueType}\" was requested.`,\n { resolution }\n )\n return {\n ok: false,\n error: {\n code: \"TYPE_MISMATCH\",\n message: `Toggle \"${identifier}\" has type \"${resolution.valueType}\" but \"${ctx.valueType}\" was requested.`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n const isValueValid =\n (ctx.valueType === \"boolean\" && typeof resolution.value === \"boolean\") ||\n (ctx.valueType === \"string\" && typeof resolution.value === \"string\") ||\n (ctx.valueType === \"number\" && typeof resolution.value === \"number\") ||\n (ctx.valueType === \"json\")\n\n if (!isValueValid) {\n console.error(\n `[feature_toggles] Toggle \"${identifier}\" has invalid value for type \"${resolution.valueType}\".`,\n { resolution }\n )\n return {\n ok: false,\n error: {\n code: \"INVALID_VALUE\",\n message: `Toggle \"${identifier}\" has invalid value for type \"${resolution.valueType}\".`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n return {\n ok: true,\n value: resolution.value as T,\n resolution,\n }\n }\n\n public async getBoolConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<boolean>(identifier, { tenantId, valueType: \"boolean\" })\n }\n\n public async getNumberConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<number>(identifier, { tenantId, valueType: \"number\" })\n }\n\n public async getStringConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<string>(identifier, { tenantId, valueType: \"string\" })\n }\n\n public async getJsonConfig<T = unknown>(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<T>(identifier, { tenantId, valueType: \"json\" })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["result"]
|
|
7
7
|
}
|
|
@@ -3,6 +3,11 @@ import { refreshCoverageSnapshot } from "../lib/coverage.js";
|
|
|
3
3
|
const metadata = { event: "query_index.coverage.refresh", persistent: false };
|
|
4
4
|
const DEFAULT_DELAY_MS = 0;
|
|
5
5
|
const pending = /* @__PURE__ */ new Map();
|
|
6
|
+
function forkRefreshEntityManager(em) {
|
|
7
|
+
const fork = em.fork;
|
|
8
|
+
if (typeof fork !== "function") return em;
|
|
9
|
+
return fork.call(em, { clear: true, freshEventManager: true, useContext: false });
|
|
10
|
+
}
|
|
6
11
|
function scopeKey(input) {
|
|
7
12
|
const entity = String(input.entityType || "");
|
|
8
13
|
const tenant = input.tenantId ?? "__null__";
|
|
@@ -19,9 +24,9 @@ async function handle(payload, ctx) {
|
|
|
19
24
|
const organizationId = payload?.organizationId ?? null;
|
|
20
25
|
const withDeleted = payload?.withDeleted === true;
|
|
21
26
|
const delayMs = typeof payload?.delayMs === "number" && payload.delayMs >= 0 ? payload.delayMs : DEFAULT_DELAY_MS;
|
|
22
|
-
const em = ctx.resolve("em");
|
|
23
27
|
const key = scopeKey({ entityType, tenantId, organizationId, withDeleted });
|
|
24
28
|
const handleRefresh = async () => {
|
|
29
|
+
const em = forkRefreshEntityManager(ctx.resolve("em"));
|
|
25
30
|
try {
|
|
26
31
|
await refreshCoverageSnapshot(em, { entityType, tenantId, organizationId, withDeleted });
|
|
27
32
|
} catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/subscribers/coverage_refresh.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { refreshCoverageSnapshot } from '../lib/coverage'\n\nexport const metadata = { event: 'query_index.coverage.refresh', persistent: false }\n\ntype Payload = {\n entityType?: string\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n delayMs?: number\n}\n\nconst DEFAULT_DELAY_MS = 0\nconst pending = new Map<string, NodeJS.Timeout>()\n\nfunction scopeKey(input: Payload): string {\n const entity = String(input.entityType || '')\n const tenant = input.tenantId ?? '__null__'\n const org = input.organizationId ?? '__null__'\n const deleted = input.withDeleted ? '1' : '0'\n\n return `${entity}|${tenant}|${org}|${deleted}`\n}\n\nexport default async function handle(payload: Payload, ctx: { resolve: <T = any>(name: string) => T }) {\n const entityType = String(payload?.entityType || '')\n if (!entityType) {\n return\n }\n\n const tenantId = payload?.tenantId ?? null\n const organizationId = payload?.organizationId ?? null\n const withDeleted = payload?.withDeleted === true\n const delayMs = typeof payload?.delayMs === 'number' && payload.delayMs >= 0 ? payload.delayMs : DEFAULT_DELAY_MS\n\n const
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AAEjC,MAAM,WAAW,EAAE,OAAO,gCAAgC,YAAY,MAAM;AAUnF,MAAM,mBAAmB;AACzB,MAAM,UAAU,oBAAI,IAA4B;AAEhD,SAAS,SAAS,OAAwB;AACxC,QAAM,SAAS,OAAO,MAAM,cAAc,EAAE;AAC5C,QAAM,SAAS,MAAM,YAAY;AACjC,QAAM,MAAM,MAAM,kBAAkB;AACpC,QAAM,UAAU,MAAM,cAAc,MAAM;AAE1C,SAAO,GAAG,MAAM,IAAI,MAAM,IAAI,GAAG,IAAI,OAAO;AAC9C;AAEA,eAAO,OAA8B,SAAkB,KAAgD;AACrG,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,MAAI,CAAC,YAAY;AACf;AAAA,EACF;AAEA,QAAM,WAAW,SAAS,YAAY;AACtC,QAAM,iBAAiB,SAAS,kBAAkB;AAClD,QAAM,cAAc,SAAS,gBAAgB;AAC7C,QAAM,UAAU,OAAO,SAAS,YAAY,YAAY,QAAQ,WAAW,IAAI,QAAQ,UAAU;AAEjG,QAAM,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { refreshCoverageSnapshot } from '../lib/coverage'\n\nexport const metadata = { event: 'query_index.coverage.refresh', persistent: false }\n\ntype Payload = {\n entityType?: string\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n delayMs?: number\n}\n\nconst DEFAULT_DELAY_MS = 0\nconst pending = new Map<string, NodeJS.Timeout>()\n\nfunction forkRefreshEntityManager(em: EntityManager): EntityManager {\n const fork = (em as unknown as { fork?: (options?: Record<string, unknown>) => EntityManager }).fork\n if (typeof fork !== 'function') return em\n return fork.call(em, { clear: true, freshEventManager: true, useContext: false })\n}\n\nfunction scopeKey(input: Payload): string {\n const entity = String(input.entityType || '')\n const tenant = input.tenantId ?? '__null__'\n const org = input.organizationId ?? '__null__'\n const deleted = input.withDeleted ? '1' : '0'\n\n return `${entity}|${tenant}|${org}|${deleted}`\n}\n\nexport default async function handle(payload: Payload, ctx: { resolve: <T = any>(name: string) => T }) {\n const entityType = String(payload?.entityType || '')\n if (!entityType) {\n return\n }\n\n const tenantId = payload?.tenantId ?? null\n const organizationId = payload?.organizationId ?? null\n const withDeleted = payload?.withDeleted === true\n const delayMs = typeof payload?.delayMs === 'number' && payload.delayMs >= 0 ? payload.delayMs : DEFAULT_DELAY_MS\n\n const key = scopeKey({ entityType, tenantId, organizationId, withDeleted })\n\n const handleRefresh = async () => {\n const em = forkRefreshEntityManager(ctx.resolve<EntityManager>('em'))\n try {\n await refreshCoverageSnapshot(em, { entityType, tenantId, organizationId, withDeleted })\n } catch (err) {\n console.warn('[query_index] Failed to refresh coverage snapshot', {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n error: err instanceof Error ? err.message : err,\n })\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.coverage.refresh',\n error: err,\n entityType,\n tenantId,\n organizationId,\n payload,\n },\n )\n }\n }\n\n if (delayMs === 0) {\n await handleRefresh()\n return\n }\n\n const existing = pending.get(key)\n if (existing) {\n clearTimeout(existing)\n }\n\n const timer = setTimeout(() => {\n pending.delete(key)\n void handleRefresh()\n }, delayMs)\n if (typeof timer.unref === 'function') {\n timer.unref()\n }\n pending.set(key, timer)\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AAEjC,MAAM,WAAW,EAAE,OAAO,gCAAgC,YAAY,MAAM;AAUnF,MAAM,mBAAmB;AACzB,MAAM,UAAU,oBAAI,IAA4B;AAEhD,SAAS,yBAAyB,IAAkC;AAClE,QAAM,OAAQ,GAAkF;AAChG,MAAI,OAAO,SAAS,WAAY,QAAO;AACvC,SAAO,KAAK,KAAK,IAAI,EAAE,OAAO,MAAM,mBAAmB,MAAM,YAAY,MAAM,CAAC;AAClF;AAEA,SAAS,SAAS,OAAwB;AACxC,QAAM,SAAS,OAAO,MAAM,cAAc,EAAE;AAC5C,QAAM,SAAS,MAAM,YAAY;AACjC,QAAM,MAAM,MAAM,kBAAkB;AACpC,QAAM,UAAU,MAAM,cAAc,MAAM;AAE1C,SAAO,GAAG,MAAM,IAAI,MAAM,IAAI,GAAG,IAAI,OAAO;AAC9C;AAEA,eAAO,OAA8B,SAAkB,KAAgD;AACrG,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,MAAI,CAAC,YAAY;AACf;AAAA,EACF;AAEA,QAAM,WAAW,SAAS,YAAY;AACtC,QAAM,iBAAiB,SAAS,kBAAkB;AAClD,QAAM,cAAc,SAAS,gBAAgB;AAC7C,QAAM,UAAU,OAAO,SAAS,YAAY,YAAY,QAAQ,WAAW,IAAI,QAAQ,UAAU;AAEjG,QAAM,MAAM,SAAS,EAAE,YAAY,UAAU,gBAAgB,YAAY,CAAC;AAE1E,QAAM,gBAAgB,YAAY;AAChC,UAAM,KAAK,yBAAyB,IAAI,QAAuB,IAAI,CAAC;AACpE,QAAI;AACF,YAAM,wBAAwB,IAAI,EAAE,YAAY,UAAU,gBAAgB,YAAY,CAAC;AAAA,IACzF,SAAS,KAAK;AACZ,cAAQ,KAAK,qDAAqD;AAAA,QAChE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,eAAe,QAAQ,IAAI,UAAU;AAAA,MAC9C,CAAC;AACD,YAAM;AAAA,QACJ,EAAE,GAAG;AAAA,QACL;AAAA,UACE,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,YAAY,GAAG;AACjB,UAAM,cAAc;AACpB;AAAA,EACF;AAEA,QAAM,WAAW,QAAQ,IAAI,GAAG;AAChC,MAAI,UAAU;AACZ,iBAAa,QAAQ;AAAA,EACvB;AAEA,QAAM,QAAQ,WAAW,MAAM;AAC7B,YAAQ,OAAO,GAAG;AAClB,SAAK,cAAc;AAAA,EACrB,GAAG,OAAO;AACV,MAAI,OAAO,MAAM,UAAU,YAAY;AACrC,UAAM,MAAM;AAAA,EACd;AACA,UAAQ,IAAI,KAAK,KAAK;AACxB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,193 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
onEdgesChange: onEdgesChangeProp,
|
|
29
|
-
onNodeClick: onNodeClickProp,
|
|
30
|
-
onEdgeClick: onEdgeClickProp,
|
|
31
|
-
onConnect: onConnectProp,
|
|
32
|
-
editable = false,
|
|
33
|
-
className = "",
|
|
34
|
-
height = "600px"
|
|
35
|
-
}) {
|
|
36
|
-
const t = useT();
|
|
37
|
-
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
38
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
39
|
-
const { resolvedTheme } = useTheme();
|
|
40
|
-
const isDark = resolvedTheme === "dark";
|
|
41
|
-
const backgroundDotColor = isDark ? "#374151" : "#e5e7eb";
|
|
42
|
-
const [isCompactViewport, setIsCompactViewport] = useState(false);
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (typeof window === "undefined") return;
|
|
45
|
-
const mediaQuery = window.matchMedia("(max-width: 1279px)");
|
|
46
|
-
const updateViewportMode = () => setIsCompactViewport(mediaQuery.matches);
|
|
47
|
-
updateViewportMode();
|
|
48
|
-
mediaQuery.addEventListener("change", updateViewportMode);
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import dynamic from "next/dynamic";
|
|
5
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
6
|
+
const WorkflowGraphImpl = dynamic(() => import("./WorkflowGraphImpl.js"), {
|
|
7
|
+
ssr: false,
|
|
8
|
+
loading: () => null
|
|
9
|
+
});
|
|
10
|
+
function WorkflowGraphPlaceholder({ height }) {
|
|
11
|
+
return /* @__PURE__ */ jsx(
|
|
12
|
+
"div",
|
|
13
|
+
{
|
|
14
|
+
className: "workflow-graph-container flex items-center justify-center rounded-lg border border-border bg-muted/30",
|
|
15
|
+
style: { height },
|
|
16
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "h-6 w-6 text-muted-foreground" })
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function WorkflowGraph(props) {
|
|
21
|
+
const { height = "600px" } = props;
|
|
22
|
+
const [isImplReady, setIsImplReady] = React.useState(false);
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
void import("./WorkflowGraphImpl.js").then(() => {
|
|
26
|
+
if (!cancelled) setIsImplReady(true);
|
|
27
|
+
});
|
|
49
28
|
return () => {
|
|
50
|
-
|
|
29
|
+
cancelled = true;
|
|
51
30
|
};
|
|
52
31
|
}, []);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}, [initialNodes, setNodes]);
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
setEdges(initialEdges);
|
|
58
|
-
}, [initialEdges, setEdges]);
|
|
59
|
-
const onConnect = useCallback(
|
|
60
|
-
(connection) => {
|
|
61
|
-
if (onConnectProp) {
|
|
62
|
-
onConnectProp(connection);
|
|
63
|
-
} else {
|
|
64
|
-
const newEdge = {
|
|
65
|
-
...connection,
|
|
66
|
-
type: "workflowTransition",
|
|
67
|
-
animated: false,
|
|
68
|
-
markerEnd: {
|
|
69
|
-
type: MarkerType.ArrowClosed,
|
|
70
|
-
width: 16,
|
|
71
|
-
height: 16,
|
|
72
|
-
color: "#9ca3af"
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
setEdges((eds) => addEdge(newEdge, eds));
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
[setEdges, onConnectProp]
|
|
79
|
-
);
|
|
80
|
-
const handleNodesChange = useCallback(
|
|
81
|
-
(changes) => {
|
|
82
|
-
onNodesChange(changes);
|
|
83
|
-
if (onNodesChangeProp) {
|
|
84
|
-
onNodesChangeProp(changes);
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
[onNodesChange, onNodesChangeProp]
|
|
88
|
-
);
|
|
89
|
-
const handleEdgesChange = useCallback(
|
|
90
|
-
(changes) => {
|
|
91
|
-
onEdgesChange(changes);
|
|
92
|
-
if (onEdgesChangeProp) {
|
|
93
|
-
onEdgesChangeProp(changes);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
[onEdgesChange, onEdgesChangeProp]
|
|
97
|
-
);
|
|
98
|
-
const nodeTypes = useMemo(
|
|
99
|
-
() => ({
|
|
100
|
-
start: StartNode,
|
|
101
|
-
end: EndNode,
|
|
102
|
-
userTask: UserTaskNode,
|
|
103
|
-
automated: AutomatedNode,
|
|
104
|
-
subWorkflow: SubWorkflowNode,
|
|
105
|
-
waitForSignal: WaitForSignalNode,
|
|
106
|
-
waitForTimer: WaitForTimerNode
|
|
107
|
-
}),
|
|
108
|
-
[]
|
|
109
|
-
);
|
|
110
|
-
const edgeTypes = useMemo(
|
|
111
|
-
() => ({
|
|
112
|
-
workflowTransition: WorkflowTransitionEdge
|
|
113
|
-
}),
|
|
114
|
-
[]
|
|
115
|
-
);
|
|
116
|
-
return /* @__PURE__ */ jsx("div", { className: `workflow-graph-container ${className}`, style: { height }, children: /* @__PURE__ */ jsxs(
|
|
117
|
-
ReactFlow,
|
|
118
|
-
{
|
|
119
|
-
nodes,
|
|
120
|
-
edges,
|
|
121
|
-
nodeTypes,
|
|
122
|
-
edgeTypes,
|
|
123
|
-
onNodesChange: handleNodesChange,
|
|
124
|
-
onEdgesChange: handleEdgesChange,
|
|
125
|
-
onConnect: editable ? onConnect : void 0,
|
|
126
|
-
onNodeClick: onNodeClickProp,
|
|
127
|
-
onEdgeClick: onEdgeClickProp,
|
|
128
|
-
connectionMode: ConnectionMode.Loose,
|
|
129
|
-
fitView: true,
|
|
130
|
-
fitViewOptions: {
|
|
131
|
-
padding: 0.2,
|
|
132
|
-
maxZoom: isCompactViewport ? 0.9 : 1
|
|
133
|
-
},
|
|
134
|
-
minZoom: 0.1,
|
|
135
|
-
maxZoom: 2,
|
|
136
|
-
defaultEdgeOptions: {
|
|
137
|
-
type: "workflowTransition",
|
|
138
|
-
animated: false,
|
|
139
|
-
markerEnd: {
|
|
140
|
-
type: MarkerType.ArrowClosed,
|
|
141
|
-
width: 16,
|
|
142
|
-
height: 16,
|
|
143
|
-
color: "#9ca3af"
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
nodesDraggable: editable,
|
|
147
|
-
nodesConnectable: editable,
|
|
148
|
-
elementsSelectable: editable,
|
|
149
|
-
proOptions: { hideAttribution: true },
|
|
150
|
-
children: [
|
|
151
|
-
/* @__PURE__ */ jsx(
|
|
152
|
-
Background,
|
|
153
|
-
{
|
|
154
|
-
variant: BackgroundVariant.Dots,
|
|
155
|
-
gap: 16,
|
|
156
|
-
size: 1,
|
|
157
|
-
color: backgroundDotColor
|
|
158
|
-
}
|
|
159
|
-
),
|
|
160
|
-
/* @__PURE__ */ jsx(
|
|
161
|
-
Controls,
|
|
162
|
-
{
|
|
163
|
-
showZoom: true,
|
|
164
|
-
showFitView: true,
|
|
165
|
-
showInteractive: false,
|
|
166
|
-
position: isCompactViewport ? "bottom-right" : "top-right",
|
|
167
|
-
className: `!bg-card !border-border !shadow-md [&>button]:!bg-card [&>button]:!border-border [&>button]:!fill-foreground [&>button:hover]:!bg-muted ${isCompactViewport ? "scale-90 origin-bottom-right" : ""}`
|
|
168
|
-
}
|
|
169
|
-
),
|
|
170
|
-
!isCompactViewport && /* @__PURE__ */ jsx(
|
|
171
|
-
MiniMap,
|
|
172
|
-
{
|
|
173
|
-
nodeStrokeWidth: 3,
|
|
174
|
-
nodeColor: (node) => {
|
|
175
|
-
const status = node.data?.status || "not_started";
|
|
176
|
-
return STATUS_COLORS[status]?.hex || STATUS_COLORS.not_started.hex;
|
|
177
|
-
},
|
|
178
|
-
maskColor: "rgba(0, 0, 0, 0.1)",
|
|
179
|
-
position: "bottom-left",
|
|
180
|
-
className: "!bg-card !border !border-border !rounded-lg"
|
|
181
|
-
}
|
|
182
|
-
),
|
|
183
|
-
!editable && !isCompactViewport && /* @__PURE__ */ jsx(Panel, { position: "top-left", style: { margin: 10 }, children: /* @__PURE__ */ jsx("div", { className: "bg-card rounded-lg shadow-sm border border-border px-4 py-2", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground font-medium", children: t("workflows.graph.visualization") }) }) }),
|
|
184
|
-
editable && !isCompactViewport && /* @__PURE__ */ jsx(Panel, { position: "top-left", style: { margin: 10 }, children: /* @__PURE__ */ jsxs(Alert, { variant: "info", className: "max-w-sm", children: [
|
|
185
|
-
/* @__PURE__ */ jsx(Edit3, { className: "size-4" }),
|
|
186
|
-
/* @__PURE__ */ jsx(AlertDescription, { className: "font-medium", children: t("workflows.graph.editModeInfo") })
|
|
187
|
-
] }) })
|
|
188
|
-
]
|
|
189
|
-
}
|
|
190
|
-
) });
|
|
32
|
+
if (!isImplReady) return /* @__PURE__ */ jsx(WorkflowGraphPlaceholder, { height });
|
|
33
|
+
return /* @__PURE__ */ jsx(WorkflowGraphImpl, { ...props });
|
|
191
34
|
}
|
|
192
35
|
function WorkflowGraphReadOnly({
|
|
193
36
|
nodes,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/components/WorkflowGraph.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport type { Node, Edge, Connection } from '@xyflow/react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\n\nexport interface WorkflowGraphProps {\n initialNodes?: Node[]\n initialEdges?: Edge[]\n onNodesChange?: (changes: any[]) => void\n onEdgesChange?: (changes: any[]) => void\n onNodeClick?: (event: React.MouseEvent, node: Node) => void\n onEdgeClick?: (event: React.MouseEvent, edge: Edge) => void\n onConnect?: (connection: Connection) => void\n editable?: boolean\n className?: string\n height?: string\n}\n\nconst WorkflowGraphImpl = dynamic(() => import('./WorkflowGraphImpl'), {\n ssr: false,\n loading: () => null,\n})\n\nfunction WorkflowGraphPlaceholder({ height }: { height: string }) {\n return (\n <div\n className=\"workflow-graph-container flex items-center justify-center rounded-lg border border-border bg-muted/30\"\n style={{ height }}\n >\n <Spinner className=\"h-6 w-6 text-muted-foreground\" />\n </div>\n )\n}\n\n/**\n * WorkflowGraph \u2014 lazy-loaded ReactFlow wrapper.\n *\n * @xyflow/react is loaded via next/dynamic({ ssr: false }) so the ~12 MB\n * package only enters the Turbopack module graph when this component\n * actually renders.\n */\nexport function WorkflowGraph(props: WorkflowGraphProps) {\n const { height = '600px' } = props\n // Track impl-chunk readiness so the loading placeholder respects the\n // caller's `height` prop (next/dynamic's `loading` cannot access props).\n // The browser caches the module, so the duplicate `import()` is free.\n const [isImplReady, setIsImplReady] = React.useState(false)\n React.useEffect(() => {\n let cancelled = false\n void import('./WorkflowGraphImpl').then(() => {\n if (!cancelled) setIsImplReady(true)\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n if (!isImplReady) return <WorkflowGraphPlaceholder height={height} />\n return <WorkflowGraphImpl {...props} />\n}\n\n/**\n * WorkflowGraphReadOnly \u2014 read-only viewer that reuses WorkflowGraph.\n */\nexport function WorkflowGraphReadOnly({\n nodes,\n edges,\n className = '',\n height = '500px',\n}: {\n nodes: Node[]\n edges: Edge[]\n className?: string\n height?: string\n}) {\n return (\n <WorkflowGraph\n initialNodes={nodes}\n initialEdges={edges}\n editable={false}\n className={className}\n height={height}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA+BM;AA7BN,YAAY,WAAW;AACvB,OAAO,aAAa;AAEpB,SAAS,eAAe;AAexB,MAAM,oBAAoB,QAAQ,MAAM,OAAO,qBAAqB,GAAG;AAAA,EACrE,KAAK;AAAA,EACL,SAAS,MAAM;AACjB,CAAC;AAED,SAAS,yBAAyB,EAAE,OAAO,GAAuB;AAChE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO,EAAE,OAAO;AAAA,MAEhB,8BAAC,WAAQ,WAAU,iCAAgC;AAAA;AAAA,EACrD;AAEJ;AASO,SAAS,cAAc,OAA2B;AACvD,QAAM,EAAE,SAAS,QAAQ,IAAI;AAI7B,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAC1D,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,SAAK,OAAO,qBAAqB,EAAE,KAAK,MAAM;AAC5C,UAAI,CAAC,UAAW,gBAAe,IAAI;AAAA,IACrC,CAAC;AACD,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,MAAI,CAAC,YAAa,QAAO,oBAAC,4BAAyB,QAAgB;AACnE,SAAO,oBAAC,qBAAmB,GAAG,OAAO;AACvC;AAKO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,SAAS;AACX,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAc;AAAA,MACd,cAAc;AAAA,MACd,UAAU;AAAA,MACV;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|