@igstack/app-catalog-frontend-core 6.8.4 → 6.10.0
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/appCatalog/ui/detail/CommunitySection.feed.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/composeComments.test.d.ts +1 -0
- package/dist/esm/__tests__/ui/detail/CommunityInput.test.d.ts +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +8 -0
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +22 -12
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +254 -232
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +187 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +91 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +328 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +15 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +26 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +8 -19
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +49 -37
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +30 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +50 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityError.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityError.js +8 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityError.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.d.ts +27 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +66 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.d.ts +11 -0
- package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js +15 -0
- package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js.map +1 -0
- package/dist/esm/modules/extensions/features/resourceDetail.d.ts +54 -1
- package/dist/esm/modules/extensions/features/resourceDetail.js +4 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -1
- package/dist/esm/modules/extensions/index.d.ts +20 -4
- package/dist/esm/modules/extensions/index.js +10 -4
- package/dist/esm/modules/extensions/index.js.map +1 -1
- package/dist/esm/modules/extensions/types.d.ts +1 -0
- package/dist/index.css +525 -49
- package/package.json +4 -4
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +41 -4
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.test.tsx +34 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +9 -10
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +402 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +8 -4
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +175 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +281 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +179 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +423 -0
- package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +170 -0
- package/src/__tests__/ui/detail/CommunityInput.test.tsx +39 -0
- package/src/index.css +525 -49
- package/src/index.tsx +2 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +60 -36
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +310 -295
- package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +254 -0
- package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +102 -0
- package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +419 -0
- package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +41 -0
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +77 -71
- package/src/modules/appCatalog/ui/detail/communityDraft.ts +80 -0
- package/src/modules/appCatalog/ui/detail/communityError.ts +10 -0
- package/src/modules/appCatalog/ui/detail/composeComments.ts +103 -0
- package/src/modules/appCatalog/ui/detail/usePendingCount.ts +26 -0
- package/src/modules/extensions/features/resourceDetail.ts +45 -1
- package/src/modules/extensions/index.ts +39 -2
- package/src/modules/extensions/types.ts +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +0 -13
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +0 -433
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +0 -1
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +0 -181
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +0 -131
- package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +0 -605
- /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.emptyState.test.d.ts → AppDetails.behindThisCard.test.d.ts} +0 -0
- /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.reviewStatus.test.d.ts → AppDetails.knowMore.test.d.ts} +0 -0
|
@@ -3,17 +3,11 @@ import type {
|
|
|
3
3
|
SourceReference,
|
|
4
4
|
} from '@igstack/app-catalog-backend-core'
|
|
5
5
|
import { useSearch } from '@tanstack/react-router'
|
|
6
|
-
import {
|
|
6
|
+
import { ExternalLink, Plus, Trash2 } from 'lucide-react'
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
9
9
|
import { Badge } from '~/ui/badge'
|
|
10
10
|
import { Button } from '~/ui/button'
|
|
11
|
-
import {
|
|
12
|
-
Accordion,
|
|
13
|
-
AccordionContent,
|
|
14
|
-
AccordionItem,
|
|
15
|
-
AccordionTrigger,
|
|
16
|
-
} from '~/ui/accordion'
|
|
17
11
|
import { AccessRequestSection } from '../components/AccessRequestSection'
|
|
18
12
|
import { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'
|
|
19
13
|
import { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'
|
|
@@ -31,8 +25,13 @@ import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
|
31
25
|
import { TierVariantsSection } from '../components/TierVariantsSection'
|
|
32
26
|
import { SubResourcesSection } from '../components/SubResourcesSection'
|
|
33
27
|
import { getChildResources } from '../../utils/resolveHelpers'
|
|
34
|
-
import {
|
|
35
|
-
|
|
28
|
+
import {
|
|
29
|
+
ResourceDetailAccessActions,
|
|
30
|
+
ResourceDetailCardSource,
|
|
31
|
+
} from '~/modules/extensions'
|
|
32
|
+
import { CommunitySection } from './CommunitySection'
|
|
33
|
+
import type { CommunitySectionHandle } from './CommunitySection'
|
|
34
|
+
import { usePendingCount } from './usePendingCount'
|
|
36
35
|
import { displayUrl } from '~/modules/appCatalog/utils/displayUrl'
|
|
37
36
|
import { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'
|
|
38
37
|
import { AppDoor } from './AppDoor'
|
|
@@ -45,9 +44,28 @@ const TAB = {
|
|
|
45
44
|
access: 'access',
|
|
46
45
|
resources: 'resources',
|
|
47
46
|
documentation: 'documentation',
|
|
48
|
-
|
|
47
|
+
behind: 'behind',
|
|
49
48
|
} as const
|
|
50
49
|
|
|
50
|
+
/**
|
|
51
|
+
* One of Behind this card's top-level groups. A level above `Section`: its
|
|
52
|
+
* title is a plain heading, and a hairline separates it from the group before.
|
|
53
|
+
*/
|
|
54
|
+
function Group({
|
|
55
|
+
title,
|
|
56
|
+
children,
|
|
57
|
+
}: {
|
|
58
|
+
title: string
|
|
59
|
+
children: React.ReactNode
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<section className="behind-group">
|
|
63
|
+
<h2 className="behind-group__title">{title}</h2>
|
|
64
|
+
{children}
|
|
65
|
+
</section>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/** A labelled block inside a tab. One register for all of them. */
|
|
52
70
|
function Section({
|
|
53
71
|
title,
|
|
@@ -123,19 +141,6 @@ export function AppDetails({
|
|
|
123
141
|
}) {
|
|
124
142
|
const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)
|
|
125
143
|
const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)
|
|
126
|
-
/**
|
|
127
|
-
* Set by the band's "Suggest a change", which has to do two things the button
|
|
128
|
-
* cannot do alone: bring the notes tab forward, and open the composer inside it.
|
|
129
|
-
* Held here because the band and the panel are siblings — neither can reach the
|
|
130
|
-
* other, and routing it through the URL would put a transient intent in a
|
|
131
|
-
* shareable link.
|
|
132
|
-
*
|
|
133
|
-
* A count, not a flag: the composer closes itself on send, so asking a second
|
|
134
|
-
* time has to be distinguishable from the first ask still being set. As a
|
|
135
|
-
* boolean the second click changed nothing and the button was dead for the rest
|
|
136
|
-
* of the card's life.
|
|
137
|
-
*/
|
|
138
|
-
const [composeRequests, setComposeRequests] = React.useState(0)
|
|
139
144
|
|
|
140
145
|
const { approvalMethods, resources: allResources } = useAppCatalogContext()
|
|
141
146
|
const { recordClick } = useAppClickHistory()
|
|
@@ -144,6 +149,39 @@ export function AppDetails({
|
|
|
144
149
|
const user = useUser()
|
|
145
150
|
const isAdmin = user?.isAdmin ?? false
|
|
146
151
|
const { state: filterState } = useAppCatalogFilters()
|
|
152
|
+
// Agrees with the Pending badges in the feed: composed by the same rules.
|
|
153
|
+
const behindCount = usePendingCount(app.slug, app.claims)
|
|
154
|
+
|
|
155
|
+
// "Last checked 10 hours ago · added 7 months ago" under the sources.
|
|
156
|
+
const lastChecked = app.freshness?.lastCheckedAt
|
|
157
|
+
const dateParts = []
|
|
158
|
+
if (lastChecked) {
|
|
159
|
+
const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''
|
|
160
|
+
dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)
|
|
161
|
+
}
|
|
162
|
+
if (app.createdAt) {
|
|
163
|
+
const addedLabel = dateParts.length > 0 ? 'added' : 'Added'
|
|
164
|
+
dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)
|
|
165
|
+
}
|
|
166
|
+
const datesLine = dateParts.join(' · ')
|
|
167
|
+
const datesTitle = [
|
|
168
|
+
app.freshness?.lastContentChangeAt &&
|
|
169
|
+
`Content last changed ${app.freshness.lastContentChangeAt}`,
|
|
170
|
+
lastChecked && `Last checked ${lastChecked}`,
|
|
171
|
+
app.createdAt && `Added ${app.createdAt}`,
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(' · ')
|
|
175
|
+
|
|
176
|
+
// "Share what you know" asks for the input by bumping this. A counter, not a
|
|
177
|
+
// flag, so a second press while already on the tab still re-focuses. The
|
|
178
|
+
// effect runs after the click's render has mounted the tab's panel, so the
|
|
179
|
+
// ref is attached by then; opening the tab by hand bumps nothing.
|
|
180
|
+
const knowMoreRef = React.useRef<CommunitySectionHandle>(null)
|
|
181
|
+
const [shareAsk, setShareAsk] = React.useState(0)
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (shareAsk) knowMoreRef.current?.focus()
|
|
184
|
+
}, [shareAsk])
|
|
147
185
|
|
|
148
186
|
// Arriving with `?sub=` means the visitor picked one child out of a search —
|
|
149
187
|
// so the card has to open ON that child's tab. Landing on Overview and
|
|
@@ -240,6 +278,8 @@ export function AppDetails({
|
|
|
240
278
|
? (app.childrenLabel ?? 'Resources')
|
|
241
279
|
: 'Environments'
|
|
242
280
|
|
|
281
|
+
const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background
|
|
282
|
+
|
|
243
283
|
const tabs: DetailTab[] = [
|
|
244
284
|
{
|
|
245
285
|
id: TAB.overview,
|
|
@@ -367,263 +407,244 @@ export function AppDetails({
|
|
|
367
407
|
} satisfies DetailTab,
|
|
368
408
|
]
|
|
369
409
|
: []),
|
|
410
|
+
...(hasDocumentationTab
|
|
411
|
+
? [
|
|
412
|
+
{
|
|
413
|
+
id: TAB.documentation,
|
|
414
|
+
label: 'Documentation',
|
|
415
|
+
render: () => (
|
|
416
|
+
<>
|
|
417
|
+
{app.links && app.links.length > 0 && (
|
|
418
|
+
<Section title="Documentation" first>
|
|
419
|
+
<div className="space-y-0.5">
|
|
420
|
+
{app.links.map((link) => (
|
|
421
|
+
<a
|
|
422
|
+
key={link.url}
|
|
423
|
+
href={link.url}
|
|
424
|
+
target="_blank"
|
|
425
|
+
rel="noopener noreferrer"
|
|
426
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate"
|
|
427
|
+
>
|
|
428
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
429
|
+
{link.title || displayUrl(link.url)}
|
|
430
|
+
</a>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
</Section>
|
|
434
|
+
)}
|
|
435
|
+
{/* Background: why the app exists at all. Deliberately quiet — a
|
|
436
|
+
reader who came to get access has already passed everything they
|
|
437
|
+
need, and history should not compete with the description. */}
|
|
438
|
+
{app.background && (
|
|
439
|
+
<Section title="Background" first={!app.links?.length}>
|
|
440
|
+
<MarkdownText className="prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0">
|
|
441
|
+
{app.background}
|
|
442
|
+
</MarkdownText>
|
|
443
|
+
</Section>
|
|
444
|
+
)}
|
|
445
|
+
</>
|
|
446
|
+
),
|
|
447
|
+
} satisfies DetailTab,
|
|
448
|
+
]
|
|
449
|
+
: []),
|
|
370
450
|
{
|
|
371
|
-
id: TAB.
|
|
372
|
-
label: '
|
|
451
|
+
id: TAB.behind,
|
|
452
|
+
label: 'Behind this card',
|
|
453
|
+
chip: behindCount ? String(behindCount) : undefined,
|
|
454
|
+
ariaLabel: behindCount
|
|
455
|
+
? `Behind this card, ${behindCount} waiting for review`
|
|
456
|
+
: undefined,
|
|
373
457
|
render: () => (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
<
|
|
377
|
-
<
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate"
|
|
385
|
-
>
|
|
386
|
-
<ExternalLink className="size-3 shrink-0" />
|
|
387
|
-
{link.title || displayUrl(link.url)}
|
|
388
|
-
</a>
|
|
389
|
-
))}
|
|
390
|
-
</div>
|
|
391
|
-
</Section>
|
|
392
|
-
)}
|
|
393
|
-
{/* Background: why the app exists at all. Deliberately quiet — a
|
|
394
|
-
reader who came to get access has already passed everything they
|
|
395
|
-
need, and history should not compete with the description. */}
|
|
396
|
-
{app.background && (
|
|
397
|
-
<Section title="Background">
|
|
398
|
-
<MarkdownText className="prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0">
|
|
399
|
-
{app.background}
|
|
400
|
-
</MarkdownText>
|
|
401
|
-
</Section>
|
|
402
|
-
)}
|
|
458
|
+
<div className="behind-panel">
|
|
459
|
+
<div className="behind-layout">
|
|
460
|
+
<div className="behind-main">
|
|
461
|
+
<CommunitySection
|
|
462
|
+
ref={knowMoreRef}
|
|
463
|
+
appSlug={app.slug}
|
|
464
|
+
appName={app.displayName}
|
|
465
|
+
claims={app.claims}
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
403
468
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
<li
|
|
509
|
-
key={index}
|
|
510
|
-
className="group flex items-center gap-2 text-xs"
|
|
511
|
-
>
|
|
512
|
-
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
513
|
-
{index + 1}.
|
|
514
|
-
</span>
|
|
515
|
-
{url ? (
|
|
516
|
-
<a
|
|
517
|
-
href={url}
|
|
518
|
-
target="_blank"
|
|
519
|
-
rel="noopener noreferrer"
|
|
520
|
-
className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
|
|
469
|
+
<div className="behind-aside">
|
|
470
|
+
{(isAdmin ||
|
|
471
|
+
sourceUrls.length > 0 ||
|
|
472
|
+
app.createdAt ||
|
|
473
|
+
app.freshness?.lastCheckedAt) && (
|
|
474
|
+
<Group title="Kept up to date from">
|
|
475
|
+
{isAdmin ? (
|
|
476
|
+
<>
|
|
477
|
+
<ul className="space-y-2">
|
|
478
|
+
{displaySources.map((url, index) => {
|
|
479
|
+
const isDraft =
|
|
480
|
+
draftSource !== null && index === sourceUrls.length
|
|
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('')}
|
|
521
573
|
>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
</
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
ran — entries predating the field have none, so fall
|
|
570
|
-
back. */}
|
|
571
|
-
Updated{' '}
|
|
572
|
-
{formatRelativeTime(
|
|
573
|
-
app.freshness.lastContentChangeAt ??
|
|
574
|
-
app.freshness.lastCheckedAt,
|
|
575
|
-
)}
|
|
576
|
-
{app.freshness.isStale && (
|
|
577
|
-
<span className="italic opacity-75">
|
|
578
|
-
{' '}
|
|
579
|
-
· may be out of date
|
|
580
|
-
</span>
|
|
581
|
-
)}
|
|
582
|
-
</span>
|
|
583
|
-
)}
|
|
584
|
-
</div>
|
|
585
|
-
</Section>
|
|
586
|
-
)}
|
|
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.
|
|
584
|
+
<li
|
|
585
|
+
key={index}
|
|
586
|
+
className="group flex items-center gap-2 text-xs"
|
|
587
|
+
>
|
|
588
|
+
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
589
|
+
{index + 1}.
|
|
590
|
+
</span>
|
|
591
|
+
{url ? (
|
|
592
|
+
<a
|
|
593
|
+
href={url}
|
|
594
|
+
target="_blank"
|
|
595
|
+
rel="noopener noreferrer"
|
|
596
|
+
className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
|
|
597
|
+
>
|
|
598
|
+
{displayUrl(url)}
|
|
599
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
600
|
+
</a>
|
|
601
|
+
) : (
|
|
602
|
+
<span className="text-muted-foreground">—</span>
|
|
603
|
+
)}
|
|
604
|
+
{pulseByUrl.has(url) && (
|
|
605
|
+
<SourcePulse
|
|
606
|
+
pulse={pulseByUrl.get(url)!}
|
|
607
|
+
className="ml-auto"
|
|
608
|
+
/>
|
|
609
|
+
)}
|
|
610
|
+
</li>
|
|
611
|
+
))}
|
|
612
|
+
</ul>
|
|
613
|
+
)}
|
|
614
|
+
{datesLine && (
|
|
615
|
+
<p className="behind-dates" title={datesTitle}>
|
|
616
|
+
{datesLine}
|
|
617
|
+
</p>
|
|
618
|
+
)}
|
|
619
|
+
</Group>
|
|
620
|
+
)}
|
|
587
621
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
<Accordion type="single" collapsible className="mt-7">
|
|
591
|
-
<AccordionItem
|
|
592
|
-
value="technical-info"
|
|
593
|
-
className="border rounded-lg px-4"
|
|
594
|
-
>
|
|
595
|
-
<AccordionTrigger className="text-sm hover:no-underline py-3">
|
|
596
|
-
Technical information
|
|
597
|
-
</AccordionTrigger>
|
|
598
|
-
<AccordionContent className="pb-3 space-y-3">
|
|
622
|
+
{(app.aiPrompt || app.aiMemory) && (
|
|
623
|
+
<Group title="How the AI maintains this card">
|
|
599
624
|
{app.aiPrompt && (
|
|
600
|
-
|
|
601
|
-
<
|
|
602
|
-
<p className="
|
|
603
|
-
|
|
604
|
-
</p>
|
|
605
|
-
</div>
|
|
625
|
+
<>
|
|
626
|
+
<p className="behind-ai__label">Prompt</p>
|
|
627
|
+
<p className="behind-ai">{app.aiPrompt}</p>
|
|
628
|
+
</>
|
|
606
629
|
)}
|
|
607
630
|
{app.aiMemory && (
|
|
608
|
-
|
|
609
|
-
<
|
|
610
|
-
<p className="
|
|
611
|
-
|
|
612
|
-
</p>
|
|
613
|
-
</div>
|
|
631
|
+
<>
|
|
632
|
+
<p className="behind-ai__label">Memory</p>
|
|
633
|
+
<p className="behind-ai">{app.aiMemory}</p>
|
|
634
|
+
</>
|
|
614
635
|
)}
|
|
615
|
-
</
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
636
|
+
</Group>
|
|
637
|
+
)}
|
|
638
|
+
|
|
639
|
+
<div className="behind-card-source">
|
|
640
|
+
<ResourceDetailCardSource
|
|
641
|
+
appSlug={app.slug}
|
|
642
|
+
appName={app.displayName}
|
|
643
|
+
/>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
627
648
|
),
|
|
628
649
|
},
|
|
629
650
|
]
|
|
@@ -655,30 +676,12 @@ export function AppDetails({
|
|
|
655
676
|
: 'Deprecated'}
|
|
656
677
|
</Badge>
|
|
657
678
|
)}
|
|
658
|
-
{/*
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
card to find. Up here it is present on every tab, which is the whole
|
|
663
|
-
reason the band does not scroll away.
|
|
664
|
-
`mr-10` keeps it clear of the dialog's own close button at top-right. */}
|
|
665
|
-
{/* Plugin actions sit beside "Suggest a change", sharing its reasoning:
|
|
666
|
-
the band is mounted on every tab and never scrolls away. A slot in a
|
|
667
|
-
tab panel would be invisible until a second click, and one inside
|
|
668
|
-
SubResourceDetailPanel's `hasAnyAccess` gate would vanish for exactly
|
|
669
|
-
the resources that need help most.
|
|
670
|
-
Renders nothing when no plugin contributes, so the band keeps its
|
|
671
|
-
current shape in the open-source build. */}
|
|
679
|
+
{/* Plugin actions live in the band because it is mounted on every tab and
|
|
680
|
+
never scrolls away; a slot in a tab panel would be invisible until a
|
|
681
|
+
second click. Renders nothing when no plugin contributes.
|
|
682
|
+
`mr-10` keeps them clear of the dialog's close button at top-right. */}
|
|
672
683
|
<div className="ml-auto mr-10 flex items-center gap-2">
|
|
673
684
|
<ResourceDetailAccessActions {...accessActionsCtx} />
|
|
674
|
-
<Button
|
|
675
|
-
type="button"
|
|
676
|
-
size="sm"
|
|
677
|
-
variant="outline"
|
|
678
|
-
onClick={() => setComposeRequests((n) => n + 1)}
|
|
679
|
-
>
|
|
680
|
-
✎ Suggest a change
|
|
681
|
-
</Button>
|
|
682
685
|
</div>
|
|
683
686
|
</div>
|
|
684
687
|
|
|
@@ -780,8 +783,20 @@ export function AppDetails({
|
|
|
780
783
|
// `?sub=` means a child was picked out of a search, so the card opens
|
|
781
784
|
// on the tab that holds it rather than making them hunt for it.
|
|
782
785
|
forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}
|
|
783
|
-
|
|
784
|
-
|
|
786
|
+
trailing={(select) => (
|
|
787
|
+
<button
|
|
788
|
+
type="button"
|
|
789
|
+
className="detail-share"
|
|
790
|
+
aria-label="Share what you know"
|
|
791
|
+
onClick={() => {
|
|
792
|
+
select(TAB.behind)
|
|
793
|
+
setShareAsk((n) => n + 1)
|
|
794
|
+
}}
|
|
795
|
+
>
|
|
796
|
+
<span className="max-sm:sr-only">Share what you know</span>
|
|
797
|
+
<Plus className="size-4 sm:hidden" aria-hidden="true" />
|
|
798
|
+
</button>
|
|
799
|
+
)}
|
|
785
800
|
/>
|
|
786
801
|
</div>
|
|
787
802
|
|