@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.
Files changed (29) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/ai/ChatPaneTabs.js +2 -2
  3. package/dist/ai/ChatPaneTabs.js.map +2 -2
  4. package/dist/backend/AppShell.js +2 -2
  5. package/dist/backend/AppShell.js.map +1 -1
  6. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +20 -37
  7. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
  8. package/dist/backend/detail/AttachmentMetadataDialog.js +17 -9
  9. package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
  10. package/dist/backend/filters/AdvancedFilterBuilder.js +1 -1
  11. package/dist/backend/filters/AdvancedFilterBuilder.js.map +1 -1
  12. package/dist/backend/messages/useMessageCompose.js +0 -1
  13. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  14. package/dist/primitives/drawer.js +3 -6
  15. package/dist/primitives/drawer.js.map +2 -2
  16. package/dist/primitives/tabs.js +1 -2
  17. package/dist/primitives/tabs.js.map +2 -2
  18. package/package.json +4 -5
  19. package/src/ai/ChatPaneTabs.tsx +2 -2
  20. package/src/backend/AppShell.tsx +2 -2
  21. package/src/backend/__tests__/DataTable.extensions.test.tsx +10 -26
  22. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +24 -52
  23. package/src/backend/detail/AttachmentMetadataDialog.tsx +23 -16
  24. package/src/backend/filters/AdvancedFilterBuilder.tsx +1 -1
  25. package/src/backend/messages/useMessageCompose.ts +0 -4
  26. package/src/primitives/__tests__/drawer.test.tsx +1 -2
  27. package/src/primitives/drawer.tsx +3 -4
  28. package/src/primitives/tabs.tsx +1 -6
  29. 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, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
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?: TranslateFn
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<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
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?: TranslateFn
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<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
- )
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">{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>
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}', { code: group.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}', { code: group.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
- <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>
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-brand-violet-foreground hover:bg-brand-violet/90'
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
- // Figma Drawer [1.1] width contract: 400px
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
- // 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 ' +
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-[400px] rounded-r-xl ' +
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-lg font-semibold leading-tight text-foreground', className)}
306
+ className={cn('text-base font-semibold leading-tight text-foreground', className)}
308
307
  {...props}
309
308
  />
310
309
  ))
@@ -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, 'aria-label': ariaLabel }: TabsListProps) {
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
- })