@open-mercato/ui 0.10.1-develop.7576.1.21d5b50435 → 0.10.1-develop.7577.1.477f2486e5
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 +14 -1
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/backend/CrudForm.js +96 -52
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +40 -14
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FlashMessages.js +25 -1
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +4 -3
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +11 -0
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/crud/injectedFieldGroups.js +50 -0
- package/dist/backend/crud/injectedFieldGroups.js.map +7 -0
- package/dist/backend/detail/InlineEditors.js +17 -6
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/filters/FilterFieldPicker.js +3 -2
- package/dist/backend/filters/FilterFieldPicker.js.map +2 -2
- package/dist/backend/forms/ActionsDropdown.js +52 -2
- package/dist/backend/forms/ActionsDropdown.js.map +2 -2
- package/dist/backend/inputs/LookupPickers.js +149 -0
- package/dist/backend/inputs/LookupPickers.js.map +7 -0
- package/dist/backend/inputs/PhoneNumberField.js +17 -5
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +4 -1
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/inputs/index.js +2 -0
- package/dist/backend/inputs/index.js.map +2 -2
- package/dist/backend/inputs/lookupSources.js +84 -0
- package/dist/backend/inputs/lookupSources.js.map +7 -0
- package/dist/backend/messages/useMessageCompose.js +29 -10
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/backend/notifications/NotificationDispatcher.js +5 -0
- package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
- package/dist/backend/notifications/useNotificationActions.js +13 -2
- package/dist/backend/notifications/useNotificationActions.js.map +2 -2
- package/dist/backend/utils/api.js +25 -4
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/groupLabels.js +16 -0
- package/dist/backend/utils/groupLabels.js.map +7 -0
- package/dist/backend/version-history/VersionHistoryPanel.js +18 -2
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/theme/QueryProvider.js +7 -1
- package/dist/theme/QueryProvider.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/parts/MutationResultCard.tsx +30 -1
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +48 -0
- package/src/backend/CrudForm.tsx +121 -60
- package/src/backend/DataTable.tsx +54 -14
- package/src/backend/FlashMessages.tsx +34 -1
- package/src/backend/__tests__/ActionsDropdown.test.tsx +105 -0
- package/src/backend/__tests__/CrudForm.disabledFields.test.tsx +57 -0
- package/src/backend/__tests__/CrudForm.embeddedExtraActions.test.tsx +114 -0
- package/src/backend/__tests__/CrudForm.fieldChangeRenders.test.tsx +127 -0
- package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +85 -4
- package/src/backend/__tests__/DataTable.injectedColumnPlacement.test.tsx +146 -0
- package/src/backend/__tests__/FlashMessages.test.tsx +65 -0
- package/src/backend/__tests__/VersionHistoryPanel.test.tsx +81 -1
- package/src/backend/__tests__/next-dynamic-specifier.test.ts +23 -0
- package/src/backend/columns/ColumnChooserPanel.tsx +4 -3
- package/src/backend/columns/__tests__/ColumnChooserPanel.groupLabels.test.tsx +72 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -0
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +29 -0
- package/src/backend/crud/__tests__/injectedFieldGroups.test.ts +90 -0
- package/src/backend/crud/injectedFieldGroups.ts +75 -0
- package/src/backend/detail/InlineEditors.tsx +28 -6
- package/src/backend/detail/__tests__/InlineEditors.test.tsx +121 -6
- package/src/backend/filters/FilterFieldPicker.tsx +3 -2
- package/src/backend/filters/__tests__/FilterFieldPicker.groupLabels.test.tsx +77 -0
- package/src/backend/forms/ActionsDropdown.tsx +71 -2
- package/src/backend/inputs/LookupPickers.tsx +181 -0
- package/src/backend/inputs/PhoneNumberField.tsx +17 -11
- package/src/backend/inputs/TagsInput.tsx +4 -0
- package/src/backend/inputs/__tests__/LookupPickers.test.tsx +120 -0
- package/src/backend/inputs/__tests__/PhoneNumberField.renders.test.tsx +35 -0
- package/src/backend/inputs/__tests__/TagsInput.test.tsx +41 -0
- package/src/backend/inputs/__tests__/lookupSources.test.ts +87 -0
- package/src/backend/inputs/index.ts +2 -0
- package/src/backend/inputs/lookupSources.ts +114 -0
- package/src/backend/messages/__tests__/useMessageCompose.test.tsx +106 -1
- package/src/backend/messages/message-composer.types.ts +2 -0
- package/src/backend/messages/useMessageCompose.ts +34 -13
- package/src/backend/notifications/NotificationDispatcher.ts +5 -0
- package/src/backend/notifications/__tests__/NotificationDispatcher.navigate.test.ts +76 -0
- package/src/backend/notifications/__tests__/useNotificationActions.test.tsx +86 -1
- package/src/backend/notifications/useNotificationActions.ts +19 -4
- package/src/backend/utils/__tests__/api.test.ts +67 -0
- package/src/backend/utils/api.ts +33 -4
- package/src/backend/utils/groupLabels.ts +26 -0
- package/src/backend/version-history/VersionHistoryPanel.tsx +24 -2
- package/src/theme/QueryProvider.tsx +7 -1
- package/src/theme/__tests__/QueryProvider.i18n.test.tsx +40 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -14,7 +14,10 @@ function isSuccessStatus(status) {
|
|
|
14
14
|
return status === "confirmed";
|
|
15
15
|
}
|
|
16
16
|
function isFailureStatus(status) {
|
|
17
|
-
return status === "failed" || status === "
|
|
17
|
+
return status === "failed" || status === "expired";
|
|
18
|
+
}
|
|
19
|
+
function isCancelledStatus(status) {
|
|
20
|
+
return status === "cancelled";
|
|
18
21
|
}
|
|
19
22
|
function MutationResultCard(props) {
|
|
20
23
|
const t = useT();
|
|
@@ -86,6 +89,16 @@ function MutationResultCard(props) {
|
|
|
86
89
|
] })
|
|
87
90
|
] });
|
|
88
91
|
}
|
|
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
|
+
}
|
|
89
102
|
if (isFailureStatus(status) || result?.error) {
|
|
90
103
|
const code = result?.error?.code ?? status ?? "failed";
|
|
91
104
|
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 === '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": ";
|
|
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": "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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 { placeInjectedFieldsInGroups } from "./crud/injectedFieldGroups.js";
|
|
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,6 +173,21 @@ 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
|
+
}
|
|
176
191
|
function mergeByKey(primary, legacy, keyOf) {
|
|
177
192
|
if (!legacy.length) return primary;
|
|
178
193
|
const seen = new Set(primary.map(keyOf));
|
|
@@ -553,7 +568,13 @@ function CrudForm({
|
|
|
553
568
|
const navigationConfirmPendingRef = React.useRef(false);
|
|
554
569
|
const submitNavigationBypassRef = React.useRef(false);
|
|
555
570
|
const submitNavigationBypassTimeoutRef = React.useRef(null);
|
|
556
|
-
const [hasUnsavedChanges,
|
|
571
|
+
const [hasUnsavedChanges, setHasUnsavedChangesState] = React.useState(false);
|
|
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
|
+
}, []);
|
|
557
578
|
const popStateRollbackRef = React.useRef(false);
|
|
558
579
|
const shouldBypassUnsavedChangesGuardRef = React.useRef(shouldBypassUnsavedChangesGuard);
|
|
559
580
|
React.useEffect(() => {
|
|
@@ -575,7 +596,7 @@ function CrudForm({
|
|
|
575
596
|
const dirty = currentSnapshot !== snapshot;
|
|
576
597
|
isDirtyRef.current = dirty;
|
|
577
598
|
setHasUnsavedChanges(dirty);
|
|
578
|
-
}, [embedded, trackDirtyWhenEmbedded, values]);
|
|
599
|
+
}, [embedded, trackDirtyWhenEmbedded, values, setHasUnsavedChanges]);
|
|
579
600
|
React.useEffect(() => {
|
|
580
601
|
onDirtyChange?.(hasUnsavedChanges);
|
|
581
602
|
}, [hasUnsavedChanges, onDirtyChange]);
|
|
@@ -619,7 +640,7 @@ function CrudForm({
|
|
|
619
640
|
everEditedFieldIdsRef.current.clear();
|
|
620
641
|
isDirtyRef.current = false;
|
|
621
642
|
setHasUnsavedChanges(false);
|
|
622
|
-
}, []);
|
|
643
|
+
}, [setHasUnsavedChanges]);
|
|
623
644
|
const confirmUnsavedChanges = React.useCallback(async () => {
|
|
624
645
|
if (!isDirtyRef.current || typeof window === "undefined") return true;
|
|
625
646
|
if (navigationPromptBypassRef.current || submitNavigationBypassRef.current) return true;
|
|
@@ -725,7 +746,7 @@ function CrudForm({
|
|
|
725
746
|
window.history.pushState = originalPushState;
|
|
726
747
|
window.history.replaceState = originalReplaceState;
|
|
727
748
|
};
|
|
728
|
-
}, [allowNextNavigation, clearDirtyState, confirmUnsavedChanges, embedded, hasUnsavedChanges, router, trackDirtyWhenEmbedded]);
|
|
749
|
+
}, [allowNextNavigation, clearDirtyState, confirmUnsavedChanges, embedded, hasUnsavedChanges, router, trackDirtyWhenEmbedded, setHasUnsavedChanges]);
|
|
729
750
|
const { widgets: primaryInjectionWidgets } = useInjectionWidgets(resolvedInjectionSpotId, {
|
|
730
751
|
context: injectionContext,
|
|
731
752
|
triggerOnLoad: true
|
|
@@ -748,7 +769,26 @@ function CrudForm({
|
|
|
748
769
|
() => mergeByKey(primaryFieldWidgets, legacyFieldWidgets, (w) => w.metadata.id),
|
|
749
770
|
[primaryFieldWidgets, legacyFieldWidgets]
|
|
750
771
|
);
|
|
751
|
-
const
|
|
772
|
+
const injectionEventWidgets = React.useMemo(() => {
|
|
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);
|
|
752
792
|
const extendedInjectionEventsEnabled = CRUDFORM_EXTENDED_EVENTS_ENABLED && Boolean(resolvedInjectionSpotId);
|
|
753
793
|
const widgetRequiredFieldIds = React.useMemo(() => {
|
|
754
794
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -1115,6 +1155,13 @@ function CrudForm({
|
|
|
1115
1155
|
headerInjectionAction,
|
|
1116
1156
|
extraActions
|
|
1117
1157
|
] }) : 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;
|
|
1118
1165
|
React.useEffect(() => {
|
|
1119
1166
|
let cancelled = false;
|
|
1120
1167
|
async function load() {
|
|
@@ -1347,6 +1394,24 @@ function CrudForm({
|
|
|
1347
1394
|
}
|
|
1348
1395
|
return definitions;
|
|
1349
1396
|
}, [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]);
|
|
1350
1415
|
const injectedFieldContext = React.useMemo(() => {
|
|
1351
1416
|
const recordValues = values;
|
|
1352
1417
|
const organizationId = typeof recordValues.organizationId === "string" ? recordValues.organizationId : null;
|
|
@@ -1393,22 +1458,15 @@ function CrudForm({
|
|
|
1393
1458
|
}
|
|
1394
1459
|
return (group.fields ?? []).map((entry) => typeof entry === "string" ? entry : entry.id);
|
|
1395
1460
|
};
|
|
1396
|
-
const declaredGroups = groups ?? [];
|
|
1397
1461
|
const hidden = /* @__PURE__ */ new Set();
|
|
1398
1462
|
const visible = /* @__PURE__ */ new Set();
|
|
1399
|
-
for (const group of
|
|
1463
|
+
for (const group of groupsWithInjectedFields ?? []) {
|
|
1400
1464
|
const target = hiddenGroupIdSet.has(group.id) ? hidden : visible;
|
|
1401
1465
|
for (const fieldId of groupFieldIds(group)) target.add(fieldId);
|
|
1402
1466
|
}
|
|
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
|
-
}
|
|
1409
1467
|
for (const fieldId of visible) hidden.delete(fieldId);
|
|
1410
1468
|
return hidden;
|
|
1411
|
-
}, [cfFields,
|
|
1469
|
+
}, [cfFields, groupsWithInjectedFields, hiddenGroupIdSet, placedCustomFieldIds]);
|
|
1412
1470
|
const hiddenInjectedFieldIds = React.useMemo(() => {
|
|
1413
1471
|
const hidden = /* @__PURE__ */ new Set();
|
|
1414
1472
|
for (const definition of injectedFieldDefinitions) {
|
|
@@ -1615,31 +1673,6 @@ function CrudForm({
|
|
|
1615
1673
|
pairs.sort((a, b) => b.priority - a.priority);
|
|
1616
1674
|
return pairs.map((p) => p.group);
|
|
1617
1675
|
}, [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]);
|
|
1643
1676
|
const shouldAutoGroup = (!groupsWithInjectedFields || groupsWithInjectedFields.length === 0) && injectionGroupCards.length > 0;
|
|
1644
1677
|
const declaredGroupsForLayout = React.useMemo(() => {
|
|
1645
1678
|
const baseGroups = groupsWithInjectedFields && groupsWithInjectedFields.length ? groupsWithInjectedFields : [];
|
|
@@ -1964,14 +1997,18 @@ function CrudForm({
|
|
|
1964
1997
|
if (activeDispatch?.eventId === dispatchToken.eventId && activeDispatch.formGeneration === dispatchToken.formGeneration) {
|
|
1965
1998
|
activeFieldChangeDispatchRef.current = null;
|
|
1966
1999
|
}
|
|
1967
|
-
if (fieldChangeDispatcherMountedRef.current && fieldChangeFormGenerationRef.current === dispatchToken.formGeneration) {
|
|
2000
|
+
if (fieldChangeDispatcherMountedRef.current && fieldChangeFormGenerationRef.current === dispatchToken.formGeneration && pendingFieldChangeEventsRef.current.length > 0) {
|
|
1968
2001
|
setFieldChangeDispatchVersion((version) => version + 1);
|
|
1969
2002
|
}
|
|
1970
2003
|
});
|
|
1971
2004
|
}, [extendedInjectionEventsEnabled, fieldChangeDispatchVersion, triggerInjectionEvent, values]);
|
|
1972
|
-
const
|
|
1973
|
-
|
|
2005
|
+
const validateFieldOnBlurRef = React.useRef(validateFieldOnBlur);
|
|
2006
|
+
React.useLayoutEffect(() => {
|
|
2007
|
+
validateFieldOnBlurRef.current = validateFieldOnBlur;
|
|
1974
2008
|
}, [validateFieldOnBlur]);
|
|
2009
|
+
const onBlurRequest = React.useCallback((fieldId) => {
|
|
2010
|
+
void validateFieldOnBlurRef.current(fieldId);
|
|
2011
|
+
}, []);
|
|
1975
2012
|
const handleFieldsetSelectionChange = React.useCallback(
|
|
1976
2013
|
(entityId2, nextCode) => {
|
|
1977
2014
|
if (!nextCode) return;
|
|
@@ -2953,7 +2990,7 @@ function CrudForm({
|
|
|
2953
2990
|
submit: formReadOnly ? void 0 : { formId, pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
2954
2991
|
}
|
|
2955
2992
|
}
|
|
2956
|
-
) :
|
|
2993
|
+
) : embeddedHeaderActions ? /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mb-2", children: embeddedHeaderActions }) : null,
|
|
2957
2994
|
contentHeader,
|
|
2958
2995
|
/* @__PURE__ */ jsx(
|
|
2959
2996
|
DataLoader,
|
|
@@ -2986,7 +3023,7 @@ function CrudForm({
|
|
|
2986
3023
|
}
|
|
2987
3024
|
),
|
|
2988
3025
|
formError && !Object.keys(errors).length ? /* @__PURE__ */ jsx("div", { className: "text-sm text-status-error-text", children: formError }) : null,
|
|
2989
|
-
|
|
3026
|
+
renderFooterActions ? /* @__PURE__ */ jsx(
|
|
2990
3027
|
FormFooter,
|
|
2991
3028
|
{
|
|
2992
3029
|
embedded,
|
|
@@ -3002,7 +3039,7 @@ function CrudForm({
|
|
|
3002
3039
|
submit: { pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
3003
3040
|
}
|
|
3004
3041
|
}
|
|
3005
|
-
)
|
|
3042
|
+
) : null
|
|
3006
3043
|
] })
|
|
3007
3044
|
)
|
|
3008
3045
|
}
|
|
@@ -3031,7 +3068,7 @@ function CrudForm({
|
|
|
3031
3068
|
submit: formReadOnly ? void 0 : { formId, pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
3032
3069
|
}
|
|
3033
3070
|
}
|
|
3034
|
-
) :
|
|
3071
|
+
) : embeddedHeaderActions ? /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mb-2", children: embeddedHeaderActions }) : null,
|
|
3035
3072
|
contentHeader,
|
|
3036
3073
|
/* @__PURE__ */ jsx(
|
|
3037
3074
|
DataLoader,
|
|
@@ -3085,7 +3122,7 @@ function CrudForm({
|
|
|
3085
3122
|
);
|
|
3086
3123
|
}) }),
|
|
3087
3124
|
formError && !Object.keys(errors).length ? /* @__PURE__ */ jsx("div", { className: "text-sm text-status-error-text", children: formError }) : null,
|
|
3088
|
-
|
|
3125
|
+
renderFooterActions ? /* @__PURE__ */ jsx(
|
|
3089
3126
|
FormFooter,
|
|
3090
3127
|
{
|
|
3091
3128
|
embedded,
|
|
@@ -3101,7 +3138,7 @@ function CrudForm({
|
|
|
3101
3138
|
submit: { pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
|
|
3102
3139
|
}
|
|
3103
3140
|
}
|
|
3104
|
-
)
|
|
3141
|
+
) : null
|
|
3105
3142
|
]
|
|
3106
3143
|
}
|
|
3107
3144
|
) })
|
|
@@ -3256,7 +3293,9 @@ function NumberInput({
|
|
|
3256
3293
|
onChange,
|
|
3257
3294
|
placeholder,
|
|
3258
3295
|
autoFocus,
|
|
3259
|
-
onSubmit
|
|
3296
|
+
onSubmit,
|
|
3297
|
+
disabled,
|
|
3298
|
+
readOnly
|
|
3260
3299
|
}) {
|
|
3261
3300
|
const locale = useOptionalLocale();
|
|
3262
3301
|
const serializedValue = value !== void 0 && value !== null ? String(value) : "";
|
|
@@ -3324,6 +3363,8 @@ function NumberInput({
|
|
|
3324
3363
|
onFocus: handleFocus,
|
|
3325
3364
|
onBlur: handleBlur,
|
|
3326
3365
|
autoFocus,
|
|
3366
|
+
disabled,
|
|
3367
|
+
readOnly,
|
|
3327
3368
|
"data-crud-focus-target": ""
|
|
3328
3369
|
}
|
|
3329
3370
|
)
|
|
@@ -3649,7 +3690,9 @@ const FieldControl = React.memo(
|
|
|
3649
3690
|
placeholder,
|
|
3650
3691
|
onChange: fieldSetValue,
|
|
3651
3692
|
autoFocus: autoFocusField,
|
|
3652
|
-
onSubmit: onSubmitRequest
|
|
3693
|
+
onSubmit: onSubmitRequest,
|
|
3694
|
+
disabled,
|
|
3695
|
+
readOnly
|
|
3653
3696
|
}
|
|
3654
3697
|
),
|
|
3655
3698
|
field.type === "date" && /* @__PURE__ */ jsx(
|
|
@@ -3749,7 +3792,8 @@ const FieldControl = React.memo(
|
|
|
3749
3792
|
autoFocus: autoFocusField,
|
|
3750
3793
|
suppressInitialSuggestionsOnFocus: autoFocusField,
|
|
3751
3794
|
suggestions: options.map((opt) => ({ value: opt.value, label: opt.label })),
|
|
3752
|
-
loadSuggestions: loadFieldSuggestions
|
|
3795
|
+
loadSuggestions: loadFieldSuggestions,
|
|
3796
|
+
disabled
|
|
3753
3797
|
}
|
|
3754
3798
|
),
|
|
3755
3799
|
field.type === "combobox" && /* @__PURE__ */ jsx(
|