@igstack/app-catalog-frontend-core 6.6.0 → 6.7.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/App.d.ts +29 -3
- package/dist/esm/App.js +4 -2
- package/dist/esm/App.js.map +1 -1
- package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/dismiss/DismissStack.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/extensions/slots.test.d.ts +0 -0
- package/dist/esm/__tests__/publicEntryResolves.test.d.ts +1 -0
- package/dist/esm/appPropsFactory.d.ts +19 -1
- package/dist/esm/appPropsFactory.js +4 -3
- package/dist/esm/appPropsFactory.js.map +1 -1
- package/dist/esm/index.d.ts +15 -2
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal.d.ts +7 -1
- package/dist/esm/internal.js +0 -2
- package/dist/esm/internal.js.map +1 -1
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +17 -0
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +7 -16
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +20 -11
- package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +39 -17
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +0 -14
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js +5 -18
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.d.ts +12 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +3 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +17 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +212 -79
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +0 -14
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.d.ts +1 -2
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +31 -23
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +5 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +5 -3
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.d.ts +12 -0
- package/dist/esm/modules/auth/AuthContext.js +5 -0
- package/dist/esm/modules/auth/AuthContext.js.map +1 -1
- package/dist/esm/modules/auth/index.d.ts +1 -1
- package/dist/esm/modules/dismiss/DismissStack.d.ts +102 -0
- package/dist/esm/modules/dismiss/DismissStack.js +82 -0
- package/dist/esm/modules/dismiss/DismissStack.js.map +1 -0
- package/dist/esm/modules/extensions/ExtensionsContext.d.ts +17 -0
- package/dist/esm/modules/extensions/ExtensionsContext.js +15 -0
- package/dist/esm/modules/extensions/ExtensionsContext.js.map +1 -0
- package/dist/esm/modules/extensions/factories.d.ts +18 -0
- package/dist/esm/modules/extensions/factories.js +12 -0
- package/dist/esm/modules/extensions/factories.js.map +1 -0
- package/dist/esm/modules/extensions/features/resourceDetail.d.ts +92 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js +14 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -0
- package/dist/esm/modules/extensions/index.d.ts +79 -0
- package/dist/esm/modules/extensions/index.js +26 -0
- package/dist/esm/modules/extensions/index.js.map +1 -0
- package/dist/esm/modules/extensions/makeSlot.d.ts +46 -0
- package/dist/esm/modules/extensions/makeSlot.js +62 -0
- package/dist/esm/modules/extensions/makeSlot.js.map +1 -0
- package/dist/esm/modules/extensions/types.d.ts +46 -0
- package/dist/esm/modules/gallery/Gallery.js +6 -14
- package/dist/esm/modules/gallery/Gallery.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +34 -34
- package/dist/esm/routeTree.gen.js +14 -14
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/router.d.ts +23 -0
- package/dist/esm/router.js +5 -0
- package/dist/esm/router.js.map +1 -0
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
- package/dist/esm/routes/_layout/app.$slug_.sub.$subSlug.d.ts +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.d.ts +2 -2
- package/dist/esm/routes/_layout/index.d.ts +1 -1
- package/dist/esm/routes/_layout/login.d.ts +1 -1
- package/dist/esm/routes/_layout/mcp.d.ts +1 -1
- package/dist/esm/routes/_layout/service-desks.d.ts +1 -1
- package/dist/esm/routes/_layout.d.ts +1 -1
- package/dist/esm/routes/_layout.js +13 -2
- package/dist/esm/routes/_layout.js.map +1 -1
- package/dist/esm/routes/auth.callback.d.ts +1 -1
- package/dist/esm/ui/components/error/DefaultErrorComponent.js +9 -2
- package/dist/esm/ui/components/error/DefaultErrorComponent.js.map +1 -1
- package/dist/esm/ui/components/header/Header.js +0 -1
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/ui/dialog.d.ts +1 -1
- package/dist/esm/ui/dialog.js +9 -0
- package/dist/esm/ui/dialog.js.map +1 -1
- package/dist/esm/ui/table.d.ts +25 -1
- package/dist/esm/ui/table.js +20 -3
- package/dist/esm/ui/table.js.map +1 -1
- package/dist/esm/util/createAcRouter.d.ts +1 -1
- package/package.json +19 -11
- package/src/App.tsx +34 -3
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +4 -1
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.focus.test.tsx +1 -1
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +115 -0
- package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.rowMemo.test.tsx +120 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -5
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +9 -9
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +31 -5
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -3
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +48 -6
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +1 -7
- package/src/__tests__/modules/dismiss/DismissStack.test.tsx +330 -0
- package/src/__tests__/modules/extensions/slots.test.tsx +252 -0
- package/src/__tests__/publicEntryResolves.test.ts +63 -0
- package/src/appPropsFactory.ts +23 -3
- package/src/index.tsx +32 -2
- package/src/internal.ts +7 -1
- package/src/modules/appCatalog/context/AppCatalogContext.tsx +21 -0
- package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +52 -12
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +59 -18
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +12 -20
- package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +19 -0
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +396 -111
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +46 -26
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +20 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +17 -3
- package/src/modules/auth/AuthContext.tsx +15 -0
- package/src/modules/auth/index.ts +1 -0
- package/src/modules/dismiss/DismissStack.tsx +224 -0
- package/src/modules/extensions/ExtensionsContext.tsx +36 -0
- package/src/modules/extensions/factories.ts +19 -0
- package/src/modules/extensions/features/resourceDetail.ts +74 -0
- package/src/modules/extensions/index.ts +121 -0
- package/src/modules/extensions/makeSlot.tsx +123 -0
- package/src/modules/extensions/types.ts +51 -0
- package/src/modules/gallery/Gallery.tsx +13 -17
- package/src/routeTree.gen.ts +31 -31
- package/src/router.ts +23 -0
- package/src/routes/_layout.tsx +51 -1
- package/src/ui/components/error/DefaultErrorComponent.tsx +22 -4
- package/src/ui/dialog.tsx +26 -0
- package/src/ui/table.tsx +46 -3
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { appPropsFactory } from './appPropsFactory.js'
|
|
2
|
+
export type { AppPropsFactoryOptions } from './appPropsFactory.js'
|
|
3
|
+
// What a deployment's own router factory is handed. Safe on this entry: it
|
|
4
|
+
// names no route tree, so it carries no `declare module` with it.
|
|
5
|
+
export type { AcRouterContext, AcRouterInitParams } from './types/types'
|
|
2
6
|
export { App } from './App'
|
|
3
7
|
export type { AppProps } from './App'
|
|
4
8
|
export type { AreaIcon, AreasSettings, UiSettings } from './types/uiSettings'
|
|
@@ -6,5 +10,31 @@ export {
|
|
|
6
10
|
DAY_TO_DAY_AREA_KEY,
|
|
7
11
|
DAY_TO_DAY_TAG,
|
|
8
12
|
} from './modules/appCatalog/utils/areaGrouping'
|
|
9
|
-
export { PwaAutoUpdateProvider, usePwaAutoUpdate } from './modules/pwa'
|
|
10
|
-
export type { PwaAutoUpdateOptions, PwaUpdateHandle } from './modules/pwa'
|
|
13
|
+
export { PwaAutoUpdateProvider, usePwaAutoUpdate } from './modules/pwa/index'
|
|
14
|
+
export type { PwaAutoUpdateOptions, PwaUpdateHandle } from './modules/pwa/index'
|
|
15
|
+
|
|
16
|
+
// ── Plugins ───────────────────────────────────────────────────────────────────
|
|
17
|
+
// A deployment registers these via <App extensions={[...]} />. `Resource` is
|
|
18
|
+
// re-exported because the slot payloads name it: without that, a consumer would
|
|
19
|
+
// have to depend on backend-core directly just to type a handler.
|
|
20
|
+
export type {
|
|
21
|
+
AcPlugin,
|
|
22
|
+
PluginSlots,
|
|
23
|
+
PluginWrappers,
|
|
24
|
+
PluginUser,
|
|
25
|
+
SlotSpec,
|
|
26
|
+
WrapperSpec,
|
|
27
|
+
} from './modules/extensions/index'
|
|
28
|
+
export type { Resource } from '@igstack/app-catalog-backend-core'
|
|
29
|
+
// For a deployment that owns its own routes: resolving its own path params
|
|
30
|
+
// against the catalog. Narrow on purpose — see the hook's own comment.
|
|
31
|
+
export { useCatalogResources } from './modules/appCatalog/context/AppCatalogContext'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Escape handling, for a deployment's own overlays.
|
|
35
|
+
*
|
|
36
|
+
* Call `useDismissable(close)` in any overlay and Escape dismisses whatever
|
|
37
|
+
* opened last, across core and deployment layers alike. Nothing needs to know
|
|
38
|
+
* what else is on screen.
|
|
39
|
+
*/
|
|
40
|
+
export { useDismissable, useDismissStack } from './modules/dismiss/DismissStack'
|
package/src/internal.ts
CHANGED
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* No stability guarantees: this moves in lockstep with
|
|
7
7
|
* `@igstack/app-catalog-test-kit`, which is its only intended consumer.
|
|
8
|
+
*
|
|
9
|
+
* Must stay free of `createAcRouter`. The test kit runs inside a DEPLOYMENT's
|
|
10
|
+
* test program, which declares its own `Register` augmentation for its own
|
|
11
|
+
* route tree — and re-exporting `createAcRouter` here would drag the core's
|
|
12
|
+
* competing augmentation in with it, which is a hard `TS2717`. The router
|
|
13
|
+
* factory is injected instead; see `setRouterFactory` in the test kit. The
|
|
14
|
+
* core's own router is published separately, at `/router`.
|
|
8
15
|
*/
|
|
9
16
|
export { App } from './App'
|
|
10
17
|
export type { AppProps } from './App'
|
|
11
18
|
export { AcDb, dbCacheDbKeys } from './userDb/AcDb'
|
|
12
|
-
export { createAcRouter } from './util/createAcRouter'
|
|
13
19
|
export {
|
|
14
20
|
SEARCH_STORAGE_KEY,
|
|
15
21
|
clearSessionState,
|
|
@@ -83,3 +83,24 @@ export function useAppCatalogContext(): AppCatalogContextIface {
|
|
|
83
83
|
}
|
|
84
84
|
return context
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The catalog, for a deployment that owns its own routes.
|
|
89
|
+
*
|
|
90
|
+
* A deployment composing its own route tree has to resolve its own path params
|
|
91
|
+
* against the catalog — `/app/$slug/…/$account` means nothing until `$account`
|
|
92
|
+
* is matched to a resource. It needs the resources to do that, and the loading
|
|
93
|
+
* flag to tell "still fetching" from "no such resource", which is the
|
|
94
|
+
* difference between a spinner and a redirect.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately narrower than `useAppCatalogContext`: persons, groups, tags and
|
|
97
|
+
* approval methods are the core's own rendering concerns, and publishing them
|
|
98
|
+
* would make every one of them part of the supported surface.
|
|
99
|
+
*/
|
|
100
|
+
export function useCatalogResources(): {
|
|
101
|
+
resources: Resource[]
|
|
102
|
+
isLoading: boolean
|
|
103
|
+
} {
|
|
104
|
+
const { resources, isLoadingApps } = useAppCatalogContext()
|
|
105
|
+
return { resources, isLoading: isLoadingApps }
|
|
106
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
2
|
import { ArrowUpRight, Search, X } from 'lucide-react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
use,
|
|
5
|
+
useCallback,
|
|
6
|
+
useDeferredValue,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
4
12
|
import { cn } from '~/lib/utils'
|
|
5
13
|
import { useAppClickHistory } from '../../hooks/useAppClickHistory'
|
|
6
14
|
import { markdownToPlainText } from '../../utils/markdownToPlainText'
|
|
@@ -697,7 +705,29 @@ export function AppCatalogGrid({
|
|
|
697
705
|
// on the areas most people need. Within an area, alpha (from browse).
|
|
698
706
|
const browseGroups = useMemo(() => groupByArea(browse), [browse])
|
|
699
707
|
|
|
700
|
-
|
|
708
|
+
/**
|
|
709
|
+
* The query the RESULTS are computed from; the input stays on `searchValue`.
|
|
710
|
+
*
|
|
711
|
+
* Searching rebuilds the ranked result set from scratch on every keystroke —
|
|
712
|
+
* `searchResources` rebuilds its parent index, scores every resource across
|
|
713
|
+
* 15 tiers and sorts — and the first keystroke additionally unmounts the
|
|
714
|
+
* whole discovery spine while the last one remounts it. Doing that between
|
|
715
|
+
* the keypress and the caret moving is what made typing stutter. Deferring
|
|
716
|
+
* hands React permission to render the typed character first and the list
|
|
717
|
+
* after, abandoning a superseded pass instead of finishing one per
|
|
718
|
+
* character.
|
|
719
|
+
*
|
|
720
|
+
* `isSearching` is derived from the DEFERRED value on purpose. Driving the
|
|
721
|
+
* results/spine swap from the live value instead tears the frame: the spine
|
|
722
|
+
* unmounts immediately while the results are still a render behind, so the
|
|
723
|
+
* page blanks for a frame on the first keystroke. Everything the user sees
|
|
724
|
+
* for a given query — the swap, the rows and the highlight marks — comes off
|
|
725
|
+
* one value, so they can never disagree.
|
|
726
|
+
*/
|
|
727
|
+
const deferredSearch = useDeferredValue(searchValue)
|
|
728
|
+
const isSearching = deferredSearch.trim() !== ''
|
|
729
|
+
// True while the list is a render behind the box, used only to dim it.
|
|
730
|
+
const isSearchPending = searchValue !== deferredSearch
|
|
701
731
|
const searchRef = useRef<HTMLInputElement>(null)
|
|
702
732
|
|
|
703
733
|
// ⌘K / Ctrl+K focuses the hero search box from anywhere on the page.
|
|
@@ -764,16 +794,26 @@ export function AppCatalogGrid({
|
|
|
764
794
|
search box. The discovery spine below is what gets replaced — the
|
|
765
795
|
shell, hero and search box stay put, so typing never shifts layout. */}
|
|
766
796
|
{isSearching && (
|
|
767
|
-
<
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
797
|
+
<div
|
|
798
|
+
className={`transition-opacity ${
|
|
799
|
+
isSearchPending ? 'opacity-60' : ''
|
|
800
|
+
}`}
|
|
801
|
+
aria-busy={isSearchPending || undefined}
|
|
802
|
+
>
|
|
803
|
+
<SearchResultsList
|
|
804
|
+
apps={allResources ?? apps}
|
|
805
|
+
// Deferred, not live: `Highlight` inside marks the matched text, and
|
|
806
|
+
// feeding it a newer query than the rows were built from highlights
|
|
807
|
+
// substrings the rows do not contain.
|
|
808
|
+
searchValue={deferredSearch}
|
|
809
|
+
onAppClick={onAppClick}
|
|
810
|
+
onSubClick={onSubClick}
|
|
811
|
+
onLaunch={onLaunch}
|
|
812
|
+
onClear={() => onSearchChange('')}
|
|
813
|
+
keyboardEnabled={!detailOpen}
|
|
814
|
+
selectedSubSlug={selectedSubSlug}
|
|
815
|
+
/>
|
|
816
|
+
</div>
|
|
777
817
|
)}
|
|
778
818
|
|
|
779
819
|
{/* Discovery spine: only shown when not searching */}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
2
|
import { X } from 'lucide-react'
|
|
3
|
-
import { useEffect, useLayoutEffect, useRef } from 'react'
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
4
4
|
import { useAppCatalogContext } from '../../context/AppCatalogContext'
|
|
5
|
+
import {
|
|
6
|
+
ResourceDetailAccessActions,
|
|
7
|
+
ResourceDetailProvider,
|
|
8
|
+
} from '~/modules/extensions'
|
|
9
|
+
import { useOptionalUser } from '~/modules/auth'
|
|
10
|
+
import { useDismissStack, useDismissable } from '~/modules/dismiss/DismissStack'
|
|
11
|
+
import { getChildResources } from '../../utils/resolveHelpers'
|
|
5
12
|
import { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'
|
|
6
13
|
import { AppDetails } from '../detail/AppDetails'
|
|
7
14
|
|
|
@@ -35,7 +42,20 @@ export function AppDetailPanel({
|
|
|
35
42
|
/** Clears `?sub=` and returns to the parent's detail. */
|
|
36
43
|
onBackToParent?: () => void
|
|
37
44
|
}) {
|
|
38
|
-
const { approvalMethods } = useAppCatalogContext()
|
|
45
|
+
const { approvalMethods, resources: allResources } = useAppCatalogContext()
|
|
46
|
+
const user = useOptionalUser()
|
|
47
|
+
|
|
48
|
+
// The sub-resource branch has no band of its own, so its slot payload is
|
|
49
|
+
// assembled here, where the catalog context already is, and handed down.
|
|
50
|
+
const subResourceActionsCtx = useMemo(
|
|
51
|
+
() => ({
|
|
52
|
+
resource: subResource ?? app,
|
|
53
|
+
parent: subResource ? app : undefined,
|
|
54
|
+
subResources: getChildResources(allResources, app.slug),
|
|
55
|
+
user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,
|
|
56
|
+
}),
|
|
57
|
+
[subResource, app, allResources, user],
|
|
58
|
+
)
|
|
39
59
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
40
60
|
|
|
41
61
|
/**
|
|
@@ -92,10 +112,24 @@ export function AppDetailPanel({
|
|
|
92
112
|
// caret would land on <body> — where the next Tab walks the catalog grid
|
|
93
113
|
// behind the scrim, since the card has no focus trap.
|
|
94
114
|
const shownSlug = subResource?.slug ?? app.slug
|
|
115
|
+
const stack = useDismissStack()
|
|
95
116
|
useEffect(() => {
|
|
96
117
|
const dialog = dialogRef.current
|
|
97
|
-
if (dialog
|
|
98
|
-
|
|
118
|
+
if (!dialog || dialog.contains(document.activeElement)) return
|
|
119
|
+
// Only while the card is on top. A layer opened OVER it — a deployment's
|
|
120
|
+
// dialog on a nested route — owns the caret, and on a cold deep link that
|
|
121
|
+
// layer mounts before the card does, so without this check the card took
|
|
122
|
+
// focus back off the dialog's first field the moment the catalog arrived.
|
|
123
|
+
const top = stack.topElement()
|
|
124
|
+
if (top && top !== dialog) return
|
|
125
|
+
dialog.focus()
|
|
126
|
+
}, [shownSlug, stack])
|
|
127
|
+
|
|
128
|
+
// Escape closes the card. Registered here rather than in AppDetails because
|
|
129
|
+
// this is the layer's element, and because the sub-resource view renders a
|
|
130
|
+
// different body in place of AppDetails — which left that view with no
|
|
131
|
+
// Escape at all while the registration lived in the body.
|
|
132
|
+
useDismissable(dialogRef, onClose)
|
|
99
133
|
|
|
100
134
|
return (
|
|
101
135
|
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-6 md:p-10">
|
|
@@ -147,20 +181,27 @@ export function AppDetailPanel({
|
|
|
147
181
|
key={shownSlug}
|
|
148
182
|
className="max-h-[85vh] overflow-y-auto px-6 py-5 sm:px-8 sm:py-7"
|
|
149
183
|
>
|
|
150
|
-
{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
{/* Inside the key, so anything a plugin provides here is scoped to
|
|
185
|
+
the resource on screen and is discarded when the card navigates.
|
|
186
|
+
Renders its children untouched when no plugin contributes. */}
|
|
187
|
+
<ResourceDetailProvider
|
|
188
|
+
resource={subResource ?? app}
|
|
189
|
+
parent={subResource ? app : undefined}
|
|
190
|
+
>
|
|
191
|
+
{subResource ? (
|
|
192
|
+
<SubResourceDetailPanel
|
|
193
|
+
subResource={subResource}
|
|
194
|
+
parent={app}
|
|
195
|
+
approvalMethods={approvalMethods}
|
|
196
|
+
onBack={onBackToParent ?? onClose}
|
|
197
|
+
actions={
|
|
198
|
+
<ResourceDetailAccessActions {...subResourceActionsCtx} />
|
|
199
|
+
}
|
|
200
|
+
/>
|
|
201
|
+
) : (
|
|
202
|
+
<AppDetails app={app} onAppClick={onAppClick} />
|
|
203
|
+
)}
|
|
204
|
+
</ResourceDetailProvider>
|
|
164
205
|
</div>
|
|
165
206
|
</div>
|
|
166
207
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo, useRef } from 'react'
|
|
2
|
+
import { useDismissable } from '~/modules/dismiss/DismissStack'
|
|
2
3
|
|
|
3
4
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
4
5
|
|
|
@@ -24,8 +25,15 @@ export function ScreenshotGallery({
|
|
|
24
25
|
onOpenChange,
|
|
25
26
|
title,
|
|
26
27
|
}: ScreenshotGalleryProps) {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The dialog is dismissable through the stack, not through Radix.
|
|
30
|
+
*
|
|
31
|
+
* It no longer needs to know whether the gallery inside it is fullscreen:
|
|
32
|
+
* fullscreen registers above this layer while it is on, so Escape reaches it
|
|
33
|
+
* first and this dialog only hears Escape once fullscreen is off.
|
|
34
|
+
*/
|
|
35
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
36
|
+
useDismissable(contentRef, () => onOpenChange(false), { enabled: open })
|
|
29
37
|
|
|
30
38
|
// Transform screenshot IDs to URLs. Capped at 2000px — plenty for on-screen
|
|
31
39
|
// viewing (incl. fullscreen zoom) while avoiding multi-MB original downloads
|
|
@@ -47,33 +55,17 @@ export function ScreenshotGallery({
|
|
|
47
55
|
return (
|
|
48
56
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
49
57
|
<DialogContent
|
|
58
|
+
ref={contentRef}
|
|
50
59
|
aria-describedby={undefined}
|
|
51
60
|
className="h-[85vh] w-full max-w-[calc(100vw-2rem)] sm:max-w-[calc(100vw-3rem)] md:max-w-[calc(100vw-4rem)] p-0 overflow-hidden"
|
|
52
61
|
showCloseButton={true}
|
|
53
|
-
onEscapeKeyDown={(e) => {
|
|
54
|
-
// Stop propagation so the Esc event doesn't reach outer document listeners
|
|
55
|
-
// (e.g. SearchResultsList's raw keydown handler, which would clear search).
|
|
56
|
-
e.stopPropagation()
|
|
57
|
-
// If Gallery is in fullscreen, its capture listener already handled Escape.
|
|
58
|
-
// Prevent Radix from also closing the dialog on the same event.
|
|
59
|
-
if (isFullscreenRef.current) {
|
|
60
|
-
e.preventDefault()
|
|
61
|
-
}
|
|
62
|
-
}}
|
|
63
62
|
>
|
|
64
63
|
<VisuallyHidden>
|
|
65
64
|
<DialogTitle>
|
|
66
65
|
{title || `${app.abbreviation || app.displayName} screenshots`}
|
|
67
66
|
</DialogTitle>
|
|
68
67
|
</VisuallyHidden>
|
|
69
|
-
<Gallery
|
|
70
|
-
images={images}
|
|
71
|
-
initialIndex={initialIndex}
|
|
72
|
-
title={title}
|
|
73
|
-
onFullscreenChange={(fs) => {
|
|
74
|
-
isFullscreenRef.current = fs
|
|
75
|
-
}}
|
|
76
|
-
/>
|
|
68
|
+
<Gallery images={images} initialIndex={initialIndex} title={title} />
|
|
77
69
|
</DialogContent>
|
|
78
70
|
</Dialog>
|
|
79
71
|
)
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AppApprovalMethod,
|
|
3
3
|
Resource,
|
|
4
4
|
} from '@igstack/app-catalog-backend-core'
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
5
6
|
import { ArrowLeft, ExternalLink } from 'lucide-react'
|
|
6
7
|
import { Button } from '~/ui/button'
|
|
7
8
|
import { Badge } from '~/ui/badge'
|
|
@@ -13,6 +14,16 @@ interface SubResourceDetailPanelProps {
|
|
|
13
14
|
parent: Resource
|
|
14
15
|
approvalMethods: AppApprovalMethod[]
|
|
15
16
|
onBack: () => void
|
|
17
|
+
/**
|
|
18
|
+
* Plugin actions for this sub-resource, rendered under the title.
|
|
19
|
+
*
|
|
20
|
+
* Passed in rather than rendered here on purpose. The slot's payload needs the
|
|
21
|
+
* catalog context and the signed-in user, and calling those hooks in this
|
|
22
|
+
* component would break the tests that render it bare — the same failure mode
|
|
23
|
+
* a `useTRPC()` call has in `AppCatalogGrid`. Optional, so those tests keep
|
|
24
|
+
* passing untouched.
|
|
25
|
+
*/
|
|
26
|
+
actions?: ReactNode
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
function StepBadge({ step }: { step: number }) {
|
|
@@ -28,6 +39,7 @@ export function SubResourceDetailPanel({
|
|
|
28
39
|
parent,
|
|
29
40
|
approvalMethods,
|
|
30
41
|
onBack,
|
|
42
|
+
actions,
|
|
31
43
|
}: SubResourceDetailPanelProps) {
|
|
32
44
|
// Mirrors AccessRequestSection's fallback: a sub-resource usually documents
|
|
33
45
|
// access through top-level `approverSlugs`/`accessComments`, not through an
|
|
@@ -102,6 +114,13 @@ export function SubResourceDetailPanel({
|
|
|
102
114
|
)}
|
|
103
115
|
</div>
|
|
104
116
|
|
|
117
|
+
{/* ABOVE the gate below on purpose: an account with no approver, owner
|
|
118
|
+
or access comments renders no access region at all, and that is
|
|
119
|
+
exactly the case where an approver most needs the action. */}
|
|
120
|
+
{actions ? (
|
|
121
|
+
<div className="mt-4 flex items-center gap-2">{actions}</div>
|
|
122
|
+
) : null}
|
|
123
|
+
|
|
105
124
|
{/* Two-step access section */}
|
|
106
125
|
{hasAnyAccess && (
|
|
107
126
|
// The whole two-step region carries the anchor: the step labels naming
|