@dudousxd/nestjs-media-dashboard 0.10.0 → 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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Headless client for opening the console from YOUR app.
3
+ *
4
+ * The console SPA is served at a literal path and knows nothing about your app's auth, so a plain
5
+ * browser navigation to it carries no identity. Mode A closes that gap: an XHR from inside your app
6
+ * — which DOES carry your auth — posts to the console's session endpoint, the `session` hook
7
+ * decides, and the library answers with its own signed cookie. The navigation that follows rides it.
8
+ *
9
+ * No UI here on purpose: you own the button, the page and the copy. This module owns the two things
10
+ * a host would otherwise have to rediscover — where the session endpoint actually lives, and how to
11
+ * call it without the failure mode below.
12
+ */
13
+ /**
14
+ * Where `POST` mints the console session cookie.
15
+ *
16
+ * Note this derives from `apiBasePath`, NOT `basePath`: the auth controller is mounted with the
17
+ * console's JSON API, which `MediaDashboardModule` mounts separately from the SPA. Deriving it here
18
+ * rather than at the call site means a host cannot get that split wrong, and cannot be silently
19
+ * broken by the route moving under a version bump — the break would only show up as a runtime 404.
20
+ */
21
+ export declare function mediaConsoleSessionUrl(apiBasePath?: string): string;
22
+ /** Where the console SPA itself is served. */
23
+ export declare function mediaConsoleUrl(basePath?: string): string;
24
+ export interface OpenConsoleOptions {
25
+ /** Where the console SPA is mounted. MUST match `MediaDashboardModule.forRoot({ basePath })`. */
26
+ basePath?: string;
27
+ /**
28
+ * Where the console's JSON API — and with it the session endpoint — is mounted. MUST match
29
+ * `MediaDashboardModule.forRoot({ apiBasePath })`. Defaults to `${basePath}/api`, the same
30
+ * defaulting the module itself does, so a host that set neither can pass neither.
31
+ */
32
+ apiBasePath?: string;
33
+ /**
34
+ * Headers for the mint request — in practice your app's `Authorization` header. A function (sync
35
+ * or async) is accepted so a token can be read at call time rather than captured at wiring time,
36
+ * which is what a refreshing token needs.
37
+ */
38
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
39
+ /** Injected for tests and non-browser callers. Defaults to the global `fetch`. */
40
+ fetch?: typeof globalThis.fetch;
41
+ signal?: AbortSignal;
42
+ /**
43
+ * Performs the navigation after a successful mint. Defaults to `location.assign`. Override to
44
+ * route through your own router, or to open in a new tab.
45
+ */
46
+ navigate?: (url: string) => void;
47
+ }
48
+ /** Thrown when the session could not be minted. Never thrown after a successful mint. */
49
+ export declare class ConsoleSessionError extends Error {
50
+ readonly url: string;
51
+ /** The HTTP status, or `undefined` when the request never produced one (network error). */
52
+ readonly status?: number | undefined;
53
+ constructor(message: string, url: string,
54
+ /** The HTTP status, or `undefined` when the request never produced one (network error). */
55
+ status?: number | undefined);
56
+ }
57
+ /**
58
+ * Mint the console session cookie. Resolves on success; throws {@link ConsoleSessionError} on
59
+ * refusal. Use this directly when you want to mint without navigating (a pre-flight check, or a
60
+ * link the user opens later).
61
+ */
62
+ export declare function mintMediaConsoleSession(options?: OpenConsoleOptions): Promise<void>;
63
+ /**
64
+ * Mint the session, then navigate to the console. Throws without navigating when the mint is
65
+ * refused, so a denied user gets a real error instead of landing on the console's
66
+ * "no session" page — which reads as a bug rather than a permission decision.
67
+ */
68
+ export declare function openMediaConsole(options?: OpenConsoleOptions): Promise<void>;
69
+ //# sourceMappingURL=console-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-session.d.ts","sourceRoot":"","sources":["../../src/client/console-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,GAAE,MAA8B,GAAG,MAAM,CAE1F;AAED,8CAA8C;AAC9C,wBAAgB,eAAe,CAAC,QAAQ,GAAE,MAA0B,GAAG,MAAM,CAE5E;AAED,MAAM,WAAW,kBAAkB;IACjC,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,yFAAyF;AACzF,qBAAa,mBAAoB,SAAQ,KAAK;IAG1C,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;gBAHxB,OAAO,EAAE,MAAM,EACN,GAAG,EAAE,MAAM;IACpB,2FAA2F;IAClF,MAAM,CAAC,EAAE,MAAM,YAAA;CAK3B;AAiBD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkD7F;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKtF"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Headless client for opening the console from YOUR app.
3
+ *
4
+ * The console SPA is served at a literal path and knows nothing about your app's auth, so a plain
5
+ * browser navigation to it carries no identity. Mode A closes that gap: an XHR from inside your app
6
+ * — which DOES carry your auth — posts to the console's session endpoint, the `session` hook
7
+ * decides, and the library answers with its own signed cookie. The navigation that follows rides it.
8
+ *
9
+ * No UI here on purpose: you own the button, the page and the copy. This module owns the two things
10
+ * a host would otherwise have to rediscover — where the session endpoint actually lives, and how to
11
+ * call it without the failure mode below.
12
+ */
13
+ /** The mount default; matches `DurableDashboardModule.forRoot()`'s own `basePath` default. */
14
+ const DEFAULT_BASE_PATH = '/media';
15
+ /** Matches `MediaDashboardModule`'s own `apiBasePath` default of `${basePath}/api`. */
16
+ const DEFAULT_API_BASE_PATH = '/media/api';
17
+ function normalizeBasePath(basePath) {
18
+ const trimmed = basePath.trim().replace(/\/+$/, '');
19
+ if (trimmed === '')
20
+ return '';
21
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
22
+ }
23
+ /**
24
+ * Where `POST` mints the console session cookie.
25
+ *
26
+ * Note this derives from `apiBasePath`, NOT `basePath`: the auth controller is mounted with the
27
+ * console's JSON API, which `MediaDashboardModule` mounts separately from the SPA. Deriving it here
28
+ * rather than at the call site means a host cannot get that split wrong, and cannot be silently
29
+ * broken by the route moving under a version bump — the break would only show up as a runtime 404.
30
+ */
31
+ export function mediaConsoleSessionUrl(apiBasePath = DEFAULT_API_BASE_PATH) {
32
+ return `${normalizeBasePath(apiBasePath)}/session`;
33
+ }
34
+ /** Where the console SPA itself is served. */
35
+ export function mediaConsoleUrl(basePath = DEFAULT_BASE_PATH) {
36
+ return normalizeBasePath(basePath) || '/';
37
+ }
38
+ /** Thrown when the session could not be minted. Never thrown after a successful mint. */
39
+ export class ConsoleSessionError extends Error {
40
+ url;
41
+ status;
42
+ constructor(message, url,
43
+ /** The HTTP status, or `undefined` when the request never produced one (network error). */
44
+ status) {
45
+ super(message);
46
+ this.url = url;
47
+ this.status = status;
48
+ this.name = 'ConsoleSessionError';
49
+ }
50
+ }
51
+ /**
52
+ * Mirror the module's own defaulting: an explicit `apiBasePath` wins, otherwise it hangs off
53
+ * `basePath`, so a host that only customised the SPA mount still hits the right session endpoint.
54
+ */
55
+ function resolveApiBasePath(options) {
56
+ if (options.apiBasePath !== undefined)
57
+ return options.apiBasePath;
58
+ if (options.basePath !== undefined)
59
+ return `${normalizeBasePath(options.basePath)}/api`;
60
+ return DEFAULT_API_BASE_PATH;
61
+ }
62
+ async function resolveHeaders(headers) {
63
+ if (headers === undefined)
64
+ return {};
65
+ return typeof headers === 'function' ? await headers() : headers;
66
+ }
67
+ /**
68
+ * Mint the console session cookie. Resolves on success; throws {@link ConsoleSessionError} on
69
+ * refusal. Use this directly when you want to mint without navigating (a pre-flight check, or a
70
+ * link the user opens later).
71
+ */
72
+ export async function mintMediaConsoleSession(options = {}) {
73
+ const url = mediaConsoleSessionUrl(resolveApiBasePath(options));
74
+ const doFetch = options.fetch ?? globalThis.fetch;
75
+ if (typeof doFetch !== 'function') {
76
+ throw new ConsoleSessionError('No `fetch` available; pass one via `options.fetch`.', url);
77
+ }
78
+ let response;
79
+ try {
80
+ response = await doFetch(url, {
81
+ method: 'POST',
82
+ // The whole point: the response's Set-Cookie must stick, and the cookie must ride the
83
+ // navigation that follows.
84
+ credentials: 'include',
85
+ headers: await resolveHeaders(options.headers),
86
+ // Load-bearing, and the reason this helper exists rather than three lines at the call site.
87
+ // `fetch` FOLLOWS redirects by default, so an app whose auth layer rewrites a 401 into a
88
+ // "go to /signin" redirect makes this request resolve 200 against the sign-in HTML —
89
+ // `response.ok` reads true, the caller navigates, and the user lands in a console with no
90
+ // session, which looks exactly like a permissions bug. Handling the redirect explicitly turns
91
+ // that into a clear error instead of a silent false success.
92
+ redirect: 'manual',
93
+ ...(options.signal ? { signal: options.signal } : {}),
94
+ });
95
+ }
96
+ catch (cause) {
97
+ throw new ConsoleSessionError(`Could not reach the console session endpoint at ${url}: ${String(cause)}`, url);
98
+ }
99
+ // `redirect: 'manual'` surfaces differently per runtime: browsers give an opaque response
100
+ // (`type: 'opaqueredirect'`, status 0), Node/undici gives the real 3xx. Both mean the same thing.
101
+ const redirected = response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400);
102
+ if (redirected) {
103
+ throw new ConsoleSessionError(`The console session endpoint at ${url} answered with a redirect instead of minting a session. Something in front of it — usually an auth middleware or exception filter that rewrites 401s into a sign-in redirect — is intercepting the response. Exempt this path from that rewrite so the real status reaches this caller.`, url, response.status || undefined);
104
+ }
105
+ if (!response.ok) {
106
+ throw new ConsoleSessionError(`The console refused to open (HTTP ${response.status}).`, url, response.status);
107
+ }
108
+ }
109
+ /**
110
+ * Mint the session, then navigate to the console. Throws without navigating when the mint is
111
+ * refused, so a denied user gets a real error instead of landing on the console's
112
+ * "no session" page — which reads as a bug rather than a permission decision.
113
+ */
114
+ export async function openMediaConsole(options = {}) {
115
+ await mintMediaConsoleSession(options);
116
+ const target = mediaConsoleUrl(options.basePath);
117
+ const navigate = options.navigate ?? ((url) => globalThis.location?.assign(url));
118
+ navigate(target);
119
+ }
120
+ //# sourceMappingURL=console-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-session.js","sourceRoot":"","sources":["../../src/client/console-session.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ export { ConsoleSessionError, mediaConsoleSessionUrl, mediaConsoleUrl, mintMediaConsoleSession, openMediaConsole, type OpenConsoleOptions, } from './console-session.js';
1
2
  export * from './types.js';
