@open-mercato/ui 0.6.6 → 0.6.7-develop.6552.1.5208a19369
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 +1 -1
- package/dist/ai/ChatPaneTabs.js +2 -2
- package/dist/ai/ChatPaneTabs.js.map +2 -2
- package/dist/backend/AppShell.js +2 -2
- package/dist/backend/AppShell.js.map +1 -1
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +37 -20
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +9 -17
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js +1 -1
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +1 -1
- package/dist/backend/messages/useMessageCompose.js +1 -0
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/primitives/drawer.js +6 -3
- package/dist/primitives/drawer.js.map +2 -2
- package/dist/primitives/tabs.js +2 -1
- package/dist/primitives/tabs.js.map +2 -2
- package/package.json +5 -4
- package/src/ai/ChatPaneTabs.tsx +2 -2
- package/src/backend/AppShell.tsx +2 -2
- package/src/backend/__tests__/DataTable.extensions.test.tsx +26 -10
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +52 -24
- package/src/backend/detail/AttachmentMetadataDialog.tsx +16 -23
- package/src/backend/filters/AdvancedFilterBuilder.tsx +1 -1
- package/src/backend/messages/__tests__/useMessageCompose.test.tsx +60 -0
- package/src/backend/messages/useMessageCompose.ts +4 -0
- package/src/primitives/__tests__/drawer.test.tsx +2 -1
- package/src/primitives/drawer.tsx +4 -3
- package/src/primitives/tabs.tsx +6 -1
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SelectValue,
|
|
16
16
|
} from '../../primitives/select'
|
|
17
17
|
import { CUSTOM_FIELD_KINDS } from '@open-mercato/shared/modules/entities/kinds'
|
|
18
|
-
import { useOptionalT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
|
+
import { useOptionalT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
19
19
|
import { FieldRegistry } from '../fields/registry'
|
|
20
20
|
import { slugify } from '@open-mercato/shared/lib/slugify'
|
|
21
21
|
import { useConfirmDialog } from '../confirm-dialog'
|
|
@@ -35,6 +35,16 @@ import {
|
|
|
35
35
|
type FieldsetGroup = { code: string; title?: string; hint?: string }
|
|
36
36
|
type FieldsetConfig = { code: string; label: string; icon?: string; description?: string; groups?: FieldsetGroup[] }
|
|
37
37
|
|
|
38
|
+
function formatFallback(template: string, params?: Record<string, string | number>) {
|
|
39
|
+
if (!params) return template
|
|
40
|
+
return template.replace(/\{\{(\w+)\}\}|\{(\w+)\}/g, (match, doubleKey, singleKey) => {
|
|
41
|
+
const key = doubleKey ?? singleKey
|
|
42
|
+
if (!key) return match
|
|
43
|
+
const value = params[key]
|
|
44
|
+
return value === undefined ? match : String(value)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
export type FieldDefinition = {
|
|
39
49
|
key: string
|
|
40
50
|
kind: string
|
|
@@ -68,7 +78,7 @@ export type FieldDefinitionsEditorProps = {
|
|
|
68
78
|
listProps?: React.HTMLAttributes<HTMLDivElement>
|
|
69
79
|
singleFieldsetPerRecord?: boolean
|
|
70
80
|
onSingleFieldsetPerRecordChange?: (value: boolean) => void
|
|
71
|
-
translate?:
|
|
81
|
+
translate?: TranslateFn
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
const DEFAULT_VALUE_NONE = '__open_mercato_no_default__'
|
|
@@ -180,11 +190,13 @@ export function FieldDefinitionsEditor({
|
|
|
180
190
|
const dragIndex = React.useRef<number | null>(null)
|
|
181
191
|
const hasFieldsets = fieldsets.length > 0
|
|
182
192
|
const contextT = useOptionalT()
|
|
183
|
-
const t = React.useCallback(
|
|
184
|
-
(key
|
|
185
|
-
if (translate) return translate(key,
|
|
186
|
-
if (contextT) return contextT(key,
|
|
187
|
-
return
|
|
193
|
+
const t = React.useCallback<TranslateFn>(
|
|
194
|
+
(key, fallbackOrParams, params) => {
|
|
195
|
+
if (translate) return translate(key, fallbackOrParams, params)
|
|
196
|
+
if (contextT) return contextT(key, fallbackOrParams, params)
|
|
197
|
+
return typeof fallbackOrParams === 'string'
|
|
198
|
+
? formatFallback(fallbackOrParams, params)
|
|
199
|
+
: key
|
|
188
200
|
},
|
|
189
201
|
[translate, contextT],
|
|
190
202
|
)
|
|
@@ -264,7 +276,9 @@ export function FieldDefinitionsEditor({
|
|
|
264
276
|
if (!onFieldsetsChange) return
|
|
265
277
|
if (!resolvedActiveFieldset) return
|
|
266
278
|
const confirmed = await confirm({
|
|
267
|
-
title: t('entities.customFields.editor.deleteFieldsetTitle', 'Delete fieldset "{code}"?'
|
|
279
|
+
title: t('entities.customFields.editor.deleteFieldsetTitle', 'Delete fieldset "{code}"?', {
|
|
280
|
+
code: resolvedActiveFieldset,
|
|
281
|
+
}),
|
|
268
282
|
text: t('entities.customFields.editor.deleteFieldsetText', 'This will move its fields to Unassigned.'),
|
|
269
283
|
variant: 'destructive',
|
|
270
284
|
})
|
|
@@ -541,7 +555,7 @@ type FieldDefinitionCardProps = {
|
|
|
541
555
|
onRegisterGroup?: (fieldsetCode: string, group: FieldsetGroup) => void
|
|
542
556
|
onRemoveGroup?: (fieldsetCode: string, groupCode: string) => void
|
|
543
557
|
onTranslate?: () => void
|
|
544
|
-
translate?:
|
|
558
|
+
translate?: TranslateFn
|
|
545
559
|
}
|
|
546
560
|
|
|
547
561
|
const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
@@ -573,7 +587,15 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
573
587
|
() => (typeof local.configJson?.fieldset === 'string' ? local.configJson.fieldset : ''),
|
|
574
588
|
[local.configJson?.fieldset],
|
|
575
589
|
)
|
|
576
|
-
const t = React.useCallback(
|
|
590
|
+
const t = React.useCallback<TranslateFn>(
|
|
591
|
+
(key, fallbackOrParams, params) => {
|
|
592
|
+
if (translate) return translate(key, fallbackOrParams, params)
|
|
593
|
+
return typeof fallbackOrParams === 'string'
|
|
594
|
+
? formatFallback(fallbackOrParams, params)
|
|
595
|
+
: key
|
|
596
|
+
},
|
|
597
|
+
[translate],
|
|
598
|
+
)
|
|
577
599
|
React.useEffect(() => {
|
|
578
600
|
localRef.current = definition
|
|
579
601
|
setLocal(definition)
|
|
@@ -1093,23 +1115,29 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1093
1115
|
<SelectValue />
|
|
1094
1116
|
</SelectTrigger>
|
|
1095
1117
|
<SelectContent>
|
|
1096
|
-
<SelectItem value="required">required</SelectItem>
|
|
1097
|
-
<SelectItem value="date">date</SelectItem>
|
|
1098
|
-
<SelectItem value="integer">integer</SelectItem>
|
|
1099
|
-
<SelectItem value="float">float</SelectItem>
|
|
1100
|
-
<SelectItem value="lt">lt</SelectItem>
|
|
1101
|
-
<SelectItem value="lte">lte</SelectItem>
|
|
1102
|
-
<SelectItem value="gt">gt</SelectItem>
|
|
1103
|
-
<SelectItem value="gte">gte</SelectItem>
|
|
1104
|
-
<SelectItem value="eq">eq</SelectItem>
|
|
1105
|
-
<SelectItem value="ne">ne</SelectItem>
|
|
1106
|
-
<SelectItem value="regex">regex</SelectItem>
|
|
1118
|
+
<SelectItem value="required">{t('entities.customFields.editor.validation.required', 'required')}</SelectItem>
|
|
1119
|
+
<SelectItem value="date">{t('entities.customFields.editor.validation.date', 'date')}</SelectItem>
|
|
1120
|
+
<SelectItem value="integer">{t('entities.customFields.editor.validation.integer', 'integer')}</SelectItem>
|
|
1121
|
+
<SelectItem value="float">{t('entities.customFields.editor.validation.float', 'float')}</SelectItem>
|
|
1122
|
+
<SelectItem value="lt">{t('entities.customFields.editor.validation.lt', 'lt')}</SelectItem>
|
|
1123
|
+
<SelectItem value="lte">{t('entities.customFields.editor.validation.lte', 'lte')}</SelectItem>
|
|
1124
|
+
<SelectItem value="gt">{t('entities.customFields.editor.validation.gt', 'gt')}</SelectItem>
|
|
1125
|
+
<SelectItem value="gte">{t('entities.customFields.editor.validation.gte', 'gte')}</SelectItem>
|
|
1126
|
+
<SelectItem value="eq">{t('entities.customFields.editor.validation.eq', 'eq')}</SelectItem>
|
|
1127
|
+
<SelectItem value="ne">{t('entities.customFields.editor.validation.ne', 'ne')}</SelectItem>
|
|
1128
|
+
<SelectItem value="regex">{t('entities.customFields.editor.validation.regex', 'regex')}</SelectItem>
|
|
1107
1129
|
</SelectContent>
|
|
1108
1130
|
</Select>
|
|
1109
1131
|
</div>
|
|
1110
1132
|
<div className="md:col-span-4">
|
|
1111
1133
|
<Input
|
|
1112
|
-
placeholder={
|
|
1134
|
+
placeholder={
|
|
1135
|
+
rule?.rule === 'regex'
|
|
1136
|
+
? t('entities.customFields.editor.rulePatternPlaceholder', 'Pattern (e.g. ^[a-z]+$)')
|
|
1137
|
+
: (['lt','lte','gt','gte'].includes(rule?.rule)
|
|
1138
|
+
? t('entities.customFields.editor.ruleNumberPlaceholder', 'Number')
|
|
1139
|
+
: t('entities.customFields.editor.noParameterPlaceholder', '—'))
|
|
1140
|
+
}
|
|
1113
1141
|
value={rule?.param ?? ''}
|
|
1114
1142
|
onChange={(event) => {
|
|
1115
1143
|
const value = ['lt','lte','gt','gte'].includes(rule?.rule) ? Number(event.target.value) : event.target.value
|
|
@@ -1426,7 +1454,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1426
1454
|
size="xs"
|
|
1427
1455
|
className="text-muted-foreground hover:text-foreground"
|
|
1428
1456
|
onClick={() => handleEditGroupEntry(group)}
|
|
1429
|
-
aria-label={t('entities.customFields.editor.editGroupAria', 'Edit {code}'
|
|
1457
|
+
aria-label={t('entities.customFields.editor.editGroupAria', 'Edit {code}', { code: group.code })}
|
|
1430
1458
|
>
|
|
1431
1459
|
<Pencil className="h-4 w-4" />
|
|
1432
1460
|
</IconButton>
|
|
@@ -1435,7 +1463,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1435
1463
|
size="xs"
|
|
1436
1464
|
className="text-destructive hover:text-destructive/80"
|
|
1437
1465
|
onClick={() => handleRemoveGroupEntry(group.code)}
|
|
1438
|
-
aria-label={t('entities.customFields.editor.deleteGroupAria', 'Delete {code}'
|
|
1466
|
+
aria-label={t('entities.customFields.editor.deleteGroupAria', 'Delete {code}', { code: group.code })}
|
|
1439
1467
|
>
|
|
1440
1468
|
<Trash2 className="h-4 w-4" />
|
|
1441
1469
|
</IconButton>
|
|
@@ -6,6 +6,7 @@ import { Button } from '../../primitives/button'
|
|
|
6
6
|
import { IconButton } from '../../primitives/icon-button'
|
|
7
7
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
8
8
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
9
|
+
import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
9
10
|
import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
|
|
10
11
|
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
11
12
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
@@ -576,29 +577,21 @@ export function AttachmentMetadataDialog({ open, onOpenChange, item, availableTa
|
|
|
576
577
|
</div>
|
|
577
578
|
{isImage ? (
|
|
578
579
|
<div className="rounded border">
|
|
579
|
-
<
|
|
580
|
-
{
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
)}
|
|
595
|
-
>
|
|
596
|
-
{tab === 'preview'
|
|
597
|
-
? t('attachments.library.metadata.preview', 'Preview')
|
|
598
|
-
: t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}
|
|
599
|
-
</Button>
|
|
600
|
-
))}
|
|
601
|
-
</div>
|
|
580
|
+
<Tabs
|
|
581
|
+
value={imageTab}
|
|
582
|
+
onValueChange={(value) => setImageTab(value as 'preview' | 'resize')}
|
|
583
|
+
variant="underline"
|
|
584
|
+
>
|
|
585
|
+
<TabsList className="w-full flex-wrap px-3">
|
|
586
|
+
{(['preview', 'resize'] as const).map((tab) => (
|
|
587
|
+
<TabsTrigger key={tab} value={tab}>
|
|
588
|
+
{tab === 'preview'
|
|
589
|
+
? t('attachments.library.metadata.preview', 'Preview')
|
|
590
|
+
: t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}
|
|
591
|
+
</TabsTrigger>
|
|
592
|
+
))}
|
|
593
|
+
</TabsList>
|
|
594
|
+
</Tabs>
|
|
602
595
|
<div className="space-y-3 p-3">
|
|
603
596
|
{imageTab === 'preview' ? (
|
|
604
597
|
previewUrl ? (
|
|
@@ -332,7 +332,7 @@ function AndOrToggle({
|
|
|
332
332
|
const andLabel = t('ui.advancedFilter.combinator.and', 'And')
|
|
333
333
|
const orLabel = t('ui.advancedFilter.combinator.or', 'Or')
|
|
334
334
|
const baseBtn = 'h-8 px-3 text-sm font-medium outline-none focus-visible:shadow-focus disabled:cursor-not-allowed'
|
|
335
|
-
const selBtn = 'bg-brand-violet text-
|
|
335
|
+
const selBtn = 'bg-brand-violet text-brand-violet-foreground hover:bg-brand-violet/90'
|
|
336
336
|
const unselBtn = 'bg-background text-foreground hover:bg-accent'
|
|
337
337
|
return (
|
|
338
338
|
<div
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { renderHook } from '@testing-library/react'
|
|
7
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
8
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
9
|
+
import { useMessageCompose } from '../useMessageCompose'
|
|
10
|
+
import { apiCall } from '../../utils/apiCall'
|
|
11
|
+
|
|
12
|
+
jest.mock('../../utils/apiCall', () => ({
|
|
13
|
+
apiCall: jest.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
jest.mock('../../FlashMessages', () => ({
|
|
17
|
+
flash: jest.fn(),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
function createWrapper() {
|
|
21
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
22
|
+
return function Wrapper({ children }: { children: React.ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<QueryClientProvider client={queryClient}>
|
|
25
|
+
{/* @ts-expect-error shared provider accepts a loose dict shape */}
|
|
26
|
+
<I18nProvider locale="en" dict={{}}>
|
|
27
|
+
{children}
|
|
28
|
+
</I18nProvider>
|
|
29
|
+
</QueryClientProvider>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('useMessageCompose recipient suggestions', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.resetAllMocks()
|
|
37
|
+
;(apiCall as jest.Mock).mockResolvedValue({
|
|
38
|
+
ok: true,
|
|
39
|
+
status: 200,
|
|
40
|
+
result: { items: [] },
|
|
41
|
+
response: { status: 200 },
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('scopes recipient suggestions to the composer active organization', async () => {
|
|
46
|
+
const { result } = renderHook(() => useMessageCompose({ variant: 'compose' }), {
|
|
47
|
+
wrapper: createWrapper(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await result.current.loadRecipientSuggestions()
|
|
51
|
+
|
|
52
|
+
const authUsersCall = (apiCall as jest.Mock).mock.calls.find(
|
|
53
|
+
([url]) => typeof url === 'string' && url.startsWith('/api/auth/users'),
|
|
54
|
+
)
|
|
55
|
+
expect(authUsersCall).toBeDefined()
|
|
56
|
+
|
|
57
|
+
const requestedUrl = new URL(`http://localhost${authUsersCall[0]}`)
|
|
58
|
+
expect(requestedUrl.searchParams.get('scopeToActiveOrganization')).toBe('1')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -413,6 +413,10 @@ export function useMessageCompose({
|
|
|
413
413
|
const params = new URLSearchParams()
|
|
414
414
|
params.set('page', '1')
|
|
415
415
|
params.set('pageSize', '100')
|
|
416
|
+
// Scope suggestions to the composer's active organization: a message is stamped with
|
|
417
|
+
// that org and its detail endpoint denies cross-org recipients, so a recipient from
|
|
418
|
+
// another org could never open what they were sent.
|
|
419
|
+
params.set('scopeToActiveOrganization', '1')
|
|
416
420
|
// Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.
|
|
417
421
|
|
|
418
422
|
const call = await apiCall<{ items?: UserListItem[] }>(
|
|
@@ -86,7 +86,8 @@ describe('Drawer', () => {
|
|
|
86
86
|
const content = document.querySelector('[data-slot="drawer-content"]') as HTMLElement
|
|
87
87
|
expect(content.className).toContain('right-0')
|
|
88
88
|
expect(content.className).toContain('inset-y-0')
|
|
89
|
-
|
|
89
|
+
// Figma Drawer [1.1] width contract: 400px
|
|
90
|
+
expect(content.className).toContain('max-w-[400px]')
|
|
90
91
|
// Per Figma: inner edge rounded, no panel border on the seam.
|
|
91
92
|
expect(content.className).toContain('rounded-l-xl')
|
|
92
93
|
expect(content.className).not.toMatch(/\bborder-l\b/)
|
|
@@ -94,10 +94,11 @@ const drawerContentVariants = cva(
|
|
|
94
94
|
variants: {
|
|
95
95
|
side: {
|
|
96
96
|
right:
|
|
97
|
-
|
|
97
|
+
// Figma Drawer [1.1] width: 400px (not the Tailwind max-w-md 448px)
|
|
98
|
+
'inset-y-0 right-0 h-full w-full max-w-[400px] rounded-l-xl ' +
|
|
98
99
|
'data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right',
|
|
99
100
|
left:
|
|
100
|
-
'inset-y-0 left-0 h-full w-full max-w-
|
|
101
|
+
'inset-y-0 left-0 h-full w-full max-w-[400px] rounded-r-xl ' +
|
|
101
102
|
'data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left',
|
|
102
103
|
top:
|
|
103
104
|
'inset-x-0 top-0 max-h-[80vh] w-full rounded-b-xl ' +
|
|
@@ -303,7 +304,7 @@ const DrawerTitle = React.forwardRef<
|
|
|
303
304
|
<DialogPrimitive.Title
|
|
304
305
|
ref={ref}
|
|
305
306
|
data-slot="drawer-title"
|
|
306
|
-
className={cn('text-
|
|
307
|
+
className={cn('text-lg font-semibold leading-tight text-foreground', className)}
|
|
307
308
|
{...props}
|
|
308
309
|
/>
|
|
309
310
|
))
|
package/src/primitives/tabs.tsx
CHANGED
|
@@ -108,9 +108,13 @@ export function Tabs({
|
|
|
108
108
|
export type TabsListProps = {
|
|
109
109
|
children: React.ReactNode
|
|
110
110
|
className?: string
|
|
111
|
+
/** Accessible name for the `role="tablist"` element. Set it when the
|
|
112
|
+
* surrounding context does not already label the strip (e.g. a bare
|
|
113
|
+
* detail-page section switcher). */
|
|
114
|
+
'aria-label'?: string
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
export function TabsList({ children, className }: TabsListProps) {
|
|
117
|
+
export function TabsList({ children, className, 'aria-label': ariaLabel }: TabsListProps) {
|
|
114
118
|
const { variant, orientation } = useTabsContext()
|
|
115
119
|
|
|
116
120
|
const baseClasses =
|
|
@@ -130,6 +134,7 @@ export function TabsList({ children, className }: TabsListProps) {
|
|
|
130
134
|
data-slot="tabs-list"
|
|
131
135
|
className={cn(baseClasses, className)}
|
|
132
136
|
role="tablist"
|
|
137
|
+
aria-label={ariaLabel}
|
|
133
138
|
aria-orientation={orientation}
|
|
134
139
|
>
|
|
135
140
|
{children}
|