@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -4
- package/agentic/standalone-guide.md +1 -1
- package/build.mjs +54 -2
- package/dist/ai/AiChat.js +2 -21
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/backend/AppShell.js +4 -43
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/AuthSessionGuard.js +0 -14
- package/dist/backend/AuthSessionGuard.js.map +2 -2
- package/dist/backend/BackendChromeProvider.js +1 -6
- package/dist/backend/BackendChromeProvider.js.map +2 -2
- package/dist/backend/CrudForm.js +4 -6
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +18 -20
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/JsonBuilder.js +3 -3
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/NextStepCallout.js +4 -4
- package/dist/backend/NextStepCallout.js.map +1 -1
- package/dist/backend/ProfileDropdown.js +1 -2
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/RowActions.js +2 -2
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +1 -13
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/crud/useGroupOrder.js +21 -14
- package/dist/backend/crud/useGroupOrder.js.map +2 -2
- package/dist/backend/detail/AddressTiles.js +5 -4
- package/dist/backend/detail/AddressTiles.js.map +2 -2
- package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
- package/dist/backend/detail/AttachmentsSection.js +1 -1
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +1 -1
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/detail/TagsSection.js +1 -1
- package/dist/backend/detail/TagsSection.js.map +2 -2
- package/dist/backend/forms/FormHeader.js +2 -1
- package/dist/backend/forms/FormHeader.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +25 -1
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.js +3 -3
- package/dist/backend/icons/lucideRegistry.js.map +2 -2
- package/dist/backend/injection/spotIds.js +18 -23
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/schedule/ScheduleAgenda.js +3 -3
- package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
- package/dist/backend/schedule/ScheduleGrid.js +3 -3
- package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
- package/dist/backend/schedule/ScheduleToolbar.js +70 -87
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +2 -30
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/sidebar/customization-helpers.js +0 -30
- package/dist/backend/sidebar/customization-helpers.js.map +2 -2
- package/dist/backend/utils/api.js +13 -3
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/apiCall.js +1 -21
- package/dist/backend/utils/apiCall.js.map +2 -2
- package/dist/backend/utils/customFieldDefs.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +9 -10
- package/dist/backend/utils/customFieldForms.js.map +2 -2
- package/dist/backend/utils/nav.js +2 -5
- package/dist/backend/utils/nav.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +1 -2
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/portal/PortalContext.js +1 -1
- package/dist/portal/PortalContext.js.map +2 -2
- package/dist/portal/hooks/usePortalEventBridge.js +0 -6
- package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
- package/dist/portal/hooks/usePortalNotifications.js +4 -42
- package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
- package/dist/portal/utils/nav.js +3 -2
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/button.js +4 -9
- package/dist/primitives/button.js.map +2 -2
- package/jest.config.cjs +1 -2
- package/package.json +18 -21
- package/src/ai/AiChat.tsx +1 -30
- package/src/ai/__tests__/AiChat.test.tsx +0 -31
- package/src/backend/AppShell.tsx +4 -55
- package/src/backend/AuthSessionGuard.tsx +0 -29
- package/src/backend/BackendChromeProvider.tsx +1 -19
- package/src/backend/CrudForm.tsx +4 -18
- package/src/backend/DataTable.tsx +19 -36
- package/src/backend/JsonBuilder.tsx +3 -3
- package/src/backend/NextStepCallout.tsx +4 -4
- package/src/backend/ProfileDropdown.tsx +0 -1
- package/src/backend/RowActions.tsx +2 -2
- package/src/backend/__tests__/AppShell.test.tsx +7 -65
- package/src/backend/__tests__/AuthSessionGuard.test.tsx +0 -52
- package/src/backend/__tests__/BackendChromeProvider.test.tsx +2 -98
- package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +1 -62
- package/src/backend/__tests__/nav-utils.test.ts +0 -139
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +1 -16
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +1 -68
- package/src/backend/crud/__tests__/useGroupOrder.test.ts +1 -39
- package/src/backend/crud/useGroupOrder.ts +25 -30
- package/src/backend/detail/AddressTiles.tsx +6 -3
- package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/AttachmentsSection.tsx +1 -1
- package/src/backend/detail/NotesSection.tsx +1 -1
- package/src/backend/detail/TagsSection.tsx +1 -1
- package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +0 -21
- package/src/backend/forms/FormHeader.tsx +2 -1
- package/src/backend/icons/lucideRegistry.generated.tsx +37 -3
- package/src/backend/icons/lucideRegistry.ts +2 -2
- package/src/backend/injection/spotIds.ts +18 -23
- package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
- package/src/backend/schedule/ScheduleGrid.tsx +3 -3
- package/src/backend/schedule/ScheduleToolbar.tsx +68 -89
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +3 -42
- package/src/backend/sidebar/customization-helpers.ts +0 -47
- package/src/backend/utils/__tests__/api.test.ts +27 -0
- package/src/backend/utils/__tests__/apiCall.test.ts +0 -61
- package/src/backend/utils/api.ts +20 -3
- package/src/backend/utils/apiCall.ts +1 -29
- package/src/backend/utils/customFieldDefs.ts +0 -1
- package/src/backend/utils/customFieldForms.ts +11 -15
- package/src/backend/utils/nav.ts +2 -22
- package/src/frontend/LanguageSwitcher.tsx +0 -1
- package/src/portal/PortalContext.tsx +1 -1
- package/src/portal/hooks/usePortalEventBridge.ts +0 -6
- package/src/portal/hooks/usePortalNotifications.ts +12 -55
- package/src/portal/utils/__tests__/nav.test.ts +2 -2
- package/src/portal/utils/nav.ts +4 -3
- package/src/primitives/button.tsx +4 -10
- package/dist/backend/icons/lucideRegistryRuntime.js +0 -28
- package/dist/backend/icons/lucideRegistryRuntime.js.map +0 -7
- package/dist/backend/perspectiveState.js +0 -34
- package/dist/backend/perspectiveState.js.map +0 -7
- package/dist/backend/utils/fetchAllPages.js +0 -57
- package/dist/backend/utils/fetchAllPages.js.map +0 -7
- package/dist/portal/hooks/portalBridgeStatus.js +0 -25
- package/dist/portal/hooks/portalBridgeStatus.js.map +0 -7
- package/figma/alert.figma.tsx +0 -35
- package/figma/badge.figma.tsx +0 -33
- package/figma/button.figma.tsx +0 -33
- package/figma/checkbox.figma.tsx +0 -28
- package/figma/drawer.figma.tsx +0 -40
- package/figma/input-variants.figma.tsx +0 -58
- package/figma/radio.figma.tsx +0 -16
- package/figma/select.figma.tsx +0 -24
- package/figma/status-badge.figma.tsx +0 -24
- package/figma/switch.figma.tsx +0 -28
- package/figma/tabs.figma.tsx +0 -22
- package/figma/tag.figma.tsx +0 -27
- package/figma.config.json +0 -6
- package/scripts/lucideRegistrySource.cjs +0 -133
- package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +0 -158
- package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +0 -98
- package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +0 -105
- package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +0 -177
- package/src/backend/icons/lucideRegistryRuntime.tsx +0 -36
- package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +0 -38
- package/src/backend/perspectiveState.ts +0 -44
- package/src/backend/sidebar/__tests__/group-visibility.test.ts +0 -131
- package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +0 -95
- package/src/backend/utils/__tests__/fetchAllPages.test.ts +0 -103
- package/src/backend/utils/fetchAllPages.ts +0 -93
- package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +0 -66
- package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +0 -155
- package/src/portal/hooks/portalBridgeStatus.ts +0 -29
- 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 {
|
|
7
|
-
import {
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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-
|
|
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-
|
|
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
|
|
41
|
+
<Button type="button" variant="destructive" onClick={onConfirm} disabled={isDeleting}>
|
|
42
42
|
{t('attachments.library.actions.delete', 'Delete')}
|
|
43
43
|
</Button>
|
|
44
44
|
</DialogFooter>
|
|
@@ -256,7 +256,7 @@ function AttachmentsSectionImpl({
|
|
|
256
256
|
</div>
|
|
257
257
|
)}
|
|
258
258
|
|
|
259
|
-
{error ? <p className="text-xs font-medium text-
|
|
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-
|
|
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-
|
|
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}>
|