@open-mercato/ui 0.9.1-develop.7575.1.8480826a9b → 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.
Files changed (93) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/ai/parts/MutationResultCard.js +14 -1
  3. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  4. package/dist/backend/CrudForm.js +96 -52
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/DataTable.js +40 -14
  7. package/dist/backend/DataTable.js.map +2 -2
  8. package/dist/backend/FlashMessages.js +25 -1
  9. package/dist/backend/FlashMessages.js.map +2 -2
  10. package/dist/backend/columns/ColumnChooserPanel.js +4 -3
  11. package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
  12. package/dist/backend/confirm-dialog/ConfirmDialog.js +11 -0
  13. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  14. package/dist/backend/crud/injectedFieldGroups.js +50 -0
  15. package/dist/backend/crud/injectedFieldGroups.js.map +7 -0
  16. package/dist/backend/detail/InlineEditors.js +17 -6
  17. package/dist/backend/detail/InlineEditors.js.map +2 -2
  18. package/dist/backend/filters/FilterFieldPicker.js +3 -2
  19. package/dist/backend/filters/FilterFieldPicker.js.map +2 -2
  20. package/dist/backend/forms/ActionsDropdown.js +52 -2
  21. package/dist/backend/forms/ActionsDropdown.js.map +2 -2
  22. package/dist/backend/inputs/LookupPickers.js +149 -0
  23. package/dist/backend/inputs/LookupPickers.js.map +7 -0
  24. package/dist/backend/inputs/PhoneNumberField.js +17 -5
  25. package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
  26. package/dist/backend/inputs/TagsInput.js +4 -1
  27. package/dist/backend/inputs/TagsInput.js.map +2 -2
  28. package/dist/backend/inputs/index.js +2 -0
  29. package/dist/backend/inputs/index.js.map +2 -2
  30. package/dist/backend/inputs/lookupSources.js +84 -0
  31. package/dist/backend/inputs/lookupSources.js.map +7 -0
  32. package/dist/backend/messages/useMessageCompose.js +29 -10
  33. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  34. package/dist/backend/notifications/NotificationDispatcher.js +5 -0
  35. package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
  36. package/dist/backend/notifications/useNotificationActions.js +13 -2
  37. package/dist/backend/notifications/useNotificationActions.js.map +2 -2
  38. package/dist/backend/utils/api.js +25 -4
  39. package/dist/backend/utils/api.js.map +2 -2
  40. package/dist/backend/utils/groupLabels.js +16 -0
  41. package/dist/backend/utils/groupLabels.js.map +7 -0
  42. package/dist/backend/version-history/VersionHistoryPanel.js +18 -2
  43. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  44. package/dist/theme/QueryProvider.js +7 -1
  45. package/dist/theme/QueryProvider.js.map +2 -2
  46. package/package.json +4 -4
  47. package/src/ai/parts/MutationResultCard.tsx +30 -1
  48. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +48 -0
  49. package/src/backend/CrudForm.tsx +121 -60
  50. package/src/backend/DataTable.tsx +54 -14
  51. package/src/backend/FlashMessages.tsx +34 -1
  52. package/src/backend/__tests__/ActionsDropdown.test.tsx +105 -0
  53. package/src/backend/__tests__/CrudForm.disabledFields.test.tsx +57 -0
  54. package/src/backend/__tests__/CrudForm.embeddedExtraActions.test.tsx +114 -0
  55. package/src/backend/__tests__/CrudForm.fieldChangeRenders.test.tsx +127 -0
  56. package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +85 -4
  57. package/src/backend/__tests__/DataTable.injectedColumnPlacement.test.tsx +146 -0
  58. package/src/backend/__tests__/FlashMessages.test.tsx +65 -0
  59. package/src/backend/__tests__/VersionHistoryPanel.test.tsx +81 -1
  60. package/src/backend/__tests__/next-dynamic-specifier.test.ts +23 -0
  61. package/src/backend/columns/ColumnChooserPanel.tsx +4 -3
  62. package/src/backend/columns/__tests__/ColumnChooserPanel.groupLabels.test.tsx +72 -0
  63. package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -0
  64. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +29 -0
  65. package/src/backend/crud/__tests__/injectedFieldGroups.test.ts +90 -0
  66. package/src/backend/crud/injectedFieldGroups.ts +75 -0
  67. package/src/backend/detail/InlineEditors.tsx +28 -6
  68. package/src/backend/detail/__tests__/InlineEditors.test.tsx +121 -6
  69. package/src/backend/filters/FilterFieldPicker.tsx +3 -2
  70. package/src/backend/filters/__tests__/FilterFieldPicker.groupLabels.test.tsx +77 -0
  71. package/src/backend/forms/ActionsDropdown.tsx +71 -2
  72. package/src/backend/inputs/LookupPickers.tsx +181 -0
  73. package/src/backend/inputs/PhoneNumberField.tsx +17 -11
  74. package/src/backend/inputs/TagsInput.tsx +4 -0
  75. package/src/backend/inputs/__tests__/LookupPickers.test.tsx +120 -0
  76. package/src/backend/inputs/__tests__/PhoneNumberField.renders.test.tsx +35 -0
  77. package/src/backend/inputs/__tests__/TagsInput.test.tsx +41 -0
  78. package/src/backend/inputs/__tests__/lookupSources.test.ts +87 -0
  79. package/src/backend/inputs/index.ts +2 -0
  80. package/src/backend/inputs/lookupSources.ts +114 -0
  81. package/src/backend/messages/__tests__/useMessageCompose.test.tsx +106 -1
  82. package/src/backend/messages/message-composer.types.ts +2 -0
  83. package/src/backend/messages/useMessageCompose.ts +34 -13
  84. package/src/backend/notifications/NotificationDispatcher.ts +5 -0
  85. package/src/backend/notifications/__tests__/NotificationDispatcher.navigate.test.ts +76 -0
  86. package/src/backend/notifications/__tests__/useNotificationActions.test.tsx +86 -1
  87. package/src/backend/notifications/useNotificationActions.ts +19 -4
  88. package/src/backend/utils/__tests__/api.test.ts +67 -0
  89. package/src/backend/utils/api.ts +33 -4
  90. package/src/backend/utils/groupLabels.ts +26 -0
  91. package/src/backend/version-history/VersionHistoryPanel.tsx +24 -2
  92. package/src/theme/QueryProvider.tsx +7 -1
  93. package/src/theme/__tests__/QueryProvider.i18n.test.tsx +40 -0