2
3
  export { apiBase, mediaConsoleClient, type MediaConsoleClient } from './media-console-client.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -1,3 +1,6 @@
1
+ // Headless console-launcher primitives (path derivation + mint-then-navigate). Re-exported here
2
+ // because `./client` resolves to this file — see the package's `exports` map.
3
+ export { ConsoleSessionError, mediaConsoleSessionUrl, mediaConsoleUrl, mintMediaConsoleSession, openMediaConsole, } from './console-session.js';
1
4
  export * from './types.js';
2
5
  export { apiBase, mediaConsoleClient } from './media-console-client.js';
3
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAA2B,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,8EAA8E;AAC9E,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,GAEjB,MAAM,sBAAsB,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAA2B,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './use-open-console.js';
2
+ export * from './open-console-button.js';
3
+ export * from '../client/console-session.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AASA,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AAGzC,cAAc,8BAA8B,CAAC"}
@@ -0,0 +1,15 @@
1
+ // The React tier of `@dudousxd/nestjs-media-dashboard`, published at the `./react` subpath so a
2
+ // host that only mounts the NestJS module never pulls React in. Three levels, pick one:
3
+ //
4
+ // openMediaConsole(...) — no React at all (`./client`), you own everything
5
+ // useOpenMediaConsole(...) — state for a launcher UI, you own the markup
6
+ // <OpenMediaConsoleButton /> — drop-in, unstyled, forwards every button prop
7
+ //
8
+ // `openMediaConsoleMutationOptions` wires the same call into TanStack Query without this package
9
+ // depending on TanStack.
10
+ export * from './use-open-console.js';
11
+ export * from './open-console-button.js';
12
+ // Re-exported so a React consumer needs one import path, not two. Only the console-launcher
13
+ // primitives: `./client` also exports the console's own data client, which a launcher never needs.
14
+ export * from '../client/console-session.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,wFAAwF;AACxF,EAAE;AACF,oFAAoF;AACpF,+EAA+E;AAC/E,iFAAiF;AACjF,EAAE;AACF,iGAAiG;AACjG,yBAAyB;AACzB,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,4FAA4F;AAC5F,mGAAmG;AACnG,cAAc,8BAA8B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ import type { OpenConsoleOptions } from '../client/console-session.js';
3
+ export interface OpenMediaConsoleButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'children'>, OpenConsoleOptions {
4
+ /** Button label. Defaults to "Open Media console". */
5
+ children?: ReactNode;
6
+ /** Shown while the session is being minted. Defaults to "Opening…". */
7
+ pendingLabel?: ReactNode;
8
+ /**
9
+ * Render the refusal yourself. Omit and the button renders a plain `<p role="alert">` under
10
+ * itself; pass `null` to render nothing and read the error from {@link useOpenMediaConsole}.
11
+ */
12
+ renderError?: ((error: Error) => ReactNode) | null;
13
+ }
14
+ /**
15
+ * Drop-in launcher: the top tier, for a host that just wants a working button.
16
+ *
17
+ * Deliberately unstyled — it emits a bare `<button>` and forwards `className`/`style`/every other
18
+ * button prop, so it inherits whatever design system the host already has instead of importing CSS
19
+ * that would fight it. When it doesn't fit, drop to {@link useOpenMediaConsole} (same behaviour,
20
+ * your markup) or to `openMediaConsole` (no React at all).
21
+ *
22
+ * The error is rendered by default rather than swallowed: a refused mint is the case a launcher most
23
+ * needs to surface, and a button that silently does nothing reads as broken rather than forbidden.
24
+ */
25
+ export declare function OpenMediaConsoleButton({ children, pendingLabel, renderError, basePath, apiBasePath, headers, fetch: fetchImpl, signal, navigate, disabled, ...buttonProps }: OpenMediaConsoleButtonProps): import("react/jsx-runtime").JSX.Element;
26
+ //# sourceMappingURL=open-console-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-console-button.d.ts","sourceRoot":"","sources":["../../src/react/open-console-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,MAAM,WAAW,2BACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC,EAC3E,kBAAkB;IACpB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,uEAAuE;IACvE,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,GAAG,IAAI,CAAC;CACpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,WAAW,EACX,OAAO,EACP,KAAK,EAAE,SAAS,EAChB,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,GAAG,WAAW,EACf,EAAE,2BAA2B,2CA4B7B"}
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useOpenMediaConsole } from './use-open-console.js';
3
+ /**
4
+ * Drop-in launcher: the top tier, for a host that just wants a working button.
5
+ *
6
+ * Deliberately unstyled — it emits a bare `<button>` and forwards `className`/`style`/every other
7
+ * button prop, so it inherits whatever design system the host already has instead of importing CSS
8
+ * that would fight it. When it doesn't fit, drop to {@link useOpenMediaConsole} (same behaviour,
9
+ * your markup) or to `openMediaConsole` (no React at all).
10
+ *
11
+ * The error is rendered by default rather than swallowed: a refused mint is the case a launcher most
12
+ * needs to surface, and a button that silently does nothing reads as broken rather than forbidden.
13
+ */
14
+ export function OpenMediaConsoleButton({ children, pendingLabel, renderError, basePath, apiBasePath, headers, fetch: fetchImpl, signal, navigate, disabled, ...buttonProps }) {
15
+ // Spread conditionally rather than passing `undefined`: `exactOptionalPropertyTypes` is on, and
16
+ // an explicit `undefined` is not the same as an absent key to the option defaulting downstream.
17
+ const { open, isPending, error } = useOpenMediaConsole({
18
+ ...(basePath !== undefined ? { basePath } : {}),
19
+ ...(apiBasePath !== undefined ? { apiBasePath } : {}),
20
+ ...(headers !== undefined ? { headers } : {}),
21
+ ...(fetchImpl !== undefined ? { fetch: fetchImpl } : {}),
22
+ ...(signal !== undefined ? { signal } : {}),
23
+ ...(navigate !== undefined ? { navigate } : {}),
24
+ });
25
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", ...buttonProps, onClick: open, disabled: disabled || isPending, "aria-busy": isPending || undefined, children: isPending ? (pendingLabel ?? 'Opening…') : (children ?? 'Open Media console') }), error &&
26
+ renderError !== null &&
27
+ (renderError?.(error) ?? _jsx("p", { role: "alert", children: error.message }))] }));
28
+ }
29
+ //# sourceMappingURL=open-console-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-console-button.js","sourceRoot":"","sources":["../../src/react/open-console-button.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAgB5D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,WAAW,EACX,OAAO,EACP,KAAK,EAAE,SAAS,EAChB,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,GAAG,WAAW,EACc;IAC5B,gGAAgG;IAChG,gGAAgG;IAChG,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,mBAAmB,CAAC;QACrD,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChD,CAAC,CAAC;IAEH,OAAO,CACL,8BACE,iBACE,IAAI,EAAC,QAAQ,KACT,WAAW,EACf,OAAO,EAAE,IAAI,EACb,QAAQ,EAAE,QAAQ,IAAI,SAAS,eACpB,SAAS,IAAI,SAAS,YAEhC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,oBAAoB,CAAC,GACvE,EACR,KAAK;gBACJ,WAAW,KAAK,IAAI;gBACpB,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,IAAI,YAAG,IAAI,EAAC,OAAO,YAAE,KAAK,CAAC,OAAO,GAAK,CAAC,IAC9D,CACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { type OpenConsoleOptions } from '../client/console-session.js';
2
+ /**
3
+ * React layer over `openMediaConsole` — the middle tier between the bare function
4
+ * (`../client/console-session.ts`) and the drop-in `<OpenMediaConsoleButton>`.
5
+ *
6
+ * You get the state a launcher UI actually needs (in-flight, error) and keep full control of the
7
+ * markup. Nothing here is Media-specific beyond the endpoint: it is a mint-then-navigate call with
8
+ * the two states that call can be in.
9
+ */
10
+ export interface UseOpenConsoleResult {
11
+ /** Start the mint-then-navigate. Never rejects — read `error` instead. */
12
+ open: () => void;
13
+ /** True from the click until the navigation starts, or until it fails. */
14
+ isPending: boolean;
15
+ /**
16
+ * The last refusal, or `null`. Cleared when `open()` is called again.
17
+ *
18
+ * Typed as `Error` rather than `ConsoleSessionError` because `ConsoleSessionError` carries the
19
+ * session endpoint it was refused by, and a failure that did not come from the mint (a `navigate`
20
+ * override that throws) has no endpoint to attribute — inventing one would be a lie. Every
21
+ * refusal from the mint itself IS a `ConsoleSessionError`, so `instanceof` narrows.
22
+ */
23
+ error: Error | null;
24
+ /** Drop a stale error without retrying — e.g. when a dialog closes. */
25
+ reset: () => void;
26
+ }
27
+ export declare function useOpenMediaConsole(options?: OpenConsoleOptions): UseOpenConsoleResult;
28
+ /**
29
+ * TanStack Query integration WITHOUT a TanStack dependency: the returned object is the shape
30
+ * `useMutation` takes, so a host that already uses Query wires the launcher into its own cache,
31
+ * devtools and error handling with no extra adapter — and a host that doesn't never pays for it.
32
+ *
33
+ * ```ts
34
+ * const { mutate, isPending } = useMutation(openMediaConsoleMutationOptions({ headers }));
35
+ * ```
36
+ */
37
+ export declare function openMediaConsoleMutationOptions(options?: OpenConsoleOptions): {
38
+ mutationKey: readonly unknown[];
39
+ mutationFn: () => Promise<void>;
40
+ };
41
+ //# sourceMappingURL=use-open-console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-open-console.d.ts","sourceRoot":"","sources":["../../src/react/use-open-console.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,kBAAkB,EAAoB,MAAM,8BAA8B,CAAC;AAEzF;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,uEAAuE;IACvE,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,oBAAoB,CA2B1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,GAAE,kBAAuB,GAAG;IACjF,WAAW,EAAE,SAAS,OAAO,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,CAeA"}
@@ -0,0 +1,52 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import { openMediaConsole } from '../client/console-session.js';
3
+ export function useOpenMediaConsole(options = {}) {
4
+ const [isPending, setIsPending] = useState(false);
5
+ const [error, setError] = useState(null);
6
+ // Kept in a ref so a caller passing an inline object literal (the common case) doesn't change the
7
+ // identity of `open` on every render, which would defeat memoizing anything downstream.
8
+ const optionsRef = useRef(options);
9
+ optionsRef.current = options;
10
+ const open = useCallback(() => {
11
+ setIsPending(true);
12
+ setError(null);
13
+ void openMediaConsole(optionsRef.current)
14
+ .then(() => {
15
+ // Deliberately NOT clearing `isPending` on success: the navigation is already underway and
16
+ // this component is about to be torn down. Flipping the button back to idle first produces
17
+ // a visible flicker of "ready to click again" on a page that is leaving.
18
+ })
19
+ .catch((cause) => {
20
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
21
+ setIsPending(false);
22
+ });
23
+ }, []);
24
+ const reset = useCallback(() => setError(null), []);
25
+ return { open, isPending, error, reset };
26
+ }
27
+ /**
28
+ * TanStack Query integration WITHOUT a TanStack dependency: the returned object is the shape
29
+ * `useMutation` takes, so a host that already uses Query wires the launcher into its own cache,
30
+ * devtools and error handling with no extra adapter — and a host that doesn't never pays for it.
31
+ *
32
+ * ```ts
33
+ * const { mutate, isPending } = useMutation(openMediaConsoleMutationOptions({ headers }));
34
+ * ```
35
+ */
36
+ export function openMediaConsoleMutationOptions(options = {}) {
37
+ return {
38
+ // Both mounts points are in the key: `apiBasePath` decides which endpoint mints the session and
39
+ // is settable independently of `basePath` (flip mounts the console API under its own `/api`
40
+ // prefix), so two mounts differing only in it are two different calls and must not share cache
41
+ // state.
42
+ mutationKey: [
43
+ 'media',
44
+ 'console',
45
+ 'open',
46
+ options.basePath ?? null,
47
+ options.apiBasePath ?? null,
48
+ ],
49
+ mutationFn: () => openMediaConsole(options),
50
+ };
51
+ }
52
+ //# sourceMappingURL=use-open-console.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-open-console.js","sourceRoot":"","sources":["../../src/react/use-open-console.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,EAA2B,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AA4BzF,MAAM,UAAU,mBAAmB,CAAC,UAA8B,EAAE;IAClE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAEvD,kGAAkG;IAClG,wFAAwF;IACxF,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;IAE7B,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,KAAK,gBAAgB,CAAC,UAAU,CAAC,OAAO,CAAC;aACtC,IAAI,CAAC,GAAG,EAAE;YACT,2FAA2F;YAC3F,2FAA2F;YAC3F,yEAAyE;QAC3E,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,QAAQ,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACpE,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAEpD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,+BAA+B,CAAC,UAA8B,EAAE;IAI9E,OAAO;QACL,gGAAgG;QAChG,4FAA4F;QAC5F,+FAA+F;QAC/F,SAAS;QACT,WAAW,EAAE;YACX,OAAO;YACP,SAAS;YACT,MAAM;YACN,OAAO,CAAC,QAAQ,IAAI,IAAI;YACxB,OAAO,CAAC,WAAW,IAAI,IAAI;SACnB;QACV,UAAU,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC;KAC5C,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-media-dashboard",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Standalone navigable /media console (disks, live uploads, media library) for nestjs-media — a bundled React SPA served by NestJS",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",
@@ -23,6 +23,11 @@
23
23
  "types": "./dist/client/index.d.ts",
