@igstack/app-catalog-frontend-core 6.9.0 → 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.
Files changed (78) hide show
  1. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.d.ts +0 -0
  3. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.d.ts +0 -0
  4. package/dist/esm/__tests__/modules/appCatalog/ui/detail/composeComments.test.d.ts +1 -0
  5. package/dist/esm/__tests__/ui/detail/CommunityInput.test.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +8 -0
  7. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +254 -232
  8. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +17 -0
  10. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +187 -0
  11. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -0
  12. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +9 -0
  13. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +91 -0
  14. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -0
  15. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +17 -0
  16. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +328 -0
  17. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -0
  18. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +15 -0
  19. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +26 -0
  20. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -0
  21. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +8 -19
  22. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +49 -37
  23. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +30 -0
  25. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +50 -0
  26. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -0
  27. package/dist/esm/modules/appCatalog/ui/detail/communityError.d.ts +6 -0
  28. package/dist/esm/modules/appCatalog/ui/detail/communityError.js +8 -0
  29. package/dist/esm/modules/appCatalog/ui/detail/communityError.js.map +1 -0
  30. package/dist/esm/modules/appCatalog/ui/detail/composeComments.d.ts +27 -0
  31. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +66 -0
  32. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -0
  33. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.d.ts +11 -0
  34. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js +15 -0
  35. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js.map +1 -0
  36. package/dist/esm/modules/extensions/features/resourceDetail.d.ts +38 -0
  37. package/dist/esm/modules/extensions/features/resourceDetail.js +4 -0
  38. package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -1
  39. package/dist/esm/modules/extensions/index.d.ts +1 -1
  40. package/dist/esm/modules/extensions/index.js.map +1 -1
  41. package/dist/index.css +525 -49
  42. package/package.json +4 -4
  43. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +5 -2
  44. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +5 -2
  45. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +9 -10
  46. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +402 -0
  47. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +8 -4
  48. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +5 -2
  49. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +175 -0
  50. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +5 -2
  51. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +5 -2
  52. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +5 -2
  53. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +281 -0
  54. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +179 -0
  55. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +423 -0
  56. package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +170 -0
  57. package/src/__tests__/ui/detail/CommunityInput.test.tsx +39 -0
  58. package/src/index.css +525 -49
  59. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +310 -295
  60. package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +254 -0
  61. package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +102 -0
  62. package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +419 -0
  63. package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +41 -0
  64. package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +77 -71
  65. package/src/modules/appCatalog/ui/detail/communityDraft.ts +80 -0
  66. package/src/modules/appCatalog/ui/detail/communityError.ts +10 -0
  67. package/src/modules/appCatalog/ui/detail/composeComments.ts +103 -0
  68. package/src/modules/appCatalog/ui/detail/usePendingCount.ts +26 -0
  69. package/src/modules/extensions/features/resourceDetail.ts +28 -0
  70. package/src/modules/extensions/index.ts +2 -0
  71. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +0 -13
  72. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +0 -433
  73. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +0 -1
  74. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +0 -181
  75. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +0 -131
  76. package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +0 -605
  77. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.emptyState.test.d.ts → AppDetails.behindThisCard.test.d.ts} +0 -0
  78. /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
+ }
@@ -49,6 +49,32 @@ export interface ResourceDetailSlots {
49
49
  parent: Resource
50
50
  user: PluginUser | null
51
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
+ }
52
78
  }
53
79
 
54
80
  /**
@@ -87,4 +113,6 @@ export const ResourceDetailAccessActions = mkSlot('resourceDetailAccessActions')
87
113
  export const ResourceSubResourceRowActions = mkSlot(
88
114
  'resourceSubResourceRowActions',
89
115
  )
116
+ export const ResourceDetailFeedbackNote = mkSlot('resourceDetailFeedbackNote')
117
+ export const ResourceDetailCardSource = mkSlot('resourceDetailCardSource')
90
118
  export const ResourceDetailProvider = mkWrapper('resourceDetailProvider')
@@ -21,6 +21,8 @@ export type { AssertDisjoint } from './types'
21
21
  export {
22
22
  ResourceDetailAccessActions,
23
23
  ResourceSubResourceRowActions,
24
+ ResourceDetailFeedbackNote,
25
+ ResourceDetailCardSource,
24
26
  ResourceDetailProvider,
25
27
  } from './features/resourceDetail'
26
28
  export type {
@@ -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;