@@ -1,3 +1,3 @@
1
1
  Generated lucide registry with 182 icons -> /home/runner/work/open-mercato/open-mercato/packages/ui/src/backend/icons/lucideRegistry.generated.tsx
2
- [build:ui] found 452 entry points
2
+ [build:ui] found 456 entry points
3
3
  [build:ui] built successfully
@@ -14,7 +14,10 @@ function isSuccessStatus(status) {
14
14
  return status === "confirmed";
15
15
  }
16
16
  function isFailureStatus(status) {
17
- return status === "failed" || status === "cancelled" || status === "expired";
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": "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",
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
  }
@@ -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 { insertByInjectionPlacement } from "@open-mercato/shared/modules/widgets/injection-position";
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, setHasUnsavedChanges] = React.useState(false);
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 { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? "", injectionWidgets);
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 declaredGroups) {
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, groups, hiddenGroupIdSet, injectedFieldDefinitions, placedCustomFieldIds]);
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 onBlurRequest = React.useCallback((fieldId) => {
1973
- void validateFieldOnBlur(fieldId);
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
- ) : headerExtraActions ? /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mb-2", children: headerExtraActions }) : null,
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
- hideFooterActions || formReadOnly ? null : /* @__PURE__ */ jsx(
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
- ) : headerExtraActions ? /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mb-2", children: headerExtraActions }) : null,
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
- hideFooterActions || formReadOnly ? null : /* @__PURE__ */ jsx(
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(