@igstack/app-catalog-frontend-core 6.3.1-alpha-20261003051755 → 6.5.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 (73) hide show
  1. package/dist/esm/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.d.ts +0 -0
  3. package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.d.ts +0 -0
  4. package/dist/esm/__tests__/modules/appCatalog/utils/accessTabLabel.test.d.ts +1 -0
  5. package/dist/esm/api/infra/trpc.d.ts +42 -21
  6. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -0
  7. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +19 -0
  8. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +17 -2
  10. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.d.ts +14 -0
  12. package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js +115 -0
  13. package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js.map +1 -0
  14. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +1 -0
  15. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +11 -4
  17. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js +0 -4
  19. package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +4 -1
  21. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
  22. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +377 -385
  23. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/detail/AppDoor.d.ts +20 -0
  25. package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js +59 -0
  26. package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js.map +1 -0
  27. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +64 -0
  28. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +90 -0
  29. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -0
  30. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +13 -0
  31. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +433 -0
  32. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +1 -0
  33. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +18 -1
  34. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  35. package/dist/esm/modules/appCatalog/utils/accessTabLabel.d.ts +30 -0
  36. package/dist/esm/modules/appCatalog/utils/accessTabLabel.js +34 -0
  37. package/dist/esm/modules/appCatalog/utils/accessTabLabel.js.map +1 -0
  38. package/dist/index.css +269 -5
  39. package/package.json +4 -4
  40. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +17 -3
  41. package/src/__tests__/modules/appCatalog/ui/catalog/RequestAppDialog.test.tsx +113 -0
  42. package/src/__tests__/modules/appCatalog/ui/components/AccessRequestSection.test.tsx +6 -4
  43. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +25 -10
  44. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +193 -0
  45. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +2 -2
  46. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +32 -9
  47. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +125 -0
  48. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +2 -2
  49. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +181 -0
  50. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +131 -0
  51. package/src/__tests__/modules/appCatalog/utils/accessTabLabel.test.ts +206 -0
  52. package/src/__tests__/ui/components/header/Header.loginHidden.test.tsx +1 -1
  53. package/src/index.css +269 -5
  54. package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +24 -0
  55. package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +46 -2
  56. package/src/modules/appCatalog/ui/catalog/RequestAppDialog.tsx +169 -0
  57. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +3 -0
  58. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +9 -4
  59. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +4 -1
  60. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +574 -554
  61. package/src/modules/appCatalog/ui/detail/AppDoor.tsx +97 -0
  62. package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +195 -0
  63. package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +605 -0
  64. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +20 -1
  65. package/src/modules/appCatalog/utils/accessTabLabel.ts +95 -0
  66. package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.d.ts +0 -3
  67. package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js +0 -228
  68. package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js.map +0 -1
  69. package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.emptyState.test.tsx +0 -64
  70. package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.reviewStatus.test.tsx +0 -93
  71. package/src/modules/appCatalog/ui/detail/CommentsSection.tsx +0 -296
  72. /package/dist/esm/__tests__/modules/appCatalog/ui/{detail/CommentsSection.emptyState.test.d.ts → catalog/RequestAppDialog.test.d.ts} +0 -0
  73. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{CommentsSection.reviewStatus.test.d.ts → AppDetails.doorAndTabs.test.d.ts} +0 -0
