@open-mercato/ui 0.9.1-develop.7574.1.837288eec1 → 0.9.1-develop.7575.1.8480826a9b
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/ai/parts/MutationResultCard.js +1 -14
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/backend/CrudForm.js +52 -96
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +14 -40
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FlashMessages.js +1 -25
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +3 -4
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +0 -11
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +6 -17
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/filters/FilterFieldPicker.js +2 -3
- package/dist/backend/filters/FilterFieldPicker.js.map +2 -2
- package/dist/backend/forms/ActionsDropdown.js +2 -52
- package/dist/backend/forms/ActionsDropdown.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +5 -17
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +1 -4
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/inputs/index.js +0 -2
- package/dist/backend/inputs/index.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +10 -29
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/backend/notifications/NotificationDispatcher.js +0 -5
- package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
- package/dist/backend/notifications/useNotificationActions.js +2 -13
- package/dist/backend/notifications/useNotificationActions.js.map +2 -2
- package/dist/backend/utils/api.js +4 -25
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryPanel.js +2 -18
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/theme/QueryProvider.js +1 -7
- package/dist/theme/QueryProvider.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/parts/MutationResultCard.tsx +1 -30
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +0 -48
- package/src/backend/CrudForm.tsx +60 -121
- package/src/backend/DataTable.tsx +14 -54
- package/src/backend/FlashMessages.tsx +1 -34
- package/src/backend/__tests__/ActionsDropdown.test.tsx +0 -105
- package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +4 -85
- package/src/backend/__tests__/FlashMessages.test.tsx +0 -65
- package/src/backend/__tests__/VersionHistoryPanel.test.tsx +1 -81
- package/src/backend/columns/ColumnChooserPanel.tsx +3 -4
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +0 -16
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +0 -29
- package/src/backend/detail/InlineEditors.tsx +6 -28
- package/src/backend/detail/__tests__/InlineEditors.test.tsx +6 -121
- package/src/backend/filters/FilterFieldPicker.tsx +2 -3
- package/src/backend/forms/ActionsDropdown.tsx +2 -71
- package/src/backend/inputs/PhoneNumberField.tsx +11 -17
- package/src/backend/inputs/TagsInput.tsx +0 -4
- package/src/backend/inputs/__tests__/TagsInput.test.tsx +0 -41
- package/src/backend/inputs/index.ts +0 -2
- package/src/backend/messages/__tests__/useMessageCompose.test.tsx +1 -106
- package/src/backend/messages/message-composer.types.ts +0 -2
- package/src/backend/messages/useMessageCompose.ts +13 -34
- package/src/backend/notifications/NotificationDispatcher.ts +0 -5
- package/src/backend/notifications/__tests__/useNotificationActions.test.tsx +1 -86
- package/src/backend/notifications/useNotificationActions.ts +4 -19
- package/src/backend/utils/__tests__/api.test.ts +0 -67
- package/src/backend/utils/api.ts +4 -33
- package/src/backend/version-history/VersionHistoryPanel.tsx +2 -24
- package/src/theme/QueryProvider.tsx +1 -7
- package/dist/backend/crud/injectedFieldGroups.js +0 -50
- package/dist/backend/crud/injectedFieldGroups.js.map +0 -7
- package/dist/backend/inputs/LookupPickers.js +0 -149
- package/dist/backend/inputs/LookupPickers.js.map +0 -7
- package/dist/backend/inputs/lookupSources.js +0 -84
- package/dist/backend/inputs/lookupSources.js.map +0 -7
- package/dist/backend/utils/groupLabels.js +0 -16
- package/dist/backend/utils/groupLabels.js.map +0 -7
- package/src/backend/__tests__/CrudForm.disabledFields.test.tsx +0 -57
- package/src/backend/__tests__/CrudForm.embeddedExtraActions.test.tsx +0 -114
- package/src/backend/__tests__/CrudForm.fieldChangeRenders.test.tsx +0 -127
- package/src/backend/__tests__/DataTable.injectedColumnPlacement.test.tsx +0 -146
- package/src/backend/__tests__/next-dynamic-specifier.test.ts +0 -23
- package/src/backend/columns/__tests__/ColumnChooserPanel.groupLabels.test.tsx +0 -72
- package/src/backend/crud/__tests__/injectedFieldGroups.test.ts +0 -90
- package/src/backend/crud/injectedFieldGroups.ts +0 -75
- package/src/backend/filters/__tests__/FilterFieldPicker.groupLabels.test.tsx +0 -77
- package/src/backend/inputs/LookupPickers.tsx +0 -181
- package/src/backend/inputs/__tests__/LookupPickers.test.tsx +0 -120
- package/src/backend/inputs/__tests__/PhoneNumberField.renders.test.tsx +0 -35
- package/src/backend/inputs/__tests__/lookupSources.test.ts +0 -87
- package/src/backend/inputs/lookupSources.ts +0 -114
- package/src/backend/notifications/__tests__/NotificationDispatcher.navigate.test.ts +0 -76
- package/src/backend/utils/groupLabels.ts +0 -26
- package/src/theme/__tests__/QueryProvider.i18n.test.tsx +0 -40
package/.turbo/turbo-build.log
CHANGED
|
@@ -14,10 +14,7 @@ function isSuccessStatus(status) {
|
|
|
14
14
|
return status === "confirmed";
|
|
15
15
|
}
|
|
16
16
|
function isFailureStatus(status) {
|
|
17
|
-
return status === "failed" || status === "expired";
|
|
18
|
-
}
|
|
19
|
-
function isCancelledStatus(status) {
|
|
20
|
-
return status === "cancelled";
|
|
17
|
+
return status === "failed" || status === "cancelled" || status === "expired";
|
|
21
18
|
}
|
|
22
19
|
function MutationResultCard(props) {
|
|
23
20
|
const t = useT();
|
|
@@ -89,16 +86,6 @@ function MutationResultCard(props) {
|
|
|
89
86
|
] })
|
|
90
87
|
] });
|
|
91
88
|
}
|
|
92
|
-
if (isCancelledStatus(status)) {
|
|
93
|
-
const reason = result?.error?.message?.trim() || null;
|
|
94
|
-
return /* @__PURE__ */ jsxs(Alert, { status: "information", "data-ai-mutation-result": "cancelled", children: [
|
|
95
|
-
/* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.chat.mutation_cards.result.cancelledTitle", "Proposal cancelled") }),
|
|
96
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm leading-relaxed", children: /* @__PURE__ */ jsx("p", { children: reason ?? t(
|
|
97
|
-
"ai_assistant.chat.mutation_cards.result.cancelledBody",
|
|
98
|
-
"You cancelled this proposed change. No changes were made."
|
|
99
|
-
) }) })
|
|
100
|
-
] });
|
|
101
|
-
}
|
|
102
89
|
if (isFailureStatus(status) || result?.error) {
|
|
103
90
|
const code = result?.error?.code ?? status ?? "failed";
|
|
104
91
|
const message = result?.error?.message ?? t(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/ai/parts/MutationResultCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Wand2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'\n\n/** Custom DOM event the failure card dispatches when the operator clicks\n * \"Fix with AI\". `<AiChat>` listens for this and sends a follow-up user\n * message asking the agent to diagnose and retry the failed call. */\nexport const AI_CHAT_FIX_REQUEST_EVENT = 'om-ai-chat-fix-request'\n\nexport interface AiChatFixRequestDetail {\n message: string\n toolName?: string\n pendingActionId?: string\n}\n\nfunction dispatchFixRequest(detail: AiChatFixRequestDetail): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<AiChatFixRequestDetail>(AI_CHAT_FIX_REQUEST_EVENT, { detail }))\n}\n\n/**\n * Terminal-state card that renders the `executionResult` of a pending\n * action. Success \u2192 `Alert variant=\"success\"` with a record link; partial\n * success (batch `failedRecords[]`) \u2192 `variant=\"warning\"` with the list;\n * failure \u2192 `variant=\"destructive\"` with the error code + message.\n *\n * Reads the pending action via the shared polling hook so page reloads\n * still recover state. The hook short-circuits once the row is terminal\n * (spec's reconnect behavior), which is always the case for this card.\n */\nexport interface MutationResultCardPayload {\n /** Server-serialized pending action snapshot (optional \u2014 the hook refetches). */\n pendingAction?: AiPendingActionCardAction\n /** Optional link target for the success record. */\n recordHref?: string\n}\n\nexport interface MutationResultCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Poll endpoint override for tests. */\n endpoint?: string\n}\n\nfunction isSuccessStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'confirmed'\n}\n\nfunction isFailureStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'failed' || status === 'expired'\n}\n\nfunction isCancelledStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'cancelled'\n}\n\nexport function MutationResultCard(props: MutationResultCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationResultCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId || Boolean(injected),\n })\n const action = injected ?? polled\n const status = action?.status ?? null\n const failedRecords = action?.failedRecords ?? null\n const result = action?.executionResult ?? null\n\n if (!action) {\n return null\n }\n\n if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {\n return (\n <Alert status=\"warning\" data-ai-mutation-result=\"partial\">\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialTitle',\n 'Action applied with failures',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialBody',\n 'Some records could not be updated.',\n )}\n </p>\n <ul\n className=\"mt-2 list-disc space-y-1 pl-5 text-xs\"\n data-ai-mutation-failed-records\n >\n {failedRecords.map((record) => (\n <li key={record.recordId} data-ai-mutation-failed-record={record.recordId}>\n <span className=\"font-mono\">{record.recordId}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2022</span>\n <span className=\"font-mono\">{record.error.code}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2014</span>\n <span>{record.error.message}</span>\n </li>\n ))}\n </ul>\n </div>\n </Alert>\n )\n }\n\n if (isSuccessStatus(status)) {\n const recordId = result?.recordId ?? action.targetRecordId ?? null\n const href = payload.recordHref ?? null\n return (\n <Alert status=\"success\" data-ai-mutation-result=\"success\">\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The requested change was saved successfully.',\n )}\n </p>\n {recordId && href ? (\n <p className=\"mt-1 text-xs\">\n <a\n className=\"text-primary underline\"\n href={href}\n data-ai-mutation-result-link\n >\n {t(\n 'ai_assistant.chat.mutation_cards.result.viewRecord',\n 'View record',\n )}\n </a>\n </p>\n ) : null}\n </div>\n </Alert>\n )\n }\n\n // A deliberate cancellation is not a failure: no destructive styling, no\n // internal error code, and no \"Fix with AI\" (which would just re-propose\n // the change the operator just rejected). Show the operator's own\n // cancellation reason verbatim when they gave one, otherwise a neutral\n // localized default.\n if (isCancelledStatus(status)) {\n const reason = result?.error?.message?.trim() || null\n return (\n <Alert status=\"information\" data-ai-mutation-result=\"cancelled\">\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.cancelledTitle', 'Proposal cancelled')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {reason ??\n t(\n 'ai_assistant.chat.mutation_cards.result.cancelledBody',\n 'You cancelled this proposed change. No changes were made.',\n )}\n </p>\n </div>\n </Alert>\n )\n }\n\n // Render the failure alert when the action is in a failure status OR when\n // the dispatcher already captured an `executionResult.error` (the row may\n // not yet have transitioned out of `executing` in the polling snapshot,\n // but the handler error is authoritative \u2014 surface it immediately so the\n // operator never sees a stuck \"applying\u2026\" state silently masking a real\n // failure).\n if (isFailureStatus(status) || result?.error) {\n const code = result?.error?.code ?? status ?? 'failed'\n const message =\n result?.error?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.result.failureBody',\n 'The mutation could not be applied.',\n )\n const errorObj = result?.error\n const errorDetails = errorObj?.details\n const errorInput = errorObj?.input\n const errorName = errorObj?.name\n const onFixWithAi = () => {\n // Build a structured prompt that gives the agent enough context to\n // diagnose and retry without copy/paste from the operator. Keeping\n // it explicit (\"retry with corrected arguments\") nudges the model\n // away from re-issuing the same args (which would just hit the\n // same error). The repository's idempotency check only dedupes\n // active `pending` rows, so a fresh prepareMutation call after a\n // terminal failure always produces a new pending action \u2014 the\n // retry is never silently collapsed.\n //\n // The prompt now embeds the full structured failure context the\n // server captured (Zod issues / fieldErrors, original arguments,\n // failedRecords for batch tools, error name + cause). Without this\n // the operator routinely saw \"Invalid input\" with no field path \u2014\n // the model literally could not fix what it could not see.\n const promptLines: string[] = [\n `The previous call to tool \"${action.toolName}\" failed.`,\n `Error: ${code} \u2014 ${message}.`,\n ]\n if (errorName && errorName !== 'Error') {\n promptLines.push(`Error class: ${errorName}.`)\n }\n if (action.targetEntityType || action.targetRecordId) {\n promptLines.push(\n `Target: ${action.targetEntityType ?? '?'}${action.targetRecordId ? ' / ' + action.targetRecordId : ''}.`,\n )\n }\n\n // Field-level validation issues (Zod, custom). Render as a bulleted\n // list of `path: message` so the model can locate the offender by\n // schema path instead of guessing from a generic message.\n const issues = errorDetails?.issues ?? []\n if (Array.isArray(issues) && issues.length > 0) {\n promptLines.push('', 'Validation issues:')\n for (const issue of issues) {\n const path = Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : '(root)'\n const msg = issue?.message ?? '(no message)'\n const codeHint = issue?.code ? ` [${issue.code}]` : ''\n const expHint =\n issue?.expected || issue?.received\n ? ` (expected ${issue?.expected ?? '?'}, got ${issue?.received ?? '?'})`\n : ''\n promptLines.push(`- ${path}: ${msg}${codeHint}${expHint}`)\n }\n } else if (errorDetails?.fieldErrors && typeof errorDetails.fieldErrors === 'object') {\n const entries = Object.entries(errorDetails.fieldErrors)\n if (entries.length > 0) {\n promptLines.push('', 'Field errors:')\n for (const [path, msgs] of entries) {\n const list = Array.isArray(msgs) ? msgs.join('; ') : String(msgs)\n promptLines.push(`- ${path}: ${list}`)\n }\n }\n }\n\n // Echo the arguments the handler was invoked with so the model can\n // see exactly what it sent and change at least one parameter on\n // retry. JSON-stringified inline for compactness; non-serializable\n // values are dropped on the server side already.\n if (errorInput !== undefined) {\n try {\n const json = JSON.stringify(errorInput, null, 2)\n if (json && json !== '{}' && json.length <= 4000) {\n promptLines.push('', 'Arguments you sent:', '```json', json, '```')\n } else if (json && json.length > 4000) {\n promptLines.push(\n '',\n 'Arguments you sent (truncated):',\n '```json',\n json.slice(0, 4000) + '\\n\u2026 [truncated]',\n '```',\n )\n }\n } catch {\n // ignore\n }\n }\n\n // Surface root-cause when the handler nested another error inside.\n if (errorDetails?.cause !== undefined) {\n try {\n const causeJson = JSON.stringify(errorDetails.cause, null, 2)\n if (causeJson && causeJson !== '{}' && causeJson.length <= 1500) {\n promptLines.push('', 'Underlying cause:', '```json', causeJson, '```')\n }\n } catch {\n // ignore\n }\n }\n\n // Per-record failures from batch tools (Step 5.14). These usually\n // carry the most actionable information for partial-success cases.\n if (failedRecords && failedRecords.length > 0) {\n promptLines.push('', 'Records that failed:')\n for (const rec of failedRecords) {\n promptLines.push(\n `- ${rec.recordId} \u2192 ${rec.error.code}: ${rec.error.message}`,\n )\n }\n }\n\n promptLines.push(\n '',\n 'Diagnose what went wrong using the validation issues / cause / arguments above, correct the arguments, and call the tool again. If the failure indicates missing prerequisites (e.g. a deal needs a linked person/company before commenting), tell me what to fix on the platform side instead of retrying blindly. Do not repeat the exact same arguments \u2014 you must change at least one parameter or stop and explain.',\n )\n dispatchFixRequest({\n message: promptLines.join('\\n'),\n toolName: action.toolName,\n pendingActionId: action.id,\n })\n }\n const visibleIssues = Array.isArray(errorDetails?.issues)\n ? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))\n : []\n return (\n <Alert status=\"error\" data-ai-mutation-result=\"failure\">\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.failureTitle',\n 'Action failed',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <div>\n <span className=\"mr-2 font-mono text-xs\" data-ai-mutation-result-code>\n {code}\n </span>\n <span>{message}</span>\n </div>\n {visibleIssues.length > 0 ? (\n <ul\n className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs\"\n data-ai-mutation-result-issues\n >\n {visibleIssues.map((issue, index) => {\n const path =\n Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : null\n return (\n <li key={index}>\n {path ? (\n <span className=\"font-mono\">{path}</span>\n ) : null}\n {path && issue?.message ? <span className=\"mx-1\">\u2014</span> : null}\n {issue?.message ? <span>{issue.message}</span> : null}\n </li>\n )\n })}\n </ul>\n ) : null}\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onFixWithAi}\n data-ai-mutation-result-fix\n >\n <Wand2 className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.result.fixWithAi',\n 'Fix with AI',\n )}\n </span>\n </Button>\n </div>\n </div>\n </Alert>\n )\n }\n\n return null\n}\n\nexport default MutationResultCard\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Wand2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'\n\n/** Custom DOM event the failure card dispatches when the operator clicks\n * \"Fix with AI\". `<AiChat>` listens for this and sends a follow-up user\n * message asking the agent to diagnose and retry the failed call. */\nexport const AI_CHAT_FIX_REQUEST_EVENT = 'om-ai-chat-fix-request'\n\nexport interface AiChatFixRequestDetail {\n message: string\n toolName?: string\n pendingActionId?: string\n}\n\nfunction dispatchFixRequest(detail: AiChatFixRequestDetail): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<AiChatFixRequestDetail>(AI_CHAT_FIX_REQUEST_EVENT, { detail }))\n}\n\n/**\n * Terminal-state card that renders the `executionResult` of a pending\n * action. Success \u2192 `Alert variant=\"success\"` with a record link; partial\n * success (batch `failedRecords[]`) \u2192 `variant=\"warning\"` with the list;\n * failure \u2192 `variant=\"destructive\"` with the error code + message.\n *\n * Reads the pending action via the shared polling hook so page reloads\n * still recover state. The hook short-circuits once the row is terminal\n * (spec's reconnect behavior), which is always the case for this card.\n */\nexport interface MutationResultCardPayload {\n /** Server-serialized pending action snapshot (optional \u2014 the hook refetches). */\n pendingAction?: AiPendingActionCardAction\n /** Optional link target for the success record. */\n recordHref?: string\n}\n\nexport interface MutationResultCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Poll endpoint override for tests. */\n endpoint?: string\n}\n\nfunction isSuccessStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'confirmed'\n}\n\nfunction isFailureStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'failed' || status === 'cancelled' || status === 'expired'\n}\n\nexport function MutationResultCard(props: MutationResultCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationResultCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId || Boolean(injected),\n })\n const action = injected ?? polled\n const status = action?.status ?? null\n const failedRecords = action?.failedRecords ?? null\n const result = action?.executionResult ?? null\n\n if (!action) {\n return null\n }\n\n if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {\n return (\n <Alert status=\"warning\" data-ai-mutation-result=\"partial\">\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialTitle',\n 'Action applied with failures',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialBody',\n 'Some records could not be updated.',\n )}\n </p>\n <ul\n className=\"mt-2 list-disc space-y-1 pl-5 text-xs\"\n data-ai-mutation-failed-records\n >\n {failedRecords.map((record) => (\n <li key={record.recordId} data-ai-mutation-failed-record={record.recordId}>\n <span className=\"font-mono\">{record.recordId}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2022</span>\n <span className=\"font-mono\">{record.error.code}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2014</span>\n <span>{record.error.message}</span>\n </li>\n ))}\n </ul>\n </div>\n </Alert>\n )\n }\n\n if (isSuccessStatus(status)) {\n const recordId = result?.recordId ?? action.targetRecordId ?? null\n const href = payload.recordHref ?? null\n return (\n <Alert status=\"success\" data-ai-mutation-result=\"success\">\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The requested change was saved successfully.',\n )}\n </p>\n {recordId && href ? (\n <p className=\"mt-1 text-xs\">\n <a\n className=\"text-primary underline\"\n href={href}\n data-ai-mutation-result-link\n >\n {t(\n 'ai_assistant.chat.mutation_cards.result.viewRecord',\n 'View record',\n )}\n </a>\n </p>\n ) : null}\n </div>\n </Alert>\n )\n }\n\n // Render the failure alert when the action is in a failure status OR when\n // the dispatcher already captured an `executionResult.error` (the row may\n // not yet have transitioned out of `executing` in the polling snapshot,\n // but the handler error is authoritative \u2014 surface it immediately so the\n // operator never sees a stuck \"applying\u2026\" state silently masking a real\n // failure).\n if (isFailureStatus(status) || result?.error) {\n const code = result?.error?.code ?? status ?? 'failed'\n const message =\n result?.error?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.result.failureBody',\n 'The mutation could not be applied.',\n )\n const errorObj = result?.error\n const errorDetails = errorObj?.details\n const errorInput = errorObj?.input\n const errorName = errorObj?.name\n const onFixWithAi = () => {\n // Build a structured prompt that gives the agent enough context to\n // diagnose and retry without copy/paste from the operator. Keeping\n // it explicit (\"retry with corrected arguments\") nudges the model\n // away from re-issuing the same args (which would just hit the\n // same error). The repository's idempotency check only dedupes\n // active `pending` rows, so a fresh prepareMutation call after a\n // terminal failure always produces a new pending action \u2014 the\n // retry is never silently collapsed.\n //\n // The prompt now embeds the full structured failure context the\n // server captured (Zod issues / fieldErrors, original arguments,\n // failedRecords for batch tools, error name + cause). Without this\n // the operator routinely saw \"Invalid input\" with no field path \u2014\n // the model literally could not fix what it could not see.\n const promptLines: string[] = [\n `The previous call to tool \"${action.toolName}\" failed.`,\n `Error: ${code} \u2014 ${message}.`,\n ]\n if (errorName && errorName !== 'Error') {\n promptLines.push(`Error class: ${errorName}.`)\n }\n if (action.targetEntityType || action.targetRecordId) {\n promptLines.push(\n `Target: ${action.targetEntityType ?? '?'}${action.targetRecordId ? ' / ' + action.targetRecordId : ''}.`,\n )\n }\n\n // Field-level validation issues (Zod, custom). Render as a bulleted\n // list of `path: message` so the model can locate the offender by\n // schema path instead of guessing from a generic message.\n const issues = errorDetails?.issues ?? []\n if (Array.isArray(issues) && issues.length > 0) {\n promptLines.push('', 'Validation issues:')\n for (const issue of issues) {\n const path = Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : '(root)'\n const msg = issue?.message ?? '(no message)'\n const codeHint = issue?.code ? ` [${issue.code}]` : ''\n const expHint =\n issue?.expected || issue?.received\n ? ` (expected ${issue?.expected ?? '?'}, got ${issue?.received ?? '?'})`\n : ''\n promptLines.push(`- ${path}: ${msg}${codeHint}${expHint}`)\n }\n } else if (errorDetails?.fieldErrors && typeof errorDetails.fieldErrors === 'object') {\n const entries = Object.entries(errorDetails.fieldErrors)\n if (entries.length > 0) {\n promptLines.push('', 'Field errors:')\n for (const [path, msgs] of entries) {\n const list = Array.isArray(msgs) ? msgs.join('; ') : String(msgs)\n promptLines.push(`- ${path}: ${list}`)\n }\n }\n }\n\n // Echo the arguments the handler was invoked with so the model can\n // see exactly what it sent and change at least one parameter on\n // retry. JSON-stringified inline for compactness; non-serializable\n // values are dropped on the server side already.\n if (errorInput !== undefined) {\n try {\n const json = JSON.stringify(errorInput, null, 2)\n if (json && json !== '{}' && json.length <= 4000) {\n promptLines.push('', 'Arguments you sent:', '```json', json, '```')\n } else if (json && json.length > 4000) {\n promptLines.push(\n '',\n 'Arguments you sent (truncated):',\n '```json',\n json.slice(0, 4000) + '\\n\u2026 [truncated]',\n '```',\n )\n }\n } catch {\n // ignore\n }\n }\n\n // Surface root-cause when the handler nested another error inside.\n if (errorDetails?.cause !== undefined) {\n try {\n const causeJson = JSON.stringify(errorDetails.cause, null, 2)\n if (causeJson && causeJson !== '{}' && causeJson.length <= 1500) {\n promptLines.push('', 'Underlying cause:', '```json', causeJson, '```')\n }\n } catch {\n // ignore\n }\n }\n\n // Per-record failures from batch tools (Step 5.14). These usually\n // carry the most actionable information for partial-success cases.\n if (failedRecords && failedRecords.length > 0) {\n promptLines.push('', 'Records that failed:')\n for (const rec of failedRecords) {\n promptLines.push(\n `- ${rec.recordId} \u2192 ${rec.error.code}: ${rec.error.message}`,\n )\n }\n }\n\n promptLines.push(\n '',\n 'Diagnose what went wrong using the validation issues / cause / arguments above, correct the arguments, and call the tool again. If the failure indicates missing prerequisites (e.g. a deal needs a linked person/company before commenting), tell me what to fix on the platform side instead of retrying blindly. Do not repeat the exact same arguments \u2014 you must change at least one parameter or stop and explain.',\n )\n dispatchFixRequest({\n message: promptLines.join('\\n'),\n toolName: action.toolName,\n pendingActionId: action.id,\n })\n }\n const visibleIssues = Array.isArray(errorDetails?.issues)\n ? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))\n : []\n return (\n <Alert status=\"error\" data-ai-mutation-result=\"failure\">\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.failureTitle',\n 'Action failed',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <div>\n <span className=\"mr-2 font-mono text-xs\" data-ai-mutation-result-code>\n {code}\n </span>\n <span>{message}</span>\n </div>\n {visibleIssues.length > 0 ? (\n <ul\n className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs\"\n data-ai-mutation-result-issues\n >\n {visibleIssues.map((issue, index) => {\n const path =\n Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : null\n return (\n <li key={index}>\n {path ? (\n <span className=\"font-mono\">{path}</span>\n ) : null}\n {path && issue?.message ? <span className=\"mx-1\">\u2014</span> : null}\n {issue?.message ? <span>{issue.message}</span> : null}\n </li>\n )\n })}\n </ul>\n ) : null}\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onFixWithAi}\n data-ai-mutation-result-fix\n >\n <Wand2 className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.result.fixWithAi',\n 'Fix with AI',\n )}\n </span>\n </Button>\n </div>\n </div>\n </Alert>\n )\n }\n\n return null\n}\n\nexport default MutationResultCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/backend/CrudForm.js
CHANGED
|
@@ -98,7 +98,7 @@ import { CollapsibleGroup } from "./crud/CollapsibleGroup.js";
|
|
|
98
98
|
import { SortableGroupHandleProvider } from "./crud/SortableGroupHandle.js";
|
|
99
99
|
import { useGroupOrder } from "./crud/useGroupOrder.js";
|
|
100
100
|
import { InjectedField } from "./injection/InjectedField.js";
|
|
101
|
-
import {
|
|
101
|
+
import { insertByInjectionPlacement } from "@open-mercato/shared/modules/widgets/injection-position";
|
|
102
102
|
import { evaluateInjectedVisibility } from "./injection/visibility-utils.js";
|
|
103
103
|
import { ComponentReplacementHandles } from "@open-mercato/shared/modules/widgets/component-registry";
|
|
104
104
|
import { crudFormExtensionSpotId, extensionSpotChildId } from "@open-mercato/shared/modules/widgets/extension-points";
|
|
@@ -173,21 +173,6 @@ function buildResolvedEntityIdsKey(entityId, entityIds) {
|
|
|
173
173
|
}
|
|
174
174
|
return list.join("\0");
|
|
175
175
|
}
|
|
176
|
-
function renderNoInjectedFieldWidget() {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
async function skipInjectedFieldDeleteEvent() {
|
|
180
|
-
}
|
|
181
|
-
const INJECTED_FIELD_DELETE_EVENT_DEFAULTS = {
|
|
182
|
-
onBeforeDelete: skipInjectedFieldDeleteEvent,
|
|
183
|
-
onDelete: skipInjectedFieldDeleteEvent,
|
|
184
|
-
onAfterDelete: skipInjectedFieldDeleteEvent
|
|
185
|
-
};
|
|
186
|
-
function injectionWidgetPriority(widget) {
|
|
187
|
-
const placementPriority = widget.placement?.priority;
|
|
188
|
-
if (typeof placementPriority === "number") return placementPriority;
|
|
189
|
-
return typeof widget.module.metadata.priority === "number" ? widget.module.metadata.priority : 0;
|
|
190
|
-
}
|
|
191
176
|
function mergeByKey(primary, legacy, keyOf) {
|
|
192
177
|
if (!legacy.length) return primary;
|
|
193
178
|
const seen = new Set(primary.map(keyOf));
|
|
@@ -568,13 +553,7 @@ function CrudForm({
|
|
|
568
553
|
const navigationConfirmPendingRef = React.useRef(false);
|
|
569
554
|
const submitNavigationBypassRef = React.useRef(false);
|
|
570
555
|
const submitNavigationBypassTimeoutRef = React.useRef(null);
|
|
571
|
-
const [hasUnsavedChanges,
|
|
572
|
-
const hasUnsavedChangesRef = React.useRef(false);
|
|
573
|
-
const setHasUnsavedChanges = React.useCallback((next) => {
|
|
574
|
-
if (hasUnsavedChangesRef.current === next) return;
|
|
575
|
-
hasUnsavedChangesRef.current = next;
|
|
576
|
-
setHasUnsavedChangesState(next);
|
|
577
|
-
}, []);
|
|
556
|
+
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
|
|
578
557
|
const popStateRollbackRef = React.useRef(false);
|
|
579
558
|
const shouldBypassUnsavedChangesGuardRef = React.useRef(shouldBypassUnsavedChangesGuard);
|
|
580
559
|
React.useEffect(() => {
|
|
@@ -596,7 +575,7 @@ function CrudForm({
|
|
|
596
575
|
const dirty = currentSnapshot !== snapshot;
|
|
597
576
|
isDirtyRef.current = dirty;
|
|
598
577
|
setHasUnsavedChanges(dirty);
|
|
599
|
-
}, [embedded, trackDirtyWhenEmbedded, values
|
|
578
|
+
}, [embedded, trackDirtyWhenEmbedded, values]);
|
|
600
579
|
React.useEffect(() => {
|
|
601
580
|
onDirtyChange?.(hasUnsavedChanges);
|
|
602
581
|
}, [hasUnsavedChanges, onDirtyChange]);
|
|
@@ -640,7 +619,7 @@ function CrudForm({
|
|
|
640
619
|
everEditedFieldIdsRef.current.clear();
|
|
641
620
|
isDirtyRef.current = false;
|
|
642
621
|
setHasUnsavedChanges(false);
|
|
643
|
-
}, [
|
|
622
|
+
}, []);
|
|
644
623
|
const confirmUnsavedChanges = React.useCallback(async () => {
|
|
645
624
|
if (!isDirtyRef.current || typeof window === "undefined") return true;
|
|
646
625
|
if (navigationPromptBypassRef.current || submitNavigationBypassRef.current) return true;
|
|
@@ -746,7 +725,7 @@ function CrudForm({
|
|
|
746
725
|
window.history.pushState = originalPushState;
|
|
747
726
|
window.history.replaceState = originalReplaceState;
|
|
748
727
|
};
|
|
749
|
-
}, [allowNextNavigation, clearDirtyState, confirmUnsavedChanges, embedded, hasUnsavedChanges, router, trackDirtyWhenEmbedded
|
|
728
|
+
}, [allowNextNavigation, clearDirtyState, confirmUnsavedChanges, embedded, hasUnsavedChanges, router, trackDirtyWhenEmbedded]);
|
|
750
729
|
const { widgets: primaryInjectionWidgets } = useInjectionWidgets(resolvedInjectionSpotId, {
|
|
751
730
|
context: injectionContext,
|
|
752
731
|
triggerOnLoad: true
|
|
@@ -769,26 +748,7 @@ function CrudForm({
|
|
|
769
748
|
() => mergeByKey(primaryFieldWidgets, legacyFieldWidgets, (w) => w.metadata.id),
|
|
770
749
|
[primaryFieldWidgets, legacyFieldWidgets]
|
|
771
750
|
);
|
|
772
|
-
const
|
|
773
|
-
const fieldEventWidgets = [];
|
|
774
|
-
for (const widget of injectedFieldWidgets) {
|
|
775
|
-
if (!("fields" in widget) || !widget.eventHandlers) continue;
|
|
776
|
-
fieldEventWidgets.push({
|
|
777
|
-
widgetId: widget.metadata.id,
|
|
778
|
-
module: {
|
|
779
|
-
metadata: widget.metadata,
|
|
780
|
-
eventHandlers: { ...INJECTED_FIELD_DELETE_EVENT_DEFAULTS, ...widget.eventHandlers },
|
|
781
|
-
Widget: renderNoInjectedFieldWidget
|
|
782
|
-
},
|
|
783
|
-
moduleId: widget.moduleId,
|
|
784
|
-
key: widget.key,
|
|
785
|
-
placement: widget.placement
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
if (!fieldEventWidgets.length) return injectionWidgets;
|
|
789
|
-
return mergeByKey(injectionWidgets, fieldEventWidgets, (widget) => widget.widgetId).map((widget, index) => ({ widget, index })).sort((left, right) => injectionWidgetPriority(right.widget) - injectionWidgetPriority(left.widget) || left.index - right.index).map((entry) => entry.widget);
|
|
790
|
-
}, [injectionWidgets, injectedFieldWidgets]);
|
|
791
|
-
const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? "", injectionEventWidgets);
|
|
751
|
+
const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? "", injectionWidgets);
|
|
792
752
|
const extendedInjectionEventsEnabled = CRUDFORM_EXTENDED_EVENTS_ENABLED && Boolean(resolvedInjectionSpotId);
|
|
793
753
|
const widgetRequiredFieldIds = React.useMemo(() => {
|
|
794
754
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -1155,13 +1115,6 @@ function CrudForm({
|
|
|
1155
1115
|
headerInjectionAction,
|
|
1156
1116
|
extraActions
|
|
1157
1117
|
] }) : void 0;
|
|
1158
|
-
const renderFooterActions = !hideFooterActions && !formReadOnly;
|
|
1159
|
-
const embeddedExtraActions = renderFooterActions ? void 0 : extraActions;
|
|
1160
|
-
const embeddedHeaderActions = versionHistoryEnabled || headerInjectionAction || embeddedExtraActions ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1161
|
-
versionHistoryEnabled ? versionHistoryAction : null,
|
|
1162
|
-
headerInjectionAction,
|
|
1163
|
-
embeddedExtraActions
|
|
1164
|
-
] }) : void 0;
|
|
1165
1118
|
React.useEffect(() => {
|
|
1166
1119
|
let cancelled = false;
|
|
1167
1120
|
async function load() {
|
|
@@ -1394,24 +1347,6 @@ function CrudForm({
|
|
|
1394
1347
|
}
|
|
1395
1348
|
return definitions;
|
|
1396
1349
|
}, [injectedFieldWidgets]);
|
|
1397
|
-
const injectedFieldPlacements = React.useMemo(() => {
|
|
1398
|
-
const placements = /* @__PURE__ */ new Map();
|
|
1399
|
-
for (const widget of injectedFieldWidgets) {
|
|
1400
|
-
if (!("fields" in widget)) continue;
|
|
1401
|
-
for (const field of widget.fields ?? []) {
|
|
1402
|
-
placements.set(field.id, widget.placement);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
return placements;
|
|
1406
|
-
}, [injectedFieldWidgets]);
|
|
1407
|
-
const groupsWithInjectedFields = React.useMemo(() => {
|
|
1408
|
-
if (!groups || groups.length === 0 || injectedFieldDefinitions.length === 0) return groups;
|
|
1409
|
-
return placeInjectedFieldsInGroups(groups, injectedFieldDefinitions, injectedFieldPlacements, (definition) => {
|
|
1410
|
-
if (process.env.NODE_ENV !== "production") {
|
|
1411
|
-
logger.warn("Injected field targets a group that does not exist; appended to the last plain-field group", { fieldId: definition.id, group: definition.group });
|
|
1412
|
-
}
|
|
1413
|
-
});
|
|
1414
|
-
}, [groups, injectedFieldDefinitions, injectedFieldPlacements]);
|
|
1415
1350
|
const injectedFieldContext = React.useMemo(() => {
|
|
1416
1351
|
const recordValues = values;
|
|
1417
1352
|
const organizationId = typeof recordValues.organizationId === "string" ? recordValues.organizationId : null;
|
|
@@ -1458,15 +1393,22 @@ function CrudForm({
|
|
|
1458
1393
|
}
|
|
1459
1394
|
return (group.fields ?? []).map((entry) => typeof entry === "string" ? entry : entry.id);
|
|
1460
1395
|
};
|
|
1396
|
+
const declaredGroups = groups ?? [];
|
|
1461
1397
|
const hidden = /* @__PURE__ */ new Set();
|
|
1462
1398
|
const visible = /* @__PURE__ */ new Set();
|
|
1463
|
-
for (const group of
|
|
1399
|
+
for (const group of declaredGroups) {
|
|
1464
1400
|
const target = hiddenGroupIdSet.has(group.id) ? hidden : visible;
|
|
1465
1401
|
for (const fieldId of groupFieldIds(group)) target.add(fieldId);
|
|
1466
1402
|
}
|
|
1403
|
+
const declaredGroupIds = new Set(declaredGroups.map((group) => group.id));
|
|
1404
|
+
const fallbackGroupId = declaredGroups[declaredGroups.length - 1]?.id;
|
|
1405
|
+
for (const definition of injectedFieldDefinitions) {
|
|
1406
|
+
const targetGroupId = definition.group && declaredGroupIds.has(definition.group) ? definition.group : fallbackGroupId;
|
|
1407
|
+
if (targetGroupId && hiddenGroupIdSet.has(targetGroupId)) hidden.add(definition.id);
|
|
1408
|
+
}
|
|
1467
1409
|
for (const fieldId of visible) hidden.delete(fieldId);
|
|
1468
1410
|
return hidden;
|
|
1469
|
-
}, [cfFields,
|
|
1411
|
+
}, [cfFields, groups, hiddenGroupIdSet, injectedFieldDefinitions, placedCustomFieldIds]);
|
|
1470
1412
|
const hiddenInjectedFieldIds = React.useMemo(() => {
|
|
1471
1413
|
const hidden = /* @__PURE__ */ new Set();
|
|
1472
1414
|
for (const definition of injectedFieldDefinitions) {
|
|
@@ -1673,6 +1615,31 @@ function CrudForm({
|
|
|
1673
1615
|
pairs.sort((a, b) => b.priority - a.priority);
|
|
1674
1616
|
return pairs.map((p) => p.group);
|
|
1675
1617
|
}, [injectionWidgets, injectionContext, pending, setValues, values]);
|
|
1618
|
+
const groupsWithInjectedFields = React.useMemo(() => {
|
|
1619
|
+
if (!groups || groups.length === 0 || injectedFieldDefinitions.length === 0) return groups;
|
|
1620
|
+
const cloned = groups.map((group) => ({ ...group, fields: [...group.fields ?? []] }));
|
|
1621
|
+
const fallbackIndex = cloned.length - 1;
|
|
1622
|
+
for (const definition of injectedFieldDefinitions) {
|
|
1623
|
+
const targetIndex = cloned.findIndex((group) => group.id === definition.group);
|
|
1624
|
+
const index = targetIndex >= 0 ? targetIndex : fallbackIndex;
|
|
1625
|
+
if (targetIndex < 0 && process.env.NODE_ENV !== "production") {
|
|
1626
|
+
logger.warn("Injected field targets a group that does not exist; appended to last group", { fieldId: definition.id, group: definition.group });
|
|
1627
|
+
}
|
|
1628
|
+
if (index < 0) continue;
|
|
1629
|
+
const fieldEntries = cloned[index].fields ?? [];
|
|
1630
|
+
const alreadyPresent = fieldEntries.some((entry) => {
|
|
1631
|
+
const entryId = typeof entry === "string" ? entry : entry.id;
|
|
1632
|
+
return entryId === definition.id;
|
|
1633
|
+
});
|
|
1634
|
+
cloned[index].fields = alreadyPresent ? fieldEntries : insertByInjectionPlacement(
|
|
1635
|
+
fieldEntries,
|
|
1636
|
+
definition.id,
|
|
1637
|
+
definition.placement,
|
|
1638
|
+
(entry) => typeof entry === "string" ? entry : entry.id
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
return cloned;
|
|
1642
|
+
}, [groups, injectedFieldDefinitions]);
|
|
1676
1643
|
const shouldAutoGroup = (!groupsWithInjectedFields || groupsWithInjectedFields.length === 0) && injectionGroupCards.length > 0;
|
|
1677
1644
|
const declaredGroupsForLayout = React.useMemo(() => {
|
|
1678
1645
|
const baseGroups = groupsWithInjectedFields && groupsWithInjectedFields.length ? groupsWithInjectedFields : [];
|
|
@@ -1997,18 +1964,14 @@ function CrudForm({
|
|
|
1997
1964
|
if (activeDispatch?.eventId === dispatchToken.eventId && activeDispatch.formGeneration === dispatchToken.formGeneration) {
|
|
1998
1965
|
activeFieldChangeDispatchRef.current = null;
|
|
1999
1966
|
}
|
|
2000
|
-
if (fieldChangeDispatcherMountedRef.current && fieldChangeFormGenerationRef.current === dispatchToken.formGeneration
|
|
1967
|
+
if (fieldChangeDispatcherMountedRef.current && fieldChangeFormGenerationRef.current === dispatchToken.formGeneration) {
|
|
2001
1968
|
setFieldChangeDispatchVersion((version) => version + 1);
|
|
2002
1969
|
}
|
|
2003
1970
|
});
|
|
2004
1971
|
}, [extendedInjectionEventsEnabled, fieldChangeDispatchVersion, triggerInjectionEvent, values]);
|
|
2005
|
-
const validateFieldOnBlurRef = React.useRef(validateFieldOnBlur);
|
|
2006
|
-
React.useLayoutEffect(() => {
|
|
2007
|
-
validateFieldOnBlurRef.current = validateFieldOnBlur;
|
|
2008
|
-
}, [validateFieldOnBlur]);
|
|
2009
1972
|
const onBlurRequest = React.useCallback((fieldId) => {
|
|
2010
|
-
void
|
|
2011
|
-
}, []);
|
|
1973
|
+
void validateFieldOnBlur(fieldId);
|
|
1974
|
+
}, [validateFieldOnBlur]);
|
|
2012
1975
|
const handleFieldsetSelectionChange = React.useCallback(
|
|
2013
1976
|
(entityId2, nextCode) => {
|
|
2014
1977
|
if (!nextCode) return;
|
|
@@ -2990,7 +2953,7 @@ function CrudForm({
|
|
|
2990
2953
|
submit: formReadOnly ? void 0 : { formId, pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
2991
2954
|
}
|
|
2992
2955
|
}
|
|
2993
|
-
) :
|
|
2956
|
+
) : headerExtraActions ? /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mb-2", children: headerExtraActions }) : null,
|
|
2994
2957
|
contentHeader,
|
|
2995
2958
|
/* @__PURE__ */ jsx(
|
|
2996
2959
|
DataLoader,
|
|
@@ -3023,7 +2986,7 @@ function CrudForm({
|
|
|
3023
2986
|
}
|
|
3024
2987
|
),
|
|
3025
2988
|
formError && !Object.keys(errors).length ? /* @__PURE__ */ jsx("div", { className: "text-sm text-status-error-text", children: formError }) : null,
|
|
3026
|
-
|
|
2989
|
+
hideFooterActions || formReadOnly ? null : /* @__PURE__ */ jsx(
|
|
3027
2990
|
FormFooter,
|
|
3028
2991
|
{
|
|
3029
2992
|
embedded,
|
|
@@ -3039,7 +3002,7 @@ function CrudForm({
|
|
|
3039
3002
|
submit: { pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
3040
3003
|
}
|
|
3041
3004
|
}
|
|
3042
|
-
)
|
|
3005
|
+
)
|
|
3043
3006
|
] })
|
|
3044
3007
|
)
|
|
3045
3008
|
}
|
|
@@ -3068,7 +3031,7 @@ function CrudForm({
|
|
|
3068
3031
|
submit: formReadOnly ? void 0 : { formId, pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
3069
3032
|
}
|
|
3070
3033
|
}
|
|
3071
|
-
) :
|
|
3034
|
+
) : headerExtraActions ? /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mb-2", children: headerExtraActions }) : null,
|
|
3072
3035
|
contentHeader,
|
|
3073
3036
|
/* @__PURE__ */ jsx(
|
|
3074
3037
|
DataLoader,
|
|
@@ -3122,7 +3085,7 @@ function CrudForm({
|
|
|
3122
3085
|
);
|
|
3123
3086
|
}) }),
|
|
3124
3087
|
formError && !Object.keys(errors).length ? /* @__PURE__ */ jsx("div", { className: "text-sm text-status-error-text", children: formError }) : null,
|
|
3125
|
-
|
|
3088
|
+
hideFooterActions || formReadOnly ? null : /* @__PURE__ */ jsx(
|
|
3126
3089
|
FormFooter,
|
|
3127
3090
|
{
|
|
3128
3091
|
embedded,
|
|
@@ -3138,7 +3101,7 @@ function CrudForm({
|
|
|
3138
3101
|
submit: { pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
3139
3102
|
}
|
|
3140
3103
|
}
|
|
3141
|
-
)
|
|
3104
|
+
)
|
|
3142
3105
|
]
|
|
3143
3106
|
}
|
|
3144
3107
|
) })
|
|
@@ -3293,9 +3256,7 @@ function NumberInput({
|
|
|
3293
3256
|
onChange,
|
|
3294
3257
|
placeholder,
|
|
3295
3258
|
autoFocus,
|
|
3296
|
-
onSubmit
|
|
3297
|
-
disabled,
|
|
3298
|
-
readOnly
|
|
3259
|
+
onSubmit
|
|
3299
3260
|
}) {
|
|
3300
3261
|
const locale = useOptionalLocale();
|
|
3301
3262
|
const serializedValue = value !== void 0 && value !== null ? String(value) : "";
|
|
@@ -3363,8 +3324,6 @@ function NumberInput({
|
|
|
3363
3324
|
onFocus: handleFocus,
|
|
3364
3325
|
onBlur: handleBlur,
|
|
3365
3326
|
autoFocus,
|
|
3366
|
-
disabled,
|
|
3367
|
-
readOnly,
|
|
3368
3327
|
"data-crud-focus-target": ""
|
|
3369
3328
|
}
|
|
3370
3329
|
)
|
|
@@ -3690,9 +3649,7 @@ const FieldControl = React.memo(
|
|
|
3690
3649
|
placeholder,
|
|
3691
3650
|
onChange: fieldSetValue,
|
|
3692
3651
|
autoFocus: autoFocusField,
|
|
3693
|
-
onSubmit: onSubmitRequest
|
|
3694
|
-
disabled,
|
|
3695
|
-
readOnly
|
|
3652
|
+
onSubmit: onSubmitRequest
|
|
3696
3653
|
}
|
|
3697
3654
|
),
|
|
3698
3655
|
field.type === "date" && /* @__PURE__ */ jsx(
|
|
@@ -3792,8 +3749,7 @@ const FieldControl = React.memo(
|
|
|
3792
3749
|
autoFocus: autoFocusField,
|
|
3793
3750
|
suppressInitialSuggestionsOnFocus: autoFocusField,
|
|
3794
3751
|
suggestions: options.map((opt) => ({ value: opt.value, label: opt.label })),
|
|
3795
|
-
loadSuggestions: loadFieldSuggestions
|
|
3796
|
-
disabled
|
|
3752
|
+
loadSuggestions: loadFieldSuggestions
|
|
3797
3753
|
}
|
|
3798
3754
|
),
|
|
3799
3755
|
field.type === "combobox" && /* @__PURE__ */ jsx(
|