@open-mercato/ui 0.5.1-develop.2800.bfe2178a4f → 0.5.1-develop.2851.2854b4507f
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/dist/backend/DataTable.js +15 -0
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +1 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +1 -1
- package/dist/backend/crud/CollapsibleGroup.js +64 -50
- package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
- package/dist/backend/crud/CollapsibleZoneLayout.js +23 -3
- package/dist/backend/crud/CollapsibleZoneLayout.js.map +2 -2
- package/dist/backend/crud/useGroupCollapse.js +2 -2
- package/dist/backend/crud/useGroupCollapse.js.map +2 -2
- package/dist/backend/crud/usePersistedBooleanFlag.js +57 -16
- package/dist/backend/crud/usePersistedBooleanFlag.js.map +2 -2
- package/dist/backend/crud/useZoneCollapse.js +2 -2
- package/dist/backend/crud/useZoneCollapse.js.map +2 -2
- package/dist/backend/messages/SendObjectMessageDialog.js +34 -13
- package/dist/backend/messages/SendObjectMessageDialog.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryAction.js +3 -3
- package/dist/backend/version-history/VersionHistoryAction.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/DataTable.tsx +16 -0
- package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +60 -0
- package/src/backend/__tests__/DataTable.extensions.test.tsx +61 -8
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +1 -1
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +44 -0
- package/src/backend/crud/CollapsibleGroup.tsx +12 -2
- package/src/backend/crud/CollapsibleZoneLayout.tsx +29 -4
- package/src/backend/crud/__tests__/usePersistedBooleanFlag.test.ts +83 -7
- package/src/backend/crud/useGroupCollapse.ts +2 -2
- package/src/backend/crud/usePersistedBooleanFlag.ts +75 -21
- package/src/backend/crud/useZoneCollapse.ts +2 -2
- package/src/backend/messages/SendObjectMessageDialog.tsx +37 -7
- package/src/backend/messages/__tests__/SendObjectMessageDialog.test.tsx +21 -0
- package/src/backend/version-history/VersionHistoryAction.tsx +4 -4
|
@@ -7,7 +7,7 @@ function getStorageKey(pageType: string, groupId: string) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function useGroupCollapse(pageType: string, groupId: string, defaultExpanded = true) {
|
|
10
|
-
const { value: expanded, toggle, setValue } = usePersistedBooleanFlag(
|
|
10
|
+
const { value: expanded, toggle, setValue, isHydrated } = usePersistedBooleanFlag(
|
|
11
11
|
getStorageKey(pageType, groupId),
|
|
12
12
|
defaultExpanded,
|
|
13
13
|
)
|
|
@@ -18,5 +18,5 @@ export function useGroupCollapse(pageType: string, groupId: string, defaultExpan
|
|
|
18
18
|
setValue(next)
|
|
19
19
|
}
|
|
20
20
|
}, [setValue])
|
|
21
|
-
return { expanded, toggle, setExpanded }
|
|
21
|
+
return { expanded, toggle, setExpanded, isHydrated }
|
|
22
22
|
}
|
|
@@ -1,35 +1,89 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
3
3
|
import {
|
|
4
4
|
readJsonFromLocalStorage,
|
|
5
5
|
writeJsonToLocalStorage,
|
|
6
6
|
} from '@open-mercato/shared/lib/browser/safeLocalStorage'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const LOCAL_BROADCAST_EVENT = 'om:persisted-boolean-flag:change'
|
|
9
|
+
|
|
10
|
+
type SafeEventTarget = {
|
|
11
|
+
addEventListener: EventTarget['addEventListener']
|
|
12
|
+
removeEventListener: EventTarget['removeEventListener']
|
|
13
|
+
dispatchEvent: EventTarget['dispatchEvent']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const localEmitter: SafeEventTarget | null =
|
|
17
|
+
typeof window !== 'undefined' ? new EventTarget() : null
|
|
18
|
+
|
|
19
|
+
function readCurrentValue(storageKey: string, defaultValue: boolean): boolean {
|
|
20
|
+
const saved = readJsonFromLocalStorage<string | null>(storageKey, null)
|
|
21
|
+
if (saved === '1') return true
|
|
22
|
+
if (saved === '0') return false
|
|
23
|
+
return defaultValue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function persistValue(storageKey: string, next: boolean): void {
|
|
27
|
+
writeJsonToLocalStorage(storageKey, next ? '1' : '0')
|
|
28
|
+
localEmitter?.dispatchEvent(
|
|
29
|
+
new CustomEvent(LOCAL_BROADCAST_EVENT, { detail: storageKey }),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function subscribe(storageKey: string, onChange: () => void): () => void {
|
|
34
|
+
if (typeof window === 'undefined') return () => {}
|
|
35
|
+
|
|
36
|
+
const handleStorage = (event: StorageEvent) => {
|
|
37
|
+
if (event.key === storageKey) onChange()
|
|
38
|
+
}
|
|
39
|
+
const handleLocal = (event: Event) => {
|
|
40
|
+
const detail = (event as CustomEvent<string>).detail
|
|
41
|
+
if (detail === storageKey) onChange()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.addEventListener('storage', handleStorage)
|
|
45
|
+
localEmitter?.addEventListener(LOCAL_BROADCAST_EVENT, handleLocal)
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener('storage', handleStorage)
|
|
49
|
+
localEmitter?.removeEventListener(LOCAL_BROADCAST_EVENT, handleLocal)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
13
53
|
export function usePersistedBooleanFlag(storageKey: string, defaultValue: boolean) {
|
|
14
|
-
const [
|
|
15
|
-
const
|
|
54
|
+
const [isHydrated, setIsHydrated] = useState(false)
|
|
55
|
+
const getSnapshot = useCallback(
|
|
56
|
+
() => readCurrentValue(storageKey, defaultValue),
|
|
57
|
+
[storageKey, defaultValue],
|
|
58
|
+
)
|
|
59
|
+
const getServerSnapshot = useCallback(() => defaultValue, [defaultValue])
|
|
60
|
+
const subscribeKey = useCallback(
|
|
61
|
+
(onChange: () => void) => subscribe(storageKey, onChange),
|
|
62
|
+
[storageKey],
|
|
63
|
+
)
|
|
16
64
|
|
|
17
|
-
|
|
18
|
-
mounted.current = true
|
|
19
|
-
const saved = readJsonFromLocalStorage<string | null>(storageKey, null)
|
|
20
|
-
if (saved !== null) {
|
|
21
|
-
setValue(saved === '1')
|
|
22
|
-
}
|
|
23
|
-
}, [storageKey])
|
|
65
|
+
const value = useSyncExternalStore(subscribeKey, getSnapshot, getServerSnapshot)
|
|
24
66
|
|
|
25
67
|
useEffect(() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
68
|
+
setIsHydrated(true)
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
const setValue = useCallback(
|
|
72
|
+
(next: boolean | ((prev: boolean) => boolean)) => {
|
|
73
|
+
const current = readCurrentValue(storageKey, defaultValue)
|
|
74
|
+
const nextValue =
|
|
75
|
+
typeof next === 'function'
|
|
76
|
+
? (next as (prev: boolean) => boolean)(current)
|
|
77
|
+
: next
|
|
78
|
+
persistValue(storageKey, nextValue)
|
|
79
|
+
},
|
|
80
|
+
[storageKey, defaultValue],
|
|
81
|
+
)
|
|
29
82
|
|
|
30
83
|
const toggle = useCallback(() => {
|
|
31
|
-
|
|
32
|
-
|
|
84
|
+
const current = readCurrentValue(storageKey, defaultValue)
|
|
85
|
+
persistValue(storageKey, !current)
|
|
86
|
+
}, [storageKey, defaultValue])
|
|
33
87
|
|
|
34
|
-
return { value, toggle, setValue }
|
|
88
|
+
return { value, toggle, setValue, isHydrated }
|
|
35
89
|
}
|
|
@@ -7,7 +7,7 @@ function getStorageKey(pageType: string) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function useZoneCollapse(pageType: string) {
|
|
10
|
-
const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(
|
|
10
|
+
const { value: collapsed, toggle, setValue, isHydrated } = usePersistedBooleanFlag(
|
|
11
11
|
getStorageKey(pageType),
|
|
12
12
|
false,
|
|
13
13
|
)
|
|
@@ -18,5 +18,5 @@ export function useZoneCollapse(pageType: string) {
|
|
|
18
18
|
setValue(next)
|
|
19
19
|
}
|
|
20
20
|
}, [setValue])
|
|
21
|
-
return { collapsed, toggle, setCollapsed }
|
|
21
|
+
return { collapsed, toggle, setCollapsed, isHydrated }
|
|
22
22
|
}
|
|
@@ -4,6 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import { Send } from 'lucide-react'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { Button } from '../../primitives/button'
|
|
7
|
+
import { IconButton } from '../../primitives/icon-button'
|
|
7
8
|
import {
|
|
8
9
|
MessageComposer,
|
|
9
10
|
type MessageComposerContextObject,
|
|
@@ -17,6 +18,10 @@ export type SendObjectMessageDialogProps = {
|
|
|
17
18
|
lockedType?: string | null
|
|
18
19
|
requiredActionConfig?: MessageComposerRequiredActionConfig | null
|
|
19
20
|
disabled?: boolean
|
|
21
|
+
buttonVariant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'muted' | 'link'
|
|
22
|
+
buttonSize?: 'default' | 'sm' | 'lg' | 'icon'
|
|
23
|
+
buttonClassName?: string
|
|
24
|
+
buttonLabel?: string
|
|
20
25
|
viewHref?: string | null
|
|
21
26
|
onSuccess?: MessageComposerProps['onSuccess']
|
|
22
27
|
}
|
|
@@ -27,11 +32,16 @@ export function SendObjectMessageDialog({
|
|
|
27
32
|
lockedType = 'messages.defaultWithObjects',
|
|
28
33
|
requiredActionConfig = null,
|
|
29
34
|
disabled = false,
|
|
35
|
+
buttonVariant = 'ghost',
|
|
36
|
+
buttonSize = 'icon',
|
|
37
|
+
buttonClassName,
|
|
38
|
+
buttonLabel,
|
|
30
39
|
viewHref: _viewHref = null,
|
|
31
40
|
onSuccess,
|
|
32
41
|
}: SendObjectMessageDialogProps) {
|
|
33
42
|
const t = useT()
|
|
34
43
|
const [open, setOpen] = React.useState(false)
|
|
44
|
+
const label = buttonLabel ?? t('messages.compose', 'Compose message')
|
|
35
45
|
|
|
36
46
|
const openComposer = React.useCallback(() => {
|
|
37
47
|
if (disabled) return
|
|
@@ -46,19 +56,39 @@ export function SendObjectMessageDialog({
|
|
|
46
56
|
previewData: object.previewData ?? null,
|
|
47
57
|
}), [object.entityId, object.entityModule, object.entityType, object.sourceEntityId, object.sourceEntityType, object.previewData])
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
const trigger = buttonSize === 'icon' && (buttonVariant === 'outline' || buttonVariant === 'ghost')
|
|
60
|
+
? (
|
|
61
|
+
<IconButton
|
|
62
|
+
type="button"
|
|
63
|
+
size="default"
|
|
64
|
+
variant={buttonVariant}
|
|
65
|
+
className={buttonClassName}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
onClick={openComposer}
|
|
68
|
+
aria-label={label}
|
|
69
|
+
title={label}
|
|
70
|
+
>
|
|
71
|
+
<Send className="size-4" />
|
|
72
|
+
</IconButton>
|
|
73
|
+
)
|
|
74
|
+
: (
|
|
51
75
|
<Button
|
|
52
76
|
type="button"
|
|
53
|
-
size=
|
|
54
|
-
variant=
|
|
77
|
+
size={buttonSize}
|
|
78
|
+
variant={buttonVariant}
|
|
79
|
+
className={buttonClassName}
|
|
55
80
|
disabled={disabled}
|
|
56
81
|
onClick={openComposer}
|
|
57
|
-
aria-label={
|
|
58
|
-
title={
|
|
82
|
+
aria-label={label}
|
|
83
|
+
title={label}
|
|
59
84
|
>
|
|
60
|
-
<Send className="
|
|
85
|
+
<Send className="size-4" />
|
|
61
86
|
</Button>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
{trigger}
|
|
62
92
|
<MessageComposer
|
|
63
93
|
variant="compose"
|
|
64
94
|
open={open}
|
|
@@ -74,4 +74,25 @@ describe('SendObjectMessageDialog', () => {
|
|
|
74
74
|
open: true,
|
|
75
75
|
}))
|
|
76
76
|
})
|
|
77
|
+
|
|
78
|
+
it('allows callers to style and label the trigger button', () => {
|
|
79
|
+
renderWithProviders(
|
|
80
|
+
<SendObjectMessageDialog
|
|
81
|
+
object={{
|
|
82
|
+
entityModule: 'customers',
|
|
83
|
+
entityType: 'person',
|
|
84
|
+
entityId: '11111111-1111-4111-8111-111111111111',
|
|
85
|
+
}}
|
|
86
|
+
buttonVariant="outline"
|
|
87
|
+
buttonSize="icon"
|
|
88
|
+
buttonClassName="size-8"
|
|
89
|
+
buttonLabel="Send message"
|
|
90
|
+
/>,
|
|
91
|
+
{ dict: {} },
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const button = screen.getByRole('button', { name: 'Send message' })
|
|
95
|
+
expect(button.className).toEqual(expect.stringContaining('size-8'))
|
|
96
|
+
expect(button.className).toEqual(expect.stringContaining('border'))
|
|
97
|
+
})
|
|
77
98
|
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Clock } from 'lucide-react'
|
|
5
|
-
import {
|
|
5
|
+
import { IconButton } from '../../primitives/icon-button'
|
|
6
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
7
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
8
8
|
import type { VersionHistoryConfig } from './types'
|
|
@@ -48,17 +48,17 @@ export function VersionHistoryAction({
|
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<>
|
|
51
|
-
<
|
|
51
|
+
<IconButton
|
|
52
52
|
type="button"
|
|
53
53
|
variant="ghost"
|
|
54
|
-
size="
|
|
54
|
+
size="default"
|
|
55
55
|
onClick={() => setOpen(true)}
|
|
56
56
|
aria-label={t('audit_logs.version_history.title')}
|
|
57
57
|
title={t('audit_logs.version_history.title')}
|
|
58
58
|
className={buttonClassName}
|
|
59
59
|
>
|
|
60
60
|
<Clock className={cn('size-4', iconClassName)} />
|
|
61
|
-
</
|
|
61
|
+
</IconButton>
|
|
62
62
|
<VersionHistoryPanel
|
|
63
63
|
open={open}
|
|
64
64
|
onOpenChange={setOpen}
|