@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,6 +1,6 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
2
|
import { X } from 'lucide-react'
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef } from 'react'
|
|
4
4
|
import { useAppCatalogContext } from '../../context/AppCatalogContext'
|
|
5
5
|
import { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'
|
|
6
6
|
import { AppDetails } from '../detail/AppDetails'
|
|
@@ -38,6 +38,47 @@ export function AppDetailPanel({
|
|
|
38
38
|
const { approvalMethods } = useAppCatalogContext()
|
|
39
39
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Centre the card ONCE when it opens, then hold that position.
|
|
43
|
+
*
|
|
44
|
+
* `my-auto` centred it on every render, so each panel height change pushed both
|
|
45
|
+
* edges and half the delta became upward movement of everything above it —
|
|
46
|
+
* including the tab strip, which moved 113px and took the control the user had
|
|
47
|
+
* just clicked with it (CLS 0.19, "needs improvement").
|
|
48
|
+
*
|
|
49
|
+
* Top-anchoring fixed the shift but looked wrong: a short card sat high on the
|
|
50
|
+
* page. So pin the centred offset instead. The card still opens centred, and
|
|
51
|
+
* anything that grows it afterwards extends downward only — which is what a
|
|
52
|
+
* reader expects when they open a tab with more in it.
|
|
53
|
+
*
|
|
54
|
+
* Only on open and on resize. Deliberately NOT on content change: reacting to
|
|
55
|
+
* content is precisely the bug.
|
|
56
|
+
*/
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
const card = dialogRef.current
|
|
59
|
+
const scroller = card?.parentElement
|
|
60
|
+
if (!card || !scroller) return
|
|
61
|
+
|
|
62
|
+
const pinCentre = () => {
|
|
63
|
+
card.style.marginTop = '0px'
|
|
64
|
+
const style = getComputedStyle(scroller)
|
|
65
|
+
const usable =
|
|
66
|
+
scroller.clientHeight -
|
|
67
|
+
parseFloat(style.paddingTop) -
|
|
68
|
+
parseFloat(style.paddingBottom)
|
|
69
|
+
const slack = usable - card.offsetHeight
|
|
70
|
+
// Negative slack means the card is already taller than the viewport; leave it
|
|
71
|
+
// at the top and let the overlay scroll, rather than pushing its head off.
|
|
72
|
+
card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pinCentre()
|
|
76
|
+
window.addEventListener('resize', pinCentre)
|
|
77
|
+
return () => window.removeEventListener('resize', pinCentre)
|
|
78
|
+
// Keyed on the opened entry: a different card is a new open, and should
|
|
79
|
+
// re-centre for its own height.
|
|
80
|
+
}, [app.slug, subResource?.slug])
|
|
81
|
+
|
|
41
82
|
// Focus the dialog on mount so Esc hotkeys fire immediately, even when
|
|
42
83
|
// the card was opened by a mouse click (which leaves focus on the grid row).
|
|
43
84
|
//
|
|
@@ -75,7 +116,10 @@ export function AppDetailPanel({
|
|
|
75
116
|
aria-modal="true"
|
|
76
117
|
aria-label={`${subResource?.displayName ?? app.displayName} details`}
|
|
77
118
|
tabIndex={-1}
|
|
78
|
-
|
|
119
|
+
// Centred on open, then held — see the pin effect above. `my-auto` is
|
|
120
|
+
// deliberately absent: it re-centres on EVERY height change, which is the
|
|
121
|
+
// layout-shift bug.
|
|
122
|
+
className="relative w-full max-w-[min(1120px,94vw)] rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none"
|
|
79
123
|
>
|
|
80
124
|
<button
|
|
81
125
|
type="button"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
import { useTRPC } from '~/api/infra/trpc'
|
|
4
|
+
import { Button } from '~/ui/button'
|
|
5
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/ui/dialog'
|
|
6
|
+
import { cn } from '~/lib/utils'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Asking for something that is not in the catalog yet.
|
|
10
|
+
*
|
|
11
|
+
* The search box is where people discover the gap — they type a tool's name, get
|
|
12
|
+
* nothing, and until now had nowhere to go. A zero-result search is the single most
|
|
13
|
+
* informative moment in the catalog: someone has told us exactly what they expected to
|
|
14
|
+
* find. Throwing that away and showing only "No results" wastes it.
|
|
15
|
+
*
|
|
16
|
+
* This is a catalog-level ask rather than feedback on an entry: `feedback.add` with a
|
|
17
|
+
* `subject` and NO `resourceSlug`. The server already distinguishes the two that way,
|
|
18
|
+
* and the tracker lists both.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Same shape the input primitive uses, minus the fixed height. */
|
|
22
|
+
const FIELD_CLASSES =
|
|
23
|
+
'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'
|
|
24
|
+
|
|
25
|
+
const MAX_SUBJECT = 120
|
|
26
|
+
|
|
27
|
+
interface RequestAppDialogProps {
|
|
28
|
+
open: boolean
|
|
29
|
+
onOpenChange: (open: boolean) => void
|
|
30
|
+
/** What they searched for. Pre-fills the name, because they just typed it. */
|
|
31
|
+
initialSubject?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mounts nothing until it is opened, which is what keeps `useTRPC()` out of the
|
|
36
|
+
* closed path. The catalog grid is rendered bare in several tests with no
|
|
37
|
+
* TRPCProvider, and a hook cannot be called conditionally — so the gate has to be a
|
|
38
|
+
* component boundary rather than an `if` inside one.
|
|
39
|
+
*/
|
|
40
|
+
export function RequestAppDialog(props: RequestAppDialogProps) {
|
|
41
|
+
if (!props.open) return null
|
|
42
|
+
return <RequestAppDialogBody {...props} />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function RequestAppDialogBody({
|
|
46
|
+
open,
|
|
47
|
+
onOpenChange,
|
|
48
|
+
initialSubject = '',
|
|
49
|
+
}: RequestAppDialogProps) {
|
|
50
|
+
const trpc = useTRPC()
|
|
51
|
+
const queryClient = useQueryClient()
|
|
52
|
+
const [subject, setSubject] = useState(initialSubject)
|
|
53
|
+
const [body, setBody] = useState('')
|
|
54
|
+
const [sent, setSent] = useState(false)
|
|
55
|
+
|
|
56
|
+
// Re-seed when opened for a different search. Keyed on the dialog opening rather
|
|
57
|
+
// than on every keystroke, so it never overwrites something half-typed.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (open) {
|
|
60
|
+
setSubject(initialSubject)
|
|
61
|
+
setBody('')
|
|
62
|
+
setSent(false)
|
|
63
|
+
}
|
|
64
|
+
}, [open, initialSubject])
|
|
65
|
+
|
|
66
|
+
const add = useMutation(
|
|
67
|
+
trpc.feedback.add.mutationOptions({
|
|
68
|
+
onSuccess: () => {
|
|
69
|
+
setSent(true)
|
|
70
|
+
// The tracker counts this, so its badge has to notice.
|
|
71
|
+
void queryClient.invalidateQueries({
|
|
72
|
+
queryKey: trpc.feedback.mine.queryKey(),
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const trimmed = subject.trim()
|
|
79
|
+
const send = () => {
|
|
80
|
+
if (!trimmed) return
|
|
81
|
+
add.mutate({
|
|
82
|
+
subject: trimmed.slice(0, MAX_SUBJECT),
|
|
83
|
+
...(body.trim() ? { body: body.trim() } : {}),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
89
|
+
<DialogContent className="sm:max-w-[480px]">
|
|
90
|
+
<DialogHeader>
|
|
91
|
+
<DialogTitle>
|
|
92
|
+
{sent ? 'Thanks — that is logged' : 'Ask for an app to be added'}
|
|
93
|
+
</DialogTitle>
|
|
94
|
+
</DialogHeader>
|
|
95
|
+
|
|
96
|
+
{sent ? (
|
|
97
|
+
<div className="space-y-3">
|
|
98
|
+
<p className="text-muted-foreground text-sm">
|
|
99
|
+
A maintainer will look at it. You can see what happened to it
|
|
100
|
+
under your own requests.
|
|
101
|
+
</p>
|
|
102
|
+
<Button type="button" size="sm" onClick={() => onOpenChange(false)}>
|
|
103
|
+
Close
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="space-y-3">
|
|
108
|
+
<div className="space-y-1.5">
|
|
109
|
+
<label htmlFor="request-app-name" className="text-sm font-medium">
|
|
110
|
+
What are you looking for?
|
|
111
|
+
</label>
|
|
112
|
+
<input
|
|
113
|
+
id="request-app-name"
|
|
114
|
+
autoFocus
|
|
115
|
+
className={cn(FIELD_CLASSES)}
|
|
116
|
+
placeholder="Name of the tool, or what it does"
|
|
117
|
+
maxLength={MAX_SUBJECT}
|
|
118
|
+
value={subject}
|
|
119
|
+
onChange={(event) => setSubject(event.target.value)}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="space-y-1.5">
|
|
124
|
+
<label
|
|
125
|
+
htmlFor="request-app-detail"
|
|
126
|
+
className="text-sm font-medium"
|
|
127
|
+
>
|
|
128
|
+
Anything that would help us find it{' '}
|
|
129
|
+
<span className="text-muted-foreground font-normal">
|
|
130
|
+
(optional)
|
|
131
|
+
</span>
|
|
132
|
+
</label>
|
|
133
|
+
<textarea
|
|
134
|
+
id="request-app-detail"
|
|
135
|
+
rows={3}
|
|
136
|
+
className={cn(FIELD_CLASSES)}
|
|
137
|
+
placeholder="A link, the team that owns it, what you needed it for"
|
|
138
|
+
value={body}
|
|
139
|
+
onChange={(event) => setBody(event.target.value)}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
144
|
+
<Button
|
|
145
|
+
type="button"
|
|
146
|
+
size="sm"
|
|
147
|
+
disabled={!trimmed || add.isPending}
|
|
148
|
+
onClick={send}
|
|
149
|
+
>
|
|
150
|
+
{add.isPending ? 'Sending…' : 'Send request'}
|
|
151
|
+
</Button>
|
|
152
|
+
<span className="text-muted-foreground text-xs">
|
|
153
|
+
Sent under a nickname. No login needed.
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{add.error && (
|
|
158
|
+
<p className="text-destructive text-xs">
|
|
159
|
+
{add.error instanceof Error
|
|
160
|
+
? add.error.message
|
|
161
|
+
: 'Something went wrong'}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</DialogContent>
|
|
167
|
+
</Dialog>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
@@ -31,6 +31,9 @@ export function AccessPrerequisiteChain({
|
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
33
|
<div
|
|
34
|
+
// Part of the access story, so it carries the same anchor the instructions
|
|
35
|
+
// do — a reader (and a test) wants the whole chain, not half of it.
|
|
36
|
+
data-access-section=""
|
|
34
37
|
className="mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4"
|
|
35
38
|
role="note"
|
|
36
39
|
aria-label="Access requires prerequisite steps"
|
|
@@ -235,10 +235,15 @@ const UNDOCUMENTED_WITHOUT_CONTACT =
|
|
|
235
235
|
*/
|
|
236
236
|
function AccessBox({ children }: { children: ReactNode }) {
|
|
237
237
|
return (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
// No frame and no heading. Both existed to mark this out as THE thing on a long
|
|
239
|
+
// scrolling card; now it is its own tab, so the tab's label already says what
|
|
240
|
+
// this is and the panel edge already bounds it. A box inside a box, under a
|
|
241
|
+
// heading that repeats the tab you just clicked, is two statements of the same
|
|
242
|
+
// fact and ink the reader has to look past.
|
|
243
|
+
// A stable anchor for tests, because neither of the old handles survives: the
|
|
244
|
+
// heading is gone, and a sub-resource page has no tab panel to read either.
|
|
245
|
+
// Text and structure both move; this does not.
|
|
246
|
+
<div data-access-section="" className="space-y-4">
|
|
242
247
|
{children}
|
|
243
248
|
</div>
|
|
244
249
|
)
|
|
@@ -104,7 +104,10 @@ export function SubResourceDetailPanel({
|
|
|
104
104
|
|
|
105
105
|
{/* Two-step access section */}
|
|
106
106
|
{hasAnyAccess && (
|
|
107
|
-
|
|
107
|
+
// The whole two-step region carries the anchor: the step labels naming
|
|
108
|
+
// the parent live out here, not inside the nested AccessRequestSections,
|
|
109
|
+
// so anchoring only those dropped the parent from the chain.
|
|
110
|
+
<div data-access-section="" className="mt-6 space-y-6">
|
|
108
111
|
<h3 className="text-sm font-medium">How to get access</h3>
|
|
109
112
|
|
|
110
113
|
{hasTwoStepAccess ? (
|