@@ -1,296 +0,0 @@
1
- import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
- import { useState } from 'react'
3
- import { useTRPC } from '~/api/infra/trpc'
4
- import { Button } from '~/ui/button'
5
- import { cn } from '~/lib/utils'
6
- import { formatRelativeTime } from '../../utils/formatRelativeTime'
7
-
8
- /**
9
- * Feedback on one app, from anyone browsing the catalog.
10
- *
11
- * Nobody has to log in to leave a comment, so the server attributes each one to a
12
- * pseudonym derived from an httpOnly cookie ("Curious Ferret"). That cookie is also
13
- * the only thing that authorises editing or deleting, and only for the first hour —
14
- * `canEditUntil` arrives from the server as an absolute instant so a skewed client
15
- * clock cannot show controls the server would refuse.
16
- */
17
-
18
- /** Same shape the input primitive uses, minus the fixed height. */
19
- const FIELD_CLASSES =
20
- 'placeholder:text-muted-foreground dark:bg-input/30 border-input w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50'
21
-
22
- function errorMessage(error: unknown): string {
23
- return error instanceof Error ? error.message : 'Something went wrong'
24
- }
25
-
26
- /**
27
- * What a maintainer did with a comment, as the reader sees it.
28
- *
29
- * `applied` is the point of the whole thing: a comment that improved the entry
30
- * gets public credit, so the next person can see that commenting changes the
31
- * catalog. Nobody sets these from the UI — the review tooling writes them.
32
- */
33
- const STATUS_LABELS = {
34
- applied: {
35
- text: '✓ Applied — thank you!',
36
- className: 'comment-chip-applied',
37
- title:
38
- 'This comment improved the entry — the catalog now carries what it said.',
39
- },
40
- acknowledged: {
41
- text: 'Seen',
42
- className: 'comment-chip-seen',
43
- title: 'A maintainer read this comment and left the entry as it was.',
44
- },
45
- } as const
46
-
47
- function StatusChip({ status }: { status: 'applied' | 'acknowledged' }) {
48
- const label = STATUS_LABELS[status]
49
- return (
50
- <span className={cn('comment-chip', label.className)} title={label.title}>
51
- {label.text}
52
- </span>
53
- )
54
- }
55
-
56
- /** The maintainers' answer, attributed to the team rather than a pseudonym. */
57
- function ReviewerReply({ reply }: { reply: string }) {
58
- return (
59
- <div className="border-primary/45 bg-muted/60 mt-1.5 rounded-r-lg border-l-2 px-2.5 py-1.5">
60
- <span className="text-muted-foreground font-medium">Catalog team</span>
61
- <p className="mt-0.5 whitespace-pre-wrap">{reply}</p>
62
- </div>
63
- )
64
- }
65
-
66
- /** Cmd/Ctrl+Enter submits; plain Enter keeps making paragraphs. */
67
- function isSubmitChord(event: React.KeyboardEvent): boolean {
68
- return event.key === 'Enter' && (event.metaKey || event.ctrlKey)
69
- }
70
-
71
- export function CommentsSection({ appSlug }: { appSlug: string }) {
72
- const trpc = useTRPC()
73
- const queryClient = useQueryClient()
74
- const listOptions = trpc.comments.list.queryOptions({ resourceSlug: appSlug })
75
- const { data: comments, isLoading, error } = useQuery(listOptions)
76
-
77
- const [draft, setDraft] = useState('')
78
- const [editingId, setEditingId] = useState<string | null>(null)
79
- const [editDraft, setEditDraft] = useState('')
80
- /**
81
- * Most apps have no comments, and an empty form under "no comments yet" is just
82
- * noise. So the composer stays behind the invitation until someone accepts it —
83
- * which is also what earns the caret: focus follows the click, never the mount.
84
- */
85
- const [composerOpen, setComposerOpen] = useState(false)
86
-
87
- const refresh = () =>
88
- queryClient.invalidateQueries({ queryKey: listOptions.queryKey })
89
-
90
- const add = useMutation(
91
- trpc.comments.add.mutationOptions({
92
- onSuccess: () => {
93
- setDraft('')
94
- void refresh()
95
- },
96
- }),
97
- )
98
- const edit = useMutation(
99
- trpc.comments.edit.mutationOptions({
100
- onSuccess: () => {
101
- setEditingId(null)
102
- void refresh()
103
- },
104
- }),
105
- )
106
- const remove = useMutation(
107
- trpc.comments.remove.mutationOptions({ onSuccess: () => void refresh() }),
108
- )
109
-
110
- const post = () => {
111
- const body = draft.trim()
112
- if (body) add.mutate({ resourceSlug: appSlug, body })
113
- }
114
-
115
- const saveEdit = (id: string) => {
116
- const body = editDraft.trim()
117
- if (body) edit.mutate({ id, body })
118
- }
119
-
120
- // One line for every failed action: three mutations and one query, one place to look.
121
- const failure = error ?? add.error ?? edit.error ?? remove.error
122
- const hasComments = !!comments && comments.length > 0
123
- const hasLabelled = !!comments?.some((comment) => comment.status)
124
-
125
- return (
126
- <div className="mt-6">
127
- <h3 className="mb-2 text-sm font-medium">
128
- Comments
129
- {comments && comments.length > 0 && (
130
- <span className="text-muted-foreground ml-1 font-normal">
131
- ({comments.length})
132
- </span>
133
- )}
134
- </h3>
135
-
136
- {isLoading ? (
137
- <p className="text-muted-foreground text-xs">Loading comments…</p>
138
- ) : comments && comments.length > 0 ? (
139
- <ul className="space-y-3">
140
- {comments.map((comment) => {
141
- const canEdit =
142
- comment.isMine &&
143
- !!comment.canEditUntil &&
144
- Date.parse(comment.canEditUntil) > Date.now()
145
-
146
- return (
147
- <li key={comment.id} className="text-xs">
148
- <div className="text-muted-foreground flex items-center gap-2">
149
- <span className="text-foreground font-medium">
150
- {comment.authorName}
151
- </span>
152
- <span>{formatRelativeTime(comment.createdAt)}</span>
153
- {comment.editedAt && <span>(edited)</span>}
154
- {comment.status && <StatusChip status={comment.status} />}
155
- {canEdit && editingId !== comment.id && (
156
- <>
157
- <button
158
- type="button"
159
- className="hover:text-foreground underline"
160
- onClick={() => {
161
- setEditingId(comment.id)
162
- setEditDraft(comment.body)
163
- }}
164
- >
165
- Edit
166
- </button>
167
- <button
168
- type="button"
169
- className="hover:text-destructive underline"
170
- onClick={() => remove.mutate({ id: comment.id })}
171
- >
172
- Delete
173
- </button>
174
- </>
175
- )}
176
- </div>
177
-
178
- {editingId === comment.id ? (
179
- <div className="mt-1 space-y-2">
180
- <textarea
181
- autoFocus
182
- rows={3}
183
- className={cn(FIELD_CLASSES)}
184
- value={editDraft}
185
- onChange={(event) => setEditDraft(event.target.value)}
186
- onKeyDown={(event) => {
187
- if (isSubmitChord(event)) {
188
- event.preventDefault()
189
- saveEdit(comment.id)
190
- }
191
- }}
192
- />
193
- <div className="flex gap-2">
194
- <Button
195
- type="button"
196
- size="sm"
197
- disabled={!editDraft.trim() || edit.isPending}
198
- onClick={() => saveEdit(comment.id)}
199
- >
200
- Save
201
- </Button>
202
- <Button
203
- type="button"
204
- size="sm"
205
- variant="ghost"
206
- onClick={() => setEditingId(null)}
207
- >
208
- Cancel
209
- </Button>
210
- </div>
211
- </div>
212
- ) : (
213
- <p className="mt-0.5 whitespace-pre-wrap">{comment.body}</p>
214
- )}
215
-
216
- {comment.reviewerReply && (
217
- <ReviewerReply reply={comment.reviewerReply} />
218
- )}
219
- </li>
220
- )
221
- })}
222
- </ul>
223
- ) : null}
224
-
225
- {/* A chip nobody can decode motivates nobody — but the legend is noise on
226
- an entry where no comment has been reviewed yet, so it comes and goes
227
- with the chips. */}
228
- {hasLabelled && (
229
- <p className="text-muted-foreground border-border mt-3 border-t pt-2.5 text-[0.6875rem] leading-relaxed">
230
- <StatusChip status="applied" /> means the comment improved this entry
231
- — the catalog now carries what it said.{' '}
232
- <StatusChip status="acknowledged" /> means a maintainer read it and
233
- left the entry as it was.
234
- </p>
235
- )}
236
-
237
- {!isLoading && !hasComments && (
238
- <p className="text-muted-foreground text-xs">
239
- No comments yet.{' '}
240
- {!composerOpen && (
241
- <button
242
- type="button"
243
- className="hover:text-foreground underline"
244
- onClick={() => setComposerOpen(true)}
245
- >
246
- Be the first.
247
- </button>
248
- )}
249
- </p>
250
- )}
251
-
252
- {(hasComments || composerOpen) && (
253
- <div
254
- className={cn(
255
- 'mt-3 space-y-2',
256
- composerOpen &&
257
- 'animate-in fade-in slide-in-from-top-2 duration-200',
258
- )}
259
- >
260
- <textarea
261
- autoFocus={composerOpen}
262
- rows={3}
263
- className={cn(FIELD_CLASSES)}
264
- placeholder="Leave a comment…"
265
- value={draft}
266
- onChange={(event) => setDraft(event.target.value)}
267
- onKeyDown={(event) => {
268
- if (isSubmitChord(event)) {
269
- event.preventDefault()
270
- post()
271
- }
272
- }}
273
- />
274
- <div className="flex items-center gap-3">
275
- <Button
276
- type="button"
277
- size="sm"
278
- disabled={!draft.trim() || add.isPending}
279
- onClick={post}
280
- >
281
- {add.isPending ? 'Posting…' : 'Post'}
282
- </Button>
283
- <span className="text-muted-foreground text-xs">
284
- Posted under a nickname. You can edit or delete it for one hour.
285
- </span>
286
- </div>
287
- </div>
288
- )}
289
-
290
- {/* Outside the composer: a failed list query leaves nothing to collapse into. */}
291
- {failure && (
292
- <p className="text-destructive mt-2 text-xs">{errorMessage(failure)}</p>
293
- )}
294
- </div>
295
- )
296
- }