@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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { DataTable } from '../DataTable'
|
|
7
|
+
|
|
8
|
+
jest.mock('next/navigation', () => ({
|
|
9
|
+
useRouter: () => ({ push: jest.fn(), replace: jest.fn(), prefetch: jest.fn(), refresh: jest.fn() }),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const useInjectionDataWidgetsMock = jest.fn()
|
|
13
|
+
jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
14
|
+
useInjectionDataWidgets: (spotId: string) => useInjectionDataWidgetsMock(spotId),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
type Row = { id: string; name: string }
|
|
18
|
+
|
|
19
|
+
describe('DataTable extensions', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
useInjectionDataWidgetsMock.mockImplementation(() => ({ widgets: [], isLoading: false, error: null }))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders injected columns from data-table extension surface', () => {
|
|
25
|
+
useInjectionDataWidgetsMock.mockImplementation((spotId: string) => {
|
|
26
|
+
if (spotId === 'data-table:customers.people:columns') {
|
|
27
|
+
return {
|
|
28
|
+
widgets: [
|
|
29
|
+
{
|
|
30
|
+
metadata: { id: 'test.columns' },
|
|
31
|
+
columns: [
|
|
32
|
+
{
|
|
33
|
+
id: 'ext_col',
|
|
34
|
+
header: 'Injected',
|
|
35
|
+
accessorKey: 'name',
|
|
36
|
+
sortable: false,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
isLoading: false,
|
|
42
|
+
error: null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { widgets: [], isLoading: false, error: null }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const columns: ColumnDef<Row>[] = [{ accessorKey: 'name', header: 'Name' }]
|
|
49
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 0 } } })
|
|
50
|
+
const html = renderToString(
|
|
51
|
+
React.createElement(
|
|
52
|
+
QueryClientProvider as any,
|
|
53
|
+
{ client: queryClient },
|
|
54
|
+
React.createElement(
|
|
55
|
+
I18nProvider as any,
|
|
56
|
+
{ locale: 'en', dict: {} },
|
|
57
|
+
React.createElement(DataTable as any, {
|
|
58
|
+
columns,
|
|
59
|
+
data: [{ id: 'r1', name: 'Alice' }],
|
|
60
|
+
injectionSpotId: 'data-table:customers.people',
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expect(html).toContain('Injected')
|
|
67
|
+
queryClient.clear()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('renders injected bulk action button when bulk extension exists', () => {
|
|
71
|
+
useInjectionDataWidgetsMock.mockImplementation((spotId: string) => {
|
|
72
|
+
if (spotId === 'data-table:customers.people:bulk-actions') {
|
|
73
|
+
return {
|
|
74
|
+
widgets: [
|
|
75
|
+
{
|
|
76
|
+
metadata: { id: 'test.bulk-actions' },
|
|
77
|
+
bulkActions: [
|
|
78
|
+
{
|
|
79
|
+
id: 'bulk-normal',
|
|
80
|
+
label: 'Set normal',
|
|
81
|
+
onExecute: async () => ({ ok: true }),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
isLoading: false,
|
|
87
|
+
error: null,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { widgets: [], isLoading: false, error: null }
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const columns: ColumnDef<Row>[] = [{ accessorKey: 'name', header: 'Name' }]
|
|
94
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 0 } } })
|
|
95
|
+
const html = renderToString(
|
|
96
|
+
React.createElement(
|
|
97
|
+
QueryClientProvider as any,
|
|
98
|
+
{ client: queryClient },
|
|
99
|
+
React.createElement(
|
|
100
|
+
I18nProvider as any,
|
|
101
|
+
{ locale: 'en', dict: {} },
|
|
102
|
+
React.createElement(DataTable as any, {
|
|
103
|
+
columns,
|
|
104
|
+
data: [{ id: 'r1', name: 'Alice' }],
|
|
105
|
+
injectionSpotId: 'data-table:customers.people',
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(html).toContain('Set normal')
|
|
112
|
+
expect(html).toContain('Select all rows')
|
|
113
|
+
queryClient.clear()
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { withDataTableNamespaces } from '../DataTable'
|
|
2
|
+
|
|
3
|
+
describe('withDataTableNamespaces', () => {
|
|
4
|
+
it('preserves mapped row fields and appends namespaced payload fields', () => {
|
|
5
|
+
const row = { id: '1', name: 'Alice' }
|
|
6
|
+
const source = {
|
|
7
|
+
id: '1',
|
|
8
|
+
name: 'Alice',
|
|
9
|
+
_example: { priority: 'high' },
|
|
10
|
+
_other: { flag: true },
|
|
11
|
+
ignored: 'value',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
expect(withDataTableNamespaces(row, source)).toEqual({
|
|
15
|
+
id: '1',
|
|
16
|
+
name: 'Alice',
|
|
17
|
+
_example: { priority: 'high' },
|
|
18
|
+
_other: { flag: true },
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('does not overwrite mapped values with non-namespaced source fields', () => {
|
|
23
|
+
const row = { id: '1', status: 'mapped' }
|
|
24
|
+
const source = { id: '1', status: 'source', plain: 123 }
|
|
25
|
+
|
|
26
|
+
expect(withDataTableNamespaces(row, source)).toEqual({
|
|
27
|
+
id: '1',
|
|
28
|
+
status: 'mapped',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import {
|
|
6
|
+
registerComponent,
|
|
7
|
+
registerComponentOverrides,
|
|
8
|
+
ComponentReplacementHandles,
|
|
9
|
+
} from '@open-mercato/shared/modules/widgets/component-registry'
|
|
10
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
11
|
+
import { DetailFieldsSection } from '../detail/DetailFieldsSection'
|
|
12
|
+
|
|
13
|
+
describe('component replacement', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
registerComponentOverrides([])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('falls back to passed component when no registry entry exists', () => {
|
|
19
|
+
const Fallback = ({ value }: { value: string }) => <div>{value}</div>
|
|
20
|
+
|
|
21
|
+
function Consumer() {
|
|
22
|
+
const Resolved = useRegisteredComponent<{ value: string }>('missing.component', Fallback)
|
|
23
|
+
return <Resolved value="fallback rendered" />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(<Consumer />)
|
|
27
|
+
expect(screen.getByText('fallback rendered')).toBeInTheDocument()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('applies wrapper override around registered component', () => {
|
|
31
|
+
const componentId = 'test.section'
|
|
32
|
+
const Base = ({ value }: { value: string }) => <span>{value}</span>
|
|
33
|
+
|
|
34
|
+
registerComponent({
|
|
35
|
+
id: componentId,
|
|
36
|
+
component: Base,
|
|
37
|
+
metadata: {
|
|
38
|
+
module: 'test',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
registerComponentOverrides([
|
|
42
|
+
{
|
|
43
|
+
target: { componentId },
|
|
44
|
+
priority: 10,
|
|
45
|
+
metadata: { module: 'test' },
|
|
46
|
+
wrapper: (Original) => {
|
|
47
|
+
const Wrapped = (props: { value: string }) => (
|
|
48
|
+
<div data-testid="wrapped">
|
|
49
|
+
<Original {...props} />
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
Wrapped.displayName = 'Wrapped'
|
|
53
|
+
return Wrapped
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
function Consumer() {
|
|
59
|
+
const Resolved = useRegisteredComponent<{ value: string }>(componentId)
|
|
60
|
+
return <Resolved value="wrapped rendered" />
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(<Consumer />)
|
|
64
|
+
expect(screen.getByTestId('wrapped')).toBeInTheDocument()
|
|
65
|
+
expect(screen.getByText('wrapped rendered')).toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('renders section handle for DetailFieldsSection', () => {
|
|
69
|
+
render(
|
|
70
|
+
<DetailFieldsSection
|
|
71
|
+
fields={[
|
|
72
|
+
{
|
|
73
|
+
key: 'name',
|
|
74
|
+
kind: 'custom',
|
|
75
|
+
label: 'Name',
|
|
76
|
+
emptyLabel: '-',
|
|
77
|
+
render: () => <span>Name</span>,
|
|
78
|
+
},
|
|
79
|
+
]}
|
|
80
|
+
/>,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'DetailFieldsSection')
|
|
84
|
+
const wrapper = document.querySelector(`[data-component-handle="${handle}"]`)
|
|
85
|
+
expect(wrapper).not.toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('falls back to original component when replacement props schema validation fails', () => {
|
|
89
|
+
const componentId = 'test.replace.schema'
|
|
90
|
+
const Original = ({ value }: { value: string }) => <span>original:{value}</span>
|
|
91
|
+
const Replacement = ({ value }: { value: string }) => <span>replacement:{value}</span>
|
|
92
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
93
|
+
|
|
94
|
+
registerComponent({
|
|
95
|
+
id: componentId,
|
|
96
|
+
component: Original,
|
|
97
|
+
metadata: { module: 'test' },
|
|
98
|
+
})
|
|
99
|
+
registerComponentOverrides([
|
|
100
|
+
{
|
|
101
|
+
target: { componentId },
|
|
102
|
+
priority: 100,
|
|
103
|
+
metadata: { module: 'test' },
|
|
104
|
+
replacement: Replacement,
|
|
105
|
+
propsSchema: z.object({ value: z.string().min(4) }),
|
|
106
|
+
},
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
function Consumer() {
|
|
110
|
+
const Resolved = useRegisteredComponent<{ value: string }>(componentId)
|
|
111
|
+
return <Resolved value="bad" />
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(<Consumer />)
|
|
115
|
+
expect(screen.getByText('original:bad')).toBeInTheDocument()
|
|
116
|
+
expect(screen.queryByText('replacement:bad')).toBeNull()
|
|
117
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
118
|
+
consoleSpy.mockRestore()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('uses highest-priority replacement when multiple replacements are registered', () => {
|
|
122
|
+
const componentId = 'test.replace.priority'
|
|
123
|
+
const Original = ({ value }: { value: string }) => <span>original:{value}</span>
|
|
124
|
+
const LowPriorityReplacement = ({ value }: { value: string }) => <span>low:{value}</span>
|
|
125
|
+
const HighPriorityReplacement = ({ value }: { value: string }) => <span>high:{value}</span>
|
|
126
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
127
|
+
|
|
128
|
+
registerComponent({
|
|
129
|
+
id: componentId,
|
|
130
|
+
component: Original,
|
|
131
|
+
metadata: { module: 'test' },
|
|
132
|
+
})
|
|
133
|
+
registerComponentOverrides([
|
|
134
|
+
{
|
|
135
|
+
target: { componentId },
|
|
136
|
+
priority: 50,
|
|
137
|
+
metadata: { module: 'low-priority-module' },
|
|
138
|
+
replacement: LowPriorityReplacement,
|
|
139
|
+
propsSchema: z.object({ value: z.string() }),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
target: { componentId },
|
|
143
|
+
priority: 100,
|
|
144
|
+
metadata: { module: 'high-priority-module' },
|
|
145
|
+
replacement: HighPriorityReplacement,
|
|
146
|
+
propsSchema: z.object({ value: z.string() }),
|
|
147
|
+
},
|
|
148
|
+
])
|
|
149
|
+
|
|
150
|
+
function Consumer() {
|
|
151
|
+
const Resolved = useRegisteredComponent<{ value: string }>(componentId)
|
|
152
|
+
return <Resolved value="picked" />
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
render(<Consumer />)
|
|
156
|
+
expect(screen.queryByText('low:picked')).toBeNull()
|
|
157
|
+
expect(screen.getByText('high:picked')).toBeInTheDocument()
|
|
158
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
159
|
+
`[UMES] Multiple replacements registered for "${componentId}". Highest-priority replacement is applied.`,
|
|
160
|
+
)
|
|
161
|
+
warnSpy.mockRestore()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('applies propsTransform overrides before rendering', () => {
|
|
165
|
+
const componentId = 'test.props.transform'
|
|
166
|
+
const Base = ({ value, className }: { value: string; className?: string }) => (
|
|
167
|
+
<span data-testid="props-transform-target" className={className}>
|
|
168
|
+
{value}
|
|
169
|
+
</span>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
registerComponent({
|
|
173
|
+
id: componentId,
|
|
174
|
+
component: Base,
|
|
175
|
+
metadata: { module: 'test' },
|
|
176
|
+
})
|
|
177
|
+
registerComponentOverrides([
|
|
178
|
+
{
|
|
179
|
+
target: { componentId },
|
|
180
|
+
priority: 50,
|
|
181
|
+
metadata: { module: 'props-transform-module' },
|
|
182
|
+
propsTransform: (props: { value: string; className?: string }) => ({
|
|
183
|
+
...props,
|
|
184
|
+
className: 'injected-class',
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
function Consumer() {
|
|
190
|
+
const Resolved = useRegisteredComponent<{ value: string; className?: string }>(componentId)
|
|
191
|
+
return <Resolved value="props transformed" />
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
render(<Consumer />)
|
|
195
|
+
expect(screen.getByTestId('props-transform-target')).toHaveClass('injected-class')
|
|
196
|
+
expect(screen.getByText('props transformed')).toBeInTheDocument()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('falls back to original component when replacement crashes at render time', () => {
|
|
200
|
+
const componentId = 'test.replace.crash'
|
|
201
|
+
const Original = ({ value }: { value: string }) => <span>original:{value}</span>
|
|
202
|
+
const CrashingReplacement = () => {
|
|
203
|
+
throw new Error('replacement render crash')
|
|
204
|
+
}
|
|
205
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
206
|
+
|
|
207
|
+
registerComponent({
|
|
208
|
+
id: componentId,
|
|
209
|
+
component: Original,
|
|
210
|
+
metadata: { module: 'test' },
|
|
211
|
+
})
|
|
212
|
+
registerComponentOverrides([
|
|
213
|
+
{
|
|
214
|
+
target: { componentId },
|
|
215
|
+
priority: 100,
|
|
216
|
+
metadata: { module: 'crashing-module' },
|
|
217
|
+
replacement: CrashingReplacement,
|
|
218
|
+
propsSchema: z.object({ value: z.string() }),
|
|
219
|
+
},
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
function Consumer() {
|
|
223
|
+
const Resolved = useRegisteredComponent<{ value: string }>(componentId)
|
|
224
|
+
return <Resolved value="fallback" />
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render(<Consumer />)
|
|
228
|
+
expect(screen.getByText('original:fallback')).toBeInTheDocument()
|
|
229
|
+
expect(errorSpy).toHaveBeenCalled()
|
|
230
|
+
errorSpy.mockRestore()
|
|
231
|
+
})
|
|
232
|
+
})
|
|
@@ -16,6 +16,8 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
16
16
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
17
17
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
18
18
|
import { useConfirmDialog } from '../confirm-dialog'
|
|
19
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
20
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
19
21
|
|
|
20
22
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
21
23
|
|
|
@@ -692,7 +694,7 @@ export type ActivitiesSectionProps<C = unknown> = {
|
|
|
692
694
|
manageHref?: string
|
|
693
695
|
}
|
|
694
696
|
|
|
695
|
-
|
|
697
|
+
function ActivitiesSectionImpl<C = unknown>({
|
|
696
698
|
entityId,
|
|
697
699
|
dealId,
|
|
698
700
|
addActionLabel,
|
|
@@ -1248,4 +1250,18 @@ export function ActivitiesSection<C = unknown>({
|
|
|
1248
1250
|
)
|
|
1249
1251
|
}
|
|
1250
1252
|
|
|
1253
|
+
export function ActivitiesSection<C = unknown>(props: ActivitiesSectionProps<C>) {
|
|
1254
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'ActivitiesSection')
|
|
1255
|
+
const Resolved = useRegisteredComponent<ActivitiesSectionProps<C>>(
|
|
1256
|
+
handle,
|
|
1257
|
+
ActivitiesSectionImpl as React.ComponentType<ActivitiesSectionProps<C>>,
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
return (
|
|
1261
|
+
<div data-component-handle={handle}>
|
|
1262
|
+
<Resolved {...props} />
|
|
1263
|
+
</div>
|
|
1264
|
+
)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1251
1267
|
export default ActivitiesSection
|
|
@@ -8,6 +8,8 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
8
8
|
import AddressTiles, { type AddressInput, type AddressValue } from './AddressTiles'
|
|
9
9
|
import type { AddressTypesAdapter } from './AddressEditor'
|
|
10
10
|
import type { AddressFormatStrategy } from './addressFormat'
|
|
11
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
12
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
11
13
|
|
|
12
14
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
13
15
|
|
|
@@ -69,7 +71,7 @@ function generateTempId() {
|
|
|
69
71
|
return `tmp_${Math.random().toString(36).slice(2)}`
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
function AddressesSectionImpl<C = unknown>({
|
|
73
75
|
entityId,
|
|
74
76
|
emptyLabel,
|
|
75
77
|
addActionLabel,
|
|
@@ -343,4 +345,18 @@ export function AddressesSection<C = unknown>({
|
|
|
343
345
|
)
|
|
344
346
|
}
|
|
345
347
|
|
|
348
|
+
export function AddressesSection<C = unknown>(props: AddressesSectionProps<C>) {
|
|
349
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'AddressesSection')
|
|
350
|
+
const Resolved = useRegisteredComponent<AddressesSectionProps<C>>(
|
|
351
|
+
handle,
|
|
352
|
+
AddressesSectionImpl as React.ComponentType<AddressesSectionProps<C>>,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<div data-component-handle={handle}>
|
|
357
|
+
<Resolved {...props} />
|
|
358
|
+
</div>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
346
362
|
export default AddressesSection
|
|
@@ -9,6 +9,8 @@ import { cn } from '@open-mercato/shared/lib/utils'
|
|
|
9
9
|
import { AttachmentVisualPreview, formatAttachmentFileSize } from './AttachmentVisualPreview'
|
|
10
10
|
import { AttachmentDeleteDialog } from './AttachmentDeleteDialog'
|
|
11
11
|
import { AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload } from './AttachmentMetadataDialog'
|
|
12
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
13
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
12
14
|
|
|
13
15
|
type AttachmentsResponse = {
|
|
14
16
|
items?: AttachmentItem[]
|
|
@@ -26,7 +28,7 @@ type Props = {
|
|
|
26
28
|
onChanged?: () => void
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
function AttachmentsSectionImpl({
|
|
30
32
|
entityId,
|
|
31
33
|
recordId,
|
|
32
34
|
title,
|
|
@@ -309,3 +311,17 @@ export function AttachmentsSection({
|
|
|
309
311
|
</div>
|
|
310
312
|
)
|
|
311
313
|
}
|
|
314
|
+
|
|
315
|
+
export function AttachmentsSection(props: Props) {
|
|
316
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'AttachmentsSection')
|
|
317
|
+
const Resolved = useRegisteredComponent<Props>(
|
|
318
|
+
handle,
|
|
319
|
+
AttachmentsSectionImpl as React.ComponentType<Props>,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<div data-component-handle={handle}>
|
|
324
|
+
<Resolved {...props} />
|
|
325
|
+
</div>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import { ensureDictionaryEntries } from '@open-mercato/core/modules/dictionaries/components/hooks/useDictionaryEntries'
|
|
20
20
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
21
21
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
22
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
23
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
22
24
|
|
|
23
25
|
type MarkdownPreviewProps = { children: string; className?: string; remarkPlugins?: PluggableList }
|
|
24
26
|
|
|
@@ -216,7 +218,7 @@ function formatFieldValue(
|
|
|
216
218
|
return resolved
|
|
217
219
|
}
|
|
218
220
|
|
|
219
|
-
|
|
221
|
+
function CustomDataSectionImpl({
|
|
220
222
|
entityId,
|
|
221
223
|
entityIds,
|
|
222
224
|
values,
|
|
@@ -527,4 +529,18 @@ export function CustomDataSection({
|
|
|
527
529
|
)
|
|
528
530
|
}
|
|
529
531
|
|
|
532
|
+
export function CustomDataSection(props: CustomDataSectionProps) {
|
|
533
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'CustomDataSection')
|
|
534
|
+
const Resolved = useRegisteredComponent<CustomDataSectionProps>(
|
|
535
|
+
handle,
|
|
536
|
+
CustomDataSectionImpl as React.ComponentType<CustomDataSectionProps>,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<div data-component-handle={handle}>
|
|
541
|
+
<Resolved {...props} />
|
|
542
|
+
</div>
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
530
546
|
export default CustomDataSection
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
5
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
4
6
|
import {
|
|
5
7
|
InlineMultilineEditor,
|
|
6
8
|
InlineSelectEditor,
|
|
@@ -66,7 +68,7 @@ export type DetailFieldsSectionProps = {
|
|
|
66
68
|
className?: string
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
function DetailFieldsSectionImpl({ fields, className }: DetailFieldsSectionProps) {
|
|
70
72
|
return (
|
|
71
73
|
<div className={['grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3', className].filter(Boolean).join(' ')}>
|
|
72
74
|
{fields.map((field) => {
|
|
@@ -145,3 +147,17 @@ export function DetailFieldsSection({ fields, className }: DetailFieldsSectionPr
|
|
|
145
147
|
</div>
|
|
146
148
|
)
|
|
147
149
|
}
|
|
150
|
+
|
|
151
|
+
export function DetailFieldsSection(props: DetailFieldsSectionProps) {
|
|
152
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'DetailFieldsSection')
|
|
153
|
+
const Resolved = useRegisteredComponent<DetailFieldsSectionProps>(
|
|
154
|
+
handle,
|
|
155
|
+
DetailFieldsSectionImpl as React.ComponentType<DetailFieldsSectionProps>,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div data-component-handle={handle}>
|
|
160
|
+
<Resolved {...props} />
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -16,6 +16,8 @@ import { LoadingMessage } from './LoadingMessage'
|
|
|
16
16
|
import { TabEmptyState } from './TabEmptyState'
|
|
17
17
|
import { useConfirmDialog } from '../confirm-dialog'
|
|
18
18
|
import { formatDateTime } from '@open-mercato/shared/lib/time'
|
|
19
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
20
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
19
21
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
20
22
|
|
|
21
23
|
export type SectionAction = {
|
|
@@ -262,7 +264,7 @@ export function mapCommentSummary(input: unknown): CommentSummary {
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
|
|
267
|
+
function NotesSectionImpl<C = unknown>({
|
|
266
268
|
entityId,
|
|
267
269
|
dealId,
|
|
268
270
|
emptyLabel,
|
|
@@ -1246,3 +1248,17 @@ export function NotesSection<C = unknown>({
|
|
|
1246
1248
|
</div>
|
|
1247
1249
|
)
|
|
1248
1250
|
}
|
|
1251
|
+
|
|
1252
|
+
export function NotesSection<C = unknown>(props: NotesSectionProps<C>) {
|
|
1253
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'NotesSection')
|
|
1254
|
+
const Resolved = useRegisteredComponent<NotesSectionProps<C>>(
|
|
1255
|
+
handle,
|
|
1256
|
+
NotesSectionImpl as React.ComponentType<NotesSectionProps<C>>,
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
return (
|
|
1260
|
+
<div data-component-handle={handle}>
|
|
1261
|
+
<Resolved {...props} />
|
|
1262
|
+
</div>
|
|
1263
|
+
)
|
|
1264
|
+
}
|
|
@@ -6,6 +6,8 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
6
6
|
import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
|
|
7
7
|
import { DataLoader } from '@open-mercato/ui/primitives/DataLoader'
|
|
8
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
10
|
+
import { useRegisteredComponent } from '../injection/useRegisteredComponent'
|
|
9
11
|
|
|
10
12
|
export type TagOption = {
|
|
11
13
|
id: string
|
|
@@ -44,7 +46,7 @@ export type TagsSectionProps = {
|
|
|
44
46
|
labels: TagsSectionLabels
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
function TagsSectionImpl({
|
|
48
50
|
title,
|
|
49
51
|
tags,
|
|
50
52
|
onChange,
|
|
@@ -311,4 +313,18 @@ export function TagsSection({
|
|
|
311
313
|
)
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
export function TagsSection(props: TagsSectionProps) {
|
|
317
|
+
const handle = ComponentReplacementHandles.section('ui.detail', 'TagsSection')
|
|
318
|
+
const Resolved = useRegisteredComponent<TagsSectionProps>(
|
|
319
|
+
handle,
|
|
320
|
+
TagsSectionImpl as React.ComponentType<TagsSectionProps>,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div data-component-handle={handle}>
|
|
325
|
+
<Resolved {...props} />
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
314
330
|
export default TagsSection
|