@datalayer/core 1.2.23 → 1.2.24
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/README.md +4 -3
- package/lib/components/buttons/CopyToClipboardButton.d.ts +13 -0
- package/lib/components/buttons/CopyToClipboardButton.js +73 -0
- package/lib/components/buttons/index.d.ts +1 -0
- package/lib/components/buttons/index.js +1 -0
- package/lib/hooks/deckSpec.d.ts +2 -0
- package/lib/hooks/deckSpec.js +22 -0
- package/lib/hooks/useCache.d.ts +48 -0
- package/lib/hooks/useCache.js +111 -2
- package/lib/state/substates/NavigationState.js +14 -0
- package/lib/views/profile/UserIdentity.d.ts +41 -0
- package/lib/views/profile/UserIdentity.js +9 -0
- package/lib/views/profile/index.d.ts +2 -0
- package/lib/views/profile/index.js +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -100,10 +100,11 @@ Use `agent-runtimes` for runtime workloads (`RuntimeClient`, `@datalayer`, snaps
|
|
|
100
100
|
|
|
101
101
|
## Examples
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
The [examples](./examples/README.md) show the Datalayer Client at work:
|
|
104
104
|
|
|
105
|
-
- [
|
|
106
|
-
|
|
105
|
+
- [OpenTelemetry observability](./examples/otel/README.md): instrument Datalayer usage with OpenTelemetry traces and metrics, with a small FastAPI service and a React UI to visualize them (`make` targets in [examples/otel](./examples/otel)).
|
|
106
|
+
|
|
107
|
+
For agents, chats and sandboxes built on the client, see the [Agent Runtimes examples](https://github.com/datalayer/agent-runtimes/tree/main/examples).
|
|
107
108
|
|
|
108
109
|
## Platform Integration
|
|
109
110
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type CopyToClipboardButtonProps = {
|
|
2
|
+
value: string;
|
|
3
|
+
ariaLabel?: string;
|
|
4
|
+
successText?: string;
|
|
5
|
+
errorText?: string;
|
|
6
|
+
direction?: 'n' | 's' | 'e' | 'w' | 'nw' | 'ne' | 'sw' | 'se';
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
size?: 'small' | 'medium';
|
|
9
|
+
variant?: 'default' | 'invisible' | 'primary' | 'danger';
|
|
10
|
+
onResult?: (copied: boolean) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function CopyToClipboardButton({ value, ariaLabel, successText, errorText, direction, disabled, size, variant, onResult, }: CopyToClipboardButtonProps): import("react").JSX.Element;
|
|
13
|
+
export default CopyToClipboardButton;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A button that copies a value, its tooltip saying what it copies, then
|
|
8
|
+
* whether it did.
|
|
9
|
+
*
|
|
10
|
+
* @module components/buttons/CopyToClipboardButton
|
|
11
|
+
*/
|
|
12
|
+
import { useMemo, useState } from 'react';
|
|
13
|
+
import { IconButton, Tooltip } from '@primer/react';
|
|
14
|
+
import { CopyIcon } from '@primer/octicons-react';
|
|
15
|
+
const ellipsize = (value, max = 96) => {
|
|
16
|
+
if (!value) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
if (value.length <= max) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
const head = Math.floor((max - 1) / 2);
|
|
23
|
+
const tail = max - head - 1;
|
|
24
|
+
return `${value.slice(0, head)}…${value.slice(-tail)}`;
|
|
25
|
+
};
|
|
26
|
+
export function CopyToClipboardButton({ value, ariaLabel = 'Copy to clipboard', successText = 'Copied', errorText = 'Unable to copy', direction = 'n', disabled = false, size = 'small', variant = 'invisible', onResult, }) {
|
|
27
|
+
const [status, setStatus] = useState('idle');
|
|
28
|
+
const hasValue = Boolean(value && value.trim());
|
|
29
|
+
const tooltip = useMemo(() => {
|
|
30
|
+
if (status === 'success') {
|
|
31
|
+
return successText;
|
|
32
|
+
}
|
|
33
|
+
if (status === 'error') {
|
|
34
|
+
return errorText;
|
|
35
|
+
}
|
|
36
|
+
return hasValue ? `Copy: ${ellipsize(value)}` : 'Nothing to copy';
|
|
37
|
+
}, [status, successText, errorText, hasValue, value]);
|
|
38
|
+
const handleClick = async () => {
|
|
39
|
+
if (!hasValue || disabled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
if (!navigator?.clipboard?.writeText) {
|
|
44
|
+
throw new Error('Clipboard API unavailable');
|
|
45
|
+
}
|
|
46
|
+
await navigator.clipboard.writeText(value);
|
|
47
|
+
setStatus('success');
|
|
48
|
+
onResult?.(true);
|
|
49
|
+
window.setTimeout(() => setStatus('idle'), 1500);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
setStatus('error');
|
|
53
|
+
onResult?.(false);
|
|
54
|
+
window.setTimeout(() => setStatus('idle'), 1800);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const isDisabled = disabled || !hasValue;
|
|
58
|
+
const button = (_jsx(IconButton, { "aria-label": ariaLabel, icon: CopyIcon, size: size, variant: variant, disabled: isDisabled, onClick: handleClick }));
|
|
59
|
+
/*
|
|
60
|
+
* A disabled button wears no tooltip.
|
|
61
|
+
*
|
|
62
|
+
* `Tooltip` only takes interactive content — `button:not([disabled])` — and
|
|
63
|
+
* throws on anything else, which is what a disabled button is. Nothing is
|
|
64
|
+
* lost by leaving it out: a disabled element fires no pointer event, so the
|
|
65
|
+
* tooltip would never have shown. `IconButton` does the same with the one
|
|
66
|
+
* it renders of its own.
|
|
67
|
+
*/
|
|
68
|
+
if (isDisabled) {
|
|
69
|
+
return button;
|
|
70
|
+
}
|
|
71
|
+
return (_jsx(Tooltip, { text: tooltip, direction: direction, children: button }));
|
|
72
|
+
}
|
|
73
|
+
export default CopyToClipboardButton;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
export * from './CopyToClipboardButton.js';
|
|
5
6
|
export * from './DownloadCSVButton.js';
|
|
6
7
|
export * from './DownloadJsonButton.js';
|
|
7
8
|
export * from './LongActionButton.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A deck's specification, as Spacer serves it: YAML.
|
|
7
|
+
*
|
|
8
|
+
* `GET /decks/{id}` inlines the specification as `model_s`, the text Spacer
|
|
9
|
+
* stored — YAML, or JSON for a deck stored before the specification was
|
|
10
|
+
* YAML, which the same parser reads (JSON is YAML). Saving sends the parsed
|
|
11
|
+
* value back (`PUT /decks/{id}/model`), and Spacer writes it as YAML.
|
|
12
|
+
*
|
|
13
|
+
* @module hooks/deckSpec
|
|
14
|
+
*/
|
|
15
|
+
import { parse } from 'yaml';
|
|
16
|
+
/** The value a deck's `model_s` holds, or `undefined` when it holds none. */
|
|
17
|
+
export const parseDeckSpec = (text) => {
|
|
18
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return parse(text);
|
|
22
|
+
};
|
package/lib/hooks/useCache.d.ts
CHANGED
|
@@ -38,6 +38,27 @@ type IAnySpace = any;
|
|
|
38
38
|
export type CacheProps = {
|
|
39
39
|
loginRoute?: string;
|
|
40
40
|
};
|
|
41
|
+
/** A deck's video, as Spacer keeps it with the deck (`GET /decks/{id}/video`). */
|
|
42
|
+
export type SavedDeckVideo = {
|
|
43
|
+
/** Signed: plays it for `expiresIn` seconds. */
|
|
44
|
+
url: string;
|
|
45
|
+
/** Signed: downloads it as `<deck name>.mp4`. */
|
|
46
|
+
downloadUrl: string;
|
|
47
|
+
/** Signed: its first frame, a PNG. */
|
|
48
|
+
posterUrl: string;
|
|
49
|
+
/** Its captions, as WebVTT text. */
|
|
50
|
+
captions: string;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
bytes: number;
|
|
54
|
+
madeAt: string;
|
|
55
|
+
madeBy: string;
|
|
56
|
+
/** When the deck was last edited, if it says. */
|
|
57
|
+
editedAt: string;
|
|
58
|
+
/** The deck has been edited since the video was made. */
|
|
59
|
+
stale: boolean;
|
|
60
|
+
expiresIn: number;
|
|
61
|
+
};
|
|
41
62
|
export type ISearchOpts = {
|
|
42
63
|
q: string;
|
|
43
64
|
types: string[];
|
|
@@ -282,6 +303,8 @@ export declare const queryKeys: {
|
|
|
282
303
|
readonly detail: (id: string) => readonly ["decks", "detail", string];
|
|
283
304
|
readonly bySpace: (spaceId: string) => readonly ["decks", "space", string];
|
|
284
305
|
readonly model: (deckId: string) => readonly ["decks", "detail", string, "model"];
|
|
306
|
+
/** Under the detail: saving the deck refreshes whether it is stale. */
|
|
307
|
+
readonly video: (deckId: string) => readonly ["decks", "detail", string, "video"];
|
|
285
308
|
};
|
|
286
309
|
readonly cells: {
|
|
287
310
|
readonly all: () => readonly ["cells"];
|
|
@@ -803,6 +826,15 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
|
|
|
803
826
|
spec: unknown;
|
|
804
827
|
}, unknown>;
|
|
805
828
|
useCloneDeck: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
|
|
829
|
+
useSavedDeckVideo: (deckId: string) => import("@tanstack/react-query").UseQueryResult<SavedDeckVideo | null, Error>;
|
|
830
|
+
useSaveDeckVideo: () => import("@tanstack/react-query").UseMutationResult<SavedDeckVideo, Error, {
|
|
831
|
+
deckId: string;
|
|
832
|
+
video: Blob;
|
|
833
|
+
captions: string;
|
|
834
|
+
poster: Blob;
|
|
835
|
+
spec: unknown;
|
|
836
|
+
}, unknown>;
|
|
837
|
+
useDeleteDeckVideo: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
|
|
806
838
|
useDocument: (documentId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
|
|
807
839
|
useDocumentsBySpace: (spaceId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
|
|
808
840
|
useCreateDocument: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
|
|
@@ -958,6 +990,12 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
|
|
|
958
990
|
data?: string;
|
|
959
991
|
}, unknown>;
|
|
960
992
|
useResetItemImage: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
|
|
993
|
+
useSetItemVideo: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
|
|
994
|
+
itemId: string;
|
|
995
|
+
video: Blob;
|
|
996
|
+
captions: string;
|
|
997
|
+
poster: Blob;
|
|
998
|
+
}, unknown>;
|
|
961
999
|
useCountItemReuse: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
|
|
962
1000
|
itemId: string;
|
|
963
1001
|
kind: "clone" | "instantiate";
|
|
@@ -975,6 +1013,14 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
|
|
|
975
1013
|
ref?: string;
|
|
976
1014
|
data?: string;
|
|
977
1015
|
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Publish a deck *as a video*: the video saved with it in its space
|
|
1018
|
+
* becomes the published deck's video — its page plays it and its
|
|
1019
|
+
* shared link announces it (`og:video`). The library copies it in
|
|
1020
|
+
* the same request that publishes, and refuses before publishing a
|
|
1021
|
+
* deck that has none.
|
|
1022
|
+
*/
|
|
1023
|
+
video?: boolean;
|
|
978
1024
|
/**
|
|
979
1025
|
* The publisher's own words for their work, which is what somebody
|
|
980
1026
|
* else will search for.
|
|
@@ -1415,6 +1461,8 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
|
|
|
1415
1461
|
readonly detail: (id: string) => readonly ["decks", "detail", string];
|
|
1416
1462
|
readonly bySpace: (spaceId: string) => readonly ["decks", "space", string];
|
|
1417
1463
|
readonly model: (deckId: string) => readonly ["decks", "detail", string, "model"];
|
|
1464
|
+
/** Under the detail: saving the deck refreshes whether it is stale. */
|
|
1465
|
+
readonly video: (deckId: string) => readonly ["decks", "detail", string, "video"];
|
|
1418
1466
|
};
|
|
1419
1467
|
readonly cells: {
|
|
1420
1468
|
readonly all: () => readonly ["cells"];
|
package/lib/hooks/useCache.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { useQuery, useQueries, useMutation, useQueryClient, } from '@tanstack/react-query';
|
|
40
40
|
import { BOOTSTRAP_USER_ONBOARDING, asContact, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, } from '../models/index.js';
|
|
41
41
|
import { asSpace } from './cacheConverters.js';
|
|
42
|
+
import { parseDeckSpec } from './deckSpec.js';
|
|
42
43
|
import { libraryAppFieldsOf } from './libraryApp.js';
|
|
43
44
|
import { unlinkIAMProvider } from './unlinkIAMProvider.js';
|
|
44
45
|
import { useCoreStore, useIAMStore, profileStore } from '../state/index.js';
|
|
@@ -368,6 +369,8 @@ export const queryKeys = {
|
|
|
368
369
|
detail: (id) => [...queryKeys.decks.details(), id],
|
|
369
370
|
bySpace: (spaceId) => [...queryKeys.decks.all(), 'space', spaceId],
|
|
370
371
|
model: (deckId) => [...queryKeys.decks.detail(deckId), 'model'],
|
|
372
|
+
/** Under the detail: saving the deck refreshes whether it is stale. */
|
|
373
|
+
video: (deckId) => [...queryKeys.decks.detail(deckId), 'video'],
|
|
371
374
|
},
|
|
372
375
|
// Cells
|
|
373
376
|
cells: {
|
|
@@ -721,6 +724,10 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
721
724
|
meta.publisher = raw.publisher;
|
|
722
725
|
if (raw.image !== undefined)
|
|
723
726
|
meta.image = raw.image;
|
|
727
|
+
// A deck's video, made by its owner: `url`, `captionsUrl`, `posterUrl`,
|
|
728
|
+
// `width`, `height`. Absent when it has none.
|
|
729
|
+
if (raw.video)
|
|
730
|
+
meta.video = raw.video;
|
|
724
731
|
/*
|
|
725
732
|
* Where the artifact can be opened for editing.
|
|
726
733
|
*
|
|
@@ -1074,7 +1081,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
1074
1081
|
name: raw.name_t,
|
|
1075
1082
|
description: raw.description_t,
|
|
1076
1083
|
tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
|
|
1077
|
-
|
|
1084
|
+
// YAML, as Spacer stores it (a deck stored as JSON reads the same).
|
|
1085
|
+
spec: parseDeckSpec(raw.model_s),
|
|
1078
1086
|
public: raw.is_public_b ?? false,
|
|
1079
1087
|
creationDate: new Date(raw.creation_ts_dt),
|
|
1080
1088
|
lastUpdateDate: raw.last_update_ts_dt
|
|
@@ -2628,6 +2636,77 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
2628
2636
|
},
|
|
2629
2637
|
});
|
|
2630
2638
|
};
|
|
2639
|
+
/**
|
|
2640
|
+
* The video saved with a deck in its space, or `null` when it has none.
|
|
2641
|
+
*
|
|
2642
|
+
* Signed URLs that read it for a few hours (`url`, `posterUrl`, and
|
|
2643
|
+
* `downloadUrl`, an attachment named after the deck), its captions inline
|
|
2644
|
+
* (WebVTT), its size, when it was made, and `stale` — the deck has been
|
|
2645
|
+
* edited since: its spec is not the one the video was made from.
|
|
2646
|
+
*/
|
|
2647
|
+
const useSavedDeckVideo = (deckId) => {
|
|
2648
|
+
return useQuery({
|
|
2649
|
+
queryKey: queryKeys.decks.video(deckId),
|
|
2650
|
+
queryFn: async () => {
|
|
2651
|
+
const resp = await requestDatalayer({
|
|
2652
|
+
url: `${configuration.spacerUrl}/api/spacer/v1/decks/${encodeURIComponent(deckId)}/video`,
|
|
2653
|
+
method: 'GET',
|
|
2654
|
+
});
|
|
2655
|
+
if (!resp.success) {
|
|
2656
|
+
throw new Error(resp.message || 'Failed to read the deck video');
|
|
2657
|
+
}
|
|
2658
|
+
return (resp.video ?? null);
|
|
2659
|
+
},
|
|
2660
|
+
...DEFAULT_QUERY_OPTIONS,
|
|
2661
|
+
enabled: !!deckId,
|
|
2662
|
+
});
|
|
2663
|
+
};
|
|
2664
|
+
/**
|
|
2665
|
+
* Keep the video the deck engine made of a deck with the deck, in its
|
|
2666
|
+
* space, replacing the one it had: the MP4, its WebVTT captions, its
|
|
2667
|
+
* poster, and the spec it was made from.
|
|
2668
|
+
*/
|
|
2669
|
+
const useSaveDeckVideo = () => {
|
|
2670
|
+
return useMutation({
|
|
2671
|
+
mutationFn: async ({ deckId, video, captions, poster, spec, }) => {
|
|
2672
|
+
const body = new FormData();
|
|
2673
|
+
body.append('video', video, 'video.mp4');
|
|
2674
|
+
body.append('captions', new Blob([captions], { type: 'text/vtt' }), 'captions.vtt');
|
|
2675
|
+
body.append('poster', poster, 'poster.png');
|
|
2676
|
+
body.append('spec', JSON.stringify(spec));
|
|
2677
|
+
const resp = await requestDatalayer({
|
|
2678
|
+
url: `${configuration.spacerUrl}/api/spacer/v1/decks/${encodeURIComponent(deckId)}/video`,
|
|
2679
|
+
method: 'PUT',
|
|
2680
|
+
body,
|
|
2681
|
+
});
|
|
2682
|
+
if (!resp.success) {
|
|
2683
|
+
throw new Error(resp.message || 'Failed to save the video');
|
|
2684
|
+
}
|
|
2685
|
+
return resp.video;
|
|
2686
|
+
},
|
|
2687
|
+
onSuccess: (video, { deckId }) => {
|
|
2688
|
+
queryClient.setQueryData(queryKeys.decks.video(deckId), video);
|
|
2689
|
+
},
|
|
2690
|
+
});
|
|
2691
|
+
};
|
|
2692
|
+
/** The deck without its saved video. */
|
|
2693
|
+
const useDeleteDeckVideo = () => {
|
|
2694
|
+
return useMutation({
|
|
2695
|
+
mutationFn: async (deckId) => {
|
|
2696
|
+
const resp = await requestDatalayer({
|
|
2697
|
+
url: `${configuration.spacerUrl}/api/spacer/v1/decks/${encodeURIComponent(deckId)}/video`,
|
|
2698
|
+
method: 'DELETE',
|
|
2699
|
+
});
|
|
2700
|
+
if (!resp.success) {
|
|
2701
|
+
throw new Error(resp.message || 'Failed to remove the video');
|
|
2702
|
+
}
|
|
2703
|
+
return resp;
|
|
2704
|
+
},
|
|
2705
|
+
onSuccess: (_, deckId) => {
|
|
2706
|
+
queryClient.setQueryData(queryKeys.decks.video(deckId), null);
|
|
2707
|
+
},
|
|
2708
|
+
});
|
|
2709
|
+
};
|
|
2631
2710
|
/**
|
|
2632
2711
|
* Clone notebook
|
|
2633
2712
|
*/
|
|
@@ -6915,6 +6994,31 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
6915
6994
|
onSuccess: invalidateArtifactVisibility,
|
|
6916
6995
|
});
|
|
6917
6996
|
};
|
|
6997
|
+
/**
|
|
6998
|
+
* A published deck's video, made in its owner's browser by the deck
|
|
6999
|
+
* engine: the MP4, its WebVTT captions and its poster (the first slide).
|
|
7000
|
+
* Making it again replaces it.
|
|
7001
|
+
*/
|
|
7002
|
+
const useSetItemVideo = () => {
|
|
7003
|
+
return useMutation({
|
|
7004
|
+
mutationFn: async ({ itemId, video, captions, poster, }) => {
|
|
7005
|
+
const body = new FormData();
|
|
7006
|
+
body.append('video', video, 'video.mp4');
|
|
7007
|
+
body.append('captions', new Blob([captions], { type: 'text/vtt' }), 'captions.vtt');
|
|
7008
|
+
body.append('poster', poster, 'poster.png');
|
|
7009
|
+
const resp = await requestDatalayer({
|
|
7010
|
+
url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/video`,
|
|
7011
|
+
method: 'PUT',
|
|
7012
|
+
body,
|
|
7013
|
+
});
|
|
7014
|
+
if (!resp.success) {
|
|
7015
|
+
throw new Error(resp.message || 'Failed to set the video');
|
|
7016
|
+
}
|
|
7017
|
+
return resp.video;
|
|
7018
|
+
},
|
|
7019
|
+
onSuccess: invalidateArtifactVisibility,
|
|
7020
|
+
});
|
|
7021
|
+
};
|
|
6918
7022
|
/** Publish a public artifact natively to LinkedIn through Library's saga. */
|
|
6919
7023
|
const usePublishLinkedInShare = () => {
|
|
6920
7024
|
return useMutation({
|
|
@@ -7131,7 +7235,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
7131
7235
|
const usePublishArtifact = () => {
|
|
7132
7236
|
const setImage = useSetItemImage();
|
|
7133
7237
|
return useMutation({
|
|
7134
|
-
mutationFn: async ({ itemId, image, tags, description, }) => {
|
|
7238
|
+
mutationFn: async ({ itemId, image, tags, description, video, }) => {
|
|
7135
7239
|
/*
|
|
7136
7240
|
* Publish first, then dress it.
|
|
7137
7241
|
*
|
|
@@ -7156,6 +7260,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
7156
7260
|
// words must not be able to clear them by omission.
|
|
7157
7261
|
...(tags === undefined ? {} : { tags }),
|
|
7158
7262
|
...(description === undefined ? {} : { description }),
|
|
7263
|
+
...(video ? { video: true } : {}),
|
|
7159
7264
|
},
|
|
7160
7265
|
});
|
|
7161
7266
|
if (!resp.success) {
|
|
@@ -8584,6 +8689,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
8584
8689
|
useUpdateDeck,
|
|
8585
8690
|
useUpdateDeckModel,
|
|
8586
8691
|
useCloneDeck,
|
|
8692
|
+
useSavedDeckVideo,
|
|
8693
|
+
useSaveDeckVideo,
|
|
8694
|
+
useDeleteDeckVideo,
|
|
8587
8695
|
// Documents
|
|
8588
8696
|
useDocument,
|
|
8589
8697
|
useDocumentsBySpace,
|
|
@@ -8655,6 +8763,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
8655
8763
|
useSetFeaturedRank,
|
|
8656
8764
|
useSetItemImage,
|
|
8657
8765
|
useResetItemImage,
|
|
8766
|
+
useSetItemVideo,
|
|
8658
8767
|
useCountItemReuse,
|
|
8659
8768
|
useSyncArtifactCatalogue,
|
|
8660
8769
|
usePublishArtifact,
|
|
@@ -301,8 +301,22 @@ export function viewForRoute(route, current) {
|
|
|
301
301
|
if (path === '/apps' || path.startsWith('/apps/')) {
|
|
302
302
|
return 'apps';
|
|
303
303
|
}
|
|
304
|
+
// A space's decks and teaching pages are drawn beside the Agentify sidebar:
|
|
305
|
+
// filed under the Platform, the Platform button came back to a deck with
|
|
306
|
+
// the Agentify sidebar beside it (reported 23 October 2026).
|
|
307
|
+
if (AGENTIFY_SPACE_PAGE.test(path) || AGENTIFY_ACCOUNT_PAGE.test(path)) {
|
|
308
|
+
return 'agentify';
|
|
309
|
+
}
|
|
304
310
|
return 'home';
|
|
305
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* `/<account>/<space>/<kind>[/…]` for the kinds a space shows beside the
|
|
314
|
+
* Agentify sidebar — decks, courses and what a course is made of. The
|
|
315
|
+
* documentation (`/docs/…`) is never an account.
|
|
316
|
+
*/
|
|
317
|
+
const AGENTIFY_SPACE_PAGE = /^\/(?!docs\/)[^/]+\/[^/]+\/(?:decks|courses|course|lessons|exercises|assignments)(?:\/|$)/;
|
|
318
|
+
/** `/<account>/courses`: an account's courses, beside the Agentify sidebar. */
|
|
319
|
+
const AGENTIFY_ACCOUNT_PAGE = /^\/(?!docs\/)[^/]+\/courses$/;
|
|
306
320
|
/** Whether a page may be the one a shell returns to. */
|
|
307
321
|
function isRouteOfView(route, view) {
|
|
308
322
|
return (typeof route === 'string' &&
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who an account is, to be read: its avatar, then its username, display
|
|
3
|
+
* name, initials, origin and identifier, the identifier with a button that
|
|
4
|
+
* copies it.
|
|
5
|
+
*
|
|
6
|
+
* It is given the identity and fetches nothing, so any host draws it from
|
|
7
|
+
* what it already holds: the web's profile page beside its form, VS Code's
|
|
8
|
+
* Profile view from its signed-in user.
|
|
9
|
+
*
|
|
10
|
+
* @module views/profile/UserIdentity
|
|
11
|
+
*/
|
|
12
|
+
import type { JSX } from 'react';
|
|
13
|
+
/** What {@link UserIdentity} shows of an account. */
|
|
14
|
+
export interface UserIdentityProfile {
|
|
15
|
+
/** The account's identifier. */
|
|
16
|
+
id?: string;
|
|
17
|
+
username: string;
|
|
18
|
+
display_name: string;
|
|
19
|
+
initials: string;
|
|
20
|
+
/** Its photograph, when it has one. */
|
|
21
|
+
avatar_url?: string;
|
|
22
|
+
/** Its chosen picture, which wins over the photograph. */
|
|
23
|
+
avatar_icon?: string;
|
|
24
|
+
/** Where the account comes from (the identity provider). */
|
|
25
|
+
origin?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface UserIdentityProps {
|
|
28
|
+
identity: UserIdentityProfile;
|
|
29
|
+
/** Whether the identifier is shown. @default true */
|
|
30
|
+
displayId?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the display name and the initials are shown. They are computed
|
|
33
|
+
* from the first and the last name, so a host showing the form that edits
|
|
34
|
+
* those states them there instead. @default true
|
|
35
|
+
*/
|
|
36
|
+
displayComputed?: boolean;
|
|
37
|
+
/** Rendered under the avatar, typically the button changing it. */
|
|
38
|
+
avatarAction?: JSX.Element;
|
|
39
|
+
}
|
|
40
|
+
export declare const UserIdentity: ({ identity, displayId, displayComputed, avatarAction, }: UserIdentityProps) => JSX.Element;
|
|
41
|
+
export default UserIdentity;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from '@primer/react';
|
|
3
|
+
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
import { UserAvatar } from '../../components/avatars/index.js';
|
|
5
|
+
import { CopyToClipboardButton } from '../../components/buttons/index.js';
|
|
6
|
+
/** One row of the list: its label, then its value. */
|
|
7
|
+
const Row = ({ label, children, }) => (_jsxs(_Fragment, { children: [_jsx(Box, { p: 1, children: _jsx(Text, { style: { fontWeight: 'bold' }, children: label }) }), _jsx(Box, { p: 1, display: "flex", alignItems: "center", gap: 1, children: typeof children === 'string' ? _jsx(Text, { children: children }) : children })] }));
|
|
8
|
+
export const UserIdentity = ({ identity, displayId = true, displayComputed = true, avatarAction, }) => (_jsxs(Box, { children: [_jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 2, children: [_jsx(UserAvatar, { avatarUrl: identity.avatar_url, avatar: identity.avatar_icon, size: 100 }), avatarAction] }), _jsxs(Box, { display: "grid", gridTemplateColumns: "1fr 2fr", rowGap: 3, columnGap: 3, maxWidth: 500, mt: 3, children: [_jsx(Row, { label: "Username", children: identity.username }), displayComputed && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Display name", children: identity.display_name }), _jsx(Row, { label: "Initials", children: identity.initials })] })), identity.origin && _jsx(Row, { label: "Origin", children: identity.origin }), displayId && identity.id && (_jsx(Row, { label: "UID", children: _jsxs(_Fragment, { children: [_jsx(Text, { children: identity.id }), _jsx(CopyToClipboardButton, { value: identity.id, ariaLabel: "Copy the identifier of the account" })] }) }))] })] }));
|
|
9
|
+
export default UserIdentity;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
".",
|
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
"usehooks-ts": "3.1.1",
|
|
178
178
|
"uuid": "^13.0.0",
|
|
179
179
|
"validator": "^13.7.0",
|
|
180
|
+
"yaml": "^2.9.1",
|
|
180
181
|
"zod": "^4.1.13",
|
|
181
182
|
"zod-to-json-schema": "^3.25.0",
|
|
182
183
|
"zustand": "^4.4.1"
|