@igstack/app-catalog-frontend-core 0.10.1 → 0.12.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__/integration/harness/given.d.ts +1 -0
- package/dist/esm/__tests__/integration/skewWarning.integration.test.d.ts +0 -0
- package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +5 -0
- package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +15 -1
- package/dist/esm/__tests__/modules/appCatalog/utils/resolveHelpers.test.d.ts +1 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.d.ts +40 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js +40 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +61 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +9 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +95 -31
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +9 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -3
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js +3 -2
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -0
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +73 -18
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/AttributionFooter.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/launcher/AttributionFooter.js +31 -0
- package/dist/esm/modules/appCatalog/ui/launcher/AttributionFooter.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.d.ts +16 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js +40 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.d.ts +19 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +389 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.d.ts +13 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js +45 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.d.ts +4 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.js +41 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +40 -6
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +10 -0
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +17 -0
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +1 -1
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
- package/dist/esm/routes/_layout/app._slug.js +4 -3
- package/dist/esm/routes/_layout/app._slug.js.map +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
- package/dist/esm/routes/_layout/index.js +2 -1
- package/dist/esm/routes/_layout/index.js.map +1 -1
- package/dist/esm/routes/_layout/service-desks.js +2 -1
- package/dist/esm/routes/_layout/service-desks.js.map +1 -1
- package/dist/esm/types/uiSettings.d.ts +15 -0
- package/dist/esm/ui/components/header/Header.js +1 -1
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/index.css +73 -66
- package/package.json +4 -4
- package/src/__tests__/integration/appCatalog.integration.test.ts +106 -0
- package/src/__tests__/integration/appDeepLink.integration.test.ts +50 -24
- package/src/__tests__/integration/appFreshness.integration.test.ts +3 -3
- package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
- package/src/__tests__/integration/harness/given.tsx +9 -1
- package/src/__tests__/integration/replacementLink.integration.test.ts +6 -2
- package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +21 -16
- package/src/__tests__/integration/setup/testSetup.ts +1 -0
- package/src/__tests__/integration/skewWarning.integration.test.ts +56 -0
- package/src/__tests__/integration/tools/AppDetailTools.ts +13 -0
- package/src/__tests__/integration/tools/CatalogTools.ts +62 -11
- package/src/__tests__/integration/tools/GalleryTools.ts +7 -1
- package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +5 -5
- package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +79 -0
- package/src/index.css +73 -66
- package/src/modules/appCatalog/hooks/useSessionSyncedState.ts +83 -0
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +89 -0
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +31 -2
- package/src/modules/appCatalog/ui/components/OnboardingCard.tsx +1 -1
- package/src/modules/appCatalog/ui/components/PersonBadge.tsx +103 -30
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +22 -1
- package/src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx +11 -5
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +90 -23
- package/src/modules/appCatalog/ui/launcher/AttributionFooter.tsx +43 -0
- package/src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx +52 -0
- package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +547 -0
- package/src/modules/appCatalog/ui/launcher/ResourceIcon.tsx +54 -0
- package/src/modules/appCatalog/ui/launcher/monogram.ts +41 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +75 -13
- package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
- package/src/modules/appCatalog/utils/resolveHelpers.ts +31 -0
- package/src/routes/_layout/app.$slug.tsx +7 -4
- package/src/routes/_layout/catalog.apps.index.tsx +4 -1
- package/src/routes/_layout/index.tsx +5 -1
- package/src/routes/_layout/service-desks.tsx +4 -1
- package/src/types/uiSettings.ts +15 -0
- package/src/ui/components/header/Header.tsx +3 -1
|
File without changes
|
|
@@ -26,6 +26,11 @@ export declare class AppDetailTools {
|
|
|
26
26
|
visible: number;
|
|
27
27
|
names: string[];
|
|
28
28
|
} | null;
|
|
29
|
+
/**
|
|
30
|
+
* Read the "How to get access" section text from the detail panel.
|
|
31
|
+
* Returns null if the section is not present.
|
|
32
|
+
*/
|
|
33
|
+
getAccessText(): string | null;
|
|
29
34
|
screenshots: {
|
|
30
35
|
/**
|
|
31
36
|
* Click the screenshot preview to open the gallery modal.
|
|
@@ -6,6 +6,8 @@ export declare class CatalogTools {
|
|
|
6
6
|
private user;
|
|
7
7
|
/**
|
|
8
8
|
* Click an app row by display name.
|
|
9
|
+
* Works against both the table/grid view and the launcher home (#38), which
|
|
10
|
+
* renders app rows as buttons rather than a <table>.
|
|
9
11
|
* Throws with list of visible apps if name not found.
|
|
10
12
|
*/
|
|
11
13
|
openApp(name: string): Promise<void>;
|
|
@@ -20,6 +22,11 @@ export declare class CatalogTools {
|
|
|
20
22
|
* Skips group header rows (colspan rows).
|
|
21
23
|
*/
|
|
22
24
|
getTableData(): TableRow[];
|
|
25
|
+
/**
|
|
26
|
+
* The secondary "open in new tab" launch link for a row, by app name.
|
|
27
|
+
* Returns null if the row has no launch link.
|
|
28
|
+
*/
|
|
29
|
+
getLaunchLink(name: string): HTMLAnchorElement | null;
|
|
23
30
|
/**
|
|
24
31
|
* Whether the right detail panel is currently visible.
|
|
25
32
|
*/
|
|
@@ -28,12 +35,19 @@ export declare class CatalogTools {
|
|
|
28
35
|
isShowDeprecatedChecked(): boolean;
|
|
29
36
|
/** Whether the "showing deprecated matches" fallback notice is visible. */
|
|
30
37
|
hasDeprecatedFallbackNotice(): boolean;
|
|
31
|
-
/** Whether the "No apps found" empty state is visible. */
|
|
38
|
+
/** Whether the "No apps found" empty state or search-morph "No results for" is visible. */
|
|
32
39
|
isEmptyStateVisible(): boolean;
|
|
33
40
|
/**
|
|
34
41
|
* Whether the onboarding/welcome card is visible.
|
|
35
42
|
*/
|
|
36
43
|
isOnboardingVisible(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Read app rows from the launcher home (#38): buttons titled "View <name>".
|
|
46
|
+
* Name is the first `.font-semibold`/`.font-bold` span; description the
|
|
47
|
+
* `.text-muted-foreground` span. Deduped by name (an app can appear both in
|
|
48
|
+
* "Your apps" and "Browse all").
|
|
49
|
+
*/
|
|
50
|
+
private getLauncherRows;
|
|
37
51
|
/**
|
|
38
52
|
* Get the main catalog table (first table on page, skipping sub-resource tables in detail panel).
|
|
39
53
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for useSessionSyncedState hook
|
|
3
|
+
*/
|
|
4
|
+
export interface UseSessionSyncedStateOptions<T> {
|
|
5
|
+
/** The sessionStorage key */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Default value when the stored value is absent */
|
|
8
|
+
defaultValue: T;
|
|
9
|
+
/** Optional decoder to transform the stored string to a state value */
|
|
10
|
+
decode?: (storedValue: string) => T;
|
|
11
|
+
/** Optional encoder to transform the state value to a string (return undefined to clear) */
|
|
12
|
+
encode?: (stateValue: T) => string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hook for state that persists in `sessionStorage` instead of the URL.
|
|
16
|
+
*
|
|
17
|
+
* Why this exists (see #27): the catalog search value used to be
|
|
18
|
+
* synced to the URL (`?q=`) so it survived the per-route remount of
|
|
19
|
+
* `AppCatalogFiltersProvider` (#10). But `q` leaked into every shared/bookmarked
|
|
20
|
+
* app link. Backing the value with `sessionStorage` keeps the #10 guarantee —
|
|
21
|
+
* the value survives the remount and the auto-navigate to a single match — while
|
|
22
|
+
* keeping the URL clean.
|
|
23
|
+
*
|
|
24
|
+
* Key features vs {@link useUrlSyncedState}:
|
|
25
|
+
* - Initializes synchronously from `sessionStorage` on mount.
|
|
26
|
+
* - Writes synchronously in the setter (no async effect), which also removes the
|
|
27
|
+
* effect-ordering race #10 fought (a child auto-navigate effect could run
|
|
28
|
+
* before the provider's async state->URL sync).
|
|
29
|
+
* - Never navigates, so it never touches the URL.
|
|
30
|
+
*
|
|
31
|
+
* Defensive against environments without a functional `sessionStorage`
|
|
32
|
+
* (some CI jsdom setups) — falls back to plain in-memory state.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useSessionSyncedState<T>({ key, defaultValue, decode, encode, }: UseSessionSyncedStateOptions<T>): [T, (value: T) => void];
|
|
35
|
+
/**
|
|
36
|
+
* sessionStorage key backing the catalog search value (#27). Exported so tests
|
|
37
|
+
* (and any tooling that needs to seed a "returning user" search) reference the
|
|
38
|
+
* same key as the provider.
|
|
39
|
+
*/
|
|
40
|
+
export declare const SEARCH_STORAGE_KEY = "app-catalog:search";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
function useSessionSyncedState({
|
|
3
|
+
key,
|
|
4
|
+
defaultValue,
|
|
5
|
+
decode,
|
|
6
|
+
encode
|
|
7
|
+
}) {
|
|
8
|
+
const [state, setStateInternal] = useState(() => {
|
|
9
|
+
try {
|
|
10
|
+
const stored = globalThis.sessionStorage.getItem(key);
|
|
11
|
+
if (stored !== null) {
|
|
12
|
+
return decode ? decode(stored) : stored;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
return defaultValue;
|
|
17
|
+
});
|
|
18
|
+
const setState = useCallback(
|
|
19
|
+
(value) => {
|
|
20
|
+
setStateInternal(value);
|
|
21
|
+
try {
|
|
22
|
+
const encoded = encode ? encode(value) : value;
|
|
23
|
+
if (encoded === void 0) {
|
|
24
|
+
globalThis.sessionStorage.removeItem(key);
|
|
25
|
+
} else {
|
|
26
|
+
globalThis.sessionStorage.setItem(key, encoded);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
[key, encode]
|
|
32
|
+
);
|
|
33
|
+
return [state, setState];
|
|
34
|
+
}
|
|
35
|
+
const SEARCH_STORAGE_KEY = "app-catalog:search";
|
|
36
|
+
export {
|
|
37
|
+
SEARCH_STORAGE_KEY,
|
|
38
|
+
useSessionSyncedState
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=useSessionSyncedState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionSyncedState.js","sources":["../../../../../src/modules/appCatalog/hooks/useSessionSyncedState.ts"],"sourcesContent":["import { useCallback, useState } from 'react'\n\n/**\n * Options for useSessionSyncedState hook\n */\nexport interface UseSessionSyncedStateOptions<T> {\n /** The sessionStorage key */\n key: string\n /** Default value when the stored value is absent */\n defaultValue: T\n /** Optional decoder to transform the stored string to a state value */\n decode?: (storedValue: string) => T\n /** Optional encoder to transform the state value to a string (return undefined to clear) */\n encode?: (stateValue: T) => string | undefined\n}\n\n/**\n * Hook for state that persists in `sessionStorage` instead of the URL.\n *\n * Why this exists (see #27): the catalog search value used to be\n * synced to the URL (`?q=`) so it survived the per-route remount of\n * `AppCatalogFiltersProvider` (#10). But `q` leaked into every shared/bookmarked\n * app link. Backing the value with `sessionStorage` keeps the #10 guarantee —\n * the value survives the remount and the auto-navigate to a single match — while\n * keeping the URL clean.\n *\n * Key features vs {@link useUrlSyncedState}:\n * - Initializes synchronously from `sessionStorage` on mount.\n * - Writes synchronously in the setter (no async effect), which also removes the\n * effect-ordering race #10 fought (a child auto-navigate effect could run\n * before the provider's async state->URL sync).\n * - Never navigates, so it never touches the URL.\n *\n * Defensive against environments without a functional `sessionStorage`\n * (some CI jsdom setups) — falls back to plain in-memory state.\n */\nexport function useSessionSyncedState<T>({\n key,\n defaultValue,\n decode,\n encode,\n}: UseSessionSyncedStateOptions<T>): [T, (value: T) => void] {\n const [state, setStateInternal] = useState<T>(() => {\n try {\n // The try/catch is the real guard against a missing/throwing\n // sessionStorage (SSR, locked-down jsdom); the access itself is typed\n // non-null, so no optional chaining is needed here.\n const stored = globalThis.sessionStorage.getItem(key)\n if (stored !== null) {\n return decode ? decode(stored) : (stored as T)\n }\n } catch {\n // sessionStorage unavailable — fall through to the default\n }\n return defaultValue\n })\n\n const setState = useCallback(\n (value: T) => {\n setStateInternal(value)\n try {\n const encoded = encode ? encode(value) : (value as string | undefined)\n if (encoded === undefined) {\n globalThis.sessionStorage.removeItem(key)\n } else {\n globalThis.sessionStorage.setItem(key, encoded)\n }\n } catch {\n // sessionStorage unavailable — state still works in-memory\n }\n },\n [key, encode],\n )\n\n return [state, setState]\n}\n\n/**\n * sessionStorage key backing the catalog search value (#27). Exported so tests\n * (and any tooling that needs to seed a \"returning user\" search) reference the\n * same key as the provider.\n */\nexport const SEARCH_STORAGE_KEY = 'app-catalog:search'\n"],"names":[],"mappings":";AAoCO,SAAS,sBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6D;AAC3D,QAAM,CAAC,OAAO,gBAAgB,IAAI,SAAY,MAAM;AAClD,QAAI;AAIF,YAAM,SAAS,WAAW,eAAe,QAAQ,GAAG;AACpD,UAAI,WAAW,MAAM;AACnB,eAAO,SAAS,OAAO,MAAM,IAAK;AAAA,MACpC;AAAA,IACF,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,WAAW;AAAA,IACf,CAAC,UAAa;AACZ,uBAAiB,KAAK;AACtB,UAAI;AACF,cAAM,UAAU,SAAS,OAAO,KAAK,IAAK;AAC1C,YAAI,YAAY,QAAW;AACzB,qBAAW,eAAe,WAAW,GAAG;AAAA,QAC1C,OAAO;AACL,qBAAW,eAAe,QAAQ,KAAK,OAAO;AAAA,QAChD;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,KAAK,MAAM;AAAA,EAAA;AAGd,SAAO,CAAC,OAAO,QAAQ;AACzB;AAOO,MAAM,qBAAqB;"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
|
+
/**
|
|
3
|
+
* Two-step (prerequisite) access banner (#38 item D). When the viewed resource
|
|
4
|
+
* is a child whose ancestor(s) carry an access policy, the domain model says
|
|
5
|
+
* the user must obtain the parent's access FIRST. That chain is invisible
|
|
6
|
+
* otherwise, so surface it explicitly as ordered steps:
|
|
7
|
+
*
|
|
8
|
+
* Step 1 — get access to <parent> (e.g. AWS Console via IT Helpdesk)
|
|
9
|
+
* Step 2 — get access to THIS resource (contact its owner / access contacts)
|
|
10
|
+
*
|
|
11
|
+
* Renders nothing for root resources or children with no access-bearing parent.
|
|
12
|
+
* `onOpenParent` lets the user jump to the parent's full access instructions.
|
|
13
|
+
*/
|
|
14
|
+
export declare function AccessPrerequisiteChain({ resource, onOpenParent, }: {
|
|
15
|
+
resource: Resource;
|
|
16
|
+
onOpenParent?: (parent: Resource) => void;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { KeyRound, ArrowRight } from "lucide-react";
|
|
3
|
+
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
4
|
+
import { getAccessPrerequisiteChain } from "../../utils/resolveHelpers.js";
|
|
5
|
+
function AccessPrerequisiteChain({
|
|
6
|
+
resource,
|
|
7
|
+
onOpenParent
|
|
8
|
+
}) {
|
|
9
|
+
const { resources } = useAppCatalogContext();
|
|
10
|
+
const chain = getAccessPrerequisiteChain(resources, resource);
|
|
11
|
+
if (chain.length === 0) return null;
|
|
12
|
+
const steps = [...chain, resource];
|
|
13
|
+
return /* @__PURE__ */ jsxs(
|
|
14
|
+
"div",
|
|
15
|
+
{
|
|
16
|
+
className: "mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4",
|
|
17
|
+
role: "note",
|
|
18
|
+
"aria-label": "Access requires prerequisite steps",
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ jsxs("h3", { className: "mb-1 flex items-center gap-1.5 text-sm font-semibold text-foreground", children: [
|
|
21
|
+
/* @__PURE__ */ jsx(KeyRound, { className: "size-4 text-primary" }),
|
|
22
|
+
"Two-step access"
|
|
23
|
+
] }),
|
|
24
|
+
/* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-muted-foreground", children: "This resource is nested — you need access to its parent first, then to the resource itself." }),
|
|
25
|
+
/* @__PURE__ */ jsx("ol", { className: "space-y-2", children: steps.map((r, i) => {
|
|
26
|
+
const isLast = i === steps.length - 1;
|
|
27
|
+
return /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2.5 text-sm", children: [
|
|
28
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground", children: i + 1 }),
|
|
29
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Get access to " }),
|
|
31
|
+
isLast ? /* @__PURE__ */ jsx("span", { className: "font-semibold", children: "this resource" }) : onOpenParent ? /* @__PURE__ */ jsxs(
|
|
32
|
+
"button",
|
|
33
|
+
{
|
|
34
|
+
type: "button",
|
|
35
|
+
onClick: () => onOpenParent(r),
|
|
36
|
+
className: "inline-flex items-center gap-1 font-semibold text-primary hover:underline",
|
|
37
|
+
children: [
|
|
38
|
+
r.displayName,
|
|
39
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "size-3" })
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
) : /* @__PURE__ */ jsx("span", { className: "font-semibold", children: r.displayName }),
|
|
43
|
+
!isLast && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
44
|
+
" ",
|
|
45
|
+
"(see its access instructions)"
|
|
46
|
+
] }),
|
|
47
|
+
isLast && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
48
|
+
" ",
|
|
49
|
+
"— details below."
|
|
50
|
+
] })
|
|
51
|
+
] })
|
|
52
|
+
] }, r.slug);
|
|
53
|
+
}) })
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
AccessPrerequisiteChain
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=AccessPrerequisiteChain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccessPrerequisiteChain.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowRight, KeyRound } from 'lucide-react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { getAccessPrerequisiteChain } from '../../utils/resolveHelpers'\n\n/**\n * Two-step (prerequisite) access banner (#38 item D). When the viewed resource\n * is a child whose ancestor(s) carry an access policy, the domain model says\n * the user must obtain the parent's access FIRST. That chain is invisible\n * otherwise, so surface it explicitly as ordered steps:\n *\n * Step 1 — get access to <parent> (e.g. AWS Console via IT Helpdesk)\n * Step 2 — get access to THIS resource (contact its owner / access contacts)\n *\n * Renders nothing for root resources or children with no access-bearing parent.\n * `onOpenParent` lets the user jump to the parent's full access instructions.\n */\nexport function AccessPrerequisiteChain({\n resource,\n onOpenParent,\n}: {\n resource: Resource\n onOpenParent?: (parent: Resource) => void\n}) {\n const { resources } = useAppCatalogContext()\n const chain = getAccessPrerequisiteChain(resources, resource)\n if (chain.length === 0) return null\n\n // Steps = each prerequisite ancestor (root→nearest), then this resource last.\n const steps = [...chain, resource]\n\n return (\n <div\n className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4\"\n role=\"note\"\n aria-label=\"Access requires prerequisite steps\"\n >\n <h3 className=\"mb-1 flex items-center gap-1.5 text-sm font-semibold text-foreground\">\n <KeyRound className=\"size-4 text-primary\" />\n Two-step access\n </h3>\n <p className=\"mb-3 text-xs text-muted-foreground\">\n This resource is nested — you need access to its parent first, then to\n the resource itself.\n </p>\n <ol className=\"space-y-2\">\n {steps.map((r, i) => {\n const isLast = i === steps.length - 1\n return (\n <li key={r.slug} className=\"flex items-start gap-2.5 text-sm\">\n <span className=\"mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground\">\n {i + 1}\n </span>\n <span className=\"min-w-0\">\n <span className=\"text-muted-foreground\">Get access to </span>\n {isLast ? (\n <span className=\"font-semibold\">this resource</span>\n ) : onOpenParent ? (\n <button\n type=\"button\"\n onClick={() => onOpenParent(r)}\n className=\"inline-flex items-center gap-1 font-semibold text-primary hover:underline\"\n >\n {r.displayName}\n <ArrowRight className=\"size-3\" />\n </button>\n ) : (\n <span className=\"font-semibold\">{r.displayName}</span>\n )}\n {!isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n (see its access instructions)\n </span>\n )}\n {isLast && (\n <span className=\"text-muted-foreground\">\n {' '}\n — details below.\n </span>\n )}\n </span>\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;AAiBO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,UAAA,IAAc,qBAAA;AACtB,QAAM,QAAQ,2BAA2B,WAAW,QAAQ;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAW;AAAA,MAEX,UAAA;AAAA,QAAA,qBAAC,MAAA,EAAG,WAAU,wEACZ,UAAA;AAAA,UAAA,oBAAC,UAAA,EAAS,WAAU,sBAAA,CAAsB;AAAA,UAAE;AAAA,QAAA,GAE9C;AAAA,QACA,oBAAC,KAAA,EAAE,WAAU,sCAAqC,UAAA,+FAGlD;AAAA,QACA,oBAAC,QAAG,WAAU,aACX,gBAAM,IAAI,CAAC,GAAG,MAAM;AACnB,gBAAM,SAAS,MAAM,MAAM,SAAS;AACpC,iBACE,qBAAC,MAAA,EAAgB,WAAU,oCACzB,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,wHACb,UAAA,IAAI,GACP;AAAA,YACA,qBAAC,QAAA,EAAK,WAAU,WACd,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,kBAAc;AAAA,cACrD,SACC,oBAAC,QAAA,EAAK,WAAU,iBAAgB,UAAA,gBAAA,CAAa,IAC3C,eACF;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,aAAa,CAAC;AAAA,kBAC7B,WAAU;AAAA,kBAET,UAAA;AAAA,oBAAA,EAAE;AAAA,oBACH,oBAAC,YAAA,EAAW,WAAU,SAAA,CAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA,IAGjC,oBAAC,QAAA,EAAK,WAAU,iBAAiB,YAAE,aAAY;AAAA,cAEhD,CAAC,UACA,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,GAEP;AAAA,cAED,UACC,qBAAC,QAAA,EAAK,WAAU,yBACb,UAAA;AAAA,gBAAA;AAAA,gBAAI;AAAA,cAAA,EAAA,CAEP;AAAA,YAAA,EAAA,CAEJ;AAAA,UAAA,EAAA,GAhCO,EAAE,IAiCX;AAAA,QAEJ,CAAC,EAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -90,8 +90,15 @@ function AccessRequestSection({
|
|
|
90
90
|
}
|
|
91
91
|
}, [accessRequest == null ? void 0 : accessRequest.requestPrompt, copyToClipboard]);
|
|
92
92
|
if (!accessRequest) return null;
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const isOpen = (approvalMethod == null ? void 0 : approvalMethod.type) === "noAccessRequired";
|
|
94
|
+
const hasWrittenSteps = Boolean(
|
|
95
|
+
accessRequest.requestPrompt || accessRequest.comments
|
|
96
|
+
);
|
|
97
|
+
const isUndocumented = (approvalMethod == null ? void 0 : approvalMethod.type) === "unknown" || (approvalMethod == null ? void 0 : approvalMethod.type) === "custom" && !hasWrittenSteps;
|
|
98
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "How to get access" }),
|
|
100
|
+
isOpen && /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-primary", children: "✓ Open to everyone — no request needed. Just open it." }),
|
|
101
|
+
isUndocumented && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Access process not documented yet — contact the owner below to find out." }),
|
|
95
102
|
approvalMethod && approvalMethod.type !== "custom" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: approvalMethod.type === "service" && approvalMethod.config.url ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
96
103
|
/* @__PURE__ */ jsxs(
|
|
97
104
|
"a",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n return (\n <div className=\"mt-6 space-y-4\">\n <h3 className=\"text-sm font-medium\">Access Request</h3>\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;AA0BA,MAAM,yBAAyB;AAQ/B,SAAS,sBACP,MACA;AACA,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO,oBAAC,KAAA,EAAI,WAAU,sBAAA,CAAsB;AAAA,IAC9C,KAAK;AACH,aAAO,oBAAC,OAAA,EAAM,WAAU,sBAAA,CAAsB;AAAA,IAChD,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,oBAAC,UAAA,EAAS,WAAU,sBAAA,CAAsB;AAAA,EAAA;AAEvD;AAMA,SAAS,qBAAqB;AAC5B,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,aAAa,OAA8B,IAAI;AAGrD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,WAAW,SAAS;AACtB,qBAAa,WAAW,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,kBAAkB,YAAY,OAAO,MAAc,OAAe;AAEtE,QAAI,WAAW,SAAS;AACtB,mBAAa,WAAW,OAAO;AAAA,IACjC;AAEA,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,IAAI;AACxC,kBAAY,EAAE;AACd,eAAS,IAAI;AAGb,iBAAW,UAAU,WAAW,MAAM;AACpC,oBAAY,IAAI;AAAA,MAClB,GAAG,sBAAsB;AAAA,IAC3B,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,eAAS,gBAAgB;AAGzB,iBAAW,UAAU,WAAW,MAAM;AACpC,iBAAS,IAAI;AAAA,MACf,GAAG,sBAAsB;AAAA,IAC3B;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO,EAAE,UAAU,OAAO,gBAAA;AAC5B;AAYA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAoB;AAClB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,cAAY;AAAA,MACZ,OAAO,WAAW,YAAY;AAAA,MAE7B,qBACC,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAM,WAAU,6BAAA,CAA6B;AAAA,QAC9C,oBAAC,UAAK,WAAU,WAAU,MAAK,UAAS,aAAU,UAAS,UAAA,sBAAA,CAE3D;AAAA,MAAA,EAAA,CACF,IAEA,oBAAC,MAAA,EAAK,WAAU,cAAA,CAAc;AAAA,IAAA;AAAA,EAAA;AAItC;AAEO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,EAAE,UAAU,gBAAA,IAAoB,mBAAA;AACtC,QAAM,gBAAgB,IAAI;AAC1B,QAAM,iBAAiB,gBAAgB;AAAA,IACrC,CAAC,MAAM,EAAE,UAAS,+CAAe;AAAA,EAAA;AAGnC,QAAM,mBAAmB,YAAY,MAAM;AACzC,QAAI,+CAAe,eAAe;AAChC,sBAAgB,cAAc,eAAe,QAAQ;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,+CAAe,eAAe,eAAe,CAAC;AAGlD,MAAI,CAAC,cAAe,QAAO;AAE3B,SACE,qBAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,IAAA,oBAAC,MAAA,EAAG,WAAU,uBAAsB,UAAA,kBAAc;AAAA,IAGjD,kBAAkB,eAAe,SAAS,gCACxC,OAAA,EAAI,WAAU,2BACZ,UAAA,eAAe,SAAS,aAAa,eAAe,OAAO,MAC1D,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,eAAe,OAAO;AAAA,UAC5B,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UACV,OAAO,eAAe,OAAO;AAAA,UAE7B,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAK,UAAA,sBAAsB,eAAe,IAAI,GAAE;AAAA,YACjD,oBAAC,OAAA,EAAI,WAAU,eAAe,yBAAe,YAAA,CAAY;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAE3D;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,eAAe,OAAO;AAAA,UAC5B,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UACV,OAAO,eAAe,OAAO;AAAA,UAE7B,UAAA,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,QAAA;AAAA,MAAA;AAAA,IACnC,EAAA,CACF,IAEA,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAK,UAAA,sBAAsB,eAAe,IAAI,GAAE;AAAA,MACjD,oBAAC,OAAA,EAAI,WAAU,eAAe,yBAAe,YAAA,CAAY;AAAA,IAAA,EAAA,CAC3D,EAAA,CAEJ;AAAA,IAID,cAAc,iBACb,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,mBAAe;AAAA,MAC5D,oBAAC,QAAA,EAAK,WAAU,gDACd,UAAA,oBAAC,eAAA,EAAc,YAAY,EAAE,GAAG,gBAC7B,UAAA,cAAc,eACjB,GACF;AAAA,MACA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,QAAQ;AAAA,UACR,UAAU,aAAa;AAAA,UACvB,WAAU;AAAA,UACV,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IACZ,GACF;AAAA,IAID,cAAc,YACb,oBAAC,OAAA,EAAI,WAAU,2DACb,UAAA,oBAAC,eAAA,EAAc,YAAY,EAAE,GAAG,aAAA,GAC7B,UAAA,cAAc,UACjB,GACF;AAAA,IAID,cAAc,SAAS,cAAc,MAAM,SAAS,0BAClD,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,4BAA2B,UAAA,mBAAe;AAAA,MACxD,oBAAC,OAAA,EAAI,WAAU,qBACb,+BAAC,OAAA,EACC,UAAA;AAAA,QAAA,oBAAC,aAAA,EACC,+BAAC,UAAA,EACC,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,WAAU,qBAAoB,UAAA,QAAI;AAAA,UAC7C,oBAAC,aAAU,UAAA,cAAA,CAAW;AAAA,QAAA,EAAA,CACxB,EAAA,CACF;AAAA,QACA,oBAAC,aACE,UAAA,cAAc,MAAM,IAAI,CAAC,MAAM,QAC9B,qBAAC,UAAA,EACC,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,WAAU,iCAClB,UAAA,KAAK,aACR;AAAA,UACA,qBAAC,WAAA,EAAU,WAAU,iCAClB,UAAA;AAAA,YAAA,KAAK,eAAe;AAAA,YACpB,KAAK,cACJ,qBAAC,OAAA,EAAI,WAAU,gDAA+C,UAAA;AAAA,cAAA;AAAA,cACrD,KAAK;AAAA,YAAA,EAAA,CACd;AAAA,UAAA,EAAA,CAEJ;AAAA,QAAA,EAAA,GAXa,GAAG,KAAK,WAAW,IAAI,GAAG,EAYzC,CACD,EAAA,CACH;AAAA,MAAA,EAAA,CACF,EAAA,CACF;AAAA,IAAA,GACF;AAAA,IAID,cAAc,uBACb,cAAc,oBAAoB,SAAS,0BACxC,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,4BAA2B,UAAA,aAAS;AAAA,MAClD,oBAAC,OAAA,EAAI,WAAU,wBACZ,wBAAc,oBAAoB,IAAI,CAAC,SACtC,oBAAC,aAAA,EAAuB,KAAA,GAAN,IAAkB,CACrC,EAAA,CACH;AAAA,IAAA,GACF;AAAA,IAIH,cAAc,QAAQ,cAAc,KAAK,SAAS,0BAChD,OAAA,EACC,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,4BAA2B,UAAA,iBAAa;AAAA,MACtD,oBAAC,SAAI,WAAU,yBACZ,wBAAc,KAAK,IAAI,CAAC,QAAQ,QAC/B;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,MAAM,OAAO;AAAA,UACb,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UAET,UAAA;AAAA,YAAA,OAAO,SAAS,OAAO;AAAA,YACxB,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,QAP5B,GAAG,OAAO,GAAG,IAAI,GAAG;AAAA,MAAA,CAS5B,EAAA,CACH;AAAA,IAAA,GACF;AAAA,IAID,cAAc,4BACb,oBAAC,aAAU,MAAK,UAAS,aAAW,MAClC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,OAAM;AAAA,QACN,WAAU;AAAA,QAEV,UAAA;AAAA,UAAA,oBAAC,kBAAA,EAAiB,WAAU,mCAAkC,UAAA,8BAE9D;AAAA,8BACC,kBAAA,EAAiB,WAAU,QAC1B,UAAA,oBAAC,OAAA,EAAI,WAAU,2DACb,UAAA,oBAAC,eAAA,EAAc,YAAY,EAAE,GAAG,aAAA,GAC7B,UAAA,cAAc,0BACjB,GACF,EAAA,CACF;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,EACF,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"AccessRequestSection.js","sources":["../../../../../../src/modules/appCatalog/ui/components/AccessRequestSection.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n Resource,\n} from '@igstack/app-catalog-backend-core'\nimport { Bot, Check, Copy, ExternalLink, Settings, Users } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { Button } from '~/ui/button'\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from '~/ui/accordion'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { MarkdownLink } from './MarkdownText'\nimport { PersonBadge } from './PersonBadge'\n\n// Constants\nconst COPY_FEEDBACK_DURATION = 2000\n\ninterface AccessRequestSectionProps {\n app: Resource\n approvalMethods: AppApprovalMethod[]\n}\n\n// Helper function for approval method icons\nfunction getApprovalMethodIcon(\n type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',\n) {\n switch (type) {\n case 'service':\n return <Bot className=\"size-5 text-primary\" />\n case 'personTeam':\n return <Users className=\"size-5 text-primary\" />\n case 'custom':\n case 'noAccessRequired':\n case 'unknown':\n return <Settings className=\"size-5 text-primary\" />\n }\n}\n\n/**\n * Custom hook for handling copy-to-clipboard functionality with feedback\n * Includes proper cleanup to prevent memory leaks\n */\nfunction useCopyToClipboard() {\n const [copiedId, setCopiedId] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n }\n }, [])\n\n const copyToClipboard = useCallback(async (text: string, id: string) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n }\n\n try {\n await navigator.clipboard.writeText(text)\n setCopiedId(id)\n setError(null)\n\n // Set new timeout with cleanup\n timeoutRef.current = setTimeout(() => {\n setCopiedId(null)\n }, COPY_FEEDBACK_DURATION)\n } catch (err) {\n console.error('Failed to copy to clipboard:', err)\n setError('Failed to copy')\n\n // Clear error after duration\n timeoutRef.current = setTimeout(() => {\n setError(null)\n }, COPY_FEEDBACK_DURATION)\n }\n }, [])\n\n return { copiedId, error, copyToClipboard }\n}\n\n/**\n * Reusable copy button component with accessibility\n */\ninterface CopyButtonProps {\n onCopy: () => void\n isCopied: boolean\n ariaLabel: string\n className?: string\n}\n\nfunction CopyButton({\n onCopy,\n isCopied,\n ariaLabel,\n className,\n}: CopyButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onCopy}\n className={className}\n aria-label={ariaLabel}\n title={isCopied ? 'Copied!' : 'Copy to clipboard'}\n >\n {isCopied ? (\n <>\n <Check className=\"h-3.5 w-3.5 text-green-600\" />\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n Copied to clipboard\n </span>\n </>\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )\n}\n\nexport function AccessRequestSection({\n app,\n approvalMethods,\n}: AccessRequestSectionProps) {\n const { copiedId, copyToClipboard } = useCopyToClipboard()\n const accessRequest = app.accessRequest\n const approvalMethod = approvalMethods.find(\n (m) => m.slug === accessRequest?.approvalMethodSlug,\n )\n\n const handleCopyPrompt = useCallback(() => {\n if (accessRequest?.requestPrompt) {\n copyToClipboard(accessRequest.requestPrompt, 'prompt')\n }\n }, [accessRequest?.requestPrompt, copyToClipboard])\n\n // Early return if no access request\n if (!accessRequest) return null\n\n // Access UX (#31): some methods carry no clickable target — `noAccessRequired`\n // (open), `unknown` (undocumented), and bare `custom` with no comments. The\n // old UI rendered nothing for `custom`, leaving a blank/dead section. Instead\n // surface an explicit, actionable line so the user is never stranded:\n // - open → \"No request needed — just open it.\"\n // - unknown / undocumented custom → route them to the owner (shown below).\n const isOpen = approvalMethod?.type === 'noAccessRequired'\n const hasWrittenSteps = Boolean(\n accessRequest.requestPrompt || accessRequest.comments,\n )\n const isUndocumented =\n approvalMethod?.type === 'unknown' ||\n (approvalMethod?.type === 'custom' && !hasWrittenSteps)\n\n return (\n <div className=\"mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n How to get access\n </h3>\n\n {isOpen && (\n <p className=\"text-sm font-medium text-primary\">\n ✓ Open to everyone — no request needed. Just open it.\n </p>\n )}\n\n {isUndocumented && (\n <p className=\"text-sm text-muted-foreground\">\n Access process not documented yet — contact the owner below to find\n out.\n </p>\n )}\n\n {/* Approval Method */}\n {approvalMethod && approvalMethod.type !== 'custom' && (\n <div className=\"flex items-center gap-2\">\n {approvalMethod.type === 'service' && approvalMethod.config.url ? (\n <>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 hover:text-primary transition-colors\"\n title={approvalMethod.config.url}\n >\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </a>\n <a\n href={approvalMethod.config.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n title={approvalMethod.config.url}\n >\n <ExternalLink className=\"size-4\" />\n </a>\n </>\n ) : (\n <>\n <div>{getApprovalMethodIcon(approvalMethod.type)}</div>\n <div className=\"font-medium\">{approvalMethod.displayName}</div>\n </>\n )}\n </div>\n )}\n\n {/* Request Prompt - Inline */}\n {accessRequest.requestPrompt && (\n <div className=\"text-sm inline-flex items-center gap-2\">\n <span className=\"whitespace-nowrap shrink-0\">Request Prompt:</span>\n <span className=\"prose prose-sm inline [&>*]:inline [&>*]:m-0\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.requestPrompt}\n </ReactMarkdown>\n </span>\n <CopyButton\n onCopy={handleCopyPrompt}\n isCopied={copiedId === 'prompt'}\n ariaLabel=\"Copy request prompt\"\n className=\"h-6 w-6 p-0 shrink-0\"\n />\n </div>\n )}\n\n {/* Comments */}\n {accessRequest.comments && (\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.comments}\n </ReactMarkdown>\n </div>\n )}\n\n {/* Roles Table */}\n {accessRequest.roles && accessRequest.roles.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Available Roles</h4>\n <div className=\"rounded-lg border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"whitespace-nowrap\">Role</TableHead>\n <TableHead>Description</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {accessRequest.roles.map((role, idx) => (\n <TableRow key={`${role.displayName}-${idx}`}>\n <TableCell className=\"font-medium whitespace-nowrap\">\n {role.displayName}\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">\n {role.description || '—'}\n {role.adminNotes && (\n <div className=\"mt-1 text-xs italic text-muted-foreground/80\">\n Note: {role.adminNotes}\n </div>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )}\n\n {/* Approvers */}\n {accessRequest.approverPersonSlugs &&\n accessRequest.approverPersonSlugs.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Approvers</h4>\n <div className=\"flex flex-wrap gap-2\">\n {accessRequest.approverPersonSlugs.map((slug) => (\n <PersonBadge key={slug} slug={slug} />\n ))}\n </div>\n </div>\n )}\n\n {/* Documentation URLs */}\n {accessRequest.urls && accessRequest.urls.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-medium\">Documentation</h4>\n <div className=\"flex flex-col gap-1.5\">\n {accessRequest.urls.map((urlObj, idx) => (\n <a\n key={`${urlObj.url}-${idx}`}\n href={urlObj.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1.5\"\n >\n {urlObj.label || urlObj.url}\n <ExternalLink className=\"size-3\" />\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Post-Approval Instructions - Collapsible (secondary info) */}\n {accessRequest.postApprovalInstructions && (\n <Accordion type=\"single\" collapsible>\n <AccordionItem\n value=\"post-approval\"\n className=\"border rounded-lg px-4\"\n >\n <AccordionTrigger className=\"text-sm hover:no-underline py-3\">\n Post-Approval Instructions\n </AccordionTrigger>\n <AccordionContent className=\"pb-3\">\n <div className=\"text-sm text-muted-foreground prose prose-sm max-w-none\">\n <ReactMarkdown components={{ a: MarkdownLink }}>\n {accessRequest.postApprovalInstructions}\n </ReactMarkdown>\n </div>\n </AccordionContent>\n </AccordionItem>\n </Accordion>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -35,7 +35,7 @@ function OnboardingCard({
|
|
|
35
35
|
}
|
|
36
36
|
),
|
|
37
37
|
/* @__PURE__ */ jsxs("div", { className: "pr-10", children: [
|
|
38
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-
|
|
38
|
+
/* @__PURE__ */ jsx("h2", { className: "font-serif text-lg font-semibold mb-2", children: title }),
|
|
39
39
|
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-3 text-sm", children: description }),
|
|
40
40
|
/* @__PURE__ */ jsxs("div", { className: "bg-muted/50 rounded-lg p-3", children: [
|
|
41
41
|
/* @__PURE__ */ jsx("h3", { className: "font-semibold mb-2 text-xs", children: "Keyboard Navigation" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OnboardingCard.js","sources":["../../../../../../src/modules/appCatalog/ui/components/OnboardingCard.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, X } from 'lucide-react'\nimport { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button } from '~/ui/button'\nimport { Card } from '~/ui/card'\n\nconst STORAGE_KEY = 'app-catalog-onboarding-dismissed'\n\nexport interface OnboardingCardProps {\n /** Main heading text */\n title?: ReactNode\n /** Description text below the title */\n description?: ReactNode\n}\n\n/**\n * First-time user onboarding card showing keyboard navigation instructions.\n * Dismissible and persists preference to localStorage.\n */\nexport function OnboardingCard({\n title = 'Welcome to App Catalog',\n description = 'Browse and discover applications available to you. Click on any app to view details, screenshots, and request access if needed.',\n}: OnboardingCardProps = {}) {\n // Initialize from localStorage to avoid hydration flash\n const [isDismissed, setIsDismissed] = useState(() => {\n // Check localStorage on mount\n if (typeof window !== 'undefined') {\n const dismissed = localStorage.getItem(STORAGE_KEY)\n return dismissed === 'true'\n }\n return true // Default to dismissed during SSR\n })\n\n const handleDismiss = () => {\n localStorage.setItem(STORAGE_KEY, 'true')\n setIsDismissed(true)\n }\n\n if (isDismissed) {\n return null\n }\n\n return (\n <Card className=\"p-4 mb-3 relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-2 right-2\"\n onClick={handleDismiss}\n aria-label=\"Dismiss onboarding\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n\n <div className=\"pr-10\">\n <h2 className=\"text-lg font-
|
|
1
|
+
{"version":3,"file":"OnboardingCard.js","sources":["../../../../../../src/modules/appCatalog/ui/components/OnboardingCard.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, X } from 'lucide-react'\nimport { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button } from '~/ui/button'\nimport { Card } from '~/ui/card'\n\nconst STORAGE_KEY = 'app-catalog-onboarding-dismissed'\n\nexport interface OnboardingCardProps {\n /** Main heading text */\n title?: ReactNode\n /** Description text below the title */\n description?: ReactNode\n}\n\n/**\n * First-time user onboarding card showing keyboard navigation instructions.\n * Dismissible and persists preference to localStorage.\n */\nexport function OnboardingCard({\n title = 'Welcome to App Catalog',\n description = 'Browse and discover applications available to you. Click on any app to view details, screenshots, and request access if needed.',\n}: OnboardingCardProps = {}) {\n // Initialize from localStorage to avoid hydration flash\n const [isDismissed, setIsDismissed] = useState(() => {\n // Check localStorage on mount\n if (typeof window !== 'undefined') {\n const dismissed = localStorage.getItem(STORAGE_KEY)\n return dismissed === 'true'\n }\n return true // Default to dismissed during SSR\n })\n\n const handleDismiss = () => {\n localStorage.setItem(STORAGE_KEY, 'true')\n setIsDismissed(true)\n }\n\n if (isDismissed) {\n return null\n }\n\n return (\n <Card className=\"p-4 mb-3 relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-2 right-2\"\n onClick={handleDismiss}\n aria-label=\"Dismiss onboarding\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n\n <div className=\"pr-10\">\n <h2 className=\"font-serif text-lg font-semibold mb-2\">{title}</h2>\n <p className=\"text-muted-foreground mb-3 text-sm\">{description}</p>\n\n <div className=\"bg-muted/50 rounded-lg p-3\">\n <h3 className=\"font-semibold mb-2 text-xs\">Keyboard Navigation</h3>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2 text-xs\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs flex items-center\">\n <ArrowUp className=\"h-3 w-3\" />\n </kbd>\n <span className=\"text-muted-foreground\">/</span>\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs flex items-center\">\n <ArrowDown className=\"h-3 w-3\" />\n </kbd>\n </div>\n <span className=\"text-muted-foreground\">Navigate apps</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs\">\n Enter\n </kbd>\n <span className=\"text-muted-foreground\">Open screenshots</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <kbd className=\"px-2 py-1 bg-background border rounded text-xs\">\n Esc\n </kbd>\n <span className=\"text-muted-foreground\">Close details</span>\n </div>\n </div>\n </div>\n </div>\n </Card>\n )\n}\n"],"names":[],"mappings":";;;;;AAMA,MAAM,cAAc;AAab,SAAS,eAAe;AAAA,EAC7B,QAAQ;AAAA,EACR,cAAc;AAChB,IAAyB,IAAI;AAE3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM;AAEnD,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,YAAY,aAAa,QAAQ,WAAW;AAClD,aAAO,cAAc;AAAA,IACvB;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,gBAAgB,MAAM;AAC1B,iBAAa,QAAQ,aAAa,MAAM;AACxC,mBAAe,IAAI;AAAA,EACrB;AAEA,MAAI,aAAa;AACf,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,MAAA,EAAK,WAAU,qBACd,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS;AAAA,QACT,cAAW;AAAA,QAEX,UAAA,oBAAC,GAAA,EAAE,WAAU,UAAA,CAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAGzB,qBAAC,OAAA,EAAI,WAAU,SACb,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,yCAAyC,UAAA,OAAM;AAAA,MAC7D,oBAAC,KAAA,EAAE,WAAU,sCAAsC,UAAA,aAAY;AAAA,MAE/D,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAG,WAAU,8BAA6B,UAAA,uBAAmB;AAAA,QAC9D,qBAAC,OAAA,EAAI,WAAU,uDACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,cAAA,oBAAC,SAAI,WAAU,oEACb,8BAAC,SAAA,EAAQ,WAAU,WAAU,EAAA,CAC/B;AAAA,cACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,KAAC;AAAA,cACzC,oBAAC,SAAI,WAAU,oEACb,8BAAC,WAAA,EAAU,WAAU,WAAU,EAAA,CACjC;AAAA,YAAA,GACF;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,gBAAA,CAAa;AAAA,UAAA,GACvD;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,kDAAiD,UAAA,SAEhE;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,mBAAA,CAAgB;AAAA,UAAA,GAC1D;AAAA,UACA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,kDAAiD,UAAA,OAEhE;AAAA,YACA,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,gBAAA,CAAa;AAAA,UAAA,EAAA,CACvD;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
interface PersonBadgeProps {
|
|
2
2
|
slug: string;
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Person chip. Clicking it opens a small popover exposing BOTH the full name
|
|
6
|
+
* and the email, each with its own copy button — so the user picks what to copy
|
|
7
|
+
* (previously a single button copied only the email). Email also links to
|
|
8
|
+
* mailto:. Falls back to just the name (no popover) when no email is known.
|
|
9
|
+
*/
|
|
4
10
|
export declare function PersonBadge({ slug }: PersonBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
5
11
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { User,
|
|
2
|
+
import { User, Copy, Mail, Check } from "lucide-react";
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
4
|
import { Badge } from "../../../../ui/badge.js";
|
|
5
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../../../../ui/popover.js";
|
|
5
6
|
import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
6
7
|
import "../context/AppCatalogFiltersContext.js";
|
|
7
8
|
import "@tanstack/react-router";
|
|
@@ -21,48 +22,111 @@ function PersonBadge({ slug }) {
|
|
|
21
22
|
const person = getPersonBySlug(persons, slug);
|
|
22
23
|
const displayName = person ? `${person.firstName} ${person.lastName}`.trim() || slug : slug;
|
|
23
24
|
const email = person == null ? void 0 : person.email;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
if (!email) {
|
|
26
|
+
return /* @__PURE__ */ jsxs(
|
|
27
|
+
Badge,
|
|
28
|
+
{
|
|
29
|
+
variant: "outline",
|
|
30
|
+
className: "font-normal inline-flex items-center gap-1",
|
|
31
|
+
title: displayName,
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(User, { className: "size-3" }),
|
|
34
|
+
displayName
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [
|
|
40
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
41
|
+
"button",
|
|
42
|
+
{
|
|
43
|
+
type: "button",
|
|
44
|
+
className: "inline-flex",
|
|
45
|
+
title: `${displayName} · ${email}`,
|
|
46
|
+
children: /* @__PURE__ */ jsxs(
|
|
47
|
+
Badge,
|
|
48
|
+
{
|
|
49
|
+
variant: "outline",
|
|
50
|
+
className: "font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors",
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx(User, { className: "size-3" }),
|
|
53
|
+
displayName,
|
|
54
|
+
/* @__PURE__ */ jsx(Copy, { className: "size-3 opacity-50" })
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
) }),
|
|
60
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-64 p-2", children: [
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
CopyRow,
|
|
63
|
+
{
|
|
64
|
+
icon: /* @__PURE__ */ jsx(User, { className: "size-3.5" }),
|
|
65
|
+
label: "Name",
|
|
66
|
+
value: displayName
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
CopyRow,
|
|
71
|
+
{
|
|
72
|
+
icon: /* @__PURE__ */ jsx(Mail, { className: "size-3.5" }),
|
|
73
|
+
label: "Email",
|
|
74
|
+
value: email,
|
|
75
|
+
href: `mailto:${email}`
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
] })
|
|
79
|
+
] });
|
|
37
80
|
}
|
|
38
|
-
function
|
|
81
|
+
function CopyRow({
|
|
82
|
+
icon,
|
|
83
|
+
label,
|
|
84
|
+
value,
|
|
85
|
+
href
|
|
86
|
+
}) {
|
|
39
87
|
const [copied, setCopied] = useState(false);
|
|
40
88
|
const timeoutRef = useRef(null);
|
|
41
|
-
useEffect(
|
|
42
|
-
|
|
89
|
+
useEffect(
|
|
90
|
+
() => () => {
|
|
43
91
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
44
|
-
}
|
|
45
|
-
|
|
92
|
+
},
|
|
93
|
+
[]
|
|
94
|
+
);
|
|
46
95
|
const handleCopy = useCallback(
|
|
47
96
|
(e) => {
|
|
48
97
|
e.stopPropagation();
|
|
49
|
-
navigator.clipboard.writeText(
|
|
98
|
+
void navigator.clipboard.writeText(value);
|
|
50
99
|
setCopied(true);
|
|
51
100
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
52
101
|
timeoutRef.current = setTimeout(() => setCopied(false), 2e3);
|
|
53
102
|
},
|
|
54
|
-
[
|
|
55
|
-
);
|
|
56
|
-
return /* @__PURE__ */ jsx(
|
|
57
|
-
"button",
|
|
58
|
-
{
|
|
59
|
-
onClick: handleCopy,
|
|
60
|
-
className: "ml-0.5 hover:text-primary transition-colors",
|
|
61
|
-
title: copied ? "Copied!" : `Copy ${email}`,
|
|
62
|
-
type: "button",
|
|
63
|
-
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-3 text-green-600" }) : /* @__PURE__ */ jsx(Copy, { className: "size-3" })
|
|
64
|
-
}
|
|
103
|
+
[value]
|
|
65
104
|
);
|
|
105
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted", children: [
|
|
106
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground shrink-0", children: icon }),
|
|
107
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] uppercase tracking-wide text-muted-foreground", children: label }),
|
|
109
|
+
href ? /* @__PURE__ */ jsx(
|
|
110
|
+
"a",
|
|
111
|
+
{
|
|
112
|
+
href,
|
|
113
|
+
className: "block truncate text-[13px] text-primary hover:underline",
|
|
114
|
+
children: value
|
|
115
|
+
}
|
|
116
|
+
) : /* @__PURE__ */ jsx("span", { className: "block truncate text-[13px]", children: value })
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ jsx(
|
|
119
|
+
"button",
|
|
120
|
+
{
|
|
121
|
+
type: "button",
|
|
122
|
+
onClick: handleCopy,
|
|
123
|
+
className: "shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors",
|
|
124
|
+
title: copied ? "Copied!" : `Copy ${label.toLowerCase()}`,
|
|
125
|
+
"aria-label": `Copy ${label.toLowerCase()}`,
|
|
126
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-3.5 text-green-600" }) : /* @__PURE__ */ jsx(Copy, { className: "size-3.5" })
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
] });
|
|
66
130
|
}
|
|
67
131
|
export {
|
|
68
132
|
PersonBadge
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n
|
|
1
|
+
{"version":3,"file":"PersonBadge.js","sources":["../../../../../../src/modules/appCatalog/ui/components/PersonBadge.tsx"],"sourcesContent":["import { Check, Copy, Mail, User } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Badge } from '~/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/ui/popover'\nimport { useAppCatalogContext } from '~/modules/appCatalog'\nimport { getPersonBySlug } from '~/modules/appCatalog/utils/resolveHelpers'\n\ninterface PersonBadgeProps {\n slug: string\n}\n\n/**\n * Person chip. Clicking it opens a small popover exposing BOTH the full name\n * and the email, each with its own copy button — so the user picks what to copy\n * (previously a single button copied only the email). Email also links to\n * mailto:. Falls back to just the name (no popover) when no email is known.\n */\nexport function PersonBadge({ slug }: PersonBadgeProps) {\n const { persons } = useAppCatalogContext()\n const person = getPersonBySlug(persons, slug)\n\n const displayName = person\n ? `${person.firstName} ${person.lastName}`.trim() || slug\n : slug\n const email = person?.email\n\n // No email → nothing to pick between; render a plain, non-interactive chip.\n if (!email) {\n return (\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1\"\n title={displayName}\n >\n <User className=\"size-3\" />\n {displayName}\n </Badge>\n )\n }\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n className=\"inline-flex\"\n title={`${displayName} · ${email}`}\n >\n <Badge\n variant=\"outline\"\n className=\"font-normal inline-flex items-center gap-1 cursor-pointer hover:border-primary transition-colors\"\n >\n <User className=\"size-3\" />\n {displayName}\n <Copy className=\"size-3 opacity-50\" />\n </Badge>\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <CopyRow\n icon={<User className=\"size-3.5\" />}\n label=\"Name\"\n value={displayName}\n />\n <CopyRow\n icon={<Mail className=\"size-3.5\" />}\n label=\"Email\"\n value={email}\n href={`mailto:${email}`}\n />\n </PopoverContent>\n </Popover>\n )\n}\n\n/** One copyable field (label + value + copy button), value optionally a link. */\nfunction CopyRow({\n icon,\n label,\n value,\n href,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n href?: string\n}) {\n const [copied, setCopied] = useState(false)\n const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n useEffect(\n () => () => {\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n },\n [],\n )\n\n const handleCopy = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n void navigator.clipboard.writeText(value)\n setCopied(true)\n if (timeoutRef.current) clearTimeout(timeoutRef.current)\n timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n },\n [value],\n )\n\n return (\n <div className=\"flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted\">\n <span className=\"text-muted-foreground shrink-0\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-[10px] uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n {href ? (\n <a\n href={href}\n className=\"block truncate text-[13px] text-primary hover:underline\"\n >\n {value}\n </a>\n ) : (\n <span className=\"block truncate text-[13px]\">{value}</span>\n )}\n </span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"shrink-0 rounded p-1 text-muted-foreground hover:text-primary hover:bg-background transition-colors\"\n title={copied ? 'Copied!' : `Copy ${label.toLowerCase()}`}\n aria-label={`Copy ${label.toLowerCase()}`}\n >\n {copied ? (\n <Check className=\"size-3.5 text-green-600\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n </button>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
2
|
interface SubResourcesSectionProps {
|
|
3
3
|
subResources: Resource[];
|
|
4
|
+
/**
|
|
5
|
+
* Initial filter text (#38 item C). When the user reached this app by
|
|
6
|
+
* searching a term that matched a sub-resource, seed the sub-resource filter
|
|
7
|
+
* with that term so the matched child is revealed instead of buried in a
|
|
8
|
+
* long list (e.g. searching "biom" → open AWS Console → the biomarker
|
|
9
|
+
* account is pre-filtered). Only applied when it actually matches a child.
|
|
10
|
+
*/
|
|
11
|
+
initialSearch?: string;
|
|
4
12
|
}
|
|
5
|
-
export declare function SubResourcesSection({ subResources, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export declare function SubResourcesSection({ subResources, initialSearch, }: SubResourcesSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
14
|
export {};
|