@open-mercato/ui 0.6.8-develop.7099.1.7ba2771d06 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/ai/records/ActivityCard.js +3 -4
- package/dist/ai/records/ActivityCard.js.map +2 -2
- package/dist/ai/records/DealCard.js +3 -4
- package/dist/ai/records/DealCard.js.map +2 -2
- package/dist/ai/records/ProductCard.js +2 -3
- package/dist/ai/records/ProductCard.js.map +2 -2
- package/dist/backend/AppShell.js +106 -129
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/DataTable.js +9 -22
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +20 -26
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +0 -4
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/injection/useRegisteredComponent.js +6 -24
- package/dist/backend/injection/useRegisteredComponent.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +4 -8
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +9 -9
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -36
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/utils/crud.js.map +2 -2
- package/dist/backend/utils/useAutoDiscoveredFields.js +8 -4
- package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
- package/dist/primitives/alert.js +1 -1
- package/dist/primitives/alert.js.map +2 -2
- package/dist/primitives/icon-button.js +1 -20
- package/dist/primitives/icon-button.js.map +2 -2
- package/dist/primitives/pagination.js +9 -14
- package/dist/primitives/pagination.js.map +2 -2
- package/dist/primitives/rich-editor.js +7 -14
- package/dist/primitives/rich-editor.js.map +2 -2
- package/dist/utils/format.js +5 -5
- package/dist/utils/format.js.map +2 -2
- package/package.json +4 -5
- package/src/ai/parts/__tests__/ConfirmationCard.test.tsx +0 -2
- package/src/ai/records/ActivityCard.tsx +3 -4
- package/src/ai/records/DealCard.tsx +3 -4
- package/src/ai/records/ProductCard.tsx +2 -3
- package/src/backend/AppShell.tsx +58 -89
- package/src/backend/DataTable.tsx +12 -46
- package/src/backend/__tests__/AppShell.test.tsx +1 -162
- package/src/backend/__tests__/DataTable.render.test.tsx +0 -37
- package/src/backend/__tests__/NotesSection.test.tsx +0 -89
- package/src/backend/detail/NotesSection.tsx +67 -92
- package/src/backend/icons/lucideRegistry.generated.tsx +0 -4
- package/src/backend/injection/useRegisteredComponent.tsx +8 -49
- package/src/backend/inputs/ComboboxInput.tsx +5 -21
- package/src/backend/inputs/LookupSelect.tsx +14 -10
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +0 -142
- package/src/backend/inputs/__tests__/LookupSelect.test.tsx +40 -63
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +11 -14
- package/src/backend/utils/__tests__/useAutoDiscoveredFields.groupLabels.test.tsx +59 -0
- package/src/backend/utils/crud.ts +0 -2
- package/src/backend/utils/useAutoDiscoveredFields.ts +10 -4
- package/src/primitives/__tests__/alert.test.tsx +1 -41
- package/src/primitives/__tests__/icon-button.test.tsx +0 -70
- package/src/primitives/__tests__/notification.test.tsx +1 -1
- package/src/primitives/__tests__/pagination.test.tsx +0 -54
- package/src/primitives/__tests__/rich-editor.test.tsx +0 -43
- package/src/primitives/alert.tsx +2 -5
- package/src/primitives/icon-button.tsx +1 -20
- package/src/primitives/pagination.tsx +9 -40
- package/src/primitives/rich-editor.tsx +4 -9
- package/src/utils/__tests__/format.test.ts +14 -27
- package/src/utils/format.ts +5 -10
- package/src/backend/__tests__/DataTable.cappedPagination.test.tsx +0 -122
- package/src/backend/__tests__/DataTable.headerWrap.test.tsx +0 -111
- package/src/backend/__tests__/useRegisteredComponent.identity.test.tsx +0 -305
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/** @jest-environment jsdom */
|
|
2
|
-
import * as React from 'react'
|
|
3
|
-
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
-
import { z } from 'zod'
|
|
5
|
-
import {
|
|
6
|
-
registerComponent,
|
|
7
|
-
registerComponentOverrides,
|
|
8
|
-
} from '@open-mercato/shared/modules/widgets/component-registry'
|
|
9
|
-
import type { ComponentOverride } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
10
|
-
import { ComponentOverrideProvider, useOverrideUserFeatures } from '../injection/ComponentOverrideProvider'
|
|
11
|
-
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
12
|
-
|
|
13
|
-
jest.mock('../utils/apiCall', () => ({
|
|
14
|
-
apiCall: jest.fn(async () => ({ ok: true, result: { granted: ['acme.override'] } })),
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
type FieldProps = { label?: string }
|
|
18
|
-
|
|
19
|
-
const mounts = { count: 0 }
|
|
20
|
-
|
|
21
|
-
function StatefulField({ label }: FieldProps) {
|
|
22
|
-
React.useEffect(() => {
|
|
23
|
-
mounts.count += 1
|
|
24
|
-
}, [])
|
|
25
|
-
return (
|
|
26
|
-
<label>
|
|
27
|
-
{label ?? 'field'}
|
|
28
|
-
<input data-testid="field" defaultValue="" />
|
|
29
|
-
</label>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function FeaturesProbe() {
|
|
34
|
-
const features = useOverrideUserFeatures()
|
|
35
|
-
return <span data-testid="features">{features.join(',')}</span>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function typeIntoField(value: string) {
|
|
39
|
-
fireEvent.change(screen.getByTestId('field'), { target: { value } })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('useRegisteredComponent identity', () => {
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
mounts.count = 0
|
|
45
|
-
registerComponentOverrides([])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
registerComponentOverrides([])
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('keeps the subtree mounted when override user features resolve after first paint', async () => {
|
|
53
|
-
const componentId = 'test.identity.features'
|
|
54
|
-
registerComponent({ id: componentId, component: StatefulField, metadata: { module: 'test' } })
|
|
55
|
-
|
|
56
|
-
function Consumer() {
|
|
57
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId)
|
|
58
|
-
return <Resolved />
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// The override targets a different component, so the resolution result for
|
|
62
|
-
// `componentId` is identical before and after the feature grant arrives.
|
|
63
|
-
const overrides: ComponentOverride[] = [
|
|
64
|
-
{
|
|
65
|
-
target: { componentId: 'test.identity.other' },
|
|
66
|
-
priority: 10,
|
|
67
|
-
features: ['acme.override'],
|
|
68
|
-
metadata: { module: 'test' },
|
|
69
|
-
propsTransform: (props: unknown) => props,
|
|
70
|
-
} as ComponentOverride,
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
render(
|
|
74
|
-
<ComponentOverrideProvider overrides={overrides}>
|
|
75
|
-
<FeaturesProbe />
|
|
76
|
-
<Consumer />
|
|
77
|
-
</ComponentOverrideProvider>,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
typeIntoField('admin@acme.com')
|
|
81
|
-
expect(mounts.count).toBe(1)
|
|
82
|
-
|
|
83
|
-
await waitFor(() => expect(screen.getByTestId('features')).toHaveTextContent('acme.override'))
|
|
84
|
-
|
|
85
|
-
expect(mounts.count).toBe(1)
|
|
86
|
-
expect(screen.getByTestId('field')).toHaveValue('admin@acme.com')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('keeps the subtree mounted when overrides are registered late', async () => {
|
|
90
|
-
const componentId = 'test.identity.late'
|
|
91
|
-
registerComponent({ id: componentId, component: StatefulField, metadata: { module: 'test' } })
|
|
92
|
-
|
|
93
|
-
function Consumer() {
|
|
94
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId)
|
|
95
|
-
return <Resolved />
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const lateOverrides: ComponentOverride[] = [
|
|
99
|
-
{
|
|
100
|
-
target: { componentId: 'test.identity.late.other' },
|
|
101
|
-
priority: 10,
|
|
102
|
-
metadata: { module: 'test' },
|
|
103
|
-
propsTransform: (props: unknown) => props,
|
|
104
|
-
} as ComponentOverride,
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
const { rerender } = render(
|
|
108
|
-
<ComponentOverrideProvider overrides={[]}>
|
|
109
|
-
<Consumer />
|
|
110
|
-
</ComponentOverrideProvider>,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
typeIntoField('typed before the registry settled')
|
|
114
|
-
expect(mounts.count).toBe(1)
|
|
115
|
-
|
|
116
|
-
await act(async () => {
|
|
117
|
-
rerender(
|
|
118
|
-
<ComponentOverrideProvider overrides={lateOverrides}>
|
|
119
|
-
<Consumer />
|
|
120
|
-
</ComponentOverrideProvider>,
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
expect(mounts.count).toBe(1)
|
|
125
|
-
expect(screen.getByTestId('field')).toHaveValue('typed before the registry settled')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('keeps a wrapper-wrapped subtree mounted across override re-registrations', async () => {
|
|
129
|
-
const componentId = 'test.identity.wrapped'
|
|
130
|
-
registerComponent({ id: componentId, component: StatefulField, metadata: { module: 'test' } })
|
|
131
|
-
|
|
132
|
-
const wrapper = (Original: React.ComponentType<FieldProps>) => {
|
|
133
|
-
const Wrapped = (props: FieldProps) => (
|
|
134
|
-
<div data-testid="wrapped">
|
|
135
|
-
<Original {...props} />
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
Wrapped.displayName = 'Wrapped'
|
|
139
|
-
return Wrapped
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const buildOverrides = (): ComponentOverride[] => [
|
|
143
|
-
{ target: { componentId }, priority: 10, metadata: { module: 'test' }, wrapper } as unknown as ComponentOverride,
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
function Consumer() {
|
|
147
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId)
|
|
148
|
-
return <Resolved />
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const { rerender } = render(
|
|
152
|
-
<ComponentOverrideProvider overrides={buildOverrides()}>
|
|
153
|
-
<Consumer />
|
|
154
|
-
</ComponentOverrideProvider>,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
expect(screen.getByTestId('wrapped')).toBeInTheDocument()
|
|
158
|
-
typeIntoField('typed inside a wrapped section')
|
|
159
|
-
expect(mounts.count).toBe(1)
|
|
160
|
-
|
|
161
|
-
// A new array with the same wrapper function — what a provider re-render looks like.
|
|
162
|
-
await act(async () => {
|
|
163
|
-
rerender(
|
|
164
|
-
<ComponentOverrideProvider overrides={buildOverrides()}>
|
|
165
|
-
<Consumer />
|
|
166
|
-
</ComponentOverrideProvider>,
|
|
167
|
-
)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
expect(mounts.count).toBe(1)
|
|
171
|
-
expect(screen.getByTestId('field')).toHaveValue('typed inside a wrapped section')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('composes a wrapper at most once per (wrapper, wrapped component) pair', async () => {
|
|
175
|
-
// The user-visible consequence is covered by the test above; this one pins the
|
|
176
|
-
// cache's own contract, so that dropping the memoization fails loudly and the
|
|
177
|
-
// purity requirement documented on the `wrapper` override member stays executable.
|
|
178
|
-
const componentId = 'test.identity.wrapper-composition'
|
|
179
|
-
const Base = (props: FieldProps) => <StatefulField {...props} />
|
|
180
|
-
Base.displayName = 'Base'
|
|
181
|
-
registerComponent({ id: componentId, component: Base, metadata: { module: 'test' } })
|
|
182
|
-
|
|
183
|
-
let compositions = 0
|
|
184
|
-
const wrapper = (Original: React.ComponentType<FieldProps>) => {
|
|
185
|
-
compositions += 1
|
|
186
|
-
const Wrapped = (props: FieldProps) => (
|
|
187
|
-
<div data-testid="wrapped">
|
|
188
|
-
<Original {...props} />
|
|
189
|
-
</div>
|
|
190
|
-
)
|
|
191
|
-
Wrapped.displayName = 'Wrapped'
|
|
192
|
-
return Wrapped
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const buildOverrides = (): ComponentOverride[] => [
|
|
196
|
-
{ target: { componentId }, priority: 10, metadata: { module: 'test' }, wrapper } as unknown as ComponentOverride,
|
|
197
|
-
]
|
|
198
|
-
|
|
199
|
-
function Consumer() {
|
|
200
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId)
|
|
201
|
-
return <Resolved />
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const { rerender } = render(
|
|
205
|
-
<ComponentOverrideProvider overrides={buildOverrides()}>
|
|
206
|
-
<Consumer />
|
|
207
|
-
</ComponentOverrideProvider>,
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
expect(screen.getByTestId('wrapped')).toBeInTheDocument()
|
|
211
|
-
expect(compositions).toBe(1)
|
|
212
|
-
|
|
213
|
-
for (let pass = 0; pass < 2; pass += 1) {
|
|
214
|
-
await act(async () => {
|
|
215
|
-
rerender(
|
|
216
|
-
<ComponentOverrideProvider overrides={buildOverrides()}>
|
|
217
|
-
<Consumer />
|
|
218
|
-
</ComponentOverrideProvider>,
|
|
219
|
-
)
|
|
220
|
-
})
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
expect(compositions).toBe(1)
|
|
224
|
-
expect(mounts.count).toBe(1)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('still applies a replacement that becomes active after the first render', async () => {
|
|
228
|
-
const componentId = 'test.identity.replacement'
|
|
229
|
-
const Original = () => <span data-testid="rendered">original</span>
|
|
230
|
-
const Replacement = () => <span data-testid="rendered">replacement</span>
|
|
231
|
-
registerComponent({ id: componentId, component: Original, metadata: { module: 'test' } })
|
|
232
|
-
|
|
233
|
-
function Consumer() {
|
|
234
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId)
|
|
235
|
-
return <Resolved />
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const lateOverrides: ComponentOverride[] = [
|
|
239
|
-
{
|
|
240
|
-
target: { componentId },
|
|
241
|
-
priority: 100,
|
|
242
|
-
metadata: { module: 'test' },
|
|
243
|
-
replacement: Replacement,
|
|
244
|
-
propsSchema: z.object({}).passthrough(),
|
|
245
|
-
} as unknown as ComponentOverride,
|
|
246
|
-
]
|
|
247
|
-
|
|
248
|
-
const { rerender } = render(
|
|
249
|
-
<ComponentOverrideProvider overrides={[]}>
|
|
250
|
-
<Consumer />
|
|
251
|
-
</ComponentOverrideProvider>,
|
|
252
|
-
)
|
|
253
|
-
expect(screen.getByTestId('rendered')).toHaveTextContent('original')
|
|
254
|
-
|
|
255
|
-
await act(async () => {
|
|
256
|
-
rerender(
|
|
257
|
-
<ComponentOverrideProvider overrides={lateOverrides}>
|
|
258
|
-
<Consumer />
|
|
259
|
-
</ComponentOverrideProvider>,
|
|
260
|
-
)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
expect(screen.getByTestId('rendered')).toHaveTextContent('replacement')
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
it('cannot save a subtree whose host re-creates the fallback on every render', () => {
|
|
267
|
-
const componentId = 'test.identity.inline-fallback'
|
|
268
|
-
|
|
269
|
-
function Consumer({ label }: { label: string }) {
|
|
270
|
-
// A fallback declared inside the host's render body is a new function on
|
|
271
|
-
// every render, and with no component registered under this id it *is*
|
|
272
|
-
// the component being rendered. The hook keeps its own identity stable,
|
|
273
|
-
// but it cannot make the caller's fallback stable — React sees a new
|
|
274
|
-
// element type below and must remount. Hosts that care about the state
|
|
275
|
-
// under an unregistered id have to hoist the fallback out of render.
|
|
276
|
-
const Fallback = (props: FieldProps) => <StatefulField {...props} />
|
|
277
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId, Fallback)
|
|
278
|
-
return <Resolved label={label} />
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const { rerender } = render(<Consumer label="first" />)
|
|
282
|
-
typeIntoField('typed against an inline fallback')
|
|
283
|
-
expect(mounts.count).toBe(1)
|
|
284
|
-
|
|
285
|
-
rerender(<Consumer label="second" />)
|
|
286
|
-
|
|
287
|
-
expect(mounts.count).toBe(2)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('remounts when the host asks for a different component id', () => {
|
|
291
|
-
function Consumer({ componentId }: { componentId: string }) {
|
|
292
|
-
const Resolved = useRegisteredComponent<FieldProps>(componentId, StatefulField)
|
|
293
|
-
return <Resolved />
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const { rerender } = render(<Consumer componentId="test.identity.provider-a" />)
|
|
297
|
-
typeIntoField('provider a input')
|
|
298
|
-
expect(mounts.count).toBe(1)
|
|
299
|
-
|
|
300
|
-
rerender(<Consumer componentId="test.identity.provider-b" />)
|
|
301
|
-
|
|
302
|
-
expect(mounts.count).toBe(2)
|
|
303
|
-
expect(screen.getByTestId('field')).toHaveValue('')
|
|
304
|
-
})
|
|
305
|
-
})
|