@open-mercato/ui 0.6.6-develop.6410.1.2ad881013e → 0.6.6-develop.6412.1.e99f3c32d9
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6412.1.e99f3c32d9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,13 +155,13 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.6-develop.6412.1.e99f3c32d9",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
164
|
+
"@open-mercato/shared": "0.6.6-develop.6412.1.e99f3c32d9",
|
|
165
165
|
"@testing-library/dom": "^10.4.1",
|
|
166
166
|
"@testing-library/jest-dom": "^6.9.1",
|
|
167
167
|
"@testing-library/react": "^16.3.1",
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -103,6 +103,7 @@ import { SortableGroupHandleProvider, type SortableGroupHandleProps } from './cr
|
|
|
103
103
|
import { useGroupOrder } from './crud/useGroupOrder'
|
|
104
104
|
import { InjectedField } from './injection/InjectedField'
|
|
105
105
|
import type { InjectionFieldDefinition, FieldContext } from '@open-mercato/shared/modules/widgets/injection'
|
|
106
|
+
import { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
106
107
|
import { evaluateInjectedVisibility } from './injection/visibility-utils'
|
|
107
108
|
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
108
109
|
import { RichEditor, type RichEditorLabels } from '../primitives/rich-editor'
|
|
@@ -1762,21 +1763,27 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1762
1763
|
)
|
|
1763
1764
|
|
|
1764
1765
|
const injectedCrudFields = React.useMemo<CrudField[]>(() => {
|
|
1765
|
-
return injectedFieldDefinitions.map((definition) =>
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1766
|
+
return injectedFieldDefinitions.map((definition) => {
|
|
1767
|
+
// InjectedField renders its own i18n-resolved <Label> for every field type
|
|
1768
|
+
// except a custom component (type 'custom' + customComponent). Suppress the
|
|
1769
|
+
// CrudForm row label in those cases so it is not rendered twice (#3047).
|
|
1770
|
+
const injectedFieldRendersOwnLabel = !(definition.type === 'custom' && definition.customComponent)
|
|
1771
|
+
return {
|
|
1772
|
+
id: definition.id,
|
|
1773
|
+
label: injectedFieldRendersOwnLabel ? '' : definition.label,
|
|
1774
|
+
type: 'custom',
|
|
1775
|
+
readOnly: definition.readOnly,
|
|
1776
|
+
component: ({ value, setValue, values: formValues }) => (
|
|
1777
|
+
<InjectedField
|
|
1778
|
+
field={definition}
|
|
1779
|
+
value={value}
|
|
1780
|
+
onChange={(_, nextValue) => setValue(nextValue)}
|
|
1781
|
+
context={injectedFieldContext}
|
|
1782
|
+
formData={(formValues ?? values) as Record<string, unknown>}
|
|
1783
|
+
/>
|
|
1784
|
+
),
|
|
1785
|
+
}
|
|
1786
|
+
})
|
|
1780
1787
|
}, [injectedFieldContext, injectedFieldDefinitions, values])
|
|
1781
1788
|
|
|
1782
1789
|
const allFields = React.useMemo(() => {
|
|
@@ -1997,10 +2004,18 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1997
2004
|
}
|
|
1998
2005
|
if (index < 0) continue
|
|
1999
2006
|
const fieldEntries = cloned[index].fields ?? []
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2007
|
+
const alreadyPresent = fieldEntries.some((entry) => {
|
|
2008
|
+
const entryId = typeof entry === 'string' ? entry : entry.id
|
|
2009
|
+
return entryId === definition.id
|
|
2010
|
+
})
|
|
2011
|
+
cloned[index].fields = alreadyPresent
|
|
2012
|
+
? fieldEntries
|
|
2013
|
+
: insertByInjectionPlacement(
|
|
2014
|
+
fieldEntries,
|
|
2015
|
+
definition.id,
|
|
2016
|
+
definition.placement,
|
|
2017
|
+
(entry) => (typeof entry === 'string' ? entry : entry.id),
|
|
2018
|
+
)
|
|
2004
2019
|
}
|
|
2005
2020
|
return cloned
|
|
2006
2021
|
}, [groups, injectedFieldDefinitions])
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
jest.setTimeout(15000)
|
|
3
|
+
|
|
4
|
+
// Injected field definitions for the CrudForm `:fields` injection spot. The
|
|
5
|
+
// hook is mocked so the test drives `injectedFieldDefinitions` directly without
|
|
6
|
+
// standing up the full injection registry/bootstrap. See issue #3047.
|
|
7
|
+
let injectedFieldWidgets: Array<{ fields: unknown[] }> = []
|
|
8
|
+
|
|
9
|
+
jest.mock('next/navigation', () => ({
|
|
10
|
+
useRouter: () => ({ push: () => {} }),
|
|
11
|
+
usePathname: () => '/',
|
|
12
|
+
useSearchParams: () => new URLSearchParams(),
|
|
13
|
+
}))
|
|
14
|
+
jest.mock('remark-gfm', () => ({ __esModule: true, default: {} }))
|
|
15
|
+
jest.mock('../injection/InjectionSpot', () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
InjectionSpot: () => null,
|
|
18
|
+
useInjectionWidgets: () => ({ widgets: [], loading: false, error: null }),
|
|
19
|
+
useInjectionSpotEvents: () => ({ triggerEvent: jest.fn(async () => ({ ok: true, data: {} })) }),
|
|
20
|
+
}))
|
|
21
|
+
jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
22
|
+
__esModule: true,
|
|
23
|
+
useInjectionDataWidgets: () => ({ widgets: injectedFieldWidgets, isLoading: false, error: null }),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
import * as React from 'react'
|
|
27
|
+
import { waitFor } from '@testing-library/react'
|
|
28
|
+
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
29
|
+
import { CrudForm, type CrudField, type CrudFormGroup } from '../CrudForm'
|
|
30
|
+
import { InjectionPosition } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
31
|
+
|
|
32
|
+
const baseFields: CrudField[] = [
|
|
33
|
+
{ id: 'firstName', label: 'First name', type: 'text' },
|
|
34
|
+
{ id: 'lastName', label: 'Last name', type: 'text' },
|
|
35
|
+
// A trailing field so that "after lastName" is distinct from "end of group":
|
|
36
|
+
// the buggy unconditional push would land the injected field after `email`.
|
|
37
|
+
{ id: 'email', label: 'Email', type: 'text' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const groups: CrudFormGroup[] = [
|
|
41
|
+
{ id: 'personalData', label: 'Personal data', fields: ['firstName', 'lastName', 'email'] },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function orderedFieldIds(container: HTMLElement): string[] {
|
|
45
|
+
const seen: string[] = []
|
|
46
|
+
container.querySelectorAll('[data-crud-field-id]').forEach((node) => {
|
|
47
|
+
const id = node.getAttribute('data-crud-field-id')
|
|
48
|
+
if (id && !seen.includes(id)) seen.push(id)
|
|
49
|
+
})
|
|
50
|
+
return seen
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('CrudForm group field injection (#3047)', () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
injectedFieldWidgets = []
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('honors placement when injecting a field into an existing group', async () => {
|
|
59
|
+
injectedFieldWidgets = [
|
|
60
|
+
{
|
|
61
|
+
fields: [
|
|
62
|
+
{
|
|
63
|
+
id: 'cf:middle_name',
|
|
64
|
+
label: 'Middle name',
|
|
65
|
+
type: 'text',
|
|
66
|
+
group: 'personalData',
|
|
67
|
+
placement: { position: InjectionPosition.After, relativeTo: 'lastName' },
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const { container } = renderWithProviders(
|
|
74
|
+
React.createElement(CrudForm as any, {
|
|
75
|
+
title: 'Form',
|
|
76
|
+
entityId: 'customers:person',
|
|
77
|
+
fields: baseFields,
|
|
78
|
+
groups,
|
|
79
|
+
onSubmit: () => {},
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
expect(container.querySelector('[data-crud-field-id="cf:middle_name"]')).toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const ids = orderedFieldIds(container)
|
|
88
|
+
const lastNameIdx = ids.indexOf('lastName')
|
|
89
|
+
const middleIdx = ids.indexOf('cf:middle_name')
|
|
90
|
+
const firstNameIdx = ids.indexOf('firstName')
|
|
91
|
+
const emailIdx = ids.indexOf('email')
|
|
92
|
+
expect(firstNameIdx).toBeGreaterThanOrEqual(0)
|
|
93
|
+
expect(lastNameIdx).toBeGreaterThan(firstNameIdx)
|
|
94
|
+
// Injected field lands directly after `lastName` (placement honored), not at
|
|
95
|
+
// the end of the group after `email` (the pre-fix append behavior).
|
|
96
|
+
expect(middleIdx).toBe(lastNameIdx + 1)
|
|
97
|
+
expect(emailIdx).toBe(middleIdx + 1)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('renders the injected field label exactly once', async () => {
|
|
101
|
+
injectedFieldWidgets = [
|
|
102
|
+
{
|
|
103
|
+
fields: [
|
|
104
|
+
{
|
|
105
|
+
id: 'cf:middle_name',
|
|
106
|
+
label: 'Middle name',
|
|
107
|
+
type: 'text',
|
|
108
|
+
group: 'personalData',
|
|
109
|
+
placement: { position: InjectionPosition.After, relativeTo: 'lastName' },
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const { container } = renderWithProviders(
|
|
116
|
+
React.createElement(CrudForm as any, {
|
|
117
|
+
title: 'Form',
|
|
118
|
+
entityId: 'customers:person',
|
|
119
|
+
fields: baseFields,
|
|
120
|
+
groups,
|
|
121
|
+
onSubmit: () => {},
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(container.querySelector('[data-crud-field-id="cf:middle_name"]')).toBeTruthy()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const labelMatches = Array.from(container.querySelectorAll('label')).filter(
|
|
130
|
+
(node) => node.textContent?.trim() === 'Middle name',
|
|
131
|
+
)
|
|
132
|
+
expect(labelMatches).toHaveLength(1)
|
|
133
|
+
})
|
|
134
|
+
})
|