@open-mercato/ui 0.6.6-develop.6551.1.1511bfa890 → 0.6.6
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 +20 -37
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +17 -9
- 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 +0 -1
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/primitives/drawer.js +3 -6
- package/dist/primitives/drawer.js.map +2 -2
- package/dist/primitives/tabs.js +1 -2
- package/dist/primitives/tabs.js.map +2 -2
- package/package.json +4 -5
- package/src/ai/ChatPaneTabs.tsx +2 -2
- package/src/backend/AppShell.tsx +2 -2
- package/src/backend/__tests__/DataTable.extensions.test.tsx +10 -26
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +24 -52
- package/src/backend/detail/AttachmentMetadataDialog.tsx +23 -16
- package/src/backend/filters/AdvancedFilterBuilder.tsx +1 -1
- package/src/backend/messages/useMessageCompose.ts +0 -4
- package/src/primitives/__tests__/drawer.test.tsx +1 -2
- package/src/primitives/drawer.tsx +3 -4
- package/src/primitives/tabs.tsx +1 -6
- package/src/backend/messages/__tests__/useMessageCompose.test.tsx +0 -60
|
@@ -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
|
|
18
|
+
import { useOptionalT } 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,16 +35,6 @@ 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
|
-
|
|
48
38
|
export type FieldDefinition = {
|
|
49
39
|
key: string
|
|
50
40
|
kind: string
|
|
@@ -78,7 +68,7 @@ export type FieldDefinitionsEditorProps = {
|
|
|
78
68
|
listProps?: React.HTMLAttributes<HTMLDivElement>
|
|
79
69
|
singleFieldsetPerRecord?: boolean
|
|
80
70
|
onSingleFieldsetPerRecordChange?: (value: boolean) => void
|
|
81
|
-
translate?:
|
|
71
|
+
translate?: (key: string, fallback: string) => string
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
const DEFAULT_VALUE_NONE = '__open_mercato_no_default__'
|
|
@@ -190,13 +180,11 @@ export function FieldDefinitionsEditor({
|
|
|
190
180
|
const dragIndex = React.useRef<number | null>(null)
|
|
191
181
|
const hasFieldsets = fieldsets.length > 0
|
|
192
182
|
const contextT = useOptionalT()
|
|
193
|
-
const t = React.useCallback
|
|
194
|
-
(key
|
|
195
|
-
if (translate) return translate(key,
|
|
196
|
-
if (contextT) return contextT(key,
|
|
197
|
-
return
|
|
198
|
-
? formatFallback(fallbackOrParams, params)
|
|
199
|
-
: key
|
|
183
|
+
const t = React.useCallback(
|
|
184
|
+
(key: string, fallback: string) => {
|
|
185
|
+
if (translate) return translate(key, fallback)
|
|
186
|
+
if (contextT) return contextT(key, fallback)
|
|
187
|
+
return fallback
|
|
200
188
|
},
|
|
201
189
|
[translate, contextT],
|
|
202
190
|
)
|
|
@@ -276,9 +264,7 @@ export function FieldDefinitionsEditor({
|
|
|
276
264
|
if (!onFieldsetsChange) return
|
|
277
265
|
if (!resolvedActiveFieldset) return
|
|
278
266
|
const confirmed = await confirm({
|
|
279
|
-
title: t('entities.customFields.editor.deleteFieldsetTitle', 'Delete fieldset "{code}"?',
|
|
280
|
-
code: resolvedActiveFieldset,
|
|
281
|
-
}),
|
|
267
|
+
title: t('entities.customFields.editor.deleteFieldsetTitle', 'Delete fieldset "{code}"?').replace('{code}', resolvedActiveFieldset),
|
|
282
268
|
text: t('entities.customFields.editor.deleteFieldsetText', 'This will move its fields to Unassigned.'),
|
|
283
269
|
variant: 'destructive',
|
|
284
270
|
})
|
|
@@ -555,7 +541,7 @@ type FieldDefinitionCardProps = {
|
|
|
555
541
|
onRegisterGroup?: (fieldsetCode: string, group: FieldsetGroup) => void
|
|
556
542
|
onRemoveGroup?: (fieldsetCode: string, groupCode: string) => void
|
|
557
543
|
onTranslate?: () => void
|
|
558
|
-
translate?:
|
|
544
|
+
translate?: (key: string, fallback: string) => string
|
|
559
545
|
}
|
|
560
546
|
|
|
561
547
|
const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
@@ -587,15 +573,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
587
573
|
() => (typeof local.configJson?.fieldset === 'string' ? local.configJson.fieldset : ''),
|
|
588
574
|
[local.configJson?.fieldset],
|
|
589
575
|
)
|
|
590
|
-
const t = React.useCallback
|
|
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
|
-
)
|
|
576
|
+
const t = React.useCallback((key: string, fallback: string) => (translate ? translate(key, fallback) : fallback), [translate])
|
|
599
577
|
React.useEffect(() => {
|
|
600
578
|
localRef.current = definition
|
|
601
579
|
setLocal(definition)
|
|
@@ -1115,29 +1093,23 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1115
1093
|
<SelectValue />
|
|
1116
1094
|
</SelectTrigger>
|
|
1117
1095
|
<SelectContent>
|
|
1118
|
-
<SelectItem value="required">
|
|
1119
|
-
<SelectItem value="date">
|
|
1120
|
-
<SelectItem value="integer">
|
|
1121
|
-
<SelectItem value="float">
|
|
1122
|
-
<SelectItem value="lt">
|
|
1123
|
-
<SelectItem value="lte">
|
|
1124
|
-
<SelectItem value="gt">
|
|
1125
|
-
<SelectItem value="gte">
|
|
1126
|
-
<SelectItem value="eq">
|
|
1127
|
-
<SelectItem value="ne">
|
|
1128
|
-
<SelectItem value="regex">
|
|
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>
|
|
1129
1107
|
</SelectContent>
|
|
1130
1108
|
</Select>
|
|
1131
1109
|
</div>
|
|
1132
1110
|
<div className="md:col-span-4">
|
|
1133
1111
|
<Input
|
|
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
|
-
}
|
|
1112
|
+
placeholder={rule?.rule === 'regex' ? t('entities.customFields.editor.rulePatternPlaceholder', 'Pattern (e.g. ^[a-z]+$)') : (['lt','lte','gt','gte'].includes(rule?.rule) ? t('entities.customFields.editor.ruleNumberPlaceholder', 'Number') : '—')}
|
|
1141
1113
|
value={rule?.param ?? ''}
|
|
1142
1114
|
onChange={(event) => {
|
|
1143
1115
|
const value = ['lt','lte','gt','gte'].includes(rule?.rule) ? Number(event.target.value) : event.target.value
|
|
@@ -1454,7 +1426,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1454
1426
|
size="xs"
|
|
1455
1427
|
className="text-muted-foreground hover:text-foreground"
|
|
1456
1428
|
onClick={() => handleEditGroupEntry(group)}
|
|
1457
|
-
aria-label={t('entities.customFields.editor.editGroupAria', 'Edit {code}'
|
|
1429
|
+
aria-label={t('entities.customFields.editor.editGroupAria', 'Edit {code}').replace('{code}', group.code)}
|
|
1458
1430
|
>
|
|
1459
1431
|
<Pencil className="h-4 w-4" />
|
|
1460
1432
|
</IconButton>
|
|
@@ -1463,7 +1435,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1463
1435
|
size="xs"
|
|
1464
1436
|
className="text-destructive hover:text-destructive/80"
|
|
1465
1437
|
onClick={() => handleRemoveGroupEntry(group.code)}
|
|
1466
|
-
aria-label={t('entities.customFields.editor.deleteGroupAria', 'Delete {code}'
|
|
1438
|
+
aria-label={t('entities.customFields.editor.deleteGroupAria', 'Delete {code}').replace('{code}', group.code)}
|
|
1467
1439
|
>
|
|
1468
1440
|
<Trash2 className="h-4 w-4" />
|
|
1469
1441
|
</IconButton>
|
|
@@ -6,7 +6,6 @@ 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'
|
|
10
9
|
import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
|
|
11
10
|
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
12
11
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
@@ -577,21 +576,29 @@ export function AttachmentMetadataDialog({ open, onOpenChange, item, availableTa
|
|
|
577
576
|
</div>
|
|
578
577
|
{isImage ? (
|
|
579
578
|
<div className="rounded border">
|
|
580
|
-
<
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
579
|
+
<div className="flex flex-wrap gap-4 border-b px-3 py-2 text-sm font-medium" role="tablist">
|
|
580
|
+
{(['preview', 'resize'] as const).map((tab) => (
|
|
581
|
+
<Button
|
|
582
|
+
key={tab}
|
|
583
|
+
type="button"
|
|
584
|
+
variant="ghost"
|
|
585
|
+
size="sm"
|
|
586
|
+
role="tab"
|
|
587
|
+
aria-selected={imageTab === tab}
|
|
588
|
+
onClick={() => setImageTab(tab)}
|
|
589
|
+
className={cn(
|
|
590
|
+
'h-auto -mb-px rounded-none border-b-2 border-transparent px-0 py-1',
|
|
591
|
+
imageTab === tab
|
|
592
|
+
? 'border-accent-indigo text-foreground'
|
|
593
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
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>
|
|
595
602
|
<div className="space-y-3 p-3">
|
|
596
603
|
{imageTab === 'preview' ? (
|
|
597
604
|
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-white hover:bg-brand-violet/90'
|
|
336
336
|
const unselBtn = 'bg-background text-foreground hover:bg-accent'
|
|
337
337
|
return (
|
|
338
338
|
<div
|
|
@@ -413,10 +413,6 @@ 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')
|
|
420
416
|
// Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.
|
|
421
417
|
|
|
422
418
|
const call = await apiCall<{ items?: UserListItem[] }>(
|
|
@@ -86,8 +86,7 @@ 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
|
-
|
|
90
|
-
expect(content.className).toContain('max-w-[400px]')
|
|
89
|
+
expect(content.className).toContain('max-w-md')
|
|
91
90
|
// Per Figma: inner edge rounded, no panel border on the seam.
|
|
92
91
|
expect(content.className).toContain('rounded-l-xl')
|
|
93
92
|
expect(content.className).not.toMatch(/\bborder-l\b/)
|
|
@@ -94,11 +94,10 @@ const drawerContentVariants = cva(
|
|
|
94
94
|
variants: {
|
|
95
95
|
side: {
|
|
96
96
|
right:
|
|
97
|
-
|
|
98
|
-
'inset-y-0 right-0 h-full w-full max-w-[400px] rounded-l-xl ' +
|
|
97
|
+
'inset-y-0 right-0 h-full w-full max-w-md rounded-l-xl ' +
|
|
99
98
|
'data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right',
|
|
100
99
|
left:
|
|
101
|
-
'inset-y-0 left-0 h-full w-full max-w-
|
|
100
|
+
'inset-y-0 left-0 h-full w-full max-w-md rounded-r-xl ' +
|
|
102
101
|
'data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left',
|
|
103
102
|
top:
|
|
104
103
|
'inset-x-0 top-0 max-h-[80vh] w-full rounded-b-xl ' +
|
|
@@ -304,7 +303,7 @@ const DrawerTitle = React.forwardRef<
|
|
|
304
303
|
<DialogPrimitive.Title
|
|
305
304
|
ref={ref}
|
|
306
305
|
data-slot="drawer-title"
|
|
307
|
-
className={cn('text-
|
|
306
|
+
className={cn('text-base font-semibold leading-tight text-foreground', className)}
|
|
308
307
|
{...props}
|
|
309
308
|
/>
|
|
310
309
|
))
|
package/src/primitives/tabs.tsx
CHANGED
|
@@ -108,13 +108,9 @@ 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
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
export function TabsList({ children, className
|
|
113
|
+
export function TabsList({ children, className }: TabsListProps) {
|
|
118
114
|
const { variant, orientation } = useTabsContext()
|
|
119
115
|
|
|
120
116
|
const baseClasses =
|
|
@@ -134,7 +130,6 @@ export function TabsList({ children, className, 'aria-label': ariaLabel }: TabsL
|
|
|
134
130
|
data-slot="tabs-list"
|
|
135
131
|
className={cn(baseClasses, className)}
|
|
136
132
|
role="tablist"
|
|
137
|
-
aria-label={ariaLabel}
|
|
138
133
|
aria-orientation={orientation}
|
|
139
134
|
>
|
|
140
135
|
{children}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
})
|