@open-mercato/ui 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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/AGENTS.md +4 -2
- package/agentic/standalone-guide.md +1 -1
- package/build.mjs +2 -54
- package/dist/ai/AiChat.js +26 -7
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/LoopDisabledBanner.js +1 -1
- package/dist/ai/LoopDisabledBanner.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +4 -4
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +2 -2
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +3 -3
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/backend/AppShell.js +43 -4
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/AuthSessionGuard.js +14 -0
- package/dist/backend/AuthSessionGuard.js.map +2 -2
- package/dist/backend/BackendChromeProvider.js +6 -1
- package/dist/backend/BackendChromeProvider.js.map +2 -2
- package/dist/backend/CrudForm.js +6 -4
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +20 -18
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/JsonBuilder.js +3 -3
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/NextStepCallout.js +4 -4
- package/dist/backend/NextStepCallout.js.map +1 -1
- package/dist/backend/PerspectiveSidebar.js +1 -1
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +2 -1
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/RowActions.js +2 -2
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/crud/useGroupOrder.js +14 -21
- package/dist/backend/crud/useGroupOrder.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
- package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +3 -3
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/detail/AddressTiles.js +4 -5
- package/dist/backend/detail/AddressTiles.js.map +2 -2
- package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
- package/dist/backend/detail/AttachmentsSection.js +1 -1
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +1 -1
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/detail/TagsSection.js +1 -1
- package/dist/backend/detail/TagsSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
- package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
- package/dist/backend/forms/FormHeader.js +1 -2
- package/dist/backend/forms/FormHeader.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +1 -25
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.js +3 -3
- package/dist/backend/icons/lucideRegistry.js.map +2 -2
- package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
- package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
- package/dist/backend/injection/spotIds.js +23 -18
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js.map +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
- package/dist/backend/perspectiveState.js +34 -0
- package/dist/backend/perspectiveState.js.map +7 -0
- package/dist/backend/schedule/ScheduleAgenda.js +3 -3
- package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
- package/dist/backend/schedule/ScheduleGrid.js +3 -3
- package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
- package/dist/backend/schedule/ScheduleToolbar.js +87 -70
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/sidebar/customization-helpers.js +30 -0
- package/dist/backend/sidebar/customization-helpers.js.map +2 -2
- package/dist/backend/utils/apiCall.js +21 -1
- package/dist/backend/utils/apiCall.js.map +2 -2
- package/dist/backend/utils/customFieldDefs.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +10 -9
- package/dist/backend/utils/customFieldForms.js.map +2 -2
- package/dist/backend/utils/fetchAllPages.js +57 -0
- package/dist/backend/utils/fetchAllPages.js.map +7 -0
- package/dist/backend/utils/nav.js +5 -2
- package/dist/backend/utils/nav.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +2 -1
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/portal/PortalContext.js +1 -1
- package/dist/portal/PortalContext.js.map +2 -2
- package/dist/portal/hooks/portalBridgeStatus.js +25 -0
- package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
- package/dist/portal/hooks/usePortalEventBridge.js +6 -0
- package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
- package/dist/portal/hooks/usePortalNotifications.js +42 -4
- package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
- package/dist/portal/utils/nav.js +2 -3
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/ErrorNotice.js +1 -1
- package/dist/primitives/ErrorNotice.js.map +2 -2
- package/dist/primitives/Notice.js +1 -1
- package/dist/primitives/Notice.js.map +1 -1
- package/dist/primitives/button.js +9 -4
- package/dist/primitives/button.js.map +2 -2
- package/dist/theme/ThemeProvider.js +1 -1
- package/dist/theme/ThemeProvider.js.map +2 -2
- package/dist/theme/index.js +3 -0
- package/dist/theme/index.js.map +2 -2
- package/dist/theme/theme-init-script.js +17 -0
- package/dist/theme/theme-init-script.js.map +7 -0
- package/figma/alert.figma.tsx +35 -0
- package/figma/badge.figma.tsx +33 -0
- package/figma/button.figma.tsx +33 -0
- package/figma/checkbox.figma.tsx +28 -0
- package/figma/drawer.figma.tsx +40 -0
- package/figma/input-variants.figma.tsx +58 -0
- package/figma/radio.figma.tsx +16 -0
- package/figma/select.figma.tsx +24 -0
- package/figma/status-badge.figma.tsx +24 -0
- package/figma/switch.figma.tsx +28 -0
- package/figma/tabs.figma.tsx +22 -0
- package/figma/tag.figma.tsx +27 -0
- package/figma.config.json +6 -0
- package/jest.config.cjs +2 -1
- package/package.json +21 -18
- package/scripts/lucideRegistrySource.cjs +133 -0
- package/src/ai/AiChat.tsx +36 -7
- package/src/ai/LoopDisabledBanner.tsx +1 -1
- package/src/ai/__tests__/AiChat.test.tsx +31 -0
- package/src/ai/parts/ConfirmationCard.tsx +4 -4
- package/src/ai/parts/FieldDiffCard.tsx +2 -2
- package/src/ai/parts/MutationResultCard.tsx +3 -3
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
- package/src/backend/AppShell.tsx +55 -4
- package/src/backend/AuthSessionGuard.tsx +29 -0
- package/src/backend/BackendChromeProvider.tsx +19 -1
- package/src/backend/CrudForm.tsx +18 -4
- package/src/backend/DataTable.tsx +36 -19
- package/src/backend/JsonBuilder.tsx +3 -3
- package/src/backend/NextStepCallout.tsx +4 -4
- package/src/backend/PerspectiveSidebar.tsx +1 -1
- package/src/backend/ProfileDropdown.tsx +1 -0
- package/src/backend/RowActions.tsx +2 -2
- package/src/backend/__tests__/AppShell.test.tsx +65 -7
- package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
- package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
- package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
- package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
- package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
- package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
- package/src/backend/__tests__/nav-utils.test.ts +139 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
- package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
- package/src/backend/crud/useGroupOrder.ts +30 -25
- package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
- package/src/backend/dashboard/DashboardScreen.tsx +3 -3
- package/src/backend/detail/AddressTiles.tsx +3 -6
- package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/AttachmentsSection.tsx +1 -1
- package/src/backend/detail/NotesSection.tsx +1 -1
- package/src/backend/detail/TagsSection.tsx +1 -1
- package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
- package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
- package/src/backend/forms/FormHeader.tsx +1 -2
- package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
- package/src/backend/icons/lucideRegistry.ts +2 -2
- package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
- package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
- package/src/backend/injection/spotIds.ts +23 -18
- package/src/backend/inputs/ComboboxInput.tsx +1 -1
- package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
- package/src/backend/perspectiveState.ts +44 -0
- package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
- package/src/backend/schedule/ScheduleGrid.tsx +3 -3
- package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
- package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
- package/src/backend/sidebar/customization-helpers.ts +47 -0
- package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
- package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
- package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
- package/src/backend/utils/apiCall.ts +29 -1
- package/src/backend/utils/customFieldDefs.ts +1 -0
- package/src/backend/utils/customFieldForms.ts +15 -11
- package/src/backend/utils/fetchAllPages.ts +93 -0
- package/src/backend/utils/nav.ts +22 -2
- package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
- package/src/frontend/LanguageSwitcher.tsx +1 -0
- package/src/portal/PortalContext.tsx +1 -1
- package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
- package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
- package/src/portal/hooks/portalBridgeStatus.ts +29 -0
- package/src/portal/hooks/usePortalEventBridge.ts +6 -0
- package/src/portal/hooks/usePortalNotifications.ts +55 -12
- package/src/portal/utils/__tests__/nav.test.ts +2 -2
- package/src/portal/utils/nav.ts +3 -4
- package/src/primitives/ErrorNotice.tsx +1 -1
- package/src/primitives/Notice.tsx +5 -5
- package/src/primitives/__tests__/button.test.tsx +70 -0
- package/src/primitives/button.tsx +10 -4
- package/src/theme/ThemeProvider.tsx +1 -2
- package/src/theme/__tests__/theme-init-script.test.ts +70 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/theme-init-script.ts +23 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Pure source emitter for src/backend/icons/lucideRegistry.generated.tsx.
|
|
2
|
+
// Kept side-effect free, and CommonJS so both build.mjs and the Jest suite can load it.
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
IndentationText,
|
|
6
|
+
Project,
|
|
7
|
+
QuoteKind,
|
|
8
|
+
ScriptKind,
|
|
9
|
+
StructureKind,
|
|
10
|
+
VariableDeclarationKind,
|
|
11
|
+
} = require('ts-morph')
|
|
12
|
+
|
|
13
|
+
const GENERATED_FILE_NAME = 'lucideRegistry.generated.tsx'
|
|
14
|
+
const LUCIDE_MODULE_SPECIFIER = 'lucide-react'
|
|
15
|
+
const BANNER = `/* eslint-disable */
|
|
16
|
+
// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
|
|
17
|
+
// Generated from discovered icon string usages across the repo.
|
|
18
|
+
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
let sharedProject = null
|
|
22
|
+
|
|
23
|
+
function getProject() {
|
|
24
|
+
if (!sharedProject) {
|
|
25
|
+
sharedProject = new Project({
|
|
26
|
+
useInMemoryFileSystem: true,
|
|
27
|
+
manipulationSettings: {
|
|
28
|
+
indentationText: IndentationText.TwoSpaces,
|
|
29
|
+
quoteKind: QuoteKind.Single,
|
|
30
|
+
useTrailingCommas: true,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sharedProject
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sortResolvedIcons(resolvedIcons) {
|
|
39
|
+
return [...resolvedIcons].sort((a, b) => a.kebab.localeCompare(b.kebab))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectUniqueExports(resolvedIcons) {
|
|
43
|
+
return [...new Set(resolvedIcons.map((icon) => icon.exportName))].sort((a, b) => a.localeCompare(b))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeNamedImports(exportNames) {
|
|
47
|
+
return (writer) => {
|
|
48
|
+
writer.newLine()
|
|
49
|
+
for (const name of exportNames) {
|
|
50
|
+
writer.write(`${name},`)
|
|
51
|
+
writer.newLine()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeRegistryLiteral(entries) {
|
|
57
|
+
return (writer) => {
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
writer.write('{}')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writer.write('{')
|
|
64
|
+
writer.newLine()
|
|
65
|
+
for (const icon of entries) {
|
|
66
|
+
writer.write(`'${icon.kebab}': ${icon.exportName},`)
|
|
67
|
+
writer.newLine()
|
|
68
|
+
}
|
|
69
|
+
writer.write('}')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertNoSyntacticDiagnostics(project, sourceFile) {
|
|
74
|
+
const diagnostics = project.getProgram().getSyntacticDiagnostics(sourceFile)
|
|
75
|
+
if (diagnostics.length === 0) return
|
|
76
|
+
|
|
77
|
+
const details = diagnostics
|
|
78
|
+
.map((diagnostic) => ` line ${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
|
|
79
|
+
.join('\n')
|
|
80
|
+
throw new Error(`[internal] Generated ${GENERATED_FILE_NAME} is not syntactically valid:\n${details}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeSourceText(text) {
|
|
84
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
85
|
+
return normalized.endsWith('\n') ? normalized : `${normalized}\n`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildLucideRegistrySource(resolvedIcons) {
|
|
89
|
+
const entries = sortResolvedIcons(resolvedIcons)
|
|
90
|
+
const uniqueExports = collectUniqueExports(entries)
|
|
91
|
+
|
|
92
|
+
const project = getProject()
|
|
93
|
+
const sourceFile = project.createSourceFile(GENERATED_FILE_NAME, '', {
|
|
94
|
+
overwrite: true,
|
|
95
|
+
scriptKind: ScriptKind.TSX,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
sourceFile.addImportDeclaration({
|
|
99
|
+
kind: StructureKind.ImportDeclaration,
|
|
100
|
+
isTypeOnly: true,
|
|
101
|
+
namedImports: [{ name: 'LucideIcon' }],
|
|
102
|
+
moduleSpecifier: LUCIDE_MODULE_SPECIFIER,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (uniqueExports.length > 0) {
|
|
106
|
+
sourceFile.addImportDeclaration({
|
|
107
|
+
kind: StructureKind.ImportDeclaration,
|
|
108
|
+
namedImports: writeNamedImports(uniqueExports),
|
|
109
|
+
moduleSpecifier: LUCIDE_MODULE_SPECIFIER,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sourceFile.addVariableStatement({
|
|
114
|
+
kind: StructureKind.VariableStatement,
|
|
115
|
+
isExported: true,
|
|
116
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
117
|
+
declarations: [
|
|
118
|
+
{
|
|
119
|
+
name: 'LUCIDE_ICON_REGISTRY',
|
|
120
|
+
type: 'Record<string, LucideIcon>',
|
|
121
|
+
initializer: writeRegistryLiteral(entries),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
sourceFile.formatText({ indentSize: 2, convertTabsToSpaces: true })
|
|
127
|
+
sourceFile.insertText(0, BANNER)
|
|
128
|
+
assertNoSyntacticDiagnostics(project, sourceFile)
|
|
129
|
+
|
|
130
|
+
return normalizeSourceText(sourceFile.getFullText())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { buildLucideRegistrySource }
|
package/src/ai/AiChat.tsx
CHANGED
|
@@ -292,10 +292,10 @@ interface ServerEmittedUiPartRef {
|
|
|
292
292
|
pendingActionId?: string
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
function
|
|
295
|
+
function mapErrorCodeToStatus(
|
|
296
296
|
code: string | undefined,
|
|
297
|
-
): '
|
|
298
|
-
if (!code) return '
|
|
297
|
+
): 'error' | 'warning' {
|
|
298
|
+
if (!code) return 'error'
|
|
299
299
|
// Policy denies that describe a filtered tool or attachment surface a
|
|
300
300
|
// warning alert; caller can still continue. Hard denials (agent_unknown,
|
|
301
301
|
// agent_features_denied, unauthenticated, execution_mode_not_supported,
|
|
@@ -304,8 +304,37 @@ function mapErrorCodeToVariant(
|
|
|
304
304
|
'tool_not_whitelisted',
|
|
305
305
|
'tool_features_denied',
|
|
306
306
|
'attachment_type_not_accepted',
|
|
307
|
+
// Content-safety rejections are soft: the user can rephrase and retry.
|
|
308
|
+
'moderation_blocked',
|
|
309
|
+
'moderation_unavailable',
|
|
307
310
|
])
|
|
308
|
-
return warningCodes.has(code) ? 'warning' : '
|
|
311
|
+
return warningCodes.has(code) ? 'warning' : 'error'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Maps known error codes to a translated, user-safe message. For content-safety
|
|
316
|
+
* codes the raw server message is intentionally generic (no category oracle);
|
|
317
|
+
* the UI shows the friendly localized copy instead of the internal text.
|
|
318
|
+
*/
|
|
319
|
+
function resolveChatErrorMessage(
|
|
320
|
+
code: string | undefined,
|
|
321
|
+
fallback: string,
|
|
322
|
+
translate: (key: string, fallbackText?: string) => string,
|
|
323
|
+
): string {
|
|
324
|
+
switch (code) {
|
|
325
|
+
case 'moderation_blocked':
|
|
326
|
+
return translate(
|
|
327
|
+
'ai_assistant.errors.moderationBlocked',
|
|
328
|
+
'Your message was blocked by the content safety filter. Please rephrase and try again.',
|
|
329
|
+
)
|
|
330
|
+
case 'moderation_unavailable':
|
|
331
|
+
return translate(
|
|
332
|
+
'ai_assistant.errors.moderationUnavailable',
|
|
333
|
+
'The content safety check is temporarily unavailable. Please try again in a moment.',
|
|
334
|
+
)
|
|
335
|
+
default:
|
|
336
|
+
return fallback
|
|
337
|
+
}
|
|
309
338
|
}
|
|
310
339
|
|
|
311
340
|
const MARKDOWN_TYPOGRAPHY_CLASS = cn(
|
|
@@ -1541,7 +1570,7 @@ export function AiChat({
|
|
|
1541
1570
|
const resolvedPlaceholder =
|
|
1542
1571
|
placeholder ?? t('ai_assistant.chat.composerPlaceholder', 'Message the AI agent...')
|
|
1543
1572
|
|
|
1544
|
-
const
|
|
1573
|
+
const errorStatus = mapErrorCodeToStatus(chat.error?.code)
|
|
1545
1574
|
|
|
1546
1575
|
return (
|
|
1547
1576
|
<section
|
|
@@ -1622,7 +1651,7 @@ export function AiChat({
|
|
|
1622
1651
|
</div>
|
|
1623
1652
|
|
|
1624
1653
|
{chat.error ? (
|
|
1625
|
-
<Alert
|
|
1654
|
+
<Alert status={errorStatus} data-ai-chat-error={chat.error.code ?? 'unknown'}>
|
|
1626
1655
|
<AlertTitle>
|
|
1627
1656
|
{t('ai_assistant.chat.errorTitle', 'Agent dispatch failed')}
|
|
1628
1657
|
</AlertTitle>
|
|
@@ -1630,7 +1659,7 @@ export function AiChat({
|
|
|
1630
1659
|
{chat.error.code ? (
|
|
1631
1660
|
<span className="mr-2 font-mono text-xs">{chat.error.code}</span>
|
|
1632
1661
|
) : null}
|
|
1633
|
-
{chat.error.message}
|
|
1662
|
+
{resolveChatErrorMessage(chat.error.code, chat.error.message, t)}
|
|
1634
1663
|
</AlertDescription>
|
|
1635
1664
|
</Alert>
|
|
1636
1665
|
) : null}
|
|
@@ -19,7 +19,7 @@ export function LoopDisabledBanner({ agentId }: { agentId?: string }) {
|
|
|
19
19
|
const t = useT()
|
|
20
20
|
return (
|
|
21
21
|
<Alert
|
|
22
|
-
|
|
22
|
+
status="warning"
|
|
23
23
|
icon={<ShieldOff aria-hidden="true" />}
|
|
24
24
|
data-ai-loop-disabled-banner
|
|
25
25
|
data-ai-loop-disabled-agent-id={agentId}
|
|
@@ -93,6 +93,8 @@ const dict = {
|
|
|
93
93
|
'ai_assistant.chat.emptyTranscript':
|
|
94
94
|
'No messages yet. Ask the agent anything to get started.',
|
|
95
95
|
'ai_assistant.chat.errorTitle': 'Agent dispatch failed',
|
|
96
|
+
'ai_assistant.errors.moderationBlocked':
|
|
97
|
+
'Your message was blocked by the content safety filter. Please rephrase and try again.',
|
|
96
98
|
'ai_assistant.chat.agentTasksTitle': 'Tool calls',
|
|
97
99
|
'ai_assistant.chat.regionLabel': 'AI chat',
|
|
98
100
|
'ai_assistant.chat.send': 'Send message',
|
|
@@ -577,6 +579,35 @@ describe('<AiChat>', () => {
|
|
|
577
579
|
expect(screen.getByText('agent_unknown')).toBeInTheDocument()
|
|
578
580
|
})
|
|
579
581
|
|
|
582
|
+
it('renders the translated message (not the raw server text) for a moderation_blocked envelope', async () => {
|
|
583
|
+
const fetchMock = apiFetch as unknown as jest.Mock
|
|
584
|
+
fetchMock.mockResolvedValueOnce(
|
|
585
|
+
createErrorResponse(400, {
|
|
586
|
+
error: 'Input rejected by the content safety filter.',
|
|
587
|
+
code: 'moderation_blocked',
|
|
588
|
+
}),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
renderWithProviders(<AiChat agent="customers.account_assistant" />, { dict })
|
|
592
|
+
|
|
593
|
+
const textarea = screen.getByLabelText('Message composer') as HTMLTextAreaElement
|
|
594
|
+
fireEvent.change(textarea, { target: { value: 'flagged input' } })
|
|
595
|
+
await act(async () => {
|
|
596
|
+
fireEvent.keyDown(textarea, { key: 'Enter', ctrlKey: true })
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
await waitFor(() => {
|
|
600
|
+
expect(screen.getByText('moderation_blocked')).toBeInTheDocument()
|
|
601
|
+
})
|
|
602
|
+
// The friendly localized copy is shown; the internal server text is not.
|
|
603
|
+
expect(
|
|
604
|
+
screen.getByText(/blocked by the content safety filter/i),
|
|
605
|
+
).toBeInTheDocument()
|
|
606
|
+
expect(screen.queryByText('Input rejected by the content safety filter.')).toBeNull()
|
|
607
|
+
// Content-safety rejections render as a soft warning, not destructive.
|
|
608
|
+
expect(document.querySelector('[data-ai-chat-error="moderation_blocked"]')).not.toBeNull()
|
|
609
|
+
})
|
|
610
|
+
|
|
580
611
|
it('Escape aborts an in-flight streaming response', async () => {
|
|
581
612
|
const fetchMock = apiFetch as unknown as jest.Mock
|
|
582
613
|
// Build a stream we can keep open until the component aborts it.
|
|
@@ -210,7 +210,7 @@ function ConfirmationErrorAlert({
|
|
|
210
210
|
: []
|
|
211
211
|
return (
|
|
212
212
|
<Alert
|
|
213
|
-
|
|
213
|
+
status="warning"
|
|
214
214
|
className="mt-3"
|
|
215
215
|
data-ai-confirmation-error="stale_version"
|
|
216
216
|
>
|
|
@@ -247,7 +247,7 @@ function ConfirmationErrorAlert({
|
|
|
247
247
|
if (code === 'schema_drift') {
|
|
248
248
|
return (
|
|
249
249
|
<Alert
|
|
250
|
-
|
|
250
|
+
status="warning"
|
|
251
251
|
className="mt-3"
|
|
252
252
|
data-ai-confirmation-error="schema_drift"
|
|
253
253
|
>
|
|
@@ -270,7 +270,7 @@ function ConfirmationErrorAlert({
|
|
|
270
270
|
if (code === 'invalid_status') {
|
|
271
271
|
return (
|
|
272
272
|
<Alert
|
|
273
|
-
|
|
273
|
+
status="warning"
|
|
274
274
|
className="mt-3"
|
|
275
275
|
data-ai-confirmation-error="invalid_status"
|
|
276
276
|
>
|
|
@@ -291,7 +291,7 @@ function ConfirmationErrorAlert({
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
return (
|
|
294
|
-
<Alert
|
|
294
|
+
<Alert status="error" className="mt-3" data-ai-confirmation-error={code}>
|
|
295
295
|
<AlertTitle>
|
|
296
296
|
{t('ai_assistant.chat.mutation_cards.confirmation.errorTitle', 'Confirm failed')}
|
|
297
297
|
</AlertTitle>
|
|
@@ -128,7 +128,7 @@ export function FieldDiffCard({ fieldDiff, records }: FieldDiffCardProps) {
|
|
|
128
128
|
afterHeader={afterHeader}
|
|
129
129
|
/>
|
|
130
130
|
) : (
|
|
131
|
-
<Alert
|
|
131
|
+
<Alert status="information">
|
|
132
132
|
<AlertDescription>
|
|
133
133
|
{t(
|
|
134
134
|
'ai_assistant.chat.mutation_cards.diff.empty',
|
|
@@ -145,7 +145,7 @@ export function FieldDiffCard({ fieldDiff, records }: FieldDiffCardProps) {
|
|
|
145
145
|
|
|
146
146
|
if (flat.length === 0) {
|
|
147
147
|
return (
|
|
148
|
-
<Alert
|
|
148
|
+
<Alert status="information" data-ai-field-diff-mode="empty">
|
|
149
149
|
<AlertDescription>
|
|
150
150
|
{t(
|
|
151
151
|
'ai_assistant.chat.mutation_cards.diff.empty',
|
|
@@ -79,7 +79,7 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
79
79
|
|
|
80
80
|
if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {
|
|
81
81
|
return (
|
|
82
|
-
<Alert
|
|
82
|
+
<Alert status="warning" data-ai-mutation-result="partial">
|
|
83
83
|
<AlertTitle>
|
|
84
84
|
{t(
|
|
85
85
|
'ai_assistant.chat.mutation_cards.result.partialTitle',
|
|
@@ -116,7 +116,7 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
116
116
|
const recordId = result?.recordId ?? action.targetRecordId ?? null
|
|
117
117
|
const href = payload.recordHref ?? null
|
|
118
118
|
return (
|
|
119
|
-
<Alert
|
|
119
|
+
<Alert status="success" data-ai-mutation-result="success">
|
|
120
120
|
<AlertTitle>
|
|
121
121
|
{t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}
|
|
122
122
|
</AlertTitle>
|
|
@@ -281,7 +281,7 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
281
281
|
? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))
|
|
282
282
|
: []
|
|
283
283
|
return (
|
|
284
|
-
<Alert
|
|
284
|
+
<Alert status="error" data-ai-mutation-result="failure">
|
|
285
285
|
<AlertTitle>
|
|
286
286
|
{t(
|
|
287
287
|
'ai_assistant.chat.mutation_cards.result.failureTitle',
|
|
@@ -11,7 +11,7 @@ import type { AiUiPartProps } from '../ui-part-registry'
|
|
|
11
11
|
* Consumers that forget to register the real mutation-approval cards still
|
|
12
12
|
* see a humane "Phase 3 pending" state instead of the neutral debug chip the
|
|
13
13
|
* `<AiChat>` fallback uses for genuinely unknown component ids. Uses the
|
|
14
|
-
* shared DS `Alert` primitive with `
|
|
14
|
+
* shared DS `Alert` primitive with `status="information"` — no hardcoded colors.
|
|
15
15
|
*
|
|
16
16
|
* When Step 5.10 lands the real cards, app bootstrappers will call
|
|
17
17
|
* `registerAiUiPart('mutation-preview-card', MutationPreviewCard)` (etc.)
|
|
@@ -23,7 +23,7 @@ export function PendingPhase3Placeholder({ componentId }: AiUiPartProps) {
|
|
|
23
23
|
const t = useT()
|
|
24
24
|
return (
|
|
25
25
|
<Alert
|
|
26
|
-
|
|
26
|
+
status="information"
|
|
27
27
|
data-ai-ui-part-pending-phase3={componentId}
|
|
28
28
|
>
|
|
29
29
|
<AlertTitle>
|
package/src/backend/AppShell.tsx
CHANGED
|
@@ -87,6 +87,7 @@ function isBooleanRecord(value: unknown): value is Record<string, boolean> {
|
|
|
87
87
|
export type ShellLogo = {
|
|
88
88
|
src: string
|
|
89
89
|
alt?: string
|
|
90
|
+
preserveAspectRatio?: boolean
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export type AppShellProps = {
|
|
@@ -186,6 +187,56 @@ function shouldBypassLogoOptimization(src?: string | null): boolean {
|
|
|
186
187
|
return /^https?:\/\//.test(value) || /^\/api\/attachments\/(?:image|file)\//.test(value)
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
function ShellBrandLogo({
|
|
191
|
+
logo,
|
|
192
|
+
brandName,
|
|
193
|
+
unoptimized,
|
|
194
|
+
compact = false,
|
|
195
|
+
mobile = false,
|
|
196
|
+
}: {
|
|
197
|
+
logo?: ShellLogo
|
|
198
|
+
brandName: string
|
|
199
|
+
unoptimized?: boolean
|
|
200
|
+
compact?: boolean
|
|
201
|
+
mobile?: boolean
|
|
202
|
+
}) {
|
|
203
|
+
const src = logo?.src ?? '/open-mercato.svg'
|
|
204
|
+
const alt = logo?.alt ?? brandName
|
|
205
|
+
const isCustomLogo = Boolean(logo?.src)
|
|
206
|
+
const preserveAspectRatio = Boolean(logo?.preserveAspectRatio)
|
|
207
|
+
if (!isCustomLogo || !preserveAspectRatio) {
|
|
208
|
+
return (
|
|
209
|
+
<Image
|
|
210
|
+
src={src}
|
|
211
|
+
alt={alt}
|
|
212
|
+
width={mobile ? 28 : 40}
|
|
213
|
+
height={mobile ? 28 : 40}
|
|
214
|
+
className={`${mobile ? 'rounded' : 'rounded-full'} shrink-0 object-cover`}
|
|
215
|
+
unoptimized={unoptimized ? true : undefined}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const width = compact ? 40 : mobile ? 96 : 120
|
|
221
|
+
const height = mobile ? 28 : 40
|
|
222
|
+
const className = compact
|
|
223
|
+
? 'h-10 max-w-10 w-auto shrink-0 object-contain'
|
|
224
|
+
: mobile
|
|
225
|
+
? 'h-7 max-w-24 w-auto shrink-0 object-contain'
|
|
226
|
+
: 'h-10 max-w-[120px] w-auto shrink-0 object-contain'
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<Image
|
|
230
|
+
src={src}
|
|
231
|
+
alt={alt}
|
|
232
|
+
width={width}
|
|
233
|
+
height={height}
|
|
234
|
+
className={className}
|
|
235
|
+
unoptimized={unoptimized ? true : undefined}
|
|
236
|
+
/>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
189
240
|
function mergeSidebarItemsWithInjected(
|
|
190
241
|
items: SidebarItem[],
|
|
191
242
|
injectedItems: InjectionMenuItem[],
|
|
@@ -760,7 +811,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
760
811
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
761
812
|
aria-label={t('appShell.goToDashboard')}
|
|
762
813
|
>
|
|
763
|
-
<
|
|
814
|
+
<ShellBrandLogo logo={resolvedLogo} brandName={resolvedBrandName} compact={compact} unoptimized={resolvedLogoBypassesOptimization} />
|
|
764
815
|
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
765
816
|
</Link>
|
|
766
817
|
</div>
|
|
@@ -893,7 +944,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
893
944
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
894
945
|
aria-label={t('appShell.goToDashboard')}
|
|
895
946
|
>
|
|
896
|
-
<
|
|
947
|
+
<ShellBrandLogo logo={resolvedLogo} brandName={resolvedBrandName} compact={compact} unoptimized={resolvedLogoBypassesOptimization} />
|
|
897
948
|
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
898
949
|
</Link>
|
|
899
950
|
</div>
|
|
@@ -959,7 +1010,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
959
1010
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
960
1011
|
aria-label={t('appShell.goToDashboard')}
|
|
961
1012
|
>
|
|
962
|
-
<
|
|
1013
|
+
<ShellBrandLogo logo={resolvedLogo} brandName={resolvedBrandName} compact={compact} unoptimized={resolvedLogoBypassesOptimization} />
|
|
963
1014
|
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
964
1015
|
</Link>
|
|
965
1016
|
</div>
|
|
@@ -1458,7 +1509,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1458
1509
|
<aside className="absolute left-0 top-0 flex h-full w-[280px] max-w-[85vw] flex-col bg-background border-r shadow-lg overflow-hidden">
|
|
1459
1510
|
<div className="shrink-0 flex items-center justify-between gap-2 border-b px-4 py-3">
|
|
1460
1511
|
<Link href="/backend" className="flex items-center gap-2 min-w-0 text-sm font-semibold" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>
|
|
1461
|
-
<
|
|
1512
|
+
<ShellBrandLogo logo={resolvedLogo} brandName={resolvedBrandName} mobile unoptimized={resolvedLogoBypassesOptimization} />
|
|
1462
1513
|
<span className="truncate">{resolvedBrandName}</span>
|
|
1463
1514
|
</Link>
|
|
1464
1515
|
<IconButton variant="ghost" size="sm" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>
|
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { apiCall } from './utils/apiCall'
|
|
4
|
+
import { clearAllPerspectiveState } from './perspectiveState'
|
|
4
5
|
|
|
5
6
|
export const AUTH_IDENTITY_STORAGE_KEY = 'om:auth:identity'
|
|
6
7
|
export const AUTH_IDENTITY_BROADCAST_CHANNEL = 'om-auth-identity'
|
|
8
|
+
export const AUTH_IDENTITY_USER_STORAGE_KEY = 'om:auth:identity:user'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Purge browser-local DataTable perspective state whenever the backend renders for a
|
|
12
|
+
* different user than the one it last rendered for (#4185).
|
|
13
|
+
*
|
|
14
|
+
* The login form also purges on submit, but that only fires when its client handler
|
|
15
|
+
* runs: a form submitted before hydration, an SSO/magic-link return, or any other
|
|
16
|
+
* entry point into the backend bypasses it entirely and the previous account's unsaved
|
|
17
|
+
* column widths carry over. Anchoring the purge to the observed server identity covers
|
|
18
|
+
* every route into the backend instead of one form.
|
|
19
|
+
*
|
|
20
|
+
* Missing identity metadata is treated as a legacy browser state and purged once:
|
|
21
|
+
* snapshots may predate this marker and belong to a different account. After the
|
|
22
|
+
* marker is recorded, an account's own unsaved widths survive a plain reload.
|
|
23
|
+
*/
|
|
24
|
+
function purgePerspectiveStateOnIdentityChange(serverUserId: string | null): void {
|
|
25
|
+
if (typeof window === 'undefined' || !serverUserId) return
|
|
26
|
+
try {
|
|
27
|
+
const lastSeenUserId = window.localStorage.getItem(AUTH_IDENTITY_USER_STORAGE_KEY)
|
|
28
|
+
if (lastSeenUserId === serverUserId) return
|
|
29
|
+
clearAllPerspectiveState()
|
|
30
|
+
window.localStorage.setItem(AUTH_IDENTITY_USER_STORAGE_KEY, serverUserId)
|
|
31
|
+
} catch {
|
|
32
|
+
// private mode / quota errors are non-fatal
|
|
33
|
+
}
|
|
34
|
+
}
|
|
7
35
|
|
|
8
36
|
type FeatureCheckResponse = {
|
|
9
37
|
ok: boolean
|
|
@@ -24,6 +52,7 @@ export const __reload = {
|
|
|
24
52
|
export function AuthSessionGuard({ serverUserId }: AuthSessionGuardProps) {
|
|
25
53
|
React.useEffect(() => {
|
|
26
54
|
if (typeof window === 'undefined') return
|
|
55
|
+
purgePerspectiveStateOnIdentityChange(serverUserId)
|
|
27
56
|
let cancelled = false
|
|
28
57
|
let reloadScheduled = false
|
|
29
58
|
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { apiCall } from './utils/apiCall'
|
|
5
5
|
import { subscribeOrganizationScopeChanged, getCurrentOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/organizationEvents'
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
BackendChromeCurrentOrganization,
|
|
8
|
+
BackendChromePayload,
|
|
9
|
+
} from '@open-mercato/shared/modules/navigation/backendChrome'
|
|
7
10
|
|
|
8
11
|
type BackendChromeContextValue = {
|
|
9
12
|
payload: BackendChromePayload | null
|
|
@@ -99,3 +102,18 @@ export function useBackendChrome(): BackendChromeContextValue {
|
|
|
99
102
|
refresh: async () => {},
|
|
100
103
|
}
|
|
101
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The organization the backend chrome is currently scoped to, for UI that needs to label
|
|
108
|
+
* "you are viewing: <name>".
|
|
109
|
+
*
|
|
110
|
+
* Reads the payload this provider already holds, so it costs no additional request. Returns `null`
|
|
111
|
+
* under an all-organizations selection, outside a provider, or before the payload has arrived — treat
|
|
112
|
+
* it as "unknown", not as "no organization".
|
|
113
|
+
*
|
|
114
|
+
* Prefer this over `payload.brand`, which only populates when the organization has a logo configured.
|
|
115
|
+
*/
|
|
116
|
+
export function useCurrentOrganization(): BackendChromeCurrentOrganization | null {
|
|
117
|
+
const { payload } = useBackendChrome()
|
|
118
|
+
return payload?.currentOrganization ?? null
|
|
119
|
+
}
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -107,6 +107,7 @@ import type { InjectionFieldDefinition, FieldContext } from '@open-mercato/share
|
|
|
107
107
|
import { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
108
108
|
import { evaluateInjectedVisibility } from './injection/visibility-utils'
|
|
109
109
|
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
110
|
+
import { crudFormExtensionSpotId, extensionSpotChildId } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
110
111
|
import { RichEditor, type RichEditorLabels } from '../primitives/rich-editor'
|
|
111
112
|
import MarkdownField from './inputs/MarkdownField'
|
|
112
113
|
|
|
@@ -816,7 +817,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
816
817
|
if (injectionSpotId) return injectionSpotId
|
|
817
818
|
if (resolvedEntityIds.length) {
|
|
818
819
|
const normalized = resolvedEntityIds[0].replace(/[:]+/g, '.')
|
|
819
|
-
return
|
|
820
|
+
return crudFormExtensionSpotId(normalized)
|
|
820
821
|
}
|
|
821
822
|
return undefined
|
|
822
823
|
}, [injectionSpotId, resolvedEntityIds])
|
|
@@ -825,7 +826,9 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
825
826
|
if (resolvedEntityIds.length) return ComponentReplacementHandles.crudForm(resolvedEntityIds[0].replace(/[:]+/g, '.'))
|
|
826
827
|
return ComponentReplacementHandles.crudForm('unknown')
|
|
827
828
|
}, [replacementHandle, resolvedEntityIds])
|
|
828
|
-
const headerInjectionSpotId = resolvedInjectionSpotId
|
|
829
|
+
const headerInjectionSpotId = resolvedInjectionSpotId
|
|
830
|
+
? extensionSpotChildId(resolvedInjectionSpotId, 'header')
|
|
831
|
+
: undefined
|
|
829
832
|
|
|
830
833
|
const recordId = React.useMemo(() => {
|
|
831
834
|
const raw = values.id
|
|
@@ -1083,7 +1086,9 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1083
1086
|
triggerOnLoad: true,
|
|
1084
1087
|
})
|
|
1085
1088
|
const { widgets: injectedFieldWidgets } = useInjectionDataWidgets(
|
|
1086
|
-
resolvedInjectionSpotId
|
|
1089
|
+
resolvedInjectionSpotId
|
|
1090
|
+
? extensionSpotChildId(resolvedInjectionSpotId, 'fields')
|
|
1091
|
+
: '__disabled__:fields'
|
|
1087
1092
|
)
|
|
1088
1093
|
|
|
1089
1094
|
const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? '', injectionWidgets)
|
|
@@ -2402,10 +2407,19 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2402
2407
|
|
|
2403
2408
|
const handleFieldsetSelectionChange = React.useCallback(
|
|
2404
2409
|
(entityId: string, nextCode: string | null) => {
|
|
2410
|
+
// The fieldset selector never renders an empty option, so a user can never
|
|
2411
|
+
// pick "no fieldset". An empty/null value here only comes from Radix's
|
|
2412
|
+
// hidden native <select>, which fires a spurious onChange when the
|
|
2413
|
+
// controlled value is changed programmatically after mount (e.g. when the
|
|
2414
|
+
// persisted customFieldsetCode arrives via async initialValues and the
|
|
2415
|
+
// binding hydrates the selection). Honoring that reset would wipe both the
|
|
2416
|
+
// selection and the bound value, collapsing the form back to the default
|
|
2417
|
+
// fieldset (#2646). Ignore it.
|
|
2418
|
+
if (!nextCode) return
|
|
2405
2419
|
setCfFieldsetSelections((prev) => ({ ...prev, [entityId]: nextCode }))
|
|
2406
2420
|
const bindingKey = customFieldsetBindings?.[entityId]?.valueKey
|
|
2407
2421
|
if (bindingKey) {
|
|
2408
|
-
setValue(bindingKey, nextCode
|
|
2422
|
+
setValue(bindingKey, nextCode)
|
|
2409
2423
|
}
|
|
2410
2424
|
},
|
|
2411
2425
|
[customFieldsetBindings, setValue],
|