@raster-app/react 0.0.0-stage → 0.1.0-canary-fd0f8dd
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/LICENSE +21 -0
- package/README.md +122 -2
- package/dist/context.d.ts +30 -0
- package/dist/context.js +50 -0
- package/dist/context.js.map +1 -0
- package/dist/icons.d.ts +22 -0
- package/dist/icons.js +21 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/picker-parts.d.ts +71 -0
- package/dist/picker-parts.js +150 -0
- package/dist/picker-parts.js.map +1 -0
- package/dist/picker-sign-in.d.ts +7 -0
- package/dist/picker-sign-in.js +18 -0
- package/dist/picker-sign-in.js.map +1 -0
- package/dist/query-keys.d.ts +13 -0
- package/dist/query-keys.js +14 -0
- package/dist/query-keys.js.map +1 -0
- package/dist/raster-picker.d.ts +23 -0
- package/dist/raster-picker.js +188 -0
- package/dist/raster-picker.js.map +1 -0
- package/dist/state-store.d.ts +18 -0
- package/dist/state-store.js +47 -0
- package/dist/state-store.js.map +1 -0
- package/dist/styles.css +1000 -0
- package/dist/use-asset-search.d.ts +28 -0
- package/dist/use-asset-search.js +82 -0
- package/dist/use-asset-search.js.map +1 -0
- package/dist/use-asset-upload.d.ts +46 -0
- package/dist/use-asset-upload.js +73 -0
- package/dist/use-asset-upload.js.map +1 -0
- package/dist/use-assets.d.ts +24 -0
- package/dist/use-assets.js +63 -0
- package/dist/use-assets.js.map +1 -0
- package/dist/use-libraries.d.ts +19 -0
- package/dist/use-libraries.js +57 -0
- package/dist/use-libraries.js.map +1 -0
- package/dist/use-navigation.d.ts +31 -0
- package/dist/use-navigation.js +111 -0
- package/dist/use-navigation.js.map +1 -0
- package/dist/use-organizations.d.ts +36 -0
- package/dist/use-organizations.js +23 -0
- package/dist/use-organizations.js.map +1 -0
- package/dist/use-promote-variant.d.ts +43 -0
- package/dist/use-promote-variant.js +65 -0
- package/dist/use-promote-variant.js.map +1 -0
- package/dist/use-session.d.ts +22 -0
- package/dist/use-session.js +37 -0
- package/dist/use-session.js.map +1 -0
- package/dist/use-sign-in.d.ts +34 -0
- package/dist/use-sign-in.js +79 -0
- package/dist/use-sign-in.js.map +1 -0
- package/package.json +54 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RasterApiError, type SearchHit } from '@raster-app/sdk';
|
|
2
|
+
/** Where a search looks: one organization, optionally narrowed to one of its libraries. */
|
|
3
|
+
export type SearchScope = {
|
|
4
|
+
organizationId: string;
|
|
5
|
+
/** Null searches every library in the organization. */
|
|
6
|
+
libraryId: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type AssetSearch = {
|
|
9
|
+
/** Whether there is a query to show results for — views swap to results on this. */
|
|
10
|
+
active: boolean;
|
|
11
|
+
/** The trimmed query the results are (or will be) for. */
|
|
12
|
+
query: string;
|
|
13
|
+
/** Null until the first page has landed. */
|
|
14
|
+
hits: Array<SearchHit> | null;
|
|
15
|
+
/** Total matches in scope, from the index — not just those loaded so far. */
|
|
16
|
+
found: number;
|
|
17
|
+
isSearching: boolean;
|
|
18
|
+
hasMore: boolean;
|
|
19
|
+
/** Fetch the next page. A no-op while one is already in flight, at the end, or after a failure. */
|
|
20
|
+
loadMore: () => void;
|
|
21
|
+
error: RasterApiError | null;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Full-text search over Raster, as a header search box drives it. The previous results stay
|
|
25
|
+
* on screen while the next query runs, since a grid that blanks on every keystroke makes
|
|
26
|
+
* typing feel destructive. A null scope switches the search off.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useAssetSearch(scope: SearchScope | null, rawQuery: string): AssetSearch;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { skipToken, useInfiniteQuery, } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { SEARCH_PAGE_SIZE, } from '@raster-app/sdk';
|
|
4
|
+
import { useRasterClient } from './context.js';
|
|
5
|
+
import { rasterKeys } from './query-keys.js';
|
|
6
|
+
import { useCredentialIdentity } from './use-session.js';
|
|
7
|
+
/** How long typing must pause before a query is sent, so a word costs one request. */
|
|
8
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
9
|
+
function toResults(data) {
|
|
10
|
+
return {
|
|
11
|
+
// The index can shift between pages; a hit already shown is not shown twice.
|
|
12
|
+
hits: Array.from(new Map(data.pages.flatMap((page) => page.hits).map((hit) => [hit.id, hit])).values()),
|
|
13
|
+
found: data.pages[data.pages.length - 1]?.found ?? 0,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Full-text search over Raster, as a header search box drives it. The previous results stay
|
|
18
|
+
* on screen while the next query runs, since a grid that blanks on every keystroke makes
|
|
19
|
+
* typing feel destructive. A null scope switches the search off.
|
|
20
|
+
*/
|
|
21
|
+
export function useAssetSearch(scope, rawQuery) {
|
|
22
|
+
const client = useRasterClient();
|
|
23
|
+
const identity = useCredentialIdentity();
|
|
24
|
+
const [debouncedQuery, setDebouncedQuery] = useState('');
|
|
25
|
+
const query = rawQuery.trim();
|
|
26
|
+
const active = scope !== null && query !== '';
|
|
27
|
+
const { data, error, isFetching, isError, hasNextPage, fetchNextPage } = useInfiniteQuery({
|
|
28
|
+
queryKey: [
|
|
29
|
+
...rasterKeys.search(identity, scope?.organizationId ?? null),
|
|
30
|
+
scope?.libraryId ?? null,
|
|
31
|
+
debouncedQuery,
|
|
32
|
+
],
|
|
33
|
+
queryFn: scope === null
|
|
34
|
+
? skipToken
|
|
35
|
+
: ({ pageParam, signal }) => client.search({
|
|
36
|
+
organizationId: scope.organizationId,
|
|
37
|
+
libraryIds: scope.libraryId === null ? undefined : [scope.libraryId],
|
|
38
|
+
query: debouncedQuery,
|
|
39
|
+
page: pageParam,
|
|
40
|
+
signal,
|
|
41
|
+
}),
|
|
42
|
+
initialPageParam: 1,
|
|
43
|
+
getNextPageParam: (lastPage, pages) => lastPage.hits.length === SEARCH_PAGE_SIZE && pages.length * SEARCH_PAGE_SIZE < lastPage.found
|
|
44
|
+
? pages.length + 1
|
|
45
|
+
: undefined,
|
|
46
|
+
select: toResults,
|
|
47
|
+
// `keepPreviousData` within one sign-in's search of one organization. The observer holds
|
|
48
|
+
// the last account's results after their cache is dropped, and a hit names its library but
|
|
49
|
+
// not its organization, so one shown under another organization would be acted on there.
|
|
50
|
+
placeholderData: (previous, previousQuery) => {
|
|
51
|
+
const [, signIn, , organizationId] = previousQuery?.queryKey ?? [];
|
|
52
|
+
return signIn === identity && organizationId === scope?.organizationId ? previous : undefined;
|
|
53
|
+
},
|
|
54
|
+
// A blank `q` returns recent assets rather than matches.
|
|
55
|
+
enabled: identity !== null && active && debouncedQuery !== '',
|
|
56
|
+
});
|
|
57
|
+
const shown = active ? data : undefined;
|
|
58
|
+
// Up from the keystroke rather than from when the request goes out, so the spinner confirms
|
|
59
|
+
// the typing was noticed.
|
|
60
|
+
const isSearching = active && (query !== debouncedQuery || isFetching);
|
|
61
|
+
// A failure also stops paging, rather than let an observer retry it on every scroll.
|
|
62
|
+
const hasMore = active && hasNextPage && !isError;
|
|
63
|
+
const loadMore = useCallback(() => {
|
|
64
|
+
if (hasMore)
|
|
65
|
+
void fetchNextPage({ cancelRefetch: false });
|
|
66
|
+
}, [hasMore, fetchNextPage]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const timer = setTimeout(() => setDebouncedQuery(query), SEARCH_DEBOUNCE_MS);
|
|
69
|
+
return () => clearTimeout(timer);
|
|
70
|
+
}, [query]);
|
|
71
|
+
return {
|
|
72
|
+
active,
|
|
73
|
+
query,
|
|
74
|
+
hits: shown?.hits ?? null,
|
|
75
|
+
found: shown?.found ?? 0,
|
|
76
|
+
isSearching,
|
|
77
|
+
hasMore,
|
|
78
|
+
loadMore,
|
|
79
|
+
error: active ? error : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=use-asset-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-search.js","sourceRoot":"","sources":["../src/use-asset-search.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype InfiniteData,\n\ttype QueryKey,\n\tskipToken,\n\tuseInfiniteQuery,\n} from '@tanstack/react-query'\nimport { useCallback, useEffect, useState } from 'react'\n\nimport {\n\ttype RasterApiError,\n\tSEARCH_PAGE_SIZE,\n\ttype SearchHit,\n\ttype SearchResult,\n} from '@raster-app/sdk'\n\nimport { useRasterClient } from './context.js'\nimport { rasterKeys } from './query-keys.js'\nimport { useCredentialIdentity } from './use-session.js'\n\n/** How long typing must pause before a query is sent, so a word costs one request. */\nconst SEARCH_DEBOUNCE_MS = 300\n\n/** Where a search looks: one organization, optionally narrowed to one of its libraries. */\nexport type SearchScope = {\n\torganizationId: string\n\t/** Null searches every library in the organization. */\n\tlibraryId: string | null\n}\n\nexport type AssetSearch = {\n\t/** Whether there is a query to show results for — views swap to results on this. */\n\tactive: boolean\n\t/** The trimmed query the results are (or will be) for. */\n\tquery: string\n\t/** Null until the first page has landed. */\n\thits: Array<SearchHit> | null\n\t/** Total matches in scope, from the index — not just those loaded so far. */\n\tfound: number\n\tisSearching: boolean\n\thasMore: boolean\n\t/** Fetch the next page. A no-op while one is already in flight, at the end, or after a failure. */\n\tloadMore: () => void\n\terror: RasterApiError | null\n}\n\ntype Results = Pick<SearchResult, 'hits' | 'found'>\n\nfunction toResults(data: InfiniteData<SearchResult>): Results {\n\treturn {\n\t\t// The index can shift between pages; a hit already shown is not shown twice.\n\t\thits: Array.from(\n\t\t\tnew Map(data.pages.flatMap((page) => page.hits).map((hit) => [hit.id, hit])).values()\n\t\t),\n\t\tfound: data.pages[data.pages.length - 1]?.found ?? 0,\n\t}\n}\n\n/**\n * Full-text search over Raster, as a header search box drives it. The previous results stay\n * on screen while the next query runs, since a grid that blanks on every keystroke makes\n * typing feel destructive. A null scope switches the search off.\n */\nexport function useAssetSearch(scope: SearchScope | null, rawQuery: string): AssetSearch {\n\tconst client = useRasterClient()\n\n\tconst identity = useCredentialIdentity()\n\n\tconst [debouncedQuery, setDebouncedQuery] = useState('')\n\n\tconst query = rawQuery.trim()\n\tconst active = scope !== null && query !== ''\n\n\tconst { data, error, isFetching, isError, hasNextPage, fetchNextPage } = useInfiniteQuery<\n\t\tSearchResult,\n\t\tRasterApiError,\n\t\tResults,\n\t\tQueryKey,\n\t\tnumber\n\t>({\n\t\tqueryKey: [\n\t\t\t...rasterKeys.search(identity, scope?.organizationId ?? null),\n\t\t\tscope?.libraryId ?? null,\n\t\t\tdebouncedQuery,\n\t\t],\n\t\tqueryFn:\n\t\t\tscope === null\n\t\t\t\t? skipToken\n\t\t\t\t: ({ pageParam, signal }) =>\n\t\t\t\t\t\tclient.search({\n\t\t\t\t\t\t\torganizationId: scope.organizationId,\n\t\t\t\t\t\t\tlibraryIds: scope.libraryId === null ? undefined : [scope.libraryId],\n\t\t\t\t\t\t\tquery: debouncedQuery,\n\t\t\t\t\t\t\tpage: pageParam,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t}),\n\t\tinitialPageParam: 1,\n\t\tgetNextPageParam: (lastPage, pages) =>\n\t\t\tlastPage.hits.length === SEARCH_PAGE_SIZE && pages.length * SEARCH_PAGE_SIZE < lastPage.found\n\t\t\t\t? pages.length + 1\n\t\t\t\t: undefined,\n\t\tselect: toResults,\n\t\t// `keepPreviousData` within one sign-in's search of one organization. The observer holds\n\t\t// the last account's results after their cache is dropped, and a hit names its library but\n\t\t// not its organization, so one shown under another organization would be acted on there.\n\t\tplaceholderData: (previous, previousQuery) => {\n\t\t\tconst [, signIn, , organizationId] = previousQuery?.queryKey ?? []\n\t\t\treturn signIn === identity && organizationId === scope?.organizationId ? previous : undefined\n\t\t},\n\t\t// A blank `q` returns recent assets rather than matches.\n\t\tenabled: identity !== null && active && debouncedQuery !== '',\n\t})\n\n\tconst shown = active ? data : undefined\n\t// Up from the keystroke rather than from when the request goes out, so the spinner confirms\n\t// the typing was noticed.\n\tconst isSearching = active && (query !== debouncedQuery || isFetching)\n\t// A failure also stops paging, rather than let an observer retry it on every scroll.\n\tconst hasMore = active && hasNextPage && !isError\n\n\tconst loadMore = useCallback(() => {\n\t\tif (hasMore) void fetchNextPage({ cancelRefetch: false })\n\t}, [hasMore, fetchNextPage])\n\n\tuseEffect(() => {\n\t\tconst timer = setTimeout(() => setDebouncedQuery(query), SEARCH_DEBOUNCE_MS)\n\t\treturn () => clearTimeout(timer)\n\t}, [query])\n\n\treturn {\n\t\tactive,\n\t\tquery,\n\t\thits: shown?.hits ?? null,\n\t\tfound: shown?.found ?? 0,\n\t\tisSearching,\n\t\thasMore,\n\t\tloadMore,\n\t\terror: active ? error : null,\n\t}\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Asset, RasterApiError, UploadSource, UploadedAsset } from '@raster-app/sdk';
|
|
2
|
+
export type UploadState = {
|
|
3
|
+
status: 'idle';
|
|
4
|
+
} | {
|
|
5
|
+
status: 'uploading';
|
|
6
|
+
}
|
|
7
|
+
/** Raster has the file and is still processing it. */
|
|
8
|
+
| {
|
|
9
|
+
status: 'processing';
|
|
10
|
+
asset: UploadedAsset;
|
|
11
|
+
}
|
|
12
|
+
/** Processed. For a variant, `asset` is its parent, whose `variants` now carry it. */
|
|
13
|
+
| {
|
|
14
|
+
status: 'done';
|
|
15
|
+
asset: Asset;
|
|
16
|
+
}
|
|
17
|
+
/** Raster has the file, but was still processing it when the wait gave up. */
|
|
18
|
+
| {
|
|
19
|
+
status: 'timed-out';
|
|
20
|
+
asset: UploadedAsset;
|
|
21
|
+
} | {
|
|
22
|
+
status: 'error';
|
|
23
|
+
error: RasterApiError;
|
|
24
|
+
};
|
|
25
|
+
type UploadOptions = {
|
|
26
|
+
organizationId: string;
|
|
27
|
+
libraryId: string;
|
|
28
|
+
/** Attach as a variant of this asset; omit for a new top-level asset. */
|
|
29
|
+
parentId?: string | null;
|
|
30
|
+
source: UploadSource;
|
|
31
|
+
filename?: string;
|
|
32
|
+
};
|
|
33
|
+
export type AssetUpload = {
|
|
34
|
+
state: UploadState;
|
|
35
|
+
/**
|
|
36
|
+
* Upload a file, then wait until Raster has processed it (`assets.waitUntilProcessed`).
|
|
37
|
+
*
|
|
38
|
+
* Resolves with the finished asset. Null leaves the rest to `state`: still processing when
|
|
39
|
+
* the wait gave up, which is not a failure, just "not yet" — or failed, or cancelled.
|
|
40
|
+
*/
|
|
41
|
+
upload: (options: UploadOptions) => Promise<Asset | null>;
|
|
42
|
+
cancel: () => void;
|
|
43
|
+
reset: () => void;
|
|
44
|
+
};
|
|
45
|
+
export declare function useAssetUpload(): AssetUpload;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useRasterClient } from './context.js';
|
|
4
|
+
import { rasterKeys } from './query-keys.js';
|
|
5
|
+
import { useCredentialIdentity } from './use-session.js';
|
|
6
|
+
function uploadState(mutation, uploaded) {
|
|
7
|
+
if (mutation.isSuccess)
|
|
8
|
+
return mutation.data;
|
|
9
|
+
if (mutation.isError)
|
|
10
|
+
return { status: 'error', error: mutation.error };
|
|
11
|
+
if (mutation.isIdle)
|
|
12
|
+
return { status: 'idle' };
|
|
13
|
+
return uploaded === null ? { status: 'uploading' } : { status: 'processing', asset: uploaded };
|
|
14
|
+
}
|
|
15
|
+
export function useAssetUpload() {
|
|
16
|
+
const client = useRasterClient();
|
|
17
|
+
const queryClient = useQueryClient();
|
|
18
|
+
const identity = useCredentialIdentity();
|
|
19
|
+
const mutation = useMutation({
|
|
20
|
+
mutationFn: async ({ signal, ...options }) => {
|
|
21
|
+
const uploaded = await client.assets.upload({ ...options, signal });
|
|
22
|
+
// An upload superseded by now must not show as the one processing.
|
|
23
|
+
if (!signal.aborted)
|
|
24
|
+
setUploaded(uploaded);
|
|
25
|
+
const asset = await client.assets.waitUntilProcessed({
|
|
26
|
+
organizationId: options.organizationId,
|
|
27
|
+
libraryId: options.libraryId,
|
|
28
|
+
assetId: uploaded.id,
|
|
29
|
+
parentId: uploaded.parentId,
|
|
30
|
+
signal,
|
|
31
|
+
});
|
|
32
|
+
return asset === null ? { status: 'timed-out', asset: uploaded } : { status: 'done', asset };
|
|
33
|
+
},
|
|
34
|
+
// Whatever failed, Raster may have the file by now.
|
|
35
|
+
onSettled: (_outcome, _error, { organizationId, libraryId }) => {
|
|
36
|
+
void queryClient.invalidateQueries({
|
|
37
|
+
queryKey: rasterKeys.libraries(identity, organizationId),
|
|
38
|
+
});
|
|
39
|
+
void queryClient.invalidateQueries({
|
|
40
|
+
queryKey: rasterKeys.assets(identity, organizationId, libraryId),
|
|
41
|
+
});
|
|
42
|
+
void queryClient.invalidateQueries({ queryKey: rasterKeys.search(identity, organizationId) });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const [uploaded, setUploaded] = useState(null);
|
|
46
|
+
const controllerRef = useRef(null);
|
|
47
|
+
const { mutateAsync, reset: resetMutation } = mutation;
|
|
48
|
+
const state = uploadState(mutation, uploaded);
|
|
49
|
+
const upload = useCallback(async (options) => {
|
|
50
|
+
controllerRef.current?.abort();
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
controllerRef.current = controller;
|
|
53
|
+
setUploaded(null);
|
|
54
|
+
try {
|
|
55
|
+
const outcome = await mutateAsync({ ...options, signal: controller.signal });
|
|
56
|
+
return outcome.status === 'done' ? outcome.asset : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}, [mutateAsync]);
|
|
62
|
+
const reset = useCallback(() => {
|
|
63
|
+
resetMutation();
|
|
64
|
+
setUploaded(null);
|
|
65
|
+
}, [resetMutation]);
|
|
66
|
+
const cancel = useCallback(() => {
|
|
67
|
+
controllerRef.current?.abort();
|
|
68
|
+
reset();
|
|
69
|
+
}, [reset]);
|
|
70
|
+
useEffect(() => () => controllerRef.current?.abort(), []);
|
|
71
|
+
return { state, upload, cancel, reset };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=use-asset-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-upload.js","sourceRoot":"","sources":["../src/use-asset-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { type UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type { Asset, RasterApiError, UploadSource, UploadedAsset } from '@raster-app/sdk'\n\nimport { useRasterClient } from './context.js'\nimport { rasterKeys } from './query-keys.js'\nimport { useCredentialIdentity } from './use-session.js'\n\nexport type UploadState =\n\t| { status: 'idle' }\n\t| { status: 'uploading' }\n\t/** Raster has the file and is still processing it. */\n\t| { status: 'processing'; asset: UploadedAsset }\n\t/** Processed. For a variant, `asset` is its parent, whose `variants` now carry it. */\n\t| { status: 'done'; asset: Asset }\n\t/** Raster has the file, but was still processing it when the wait gave up. */\n\t| { status: 'timed-out'; asset: UploadedAsset }\n\t| { status: 'error'; error: RasterApiError }\n\ntype UploadOptions = {\n\torganizationId: string\n\tlibraryId: string\n\t/** Attach as a variant of this asset; omit for a new top-level asset. */\n\tparentId?: string | null\n\tsource: UploadSource\n\tfilename?: string\n}\n\ntype UploadOutcome = Extract<UploadState, { status: 'done' | 'timed-out' }>\n\ntype UploadVariables = UploadOptions & { signal: AbortSignal }\n\ntype UploadMutation = UseMutationResult<UploadOutcome, RasterApiError, UploadVariables>\n\nexport type AssetUpload = {\n\tstate: UploadState\n\t/**\n\t * Upload a file, then wait until Raster has processed it (`assets.waitUntilProcessed`).\n\t *\n\t * Resolves with the finished asset. Null leaves the rest to `state`: still processing when\n\t * the wait gave up, which is not a failure, just \"not yet\" — or failed, or cancelled.\n\t */\n\tupload: (options: UploadOptions) => Promise<Asset | null>\n\tcancel: () => void\n\treset: () => void\n}\n\nfunction uploadState(mutation: UploadMutation, uploaded: UploadedAsset | null): UploadState {\n\tif (mutation.isSuccess) return mutation.data\n\tif (mutation.isError) return { status: 'error', error: mutation.error }\n\tif (mutation.isIdle) return { status: 'idle' }\n\treturn uploaded === null ? { status: 'uploading' } : { status: 'processing', asset: uploaded }\n}\n\nexport function useAssetUpload(): AssetUpload {\n\tconst client = useRasterClient()\n\tconst queryClient = useQueryClient()\n\n\tconst identity = useCredentialIdentity()\n\tconst mutation = useMutation<UploadOutcome, RasterApiError, UploadVariables>({\n\t\tmutationFn: async ({ signal, ...options }) => {\n\t\t\tconst uploaded = await client.assets.upload({ ...options, signal })\n\t\t\t// An upload superseded by now must not show as the one processing.\n\t\t\tif (!signal.aborted) setUploaded(uploaded)\n\t\t\tconst asset = await client.assets.waitUntilProcessed({\n\t\t\t\torganizationId: options.organizationId,\n\t\t\t\tlibraryId: options.libraryId,\n\t\t\t\tassetId: uploaded.id,\n\t\t\t\tparentId: uploaded.parentId,\n\t\t\t\tsignal,\n\t\t\t})\n\t\t\treturn asset === null ? { status: 'timed-out', asset: uploaded } : { status: 'done', asset }\n\t\t},\n\t\t// Whatever failed, Raster may have the file by now.\n\t\tonSettled: (_outcome, _error, { organizationId, libraryId }) => {\n\t\t\tvoid queryClient.invalidateQueries({\n\t\t\t\tqueryKey: rasterKeys.libraries(identity, organizationId),\n\t\t\t})\n\t\t\tvoid queryClient.invalidateQueries({\n\t\t\t\tqueryKey: rasterKeys.assets(identity, organizationId, libraryId),\n\t\t\t})\n\t\t\tvoid queryClient.invalidateQueries({ queryKey: rasterKeys.search(identity, organizationId) })\n\t\t},\n\t})\n\n\tconst [uploaded, setUploaded] = useState<UploadedAsset | null>(null)\n\n\tconst controllerRef = useRef<AbortController | null>(null)\n\n\tconst { mutateAsync, reset: resetMutation } = mutation\n\tconst state = uploadState(mutation, uploaded)\n\n\tconst upload = useCallback<AssetUpload['upload']>(\n\t\tasync (options) => {\n\t\t\tcontrollerRef.current?.abort()\n\t\t\tconst controller = new AbortController()\n\t\t\tcontrollerRef.current = controller\n\t\t\tsetUploaded(null)\n\t\t\ttry {\n\t\t\t\tconst outcome = await mutateAsync({ ...options, signal: controller.signal })\n\t\t\t\treturn outcome.status === 'done' ? outcome.asset : null\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\t[mutateAsync]\n\t)\n\n\tconst reset = useCallback(() => {\n\t\tresetMutation()\n\t\tsetUploaded(null)\n\t}, [resetMutation])\n\n\tconst cancel = useCallback(() => {\n\t\tcontrollerRef.current?.abort()\n\t\treset()\n\t}, [reset])\n\n\tuseEffect(() => () => controllerRef.current?.abort(), [])\n\n\treturn { state, upload, cancel, reset }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Asset, type RasterApiError } from '@raster-app/sdk';
|
|
2
|
+
export type AssetsResource = {
|
|
3
|
+
/** Null until the first page lands. */
|
|
4
|
+
assets: Array<Asset> | null;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
error: RasterApiError | null;
|
|
7
|
+
hasMore: boolean;
|
|
8
|
+
/** Fetch the next page. A no-op while one is already in flight, at the end, or after a failure. */
|
|
9
|
+
loadMore: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Warm a library's first page, as a hover does before the click that opens it. A library already
|
|
13
|
+
* cached is left alone: its grid paints on the click and refreshes itself.
|
|
14
|
+
*/
|
|
15
|
+
export declare function usePrefetchAssets(): (organizationId: string, libraryId: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* A library's assets, paged.
|
|
18
|
+
*
|
|
19
|
+
* Pass a null `libraryId` before a library is open; the hook then sits idle.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useAssets(options: {
|
|
22
|
+
organizationId: string | null;
|
|
23
|
+
libraryId: string | null;
|
|
24
|
+
}): AssetsResource;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { skipToken, useInfiniteQuery, useQueryClient, } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { ASSETS_PAGE_SIZE, } from '@raster-app/sdk';
|
|
4
|
+
import { useRasterClient } from './context.js';
|
|
5
|
+
import { rasterKeys } from './query-keys.js';
|
|
6
|
+
import { useCredentialIdentity } from './use-session.js';
|
|
7
|
+
/** A library can change between pages; an asset already shown is not shown twice. */
|
|
8
|
+
function toAssets(data) {
|
|
9
|
+
return Array.from(new Map(data.pages.flat().map((asset) => [asset.id, asset])).values());
|
|
10
|
+
}
|
|
11
|
+
/** One page of a library, as the grid reads it and a hover warms it. */
|
|
12
|
+
function listPage(client, organizationId, libraryId) {
|
|
13
|
+
// `signal` aborts a page nobody wants any more, which would otherwise hold the connection pool.
|
|
14
|
+
return ({ pageParam, signal }) => client.assets.list({ organizationId, libraryId, page: pageParam, signal });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Warm a library's first page, as a hover does before the click that opens it. A library already
|
|
18
|
+
* cached is left alone: its grid paints on the click and refreshes itself.
|
|
19
|
+
*/
|
|
20
|
+
export function usePrefetchAssets() {
|
|
21
|
+
const client = useRasterClient();
|
|
22
|
+
const queryClient = useQueryClient();
|
|
23
|
+
const identity = useCredentialIdentity();
|
|
24
|
+
return useCallback((organizationId, libraryId) => {
|
|
25
|
+
const queryKey = rasterKeys.assets(identity, organizationId, libraryId);
|
|
26
|
+
if (identity === null || queryClient.getQueryData(queryKey) !== undefined)
|
|
27
|
+
return;
|
|
28
|
+
void queryClient.prefetchInfiniteQuery({
|
|
29
|
+
queryKey,
|
|
30
|
+
queryFn: listPage(client, organizationId, libraryId),
|
|
31
|
+
initialPageParam: 1,
|
|
32
|
+
});
|
|
33
|
+
}, [client, queryClient, identity]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A library's assets, paged.
|
|
37
|
+
*
|
|
38
|
+
* Pass a null `libraryId` before a library is open; the hook then sits idle.
|
|
39
|
+
*/
|
|
40
|
+
export function useAssets(options) {
|
|
41
|
+
const { organizationId, libraryId } = options;
|
|
42
|
+
const client = useRasterClient();
|
|
43
|
+
const identity = useCredentialIdentity();
|
|
44
|
+
const { data, error, isFetching, isError, hasNextPage, fetchNextPage } = useInfiniteQuery({
|
|
45
|
+
queryKey: rasterKeys.assets(identity, organizationId, libraryId),
|
|
46
|
+
queryFn: organizationId === null || libraryId === null
|
|
47
|
+
? skipToken
|
|
48
|
+
: listPage(client, organizationId, libraryId),
|
|
49
|
+
initialPageParam: 1,
|
|
50
|
+
getNextPageParam: (lastPage, pages) => lastPage.length === ASSETS_PAGE_SIZE ? pages.length + 1 : undefined,
|
|
51
|
+
select: toAssets,
|
|
52
|
+
enabled: identity !== null && organizationId !== null && libraryId !== null,
|
|
53
|
+
});
|
|
54
|
+
// A failure also stops paging, rather than let a scroll observer retry it on every frame.
|
|
55
|
+
const hasMore = hasNextPage && !isError;
|
|
56
|
+
// Without `cancelRefetch: false`, a call while a page is in flight would abort it to ask again.
|
|
57
|
+
const loadMore = useCallback(() => {
|
|
58
|
+
if (hasMore)
|
|
59
|
+
void fetchNextPage({ cancelRefetch: false });
|
|
60
|
+
}, [hasMore, fetchNextPage]);
|
|
61
|
+
return { assets: data ?? null, isLoading: isFetching, error, hasMore, loadMore };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=use-assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-assets.js","sourceRoot":"","sources":["../src/use-assets.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype InfiniteData,\n\ttype QueryFunctionContext,\n\ttype QueryKey,\n\tskipToken,\n\tuseInfiniteQuery,\n\tuseQueryClient,\n} from '@tanstack/react-query'\nimport { useCallback } from 'react'\n\nimport {\n\tASSETS_PAGE_SIZE,\n\ttype Asset,\n\ttype RasterApiError,\n\ttype RasterClient,\n} from '@raster-app/sdk'\n\nimport { useRasterClient } from './context.js'\nimport { rasterKeys } from './query-keys.js'\nimport { useCredentialIdentity } from './use-session.js'\n\nexport type AssetsResource = {\n\t/** Null until the first page lands. */\n\tassets: Array<Asset> | null\n\tisLoading: boolean\n\terror: RasterApiError | null\n\thasMore: boolean\n\t/** Fetch the next page. A no-op while one is already in flight, at the end, or after a failure. */\n\tloadMore: () => void\n}\n\n/** A library can change between pages; an asset already shown is not shown twice. */\nfunction toAssets(data: InfiniteData<Array<Asset>>): Array<Asset> {\n\treturn Array.from(new Map(data.pages.flat().map((asset) => [asset.id, asset])).values())\n}\n\n/** One page of a library, as the grid reads it and a hover warms it. */\nfunction listPage(client: RasterClient, organizationId: string, libraryId: string) {\n\t// `signal` aborts a page nobody wants any more, which would otherwise hold the connection pool.\n\treturn ({ pageParam, signal }: QueryFunctionContext<QueryKey, number>) =>\n\t\tclient.assets.list({ organizationId, libraryId, page: pageParam, signal })\n}\n\n/**\n * Warm a library's first page, as a hover does before the click that opens it. A library already\n * cached is left alone: its grid paints on the click and refreshes itself.\n */\nexport function usePrefetchAssets(): (organizationId: string, libraryId: string) => void {\n\tconst client = useRasterClient()\n\tconst queryClient = useQueryClient()\n\tconst identity = useCredentialIdentity()\n\n\treturn useCallback(\n\t\t(organizationId, libraryId) => {\n\t\t\tconst queryKey = rasterKeys.assets(identity, organizationId, libraryId)\n\t\t\tif (identity === null || queryClient.getQueryData(queryKey) !== undefined) return\n\t\t\tvoid queryClient.prefetchInfiniteQuery({\n\t\t\t\tqueryKey,\n\t\t\t\tqueryFn: listPage(client, organizationId, libraryId),\n\t\t\t\tinitialPageParam: 1,\n\t\t\t})\n\t\t},\n\t\t[client, queryClient, identity]\n\t)\n}\n\n/**\n * A library's assets, paged.\n *\n * Pass a null `libraryId` before a library is open; the hook then sits idle.\n */\nexport function useAssets(options: {\n\torganizationId: string | null\n\tlibraryId: string | null\n}): AssetsResource {\n\tconst { organizationId, libraryId } = options\n\n\tconst client = useRasterClient()\n\n\tconst identity = useCredentialIdentity()\n\tconst { data, error, isFetching, isError, hasNextPage, fetchNextPage } = useInfiniteQuery<\n\t\tArray<Asset>,\n\t\tRasterApiError,\n\t\tArray<Asset>,\n\t\tQueryKey,\n\t\tnumber\n\t>({\n\t\tqueryKey: rasterKeys.assets(identity, organizationId, libraryId),\n\t\tqueryFn:\n\t\t\torganizationId === null || libraryId === null\n\t\t\t\t? skipToken\n\t\t\t\t: listPage(client, organizationId, libraryId),\n\t\tinitialPageParam: 1,\n\t\tgetNextPageParam: (lastPage, pages) =>\n\t\t\tlastPage.length === ASSETS_PAGE_SIZE ? pages.length + 1 : undefined,\n\t\tselect: toAssets,\n\t\tenabled: identity !== null && organizationId !== null && libraryId !== null,\n\t})\n\n\t// A failure also stops paging, rather than let a scroll observer retry it on every frame.\n\tconst hasMore = hasNextPage && !isError\n\n\t// Without `cancelRefetch: false`, a call while a page is in flight would abort it to ask again.\n\tconst loadMore = useCallback(() => {\n\t\tif (hasMore) void fetchNextPage({ cancelRefetch: false })\n\t}, [hasMore, fetchNextPage])\n\n\treturn { assets: data ?? null, isLoading: isFetching, error, hasMore, loadMore }\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import { type Library, type MeLibraryAccess, type RasterApiError } from '@raster-app/sdk';
|
|
3
|
+
export type LibraryWithAccess = Library & {
|
|
4
|
+
/** What the credential may do here. Null is no access (see `meLibraryAccess`). */
|
|
5
|
+
access: MeLibraryAccess['access'] | null;
|
|
6
|
+
};
|
|
7
|
+
/** Warm an organization's libraries, as a hover in the organization switcher does. */
|
|
8
|
+
export declare function usePrefetchLibraries(): (organizationId: string, organizationName: string | null) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Every library in one organization, each with the credential's access to it, so a host can
|
|
11
|
+
* hide upload where it is read-only.
|
|
12
|
+
*
|
|
13
|
+
* Pass a null `organizationId` before one has been resolved; the hook then sits idle rather
|
|
14
|
+
* than fetching against a placeholder.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useLibraries(options: {
|
|
17
|
+
organizationId: string | null;
|
|
18
|
+
organizationName?: string | null;
|
|
19
|
+
}): UseQueryResult<Array<LibraryWithAccess>, RasterApiError>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { skipToken, useQuery, useQueryClient, } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { meLibraryAccess, } from '@raster-app/sdk';
|
|
4
|
+
import { useRasterClient } from './context.js';
|
|
5
|
+
import { rasterKeys } from './query-keys.js';
|
|
6
|
+
import { meQuery } from './use-organizations.js';
|
|
7
|
+
import { useCredentialIdentity } from './use-session.js';
|
|
8
|
+
/** An organization's libraries with their access, as the list reads them and a hover warms them. */
|
|
9
|
+
function librariesQuery(client, queryClient, identity, organizationId, organizationName) {
|
|
10
|
+
return {
|
|
11
|
+
queryKey: [...rasterKeys.libraries(identity, organizationId), organizationName],
|
|
12
|
+
queryFn: async ({ signal }) => {
|
|
13
|
+
const [libraries, me] = await Promise.all([
|
|
14
|
+
client.libraries.list({ organizationId, organizationName, signal }),
|
|
15
|
+
queryClient.fetchQuery(meQuery(client, identity)),
|
|
16
|
+
]);
|
|
17
|
+
return libraries.map((library) => ({
|
|
18
|
+
...library,
|
|
19
|
+
access: meLibraryAccess(me, organizationId, library.id),
|
|
20
|
+
}));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Warm an organization's libraries, as a hover in the organization switcher does. */
|
|
25
|
+
export function usePrefetchLibraries() {
|
|
26
|
+
const client = useRasterClient();
|
|
27
|
+
const queryClient = useQueryClient();
|
|
28
|
+
const identity = useCredentialIdentity();
|
|
29
|
+
return useCallback((organizationId, organizationName) => {
|
|
30
|
+
if (identity === null)
|
|
31
|
+
return;
|
|
32
|
+
void queryClient.prefetchQuery(librariesQuery(client, queryClient, identity, organizationId, organizationName));
|
|
33
|
+
}, [client, queryClient, identity]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every library in one organization, each with the credential's access to it, so a host can
|
|
37
|
+
* hide upload where it is read-only.
|
|
38
|
+
*
|
|
39
|
+
* Pass a null `organizationId` before one has been resolved; the hook then sits idle rather
|
|
40
|
+
* than fetching against a placeholder.
|
|
41
|
+
*/
|
|
42
|
+
export function useLibraries(options) {
|
|
43
|
+
const { organizationId, organizationName = null } = options;
|
|
44
|
+
const client = useRasterClient();
|
|
45
|
+
const queryClient = useQueryClient();
|
|
46
|
+
const identity = useCredentialIdentity();
|
|
47
|
+
return useQuery(organizationId === null
|
|
48
|
+
? {
|
|
49
|
+
queryKey: [...rasterKeys.libraries(identity, organizationId), organizationName],
|
|
50
|
+
queryFn: skipToken,
|
|
51
|
+
}
|
|
52
|
+
: {
|
|
53
|
+
...librariesQuery(client, queryClient, identity, organizationId, organizationName),
|
|
54
|
+
enabled: identity !== null,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=use-libraries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-libraries.js","sourceRoot":"","sources":["../src/use-libraries.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,SAAS,EACT,QAAQ,EACR,cAAc,GACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,OAAO,EAKN,eAAe,GACf,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAOxD,oGAAoG;AACpG,SAAS,cAAc,CACtB,MAAoB,EACpB,WAAwB,EACxB,QAAuB,EACvB,cAAsB,EACtB,gBAA+B;IAE/B,OAAO;QACN,QAAQ,EAAE,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,QAAQ,EAAE,cAAc,CAAC,EAAE,gBAAgB,CAAC;QAC/E,OAAO,EAAE,KAAK,EAAE,EAAE,MAAM,EAAwB,EAAqC,EAAE;YACtF,MAAM,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACzC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC;gBACnE,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;aACjD,CAAC,CAAA;YACF,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAClC,GAAG,OAAO;gBACV,MAAM,EAAE,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,CAAC;aACvD,CAAC,CAAC,CAAA;QACJ,CAAC;KACD,CAAA;AACF,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,oBAAoB;IAInC,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;IAChC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IACpC,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAA;IAExC,OAAO,WAAW,CACjB,CAAC,cAAc,EAAE,gBAAgB,EAAE,EAAE;QACpC,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAM;QAC7B,KAAK,WAAW,CAAC,aAAa,CAC7B,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAC/E,CAAA;IACF,CAAC,EACD,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAC/B,CAAA;AACF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,OAG5B;IACA,MAAM,EAAE,cAAc,EAAE,gBAAgB,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAE3D,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;IAChC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IAEpC,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAA;IAExC,OAAO,QAAQ,CACd,cAAc,KAAK,IAAI;QACtB,CAAC,CAAC;YACA,QAAQ,EAAE,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,QAAQ,EAAE,cAAc,CAAC,EAAE,gBAAgB,CAAC;YAC/E,OAAO,EAAE,SAAS;SAClB;QACF,CAAC,CAAC;YACA,GAAG,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,CAAC;YAClF,OAAO,EAAE,QAAQ,KAAK,IAAI;SAC1B,CACH,CAAA;AACF,CAAC","sourcesContent":["import {\n\ttype QueryClient,\n\ttype QueryFunctionContext,\n\ttype UseQueryResult,\n\tskipToken,\n\tuseQuery,\n\tuseQueryClient,\n} from '@tanstack/react-query'\nimport { useCallback } from 'react'\n\nimport {\n\ttype Library,\n\ttype MeLibraryAccess,\n\ttype RasterApiError,\n\ttype RasterClient,\n\tmeLibraryAccess,\n} from '@raster-app/sdk'\n\nimport { useRasterClient } from './context.js'\nimport { rasterKeys } from './query-keys.js'\nimport { meQuery } from './use-organizations.js'\nimport { useCredentialIdentity } from './use-session.js'\n\nexport type LibraryWithAccess = Library & {\n\t/** What the credential may do here. Null is no access (see `meLibraryAccess`). */\n\taccess: MeLibraryAccess['access'] | null\n}\n\n/** An organization's libraries with their access, as the list reads them and a hover warms them. */\nfunction librariesQuery(\n\tclient: RasterClient,\n\tqueryClient: QueryClient,\n\tidentity: string | null,\n\torganizationId: string,\n\torganizationName: string | null\n) {\n\treturn {\n\t\tqueryKey: [...rasterKeys.libraries(identity, organizationId), organizationName],\n\t\tqueryFn: async ({ signal }: QueryFunctionContext): Promise<Array<LibraryWithAccess>> => {\n\t\t\tconst [libraries, me] = await Promise.all([\n\t\t\t\tclient.libraries.list({ organizationId, organizationName, signal }),\n\t\t\t\tqueryClient.fetchQuery(meQuery(client, identity)),\n\t\t\t])\n\t\t\treturn libraries.map((library) => ({\n\t\t\t\t...library,\n\t\t\t\taccess: meLibraryAccess(me, organizationId, library.id),\n\t\t\t}))\n\t\t},\n\t}\n}\n\n/** Warm an organization's libraries, as a hover in the organization switcher does. */\nexport function usePrefetchLibraries(): (\n\torganizationId: string,\n\torganizationName: string | null\n) => void {\n\tconst client = useRasterClient()\n\tconst queryClient = useQueryClient()\n\tconst identity = useCredentialIdentity()\n\n\treturn useCallback(\n\t\t(organizationId, organizationName) => {\n\t\t\tif (identity === null) return\n\t\t\tvoid queryClient.prefetchQuery(\n\t\t\t\tlibrariesQuery(client, queryClient, identity, organizationId, organizationName)\n\t\t\t)\n\t\t},\n\t\t[client, queryClient, identity]\n\t)\n}\n\n/**\n * Every library in one organization, each with the credential's access to it, so a host can\n * hide upload where it is read-only.\n *\n * Pass a null `organizationId` before one has been resolved; the hook then sits idle rather\n * than fetching against a placeholder.\n */\nexport function useLibraries(options: {\n\torganizationId: string | null\n\torganizationName?: string | null\n}): UseQueryResult<Array<LibraryWithAccess>, RasterApiError> {\n\tconst { organizationId, organizationName = null } = options\n\n\tconst client = useRasterClient()\n\tconst queryClient = useQueryClient()\n\n\tconst identity = useCredentialIdentity()\n\n\treturn useQuery<Array<LibraryWithAccess>, RasterApiError>(\n\t\torganizationId === null\n\t\t\t? {\n\t\t\t\t\tqueryKey: [...rasterKeys.libraries(identity, organizationId), organizationName],\n\t\t\t\t\tqueryFn: skipToken,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\t...librariesQuery(client, queryClient, identity, organizationId, organizationName),\n\t\t\t\t\tenabled: identity !== null,\n\t\t\t\t}\n\t)\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Asset, Library, Organization } from '@raster-app/sdk';
|
|
2
|
+
export type RasterNavigation = {
|
|
3
|
+
/** The organization whose libraries are listed, or null until one is resolved. */
|
|
4
|
+
organizationId: string | null;
|
|
5
|
+
/** The open library, or null on the library list. */
|
|
6
|
+
library: Library | null;
|
|
7
|
+
/**
|
|
8
|
+
* The asset whose variants are being browsed, or null for the plain grid. Held here so a
|
|
9
|
+
* breadcrumb can tell which level the user is on.
|
|
10
|
+
*/
|
|
11
|
+
asset: Asset | null;
|
|
12
|
+
/** The search box's text. */
|
|
13
|
+
query: string;
|
|
14
|
+
selectOrganization: (organizationId: string | null) => void;
|
|
15
|
+
openLibrary: (library: Library | null) => void;
|
|
16
|
+
openAsset: (asset: Asset | null) => void;
|
|
17
|
+
setQuery: (query: string) => void;
|
|
18
|
+
/** Fetch everything on screen again, by invalidating every Raster query. */
|
|
19
|
+
refresh: () => void;
|
|
20
|
+
/** True once the persisted organization and library have been read back. */
|
|
21
|
+
isRestored: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The `organization → library → asset` model every Raster plugin navigates, remembering the
|
|
25
|
+
* last organization and library through the provider's `state`. The level is whichever
|
|
26
|
+
* fields are set, since no host has a URL to route with; "back" sets a field to null.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useRasterNavigation(options: {
|
|
29
|
+
/** Used to drop a remembered organization the credential no longer reaches. */
|
|
30
|
+
organizations: Array<Organization> | null;
|
|
31
|
+
}): RasterNavigation;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { useStateStore } from './context.js';
|
|
4
|
+
import { rasterKeys } from './query-keys.js';
|
|
5
|
+
const ORGANIZATION_KEY = 'organizationId';
|
|
6
|
+
const LIBRARY_KEY = 'library';
|
|
7
|
+
/**
|
|
8
|
+
* The `organization → library → asset` model every Raster plugin navigates, remembering the
|
|
9
|
+
* last organization and library through the provider's `state`. The level is whichever
|
|
10
|
+
* fields are set, since no host has a URL to route with; "back" sets a field to null.
|
|
11
|
+
*/
|
|
12
|
+
export function useRasterNavigation(options) {
|
|
13
|
+
const { organizations } = options;
|
|
14
|
+
const queryClient = useQueryClient();
|
|
15
|
+
const state = useStateStore();
|
|
16
|
+
const [organizationId, setOrganizationId] = useState(null);
|
|
17
|
+
const [library, setLibrary] = useState(null);
|
|
18
|
+
const [asset, setAsset] = useState(null);
|
|
19
|
+
const [query, setQuery] = useState('');
|
|
20
|
+
const [restored, setRestored] = useState(null);
|
|
21
|
+
const selectOrganization = useCallback((next) => {
|
|
22
|
+
setOrganizationId(next);
|
|
23
|
+
setLibrary(null);
|
|
24
|
+
setAsset(null);
|
|
25
|
+
}, []);
|
|
26
|
+
const openLibrary = useCallback((next) => {
|
|
27
|
+
setLibrary(next);
|
|
28
|
+
setAsset(null);
|
|
29
|
+
}, []);
|
|
30
|
+
const refresh = useCallback(() => {
|
|
31
|
+
void queryClient.invalidateQueries({ queryKey: rasterKeys.all });
|
|
32
|
+
}, [queryClient]);
|
|
33
|
+
// Read what the last session left, once.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
void (async () => {
|
|
37
|
+
const [savedOrganization, savedLibrary] = await Promise.all([
|
|
38
|
+
state.get(ORGANIZATION_KEY),
|
|
39
|
+
state.get(LIBRARY_KEY),
|
|
40
|
+
]);
|
|
41
|
+
if (cancelled)
|
|
42
|
+
return;
|
|
43
|
+
setRestored({ organizationId: savedOrganization });
|
|
44
|
+
if (savedLibrary)
|
|
45
|
+
setLibrary(savedLibrary);
|
|
46
|
+
})();
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
};
|
|
50
|
+
}, [state]);
|
|
51
|
+
// Resolve the remembered organization against what the credential actually reaches. A
|
|
52
|
+
// stale id — an organization left, or a different account — is ignored rather than
|
|
53
|
+
// leaving the UI pointed at something that will 404.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (organizations === null || restored === null)
|
|
56
|
+
return;
|
|
57
|
+
setOrganizationId((current) => {
|
|
58
|
+
if (current !== null && organizations.some((organization) => organization.id === current)) {
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
const remembered = organizations.find((organization) => organization.id === restored.organizationId);
|
|
62
|
+
return remembered?.id ?? organizations[0]?.id ?? null;
|
|
63
|
+
});
|
|
64
|
+
}, [organizations, restored]);
|
|
65
|
+
// Drop a library that does not belong to the selected organization — it can arrive that
|
|
66
|
+
// way from persistence, or from switching organization with one open.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (organizationId === null)
|
|
69
|
+
return;
|
|
70
|
+
setLibrary((current) => current !== null && current.organizationId !== organizationId ? null : current);
|
|
71
|
+
}, [organizationId]);
|
|
72
|
+
// A query belongs to the scope it was typed in. Switching organization, opening a library
|
|
73
|
+
// or going back to the list all start the next search from a clean box — results for
|
|
74
|
+
// "beach" in one library would be a different set in the next. Reset while rendering, so no
|
|
75
|
+
// render pairs the new scope with the old query:
|
|
76
|
+
// https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
|
|
77
|
+
const [queryScope, setQueryScope] = useState({ organizationId, libraryId: library?.id });
|
|
78
|
+
if (queryScope.organizationId !== organizationId || queryScope.libraryId !== library?.id) {
|
|
79
|
+
setQueryScope({ organizationId, libraryId: library?.id });
|
|
80
|
+
setQuery('');
|
|
81
|
+
}
|
|
82
|
+
// Persist, so a reopen lands where the user was: once the last session's state has been read,
|
|
83
|
+
// since the empty library a mount starts with would otherwise erase the one being read, and
|
|
84
|
+
// React's development double mount reads it again after that.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (organizationId === null)
|
|
87
|
+
return;
|
|
88
|
+
void state.set(ORGANIZATION_KEY, organizationId);
|
|
89
|
+
}, [state, organizationId]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (restored === null)
|
|
92
|
+
return;
|
|
93
|
+
if (library === null)
|
|
94
|
+
void state.delete(LIBRARY_KEY);
|
|
95
|
+
else
|
|
96
|
+
void state.set(LIBRARY_KEY, library);
|
|
97
|
+
}, [state, library, restored]);
|
|
98
|
+
return {
|
|
99
|
+
organizationId,
|
|
100
|
+
library,
|
|
101
|
+
asset,
|
|
102
|
+
query,
|
|
103
|
+
selectOrganization,
|
|
104
|
+
openLibrary,
|
|
105
|
+
openAsset: setAsset,
|
|
106
|
+
setQuery,
|
|
107
|
+
refresh,
|
|
108
|
+
isRestored: restored !== null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=use-navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-navigation.js","sourceRoot":"","sources":["../src/use-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["import { useQueryClient } from '@tanstack/react-query'\nimport { useCallback, useEffect, useState } from 'react'\n\nimport type { Asset, Library, Organization } from '@raster-app/sdk'\n\nimport { useStateStore } from './context.js'\nimport { rasterKeys } from './query-keys.js'\n\nconst ORGANIZATION_KEY = 'organizationId'\nconst LIBRARY_KEY = 'library'\n\nexport type RasterNavigation = {\n\t/** The organization whose libraries are listed, or null until one is resolved. */\n\torganizationId: string | null\n\t/** The open library, or null on the library list. */\n\tlibrary: Library | null\n\t/**\n\t * The asset whose variants are being browsed, or null for the plain grid. Held here so a\n\t * breadcrumb can tell which level the user is on.\n\t */\n\tasset: Asset | null\n\t/** The search box's text. */\n\tquery: string\n\n\tselectOrganization: (organizationId: string | null) => void\n\topenLibrary: (library: Library | null) => void\n\topenAsset: (asset: Asset | null) => void\n\tsetQuery: (query: string) => void\n\t/** Fetch everything on screen again, by invalidating every Raster query. */\n\trefresh: () => void\n\t/** True once the persisted organization and library have been read back. */\n\tisRestored: boolean\n}\n\n/**\n * The `organization → library → asset` model every Raster plugin navigates, remembering the\n * last organization and library through the provider's `state`. The level is whichever\n * fields are set, since no host has a URL to route with; \"back\" sets a field to null.\n */\nexport function useRasterNavigation(options: {\n\t/** Used to drop a remembered organization the credential no longer reaches. */\n\torganizations: Array<Organization> | null\n}): RasterNavigation {\n\tconst { organizations } = options\n\n\tconst queryClient = useQueryClient()\n\tconst state = useStateStore()\n\n\tconst [organizationId, setOrganizationId] = useState<string | null>(null)\n\tconst [library, setLibrary] = useState<Library | null>(null)\n\tconst [asset, setAsset] = useState<Asset | null>(null)\n\tconst [query, setQuery] = useState('')\n\tconst [restored, setRestored] = useState<{ organizationId: string | null } | null>(null)\n\n\tconst selectOrganization = useCallback((next: string | null) => {\n\t\tsetOrganizationId(next)\n\t\tsetLibrary(null)\n\t\tsetAsset(null)\n\t}, [])\n\n\tconst openLibrary = useCallback((next: Library | null) => {\n\t\tsetLibrary(next)\n\t\tsetAsset(null)\n\t}, [])\n\n\tconst refresh = useCallback(() => {\n\t\tvoid queryClient.invalidateQueries({ queryKey: rasterKeys.all })\n\t}, [queryClient])\n\n\t// Read what the last session left, once.\n\tuseEffect(() => {\n\t\tlet cancelled = false\n\t\tvoid (async () => {\n\t\t\tconst [savedOrganization, savedLibrary] = await Promise.all([\n\t\t\t\tstate.get<string>(ORGANIZATION_KEY),\n\t\t\t\tstate.get<Library>(LIBRARY_KEY),\n\t\t\t])\n\t\t\tif (cancelled) return\n\t\t\tsetRestored({ organizationId: savedOrganization })\n\t\t\tif (savedLibrary) setLibrary(savedLibrary)\n\t\t})()\n\t\treturn () => {\n\t\t\tcancelled = true\n\t\t}\n\t}, [state])\n\n\t// Resolve the remembered organization against what the credential actually reaches. A\n\t// stale id — an organization left, or a different account — is ignored rather than\n\t// leaving the UI pointed at something that will 404.\n\tuseEffect(() => {\n\t\tif (organizations === null || restored === null) return\n\t\tsetOrganizationId((current) => {\n\t\t\tif (current !== null && organizations.some((organization) => organization.id === current)) {\n\t\t\t\treturn current\n\t\t\t}\n\t\t\tconst remembered = organizations.find(\n\t\t\t\t(organization) => organization.id === restored.organizationId\n\t\t\t)\n\t\t\treturn remembered?.id ?? organizations[0]?.id ?? null\n\t\t})\n\t}, [organizations, restored])\n\n\t// Drop a library that does not belong to the selected organization — it can arrive that\n\t// way from persistence, or from switching organization with one open.\n\tuseEffect(() => {\n\t\tif (organizationId === null) return\n\t\tsetLibrary((current) =>\n\t\t\tcurrent !== null && current.organizationId !== organizationId ? null : current\n\t\t)\n\t}, [organizationId])\n\n\t// A query belongs to the scope it was typed in. Switching organization, opening a library\n\t// or going back to the list all start the next search from a clean box — results for\n\t// \"beach\" in one library would be a different set in the next. Reset while rendering, so no\n\t// render pairs the new scope with the old query:\n\t// https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes\n\tconst [queryScope, setQueryScope] = useState({ organizationId, libraryId: library?.id })\n\tif (queryScope.organizationId !== organizationId || queryScope.libraryId !== library?.id) {\n\t\tsetQueryScope({ organizationId, libraryId: library?.id })\n\t\tsetQuery('')\n\t}\n\n\t// Persist, so a reopen lands where the user was: once the last session's state has been read,\n\t// since the empty library a mount starts with would otherwise erase the one being read, and\n\t// React's development double mount reads it again after that.\n\tuseEffect(() => {\n\t\tif (organizationId === null) return\n\t\tvoid state.set(ORGANIZATION_KEY, organizationId)\n\t}, [state, organizationId])\n\n\tuseEffect(() => {\n\t\tif (restored === null) return\n\t\tif (library === null) void state.delete(LIBRARY_KEY)\n\t\telse void state.set(LIBRARY_KEY, library)\n\t}, [state, library, restored])\n\n\treturn {\n\t\torganizationId,\n\t\tlibrary,\n\t\tasset,\n\t\tquery,\n\t\tselectOrganization,\n\t\topenLibrary,\n\t\topenAsset: setAsset,\n\t\tsetQuery,\n\t\trefresh,\n\t\tisRestored: restored !== null,\n\t}\n}\n"]}
|