@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a half-written note lives while it is not sent.
|
|
3
|
+
*
|
|
4
|
+
* The input sits inside a tab panel, and a panel unmounts while its tab is not
|
|
5
|
+
* the active one — so component state alone loses whatever someone typed or
|
|
6
|
+
* attached the moment they look at another tab. Losing someone's words is the
|
|
7
|
+
* one failure this feature cannot afford: reporting has to cost almost nothing,
|
|
8
|
+
* and "I typed it and it vanished" is how a person learns not to bother.
|
|
9
|
+
*
|
|
10
|
+
* Session rather than local storage: a draft is in-flight work, not a
|
|
11
|
+
* preference. Keyed by slug so two entries do not share one draft. Storage can
|
|
12
|
+
* throw outright in a locked-down context; a lost draft is not worth taking the
|
|
13
|
+
* card down over, so every access swallows that.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One already-uploaded image waiting to be sent with the words. */
|
|
17
|
+
export interface PendingImage {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const draftKey = (slug: string) => `ac.feedback.draft.${slug}`
|
|
23
|
+
const imagesKey = (slug: string) => `ac.feedback.images.${slug}`
|
|
24
|
+
|
|
25
|
+
export function readDraft(slug: string): string {
|
|
26
|
+
try {
|
|
27
|
+
return sessionStorage.getItem(draftKey(slug)) ?? ''
|
|
28
|
+
} catch {
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeDraft(slug: string, value: string): void {
|
|
34
|
+
try {
|
|
35
|
+
if (value) sessionStorage.setItem(draftKey(slug), value)
|
|
36
|
+
else sessionStorage.removeItem(draftKey(slug))
|
|
37
|
+
} catch {
|
|
38
|
+
/* see the module comment */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Images upload the moment they are picked, so the draft holds ids rather than
|
|
44
|
+
* files. Dropping an id here does NOT delete the image; the server sweeps
|
|
45
|
+
* whatever is never sent, which keeps "remove" instant.
|
|
46
|
+
*/
|
|
47
|
+
export function readAttachments(slug: string): PendingImage[] {
|
|
48
|
+
try {
|
|
49
|
+
const raw = sessionStorage.getItem(imagesKey(slug))
|
|
50
|
+
if (!raw) return []
|
|
51
|
+
const parsed: unknown = JSON.parse(raw)
|
|
52
|
+
if (!Array.isArray(parsed)) return []
|
|
53
|
+
return parsed.filter(
|
|
54
|
+
(entry): entry is PendingImage =>
|
|
55
|
+
typeof entry === 'object' &&
|
|
56
|
+
entry !== null &&
|
|
57
|
+
typeof (entry as PendingImage).id === 'string' &&
|
|
58
|
+
typeof (entry as PendingImage).name === 'string',
|
|
59
|
+
)
|
|
60
|
+
} catch {
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function writeAttachments(slug: string, images: PendingImage[]): void {
|
|
66
|
+
try {
|
|
67
|
+
if (images.length > 0) {
|
|
68
|
+
sessionStorage.setItem(imagesKey(slug), JSON.stringify(images))
|
|
69
|
+
} else {
|
|
70
|
+
sessionStorage.removeItem(imagesKey(slug))
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
/* see the module comment */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The stored image, at whatever size the browser lays it out. */
|
|
78
|
+
export function attachmentUrl(id: string): string {
|
|
79
|
+
return `/api/feedback-attachments/${id}`
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server's reason when it gave a readable one ("Up to 3 images"), else a
|
|
3
|
+
* generic line. Validation failures arrive as serialized issue lists, which
|
|
4
|
+
* are not for a reader.
|
|
5
|
+
*/
|
|
6
|
+
export function formatCommunityError(error: unknown): string {
|
|
7
|
+
const reason =
|
|
8
|
+
error instanceof Error ? error.message.trim().replace(/\.$/, '') : ''
|
|
9
|
+
return reason && !/^[[{]/.test(reason) ? reason : 'Something went wrong'
|
|
10
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Claim,
|
|
3
|
+
FeedbackView as Note,
|
|
4
|
+
} from '@igstack/app-catalog-backend-core'
|
|
5
|
+
|
|
6
|
+
/** `pending` = still being checked; `used` = the card now relies on it. */
|
|
7
|
+
export type CommentBadge = 'pending' | 'used' | null
|
|
8
|
+
|
|
9
|
+
/** One row of the Comments feed. */
|
|
10
|
+
export interface CommentEntry {
|
|
11
|
+
key: string
|
|
12
|
+
/** Null when a claim names no origin at all. */
|
|
13
|
+
who: string | null
|
|
14
|
+
/** Raw ISO date or instant, for the `title`. */
|
|
15
|
+
at: string
|
|
16
|
+
/** Null for a note sent without words; the feed says what it carried instead. */
|
|
17
|
+
text: string | null
|
|
18
|
+
badge: CommentBadge
|
|
19
|
+
/** The visitor's row, when the entry is one — what Edit, Dismiss and screenshots hang off. */
|
|
20
|
+
note?: Note
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FEEDBACK_ORIGIN = 'feedback:'
|
|
24
|
+
|
|
25
|
+
function noteBadge(note: Note, linked: Claim[]): CommentBadge {
|
|
26
|
+
if (note.status === null) return 'pending'
|
|
27
|
+
if (linked.some((c) => c.status === 'review')) return 'pending'
|
|
28
|
+
if (note.status === 'acknowledged') return null
|
|
29
|
+
if (linked.some((c) => c.status === 'active')) return 'used'
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Who said a claim that has no note in the list. A URL reads as its host. */
|
|
34
|
+
function claimWho(origin: string | undefined): string | null {
|
|
35
|
+
if (!origin) return null
|
|
36
|
+
if (origin.startsWith(FEEDBACK_ORIGIN)) return 'A visitor'
|
|
37
|
+
if (/^https?:\/\//.test(origin)) {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(origin).host
|
|
40
|
+
} catch {
|
|
41
|
+
return origin
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return origin
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const time = (at: string) => Date.parse(at) || 0
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Everything said about an entry, one entry per visitor note, newest first.
|
|
51
|
+
*
|
|
52
|
+
* A claim whose origin is `feedback:<id>` belongs to that note: it sets the
|
|
53
|
+
* note's badge and is not shown again. A claim with no note in the list (its
|
|
54
|
+
* row is gone, or it came from a person, team or URL) stands on its own, in
|
|
55
|
+
* its own words. Archived claims are not shown.
|
|
56
|
+
*/
|
|
57
|
+
export function composeComments(
|
|
58
|
+
notes: Note[],
|
|
59
|
+
claims: Claim[],
|
|
60
|
+
): CommentEntry[] {
|
|
61
|
+
const ids = new Set(notes.map((n) => n.id))
|
|
62
|
+
const byNote = new Map<string, Claim[]>()
|
|
63
|
+
const standalone: Claim[] = []
|
|
64
|
+
for (const claim of claims) {
|
|
65
|
+
if (claim.status === 'archived') continue
|
|
66
|
+
const id = claim.origin?.startsWith(FEEDBACK_ORIGIN)
|
|
67
|
+
? claim.origin.slice(FEEDBACK_ORIGIN.length)
|
|
68
|
+
: null
|
|
69
|
+
if (id !== null && ids.has(id)) {
|
|
70
|
+
byNote.set(id, [...(byNote.get(id) ?? []), claim])
|
|
71
|
+
} else {
|
|
72
|
+
standalone.push(claim)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const entries: CommentEntry[] = [
|
|
76
|
+
...notes.map(
|
|
77
|
+
(note): CommentEntry => ({
|
|
78
|
+
key: note.id,
|
|
79
|
+
who: note.authorName,
|
|
80
|
+
at: note.createdAt,
|
|
81
|
+
text: note.body,
|
|
82
|
+
badge: noteBadge(note, byNote.get(note.id) ?? []),
|
|
83
|
+
note,
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
...standalone.map(
|
|
87
|
+
(claim, i): CommentEntry => ({
|
|
88
|
+
key: `claim-${i}`,
|
|
89
|
+
who: claimWho(claim.origin),
|
|
90
|
+
at: claim.addedAt,
|
|
91
|
+
text: claim.text,
|
|
92
|
+
badge: claim.status === 'active' ? 'used' : 'pending',
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
return entries.sort((a, b) => time(b.at) - time(a.at))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The Behind this card count: exactly the feed's Pending badges. */
|
|
100
|
+
export function countPending(notes: Note[], claims: Claim[]): number {
|
|
101
|
+
return composeComments(notes, claims).filter((e) => e.badge === 'pending')
|
|
102
|
+
.length
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Claim } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { useTRPC } from '~/api/infra/trpc'
|
|
4
|
+
import { countPending } from './composeComments'
|
|
5
|
+
|
|
6
|
+
const NO_CLAIMS: Claim[] = []
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* How many entries in the Comments feed say Pending: open notes, notes whose
|
|
10
|
+
* claim is still in review, and review claims with no note of their own.
|
|
11
|
+
*
|
|
12
|
+
* The same query the feed runs, so the tab count and the feed share one cache
|
|
13
|
+
* entry and one request, and are composed by the same rules. Zero while
|
|
14
|
+
* loading or on error: until the notes arrive, a claim cannot be matched to
|
|
15
|
+
* its note, and a count that is not known yet is not shown.
|
|
16
|
+
*/
|
|
17
|
+
export function usePendingCount(
|
|
18
|
+
appSlug: string,
|
|
19
|
+
claims: Claim[] = NO_CLAIMS,
|
|
20
|
+
): number {
|
|
21
|
+
const trpc = useTRPC()
|
|
22
|
+
const { data, error } = useQuery(
|
|
23
|
+
trpc.feedback.list.queryOptions({ resourceSlug: appSlug }),
|
|
24
|
+
)
|
|
25
|
+
return data || error ? countPending(data?.items ?? [], claims) : 0
|
|
26
|
+
}
|
|
@@ -38,7 +38,9 @@ export interface ResourceDetailSlots {
|
|
|
38
38
|
* family has children (hundreds, in a large catalog) — keep the cell cheap and
|
|
39
39
|
* defer anything expensive until it is interacted with. And the column only
|
|
40
40
|
* appears when a plugin actually fills it, so a build with no plugin renders
|
|
41
|
-
* the table exactly as before.
|
|
41
|
+
* the table exactly as before. A plugin that serves only some parents says so
|
|
42
|
+
* in `appliesTo` (see `ResourceDetailSlotScopes`), so every other parent's
|
|
43
|
+
* table gets no column rather than a column of empty cells.
|
|
42
44
|
*/
|
|
43
45
|
resourceSubResourceRowActions: {
|
|
44
46
|
/** The child this row is showing. */
|
|
@@ -47,6 +49,46 @@ export interface ResourceDetailSlots {
|
|
|
47
49
|
parent: Resource
|
|
48
50
|
user: PluginUser | null
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* "What happens next" line at the foot of the "How comments work" popover,
|
|
55
|
+
* beside the Comments heading on Behind this card.
|
|
56
|
+
*
|
|
57
|
+
* Tells the reader what becomes of the text they send. The OSS default is
|
|
58
|
+
* `Every note is reviewed. Facts that check out update the card and are
|
|
59
|
+
* marked Used as source.` A deployment with its own review loop substitutes
|
|
60
|
+
* its own text.
|
|
61
|
+
*/
|
|
62
|
+
resourceDetailFeedbackNote: {
|
|
63
|
+
appSlug: string
|
|
64
|
+
appName: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Where this card's own definition lives, at the foot of Behind this card.
|
|
69
|
+
*
|
|
70
|
+
* A quiet link such as "View this card's source". Only the deployment knows
|
|
71
|
+
* where its catalog entries are kept, so the core has no default and renders
|
|
72
|
+
* nothing here unless a plugin contributes.
|
|
73
|
+
*/
|
|
74
|
+
resourceDetailCardSource: {
|
|
75
|
+
appSlug: string
|
|
76
|
+
appName: string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* What the core knows when it decides a slot's LAYOUT, before any row renders.
|
|
82
|
+
*
|
|
83
|
+
* The input to a plugin's `appliesTo` predicate for that slot. A contribution
|
|
84
|
+
* renders as a component, so the core cannot ask it whether it would render
|
|
85
|
+
* nothing; the plugin has to say up front which scopes it serves.
|
|
86
|
+
*/
|
|
87
|
+
export interface ResourceDetailSlotScopes {
|
|
88
|
+
resourceSubResourceRowActions: {
|
|
89
|
+
/** The entry whose sub-resources table is being laid out. */
|
|
90
|
+
parent: Resource
|
|
91
|
+
}
|
|
50
92
|
}
|
|
51
93
|
|
|
52
94
|
/**
|
|
@@ -71,4 +113,6 @@ export const ResourceDetailAccessActions = mkSlot('resourceDetailAccessActions')
|
|
|
71
113
|
export const ResourceSubResourceRowActions = mkSlot(
|
|
72
114
|
'resourceSubResourceRowActions',
|
|
73
115
|
)
|
|
116
|
+
export const ResourceDetailFeedbackNote = mkSlot('resourceDetailFeedbackNote')
|
|
117
|
+
export const ResourceDetailCardSource = mkSlot('resourceDetailCardSource')
|
|
74
118
|
export const ResourceDetailProvider = mkWrapper('resourceDetailProvider')
|
|
@@ -5,6 +5,7 @@ import type { ReactNode } from 'react'
|
|
|
5
5
|
// resolves it if it has mapped it, which the example app has not.
|
|
6
6
|
import { useOptionalUser } from '../auth/AuthContext'
|
|
7
7
|
import type {
|
|
8
|
+
ResourceDetailSlotScopes,
|
|
8
9
|
ResourceDetailSlots,
|
|
9
10
|
ResourceDetailWrappers,
|
|
10
11
|
} from './features/resourceDetail'
|
|
@@ -20,9 +21,12 @@ export type { AssertDisjoint } from './types'
|
|
|
20
21
|
export {
|
|
21
22
|
ResourceDetailAccessActions,
|
|
22
23
|
ResourceSubResourceRowActions,
|
|
24
|
+
ResourceDetailFeedbackNote,
|
|
25
|
+
ResourceDetailCardSource,
|
|
23
26
|
ResourceDetailProvider,
|
|
24
27
|
} from './features/resourceDetail'
|
|
25
28
|
export type {
|
|
29
|
+
ResourceDetailSlotScopes,
|
|
26
30
|
ResourceDetailSlots,
|
|
27
31
|
ResourceDetailWrappers,
|
|
28
32
|
} from './features/resourceDetail'
|
|
@@ -38,6 +42,9 @@ export type SlotSpec = ResourceDetailSlots
|
|
|
38
42
|
/** Every wrapper slot in the core. */
|
|
39
43
|
export type WrapperSpec = ResourceDetailWrappers
|
|
40
44
|
|
|
45
|
+
/** The layout scope of every slot whose layout depends on more than presence. */
|
|
46
|
+
export type SlotScopeSpec = ResourceDetailSlotScopes
|
|
47
|
+
|
|
41
48
|
// Guard: with a second feature this becomes
|
|
42
49
|
// `AssertDisjoint<ResourceDetailSlots, ServiceDeskSlots>` and fails with the
|
|
43
50
|
// duplicated id spelled out in the error. With one feature it is trivially true,
|
|
@@ -63,6 +70,16 @@ export type PluginWrappers = {
|
|
|
63
70
|
) => ReactNode
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Which layout scopes a plugin's slot contribution serves. Absent = all of them.
|
|
75
|
+
*
|
|
76
|
+
* Only slots listed in `SlotScopeSpec` take a predicate: those are the slots
|
|
77
|
+
* whose layout the core decides before rendering any contribution.
|
|
78
|
+
*/
|
|
79
|
+
export type PluginAppliesTo = {
|
|
80
|
+
[N in keyof SlotScopeSpec]?: (scope: SlotScopeSpec[N]) => boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
/**
|
|
67
84
|
* One deployment-specific feature's contribution to the core's UI.
|
|
68
85
|
*
|
|
@@ -79,6 +96,7 @@ export interface AcPlugin {
|
|
|
79
96
|
name: string
|
|
80
97
|
slots?: PluginSlots
|
|
81
98
|
wrappers?: PluginWrappers
|
|
99
|
+
appliesTo?: PluginAppliesTo
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
/**
|
|
@@ -93,9 +111,28 @@ export interface AcPlugin {
|
|
|
93
111
|
* Use it ONLY for layout. A slot must never be the only route to something the
|
|
94
112
|
* core itself needs, so branching behaviour on a plugin's presence is a design
|
|
95
113
|
* smell even where branching layout is not.
|
|
114
|
+
*
|
|
115
|
+
* A slot in `SlotScopeSpec` also takes the scope being laid out, and counts only
|
|
116
|
+
* plugins whose `appliesTo` accepts it. An `undefined` scope means there is
|
|
117
|
+
* nothing to decide against, so the slot reads as unfilled.
|
|
96
118
|
*/
|
|
97
|
-
export function useSlotFilled
|
|
98
|
-
|
|
119
|
+
export function useSlotFilled<TSlot extends keyof SlotSpec>(
|
|
120
|
+
slot: TSlot,
|
|
121
|
+
...scope: TSlot extends keyof SlotScopeSpec
|
|
122
|
+
? [scope: SlotScopeSpec[TSlot] | undefined]
|
|
123
|
+
: []
|
|
124
|
+
): boolean {
|
|
125
|
+
const plugins = useExtensions()
|
|
126
|
+
const scoped = scope.length > 0
|
|
127
|
+
const value = scope[0]
|
|
128
|
+
if (scoped && value === undefined) return false
|
|
129
|
+
return plugins.some((plugin) => {
|
|
130
|
+
if (!plugin.slots?.[slot]) return false
|
|
131
|
+
const applies = plugin.appliesTo?.[slot] as
|
|
132
|
+
| ((s: typeof value) => boolean)
|
|
133
|
+
| undefined
|
|
134
|
+
return !scoped || !applies || applies(value)
|
|
135
|
+
})
|
|
99
136
|
}
|
|
100
137
|
|
|
101
138
|
/**
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export declare function FeedbackSection({ appSlug,
|
|
2
|
-
/**
|
|
3
|
-
* Asked to open by the card's band, which is where the affordance now lives —
|
|
4
|
-
* at the foot of this panel it was below the fold of a tab nobody opens unless
|
|
5
|
-
* they already know what is in it.
|
|
6
|
-
*
|
|
7
|
-
* A count of how many times it has asked, so a second ask re-opens the composer
|
|
8
|
-
* the first one closed on send. A boolean could not say "again".
|
|
9
|
-
*/
|
|
10
|
-
openComposer, }: {
|
|
11
|
-
appSlug: string;
|
|
12
|
-
openComposer?: number;
|
|
13
|
-
}): import("react/jsx-runtime").JSX.Element;
|