@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.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/AppShell.js +2 -2
  3. package/dist/backend/AppShell.js.map +2 -2
  4. package/dist/backend/dashboard/DashboardScreen.js +3 -2
  5. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  6. package/dist/backend/dashboard/index.js +4 -1
  7. package/dist/backend/dashboard/index.js.map +2 -2
  8. package/dist/backend/dashboard/widgetData.js +59 -0
  9. package/dist/backend/dashboard/widgetData.js.map +7 -0
  10. package/dist/backend/dashboard/widgetDataBatcher.js +61 -0
  11. package/dist/backend/dashboard/widgetDataBatcher.js.map +7 -0
  12. package/dist/backend/progress/ProgressTopBar.js +6 -4
  13. package/dist/backend/progress/ProgressTopBar.js.map +2 -2
  14. package/dist/backend/progress/useAutoHideCompletedJobs.js +65 -0
  15. package/dist/backend/progress/useAutoHideCompletedJobs.js.map +7 -0
  16. package/dist/primitives/date-picker.js +1 -0
  17. package/dist/primitives/date-picker.js.map +2 -2
  18. package/package.json +3 -3
  19. package/src/backend/AppShell.tsx +7 -2
  20. package/src/backend/__tests__/useAutoHideCompletedJobs.test.tsx +179 -0
  21. package/src/backend/dashboard/DashboardScreen.tsx +3 -0
  22. package/src/backend/dashboard/__tests__/widgetDataBatcher.test.ts +97 -0
  23. package/src/backend/dashboard/index.ts +1 -0
  24. package/src/backend/dashboard/widgetData.tsx +84 -0
  25. package/src/backend/dashboard/widgetDataBatcher.ts +100 -0
  26. package/src/backend/progress/ProgressTopBar.tsx +12 -4
  27. package/src/backend/progress/useAutoHideCompletedJobs.ts +100 -0
  28. package/src/primitives/__tests__/date-picker.test.tsx +12 -0
  29. 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 = recentlyCompleted.length > 0
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: recentlyCompleted.length })}
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
- {recentlyCompleted.map((job) => (
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', () => {
@@ -230,6 +230,7 @@ export function DatePicker({
230
230
  <Calendar
231
231
  mode="single"
232
232
  selected={selectedDate ?? undefined}
233
+ defaultMonth={selectedDate ?? undefined}
233
234
  onSelect={handleDaySelect}
234
235
  locale={locale}
235
236
  disabled={disabledMatcher}