@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/calendar/format.ts +25 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/workflows/backend/definitions/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { Trash2 } from 'lucide-react'\n\ntype WorkflowDefinitionSource = 'code' | 'code_override' | 'user'\n\ntype WorkflowDefinition = {\n id: string\n workflowId: string\n workflowName: string\n description: string | null\n version: number\n definition: Record<string, unknown>\n enabled: boolean\n effectiveFrom: string | null\n effectiveTo: string | null\n metadata: {\n tags?: string[]\n category?: string\n icon?: string\n } | null\n tenantId: string\n organizationId: string\n createdAt: string\n updatedAt: string\n createdBy: string | null\n source?: WorkflowDefinitionSource\n isCodeBased?: boolean\n}\n\ntype DefinitionsResponse = {\n data: WorkflowDefinition[]\n pagination: {\n total: number\n limit: number\n offset: number\n hasMore: boolean\n }\n}\n\ntype CreateDefinitionResponse = {\n data?: {\n id?: string\n }\n error?: string\n}\n\nconst WORKFLOW_ID_MAX_LENGTH = 100\n\nfunction buildDuplicateWorkflowId(sourceWorkflowId: string, attempt: number): string {\n const suffix = attempt === 0 ? '_copy' : `_copy_${attempt + 1}`\n const maxBaseLength = Math.max(1, WORKFLOW_ID_MAX_LENGTH - suffix.length)\n const base = sourceWorkflowId.slice(0, maxBaseLength)\n return `${base}${suffix}`\n}\n\nexport default function WorkflowDefinitionsListPage() {\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const t = useT()\n const router = useRouter()\n const queryClient = useQueryClient()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; name: string; updatedAt: string | null } | null>(null)\n\n const { data, isLoading, error } = useQuery({\n queryKey: ['workflow-definitions', 'list', filterValues, page],\n queryFn: async () => {\n const params = new URLSearchParams()\n const offset = (page - 1) * pageSize\n params.set('limit', pageSize.toString())\n params.set('offset', offset.toString())\n\n if (filterValues.enabled !== undefined && filterValues.enabled !== '') {\n params.set('enabled', filterValues.enabled as string)\n }\n if (filterValues.workflowId) params.set('workflowId', filterValues.workflowId as string)\n if (filterValues.search) params.set('search', filterValues.search as string)\n\n const result = await apiCall<DefinitionsResponse>(\n `/api/workflows/definitions?${params.toString()}`\n )\n\n if (!result.ok) {\n throw new Error('Failed to fetch workflow definitions')\n }\n\n const response = result.result\n if (response?.pagination) {\n setTotal(response.pagination.total || 0)\n const calculatedPages = Math.ceil((response.pagination.total || 0) / pageSize)\n setTotalPages(calculatedPages || 1)\n }\n\n return response?.data || []\n },\n })\n\n const handleDelete = (id: string, workflowName: string, updatedAt: string | null) => {\n setDeleteTarget({ id, name: workflowName, updatedAt })\n }\n\n const confirmDelete = async () => {\n if (!deleteTarget) return\n\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(deleteTarget.updatedAt),\n () => apiCall(`/api/workflows/definitions/${deleteTarget.id}`, {\n method: 'DELETE',\n }),\n )\n\n if (result.ok) {\n flash(t('workflows.messages.deleted'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n const conflictError = Object.assign(new Error(t('workflows.messages.deleteFailed')), {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n })\n if (!surfaceRecordConflict(conflictError, t)) {\n flash(t('workflows.messages.deleteFailed'), 'error')\n }\n }\n setDeleteTarget(null)\n }\n\n const handleToggleEnabled = async (id: string, currentEnabled: boolean) => {\n const result = await apiCall(`/api/workflows/definitions/${id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n enabled: !currentEnabled,\n }),\n })\n\n if (result.ok) {\n flash(t('workflows.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n flash(t('workflows.messages.updateFailed'), 'error')\n }\n }\n\n const handleDuplicate = async (definition: WorkflowDefinition) => {\n for (let attempt = 0; attempt < 10; attempt += 1) {\n const duplicateWorkflowId = buildDuplicateWorkflowId(definition.workflowId, attempt)\n const result = await apiCall<CreateDefinitionResponse>('/api/workflows/definitions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n workflowId: duplicateWorkflowId,\n workflowName: definition.workflowName,\n description: definition.description,\n version: definition.version,\n definition: definition.definition,\n metadata: definition.metadata,\n enabled: definition.enabled,\n }),\n })\n\n if (result.ok) {\n flash(t('workflows.messages.workflowDuplicated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n return\n }\n\n if (result.status !== 409) {\n break\n }\n }\n\n flash(t('workflows.errors.createFailed'), 'error')\n }\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n const next: FilterValues = {}\n Object.entries(values).forEach(([key, value]) => {\n if (value !== undefined && value !== '') next[key] = value\n })\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const filters: FilterDef[] = [\n {\n id: 'search',\n type: 'text',\n label: t('workflows.filters.search'),\n placeholder: t('workflows.filters.searchPlaceholder'),\n },\n {\n id: 'enabled',\n type: 'select',\n label: t('workflows.filters.status'),\n options: [\n { label: t('common.all'), value: '' },\n { label: t('common.enabled'), value: 'true' },\n { label: t('common.disabled'), value: 'false' },\n ],\n },\n {\n id: 'workflowId',\n type: 'text',\n label: t('workflows.filters.workflowId'),\n placeholder: t('workflows.filters.workflowIdPlaceholder'),\n },\n ]\n\n const columns: ColumnDef<WorkflowDefinition>[] = [\n {\n id: 'workflowId',\n header: t('workflows.fields.workflowId'),\n accessorKey: 'workflowId',\n meta: { truncate: false },\n cell: ({ row }) => (\n <span className=\"font-mono text-sm\">{row.original.workflowId}</span>\n ),\n },\n {\n id: 'workflowName',\n header: t('workflows.fields.workflowName'),\n accessorKey: 'workflowName',\n meta: { truncate: false },\n cell: ({ row }) => (\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{row.original.workflowName}</span>\n {row.original.source === 'code' && (\n <Badge variant=\"secondary\">{t('workflows.source.code')}</Badge>\n )}\n {row.original.source === 'code_override' && (\n <Badge variant=\"outline\">{t('workflows.source.code_override')}</Badge>\n )}\n </div>\n {row.original.description && (\n <div className=\"text-xs text-muted-foreground\">\n {row.original.description}\n </div>\n )}\n {row.original.metadata?.category && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {row.original.metadata.category}\n </div>\n )}\n </div>\n ),\n },\n {\n id: 'version',\n header: t('workflows.fields.version'),\n accessorKey: 'version',\n meta: { truncate: false },\n cell: ({ row }) => (\n <Badge variant=\"secondary\" className=\"font-mono\">\n v{row.original.version}\n </Badge>\n ),\n },\n {\n id: 'enabled',\n header: t('workflows.fields.enabled'),\n accessorKey: 'enabled',\n cell: ({ row }) => (\n <button\n onClick={() => handleToggleEnabled(row.original.id, row.original.enabled)}\n className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium cursor-pointer ${\n row.original.enabled\n ? 'bg-status-success-bg text-status-success-text hover:bg-status-success-border'\n : 'bg-status-neutral-bg text-status-neutral-text hover:bg-status-neutral-border'\n }`}\n title={t('workflows.actions.toggleEnabled')}\n >\n {row.original.enabled ? t('common.yes') : t('common.no')}\n </button>\n ),\n },\n {\n id: 'tags',\n header: t('workflows.fields.tags'),\n cell: ({ row }) => {\n const tags = row.original.metadata?.tags || []\n if (tags.length === 0) return <span className=\"text-muted-foreground\">-</span>\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.slice(0, 2).map((tag, idx) => (\n <Badge key={idx} variant=\"secondary\">\n {tag}\n </Badge>\n ))}\n {tags.length > 2 && (\n <Badge variant=\"outline\">+{tags.length - 2}</Badge>\n )}\n </div>\n )\n },\n },\n {\n id: 'createdAt',\n header: t('workflows.fields.createdAt'),\n accessorKey: 'createdAt',\n cell: ({ row }) => {\n const date = new Date(row.original.createdAt)\n return <span className=\"text-sm text-muted-foreground\">{date.toLocaleDateString()}</span>\n },\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }) => {\n const isCodeOnly = row.original.source === 'code'\n const items = [\n {\n id: 'edit',\n label: isCodeOnly ? t('common.view') : t('common.edit'),\n href: `/backend/definitions/${row.original.id}`,\n },\n ...(!isCodeOnly ? [{\n id: 'edit-visual',\n label: t('workflows.actions.editVisually'),\n href: `/backend/definitions/visual-editor?id=${row.original.id}`,\n }] : []),\n ...(!isCodeOnly ? [{\n id: row.original.enabled ? 'disable' : 'enable',\n label: row.original.enabled ? t('common.disable') : t('common.enable'),\n onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'duplicate',\n label: t('common.duplicate'),\n onSelect: () => handleDuplicate(row.original),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'delete',\n label: t('common.delete'),\n onSelect: () => handleDelete(row.original.id, row.original.workflowName, row.original.updatedAt),\n destructive: true,\n }] : []),\n ]\n return <RowActions items={items} />\n },\n },\n ]\n\n if (error) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={t('workflows.messages.loadFailed')}\n description={error.message}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })}>\n {t('common.retry', 'Retry')}\n </Button>\n )}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('workflows.list.title')}\n actions={(\n <div className=\"flex items-center gap-2\">\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/definitions/visual-editor\">\n {t('workflows.actions.createVisual')}\n </Link>\n </Button>\n <Button asChild>\n <Link href=\"/backend/definitions/create\">\n {t('workflows.actions.create')}\n </Link>\n </Button>\n </div>\n )}\n columns={columns}\n data={data || []}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n onRowClick={(row) => router.push(`/backend/definitions/visual-editor?id=${row.id}`)}\n perspective={{\n tableId: 'workflows.definitions.list',\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('workflows.list.title')}\n createHref=\"/backend/definitions/create\"\n createLabel={t('workflows.actions.create')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n />\n <Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('workflows.confirm.deleteTitle')}</DialogTitle>\n <DialogDescription>\n {t('workflows.confirm.delete', { name: deleteTarget?.name ?? '' })}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDeleteTarget(null)}>\n {t('common.cancel')}\n </Button>\n <Button variant=\"destructive\" onClick={confirmDelete}>\n <Trash2/>\n {t('common.delete')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { Trash2 } from 'lucide-react'\n\ntype WorkflowDefinitionSource = 'code' | 'code_override' | 'user'\n\ntype WorkflowDefinition = {\n id: string\n workflowId: string\n workflowName: string\n description: string | null\n version: number\n definition: Record<string, unknown>\n enabled: boolean\n effectiveFrom: string | null\n effectiveTo: string | null\n metadata: {\n tags?: string[]\n category?: string\n icon?: string\n } | null\n tenantId: string\n organizationId: string\n createdAt: string\n updatedAt: string\n createdBy: string | null\n source?: WorkflowDefinitionSource\n isCodeBased?: boolean\n}\n\ntype DefinitionsResponse = {\n data: WorkflowDefinition[]\n pagination: {\n total: number\n limit: number\n offset: number\n hasMore: boolean\n }\n}\n\ntype CreateDefinitionResponse = {\n data?: {\n id?: string\n }\n error?: string\n}\n\nconst WORKFLOW_ID_MAX_LENGTH = 100\n\nfunction buildDuplicateWorkflowId(sourceWorkflowId: string, attempt: number): string {\n const suffix = attempt === 0 ? '_copy' : `_copy_${attempt + 1}`\n const maxBaseLength = Math.max(1, WORKFLOW_ID_MAX_LENGTH - suffix.length)\n const base = sourceWorkflowId.slice(0, maxBaseLength)\n return `${base}${suffix}`\n}\n\nexport default function WorkflowDefinitionsListPage() {\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const t = useT()\n const router = useRouter()\n const queryClient = useQueryClient()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; name: string; updatedAt: string | null } | null>(null)\n\n const { data, isLoading, error } = useQuery({\n queryKey: ['workflow-definitions', 'list', filterValues, page],\n queryFn: async () => {\n const params = new URLSearchParams()\n const offset = (page - 1) * pageSize\n params.set('limit', pageSize.toString())\n params.set('offset', offset.toString())\n\n if (filterValues.enabled !== undefined && filterValues.enabled !== '') {\n params.set('enabled', filterValues.enabled as string)\n }\n if (filterValues.workflowId) params.set('workflowId', filterValues.workflowId as string)\n if (filterValues.search) params.set('search', filterValues.search as string)\n\n const result = await apiCall<DefinitionsResponse>(\n `/api/workflows/definitions?${params.toString()}`\n )\n\n if (!result.ok) {\n throw new Error('Failed to fetch workflow definitions')\n }\n\n const response = result.result\n if (response?.pagination) {\n setTotal(response.pagination.total || 0)\n const calculatedPages = Math.ceil((response.pagination.total || 0) / pageSize)\n setTotalPages(calculatedPages || 1)\n }\n\n return response?.data || []\n },\n })\n\n const handleDelete = (id: string, workflowName: string, updatedAt: string | null) => {\n setDeleteTarget({ id, name: workflowName, updatedAt })\n }\n\n const confirmDelete = async () => {\n if (!deleteTarget) return\n\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(deleteTarget.updatedAt),\n () => apiCall(`/api/workflows/definitions/${deleteTarget.id}`, {\n method: 'DELETE',\n }),\n )\n\n if (result.ok) {\n flash(t('workflows.messages.deleted'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n const conflictError = Object.assign(new Error(t('workflows.messages.deleteFailed')), {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n })\n if (!surfaceRecordConflict(conflictError, t)) {\n flash(t('workflows.messages.deleteFailed'), 'error')\n }\n }\n setDeleteTarget(null)\n }\n\n const handleToggleEnabled = async (id: string, currentEnabled: boolean, updatedAt: string | null) => {\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () => apiCall(`/api/workflows/definitions/${id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n enabled: !currentEnabled,\n }),\n }),\n )\n\n if (result.ok) {\n flash(t('workflows.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n const conflictError = Object.assign(new Error(t('workflows.messages.updateFailed')), {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n })\n if (!surfaceRecordConflict(conflictError, t)) {\n flash(t('workflows.messages.updateFailed'), 'error')\n }\n }\n }\n\n const handleDuplicate = async (definition: WorkflowDefinition) => {\n for (let attempt = 0; attempt < 10; attempt += 1) {\n const duplicateWorkflowId = buildDuplicateWorkflowId(definition.workflowId, attempt)\n const result = await apiCall<CreateDefinitionResponse>('/api/workflows/definitions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n workflowId: duplicateWorkflowId,\n workflowName: definition.workflowName,\n description: definition.description,\n version: definition.version,\n definition: definition.definition,\n metadata: definition.metadata,\n enabled: definition.enabled,\n }),\n })\n\n if (result.ok) {\n flash(t('workflows.messages.workflowDuplicated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n return\n }\n\n if (result.status !== 409) {\n break\n }\n }\n\n flash(t('workflows.errors.createFailed'), 'error')\n }\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n const next: FilterValues = {}\n Object.entries(values).forEach(([key, value]) => {\n if (value !== undefined && value !== '') next[key] = value\n })\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const filters: FilterDef[] = [\n {\n id: 'search',\n type: 'text',\n label: t('workflows.filters.search'),\n placeholder: t('workflows.filters.searchPlaceholder'),\n },\n {\n id: 'enabled',\n type: 'select',\n label: t('workflows.filters.status'),\n options: [\n { label: t('common.all'), value: '' },\n { label: t('common.enabled'), value: 'true' },\n { label: t('common.disabled'), value: 'false' },\n ],\n },\n {\n id: 'workflowId',\n type: 'text',\n label: t('workflows.filters.workflowId'),\n placeholder: t('workflows.filters.workflowIdPlaceholder'),\n },\n ]\n\n const columns: ColumnDef<WorkflowDefinition>[] = [\n {\n id: 'workflowId',\n header: t('workflows.fields.workflowId'),\n accessorKey: 'workflowId',\n meta: { truncate: false },\n cell: ({ row }) => (\n <span className=\"font-mono text-sm\">{row.original.workflowId}</span>\n ),\n },\n {\n id: 'workflowName',\n header: t('workflows.fields.workflowName'),\n accessorKey: 'workflowName',\n meta: { truncate: false },\n cell: ({ row }) => (\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{row.original.workflowName}</span>\n {row.original.source === 'code' && (\n <Badge variant=\"secondary\">{t('workflows.source.code')}</Badge>\n )}\n {row.original.source === 'code_override' && (\n <Badge variant=\"outline\">{t('workflows.source.code_override')}</Badge>\n )}\n </div>\n {row.original.description && (\n <div className=\"text-xs text-muted-foreground\">\n {row.original.description}\n </div>\n )}\n {row.original.metadata?.category && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {row.original.metadata.category}\n </div>\n )}\n </div>\n ),\n },\n {\n id: 'version',\n header: t('workflows.fields.version'),\n accessorKey: 'version',\n meta: { truncate: false },\n cell: ({ row }) => (\n <Badge variant=\"secondary\" className=\"font-mono\">\n v{row.original.version}\n </Badge>\n ),\n },\n {\n id: 'enabled',\n header: t('workflows.fields.enabled'),\n accessorKey: 'enabled',\n cell: ({ row }) => (\n <button\n onClick={() => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt)}\n className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium cursor-pointer ${\n row.original.enabled\n ? 'bg-status-success-bg text-status-success-text hover:bg-status-success-border'\n : 'bg-status-neutral-bg text-status-neutral-text hover:bg-status-neutral-border'\n }`}\n title={t('workflows.actions.toggleEnabled')}\n >\n {row.original.enabled ? t('common.yes') : t('common.no')}\n </button>\n ),\n },\n {\n id: 'tags',\n header: t('workflows.fields.tags'),\n cell: ({ row }) => {\n const tags = row.original.metadata?.tags || []\n if (tags.length === 0) return <span className=\"text-muted-foreground\">-</span>\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.slice(0, 2).map((tag, idx) => (\n <Badge key={idx} variant=\"secondary\">\n {tag}\n </Badge>\n ))}\n {tags.length > 2 && (\n <Badge variant=\"outline\">+{tags.length - 2}</Badge>\n )}\n </div>\n )\n },\n },\n {\n id: 'createdAt',\n header: t('workflows.fields.createdAt'),\n accessorKey: 'createdAt',\n cell: ({ row }) => {\n const date = new Date(row.original.createdAt)\n return <span className=\"text-sm text-muted-foreground\">{date.toLocaleDateString()}</span>\n },\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }) => {\n const isCodeOnly = row.original.source === 'code'\n const items = [\n {\n id: 'edit',\n label: isCodeOnly ? t('common.view') : t('common.edit'),\n href: `/backend/definitions/${row.original.id}`,\n },\n ...(!isCodeOnly ? [{\n id: 'edit-visual',\n label: t('workflows.actions.editVisually'),\n href: `/backend/definitions/visual-editor?id=${row.original.id}`,\n }] : []),\n ...(!isCodeOnly ? [{\n id: row.original.enabled ? 'disable' : 'enable',\n label: row.original.enabled ? t('common.disable') : t('common.enable'),\n onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'duplicate',\n label: t('common.duplicate'),\n onSelect: () => handleDuplicate(row.original),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'delete',\n label: t('common.delete'),\n onSelect: () => handleDelete(row.original.id, row.original.workflowName, row.original.updatedAt),\n destructive: true,\n }] : []),\n ]\n return <RowActions items={items} />\n },\n },\n ]\n\n if (error) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={t('workflows.messages.loadFailed')}\n description={error.message}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })}>\n {t('common.retry', 'Retry')}\n </Button>\n )}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('workflows.list.title')}\n actions={(\n <div className=\"flex items-center gap-2\">\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/definitions/visual-editor\">\n {t('workflows.actions.createVisual')}\n </Link>\n </Button>\n <Button asChild>\n <Link href=\"/backend/definitions/create\">\n {t('workflows.actions.create')}\n </Link>\n </Button>\n </div>\n )}\n columns={columns}\n data={data || []}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n onRowClick={(row) => router.push(`/backend/definitions/visual-editor?id=${row.id}`)}\n perspective={{\n tableId: 'workflows.definitions.list',\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('workflows.list.title')}\n createHref=\"/backend/definitions/create\"\n createLabel={t('workflows.actions.create')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n />\n <Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('workflows.confirm.deleteTitle')}</DialogTitle>\n <DialogDescription>\n {t('workflows.confirm.delete', { name: deleteTarget?.name ?? '' })}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDeleteTarget(null)}>\n {t('common.cancel')}\n </Button>\n <Button variant=\"destructive\" onClick={confirmDelete}>\n <Trash2/>\n {t('common.delete')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
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.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6245.1.2be3b151f8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6245.1.2be3b151f8",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6245.1.2be3b151f8",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6245.1.2be3b151f8",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6245.1.2be3b151f8",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6245.1.2be3b151f8",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6245.1.2be3b151f8",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -205,6 +205,7 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
|
|
|
205
205
|
)
|
|
206
206
|
if (!organization) throw new CrudHttpError(400, { error: 'Organization not found' })
|
|
207
207
|
const tenantId = organization.tenant?.id ? String(organization.tenant.id) : null
|
|
208
|
+
assertTargetTenantInScope(resolveActorTenantScope(ctx), tenantId, 'Organization not found')
|
|
208
209
|
|
|
209
210
|
const emailHash = computeEmailHash(parsed.email)
|
|
210
211
|
// Email is unique per-tenant, not globally (see Migration20260610120000:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import type { EnricherContext, ResponseEnricher } from '@open-mercato/shared/lib/crud/response-enricher'
|
|
3
3
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { sanitizeChannelHtml } from '../lib/sanitize-channel-html'
|
|
4
5
|
import {
|
|
5
6
|
CommunicationChannel,
|
|
6
7
|
ExternalConversation,
|
|
@@ -63,6 +64,12 @@ export type ChannelPayloadEnrichment = {
|
|
|
63
64
|
channelPayload: Record<string, unknown> | null
|
|
64
65
|
interactiveState: Record<string, unknown> | null
|
|
65
66
|
channelMetadata: Record<string, unknown> | null
|
|
67
|
+
/**
|
|
68
|
+
* Server-sanitized email HTML, safe for `dangerouslySetInnerHTML`. Populated
|
|
69
|
+
* for `email/*` payloads that carry an html body, `null` otherwise. Sanitizing
|
|
70
|
+
* here keeps `sanitize-html` off the client render path.
|
|
71
|
+
*/
|
|
72
|
+
sanitizedHtml: string | null
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
export type ChannelContactEnrichment = {
|
|
@@ -71,6 +78,23 @@ export type ChannelContactEnrichment = {
|
|
|
71
78
|
subject: string | null
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Sanitize channel-supplied email HTML on the server so the client widget never
|
|
83
|
+
* has to pull `sanitize-html` into the browser bundle or run sanitization on the
|
|
84
|
+
* render path. Returns the sanitized HTML for `email/*` payloads that carry an
|
|
85
|
+
* html body, and `null` otherwise. The raw `channelPayload` is left untouched so
|
|
86
|
+
* provider-package widget overrides keep their existing data contract.
|
|
87
|
+
*/
|
|
88
|
+
function sanitizeEmailPayloadHtml(
|
|
89
|
+
contentType: string | null | undefined,
|
|
90
|
+
payload: Record<string, unknown> | null | undefined,
|
|
91
|
+
): string | null {
|
|
92
|
+
if (!contentType || !contentType.startsWith('email/')) return null
|
|
93
|
+
const html = payload?.html
|
|
94
|
+
if (typeof html !== 'string' || html.length === 0) return null
|
|
95
|
+
return sanitizeChannelHtml(html)
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
// ── _channel + _channelPayload + _channelContact ──────────────────────────────
|
|
75
99
|
|
|
76
100
|
const messageChannelEnricher: ResponseEnricher<
|
|
@@ -196,6 +220,7 @@ const messageChannelEnricher: ResponseEnricher<
|
|
|
196
220
|
channelPayload: link.channelPayload ?? null,
|
|
197
221
|
interactiveState: link.interactiveState ?? null,
|
|
198
222
|
channelMetadata: link.channelMetadata ?? null,
|
|
223
|
+
sanitizedHtml: sanitizeEmailPayloadHtml(link.channelContentType, link.channelPayload),
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
const conversation = link.externalConversationId
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
-
import { sanitizeChannelHtml } from '../../../lib/sanitize-channel-html'
|
|
7
6
|
|
|
8
7
|
type ChannelPayloadEnrichment = {
|
|
9
8
|
channelContentType: string | null
|
|
10
9
|
channelPayload: Record<string, unknown> | null
|
|
11
10
|
interactiveState: Record<string, unknown> | null
|
|
12
11
|
channelMetadata: Record<string, unknown> | null
|
|
12
|
+
// Email HTML is sanitized server-side by the channel-payload enricher and
|
|
13
|
+
// delivered here ready to render — the client never imports `sanitize-html`.
|
|
14
|
+
sanitizedHtml: string | null
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
type MessageWithPayload = Record<string, unknown> & {
|
|
@@ -24,11 +26,10 @@ export default function ChannelPayloadRendererWidget({
|
|
|
24
26
|
const payload = data?._channelPayload ?? null
|
|
25
27
|
if (!payload || !payload.channelContentType) return null
|
|
26
28
|
|
|
27
|
-
const { channelContentType, channelPayload } = payload
|
|
29
|
+
const { channelContentType, channelPayload, sanitizedHtml } = payload
|
|
28
30
|
|
|
29
|
-
// Email / HTML —
|
|
30
|
-
if (channelContentType.startsWith('email/') && typeof
|
|
31
|
-
const sanitized = sanitizeChannelHtml(channelPayload.html as string)
|
|
31
|
+
// Email / HTML — render the HTML the server already sanitized.
|
|
32
|
+
if (channelContentType.startsWith('email/') && typeof sanitizedHtml === 'string') {
|
|
32
33
|
return (
|
|
33
34
|
<section
|
|
34
35
|
className="rounded-md border bg-card p-4 text-sm"
|
|
@@ -37,7 +38,7 @@ export default function ChannelPayloadRendererWidget({
|
|
|
37
38
|
'Channel payload — email',
|
|
38
39
|
)}
|
|
39
40
|
>
|
|
40
|
-
<div dangerouslySetInnerHTML={{ __html:
|
|
41
|
+
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
|
|
41
42
|
</section>
|
|
42
43
|
)
|
|
43
44
|
}
|
package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts
CHANGED
|
@@ -6,8 +6,11 @@ import ChannelPayloadRendererWidget from './widget.client'
|
|
|
6
6
|
* (Slack Block Kit, WhatsApp interactive, email MIME, etc.) below the body in the
|
|
7
7
|
* messages detail page (`detail:messages:message:body:after`).
|
|
8
8
|
*
|
|
9
|
-
* For `email/*` content types, HTML is sanitized
|
|
10
|
-
*
|
|
9
|
+
* For `email/*` content types, HTML is sanitized server-side by the
|
|
10
|
+
* channel-payload enricher (`data/enrichers.ts`, via `sanitizeChannelHtml`) and
|
|
11
|
+
* delivered as `_channelPayload.sanitizedHtml`, which this widget renders with
|
|
12
|
+
* `dangerouslySetInnerHTML` — keeping `sanitize-html` off the client bundle.
|
|
13
|
+
* See SPEC-045d §4.6.
|
|
11
14
|
*
|
|
12
15
|
* Provider packages override this via UMES component replacement (handle
|
|
13
16
|
* `widget:communication_channels.injection.channel-payload-renderer`) to render
|
|
@@ -8,8 +8,14 @@ import { pipelineStageReorderSchema, type PipelineStageReorderInput } from '../.
|
|
|
8
8
|
import { withScopedPayload } from '../../utils'
|
|
9
9
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
|
+
import {
|
|
12
|
+
runCrudMutationGuardAfterSuccess,
|
|
13
|
+
validateCrudMutationGuard,
|
|
14
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
11
15
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
16
|
|
|
17
|
+
const PIPELINE_STAGE_RESOURCE_KIND = 'customers.pipelineStage'
|
|
18
|
+
|
|
13
19
|
export const metadata = {
|
|
14
20
|
POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
15
21
|
}
|
|
@@ -29,15 +35,49 @@ export async function POST(req: Request) {
|
|
|
29
35
|
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
30
36
|
request: req,
|
|
31
37
|
}
|
|
38
|
+
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
39
|
+
const tenantId = auth.tenantId ?? null
|
|
40
|
+
if (!organizationId || !tenantId) {
|
|
41
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
42
|
+
}
|
|
32
43
|
|
|
33
44
|
const body = await req.json().catch(() => ({}))
|
|
34
45
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
46
|
+
const input = pipelineStageReorderSchema.parse(scoped)
|
|
47
|
+
|
|
48
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
49
|
+
tenantId,
|
|
50
|
+
organizationId,
|
|
51
|
+
userId: auth.sub,
|
|
52
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
53
|
+
resourceId: organizationId,
|
|
54
|
+
operation: 'custom',
|
|
55
|
+
requestMethod: req.method,
|
|
56
|
+
requestHeaders: req.headers,
|
|
57
|
+
mutationPayload: input,
|
|
58
|
+
})
|
|
59
|
+
if (guardResult && !guardResult.ok) {
|
|
60
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
61
|
+
}
|
|
35
62
|
|
|
36
63
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
37
64
|
await commandBus.execute<PipelineStageReorderInput, void>(
|
|
38
65
|
'customers.pipeline-stages.reorder',
|
|
39
|
-
{ input
|
|
66
|
+
{ input, ctx },
|
|
40
67
|
)
|
|
68
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
69
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
70
|
+
tenantId,
|
|
71
|
+
organizationId,
|
|
72
|
+
userId: auth.sub,
|
|
73
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
74
|
+
resourceId: organizationId,
|
|
75
|
+
operation: 'custom',
|
|
76
|
+
requestMethod: req.method,
|
|
77
|
+
requestHeaders: req.headers,
|
|
78
|
+
metadata: guardResult.metadata ?? null,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
41
81
|
return NextResponse.json({ ok: true })
|
|
42
82
|
} catch (err) {
|
|
43
83
|
if (isCrudHttpError(err)) {
|
|
@@ -18,8 +18,14 @@ import { withScopedPayload } from '../utils'
|
|
|
18
18
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
19
19
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
20
20
|
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
21
|
+
import {
|
|
22
|
+
runCrudMutationGuardAfterSuccess,
|
|
23
|
+
validateCrudMutationGuard,
|
|
24
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
21
25
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
22
26
|
|
|
27
|
+
const PIPELINE_STAGE_RESOURCE_KIND = 'customers.pipelineStage'
|
|
28
|
+
|
|
23
29
|
export const metadata = {
|
|
24
30
|
GET: { requireAuth: true, requireFeatures: ['customers.pipelines.view'] },
|
|
25
31
|
POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
@@ -29,7 +35,7 @@ export const metadata = {
|
|
|
29
35
|
|
|
30
36
|
async function buildContext(
|
|
31
37
|
req: Request
|
|
32
|
-
): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null }> {
|
|
38
|
+
): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null; translate: (key: string, fallback?: string) => string }> {
|
|
33
39
|
const container = await createRequestContainer()
|
|
34
40
|
const auth = await getAuthFromRequest(req)
|
|
35
41
|
const { translate } = await resolveTranslations()
|
|
@@ -45,14 +51,14 @@ async function buildContext(
|
|
|
45
51
|
}
|
|
46
52
|
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
47
53
|
const tenantId = auth.tenantId ?? null
|
|
48
|
-
return { ctx, organizationId, tenantId }
|
|
54
|
+
return { ctx, organizationId, tenantId, translate }
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export async function GET(req: Request) {
|
|
52
58
|
try {
|
|
53
|
-
const { ctx, organizationId, tenantId } = await buildContext(req)
|
|
59
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
54
60
|
if (!organizationId || !tenantId) {
|
|
55
|
-
return NextResponse.json({ error: 'Organization and tenant context required' }, { status: 400 })
|
|
61
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
56
62
|
}
|
|
57
63
|
const url = new URL(req.url)
|
|
58
64
|
const pipelineId = url.searchParams.get('pipelineId')
|
|
@@ -102,16 +108,47 @@ export async function GET(req: Request) {
|
|
|
102
108
|
|
|
103
109
|
export async function POST(req: Request) {
|
|
104
110
|
try {
|
|
105
|
-
const { ctx } = await buildContext(req)
|
|
111
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
112
|
+
if (!organizationId || !tenantId) {
|
|
113
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
114
|
+
}
|
|
106
115
|
const body = await req.json().catch(() => ({}))
|
|
107
|
-
const { translate } = await resolveTranslations()
|
|
108
116
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
117
|
+
const input = pipelineStageCreateSchema.parse(scoped)
|
|
118
|
+
|
|
119
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
120
|
+
tenantId,
|
|
121
|
+
organizationId,
|
|
122
|
+
userId: ctx.auth!.sub,
|
|
123
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
124
|
+
resourceId: organizationId,
|
|
125
|
+
operation: 'create',
|
|
126
|
+
requestMethod: req.method,
|
|
127
|
+
requestHeaders: req.headers,
|
|
128
|
+
mutationPayload: input,
|
|
129
|
+
})
|
|
130
|
+
if (guardResult && !guardResult.ok) {
|
|
131
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
132
|
+
}
|
|
109
133
|
|
|
110
134
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
111
135
|
const { result, logEntry } = await commandBus.execute<PipelineStageCreateInput, { stageId: string }>(
|
|
112
136
|
'customers.pipeline-stages.create',
|
|
113
|
-
{ input
|
|
137
|
+
{ input, ctx },
|
|
114
138
|
)
|
|
139
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
140
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
141
|
+
tenantId,
|
|
142
|
+
organizationId,
|
|
143
|
+
userId: ctx.auth!.sub,
|
|
144
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
145
|
+
resourceId: result?.stageId ?? organizationId,
|
|
146
|
+
operation: 'create',
|
|
147
|
+
requestMethod: req.method,
|
|
148
|
+
requestHeaders: req.headers,
|
|
149
|
+
metadata: guardResult.metadata ?? null,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
115
152
|
const response = NextResponse.json({ id: result?.stageId ?? null }, { status: 201 })
|
|
116
153
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
117
154
|
response.headers.set(
|
|
@@ -139,16 +176,47 @@ export async function POST(req: Request) {
|
|
|
139
176
|
|
|
140
177
|
export async function PUT(req: Request) {
|
|
141
178
|
try {
|
|
142
|
-
const { ctx } = await buildContext(req)
|
|
179
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
180
|
+
if (!organizationId || !tenantId) {
|
|
181
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
182
|
+
}
|
|
143
183
|
const body = await req.json().catch(() => ({}))
|
|
144
|
-
const { translate } = await resolveTranslations()
|
|
145
184
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
185
|
+
const input = pipelineStageUpdateSchema.parse(scoped)
|
|
186
|
+
|
|
187
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
188
|
+
tenantId,
|
|
189
|
+
organizationId,
|
|
190
|
+
userId: ctx.auth!.sub,
|
|
191
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
192
|
+
resourceId: input.id,
|
|
193
|
+
operation: 'update',
|
|
194
|
+
requestMethod: req.method,
|
|
195
|
+
requestHeaders: req.headers,
|
|
196
|
+
mutationPayload: input,
|
|
197
|
+
})
|
|
198
|
+
if (guardResult && !guardResult.ok) {
|
|
199
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
200
|
+
}
|
|
146
201
|
|
|
147
202
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
148
203
|
const { logEntry } = await commandBus.execute<PipelineStageUpdateInput, void>(
|
|
149
204
|
'customers.pipeline-stages.update',
|
|
150
|
-
{ input
|
|
205
|
+
{ input, ctx },
|
|
151
206
|
)
|
|
207
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
208
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
209
|
+
tenantId,
|
|
210
|
+
organizationId,
|
|
211
|
+
userId: ctx.auth!.sub,
|
|
212
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
213
|
+
resourceId: input.id,
|
|
214
|
+
operation: 'update',
|
|
215
|
+
requestMethod: req.method,
|
|
216
|
+
requestHeaders: req.headers,
|
|
217
|
+
metadata: guardResult.metadata ?? null,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
152
220
|
const response = NextResponse.json({ ok: true })
|
|
153
221
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
154
222
|
response.headers.set(
|
|
@@ -176,16 +244,47 @@ export async function PUT(req: Request) {
|
|
|
176
244
|
|
|
177
245
|
export async function DELETE(req: Request) {
|
|
178
246
|
try {
|
|
179
|
-
const { ctx } = await buildContext(req)
|
|
247
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
248
|
+
if (!organizationId || !tenantId) {
|
|
249
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
250
|
+
}
|
|
180
251
|
const body = await req.json().catch(() => ({}))
|
|
181
|
-
const { translate } = await resolveTranslations()
|
|
182
252
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
253
|
+
const input = pipelineStageDeleteSchema.parse(scoped)
|
|
254
|
+
|
|
255
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
256
|
+
tenantId,
|
|
257
|
+
organizationId,
|
|
258
|
+
userId: ctx.auth!.sub,
|
|
259
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
260
|
+
resourceId: input.id,
|
|
261
|
+
operation: 'delete',
|
|
262
|
+
requestMethod: req.method,
|
|
263
|
+
requestHeaders: req.headers,
|
|
264
|
+
mutationPayload: input,
|
|
265
|
+
})
|
|
266
|
+
if (guardResult && !guardResult.ok) {
|
|
267
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
268
|
+
}
|
|
183
269
|
|
|
184
270
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
185
271
|
await commandBus.execute<PipelineStageDeleteInput, void>(
|
|
186
272
|
'customers.pipeline-stages.delete',
|
|
187
|
-
{ input
|
|
273
|
+
{ input, ctx },
|
|
188
274
|
)
|
|
275
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
276
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
277
|
+
tenantId,
|
|
278
|
+
organizationId,
|
|
279
|
+
userId: ctx.auth!.sub,
|
|
280
|
+
resourceKind: PIPELINE_STAGE_RESOURCE_KIND,
|
|
281
|
+
resourceId: input.id,
|
|
282
|
+
operation: 'delete',
|
|
283
|
+
requestMethod: req.method,
|
|
284
|
+
requestHeaders: req.headers,
|
|
285
|
+
metadata: guardResult.metadata ?? null,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
189
288
|
return NextResponse.json({ ok: true })
|
|
190
289
|
} catch (err) {
|
|
191
290
|
if (isCrudHttpError(err)) {
|