@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
package/src/backend/AppShell.tsx
CHANGED
|
@@ -87,7 +87,6 @@ function isBooleanRecord(value: unknown): value is Record<string, boolean> {
|
|
|
87
87
|
export type ShellLogo = {
|
|
88
88
|
src: string
|
|
89
89
|
alt?: string
|
|
90
|
-
preserveAspectRatio?: boolean
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
export type AppShellProps = {
|
|
@@ -187,56 +186,6 @@ function shouldBypassLogoOptimization(src?: string | null): boolean {
|
|
|
187
186
|
return /^https?:\/\//.test(value) || /^\/api\/attachments\/(?:image|file)\//.test(value)
|
|
188
187
|
}
|
|
189
188
|
|
|
190
|
-
function ShellBrandLogo({
|
|
191
|
-
logo,
|
|
192
|
-
brandName,
|
|
193
|
-
unoptimized,
|
|
194
|
-
compact = false,
|
|
195
|
-
mobile = false,
|
|
196
|
-
}: {
|
|
197
|
-
logo?: ShellLogo
|
|
198
|
-
brandName: string
|
|
199
|
-
unoptimized?: boolean
|
|
200
|
-
compact?: boolean
|
|
201
|
-
mobile?: boolean
|
|
202
|
-
}) {
|
|
203
|
-
const src = logo?.src ?? '/open-mercato.svg'
|
|
204
|
-
const alt = logo?.alt ?? brandName
|
|
205
|
-
const isCustomLogo = Boolean(logo?.src)
|
|
206
|
-
const preserveAspectRatio = Boolean(logo?.preserveAspectRatio)
|
|
207
|
-
if (!isCustomLogo || !preserveAspectRatio) {
|
|
208
|
-
return (
|
|
209
|
-
<Image
|
|
210
|
-
src={src}
|
|
211
|
-
alt={alt}
|
|
212
|
-
width={mobile ? 28 : 40}
|
|
213
|
-
height={mobile ? 28 : 40}
|
|
214
|
-
className={`${mobile ? 'rounded' : 'rounded-full'} shrink-0 object-cover`}
|
|
215
|
-
unoptimized={unoptimized ? true : undefined}
|
|
216
|
-
/>
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const width = compact ? 40 : mobile ? 96 : 120
|
|
221
|
-
const height = mobile ? 28 : 40
|
|
222
|
-
const className = compact
|
|
223
|
-
? 'h-10 max-w-10 w-auto shrink-0 object-contain'
|
|
224
|
-
: mobile
|
|
225
|
-
? 'h-7 max-w-24 w-auto shrink-0 object-contain'
|
|
226
|
-
: 'h-10 max-w-[120px] w-auto shrink-0 object-contain'
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<Image
|
|
230
|
-
src={src}
|
|
231
|
-
alt={alt}
|
|
232
|
-
width={width}
|
|
233
|
-
height={height}
|
|
234
|
-
className={className}
|
|
235
|
-
unoptimized={unoptimized ? true : undefined}
|
|
236
|
-
/>
|
|
237
|
-
)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
189
|
function mergeSidebarItemsWithInjected(
|
|
241
190
|
items: SidebarItem[],
|
|
242
191
|
injectedItems: InjectionMenuItem[],
|
|
@@ -811,7 +760,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
811
760
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
812
761
|
aria-label={t('appShell.goToDashboard')}
|
|
813
762
|
>
|
|
814
|
-
<
|
|
763
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={40} height={40} className="rounded-full shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
815
764
|
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
816
765
|
</Link>
|
|
817
766
|
</div>
|
|
@@ -944,7 +893,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
944
893
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
945
894
|
aria-label={t('appShell.goToDashboard')}
|
|
946
895
|
>
|
|
947
|
-
<
|
|
896
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={40} height={40} className="rounded-full shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
948
897
|
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
949
898
|
</Link>
|
|
950
899
|
</div>
|
|
@@ -1010,7 +959,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1010
959
|
className={`flex items-center gap-3 rounded-xl transition-colors hover:bg-muted ${compact ? 'p-2 justify-center' : 'p-3'}`}
|
|
1011
960
|
aria-label={t('appShell.goToDashboard')}
|
|
1012
961
|
>
|
|
1013
|
-
<
|
|
962
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={40} height={40} className="rounded-full shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
1014
963
|
{!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedBrandName}</span>}
|
|
1015
964
|
</Link>
|
|
1016
965
|
</div>
|
|
@@ -1509,7 +1458,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1509
1458
|
<aside className="absolute left-0 top-0 flex h-full w-[280px] max-w-[85vw] flex-col bg-background border-r shadow-lg overflow-hidden">
|
|
1510
1459
|
<div className="shrink-0 flex items-center justify-between gap-2 border-b px-4 py-3">
|
|
1511
1460
|
<Link href="/backend" className="flex items-center gap-2 min-w-0 text-sm font-semibold" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>
|
|
1512
|
-
<
|
|
1461
|
+
<Image src={resolvedLogo?.src ?? "/open-mercato.svg"} alt={resolvedLogo?.alt ?? resolvedBrandName} width={28} height={28} className="rounded shrink-0" unoptimized={resolvedLogoBypassesOptimization ? true : undefined} />
|
|
1513
1462
|
<span className="truncate">{resolvedBrandName}</span>
|
|
1514
1463
|
</Link>
|
|
1515
1464
|
<IconButton variant="ghost" size="sm" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>
|
|
@@ -1,37 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { apiCall } from './utils/apiCall'
|
|
4
|
-
import { clearAllPerspectiveState } from './perspectiveState'
|
|
5
4
|
|
|
6
5
|
export const AUTH_IDENTITY_STORAGE_KEY = 'om:auth:identity'
|
|
7
6
|
export const AUTH_IDENTITY_BROADCAST_CHANNEL = 'om-auth-identity'
|
|
8
|
-
export const AUTH_IDENTITY_USER_STORAGE_KEY = 'om:auth:identity:user'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Purge browser-local DataTable perspective state whenever the backend renders for a
|
|
12
|
-
* different user than the one it last rendered for (#4185).
|
|
13
|
-
*
|
|
14
|
-
* The login form also purges on submit, but that only fires when its client handler
|
|
15
|
-
* runs: a form submitted before hydration, an SSO/magic-link return, or any other
|
|
16
|
-
* entry point into the backend bypasses it entirely and the previous account's unsaved
|
|
17
|
-
* column widths carry over. Anchoring the purge to the observed server identity covers
|
|
18
|
-
* every route into the backend instead of one form.
|
|
19
|
-
*
|
|
20
|
-
* Missing identity metadata is treated as a legacy browser state and purged once:
|
|
21
|
-
* snapshots may predate this marker and belong to a different account. After the
|
|
22
|
-
* marker is recorded, an account's own unsaved widths survive a plain reload.
|
|
23
|
-
*/
|
|
24
|
-
function purgePerspectiveStateOnIdentityChange(serverUserId: string | null): void {
|
|
25
|
-
if (typeof window === 'undefined' || !serverUserId) return
|
|
26
|
-
try {
|
|
27
|
-
const lastSeenUserId = window.localStorage.getItem(AUTH_IDENTITY_USER_STORAGE_KEY)
|
|
28
|
-
if (lastSeenUserId === serverUserId) return
|
|
29
|
-
clearAllPerspectiveState()
|
|
30
|
-
window.localStorage.setItem(AUTH_IDENTITY_USER_STORAGE_KEY, serverUserId)
|
|
31
|
-
} catch {
|
|
32
|
-
// private mode / quota errors are non-fatal
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
7
|
|
|
36
8
|
type FeatureCheckResponse = {
|
|
37
9
|
ok: boolean
|
|
@@ -52,7 +24,6 @@ export const __reload = {
|
|
|
52
24
|
export function AuthSessionGuard({ serverUserId }: AuthSessionGuardProps) {
|
|
53
25
|
React.useEffect(() => {
|
|
54
26
|
if (typeof window === 'undefined') return
|
|
55
|
-
purgePerspectiveStateOnIdentityChange(serverUserId)
|
|
56
27
|
let cancelled = false
|
|
57
28
|
let reloadScheduled = false
|
|
58
29
|
|
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { apiCall } from './utils/apiCall'
|
|
5
5
|
import { subscribeOrganizationScopeChanged, getCurrentOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/organizationEvents'
|
|
6
|
-
import type {
|
|
7
|
-
BackendChromeCurrentOrganization,
|
|
8
|
-
BackendChromePayload,
|
|
9
|
-
} from '@open-mercato/shared/modules/navigation/backendChrome'
|
|
6
|
+
import type { BackendChromePayload } from '@open-mercato/shared/modules/navigation/backendChrome'
|
|
10
7
|
|
|
11
8
|
type BackendChromeContextValue = {
|
|
12
9
|
payload: BackendChromePayload | null
|
|
@@ -102,18 +99,3 @@ export function useBackendChrome(): BackendChromeContextValue {
|
|
|
102
99
|
refresh: async () => {},
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The organization the backend chrome is currently scoped to, for UI that needs to label
|
|
108
|
-
* "you are viewing: <name>".
|
|
109
|
-
*
|
|
110
|
-
* Reads the payload this provider already holds, so it costs no additional request. Returns `null`
|
|
111
|
-
* under an all-organizations selection, outside a provider, or before the payload has arrived — treat
|
|
112
|
-
* it as "unknown", not as "no organization".
|
|
113
|
-
*
|
|
114
|
-
* Prefer this over `payload.brand`, which only populates when the organization has a logo configured.
|
|
115
|
-
*/
|
|
116
|
-
export function useCurrentOrganization(): BackendChromeCurrentOrganization | null {
|
|
117
|
-
const { payload } = useBackendChrome()
|
|
118
|
-
return payload?.currentOrganization ?? null
|
|
119
|
-
}
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -107,7 +107,6 @@ import type { InjectionFieldDefinition, FieldContext } from '@open-mercato/share
|
|
|
107
107
|
import { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
108
108
|
import { evaluateInjectedVisibility } from './injection/visibility-utils'
|
|
109
109
|
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
110
|
-
import { crudFormExtensionSpotId, extensionSpotChildId } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
111
110
|
import { RichEditor, type RichEditorLabels } from '../primitives/rich-editor'
|
|
112
111
|
import MarkdownField from './inputs/MarkdownField'
|
|
113
112
|
|
|
@@ -817,7 +816,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
817
816
|
if (injectionSpotId) return injectionSpotId
|
|
818
817
|
if (resolvedEntityIds.length) {
|
|
819
818
|
const normalized = resolvedEntityIds[0].replace(/[:]+/g, '.')
|
|
820
|
-
return
|
|
819
|
+
return `crud-form:${normalized}`
|
|
821
820
|
}
|
|
822
821
|
return undefined
|
|
823
822
|
}, [injectionSpotId, resolvedEntityIds])
|
|
@@ -826,9 +825,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
826
825
|
if (resolvedEntityIds.length) return ComponentReplacementHandles.crudForm(resolvedEntityIds[0].replace(/[:]+/g, '.'))
|
|
827
826
|
return ComponentReplacementHandles.crudForm('unknown')
|
|
828
827
|
}, [replacementHandle, resolvedEntityIds])
|
|
829
|
-
const headerInjectionSpotId = resolvedInjectionSpotId
|
|
830
|
-
? extensionSpotChildId(resolvedInjectionSpotId, 'header')
|
|
831
|
-
: undefined
|
|
828
|
+
const headerInjectionSpotId = resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:header` : undefined
|
|
832
829
|
|
|
833
830
|
const recordId = React.useMemo(() => {
|
|
834
831
|
const raw = values.id
|
|
@@ -1086,9 +1083,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1086
1083
|
triggerOnLoad: true,
|
|
1087
1084
|
})
|
|
1088
1085
|
const { widgets: injectedFieldWidgets } = useInjectionDataWidgets(
|
|
1089
|
-
resolvedInjectionSpotId
|
|
1090
|
-
? extensionSpotChildId(resolvedInjectionSpotId, 'fields')
|
|
1091
|
-
: '__disabled__:fields'
|
|
1086
|
+
resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:fields` : '__disabled__:fields'
|
|
1092
1087
|
)
|
|
1093
1088
|
|
|
1094
1089
|
const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? '', injectionWidgets)
|
|
@@ -2407,19 +2402,10 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2407
2402
|
|
|
2408
2403
|
const handleFieldsetSelectionChange = React.useCallback(
|
|
2409
2404
|
(entityId: string, nextCode: string | null) => {
|
|
2410
|
-
// The fieldset selector never renders an empty option, so a user can never
|
|
2411
|
-
// pick "no fieldset". An empty/null value here only comes from Radix's
|
|
2412
|
-
// hidden native <select>, which fires a spurious onChange when the
|
|
2413
|
-
// controlled value is changed programmatically after mount (e.g. when the
|
|
2414
|
-
// persisted customFieldsetCode arrives via async initialValues and the
|
|
2415
|
-
// binding hydrates the selection). Honoring that reset would wipe both the
|
|
2416
|
-
// selection and the bound value, collapsing the form back to the default
|
|
2417
|
-
// fieldset (#2646). Ignore it.
|
|
2418
|
-
if (!nextCode) return
|
|
2419
2405
|
setCfFieldsetSelections((prev) => ({ ...prev, [entityId]: nextCode }))
|
|
2420
2406
|
const bindingKey = customFieldsetBindings?.[entityId]?.valueKey
|
|
2421
2407
|
if (bindingKey) {
|
|
2422
|
-
setValue(bindingKey, nextCode)
|
|
2408
|
+
setValue(bindingKey, nextCode ?? undefined)
|
|
2423
2409
|
}
|
|
2424
2410
|
},
|
|
2425
2411
|
[customFieldsetBindings, setValue],
|
|
@@ -59,7 +59,6 @@ import type {
|
|
|
59
59
|
InjectionRowActionDefinition,
|
|
60
60
|
} from '@open-mercato/shared/modules/widgets/injection'
|
|
61
61
|
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
62
|
-
import { dataTableExtensionSpotId, extensionSpotChildId } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
63
62
|
import { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
64
63
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
65
64
|
import type {
|
|
@@ -95,11 +94,6 @@ import {
|
|
|
95
94
|
} from '@dnd-kit/sortable'
|
|
96
95
|
import { CSS } from '@dnd-kit/utilities'
|
|
97
96
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
98
|
-
import { clearAllPerspectiveState, PERSPECTIVE_COOKIE_PREFIX, PERSPECTIVE_STORAGE_PREFIX } from './perspectiveState'
|
|
99
|
-
|
|
100
|
-
// Re-exported so `@open-mercato/ui/backend/DataTable` stays the published import
|
|
101
|
-
// path for the purge (BACKWARD_COMPATIBILITY: import paths are a contract surface).
|
|
102
|
-
export { clearAllPerspectiveState }
|
|
103
97
|
|
|
104
98
|
const logger = createLogger('ui').child({ component: 'DataTable' })
|
|
105
99
|
|
|
@@ -475,6 +469,8 @@ function resolveExportSections(config: DataTableExportConfig | null | undefined)
|
|
|
475
469
|
return sections
|
|
476
470
|
}
|
|
477
471
|
|
|
472
|
+
const PERSPECTIVE_COOKIE_PREFIX = 'om_table_perspective'
|
|
473
|
+
const PERSPECTIVE_STORAGE_PREFIX = 'om_table_perspective_snapshot'
|
|
478
474
|
|
|
479
475
|
// Bounds for user-driven column resizing (#1835). Widths outside this range are
|
|
480
476
|
// clamped so a persisted/dragged value can never collapse a column to nothing or
|
|
@@ -1356,8 +1352,8 @@ export function DataTable<T>({
|
|
|
1356
1352
|
}, [injectionSpotId, perspective?.tableId, extensionTableIdProp])
|
|
1357
1353
|
const resolvedInjectionSpotId =
|
|
1358
1354
|
injectionSpotId
|
|
1359
|
-
?? (perspective?.tableId ?
|
|
1360
|
-
?? (extensionTableIdProp ?
|
|
1355
|
+
?? (perspective?.tableId ? `data-table:${perspective.tableId}` : null)
|
|
1356
|
+
?? (extensionTableIdProp ? `data-table:${extensionTableIdProp}` : null)
|
|
1361
1357
|
const resolvedReplacementHandle = replacementHandle ?? ComponentReplacementHandles.dataTable(extensionTableId ?? 'unknown')
|
|
1362
1358
|
const baseInjectionContext = React.useMemo(
|
|
1363
1359
|
() => {
|
|
@@ -1380,32 +1376,32 @@ export function DataTable<T>({
|
|
|
1380
1376
|
[injectionContext, perspective?.tableId, extensionTableId, title]
|
|
1381
1377
|
)
|
|
1382
1378
|
const headerInjectionSpotId = React.useMemo(
|
|
1383
|
-
() => (resolvedInjectionSpotId ?
|
|
1379
|
+
() => (resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:header` : null),
|
|
1384
1380
|
[resolvedInjectionSpotId]
|
|
1385
1381
|
)
|
|
1386
1382
|
const toolbarInjectionSpotId = React.useMemo(
|
|
1387
|
-
() => (resolvedInjectionSpotId ?
|
|
1383
|
+
() => (resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:toolbar` : null),
|
|
1388
1384
|
[resolvedInjectionSpotId]
|
|
1389
1385
|
)
|
|
1390
1386
|
const searchTrailingInjectionSpotId = React.useMemo(
|
|
1391
|
-
() => (resolvedInjectionSpotId ?
|
|
1387
|
+
() => (resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:search-trailing` : null),
|
|
1392
1388
|
[resolvedInjectionSpotId]
|
|
1393
1389
|
)
|
|
1394
1390
|
const footerInjectionSpotId = React.useMemo(
|
|
1395
|
-
() => (resolvedInjectionSpotId ?
|
|
1391
|
+
() => (resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:footer` : null),
|
|
1396
1392
|
[resolvedInjectionSpotId]
|
|
1397
1393
|
)
|
|
1398
1394
|
const { widgets: columnWidgets } = useInjectionDataWidgets(
|
|
1399
|
-
extensionTableId ?
|
|
1395
|
+
extensionTableId ? `data-table:${extensionTableId}:columns` : '__disabled__:columns',
|
|
1400
1396
|
)
|
|
1401
1397
|
const { widgets: rowActionWidgets } = useInjectionDataWidgets(
|
|
1402
|
-
extensionTableId ?
|
|
1398
|
+
extensionTableId ? `data-table:${extensionTableId}:row-actions` : '__disabled__:row-actions',
|
|
1403
1399
|
)
|
|
1404
1400
|
const { widgets: bulkActionWidgets } = useInjectionDataWidgets(
|
|
1405
|
-
extensionTableId ?
|
|
1401
|
+
extensionTableId ? `data-table:${extensionTableId}:bulk-actions` : '__disabled__:bulk-actions',
|
|
1406
1402
|
)
|
|
1407
1403
|
const { widgets: filterWidgets } = useInjectionDataWidgets(
|
|
1408
|
-
extensionTableId ?
|
|
1404
|
+
extensionTableId ? `data-table:${extensionTableId}:filters` : '__disabled__:filters',
|
|
1409
1405
|
)
|
|
1410
1406
|
const injectedColumnDefs = React.useMemo<{ def: ColumnDef<T, unknown>; placement: InjectionColumnDefinition['placement'] }[]>(() => {
|
|
1411
1407
|
const entries: InjectionColumnDefinition[] = []
|
|
@@ -1699,31 +1695,18 @@ export function DataTable<T>({
|
|
|
1699
1695
|
})
|
|
1700
1696
|
}, [table, mergedColumns])
|
|
1701
1697
|
|
|
1702
|
-
|
|
1703
|
-
// `meta.hidden` defaults, so the auto-hide pass is skipped entirely in that case.
|
|
1704
|
-
const visibilitySeededByStoredSettings = React.useRef(Boolean(mergedInitialSettings?.columnVisibility))
|
|
1705
|
-
// Auto-hiding is a per-column default, applied once per column — not an enforcement.
|
|
1706
|
-
// It cannot be latched by a single has-run boolean: columns arrive in waves, because
|
|
1707
|
-
// custom-field columns are built from definitions fetched asynchronously. The first
|
|
1708
|
-
// render carries no `cf_*` column at all, so a run-once pass found nothing to hide and
|
|
1709
|
-
// still burned the latch, leaving fields declared `listVisible: false` visible for the
|
|
1710
|
-
// rest of the session after a hard page load (#4859).
|
|
1711
|
-
// It also cannot lean on `columnVisibility` as the record of what has been decided:
|
|
1712
|
-
// `handleColumnChooserToggle` *deletes* a column's entry when the user turns it back on,
|
|
1713
|
-
// so a re-shown column is indistinguishable from one never seen and would be hidden again
|
|
1714
|
-
// on the next wave. Hence an explicit per-column record of what this pass has applied.
|
|
1715
|
-
const autoHiddenColumnIds = React.useRef<Set<string>>(new Set())
|
|
1698
|
+
const initialVisibilityApplied = React.useRef(Boolean(mergedInitialSettings?.columnVisibility))
|
|
1716
1699
|
React.useEffect(() => {
|
|
1717
|
-
if (
|
|
1700
|
+
if (initialVisibilityApplied.current) return
|
|
1718
1701
|
const hidden: VisibilityState = {}
|
|
1719
1702
|
table.getAllLeafColumns().forEach((column) => {
|
|
1720
1703
|
const hiddenMeta = (column.columnDef as any)?.meta?.hidden
|
|
1721
|
-
if (
|
|
1722
|
-
hidden[column.id] = false
|
|
1723
|
-
autoHiddenColumnIds.current.add(column.id)
|
|
1704
|
+
if (hiddenMeta) hidden[column.id] = false
|
|
1724
1705
|
})
|
|
1725
|
-
if (
|
|
1726
|
-
|
|
1706
|
+
if (Object.keys(hidden).length) {
|
|
1707
|
+
setColumnVisibility((prev) => ({ ...hidden, ...prev }))
|
|
1708
|
+
}
|
|
1709
|
+
initialVisibilityApplied.current = true
|
|
1727
1710
|
}, [table, mergedColumns])
|
|
1728
1711
|
|
|
1729
1712
|
const getCurrentSettings = React.useCallback((): PerspectiveSettings => {
|
|
@@ -164,7 +164,7 @@ export function JsonBuilder({
|
|
|
164
164
|
disabled={disabled}
|
|
165
165
|
/>
|
|
166
166
|
{parseError && (
|
|
167
|
-
<div className="text-xs text-
|
|
167
|
+
<div className="text-xs text-red-600">Invalid JSON format</div>
|
|
168
168
|
)}
|
|
169
169
|
</div>
|
|
170
170
|
) : (
|
|
@@ -185,7 +185,7 @@ export function JsonBuilder({
|
|
|
185
185
|
</div>
|
|
186
186
|
)}
|
|
187
187
|
|
|
188
|
-
{error && <div className="text-xs text-
|
|
188
|
+
{error && <div className="text-xs text-red-600">{error}</div>}
|
|
189
189
|
{ConfirmDialogElement}
|
|
190
190
|
</div>
|
|
191
191
|
)
|
|
@@ -363,7 +363,7 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot, confirm }
|
|
|
363
363
|
type="button"
|
|
364
364
|
variant="ghost"
|
|
365
365
|
size="xs"
|
|
366
|
-
className="text-muted-foreground hover:text-
|
|
366
|
+
className="text-muted-foreground hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
367
367
|
title="Remove item"
|
|
368
368
|
onClick={onDelete}
|
|
369
369
|
>
|
|
@@ -42,16 +42,16 @@ type NextStepCalloutProps = {
|
|
|
42
42
|
const STEP_STYLES: Record<NonNullable<NextStepCalloutStep['state']>, string> = {
|
|
43
43
|
pending: '',
|
|
44
44
|
active: 'border-primary/40 bg-primary/10 text-primary',
|
|
45
|
-
completed: 'border-
|
|
45
|
+
completed: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200',
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const STATUS_STYLES: Record<NextStepCalloutStatusTone, string> = {
|
|
49
49
|
default: 'border-border bg-background/80 text-foreground',
|
|
50
50
|
info: 'border-primary/20 bg-primary/5 text-primary',
|
|
51
|
-
success: 'border-
|
|
52
|
-
warning: 'border-
|
|
51
|
+
success: 'border-emerald-500/20 bg-emerald-500/5 text-emerald-200',
|
|
52
|
+
warning: 'border-amber-500/20 bg-amber-500/5 text-amber-200',
|
|
53
53
|
danger: 'border-destructive/20 bg-destructive/5 text-destructive',
|
|
54
|
-
muted: 'border-
|
|
54
|
+
muted: 'border-zinc-500/20 bg-zinc-500/5 text-zinc-200',
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function NextStepCallout({
|
|
@@ -123,7 +123,7 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
123
123
|
<a
|
|
124
124
|
key={idx}
|
|
125
125
|
href={it.href}
|
|
126
|
-
className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-
|
|
126
|
+
className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-red-600' : ''}`}
|
|
127
127
|
role="menuitem"
|
|
128
128
|
onClick={(event) => {
|
|
129
129
|
event.stopPropagation()
|
|
@@ -138,7 +138,7 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
138
138
|
type="button"
|
|
139
139
|
variant="ghost"
|
|
140
140
|
size="sm"
|
|
141
|
-
className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-
|
|
141
|
+
className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-red-600' : ''}`}
|
|
142
142
|
role="menuitem"
|
|
143
143
|
onClick={(event) => {
|
|
144
144
|
event.stopPropagation()
|
|
@@ -226,11 +226,7 @@ describe('AppShell', () => {
|
|
|
226
226
|
expect(screen.getByTestId('injection-spot:backend:layout:footer')).toBeInTheDocument()
|
|
227
227
|
})
|
|
228
228
|
|
|
229
|
-
it
|
|
230
|
-
['internal-file', '/api/attachments/file/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'],
|
|
231
|
-
['internal-image-query', '/api/attachments/image/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/acme.svg?width=320&height=320'],
|
|
232
|
-
['external-webp', 'https://example.com/acme-wide-logo.webp'],
|
|
233
|
-
])('uses an aspect-ratio-preserving backend chrome brand logo when enabled for %s', async (variant, logoSrc) => {
|
|
229
|
+
it('uses backend chrome brand logo when the selected organization has one', async () => {
|
|
234
230
|
const previousFetch = global.fetch
|
|
235
231
|
const previousWindowFetch = window.fetch
|
|
236
232
|
const previousOriginalFetch = (window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch
|
|
@@ -239,9 +235,8 @@ describe('AppShell', () => {
|
|
|
239
235
|
brand: {
|
|
240
236
|
name: 'Acme',
|
|
241
237
|
logo: {
|
|
242
|
-
src:
|
|
238
|
+
src: '/api/attachments/image/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/acme.png?width=320',
|
|
243
239
|
alt: 'Acme logo',
|
|
244
|
-
preserveAspectRatio: true,
|
|
245
240
|
},
|
|
246
241
|
},
|
|
247
242
|
groups,
|
|
@@ -262,7 +257,7 @@ describe('AppShell', () => {
|
|
|
262
257
|
<AppShell
|
|
263
258
|
email="demo@example.com"
|
|
264
259
|
groups={[]}
|
|
265
|
-
adminNavApi=
|
|
260
|
+
adminNavApi="/api/auth/admin/nav-brand-logo"
|
|
266
261
|
>
|
|
267
262
|
<div>Child content</div>
|
|
268
263
|
</AppShell>,
|
|
@@ -271,10 +266,11 @@ describe('AppShell', () => {
|
|
|
271
266
|
|
|
272
267
|
await waitFor(() => {
|
|
273
268
|
const logo = screen.getByAltText('Acme logo')
|
|
274
|
-
expect(logo).toHaveAttribute(
|
|
269
|
+
expect(logo).toHaveAttribute(
|
|
270
|
+
'src',
|
|
271
|
+
'/api/attachments/image/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/acme.png?width=320',
|
|
272
|
+
)
|
|
275
273
|
expect(logo).toHaveAttribute('data-unoptimized', 'true')
|
|
276
|
-
expect(logo).toHaveClass('object-contain')
|
|
277
|
-
expect(logo).not.toHaveClass('rounded-full')
|
|
278
274
|
})
|
|
279
275
|
expect(screen.getByText('Acme')).toBeInTheDocument()
|
|
280
276
|
} finally {
|
|
@@ -284,60 +280,6 @@ describe('AppShell', () => {
|
|
|
284
280
|
}
|
|
285
281
|
})
|
|
286
282
|
|
|
287
|
-
it('uses the cropped icon treatment for backend chrome brand logos by default', async () => {
|
|
288
|
-
const previousFetch = global.fetch
|
|
289
|
-
const previousWindowFetch = window.fetch
|
|
290
|
-
const previousOriginalFetch = (window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch
|
|
291
|
-
const logoSrc = 'https://example.com/acme-wide-logo.webp'
|
|
292
|
-
const fetchMock = jest.fn().mockResolvedValue(
|
|
293
|
-
new Response(JSON.stringify({
|
|
294
|
-
brand: {
|
|
295
|
-
name: 'Acme',
|
|
296
|
-
logo: {
|
|
297
|
-
src: logoSrc,
|
|
298
|
-
alt: 'Acme logo',
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
groups,
|
|
302
|
-
settingsSections: [],
|
|
303
|
-
settingsPathPrefixes: [],
|
|
304
|
-
profileSections: [],
|
|
305
|
-
profilePathPrefixes: [],
|
|
306
|
-
grantedFeatures: [],
|
|
307
|
-
roles: [],
|
|
308
|
-
}), { status: 200, headers: { 'content-type': 'application/json' } }),
|
|
309
|
-
) as typeof fetch
|
|
310
|
-
global.fetch = fetchMock
|
|
311
|
-
window.fetch = fetchMock
|
|
312
|
-
;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = fetchMock
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
renderWithProviders(
|
|
316
|
-
<AppShell
|
|
317
|
-
email="demo@example.com"
|
|
318
|
-
groups={[]}
|
|
319
|
-
adminNavApi="/api/auth/admin/nav-brand-logo-cropped"
|
|
320
|
-
>
|
|
321
|
-
<div>Child content</div>
|
|
322
|
-
</AppShell>,
|
|
323
|
-
{ dict },
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
await waitFor(() => {
|
|
327
|
-
const logo = screen.getByAltText('Acme logo')
|
|
328
|
-
expect(logo).toHaveAttribute('src', logoSrc)
|
|
329
|
-
expect(logo).toHaveAttribute('data-unoptimized', 'true')
|
|
330
|
-
expect(logo).toHaveClass('object-cover')
|
|
331
|
-
expect(logo).toHaveClass('rounded-full')
|
|
332
|
-
expect(logo).not.toHaveClass('object-contain')
|
|
333
|
-
})
|
|
334
|
-
} finally {
|
|
335
|
-
global.fetch = previousFetch
|
|
336
|
-
window.fetch = previousWindowFetch
|
|
337
|
-
;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = previousOriginalFetch
|
|
338
|
-
}
|
|
339
|
-
})
|
|
340
|
-
|
|
341
283
|
it('renders nested settings links when settings parent route is active', async () => {
|
|
342
284
|
mockPathname = '/backend/entities/user'
|
|
343
285
|
|
|
@@ -8,7 +8,6 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
8
8
|
import {
|
|
9
9
|
__reload,
|
|
10
10
|
AUTH_IDENTITY_STORAGE_KEY,
|
|
11
|
-
AUTH_IDENTITY_USER_STORAGE_KEY,
|
|
12
11
|
AuthSessionGuard,
|
|
13
12
|
notifyAuthIdentityChange,
|
|
14
13
|
} from '../AuthSessionGuard'
|
|
@@ -149,54 +148,3 @@ describe('notifyAuthIdentityChange', () => {
|
|
|
149
148
|
setItemSpy.mockRestore()
|
|
150
149
|
})
|
|
151
150
|
})
|
|
152
|
-
|
|
153
|
-
describe('perspective purge at the rendered identity boundary (#4185)', () => {
|
|
154
|
-
const snapshotKey = 'om_table_perspective_snapshot:customers-companies'
|
|
155
|
-
|
|
156
|
-
beforeEach(() => {
|
|
157
|
-
window.localStorage.clear()
|
|
158
|
-
apiCallMock.mockResolvedValue({ ok: true, status: 200, result: { ok: true } } as never)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
afterEach(() => {
|
|
162
|
-
window.localStorage.clear()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('purges legacy table state when no previous identity marker exists', () => {
|
|
166
|
-
window.localStorage.setItem(snapshotKey, '{"columnWidths":{"name":240}}')
|
|
167
|
-
|
|
168
|
-
render(<AuthSessionGuard serverUserId="user-1" />)
|
|
169
|
-
|
|
170
|
-
expect(window.localStorage.getItem(snapshotKey)).toBeNull()
|
|
171
|
-
expect(window.localStorage.getItem(AUTH_IDENTITY_USER_STORAGE_KEY)).toBe('user-1')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('keeps a returning user\'s unsaved widths across a reload', () => {
|
|
175
|
-
window.localStorage.setItem(AUTH_IDENTITY_USER_STORAGE_KEY, 'user-1')
|
|
176
|
-
window.localStorage.setItem(snapshotKey, '{"columnWidths":{"name":240}}')
|
|
177
|
-
|
|
178
|
-
render(<AuthSessionGuard serverUserId="user-1" />)
|
|
179
|
-
|
|
180
|
-
expect(window.localStorage.getItem(snapshotKey)).toBe('{"columnWidths":{"name":240}}')
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('purges perspective state when the backend renders for a different user', () => {
|
|
184
|
-
window.localStorage.setItem(AUTH_IDENTITY_USER_STORAGE_KEY, 'user-1')
|
|
185
|
-
window.localStorage.setItem(snapshotKey, '{"columnWidths":{"name":240}}')
|
|
186
|
-
|
|
187
|
-
render(<AuthSessionGuard serverUserId="user-2" />)
|
|
188
|
-
|
|
189
|
-
expect(window.localStorage.getItem(snapshotKey)).toBeNull()
|
|
190
|
-
expect(window.localStorage.getItem(AUTH_IDENTITY_USER_STORAGE_KEY)).toBe('user-2')
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('does nothing when the shell renders without an authenticated user', () => {
|
|
194
|
-
window.localStorage.setItem(AUTH_IDENTITY_USER_STORAGE_KEY, 'user-1')
|
|
195
|
-
window.localStorage.setItem(snapshotKey, '{"columnWidths":{"name":240}}')
|
|
196
|
-
|
|
197
|
-
render(<AuthSessionGuard serverUserId={null} />)
|
|
198
|
-
|
|
199
|
-
expect(window.localStorage.getItem(snapshotKey)).toBe('{"columnWidths":{"name":240}}')
|
|
200
|
-
expect(window.localStorage.getItem(AUTH_IDENTITY_USER_STORAGE_KEY)).toBe('user-1')
|
|
201
|
-
})
|
|
202
|
-
})
|