@open-mercato/ui 0.4.6-develop-ce2a0728a5 → 0.4.6-develop-4d77832982
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/AGENTS.md +16 -0
- package/dist/backend/CrudForm.js +138 -17
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +297 -24
- package/dist/backend/DataTable.js.map +3 -3
- package/dist/backend/detail/ActivitiesSection.js +11 -1
- package/dist/backend/detail/ActivitiesSection.js.map +2 -2
- package/dist/backend/detail/AddressesSection.js +11 -1
- package/dist/backend/detail/AddressesSection.js.map +2 -2
- package/dist/backend/detail/AttachmentsSection.js +11 -1
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/CustomDataSection.js +11 -1
- package/dist/backend/detail/CustomDataSection.js.map +2 -2
- package/dist/backend/detail/DetailFieldsSection.js +11 -1
- package/dist/backend/detail/DetailFieldsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +11 -1
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/detail/TagsSection.js +11 -1
- package/dist/backend/detail/TagsSection.js.map +2 -2
- package/dist/backend/injection/ComponentOverrideProvider.js +54 -0
- package/dist/backend/injection/ComponentOverrideProvider.js.map +7 -0
- package/dist/backend/injection/InjectedField.js +166 -0
- package/dist/backend/injection/InjectedField.js.map +7 -0
- package/dist/backend/injection/spotIds.js +5 -1
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/injection/useRegisteredComponent.js +89 -0
- package/dist/backend/injection/useRegisteredComponent.js.map +7 -0
- package/dist/backend/injection/visibility-utils.js +29 -0
- package/dist/backend/injection/visibility-utils.js.map +7 -0
- package/package.json +2 -2
- package/src/backend/AGENTS.md +7 -0
- package/src/backend/CrudForm.tsx +144 -16
- package/src/backend/DataTable.tsx +342 -22
- package/src/backend/__tests__/DataTable.extensions.test.tsx +115 -0
- package/src/backend/__tests__/DataTable.namespaces.test.ts +32 -0
- package/src/backend/__tests__/component-replacement.test.tsx +232 -0
- package/src/backend/detail/ActivitiesSection.tsx +17 -1
- package/src/backend/detail/AddressesSection.tsx +17 -1
- package/src/backend/detail/AttachmentsSection.tsx +17 -1
- package/src/backend/detail/CustomDataSection.tsx +17 -1
- package/src/backend/detail/DetailFieldsSection.tsx +17 -1
- package/src/backend/detail/NotesSection.tsx +17 -1
- package/src/backend/detail/TagsSection.tsx +17 -1
- package/src/backend/injection/ComponentOverrideProvider.tsx +65 -0
- package/src/backend/injection/InjectedField.tsx +194 -0
- package/src/backend/injection/spotIds.ts +4 -0
- package/src/backend/injection/useRegisteredComponent.tsx +106 -0
- package/src/backend/injection/visibility-utils.ts +31 -0
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -69,6 +69,11 @@ import { useInjectionSpotEvents, InjectionSpot, useInjectionWidgets } from './in
|
|
|
69
69
|
import { dispatchBackendMutationError } from './injection/mutationEvents'
|
|
70
70
|
import { VersionHistoryAction } from './version-history/VersionHistoryAction'
|
|
71
71
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
72
|
+
import { useInjectionDataWidgets } from './injection/useInjectionDataWidgets'
|
|
73
|
+
import { InjectedField } from './injection/InjectedField'
|
|
74
|
+
import type { InjectionFieldDefinition, FieldContext } from '@open-mercato/shared/modules/widgets/injection'
|
|
75
|
+
import { evaluateInjectedVisibility } from './injection/visibility-utils'
|
|
76
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
72
77
|
|
|
73
78
|
// Stable empty options array to avoid creating a new [] every render
|
|
74
79
|
const EMPTY_OPTIONS: CrudFieldOption[] = []
|
|
@@ -203,6 +208,7 @@ export type CrudFormProps<TValues extends Record<string, unknown>> = {
|
|
|
203
208
|
customFieldsetBindings?: Record<string, { valueKey: string }>
|
|
204
209
|
// Optional injection spot ID for widget injection
|
|
205
210
|
injectionSpotId?: string
|
|
211
|
+
replacementHandle?: string
|
|
206
212
|
}
|
|
207
213
|
|
|
208
214
|
// Group-level custom component context
|
|
@@ -228,6 +234,18 @@ export type CrudFormGroup = {
|
|
|
228
234
|
bare?: boolean
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
function readByDotPath(source: Record<string, unknown> | undefined, path: string): unknown {
|
|
238
|
+
if (!source || !path) return undefined
|
|
239
|
+
if (Object.prototype.hasOwnProperty.call(source, path)) return source[path]
|
|
240
|
+
const segments = path.split('.').filter(Boolean)
|
|
241
|
+
let current: unknown = source
|
|
242
|
+
for (const segment of segments) {
|
|
243
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) return undefined
|
|
244
|
+
current = (current as Record<string, unknown>)[segment]
|
|
245
|
+
}
|
|
246
|
+
return current
|
|
247
|
+
}
|
|
248
|
+
|
|
231
249
|
const FIELDSET_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
232
250
|
layers: Layers,
|
|
233
251
|
tag: Tag,
|
|
@@ -319,6 +337,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
319
337
|
contentHeader,
|
|
320
338
|
customFieldsetBindings,
|
|
321
339
|
injectionSpotId,
|
|
340
|
+
replacementHandle,
|
|
322
341
|
}: CrudFormProps<TValues>) {
|
|
323
342
|
// Ensure module field components are registered (client-side)
|
|
324
343
|
React.useEffect(() => { loadGeneratedFieldRegistrations().catch(() => {}) }, [])
|
|
@@ -390,6 +409,11 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
390
409
|
}
|
|
391
410
|
return undefined
|
|
392
411
|
}, [injectionSpotId, resolvedEntityIds])
|
|
412
|
+
const resolvedReplacementHandle = React.useMemo(() => {
|
|
413
|
+
if (replacementHandle) return replacementHandle
|
|
414
|
+
if (resolvedEntityIds.length) return ComponentReplacementHandles.crudForm(resolvedEntityIds[0].replace(/[:]+/g, '.'))
|
|
415
|
+
return ComponentReplacementHandles.crudForm('unknown')
|
|
416
|
+
}, [replacementHandle, resolvedEntityIds])
|
|
393
417
|
const headerInjectionSpotId = resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:header` : undefined
|
|
394
418
|
|
|
395
419
|
const recordId = React.useMemo(() => {
|
|
@@ -425,6 +449,9 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
425
449
|
context: injectionContext,
|
|
426
450
|
triggerOnLoad: true,
|
|
427
451
|
})
|
|
452
|
+
const { widgets: injectedFieldWidgets } = useInjectionDataWidgets(
|
|
453
|
+
resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:fields` : '__disabled__:fields'
|
|
454
|
+
)
|
|
428
455
|
|
|
429
456
|
const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? '', injectionWidgets)
|
|
430
457
|
const extendedInjectionEventsEnabled = CRUDFORM_EXTENDED_EVENTS_ENABLED && Boolean(resolvedInjectionSpotId)
|
|
@@ -983,12 +1010,69 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
983
1010
|
resolvedEntityIds,
|
|
984
1011
|
])
|
|
985
1012
|
|
|
1013
|
+
const injectedFieldDefinitions = React.useMemo<InjectionFieldDefinition[]>(() => {
|
|
1014
|
+
const definitions: InjectionFieldDefinition[] = []
|
|
1015
|
+
for (const widget of injectedFieldWidgets) {
|
|
1016
|
+
if (!('fields' in widget)) continue
|
|
1017
|
+
for (const field of widget.fields ?? []) {
|
|
1018
|
+
definitions.push(field as InjectionFieldDefinition)
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return definitions
|
|
1022
|
+
}, [injectedFieldWidgets])
|
|
1023
|
+
|
|
1024
|
+
const injectedFieldContext = React.useMemo<FieldContext>(() => {
|
|
1025
|
+
const recordValues = values as Record<string, unknown>
|
|
1026
|
+
const organizationId = typeof recordValues.organizationId === 'string' ? recordValues.organizationId : null
|
|
1027
|
+
const tenantId = typeof recordValues.tenantId === 'string' ? recordValues.tenantId : null
|
|
1028
|
+
const userId = typeof recordValues.userId === 'string' ? recordValues.userId : null
|
|
1029
|
+
return {
|
|
1030
|
+
organizationId,
|
|
1031
|
+
tenantId,
|
|
1032
|
+
userId,
|
|
1033
|
+
record: recordValues,
|
|
1034
|
+
}
|
|
1035
|
+
}, [values])
|
|
1036
|
+
|
|
1037
|
+
const hiddenInjectedFieldIds = React.useMemo(() => {
|
|
1038
|
+
const hidden = new Set<string>()
|
|
1039
|
+
for (const definition of injectedFieldDefinitions) {
|
|
1040
|
+
if (!evaluateInjectedVisibility(definition.visibleWhen, values as Record<string, unknown>, injectedFieldContext)) {
|
|
1041
|
+
hidden.add(definition.id)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return hidden
|
|
1045
|
+
}, [injectedFieldContext, injectedFieldDefinitions, values])
|
|
1046
|
+
const injectedFieldIdSet = React.useMemo(
|
|
1047
|
+
() => new Set(injectedFieldDefinitions.map((definition) => definition.id)),
|
|
1048
|
+
[injectedFieldDefinitions],
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
const injectedCrudFields = React.useMemo<CrudField[]>(() => {
|
|
1052
|
+
return injectedFieldDefinitions.map((definition) => ({
|
|
1053
|
+
id: definition.id,
|
|
1054
|
+
label: definition.label,
|
|
1055
|
+
type: 'custom',
|
|
1056
|
+
readOnly: definition.readOnly,
|
|
1057
|
+
component: ({ value, setValue, values: formValues }) => (
|
|
1058
|
+
<InjectedField
|
|
1059
|
+
field={definition}
|
|
1060
|
+
value={value}
|
|
1061
|
+
onChange={(_, nextValue) => setValue(nextValue)}
|
|
1062
|
+
context={injectedFieldContext}
|
|
1063
|
+
formData={(formValues ?? values) as Record<string, unknown>}
|
|
1064
|
+
/>
|
|
1065
|
+
),
|
|
1066
|
+
}))
|
|
1067
|
+
}, [injectedFieldContext, injectedFieldDefinitions, values])
|
|
1068
|
+
|
|
986
1069
|
const allFields = React.useMemo(() => {
|
|
987
|
-
|
|
988
|
-
|
|
1070
|
+
const base = [...fields, ...injectedCrudFields]
|
|
1071
|
+
if (!cfFields.length) return base
|
|
1072
|
+
const provided = new Set(base.map(f => f.id))
|
|
989
1073
|
const extras = cfFields.filter(f => !provided.has(f.id))
|
|
990
|
-
return [...
|
|
991
|
-
}, [fields, cfFields])
|
|
1074
|
+
return [...base, ...extras]
|
|
1075
|
+
}, [fields, injectedCrudFields, cfFields])
|
|
992
1076
|
|
|
993
1077
|
const fieldById = React.useMemo(() => {
|
|
994
1078
|
return new globalThis.Map(allFields.map((f) => [f.id, f]))
|
|
@@ -1020,12 +1104,32 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1020
1104
|
return pairs.map((p) => p.group)
|
|
1021
1105
|
}, [injectionWidgets, injectionContext, pending, setValues, values])
|
|
1022
1106
|
|
|
1023
|
-
const
|
|
1107
|
+
const groupsWithInjectedFields = React.useMemo(() => {
|
|
1108
|
+
if (!groups || groups.length === 0 || injectedFieldDefinitions.length === 0) return groups
|
|
1109
|
+
const cloned = groups.map((group) => ({ ...group, fields: [...(group.fields ?? [])] }))
|
|
1110
|
+
const fallbackIndex = cloned.length - 1
|
|
1111
|
+
for (const definition of injectedFieldDefinitions) {
|
|
1112
|
+
const targetIndex = cloned.findIndex((group) => group.id === definition.group)
|
|
1113
|
+
const index = targetIndex >= 0 ? targetIndex : fallbackIndex
|
|
1114
|
+
if (targetIndex < 0 && process.env.NODE_ENV !== 'production') {
|
|
1115
|
+
console.warn(`[CrudForm] Injected field "${definition.id}" targets group "${definition.group}" which does not exist. Appended to last group.`)
|
|
1116
|
+
}
|
|
1117
|
+
if (index < 0) continue
|
|
1118
|
+
const fieldEntries = cloned[index].fields ?? []
|
|
1119
|
+
if (!fieldEntries.some((entry) => typeof entry === 'string' && entry === definition.id)) {
|
|
1120
|
+
fieldEntries.push(definition.id)
|
|
1121
|
+
}
|
|
1122
|
+
cloned[index].fields = fieldEntries
|
|
1123
|
+
}
|
|
1124
|
+
return cloned
|
|
1125
|
+
}, [groups, injectedFieldDefinitions])
|
|
1126
|
+
|
|
1127
|
+
const shouldAutoGroup = (!groupsWithInjectedFields || groupsWithInjectedFields.length === 0) && injectionGroupCards.length > 0
|
|
1024
1128
|
const resolvedGroupsForLayout = React.useMemo(() => {
|
|
1025
|
-
const baseGroups =
|
|
1129
|
+
const baseGroups = groupsWithInjectedFields && groupsWithInjectedFields.length ? groupsWithInjectedFields : []
|
|
1026
1130
|
const autoGroup = shouldAutoGroup ? [{ id: '__auto-fields__', fields: allFields }] as CrudFormGroup[] : []
|
|
1027
1131
|
return [...(baseGroups.length ? baseGroups : autoGroup), ...injectionGroupCards]
|
|
1028
|
-
}, [allFields,
|
|
1132
|
+
}, [allFields, groupsWithInjectedFields, injectionGroupCards, shouldAutoGroup])
|
|
1029
1133
|
const useGroupedLayout = resolvedGroupsForLayout.length > 0
|
|
1030
1134
|
const stackedInjectionWidgets = React.useMemo(
|
|
1031
1135
|
() => (injectionWidgets ?? []).filter((widget) => (widget.placement?.kind ?? 'stack') === 'stack'),
|
|
@@ -1261,7 +1365,15 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1261
1365
|
initialValuesSnapshotRef.current = snapshot
|
|
1262
1366
|
let mergedValues: CrudFormValues<TValues> | null = null
|
|
1263
1367
|
setValues((prev) => {
|
|
1264
|
-
|
|
1368
|
+
const merged = { ...prev, ...initialValues } as CrudFormValues<TValues>
|
|
1369
|
+
for (const definition of injectedFieldDefinitions) {
|
|
1370
|
+
if (merged[definition.id] !== undefined) continue
|
|
1371
|
+
const extracted = readByDotPath(initialValues as Record<string, unknown>, definition.id)
|
|
1372
|
+
if (extracted !== undefined) {
|
|
1373
|
+
;(merged as Record<string, unknown>)[definition.id] = extracted
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
mergedValues = merged
|
|
1265
1377
|
return mergedValues
|
|
1266
1378
|
})
|
|
1267
1379
|
if (!extendedInjectionEventsEnabled || !mergedValues) return
|
|
@@ -1284,7 +1396,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1284
1396
|
return () => {
|
|
1285
1397
|
cancelled = true
|
|
1286
1398
|
}
|
|
1287
|
-
}, [extendedInjectionEventsEnabled, initialValues, triggerInjectionEvent])
|
|
1399
|
+
}, [extendedInjectionEventsEnabled, initialValues, injectedFieldDefinitions, triggerInjectionEvent])
|
|
1288
1400
|
|
|
1289
1401
|
const buildFieldsetEditorHref = React.useCallback(
|
|
1290
1402
|
(includeViewParam: boolean) => {
|
|
@@ -1338,6 +1450,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1338
1450
|
for (const field of allFields) {
|
|
1339
1451
|
if (!field.required) continue
|
|
1340
1452
|
if (field.disabled) continue
|
|
1453
|
+
if (hiddenInjectedFieldIds.has(field.id)) continue
|
|
1341
1454
|
const v = values[field.id]
|
|
1342
1455
|
const isArray = Array.isArray(v)
|
|
1343
1456
|
const isString = typeof v === 'string'
|
|
@@ -1402,9 +1515,18 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1402
1515
|
}
|
|
1403
1516
|
}
|
|
1404
1517
|
|
|
1518
|
+
const widgetValues = { ...(values as Record<string, unknown>) }
|
|
1519
|
+
for (const hiddenId of hiddenInjectedFieldIds) {
|
|
1520
|
+
delete widgetValues[hiddenId]
|
|
1521
|
+
}
|
|
1522
|
+
const coreValues = { ...widgetValues }
|
|
1523
|
+
for (const injectedId of injectedFieldIdSet) {
|
|
1524
|
+
delete coreValues[injectedId]
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1405
1527
|
let parsedValues: TValues
|
|
1406
1528
|
if (schema) {
|
|
1407
|
-
const res = schema.safeParse(
|
|
1529
|
+
const res = schema.safeParse(coreValues)
|
|
1408
1530
|
if (!res.success) {
|
|
1409
1531
|
const fieldErrors: Record<string, string> = {}
|
|
1410
1532
|
res.error.issues.forEach((issue) => {
|
|
@@ -1420,14 +1542,20 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1420
1542
|
}
|
|
1421
1543
|
parsedValues = res.data
|
|
1422
1544
|
} else {
|
|
1423
|
-
parsedValues =
|
|
1545
|
+
parsedValues = coreValues as TValues
|
|
1424
1546
|
}
|
|
1425
|
-
let submitValues =
|
|
1547
|
+
let submitValues = widgetValues as TValues
|
|
1548
|
+
let coreSubmitValues = parsedValues
|
|
1426
1549
|
if (extendedInjectionEventsEnabled) {
|
|
1427
1550
|
try {
|
|
1428
1551
|
const result = await triggerInjectionEvent('transformFormData', submitValues, injectionContext)
|
|
1429
1552
|
if (result.data) {
|
|
1430
1553
|
submitValues = result.data as TValues
|
|
1554
|
+
const projectedCoreValues = { ...(result.data as Record<string, unknown>) }
|
|
1555
|
+
for (const injectedId of injectedFieldIdSet) {
|
|
1556
|
+
delete projectedCoreValues[injectedId]
|
|
1557
|
+
}
|
|
1558
|
+
coreSubmitValues = schema ? schema.parse(projectedCoreValues) : (projectedCoreValues as TValues)
|
|
1431
1559
|
}
|
|
1432
1560
|
} catch (err) {
|
|
1433
1561
|
console.error('[CrudForm] Error in transformFormData:', err)
|
|
@@ -1492,10 +1620,10 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1492
1620
|
try {
|
|
1493
1621
|
if (injectionRequestHeaders && Object.keys(injectionRequestHeaders).length > 0) {
|
|
1494
1622
|
await withScopedApiRequestHeaders(injectionRequestHeaders, async () => {
|
|
1495
|
-
await onSubmit?.(
|
|
1623
|
+
await onSubmit?.(coreSubmitValues)
|
|
1496
1624
|
})
|
|
1497
1625
|
} else {
|
|
1498
|
-
await onSubmit?.(
|
|
1626
|
+
await onSubmit?.(coreSubmitValues)
|
|
1499
1627
|
}
|
|
1500
1628
|
|
|
1501
1629
|
// Trigger onAfterSave event for injection widgets
|
|
@@ -1958,7 +2086,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1958
2086
|
const hasSecondaryColumn = col2Content.length > 0
|
|
1959
2087
|
|
|
1960
2088
|
return (
|
|
1961
|
-
<div className="space-y-4" ref={rootRef}>
|
|
2089
|
+
<div className="space-y-4" ref={rootRef} data-component-handle={resolvedReplacementHandle}>
|
|
1962
2090
|
{!embedded ? (
|
|
1963
2091
|
<FormHeader
|
|
1964
2092
|
mode="edit"
|
|
@@ -2030,7 +2158,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2030
2158
|
|
|
2031
2159
|
// Default single-card layout (compatible with previous API)
|
|
2032
2160
|
return (
|
|
2033
|
-
<div className="space-y-4" ref={rootRef}>
|
|
2161
|
+
<div className="space-y-4" ref={rootRef} data-component-handle={resolvedReplacementHandle}>
|
|
2034
2162
|
{!embedded ? (
|
|
2035
2163
|
<FormHeader
|
|
2036
2164
|
mode="edit"
|