@open-mercato/ui 0.6.4-develop.4121.1.0d7f20d229 → 0.6.4-develop.4133.1.48fc6c8f7b
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/dist/backend/AppShell.js +2 -2
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +3 -2
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/dashboard/index.js +4 -1
- package/dist/backend/dashboard/index.js.map +2 -2
- package/dist/backend/dashboard/widgetData.js +59 -0
- package/dist/backend/dashboard/widgetData.js.map +7 -0
- package/dist/backend/dashboard/widgetDataBatcher.js +61 -0
- package/dist/backend/dashboard/widgetDataBatcher.js.map +7 -0
- package/dist/backend/progress/ProgressTopBar.js +6 -4
- package/dist/backend/progress/ProgressTopBar.js.map +2 -2
- package/dist/backend/progress/useAutoHideCompletedJobs.js +65 -0
- package/dist/backend/progress/useAutoHideCompletedJobs.js.map +7 -0
- package/dist/primitives/date-picker.js +1 -0
- package/dist/primitives/date-picker.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -2
- package/src/backend/__tests__/useAutoHideCompletedJobs.test.tsx +179 -0
- package/src/backend/dashboard/DashboardScreen.tsx +3 -0
- package/src/backend/dashboard/__tests__/widgetDataBatcher.test.ts +97 -0
- package/src/backend/dashboard/index.ts +1 -0
- package/src/backend/dashboard/widgetData.tsx +84 -0
- package/src/backend/dashboard/widgetDataBatcher.ts +100 -0
- package/src/backend/progress/ProgressTopBar.tsx +12 -4
- package/src/backend/progress/useAutoHideCompletedJobs.ts +100 -0
- package/src/primitives/__tests__/date-picker.test.tsx +12 -0
- package/src/primitives/date-picker.tsx +1 -0
|
@@ -5,6 +5,7 @@ import { Button } from '../../primitives/button'
|
|
|
5
5
|
import { Progress } from '../../primitives/progress'
|
|
6
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
7
|
import { useProgress } from './useProgress'
|
|
8
|
+
import { useAutoHideCompletedJobs } from './useAutoHideCompletedJobs'
|
|
8
9
|
import type { ProgressJobDto } from './useProgressPoll'
|
|
9
10
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
10
11
|
import { apiCall } from '../utils/apiCall'
|
|
@@ -12,10 +13,17 @@ import { apiCall } from '../utils/apiCall'
|
|
|
12
13
|
export type ProgressTopBarProps = {
|
|
13
14
|
className?: string
|
|
14
15
|
t: TranslateFn
|
|
16
|
+
/**
|
|
17
|
+
* How long (ms) to keep successfully completed jobs visible before auto-hiding.
|
|
18
|
+
* Set to `false` or `0` to disable auto-hide. Defaults to 10 000 ms.
|
|
19
|
+
* Failed and cancelled jobs are never auto-hidden.
|
|
20
|
+
*/
|
|
21
|
+
completedAutoHideMs?: number | false
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
export function ProgressTopBar({ className, t }: ProgressTopBarProps) {
|
|
24
|
+
export function ProgressTopBar({ className, t, completedAutoHideMs }: ProgressTopBarProps) {
|
|
18
25
|
const { activeJobs, recentlyCompleted, refresh } = useProgress()
|
|
26
|
+
const visibleCompleted = useAutoHideCompletedJobs(recentlyCompleted, completedAutoHideMs)
|
|
19
27
|
const [expanded, setExpanded] = React.useState(false)
|
|
20
28
|
|
|
21
29
|
React.useEffect(() => {
|
|
@@ -28,7 +36,7 @@ export function ProgressTopBar({ className, t }: ProgressTopBarProps) {
|
|
|
28
36
|
}, [expanded])
|
|
29
37
|
|
|
30
38
|
const hasActiveJobs = activeJobs.length > 0
|
|
31
|
-
const hasRecentJobs =
|
|
39
|
+
const hasRecentJobs = visibleCompleted.length > 0
|
|
32
40
|
|
|
33
41
|
if (!hasActiveJobs && !hasRecentJobs) return null
|
|
34
42
|
|
|
@@ -60,7 +68,7 @@ export function ProgressTopBar({ className, t }: ProgressTopBarProps) {
|
|
|
60
68
|
<>
|
|
61
69
|
<CheckCircle className="h-4 w-4 text-status-success-icon" />
|
|
62
70
|
<span className="text-muted-foreground">
|
|
63
|
-
{t('progress.recentlyCompleted', '{count} operations completed', { count:
|
|
71
|
+
{t('progress.recentlyCompleted', '{count} operations completed', { count: visibleCompleted.length })}
|
|
64
72
|
</span>
|
|
65
73
|
</>
|
|
66
74
|
)}
|
|
@@ -77,7 +85,7 @@ export function ProgressTopBar({ className, t }: ProgressTopBarProps) {
|
|
|
77
85
|
{activeJobs.map((job) => (
|
|
78
86
|
<ProgressJobCard key={job.id} job={job} t={t} onCancel={refresh} />
|
|
79
87
|
))}
|
|
80
|
-
{
|
|
88
|
+
{visibleCompleted.map((job) => (
|
|
81
89
|
<ProgressJobCard key={job.id} job={job} t={t} />
|
|
82
90
|
))}
|
|
83
91
|
</div>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import type { ProgressJobDto } from './useProgressPoll'
|
|
4
|
+
|
|
5
|
+
/** Default auto-hide delay for successfully completed progress jobs (ms). */
|
|
6
|
+
export const DEFAULT_AUTO_HIDE_MS = 10_000
|
|
7
|
+
|
|
8
|
+
function safeParseMs(isoString: string | null | undefined): number | null {
|
|
9
|
+
if (!isoString) return null
|
|
10
|
+
const ms = new Date(isoString).getTime()
|
|
11
|
+
return isNaN(ms) ? null : ms
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hides successfully completed progress jobs after a configurable timeout.
|
|
16
|
+
*
|
|
17
|
+
* Uses `finishedAt` so a job that finished 8s ago with a 10s timeout
|
|
18
|
+
* disappears in 2s instead of a full new 10s window — works correctly
|
|
19
|
+
* after panel collapse/expand or page focus restore.
|
|
20
|
+
*
|
|
21
|
+
* Pass `timeoutMs={false}` or `timeoutMs={0}` to disable auto-hide.
|
|
22
|
+
* Failed and cancelled jobs are never hidden automatically.
|
|
23
|
+
*
|
|
24
|
+
* Each completed job ID is scheduled at most once. If the same job ID
|
|
25
|
+
* reappears in the list after its timer has already fired (which should
|
|
26
|
+
* not occur in normal server behaviour), it remains hidden.
|
|
27
|
+
*/
|
|
28
|
+
export function useAutoHideCompletedJobs(
|
|
29
|
+
completed: ProgressJobDto[],
|
|
30
|
+
timeoutMs: number | false = DEFAULT_AUTO_HIDE_MS,
|
|
31
|
+
): ProgressJobDto[] {
|
|
32
|
+
const [expiredIds, setExpiredIds] = React.useState<ReadonlySet<string>>(new Set())
|
|
33
|
+
// Tracks IDs for which a hide-timer has been scheduled to prevent double-scheduling
|
|
34
|
+
// on poll/SSE refresh cycles. IDs remain here after the timer fires so a reappearing
|
|
35
|
+
// job (should not happen) stays hidden rather than restarting the countdown.
|
|
36
|
+
const scheduledRef = React.useRef<Set<string>>(new Set())
|
|
37
|
+
const timersRef = React.useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
|
|
38
|
+
|
|
39
|
+
// Clear all pending timers on unmount
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
return () => {
|
|
42
|
+
for (const t of timersRef.current.values()) clearTimeout(t)
|
|
43
|
+
timersRef.current.clear()
|
|
44
|
+
scheduledRef.current.clear()
|
|
45
|
+
}
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (timeoutMs === false || timeoutMs <= 0) return
|
|
50
|
+
|
|
51
|
+
const now = Date.now()
|
|
52
|
+
const timeout = timeoutMs as number
|
|
53
|
+
|
|
54
|
+
for (const job of completed) {
|
|
55
|
+
if (job.status !== 'completed') continue
|
|
56
|
+
if (scheduledRef.current.has(job.id)) continue
|
|
57
|
+
|
|
58
|
+
scheduledRef.current.add(job.id)
|
|
59
|
+
|
|
60
|
+
const finishedAt = safeParseMs(job.finishedAt) ?? now
|
|
61
|
+
const elapsed = now - finishedAt
|
|
62
|
+
const remaining = Math.max(0, timeout - elapsed)
|
|
63
|
+
|
|
64
|
+
if (remaining === 0) {
|
|
65
|
+
// Already past the timeout window (e.g. page reload with stale job)
|
|
66
|
+
setExpiredIds((prev) => {
|
|
67
|
+
const next = new Set(prev)
|
|
68
|
+
next.add(job.id)
|
|
69
|
+
return next
|
|
70
|
+
})
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const t = setTimeout(() => {
|
|
75
|
+
setExpiredIds((prev) => {
|
|
76
|
+
const next = new Set(prev)
|
|
77
|
+
next.add(job.id)
|
|
78
|
+
return next
|
|
79
|
+
})
|
|
80
|
+
timersRef.current.delete(job.id)
|
|
81
|
+
}, remaining)
|
|
82
|
+
timersRef.current.set(job.id, t)
|
|
83
|
+
}
|
|
84
|
+
}, [completed, timeoutMs])
|
|
85
|
+
|
|
86
|
+
if (timeoutMs === false || timeoutMs <= 0) return completed
|
|
87
|
+
|
|
88
|
+
const timeout = timeoutMs as number
|
|
89
|
+
const now = Date.now()
|
|
90
|
+
|
|
91
|
+
return completed.filter((job) => {
|
|
92
|
+
if (job.status !== 'completed') return true
|
|
93
|
+
if (expiredIds.has(job.id)) return false
|
|
94
|
+
// Safety: hide jobs whose finishedAt is already past the window
|
|
95
|
+
// (handles panel remount or page reload without relying solely on expiredIds)
|
|
96
|
+
const finishedAt = safeParseMs(job.finishedAt)
|
|
97
|
+
if (finishedAt !== null && now - finishedAt >= timeout) return false
|
|
98
|
+
return true
|
|
99
|
+
})
|
|
100
|
+
}
|
|
@@ -177,6 +177,18 @@ describe('DatePicker primitive', () => {
|
|
|
177
177
|
expect(dayCellButton.className).not.toMatch(/focus-visible:ring-2/)
|
|
178
178
|
}
|
|
179
179
|
})
|
|
180
|
+
|
|
181
|
+
it('opens on the month of the selected value, not the current month (regression)', async () => {
|
|
182
|
+
// A value far from any plausible "today" so the assertion stays deterministic.
|
|
183
|
+
// react-day-picker v10 derives the initial month from `month`/`defaultMonth`
|
|
184
|
+
// (falling back to today) and ignores `selected`, so DatePicker must pass
|
|
185
|
+
// `defaultMonth` or the selected day is never rendered.
|
|
186
|
+
renderWithI18n(<DatePicker value={new Date(2020, 0, 15)} onChange={() => {}} footer="none" />)
|
|
187
|
+
await openPopover()
|
|
188
|
+
const selectedCell = document.querySelector('td[data-selected="true"]')
|
|
189
|
+
expect(selectedCell).not.toBeNull()
|
|
190
|
+
expect((selectedCell as HTMLElement).className).toMatch(/!bg-primary/)
|
|
191
|
+
})
|
|
180
192
|
})
|
|
181
193
|
|
|
182
194
|
describe('DatePicker backwards-compat shims', () => {
|