@open-mercato/ui 0.6.8-develop.7100.1.fbf66fca35 → 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
|
@@ -7,33 +7,6 @@ import { NotesSection, type NotesDataAdapter } from '../detail/NotesSection'
|
|
|
7
7
|
import { dismissRecordConflict, getRecordConflictForTest } from '../conflicts'
|
|
8
8
|
import { OPTIMISTIC_LOCK_CONFLICT_CODE } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'
|
|
9
9
|
|
|
10
|
-
const createDataAdapter = (): NotesDataAdapter => ({
|
|
11
|
-
list: jest.fn(async () => [
|
|
12
|
-
{
|
|
13
|
-
id: 'note-1',
|
|
14
|
-
body: 'Existing note',
|
|
15
|
-
createdAt: '2026-04-10T08:00:00.000Z',
|
|
16
|
-
authorName: 'Ada Lovelace',
|
|
17
|
-
},
|
|
18
|
-
]),
|
|
19
|
-
create: jest.fn(async () => ({ id: 'note-2' })),
|
|
20
|
-
update: jest.fn(async () => undefined),
|
|
21
|
-
delete: jest.fn(async () => undefined),
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const baseProps = (dataAdapter: NotesDataAdapter) => ({
|
|
25
|
-
entityId: 'person-1',
|
|
26
|
-
emptyLabel: '—',
|
|
27
|
-
viewerUserId: 'user-1',
|
|
28
|
-
viewerName: 'Ada Lovelace',
|
|
29
|
-
addActionLabel: 'Add note',
|
|
30
|
-
emptyState: {
|
|
31
|
-
title: 'No notes yet',
|
|
32
|
-
actionLabel: 'Add note',
|
|
33
|
-
},
|
|
34
|
-
dataAdapter,
|
|
35
|
-
})
|
|
36
|
-
|
|
37
10
|
describe('NotesSection', () => {
|
|
38
11
|
beforeEach(() => {
|
|
39
12
|
dismissRecordConflict()
|
|
@@ -198,68 +171,6 @@ describe('NotesSection', () => {
|
|
|
198
171
|
})
|
|
199
172
|
})
|
|
200
173
|
|
|
201
|
-
it('renders the markdown toggle and appearance controls by default', async () => {
|
|
202
|
-
const { container } = renderWithProviders(<NotesSection {...baseProps(createDataAdapter())} />)
|
|
203
|
-
|
|
204
|
-
await screen.findByText('Existing note')
|
|
205
|
-
expect(container.querySelectorAll('svg.lucide-palette').length).toBe(1)
|
|
206
|
-
|
|
207
|
-
fireEvent.click(screen.getByRole('button', { name: 'Add note' }))
|
|
208
|
-
|
|
209
|
-
await waitFor(() => {
|
|
210
|
-
expect(container.querySelector('button[aria-pressed]')).not.toBeNull()
|
|
211
|
-
})
|
|
212
|
-
expect(screen.getByRole('button', { name: 'Customize appearance' })).toBeTruthy()
|
|
213
|
-
expect(screen.queryByTestId('markdown-field')).toBeNull()
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('keeps the rich editor active and hides the toggle when forceMarkdown is set', async () => {
|
|
217
|
-
const { container } = renderWithProviders(
|
|
218
|
-
<NotesSection {...baseProps(createDataAdapter())} forceMarkdown />,
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
await screen.findByText('Existing note')
|
|
222
|
-
fireEvent.click(screen.getByRole('button', { name: 'Add note' }))
|
|
223
|
-
|
|
224
|
-
await waitFor(() => {
|
|
225
|
-
expect(screen.getByTestId('markdown-field')).toBeTruthy()
|
|
226
|
-
})
|
|
227
|
-
expect(container.querySelector('button[aria-pressed]')).toBeNull()
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('hides the toggle but honors the seeded preference when hideMarkdownToggle is set', async () => {
|
|
231
|
-
const { container } = renderWithProviders(
|
|
232
|
-
<NotesSection
|
|
233
|
-
{...baseProps(createDataAdapter())}
|
|
234
|
-
hideMarkdownToggle
|
|
235
|
-
readMarkdownPreference={() => true}
|
|
236
|
-
/>,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
await screen.findByText('Existing note')
|
|
240
|
-
fireEvent.click(screen.getByRole('button', { name: 'Add note' }))
|
|
241
|
-
|
|
242
|
-
await waitFor(() => {
|
|
243
|
-
expect(screen.getByTestId('markdown-field')).toBeTruthy()
|
|
244
|
-
})
|
|
245
|
-
expect(container.querySelector('button[aria-pressed]')).toBeNull()
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('removes every appearance affordance when disableAppearance is set', async () => {
|
|
249
|
-
const { container } = renderWithProviders(
|
|
250
|
-
<NotesSection {...baseProps(createDataAdapter())} disableAppearance />,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
await screen.findByText('Existing note')
|
|
254
|
-
fireEvent.click(screen.getByRole('button', { name: 'Add note' }))
|
|
255
|
-
|
|
256
|
-
await waitFor(() => {
|
|
257
|
-
expect(container.querySelector('button[aria-pressed]')).not.toBeNull()
|
|
258
|
-
})
|
|
259
|
-
expect(screen.queryByRole('button', { name: 'Customize appearance' })).toBeNull()
|
|
260
|
-
expect(container.querySelectorAll('svg.lucide-palette').length).toBe(0)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
174
|
it('surfaces the unified conflict bar when a write fails with a 409', async () => {
|
|
264
175
|
const conflict = {
|
|
265
176
|
status: 409,
|
|
@@ -207,20 +207,7 @@ export type NotesSectionProps<C = unknown> = {
|
|
|
207
207
|
iconSuggestions?: IconOption[]
|
|
208
208
|
readMarkdownPreference?: () => boolean | null
|
|
209
209
|
writeMarkdownPreference?: (value: boolean) => void
|
|
210
|
-
/** Force the plain textarea and hide the markdown toggle. Wins over `forceMarkdown`. */
|
|
211
210
|
disableMarkdown?: boolean
|
|
212
|
-
/**
|
|
213
|
-
* Keep the rich markdown editor active for every note and hide the markdown toggle,
|
|
214
|
-
* so the editor shape cannot be switched. Ignored when `disableMarkdown` is set.
|
|
215
|
-
*/
|
|
216
|
-
forceMarkdown?: boolean
|
|
217
|
-
/**
|
|
218
|
-
* Hide the markdown toggle without changing which editor renders — the seeded
|
|
219
|
-
* `readMarkdownPreference` value stays in effect. Implied by `forceMarkdown`.
|
|
220
|
-
*/
|
|
221
|
-
hideMarkdownToggle?: boolean
|
|
222
|
-
/** Hide the appearance (icon/color) affordances: the palette buttons and the appearance dialog. */
|
|
223
|
-
disableAppearance?: boolean
|
|
224
211
|
}
|
|
225
212
|
|
|
226
213
|
export function sanitizeHexColor(value: string | null): string | null {
|
|
@@ -326,9 +313,6 @@ function NotesSectionImpl<C = unknown>({
|
|
|
326
313
|
readMarkdownPreference,
|
|
327
314
|
writeMarkdownPreference,
|
|
328
315
|
disableMarkdown,
|
|
329
|
-
forceMarkdown,
|
|
330
|
-
hideMarkdownToggle,
|
|
331
|
-
disableAppearance,
|
|
332
316
|
}: NotesSectionProps<C>) {
|
|
333
317
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
334
318
|
const t = React.useMemo<Translator>(() => translator ?? ((key, fallback) => fallback ?? key), [translator])
|
|
@@ -464,16 +448,13 @@ function NotesSectionImpl<C = unknown>({
|
|
|
464
448
|
const [draftIcon, setDraftIcon] = React.useState<string | null>(null)
|
|
465
449
|
const [draftColor, setDraftColor] = React.useState<string | null>(null)
|
|
466
450
|
const [isMarkdownEnabled, setIsMarkdownEnabled] = React.useState(false)
|
|
467
|
-
const isMarkdownActive = !disableMarkdown && (Boolean(forceMarkdown) || isMarkdownEnabled)
|
|
468
|
-
const showMarkdownToggle = !disableMarkdown && !forceMarkdown && !hideMarkdownToggle
|
|
469
|
-
const showAppearanceControls = !disableAppearance
|
|
470
451
|
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
471
452
|
const formRef = React.useRef<HTMLFormElement | null>(null)
|
|
472
453
|
const focusComposer = React.useCallback(() => {
|
|
473
454
|
if (!hasEntity) return
|
|
474
455
|
setComposerOpen(true)
|
|
475
456
|
window.requestAnimationFrame(() => {
|
|
476
|
-
if (
|
|
457
|
+
if (isMarkdownEnabled) {
|
|
477
458
|
const markdownTextarea = formRef.current?.querySelector('textarea')
|
|
478
459
|
if (markdownTextarea instanceof HTMLTextAreaElement) {
|
|
479
460
|
markdownTextarea.focus()
|
|
@@ -486,7 +467,7 @@ function NotesSectionImpl<C = unknown>({
|
|
|
486
467
|
element.focus()
|
|
487
468
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
488
469
|
})
|
|
489
|
-
}, [formRef, hasEntity,
|
|
470
|
+
}, [formRef, hasEntity, isMarkdownEnabled])
|
|
490
471
|
const [appearanceDialogState, setAppearanceDialogState] = React.useState<
|
|
491
472
|
| { mode: 'create'; icon: string | null; color: string | null }
|
|
492
473
|
| { mode: 'edit'; noteId: string; icon: string | null; color: string | null }
|
|
@@ -603,7 +584,7 @@ function NotesSectionImpl<C = unknown>({
|
|
|
603
584
|
|
|
604
585
|
React.useEffect(() => {
|
|
605
586
|
adjustTextareaSize(textareaRef.current)
|
|
606
|
-
}, [adjustTextareaSize, draftBody,
|
|
587
|
+
}, [adjustTextareaSize, draftBody, isMarkdownEnabled, composerOpen])
|
|
607
588
|
|
|
608
589
|
React.useEffect(() => {
|
|
609
590
|
const preference = readMarkdownPreference ? readMarkdownPreference() : null
|
|
@@ -1009,33 +990,31 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1009
990
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
1010
991
|
<h3 className="text-sm font-medium">{label('addLabel')}</h3>
|
|
1011
992
|
<div className="flex flex-wrap items-center gap-1">
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
>
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
) : null}
|
|
1027
|
-
{showMarkdownToggle ? (
|
|
993
|
+
<Button
|
|
994
|
+
type="button"
|
|
995
|
+
variant="ghost"
|
|
996
|
+
size="icon"
|
|
997
|
+
onClick={() => {
|
|
998
|
+
setAppearanceDialogError(null)
|
|
999
|
+
setAppearanceDialogState({ mode: 'create', icon: draftIcon, color: draftColor })
|
|
1000
|
+
}}
|
|
1001
|
+
disabled={isSubmitting || isLoading || !hasEntity}
|
|
1002
|
+
>
|
|
1003
|
+
<span className="sr-only">{label('appearance.toggleOpen', 'Customize appearance')}</span>
|
|
1004
|
+
<Palette className="h-4 w-4" />
|
|
1005
|
+
</Button>
|
|
1006
|
+
{disableMarkdown ? null : (
|
|
1028
1007
|
<Button
|
|
1029
1008
|
type="button"
|
|
1030
|
-
variant={
|
|
1009
|
+
variant={isMarkdownEnabled ? 'secondary' : 'ghost'}
|
|
1031
1010
|
size="icon"
|
|
1032
1011
|
onClick={handleMarkdownToggle}
|
|
1033
|
-
aria-pressed={
|
|
1012
|
+
aria-pressed={isMarkdownEnabled}
|
|
1034
1013
|
disabled={isSubmitting || isLoading}
|
|
1035
1014
|
>
|
|
1036
1015
|
<FileCode className="h-4 w-4" />
|
|
1037
1016
|
</Button>
|
|
1038
|
-
)
|
|
1017
|
+
)}
|
|
1039
1018
|
<Button
|
|
1040
1019
|
type="button"
|
|
1041
1020
|
size="sm"
|
|
@@ -1111,7 +1090,7 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1111
1090
|
<SwitchableMarkdownInput
|
|
1112
1091
|
value={draftBody}
|
|
1113
1092
|
onChange={setDraftBody}
|
|
1114
|
-
isMarkdownEnabled={
|
|
1093
|
+
isMarkdownEnabled={isMarkdownEnabled}
|
|
1115
1094
|
disableMarkdown={disableMarkdown}
|
|
1116
1095
|
rows={1}
|
|
1117
1096
|
placeholder={label('placeholder')}
|
|
@@ -1235,26 +1214,24 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1235
1214
|
>
|
|
1236
1215
|
<Pencil className="h-4 w-4" />
|
|
1237
1216
|
</Button>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
</Button>
|
|
1257
|
-
) : null}
|
|
1217
|
+
<Button
|
|
1218
|
+
type="button"
|
|
1219
|
+
variant="ghost"
|
|
1220
|
+
size="icon"
|
|
1221
|
+
onClick={(event) => {
|
|
1222
|
+
event.stopPropagation()
|
|
1223
|
+
setAppearanceDialogError(null)
|
|
1224
|
+
setAppearanceDialogState({
|
|
1225
|
+
mode: 'edit',
|
|
1226
|
+
noteId: note.id,
|
|
1227
|
+
icon: note.appearanceIcon ?? null,
|
|
1228
|
+
color: note.appearanceColor ?? null,
|
|
1229
|
+
})
|
|
1230
|
+
}}
|
|
1231
|
+
disabled={appearanceDialogSaving && editingAppearanceNoteId === note.id}
|
|
1232
|
+
>
|
|
1233
|
+
{isAppearanceSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Palette className="h-4 w-4" />}
|
|
1234
|
+
</Button>
|
|
1258
1235
|
<Button
|
|
1259
1236
|
type="button"
|
|
1260
1237
|
variant="ghost"
|
|
@@ -1280,7 +1257,7 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1280
1257
|
<SwitchableMarkdownInput
|
|
1281
1258
|
value={contentEditor.value}
|
|
1282
1259
|
onChange={(nextValue) => setContentEditor((prev) => ({ ...prev, value: nextValue }))}
|
|
1283
|
-
isMarkdownEnabled={
|
|
1260
|
+
isMarkdownEnabled={isMarkdownEnabled}
|
|
1284
1261
|
disableMarkdown={disableMarkdown}
|
|
1285
1262
|
rows={3}
|
|
1286
1263
|
textareaRef={contentTextareaRef}
|
|
@@ -1301,19 +1278,19 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1301
1278
|
inlineLabel('saveShortcut')
|
|
1302
1279
|
)}
|
|
1303
1280
|
</Button>
|
|
1304
|
-
{
|
|
1281
|
+
{disableMarkdown ? null : (
|
|
1305
1282
|
<Button
|
|
1306
1283
|
type="button"
|
|
1307
1284
|
variant="ghost"
|
|
1308
1285
|
size="icon"
|
|
1309
1286
|
onClick={handleMarkdownToggle}
|
|
1310
|
-
aria-pressed={
|
|
1311
|
-
className={
|
|
1287
|
+
aria-pressed={isMarkdownEnabled}
|
|
1288
|
+
className={isMarkdownEnabled ? 'text-primary' : undefined}
|
|
1312
1289
|
disabled={contentSavingId === note.id}
|
|
1313
1290
|
>
|
|
1314
1291
|
<FileCode className="h-4 w-4" />
|
|
1315
1292
|
</Button>
|
|
1316
|
-
)
|
|
1293
|
+
)}
|
|
1317
1294
|
<Button
|
|
1318
1295
|
type="button"
|
|
1319
1296
|
size="sm"
|
|
@@ -1363,31 +1340,29 @@ function NotesSectionImpl<C = unknown>({
|
|
|
1363
1340
|
</div>
|
|
1364
1341
|
)}
|
|
1365
1342
|
</div>
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
/>
|
|
1390
|
-
) : null}
|
|
1343
|
+
<AppearanceDialog
|
|
1344
|
+
open={appearanceDialogOpen}
|
|
1345
|
+
title={
|
|
1346
|
+
appearanceDialogState?.mode === 'edit'
|
|
1347
|
+
? label('appearance.edit')
|
|
1348
|
+
: label('appearance.toggleOpen', 'Customize appearance')
|
|
1349
|
+
}
|
|
1350
|
+
icon={appearanceDialogState?.icon ?? null}
|
|
1351
|
+
color={appearanceDialogState?.color ?? null}
|
|
1352
|
+
labels={noteAppearanceLabels}
|
|
1353
|
+
iconSuggestions={iconSuggestions}
|
|
1354
|
+
onIconChange={(value) => setAppearanceDialogState((prev) => (prev ? { ...prev, icon: value ?? null } : prev))}
|
|
1355
|
+
onColorChange={(value) => setAppearanceDialogState((prev) => (prev ? { ...prev, color: value ?? null } : prev))}
|
|
1356
|
+
onSubmit={() => {
|
|
1357
|
+
void handleAppearanceDialogSubmit()
|
|
1358
|
+
}}
|
|
1359
|
+
onClose={handleAppearanceDialogClose}
|
|
1360
|
+
isSaving={appearanceDialogSaving}
|
|
1361
|
+
errorMessage={appearanceDialogError}
|
|
1362
|
+
primaryLabel={appearanceDialogPrimaryLabel}
|
|
1363
|
+
savingLabel={appearanceDialogSavingLabel}
|
|
1364
|
+
cancelLabel={label('appearance.cancel')}
|
|
1365
|
+
/>
|
|
1391
1366
|
{ConfirmDialogElement}
|
|
1392
1367
|
</div>
|
|
1393
1368
|
)
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
AlertTriangle,
|
|
12
12
|
Apple,
|
|
13
13
|
Archive,
|
|
14
|
-
AtSign,
|
|
15
14
|
Award,
|
|
16
15
|
BadgeCheck,
|
|
17
16
|
Ban,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
Bolt,
|
|
23
22
|
Book,
|
|
24
23
|
Bookmark,
|
|
25
|
-
Bot,
|
|
26
24
|
Box,
|
|
27
25
|
Boxes,
|
|
28
26
|
Briefcase,
|
|
@@ -175,7 +173,6 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
175
173
|
'alert-triangle': AlertTriangle,
|
|
176
174
|
'apple': Apple,
|
|
177
175
|
'archive': Archive,
|
|
178
|
-
'at-sign': AtSign,
|
|
179
176
|
'award': Award,
|
|
180
177
|
'badge-check': BadgeCheck,
|
|
181
178
|
'ban': Ban,
|
|
@@ -186,7 +183,6 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
186
183
|
'bolt': Bolt,
|
|
187
184
|
'book': Book,
|
|
188
185
|
'bookmark': Bookmark,
|
|
189
|
-
'bot': Bot,
|
|
190
186
|
'box': Box,
|
|
191
187
|
'boxes': Boxes,
|
|
192
188
|
'briefcase': Briefcase,
|
|
@@ -40,30 +40,6 @@ type Resolution<TProps> = {
|
|
|
40
40
|
replacementModule: string
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Calling a wrapper override returns a fresh component every time, which would
|
|
45
|
-
* reintroduce the identity churn this hook exists to avoid. Memoizing per
|
|
46
|
-
* (wrapper, wrapped component) pair keeps the composed component referentially
|
|
47
|
-
* stable for as long as both inputs are.
|
|
48
|
-
*/
|
|
49
|
-
const composedWrappers = new WeakMap<object, WeakMap<object, unknown>>()
|
|
50
|
-
|
|
51
|
-
function applyWrapper<TProps>(
|
|
52
|
-
wrapper: (Original: ComponentType<TProps>) => ComponentType<TProps>,
|
|
53
|
-
Base: ComponentType<TProps>,
|
|
54
|
-
): ComponentType<TProps> {
|
|
55
|
-
let byBase = composedWrappers.get(wrapper)
|
|
56
|
-
if (!byBase) {
|
|
57
|
-
byBase = new WeakMap<object, unknown>()
|
|
58
|
-
composedWrappers.set(wrapper, byBase)
|
|
59
|
-
}
|
|
60
|
-
const cached = byBase.get(Base)
|
|
61
|
-
if (cached) return cached as ComponentType<TProps>
|
|
62
|
-
const composed = wrapper(Base)
|
|
63
|
-
byBase.set(Base, composed)
|
|
64
|
-
return composed
|
|
65
|
-
}
|
|
66
|
-
|
|
67
43
|
function resolveComponent<TProps>(
|
|
68
44
|
componentId: string,
|
|
69
45
|
fallback: ComponentType<TProps> | undefined,
|
|
@@ -99,7 +75,7 @@ function resolveComponent<TProps>(
|
|
|
99
75
|
}
|
|
100
76
|
|
|
101
77
|
const base = replacement ?? original
|
|
102
|
-
const wrapped = wrappers.reduce<ComponentType<TProps>>((acc, wrapper) =>
|
|
78
|
+
const wrapped = wrappers.reduce<ComponentType<TProps>>((acc, wrapper) => wrapper(acc), base)
|
|
103
79
|
|
|
104
80
|
return {
|
|
105
81
|
original,
|
|
@@ -111,9 +87,8 @@ function resolveComponent<TProps>(
|
|
|
111
87
|
}
|
|
112
88
|
|
|
113
89
|
/**
|
|
114
|
-
* The returned component's identity must depend only on `componentId`
|
|
115
|
-
*
|
|
116
|
-
* pass on this render.
|
|
90
|
+
* The returned component's identity must depend only on `componentId` and
|
|
91
|
+
* `fallback` — never on the override registry.
|
|
117
92
|
*
|
|
118
93
|
* Overrides arrive asynchronously: `ComponentOverridesBootstrap` dynamically
|
|
119
94
|
* imports the generated override module and hands the provider a fresh array,
|
|
@@ -128,32 +103,18 @@ function resolveComponent<TProps>(
|
|
|
128
103
|
* carries no override for this id, `wrapped` keeps its previous identity and
|
|
129
104
|
* React reconciles in place; only a genuine replacement or wrapper swaps the
|
|
130
105
|
* rendered type, where a remount is the correct behaviour.
|
|
131
|
-
*
|
|
132
|
-
* The identity is held in a ref rather than a `useMemo`, because `useMemo` is a
|
|
133
|
-
* performance hint React is free to discard, and identity here is a correctness
|
|
134
|
-
* requirement rather than an optimisation. `fallback` is read through a ref for
|
|
135
|
-
* the same reason: a host that builds its fallback inline would otherwise swap
|
|
136
|
-
* the component this hook hands back on every render. That still cannot save a
|
|
137
|
-
* subtree rendered *through* such a fallback — the fallback itself is then the
|
|
138
|
-
* element type, and only the host can stabilise it — but it keeps the churn
|
|
139
|
-
* from spreading to hosts whose id does resolve to a registered component.
|
|
140
106
|
*/
|
|
141
107
|
export function useRegisteredComponent<TProps>(
|
|
142
108
|
componentId: string,
|
|
143
109
|
fallback?: ComponentType<TProps>,
|
|
144
110
|
): ComponentType<TProps> {
|
|
145
|
-
|
|
146
|
-
fallbackRef.current = fallback
|
|
147
|
-
|
|
148
|
-
const registered = React.useRef<{ componentId: string; Component: ComponentType<TProps> } | null>(null)
|
|
149
|
-
if (!registered.current || registered.current.componentId !== componentId) {
|
|
111
|
+
return React.useMemo(() => {
|
|
150
112
|
const Registered = (props: TProps) => {
|
|
151
113
|
const userFeatures = useOverrideUserFeatures()
|
|
152
114
|
const overrideRevision = useOverrideRegistryRevision()
|
|
153
|
-
const currentFallback = fallbackRef.current
|
|
154
115
|
const { original, wrapped, transforms, replacementOverride, replacementModule } = React.useMemo(
|
|
155
|
-
() => resolveComponent<TProps>(componentId,
|
|
156
|
-
[
|
|
116
|
+
() => resolveComponent<TProps>(componentId, fallback, userFeatures),
|
|
117
|
+
[overrideRevision, userFeatures],
|
|
157
118
|
)
|
|
158
119
|
|
|
159
120
|
if (!original || !wrapped) return null
|
|
@@ -184,10 +145,8 @@ export function useRegisteredComponent<TProps>(
|
|
|
184
145
|
}
|
|
185
146
|
|
|
186
147
|
Registered.displayName = `RegisteredComponent(${componentId})`
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return registered.current.Component
|
|
148
|
+
return Registered
|
|
149
|
+
}, [componentId, fallback])
|
|
191
150
|
}
|
|
192
151
|
|
|
193
152
|
export default useRegisteredComponent
|
|
@@ -100,10 +100,6 @@ export function ComboboxInput({
|
|
|
100
100
|
const blurClosePendingRef = React.useRef(false)
|
|
101
101
|
const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))
|
|
102
102
|
const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)
|
|
103
|
-
// Tracks whether the user actually typed into the field during the current focus
|
|
104
|
-
// session. `touched` cannot serve this purpose because it is set by `onFocus`,
|
|
105
|
-
// which `autoFocus` triggers before the user does anything at all.
|
|
106
|
-
const userTypedRef = React.useRef(false)
|
|
107
103
|
|
|
108
104
|
const staticOptions = React.useMemo(
|
|
109
105
|
() => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),
|
|
@@ -251,21 +247,12 @@ export function ComboboxInput({
|
|
|
251
247
|
// including it would re-run the effect on every render when the prop is an inline function
|
|
252
248
|
}, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])
|
|
253
249
|
|
|
254
|
-
// Sync input with
|
|
255
|
-
// synced too, because `autoFocus` can focus the control before an async default value
|
|
256
|
-
// arrives and a focus-only guard then leaves the control rendering an empty label for
|
|
257
|
-
// a value the form has already committed. Two conditions still block the sync:
|
|
258
|
-
// - the user is typing, so their query is never clobbered mid-keystroke;
|
|
259
|
-
// - `optionMap` only holds the self-mapping placeholder it synthesises for an
|
|
260
|
-
// uncovered value, which would paint the raw record id over a label the user just
|
|
261
|
-
// picked (`asyncOptions` is replaced on every load, so any follow-up load that
|
|
262
|
-
// misses the picked entry — a failed request, a debounce race, a composite label
|
|
263
|
-
// the route's `?search=` cannot match — drops it back to the placeholder).
|
|
250
|
+
// Sync input with value when value changes externally and input is not focused.
|
|
264
251
|
React.useEffect(() => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
252
|
+
if (document.activeElement !== inputRef.current) {
|
|
253
|
+
const option = optionMap.get(value)
|
|
254
|
+
setInput(option?.label ?? value ?? '')
|
|
255
|
+
}
|
|
269
256
|
}, [value, optionMap])
|
|
270
257
|
|
|
271
258
|
const selectValue = React.useCallback(
|
|
@@ -278,7 +265,6 @@ export function ComboboxInput({
|
|
|
278
265
|
setInput(option?.label ?? trimmed)
|
|
279
266
|
setShowSuggestions(false)
|
|
280
267
|
setSelectedIndex(-1)
|
|
281
|
-
userTypedRef.current = false
|
|
282
268
|
},
|
|
283
269
|
[disabled, onChange, optionMap, resetBlurCloseState]
|
|
284
270
|
)
|
|
@@ -450,7 +436,6 @@ export function ComboboxInput({
|
|
|
450
436
|
}}
|
|
451
437
|
onChange={(event) => {
|
|
452
438
|
setTouched(true)
|
|
453
|
-
userTypedRef.current = true
|
|
454
439
|
setInput(event.target.value)
|
|
455
440
|
setShowSuggestions(true)
|
|
456
441
|
setSelectedIndex(-1)
|
|
@@ -460,7 +445,6 @@ export function ComboboxInput({
|
|
|
460
445
|
// Delay closing so clicks on the popup can resolve first. If async
|
|
461
446
|
// suggestions are still loading, keep the dropdown open instead of
|
|
462
447
|
// closing before the first payload arrives.
|
|
463
|
-
userTypedRef.current = false
|
|
464
448
|
blurClosePendingRef.current = true
|
|
465
449
|
clearBlurCloseTimer()
|
|
466
450
|
if (loadingRef.current) {
|
|
@@ -116,8 +116,12 @@ export function LookupSelect({
|
|
|
116
116
|
if (onReadyRef.current) onReadyRef.current({ setQuery })
|
|
117
117
|
}, [setQuery])
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// A set `value` always opens the list: it is the only place the selected row,
|
|
120
|
+
// its checkmark and the clear control render, so collapsing over a selection
|
|
121
|
+
// would hide it with no way to see or undo it. This used to also require an
|
|
122
|
+
// `options` prop, which left every caller that resolves its selection through
|
|
123
|
+
// `fetchItems` blind whenever minQuery kept the list shut.
|
|
124
|
+
const shouldSearch = defaultOpen || query.trim().length >= minQuery || Boolean(value)
|
|
121
125
|
|
|
122
126
|
React.useEffect(() => {
|
|
123
127
|
setActiveIndex(-1)
|
|
@@ -129,8 +133,8 @@ export function LookupSelect({
|
|
|
129
133
|
)
|
|
130
134
|
|
|
131
135
|
const isInteractiveItem = React.useCallback(
|
|
132
|
-
(item: LookupSelectItem) => !
|
|
133
|
-
[
|
|
136
|
+
(item: LookupSelectItem) => !item.disabled || value === item.id,
|
|
137
|
+
[value],
|
|
134
138
|
)
|
|
135
139
|
|
|
136
140
|
const moveActiveIndex = React.useCallback((direction: 1 | -1) => {
|
|
@@ -248,7 +252,7 @@ export function LookupSelect({
|
|
|
248
252
|
aria-activedescendant={activeIndex >= 0 ? optionDomId(activeIndex) : undefined}
|
|
249
253
|
/>
|
|
250
254
|
</div>
|
|
251
|
-
{actionSlot
|
|
255
|
+
{actionSlot ? <div className="sm:self-start">{actionSlot}</div> : null}
|
|
252
256
|
</div>
|
|
253
257
|
{shouldSearch ? (
|
|
254
258
|
<div className="space-y-2">
|
|
@@ -268,7 +272,7 @@ export function LookupSelect({
|
|
|
268
272
|
>
|
|
269
273
|
{items.map((item, index) => {
|
|
270
274
|
const isSelected = value === item.id
|
|
271
|
-
const isInteractive =
|
|
275
|
+
const isInteractive = !item.disabled || isSelected
|
|
272
276
|
const isActive = index === activeIndex
|
|
273
277
|
return (
|
|
274
278
|
<div
|
|
@@ -283,7 +287,7 @@ export function LookupSelect({
|
|
|
283
287
|
isActive && !isSelected ? 'border-foreground/20 bg-muted/30 shadow-sm' : null
|
|
284
288
|
)}
|
|
285
289
|
role="option"
|
|
286
|
-
tabIndex={
|
|
290
|
+
tabIndex={item.disabled ? -1 : 0}
|
|
287
291
|
onClick={() => {
|
|
288
292
|
if (!isInteractive) return
|
|
289
293
|
onChange(item.id)
|
|
@@ -296,8 +300,8 @@ export function LookupSelect({
|
|
|
296
300
|
}
|
|
297
301
|
}}
|
|
298
302
|
aria-selected={isSelected}
|
|
299
|
-
aria-disabled={
|
|
300
|
-
title={isSelected ? resolvedSelectedLabel :
|
|
303
|
+
aria-disabled={item.disabled && !isSelected ? true : undefined}
|
|
304
|
+
title={isSelected ? resolvedSelectedLabel : resolvedSelectLabel}
|
|
301
305
|
>
|
|
302
306
|
{item.icon ? (
|
|
303
307
|
<div className="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden [&>svg]:size-6 [&_svg]:text-muted-foreground">
|
|
@@ -340,7 +344,7 @@ export function LookupSelect({
|
|
|
340
344
|
)
|
|
341
345
|
})}
|
|
342
346
|
</div>
|
|
343
|
-
{value
|
|
347
|
+
{value ? (
|
|
344
348
|
<Button
|
|
345
349
|
type="button"
|
|
346
350
|
variant="ghost"
|