@igstack/app-catalog-frontend-core 6.10.0 → 6.10.2
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/esm/__tests__/modules/pwa/PwaAutoUpdateProvider.test.d.ts +1 -0
- package/dist/esm/internal.d.ts +1 -0
- package/dist/esm/internal.js +2 -0
- package/dist/esm/internal.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +143 -147
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.d.ts +2 -0
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +8 -4
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.js +1 -1
- package/dist/esm/modules/pwa/PwaAutoUpdateContext.js +16 -8
- package/dist/esm/modules/pwa/PwaAutoUpdateContext.js.map +1 -1
- package/dist/esm/ui/layout/Devtools.js.map +1 -1
- package/dist/index.css +1 -25
- package/package.json +4 -4
- package/src/__tests__/modules/dismiss/DismissStack.test.tsx +10 -12
- package/src/__tests__/modules/pwa/PwaAutoUpdateProvider.test.tsx +44 -0
- package/src/index.css +1 -25
- package/src/internal.ts +1 -0
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +158 -170
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +14 -5
- package/src/modules/pwa/PwaAutoUpdateContext.tsx +21 -10
- package/src/ui/layout/Devtools.tsx +6 -3
|
@@ -456,193 +456,181 @@ export function AppDetails({
|
|
|
456
456
|
: undefined,
|
|
457
457
|
render: () => (
|
|
458
458
|
<div className="behind-panel">
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
claims={app.claims}
|
|
466
|
-
/>
|
|
467
|
-
</div>
|
|
459
|
+
<CommunitySection
|
|
460
|
+
ref={knowMoreRef}
|
|
461
|
+
appSlug={app.slug}
|
|
462
|
+
appName={app.displayName}
|
|
463
|
+
claims={app.claims}
|
|
464
|
+
/>
|
|
468
465
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return (
|
|
482
|
-
<li
|
|
483
|
-
key={isDraft ? 'draft' : `${index}-${url}`}
|
|
484
|
-
className="group flex items-center gap-2 text-xs"
|
|
485
|
-
>
|
|
486
|
-
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
487
|
-
{index + 1}.
|
|
488
|
-
</span>
|
|
489
|
-
<InlineEditableField
|
|
490
|
-
value={url}
|
|
491
|
-
initialEditMode={isDraft}
|
|
492
|
-
onCancel={
|
|
493
|
-
isDraft
|
|
494
|
-
? () => setDraftSource(null)
|
|
495
|
-
: undefined
|
|
496
|
-
}
|
|
497
|
-
onSave={(newUrl) => {
|
|
498
|
-
if (isDraft) {
|
|
499
|
-
setDraftSource(null)
|
|
500
|
-
if (newUrl) {
|
|
501
|
-
updateApp.mutate({
|
|
502
|
-
id: app.id,
|
|
503
|
-
data: {
|
|
504
|
-
sources: [...sourceUrls, newUrl],
|
|
505
|
-
},
|
|
506
|
-
})
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
const next = [...sourceUrls]
|
|
510
|
-
next[index] = newUrl
|
|
511
|
-
updateApp.mutate({
|
|
512
|
-
id: app.id,
|
|
513
|
-
data: { sources: next.filter(Boolean) },
|
|
514
|
-
})
|
|
515
|
-
}
|
|
516
|
-
}}
|
|
517
|
-
placeholder="https://..."
|
|
518
|
-
viewClassName="flex-1 min-w-0"
|
|
519
|
-
renderView={(val) =>
|
|
520
|
-
val ? (
|
|
521
|
-
<a
|
|
522
|
-
href={val}
|
|
523
|
-
target="_blank"
|
|
524
|
-
rel="noopener noreferrer"
|
|
525
|
-
className="hover:text-primary inline-flex items-center gap-1 truncate"
|
|
526
|
-
>
|
|
527
|
-
{displayUrl(val)}
|
|
528
|
-
<ExternalLink className="size-3 shrink-0" />
|
|
529
|
-
</a>
|
|
530
|
-
) : (
|
|
531
|
-
<span className="text-muted-foreground">
|
|
532
|
-
—
|
|
533
|
-
</span>
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
/>
|
|
537
|
-
{!isDraft && pulseByUrl.has(url) && (
|
|
538
|
-
<SourcePulse
|
|
539
|
-
pulse={pulseByUrl.get(url)!}
|
|
540
|
-
className="ml-auto"
|
|
541
|
-
/>
|
|
542
|
-
)}
|
|
543
|
-
{!isDraft && (
|
|
544
|
-
<Button
|
|
545
|
-
type="button"
|
|
546
|
-
variant="ghost"
|
|
547
|
-
size="icon-sm"
|
|
548
|
-
aria-label="Remove source"
|
|
549
|
-
className="shrink-0 text-muted-foreground hover:text-destructive"
|
|
550
|
-
onClick={() => {
|
|
551
|
-
const next = sourceUrls.filter(
|
|
552
|
-
(_, i) => i !== index,
|
|
553
|
-
)
|
|
554
|
-
updateApp.mutate({
|
|
555
|
-
id: app.id,
|
|
556
|
-
data: { sources: next },
|
|
557
|
-
})
|
|
558
|
-
}}
|
|
559
|
-
>
|
|
560
|
-
<Trash2 className="size-3.5" />
|
|
561
|
-
</Button>
|
|
562
|
-
)}
|
|
563
|
-
</li>
|
|
564
|
-
)
|
|
565
|
-
})}
|
|
566
|
-
</ul>
|
|
567
|
-
<Button
|
|
568
|
-
type="button"
|
|
569
|
-
variant="ghost"
|
|
570
|
-
size="sm"
|
|
571
|
-
className="mt-2 gap-1 text-muted-foreground"
|
|
572
|
-
onClick={() => setDraftSource('')}
|
|
573
|
-
>
|
|
574
|
-
<Plus className="size-3.5" />
|
|
575
|
-
Add source
|
|
576
|
-
</Button>
|
|
577
|
-
</>
|
|
578
|
-
) : (
|
|
579
|
-
<ul className="space-y-2">
|
|
580
|
-
{sourceUrls.map((url, index) => (
|
|
581
|
-
// `group` so hovering anywhere on the row fades that source's
|
|
582
|
-
// track in — the mark sits in a right-hand column, and row
|
|
583
|
-
// hover is what ties the two ends of a wide row together.
|
|
466
|
+
{(isAdmin ||
|
|
467
|
+
sourceUrls.length > 0 ||
|
|
468
|
+
app.createdAt ||
|
|
469
|
+
app.freshness?.lastCheckedAt) && (
|
|
470
|
+
<Group title="Kept up to date from">
|
|
471
|
+
{isAdmin ? (
|
|
472
|
+
<>
|
|
473
|
+
<ul className="space-y-2">
|
|
474
|
+
{displaySources.map((url, index) => {
|
|
475
|
+
const isDraft =
|
|
476
|
+
draftSource !== null && index === sourceUrls.length
|
|
477
|
+
return (
|
|
584
478
|
<li
|
|
585
|
-
key={index}
|
|
479
|
+
key={isDraft ? 'draft' : `${index}-${url}`}
|
|
586
480
|
className="group flex items-center gap-2 text-xs"
|
|
587
481
|
>
|
|
588
482
|
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
589
483
|
{index + 1}.
|
|
590
484
|
</span>
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
485
|
+
<InlineEditableField
|
|
486
|
+
value={url}
|
|
487
|
+
initialEditMode={isDraft}
|
|
488
|
+
onCancel={
|
|
489
|
+
isDraft ? () => setDraftSource(null) : undefined
|
|
490
|
+
}
|
|
491
|
+
onSave={(newUrl) => {
|
|
492
|
+
if (isDraft) {
|
|
493
|
+
setDraftSource(null)
|
|
494
|
+
if (newUrl) {
|
|
495
|
+
updateApp.mutate({
|
|
496
|
+
id: app.id,
|
|
497
|
+
data: { sources: [...sourceUrls, newUrl] },
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
const next = [...sourceUrls]
|
|
502
|
+
next[index] = newUrl
|
|
503
|
+
updateApp.mutate({
|
|
504
|
+
id: app.id,
|
|
505
|
+
data: { sources: next.filter(Boolean) },
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
}}
|
|
509
|
+
placeholder="https://..."
|
|
510
|
+
viewClassName="flex-1 min-w-0"
|
|
511
|
+
renderView={(val) =>
|
|
512
|
+
val ? (
|
|
513
|
+
<a
|
|
514
|
+
href={val}
|
|
515
|
+
target="_blank"
|
|
516
|
+
rel="noopener noreferrer"
|
|
517
|
+
className="hover:text-primary inline-flex items-center gap-1 truncate"
|
|
518
|
+
>
|
|
519
|
+
{displayUrl(val)}
|
|
520
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
521
|
+
</a>
|
|
522
|
+
) : (
|
|
523
|
+
<span className="text-muted-foreground">—</span>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
/>
|
|
527
|
+
{!isDraft && pulseByUrl.has(url) && (
|
|
605
528
|
<SourcePulse
|
|
606
529
|
pulse={pulseByUrl.get(url)!}
|
|
607
530
|
className="ml-auto"
|
|
608
531
|
/>
|
|
609
532
|
)}
|
|
533
|
+
{!isDraft && (
|
|
534
|
+
<Button
|
|
535
|
+
type="button"
|
|
536
|
+
variant="ghost"
|
|
537
|
+
size="icon-sm"
|
|
538
|
+
aria-label="Remove source"
|
|
539
|
+
className="shrink-0 text-muted-foreground hover:text-destructive"
|
|
540
|
+
onClick={() => {
|
|
541
|
+
const next = sourceUrls.filter(
|
|
542
|
+
(_, i) => i !== index,
|
|
543
|
+
)
|
|
544
|
+
updateApp.mutate({
|
|
545
|
+
id: app.id,
|
|
546
|
+
data: { sources: next },
|
|
547
|
+
})
|
|
548
|
+
}}
|
|
549
|
+
>
|
|
550
|
+
<Trash2 className="size-3.5" />
|
|
551
|
+
</Button>
|
|
552
|
+
)}
|
|
610
553
|
</li>
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
554
|
+
)
|
|
555
|
+
})}
|
|
556
|
+
</ul>
|
|
557
|
+
<Button
|
|
558
|
+
type="button"
|
|
559
|
+
variant="ghost"
|
|
560
|
+
size="sm"
|
|
561
|
+
className="mt-2 gap-1 text-muted-foreground"
|
|
562
|
+
onClick={() => setDraftSource('')}
|
|
563
|
+
>
|
|
564
|
+
<Plus className="size-3.5" />
|
|
565
|
+
Add source
|
|
566
|
+
</Button>
|
|
567
|
+
</>
|
|
568
|
+
) : (
|
|
569
|
+
<ul className="space-y-2">
|
|
570
|
+
{sourceUrls.map((url, index) => (
|
|
571
|
+
// `group` so hovering anywhere on the row fades that source's
|
|
572
|
+
// track in — the mark sits in a right-hand column, and row
|
|
573
|
+
// hover is what ties the two ends of a wide row together.
|
|
574
|
+
<li
|
|
575
|
+
key={index}
|
|
576
|
+
className="group flex items-center gap-2 text-xs"
|
|
577
|
+
>
|
|
578
|
+
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
579
|
+
{index + 1}.
|
|
580
|
+
</span>
|
|
581
|
+
{url ? (
|
|
582
|
+
<a
|
|
583
|
+
href={url}
|
|
584
|
+
target="_blank"
|
|
585
|
+
rel="noopener noreferrer"
|
|
586
|
+
className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
|
|
587
|
+
>
|
|
588
|
+
{displayUrl(url)}
|
|
589
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
590
|
+
</a>
|
|
591
|
+
) : (
|
|
592
|
+
<span className="text-muted-foreground">—</span>
|
|
593
|
+
)}
|
|
594
|
+
{pulseByUrl.has(url) && (
|
|
595
|
+
<SourcePulse
|
|
596
|
+
pulse={pulseByUrl.get(url)!}
|
|
597
|
+
className="ml-auto"
|
|
598
|
+
/>
|
|
599
|
+
)}
|
|
600
|
+
</li>
|
|
601
|
+
))}
|
|
602
|
+
</ul>
|
|
603
|
+
)}
|
|
604
|
+
{datesLine && (
|
|
605
|
+
<p className="behind-dates" title={datesTitle}>
|
|
606
|
+
{datesLine}
|
|
607
|
+
</p>
|
|
620
608
|
)}
|
|
609
|
+
</Group>
|
|
610
|
+
)}
|
|
621
611
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)}
|
|
636
|
-
</Group>
|
|
612
|
+
{(app.aiPrompt || app.aiMemory) && (
|
|
613
|
+
<Group title="How the AI maintains this card">
|
|
614
|
+
{app.aiPrompt && (
|
|
615
|
+
<>
|
|
616
|
+
<p className="behind-ai__label">Prompt</p>
|
|
617
|
+
<p className="behind-ai">{app.aiPrompt}</p>
|
|
618
|
+
</>
|
|
619
|
+
)}
|
|
620
|
+
{app.aiMemory && (
|
|
621
|
+
<>
|
|
622
|
+
<p className="behind-ai__label">Memory</p>
|
|
623
|
+
<p className="behind-ai">{app.aiMemory}</p>
|
|
624
|
+
</>
|
|
637
625
|
)}
|
|
626
|
+
</Group>
|
|
627
|
+
)}
|
|
638
628
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
</div>
|
|
645
|
-
</div>
|
|
629
|
+
<div className="behind-card-source">
|
|
630
|
+
<ResourceDetailCardSource
|
|
631
|
+
appSlug={app.slug}
|
|
632
|
+
appName={app.displayName}
|
|
633
|
+
/>
|
|
646
634
|
</div>
|
|
647
635
|
</div>
|
|
648
636
|
),
|
|
@@ -6,12 +6,17 @@ import { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'
|
|
|
6
6
|
import { AppCatalogGrid } from '../catalog/AppCatalogGrid'
|
|
7
7
|
import { AttributionFooter } from '../catalog/AttributionFooter'
|
|
8
8
|
|
|
9
|
-
// The detail card (and the markdown stack it renders)
|
|
10
|
-
//
|
|
9
|
+
// The detail card (and the markdown stack it renders) stays out of the bundle
|
|
10
|
+
// every visitor loads first (#120). The page fetches it after its first paint,
|
|
11
|
+
// so the first click on a row does not wait for the chunk.
|
|
12
|
+
const loadAppDetailPanel = () => import('../catalog/AppDetailPanel')
|
|
13
|
+
|
|
14
|
+
/** Resolves once the detail card's chunk is loaded. */
|
|
15
|
+
export const preloadAppDetailPanel = (): Promise<void> =>
|
|
16
|
+
loadAppDetailPanel().then(() => undefined)
|
|
17
|
+
|
|
11
18
|
const AppDetailPanel = lazy(() =>
|
|
12
|
-
|
|
13
|
-
default: m.AppDetailPanel,
|
|
14
|
-
})),
|
|
19
|
+
loadAppDetailPanel().then((m) => ({ default: m.AppDetailPanel })),
|
|
15
20
|
)
|
|
16
21
|
|
|
17
22
|
export function AppCatalogPage({
|
|
@@ -84,6 +89,10 @@ export function AppCatalogPage({
|
|
|
84
89
|
[rootResources, filterState.showDeprecated],
|
|
85
90
|
)
|
|
86
91
|
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
void preloadAppDetailPanel()
|
|
94
|
+
}, [])
|
|
95
|
+
|
|
87
96
|
// Dev-only skew warning: data arrived (resources > 0) but nothing is top-level
|
|
88
97
|
// (rootResources === 0). That fingerprints a frontend/backend-core version skew
|
|
89
98
|
// or a stale service worker — NOT a normal empty search/filter (those still have
|
|
@@ -23,11 +23,29 @@ export function PwaAutoUpdateProvider({
|
|
|
23
23
|
children,
|
|
24
24
|
}: PwaAutoUpdateProviderProps) {
|
|
25
25
|
const controllerRef = useRef<PwaAutoUpdateController | undefined>(undefined)
|
|
26
|
+
const handleRef = useRef(handle)
|
|
26
27
|
|
|
28
|
+
// A new handle (the registration arriving) goes to the running controller.
|
|
29
|
+
// Declared first, so the effect below reads the current handle.
|
|
27
30
|
useEffect(() => {
|
|
28
|
-
|
|
31
|
+
handleRef.current = handle
|
|
32
|
+
if (handle) controllerRef.current?.updateHandle(handle)
|
|
33
|
+
}, [handle])
|
|
34
|
+
|
|
35
|
+
// One controller for as long as there is a handle and the option values
|
|
36
|
+
// hold. Keyed on values, not identities: rebuilding it drops its pending
|
|
37
|
+
// visibility check and its idle tracking.
|
|
38
|
+
const hasHandle = handle !== undefined
|
|
39
|
+
const { idleTimeoutMs, minCheckIntervalMs, debug } = options ?? {}
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const current = handleRef.current
|
|
42
|
+
if (!current) return
|
|
29
43
|
|
|
30
|
-
const controller = new PwaAutoUpdateController(
|
|
44
|
+
const controller = new PwaAutoUpdateController(current, {
|
|
45
|
+
idleTimeoutMs,
|
|
46
|
+
minCheckIntervalMs,
|
|
47
|
+
debug,
|
|
48
|
+
})
|
|
31
49
|
controllerRef.current = controller
|
|
32
50
|
controller.start()
|
|
33
51
|
|
|
@@ -35,14 +53,7 @@ export function PwaAutoUpdateProvider({
|
|
|
35
53
|
controller.destroy()
|
|
36
54
|
controllerRef.current = undefined
|
|
37
55
|
}
|
|
38
|
-
}, [
|
|
39
|
-
|
|
40
|
-
// Update handle when registration arrives async
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
if (handle && controllerRef.current) {
|
|
43
|
-
controllerRef.current.updateHandle(handle)
|
|
44
|
-
}
|
|
45
|
-
}, [handle])
|
|
56
|
+
}, [hasHandle, idleTimeoutMs, minCheckIntervalMs, debug])
|
|
46
57
|
|
|
47
58
|
const contextValue = useMemo<PwaAutoUpdateContextValue>(
|
|
48
59
|
() => ({
|
|
@@ -3,9 +3,12 @@ import type { QueryClient } from '@tanstack/react-query'
|
|
|
3
3
|
|
|
4
4
|
// Dev-only: `import.meta.env.DEV` is false in production builds, so Rollup drops
|
|
5
5
|
// the import() and neither devtools package reaches the shipped bundle (#120).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Off under vitest too: nobody looks at them there, and every test file would
|
|
7
|
+
// otherwise transform both packages on its first mount.
|
|
8
|
+
const DevtoolsPanels =
|
|
9
|
+
import.meta.env.DEV && import.meta.env.MODE !== 'test'
|
|
10
|
+
? lazy(() => import('./DevtoolsPanels'))
|
|
11
|
+
: null
|
|
9
12
|
|
|
10
13
|
export function Devtools({ client }: { client: QueryClient }) {
|
|
11
14
|
if (!DevtoolsPanels) return null
|