@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.
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/utils/accessTabLabel.test.d.ts +1 -0
- package/dist/esm/api/infra/trpc.d.ts +42 -21
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +19 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +17 -2
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.d.ts +14 -0
- package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js +115 -0
- package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +11 -4
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js +0 -4
- package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +4 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +377 -385
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDoor.d.ts +20 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js +59 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +64 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +90 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +13 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +433 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +18 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/accessTabLabel.d.ts +30 -0
- package/dist/esm/modules/appCatalog/utils/accessTabLabel.js +34 -0
- package/dist/esm/modules/appCatalog/utils/accessTabLabel.js.map +1 -0
- package/dist/index.css +269 -5
- package/package.json +4 -4
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +17 -3
- package/src/__tests__/modules/appCatalog/ui/catalog/RequestAppDialog.test.tsx +113 -0
- package/src/__tests__/modules/appCatalog/ui/components/AccessRequestSection.test.tsx +6 -4
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +25 -10
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +193 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +2 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +32 -9
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +125 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +2 -2
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +181 -0
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +131 -0
- package/src/__tests__/modules/appCatalog/utils/accessTabLabel.test.ts +206 -0
- package/src/__tests__/ui/components/header/Header.loginHidden.test.tsx +1 -1
- package/src/index.css +269 -5
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +24 -0
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +46 -2
- package/src/modules/appCatalog/ui/catalog/RequestAppDialog.tsx +169 -0
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +3 -0
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +9 -4
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +4 -1
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +574 -554
- package/src/modules/appCatalog/ui/detail/AppDoor.tsx +97 -0
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +195 -0
- package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +605 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +20 -1
- package/src/modules/appCatalog/utils/accessTabLabel.ts +95 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.d.ts +0 -3
- package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js +0 -228
- package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js.map +0 -1
- package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.emptyState.test.tsx +0 -64
- package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.reviewStatus.test.tsx +0 -93
- package/src/modules/appCatalog/ui/detail/CommentsSection.tsx +0 -296
- /package/dist/esm/__tests__/modules/appCatalog/ui/{detail/CommentsSection.emptyState.test.d.ts → catalog/RequestAppDialog.test.d.ts} +0 -0
- /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
|
-
}
|
|
File without changes
|