@open-mercato/ui 0.6.7-develop.6860.1.f796b22e80 → 0.6.7

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 (167) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -4
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +54 -2
  5. package/dist/ai/AiChat.js +2 -21
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/backend/AppShell.js +4 -43
  8. package/dist/backend/AppShell.js.map +2 -2
  9. package/dist/backend/AuthSessionGuard.js +0 -14
  10. package/dist/backend/AuthSessionGuard.js.map +2 -2
  11. package/dist/backend/BackendChromeProvider.js +1 -6
  12. package/dist/backend/BackendChromeProvider.js.map +2 -2
  13. package/dist/backend/CrudForm.js +4 -6
  14. package/dist/backend/CrudForm.js.map +2 -2
  15. package/dist/backend/DataTable.js +18 -20
  16. package/dist/backend/DataTable.js.map +2 -2
  17. package/dist/backend/JsonBuilder.js +3 -3
  18. package/dist/backend/JsonBuilder.js.map +2 -2
  19. package/dist/backend/NextStepCallout.js +4 -4
  20. package/dist/backend/NextStepCallout.js.map +1 -1
  21. package/dist/backend/ProfileDropdown.js +1 -2
  22. package/dist/backend/ProfileDropdown.js.map +2 -2
  23. package/dist/backend/RowActions.js +2 -2
  24. package/dist/backend/RowActions.js.map +2 -2
  25. package/dist/backend/confirm-dialog/ConfirmDialog.js +1 -13
  26. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  27. package/dist/backend/crud/useGroupOrder.js +21 -14
  28. package/dist/backend/crud/useGroupOrder.js.map +2 -2
  29. package/dist/backend/detail/AddressTiles.js +5 -4
  30. package/dist/backend/detail/AddressTiles.js.map +2 -2
  31. package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
  32. package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
  33. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  34. package/dist/backend/detail/AttachmentsSection.js +1 -1
  35. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  36. package/dist/backend/detail/NotesSection.js +1 -1
  37. package/dist/backend/detail/NotesSection.js.map +2 -2
  38. package/dist/backend/detail/TagsSection.js +1 -1
  39. package/dist/backend/detail/TagsSection.js.map +2 -2
  40. package/dist/backend/forms/FormHeader.js +2 -1
  41. package/dist/backend/forms/FormHeader.js.map +2 -2
  42. package/dist/backend/icons/lucideRegistry.generated.js +25 -1
  43. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  44. package/dist/backend/icons/lucideRegistry.js +3 -3
  45. package/dist/backend/icons/lucideRegistry.js.map +2 -2
  46. package/dist/backend/injection/spotIds.js +18 -23
  47. package/dist/backend/injection/spotIds.js.map +2 -2
  48. package/dist/backend/schedule/ScheduleAgenda.js +3 -3
  49. package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
  50. package/dist/backend/schedule/ScheduleGrid.js +3 -3
  51. package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
  52. package/dist/backend/schedule/ScheduleToolbar.js +70 -87
  53. package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
  54. package/dist/backend/sidebar/SidebarCustomizationEditor.js +2 -30
  55. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  56. package/dist/backend/sidebar/customization-helpers.js +0 -30
  57. package/dist/backend/sidebar/customization-helpers.js.map +2 -2
  58. package/dist/backend/utils/api.js +13 -3
  59. package/dist/backend/utils/api.js.map +2 -2
  60. package/dist/backend/utils/apiCall.js +1 -21
  61. package/dist/backend/utils/apiCall.js.map +2 -2
  62. package/dist/backend/utils/customFieldDefs.js.map +2 -2
  63. package/dist/backend/utils/customFieldForms.js +9 -10
  64. package/dist/backend/utils/customFieldForms.js.map +2 -2
  65. package/dist/backend/utils/nav.js +2 -5
  66. package/dist/backend/utils/nav.js.map +2 -2
  67. package/dist/frontend/LanguageSwitcher.js +1 -2
  68. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  69. package/dist/portal/PortalContext.js +1 -1
  70. package/dist/portal/PortalContext.js.map +2 -2
  71. package/dist/portal/hooks/usePortalEventBridge.js +0 -6
  72. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  73. package/dist/portal/hooks/usePortalNotifications.js +4 -42
  74. package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
  75. package/dist/portal/utils/nav.js +3 -2
  76. package/dist/portal/utils/nav.js.map +2 -2
  77. package/dist/primitives/button.js +4 -9
  78. package/dist/primitives/button.js.map +2 -2
  79. package/jest.config.cjs +1 -2
  80. package/package.json +18 -21
  81. package/src/ai/AiChat.tsx +1 -30
  82. package/src/ai/__tests__/AiChat.test.tsx +0 -31
  83. package/src/backend/AppShell.tsx +4 -55
  84. package/src/backend/AuthSessionGuard.tsx +0 -29
  85. package/src/backend/BackendChromeProvider.tsx +1 -19
  86. package/src/backend/CrudForm.tsx +4 -18
  87. package/src/backend/DataTable.tsx +19 -36
  88. package/src/backend/JsonBuilder.tsx +3 -3
  89. package/src/backend/NextStepCallout.tsx +4 -4
  90. package/src/backend/ProfileDropdown.tsx +0 -1
  91. package/src/backend/RowActions.tsx +2 -2
  92. package/src/backend/__tests__/AppShell.test.tsx +7 -65
  93. package/src/backend/__tests__/AuthSessionGuard.test.tsx +0 -52
  94. package/src/backend/__tests__/BackendChromeProvider.test.tsx +2 -98
  95. package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +1 -62
  96. package/src/backend/__tests__/nav-utils.test.ts +0 -139
  97. package/src/backend/confirm-dialog/ConfirmDialog.tsx +1 -16
  98. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +1 -68
  99. package/src/backend/crud/__tests__/useGroupOrder.test.ts +1 -39
  100. package/src/backend/crud/useGroupOrder.ts +25 -30
  101. package/src/backend/detail/AddressTiles.tsx +6 -3
  102. package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
  103. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  104. package/src/backend/detail/AttachmentsSection.tsx +1 -1
  105. package/src/backend/detail/NotesSection.tsx +1 -1
  106. package/src/backend/detail/TagsSection.tsx +1 -1
  107. package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +0 -21
  108. package/src/backend/forms/FormHeader.tsx +2 -1
  109. package/src/backend/icons/lucideRegistry.generated.tsx +37 -3
  110. package/src/backend/icons/lucideRegistry.ts +2 -2
  111. package/src/backend/injection/spotIds.ts +18 -23
  112. package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
  113. package/src/backend/schedule/ScheduleGrid.tsx +3 -3
  114. package/src/backend/schedule/ScheduleToolbar.tsx +68 -89
  115. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +3 -42
  116. package/src/backend/sidebar/customization-helpers.ts +0 -47
  117. package/src/backend/utils/__tests__/api.test.ts +27 -0
  118. package/src/backend/utils/__tests__/apiCall.test.ts +0 -61
  119. package/src/backend/utils/api.ts +20 -3
  120. package/src/backend/utils/apiCall.ts +1 -29
  121. package/src/backend/utils/customFieldDefs.ts +0 -1
  122. package/src/backend/utils/customFieldForms.ts +11 -15
  123. package/src/backend/utils/nav.ts +2 -22
  124. package/src/frontend/LanguageSwitcher.tsx +0 -1
  125. package/src/portal/PortalContext.tsx +1 -1
  126. package/src/portal/hooks/usePortalEventBridge.ts +0 -6
  127. package/src/portal/hooks/usePortalNotifications.ts +12 -55
  128. package/src/portal/utils/__tests__/nav.test.ts +2 -2
  129. package/src/portal/utils/nav.ts +4 -3
  130. package/src/primitives/button.tsx +4 -10
  131. package/dist/backend/icons/lucideRegistryRuntime.js +0 -28
  132. package/dist/backend/icons/lucideRegistryRuntime.js.map +0 -7
  133. package/dist/backend/perspectiveState.js +0 -34
  134. package/dist/backend/perspectiveState.js.map +0 -7
  135. package/dist/backend/utils/fetchAllPages.js +0 -57
  136. package/dist/backend/utils/fetchAllPages.js.map +0 -7
  137. package/dist/portal/hooks/portalBridgeStatus.js +0 -25
  138. package/dist/portal/hooks/portalBridgeStatus.js.map +0 -7
  139. package/figma/alert.figma.tsx +0 -35
  140. package/figma/badge.figma.tsx +0 -33
  141. package/figma/button.figma.tsx +0 -33
  142. package/figma/checkbox.figma.tsx +0 -28
  143. package/figma/drawer.figma.tsx +0 -40
  144. package/figma/input-variants.figma.tsx +0 -58
  145. package/figma/radio.figma.tsx +0 -16
  146. package/figma/select.figma.tsx +0 -24
  147. package/figma/status-badge.figma.tsx +0 -24
  148. package/figma/switch.figma.tsx +0 -28
  149. package/figma/tabs.figma.tsx +0 -22
  150. package/figma/tag.figma.tsx +0 -27
  151. package/figma.config.json +0 -6
  152. package/scripts/lucideRegistrySource.cjs +0 -133
  153. package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +0 -158
  154. package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +0 -98
  155. package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +0 -105
  156. package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +0 -177
  157. package/src/backend/icons/lucideRegistryRuntime.tsx +0 -36
  158. package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +0 -38
  159. package/src/backend/perspectiveState.ts +0 -44
  160. package/src/backend/sidebar/__tests__/group-visibility.test.ts +0 -131
  161. package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +0 -95
  162. package/src/backend/utils/__tests__/fetchAllPages.test.ts +0 -103
  163. package/src/backend/utils/fetchAllPages.ts +0 -93
  164. package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +0 -66
  165. package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +0 -155
  166. package/src/portal/hooks/portalBridgeStatus.ts +0 -29
  167. package/src/primitives/__tests__/button.test.tsx +0 -70