24
24
  "import": "./dist/client/index.js",
25
25
  "default": "./dist/client/index.js"
26
+ },
27
+ "./react": {
28
+ "types": "./dist/react/index.d.ts",
29
+ "import": "./dist/react/index.js",
30
+ "default": "./dist/react/index.js"
26
31
  }
27
32
  },
28
33
  "files": [
@@ -31,19 +36,32 @@
31
36
  "peerDependencies": {
32
37
  "@nestjs/common": "^10.0.0 || ^11.0.0",
33
38
  "@nestjs/core": "^10.0.0 || ^11.0.0",
39
+ "react": "^18.0.0 || ^19.0.0",
40
+ "react-dom": "^18.0.0 || ^19.0.0",
34
41
  "rxjs": "^7.0.0",
35
42
  "@dudousxd/nestjs-media-core": "0.8.0"
36
43
  },
44
+ "peerDependenciesMeta": {
45
+ "react": {
46
+ "optional": true
47
+ },
48
+ "react-dom": {
49
+ "optional": true
50
+ }
51
+ },
37
52
  "devDependencies": {
38
53
  "@nestjs/common": "11.1.26",
39
54
  "@nestjs/core": "11.1.26",
40
55
  "@nestjs/platform-express": "11.1.26",
41
56
  "@nestjs/testing": "11.1.26",
42
57
  "@tanstack/react-query": "5.62.7",
58
+ "@testing-library/dom": "10.4.1",
59
+ "@testing-library/react": "16.1.0",
43
60
  "@types/react": "18.3.18",
44
61
  "@types/react-dom": "18.3.5",
45
62
  "@vitejs/plugin-react": "4.3.4",
46
63
  "autoprefixer": "10.4.20",
64
+ "jsdom": "25.0.1",
47
65
  "postcss": "8.4.49",
48
66
  "react": "18.3.1",
49
67
  "react-dom": "18.3.1",
@@ -58,7 +76,7 @@
58
76
  "@dudousxd/nestjs-media-core": "0.8.0"
59
77
  },
60
78
  "scripts": {
61
- "build": "vite build && tsup && tsc -p tsconfig.client.json",
62
- "typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.spec.json --noEmit"
79
+ "build": "vite build && tsup && tsc -p tsconfig.client.json && tsc -p tsconfig.react.json",
80
+ "typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.react.json --noEmit && tsc -p tsconfig.spec.json --noEmit"
63
81
  }
64
82
  }