@@ -3,14 +3,8 @@
3
3
  */
4
4
 
5
5
  import * as React from 'react'
6
- import { act, render, screen, waitFor } from '@testing-library/react'
7
- import { emitOrganizationScopeChanged } from '@open-mercato/shared/lib/frontend/organizationEvents'
8
- import type { BackendChromePayload } from '@open-mercato/shared/modules/navigation/backendChrome'
9
- import {
10
- BackendChromeProvider,
11
- useBackendChrome,
12
- useCurrentOrganization,
13
- } from '../BackendChromeProvider'
6
+ import { render, screen, waitFor } from '@testing-library/react'
7
+ import { BackendChromeProvider, useBackendChrome } from '../BackendChromeProvider'
14
8
  import { apiCall } from '../utils/apiCall'
15
9
 
16
10
  jest.mock('../utils/apiCall', () => ({
@@ -27,30 +21,6 @@ function ChromeStateProbe() {
27
21
  )
28
22
  }
29
23
 
30
- function CurrentOrganizationProbe() {
31
- const organization = useCurrentOrganization()
32
- return (
33
- <span data-testid="current-organization">
34
- {organization ? `${organization.id}:${organization.name}` : 'none'}
35
- </span>
36
- )
37
- }
38
-
39
- function makePayload(
40
- currentOrganization: BackendChromePayload['currentOrganization'],
41
- ): BackendChromePayload {
42
- return {
43
- groups: [],
44
- settingsSections: [],
45
- settingsPathPrefixes: [],
46
- profileSections: [],
47
- profilePathPrefixes: [],
48
- grantedFeatures: [],
49
- roles: [],
50
- currentOrganization,
51
- }
52
- }
53
-
54
24
  describe('BackendChromeProvider', () => {
55
25
  beforeEach(() => {
56
26
  ;(apiCall as jest.Mock).mockReset()
@@ -72,70 +42,4 @@ describe('BackendChromeProvider', () => {
72
42
  expect(screen.getByTestId('ready')).toHaveTextContent('not-ready')
73
43
  expect(apiCall).toHaveBeenCalledWith('/api/auth/admin/nav', { credentials: 'include' })
74
44
  })
75
-
76
- it('returns null outside a provider', () => {
77
- render(<CurrentOrganizationProbe />)
78
-
79
- expect(screen.getByTestId('current-organization')).toHaveTextContent('none')
80
- })
81
-
82
- it('returns null before the chrome payload arrives', () => {
83
- ;(apiCall as jest.Mock).mockReturnValue(new Promise(() => {}))
84
-
85
- render(
86
- <BackendChromeProvider adminNavApi="/api/auth/admin/nav?test=pending">
87
- <CurrentOrganizationProbe />
88
- </BackendChromeProvider>,
89
- )
90
-
91
- expect(screen.getByTestId('current-organization')).toHaveTextContent('none')
92
- })
93
-
94
- it('returns the organization from the hydrated payload', async () => {
95
- ;(apiCall as jest.Mock).mockResolvedValue({
96
- ok: true,
97
- result: makePayload({ id: 'org-1', name: 'Northwind Ltd' }),
98
- })
99
-
100
- render(
101
- <BackendChromeProvider adminNavApi="/api/auth/admin/nav?test=hydrated">
102
- <CurrentOrganizationProbe />
103
- </BackendChromeProvider>,
104
- )
105
-
106
- await waitFor(() => {
107
- expect(screen.getByTestId('current-organization')).toHaveTextContent('org-1:Northwind Ltd')
108
- })
109
- })
110
-
111
- it('refreshes the organization after the active scope changes', async () => {
112
- ;(apiCall as jest.Mock)
113
- .mockResolvedValueOnce({
114
- ok: true,
115
- result: makePayload({ id: 'org-1', name: 'Northwind Ltd' }),
116
- })
117
- .mockResolvedValueOnce({
118
- ok: true,
119
- result: makePayload({ id: 'org-2', name: 'Contoso GmbH' }),
120
- })
121
-
122
- render(
123
- <BackendChromeProvider adminNavApi="/api/auth/admin/nav?test=scope-refresh">
124
- <CurrentOrganizationProbe />
125
- </BackendChromeProvider>,
126
- )
127
-
128
- await waitFor(() => {
129
- expect(screen.getByTestId('current-organization')).toHaveTextContent('org-1:Northwind Ltd')
130
- })
131
-
132
- act(() => {
133
- emitOrganizationScopeChanged({ organizationId: 'org-2', tenantId: 'tenant-1' })
134
- })
135
-
136
- await waitFor(() => {
137
- expect(screen.getByTestId('current-organization')).toHaveTextContent('org-2:Contoso GmbH')
138
- })
139
- expect(apiCall).toHaveBeenCalledTimes(2)
140
- })
141
45
  })
@@ -1,8 +1,7 @@
1
1
  /** @jest-environment jsdom */
2
- import { clearAllPerspectiveState, readPerspectiveSnapshot, writePerspectiveSnapshot } from '../DataTable'
2
+ import { readPerspectiveSnapshot, writePerspectiveSnapshot } from '../DataTable'
3
3
 
4
4
  const PREFIX = 'om_table_perspective_snapshot'
5
- const COOKIE_PREFIX = 'om_table_perspective'
6
5
 
7
6
  describe('DataTable perspective snapshot storage (versioned envelope)', () => {
8
7
  beforeEach(() => {
@@ -53,63 +52,3 @@ describe('DataTable perspective snapshot storage (versioned envelope)', () => {
53
52
  expect(localStorage.getItem(`${PREFIX}:tbl5`)).toBeNull()
54
53
  })
55
54
  })
56
-
57
- describe('clearAllPerspectiveState (tenant-isolation at the auth identity boundary, #4185)', () => {
58
- beforeEach(() => {
59
- localStorage.clear()
60
- for (const cookie of document.cookie ? document.cookie.split(';') : []) {
61
- const name = cookie.split('=')[0]?.trim()
62
- if (name) document.cookie = `${name}=; Path=/; Max-Age=0`
63
- }
64
- })
65
-
66
- it('removes every perspective snapshot across all tables', () => {
67
- writePerspectiveSnapshot('customers-companies', {
68
- perspectiveId: null,
69
- settings: { columnSizing: { name: 210, status: 60 } },
70
- updatedAt: 1,
71
- })
72
- writePerspectiveSnapshot('customers-people', {
73
- perspectiveId: null,
74
- settings: { columnSizing: { name: 300 } },
75
- updatedAt: 2,
76
- })
77
-
78
- clearAllPerspectiveState()
79
-
80
- expect(localStorage.getItem(`${PREFIX}:customers-companies`)).toBeNull()
81
- expect(localStorage.getItem(`${PREFIX}:customers-people`)).toBeNull()
82
- expect(readPerspectiveSnapshot('customers-companies')).toBeNull()
83
- expect(readPerspectiveSnapshot('customers-people')).toBeNull()
84
- })
85
-
86
- it('leaves unrelated localStorage keys untouched', () => {
87
- writePerspectiveSnapshot('customers-companies', {
88
- perspectiveId: null,
89
- settings: { columnSizing: { name: 210 } },
90
- updatedAt: 1,
91
- })
92
- localStorage.setItem('om_login_tenant', 'acme')
93
- localStorage.setItem('om:auth:identity', '123')
94
-
95
- clearAllPerspectiveState()
96
-
97
- expect(localStorage.getItem(`${PREFIX}:customers-companies`)).toBeNull()
98
- expect(localStorage.getItem('om_login_tenant')).toBe('acme')
99
- expect(localStorage.getItem('om:auth:identity')).toBe('123')
100
- })
101
-
102
- it('expires the active-view perspective cookies', () => {
103
- document.cookie = `${COOKIE_PREFIX}:customers-companies=view-1; Path=/`
104
- expect(document.cookie).toContain(`${COOKIE_PREFIX}:customers-companies=view-1`)
105
-
106
- clearAllPerspectiveState()
107
-
108
- expect(document.cookie).not.toContain(`${COOKIE_PREFIX}:customers-companies`)
109
- })
110
-
111
- it('is a no-op when nothing is stored', () => {
112
- expect(() => clearAllPerspectiveState()).not.toThrow()
113
- expect(localStorage.length).toBe(0)
114
- })
115
- })
@@ -202,143 +202,4 @@ describe('settings navigation helpers', () => {
202
202
  )
203
203
  expect(allSettingsHrefs).not.toContain('/backend/config/system-status')
204
204
  })
205
-
206
- it('orders settings sections by the untranslated group id, not the rendered label', () => {
207
- const polishEntry = (
208
- group: string,
209
- groupKey: string,
210
- href: string,
211
- ): AdminNavItem => ({
212
- group,
213
- groupId: groupKey,
214
- groupKey,
215
- groupDefaultName: group,
216
- title: href,
217
- defaultTitle: href,
218
- href,
219
- enabled: true,
220
- order: 10,
221
- pageContext: 'settings',
222
- })
223
-
224
- const entries: AdminNavItem[] = [
225
- polishEntry('System', 'settings.sections.system', '/backend/config/system-status'),
226
- polishEntry('Konfiguracja modułów', 'settings.sections.moduleConfigs', '/backend/config/wms'),
227
- polishEntry('Autoryzacja', 'settings.sections.auth', '/backend/users'),
228
- ]
229
-
230
- const sections = buildSettingsSections(entries, {
231
- 'settings.sections.system': 1,
232
- 'settings.sections.auth': 2,
233
- 'settings.sections.moduleConfigs': 5,
234
- })
235
-
236
- expect(sections.map((section) => section.id)).toEqual([
237
- 'settings.sections.system',
238
- 'settings.sections.auth',
239
- 'settings.sections.moduleConfigs',
240
- ])
241
- expect(sections.map((section) => section.order)).toEqual([1, 2, 5])
242
- })
243
-
244
- it('keeps the section label and labelKey of the rendered group', () => {
245
- const entries: AdminNavItem[] = [
246
- {
247
- group: 'Konfiguracja modułów',
248
- groupId: 'settings.sections.moduleConfigs',
249
- groupKey: 'settings.sections.moduleConfigs',
250
- groupDefaultName: 'Konfiguracja modułów',
251
- title: 'WMS',
252
- defaultTitle: 'WMS',
253
- href: '/backend/config/wms',
254
- enabled: true,
255
- pageContext: 'settings',
256
- },
257
- ]
258
-
259
- const [section] = buildSettingsSections(entries, { 'settings.sections.moduleConfigs': 5 })
260
-
261
- expect(section.label).toBe('Konfiguracja modułów')
262
- expect(section.labelKey).toBe('settings.sections.moduleConfigs')
263
- })
264
-
265
- it('still honors legacy sectionOrder maps keyed by the English label slug', () => {
266
- const entries: AdminNavItem[] = [
267
- {
268
- group: 'Module Configs',
269
- groupId: 'settings.sections.moduleConfigs',
270
- groupKey: 'settings.sections.moduleConfigs',
271
- groupDefaultName: 'Module Configs',
272
- title: 'WMS',
273
- defaultTitle: 'WMS',
274
- href: '/backend/config/wms',
275
- enabled: true,
276
- pageContext: 'settings',
277
- },
278
- ]
279
-
280
- const [section] = buildSettingsSections(entries, { 'module-configs': 5 })
281
-
282
- expect(section.order).toBe(5)
283
- })
284
-
285
- it('falls back to the catch-all weight for groups the order map does not name', () => {
286
- const entries: AdminNavItem[] = [
287
- {
288
- group: 'Bezpieczeństwo',
289
- groupId: 'settings.sections.security',
290
- groupKey: 'settings.sections.security',
291
- groupDefaultName: 'Bezpieczeństwo',
292
- title: 'Audit Logs',
293
- defaultTitle: 'Audit Logs',
294
- href: '/backend/audit-logs',
295
- enabled: true,
296
- pageContext: 'settings',
297
- },
298
- ]
299
-
300
- const [section] = buildSettingsSections(entries, { 'settings.sections.system': 1 })
301
-
302
- expect(section.order).toBe(999)
303
- })
304
-
305
- // A partially translated locale renders `translate(groupKey, group)` as the localized label for
306
- // pages whose key is covered and as the raw English fallback for the rest, so one group can reach
307
- // buildSettingsSections under two different labels.
308
- it('groups entries that share a group id even when labels differ', () => {
309
- const entries: AdminNavItem[] = [
310
- {
311
- group: 'Konfiguracja modułów',
312
- groupId: 'settings.sections.moduleConfigs',
313
- groupKey: 'settings.sections.moduleConfigs',
314
- groupDefaultName: 'Konfiguracja modułów',
315
- title: 'WMS',
316
- defaultTitle: 'WMS',
317
- href: '/backend/config/wms',
318
- enabled: true,
319
- order: 20,
320
- pageContext: 'settings',
321
- },
322
- {
323
- group: 'Module Configs',
324
- groupId: 'settings.sections.moduleConfigs',
325
- groupKey: 'settings.sections.moduleConfigs',
326
- groupDefaultName: 'Module Configs',
327
- title: 'Sales',
328
- defaultTitle: 'Sales',
329
- href: '/backend/config/sales',
330
- enabled: true,
331
- order: 10,
332
- pageContext: 'settings',
333
- },
334
- ]
335
-
336
- const sections = buildSettingsSections(entries, { 'settings.sections.moduleConfigs': 5 })
337
-
338
- expect(sections).toHaveLength(1)
339
- expect(sections[0].items.map((item) => item.href)).toEqual([
340
- '/backend/config/sales',
341
- '/backend/config/wms',
342
- ])
343
- })
344
205
  })
@@ -136,21 +136,6 @@ export function ConfirmDialog({
136
136
  return () => dialog.removeEventListener("cancel", handleCancel);
137
137
  }, [loading, requestClose]);
138
138
 
139
- React.useEffect(() => {
140
- if (!open) return;
141
-
142
- const handleEscape = (event: KeyboardEvent) => {
143
- if (event.key !== "Escape") return;
144
- if (!(event.target instanceof Node) || !dialogRef.current?.contains(event.target)) return;
145
- event.preventDefault();
146
- event.stopPropagation();
147
- if (!loading) requestClose();
148
- };
149
-
150
- window.addEventListener("keydown", handleEscape, true);
151
- return () => window.removeEventListener("keydown", handleEscape, true);
152
- }, [loading, open, requestClose]);
153
-
154
139
  // Handle backdrop click
155
140
  const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
156
141
  // Only close if clicking directly on the dialog (backdrop), not its children
@@ -277,7 +262,7 @@ export function ConfirmDialog({
277
262
  {resolvedConfirmText !== false && (
278
263
  <Button
279
264
  ref={confirmButtonRef}
280
- variant={variant === "destructive" ? "destructive-solid" : "default"}
265
+ variant={variant === "destructive" ? "destructive" : "default"}
281
266
  onClick={handleConfirm}
282
267
  disabled={loading}
283
268
  className="w-full sm:w-auto"
@@ -2,7 +2,7 @@
2
2
  * @jest-environment jsdom
3
3
  */
4
4
  import * as React from 'react'
5
- import { fireEvent, screen, within } from '@testing-library/react'
5
+ import { screen } from '@testing-library/react'
6
6
  import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
7
7
  import { ConfirmDialog } from '../ConfirmDialog'
8
8
 
@@ -60,71 +60,4 @@ describe('ConfirmDialog', () => {
60
60
  expect(container.contains(dialog)).toBe(false)
61
61
  expect(dialog.parentElement).toBe(document.body)
62
62
  })
63
-
64
- it('renders the destructive confirmation as the solid variant', () => {
65
- renderWithProviders(
66
- <ConfirmDialog
67
- open
68
- variant="destructive"
69
- onOpenChange={() => undefined}
70
- onConfirm={() => undefined}
71
- title="Delete customer?"
72
- confirmText="Delete"
73
- cancelText="Cancel"
74
- />,
75
- )
76
-
77
- const classNames = screen.getByRole('button', { name: 'Delete' }).className.split(/\s+/)
78
- expect(classNames).toContain('bg-destructive')
79
- expect(classNames).toContain('text-white')
80
- })
81
-
82
- it('keeps the default confirmation on the primary variant', () => {
83
- renderWithProviders(
84
- <ConfirmDialog
85
- open
86
- onOpenChange={() => undefined}
87
- onConfirm={() => undefined}
88
- title="Publish article?"
89
- confirmText="Publish"
90
- cancelText="Cancel"
91
- />,
92
- )
93
-
94
- const classNames = screen.getByRole('button', { name: 'Publish' }).className.split(/\s+/)
95
- expect(classNames).toContain('bg-primary')
96
- expect(classNames).not.toContain('bg-destructive')
97
- })
98
-
99
- it('closes only the confirmation that owns an Escape event', () => {
100
- const firstOpenChange = jest.fn()
101
- const secondOpenChange = jest.fn()
102
-
103
- renderWithProviders(
104
- <>
105
- <ConfirmDialog
106
- open
107
- onOpenChange={firstOpenChange}
108
- onConfirm={() => undefined}
109
- title="First confirmation"
110
- confirmText="Confirm first"
111
- cancelText="Cancel first"
112
- />
113
- <ConfirmDialog
114
- open
115
- onOpenChange={secondOpenChange}
116
- onConfirm={() => undefined}
117
- title="Second confirmation"
118
- confirmText="Confirm second"
119
- cancelText="Cancel second"
120
- />
121
- </>,
122
- )
123
-
124
- const secondDialog = screen.getByRole('alertdialog', { name: 'Second confirmation' })
125
- fireEvent.keyDown(within(secondDialog).getByRole('button', { name: 'Cancel second' }), { key: 'Escape' })
126
-
127
- expect(firstOpenChange).not.toHaveBeenCalled()
128
- expect(secondOpenChange).toHaveBeenCalledWith(false)
129
- })
130
63
  })
@@ -1,6 +1,5 @@
1
1
  /** @jest-environment jsdom */
2
- import * as React from 'react'
3
- import { act, render, renderHook, screen } from '@testing-library/react'
2
+ import { act, renderHook } from '@testing-library/react'
4
3
  import { useGroupOrder } from '../useGroupOrder'
5
4
 
6
5
  describe('useGroupOrder', () => {
@@ -37,16 +36,6 @@ describe('useGroupOrder', () => {
37
36
  expect(result.current.orderedIds).toEqual(['a', 'd', 'b', 'c'])
38
37
  })
39
38
 
40
- it('composes two reorder() calls made within a single commit', () => {
41
- const { result } = renderHook(() => useGroupOrder('people', ['a', 'b', 'c']))
42
- act(() => {
43
- result.current.reorder(0, 2)
44
- result.current.reorder(0, 1)
45
- })
46
- expect(result.current.orderedIds).toEqual(['c', 'b', 'a'])
47
- expect(JSON.parse(localStorage.getItem('om:group-order:people')!)).toEqual(['c', 'b', 'a'])
48
- })
49
-
50
39
  it('updates ordering when defaults change to include a new id', () => {
51
40
  const { result, rerender } = renderHook(
52
41
  ({ ids }) => useGroupOrder('p', ids),
@@ -71,31 +60,4 @@ describe('useGroupOrder', () => {
71
60
  expect(spy).not.toHaveBeenCalledWith('om:group-order:p', expect.anything())
72
61
  spy.mockRestore()
73
62
  })
74
-
75
- it('keeps a stable orderedIds identity when defaults are recreated with equal content', () => {
76
- const { result, rerender } = renderHook(
77
- ({ ids }) => useGroupOrder('p', ids),
78
- { initialProps: { ids: ['a', 'b'] } },
79
- )
80
- const first = result.current.orderedIds
81
- rerender({ ids: ['a', 'b'] })
82
- expect(result.current.orderedIds).toBe(first)
83
- })
84
-
85
- it('does not loop when the host recreates defaults with different content on every render (#4386)', () => {
86
- localStorage.setItem('om:group-order:unstable', JSON.stringify(['b', 'a']))
87
- let renders = 0
88
- function UnstableHost() {
89
- renders += 1
90
- if (renders > 50) throw new Error('render loop detected')
91
- const defaults = renders % 2 === 1 ? ['a', 'b'] : ['a', 'b', `extra-${renders}`]
92
- const { orderedIds } = useGroupOrder('unstable', defaults)
93
- return React.createElement('div', { 'data-testid': 'order' }, orderedIds.join(','))
94
- }
95
-
96
- render(React.createElement(UnstableHost))
97
-
98
- expect(renders).toBeLessThanOrEqual(6)
99
- expect(screen.getByTestId('order').textContent).toContain('b,a')
100
- })
101
63
  })
@@ -31,46 +31,41 @@ function arraysEqual(a: string[], b: string[]): boolean {
31
31
  /**
32
32
  * Returns the group IDs in the user's preferred order.
33
33
  * Falls back to the default order when no preference is stored.
34
- *
35
- * State holds only the saved preference; the visible order is derived during
36
- * render. Syncing derived order back into state via an effect looped forever
37
- * when a host recreated `defaultGroupIds` with different content on every
38
- * render (#4386), so no effect writes state from `defaultGroupIds` here.
39
- *
40
- * `stableIdsRef` is mutated during render, which React's render-purity rule
41
- * normally forbids. It is safe here because the guard swaps the reference only
42
- * when the *content* differs: a concurrent render that React discards can leave
43
- * behind a different array identity, but never a content-stale value, and the
44
- * render that commits always leaves `stableIdsRef.current` content-equal to its
45
- * own `mergedIds`. `reorder` advances the same ref from its event handler so
46
- * that several reorders within one commit compose instead of overwriting each
47
- * other; a discarded state update leaves the ref ahead of `savedOrder` only
48
- * until the next render's content guard pulls it back (#4691).
49
34
  */
50
35
  export function useGroupOrder(pageType: string, defaultGroupIds: string[]) {
51
- const [savedOrder, setSavedOrder] = React.useState<string[] | null>(null)
36
+ const [orderedIds, setOrderedIds] = React.useState<string[]>(defaultGroupIds)
37
+ const mounted = React.useRef(false)
52
38
 
53
39
  React.useEffect(() => {
40
+ mounted.current = true
54
41
  const saved = readJsonFromLocalStorage<string[] | null>(getStorageKey(pageType), null)
55
- setSavedOrder(Array.isArray(saved) ? saved : null)
42
+ if (Array.isArray(saved)) {
43
+ setOrderedIds(mergeOrder(saved, defaultGroupIds))
44
+ }
45
+ // Intentionally only runs on mount (per pageType); defaultGroupIds changes are
46
+ // handled by the sync effect below.
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
48
  }, [pageType])
57
49
 
58
- const mergedIds = savedOrder ? mergeOrder(savedOrder, defaultGroupIds) : defaultGroupIds
59
-
60
- const stableIdsRef = React.useRef(mergedIds)
61
- if (!arraysEqual(stableIdsRef.current, mergedIds)) {
62
- stableIdsRef.current = mergedIds
63
- }
64
- const orderedIds = stableIdsRef.current
50
+ // Sync when defaultGroupIds changes (e.g. new groups added dynamically)
51
+ React.useEffect(() => {
52
+ setOrderedIds((prev) => {
53
+ const merged = mergeOrder(prev, defaultGroupIds)
54
+ return arraysEqual(prev, merged) ? prev : merged
55
+ })
56
+ }, [defaultGroupIds])
65
57
 
66
58
  const reorder = React.useCallback(
67
59
  (fromIndex: number, toIndex: number) => {
68
- const next = [...stableIdsRef.current]
69
- const [moved] = next.splice(fromIndex, 1)
70
- next.splice(toIndex, 0, moved)
71
- stableIdsRef.current = next
72
- writeJsonToLocalStorage(getStorageKey(pageType), next)
73
- setSavedOrder(next)
60
+ setOrderedIds((prev) => {
61
+ const next = [...prev]
62
+ const [moved] = next.splice(fromIndex, 1)
63
+ next.splice(toIndex, 0, moved)
64
+ if (mounted.current) {
65
+ writeJsonToLocalStorage(getStorageKey(pageType), next)
66
+ }
67
+ return next
68
+ })
74
69
  },
75
70
  [pageType],
76
71
  )
@@ -7,7 +7,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
7
  import { TabEmptyState } from '@open-mercato/ui/backend/detail'
8
8
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
9
9
  import { normalizeCoordinateInput, validateCoordinateInput } from '@open-mercato/shared/lib/location/coordinates'
10
- import { AddressView, type AddressFormatStrategy } from './addressFormat'
10
+ import { AddressView, formatAddressString, type AddressFormatStrategy } from './addressFormat'
11
11
  import AddressEditor, { type AddressTypesAdapter } from './AddressEditor'
12
12
  import {
13
13
  Dialog,
@@ -176,7 +176,7 @@ export function AddressTiles<C = unknown>({
176
176
  t,
177
177
  emptyLabel,
178
178
  isSubmitting = false,
179
- gridClassName = 'grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2 sm:gap-4',
179
+ gridClassName = 'grid grid-cols-1 gap-2 sm:gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4',
180
180
  hideAddButton = false,
181
181
  onAddActionChange,
182
182
  emptyStateTitle,
@@ -480,7 +480,7 @@ export function AddressTiles<C = unknown>({
480
480
  addressTypesAdapter={addressTypesAdapter}
481
481
  addressTypesContext={addressTypesContext}
482
482
  />
483
- {generalError ? <p className="text-xs text-status-error-text">{generalError}</p> : null}
483
+ {generalError ? <p className="text-xs text-red-600">{generalError}</p> : null}
484
484
  <div className="flex flex-wrap justify-end gap-2">
485
485
  <Button type="button" variant="outline" onClick={handleCancel} disabled={disableActions}>
486
486
  {label('cancel', 'Cancel')}
@@ -569,6 +569,9 @@ export function AddressTiles<C = unknown>({
569
569
  </p>
570
570
  ) : null}
571
571
  <AddressView address={address} format={format} className="text-sm text-foreground" />
572
+ <p className="text-xs text-muted-foreground">
573
+ {formatAddressString(address, format)}
574
+ </p>
572
575
  </div>
573
576
  <div className="flex items-center gap-1 opacity-100 md:opacity-0 transition-opacity md:group-hover:opacity-100 focus-within:opacity-100">
574
577
  <Button
@@ -38,7 +38,7 @@ export function AttachmentDeleteDialog({ open, onOpenChange, fileName, onConfirm
38
38
  <Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isDeleting}>
39
39
  {t('attachments.library.metadata.cancel', 'Cancel')}
40
40
  </Button>
41
- <Button type="button" variant="destructive-solid" onClick={onConfirm} disabled={isDeleting}>
41
+ <Button type="button" variant="destructive" onClick={onConfirm} disabled={isDeleting}>
42
42
  {t('attachments.library.actions.delete', 'Delete')}
43
43
  </Button>
44
44
  </DialogFooter>
@@ -77,7 +77,7 @@ type AttachmentMetadataFormValues = {
77
77
  assignments?: AssignmentDraft[]
78
78
  } & Record<string, unknown>
79
79
 
80
- export type AssignmentEditorLabels = {
80
+ type AssignmentEditorLabels = {
81
81
  title: string
82
82
  description: string
83
83
  type: string
@@ -256,7 +256,7 @@ function AttachmentsSectionImpl({
256
256
  </div>
257
257
  )}
258
258
 
259
- {error ? <p className="text-xs font-medium text-status-error-text">{error}</p> : null}
259
+ {error ? <p className="text-xs font-medium text-red-600">{error}</p> : null}
260
260
 
261
261
  {loading ? (
262
262
  <div className="text-sm text-muted-foreground">{t('attachments.library.loading', 'Loading attachments…')}</div>
@@ -1256,7 +1256,7 @@ function NotesSectionImpl<C = unknown>({
1256
1256
  editorWrapperClassName="w-full rounded-md border border-muted-foreground/20 bg-background p-2"
1257
1257
  remarkPlugins={markdownPlugins}
1258
1258
  />
1259
- {contentError ? <p className="text-xs text-status-error-text">{contentError}</p> : null}
1259
+ {contentError ? <p className="text-xs text-red-600">{contentError}</p> : null}
1260
1260
  <div className="flex flex-wrap items-center gap-2">
1261
1261
  <Button type="button" size="sm" onClick={handleContentSave} disabled={contentSavingId === note.id}>
1262
1262
  {contentSavingId === note.id ? (
@@ -387,7 +387,7 @@ function TagsSectionImpl({
387
387
  loadSuggestions={loadSuggestions}
388
388
  autoFocus={!autoSave}
389
389
  />
390
- {error ? <p className="text-xs text-status-error-text">{error}</p> : null}
390
+ {error ? <p className="text-xs text-red-600">{error}</p> : null}
391
391
  {autoSave ? null : (
392
392
  <div className="flex items-center gap-2 mt-3 mb-2">
393
393
  <Button type="button" size="sm" onClick={handleSave} disabled={saving || isSubmitting}>