@datalayer/core 1.2.24 → 1.2.26

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.
@@ -12,6 +12,7 @@ export declare const ItemTypes: {
12
12
  readonly NOTEBOOK: "notebook";
13
13
  readonly LEXICAL: "document";
14
14
  readonly DECK: "deck";
15
+ readonly SPREADSHEET: "spreadsheet";
15
16
  readonly EXERCISE: "exercise";
16
17
  readonly CELL: "cell";
17
18
  readonly SPACE: "space";
@@ -16,6 +16,8 @@ export const ItemTypes = {
16
16
  NOTEBOOK: 'notebook',
17
17
  LEXICAL: 'document',
18
18
  DECK: 'deck',
19
+ // A spreadsheet drawn by `@datalayer/cells`: its content is a CellsWorkbook.
20
+ SPREADSHEET: 'spreadsheet',
19
21
  EXERCISE: 'exercise',
20
22
  CELL: 'cell',
21
23
  SPACE: 'space',
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { AlienIcon } from '@datalayer/icons-react';
3
3
  import { EntityAvatar, useColorPalette } from '@datalayer/primer-addons';
4
- import { getAvatarURL } from '../../utils/index.js';
4
+ import { getAvatarURL } from '../../utils/Avatar.js';
5
5
  import { ChosenAvatar, principalAvatarOf, } from '../principal/PrincipalAppearance.js';
6
6
  /**
7
7
  * Returns `true` when the given URL points to a real user avatar (i.e. not a
@@ -306,6 +306,12 @@ export declare const queryKeys: {
306
306
  /** Under the detail: saving the deck refreshes whether it is stale. */
307
307
  readonly video: (deckId: string) => readonly ["decks", "detail", string, "video"];
308
308
  };
309
+ readonly spreadsheets: {
310
+ readonly all: () => readonly ["spreadsheets"];
311
+ readonly details: () => readonly ["spreadsheets", "detail"];
312
+ readonly detail: (id: string) => readonly ["spreadsheets", "detail", string];
313
+ readonly bySpace: (spaceId: string) => readonly ["spreadsheets", "space", string];
314
+ };
309
315
  readonly cells: {
310
316
  readonly all: () => readonly ["cells"];
311
317
  readonly details: () => readonly ["cells", "detail"];
@@ -835,6 +841,24 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
835
841
  spec: unknown;
836
842
  }, unknown>;
837
843
  useDeleteDeckVideo: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
844
+ useSpreadsheet: (spreadsheetId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
845
+ useSpreadsheetsBySpace: (spaceId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
846
+ useCreateSpreadsheet: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
847
+ spaceId: string;
848
+ name: string;
849
+ description?: string;
850
+ workbook: unknown;
851
+ }, unknown>;
852
+ useUpdateSpreadsheet: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
853
+ id: string;
854
+ name: string;
855
+ description: string;
856
+ }, unknown>;
857
+ useUpdateSpreadsheetModel: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
858
+ id: string;
859
+ workbook: unknown;
860
+ }, unknown>;
861
+ useCloneSpreadsheet: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
838
862
  useDocument: (documentId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
839
863
  useDocumentsBySpace: (spaceId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
840
864
  useCreateDocument: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
@@ -1464,6 +1488,12 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
1464
1488
  /** Under the detail: saving the deck refreshes whether it is stale. */
1465
1489
  readonly video: (deckId: string) => readonly ["decks", "detail", string, "video"];
1466
1490
  };
1491
+ readonly spreadsheets: {
1492
+ readonly all: () => readonly ["spreadsheets"];
1493
+ readonly details: () => readonly ["spreadsheets", "detail"];
1494
+ readonly detail: (id: string) => readonly ["spreadsheets", "detail", string];
1495
+ readonly bySpace: (spaceId: string) => readonly ["spreadsheets", "space", string];
1496
+ };
1467
1497
  readonly cells: {
1468
1498
  readonly all: () => readonly ["cells"];
1469
1499
  readonly details: () => readonly ["cells", "detail"];
@@ -372,6 +372,13 @@ export const queryKeys = {
372
372
  /** Under the detail: saving the deck refreshes whether it is stale. */
373
373
  video: (deckId) => [...queryKeys.decks.detail(deckId), 'video'],
374
374
  },
375
+ // Spreadsheets: `@datalayer/cells` workbooks, kept as items of a space.
376
+ spreadsheets: {
377
+ all: () => ['spreadsheets'],
378
+ details: () => [...queryKeys.spreadsheets.all(), 'detail'],
379
+ detail: (id) => [...queryKeys.spreadsheets.details(), id],
380
+ bySpace: (spaceId) => [...queryKeys.spreadsheets.all(), 'space', spaceId],
381
+ },
375
382
  // Cells
376
383
  cells: {
377
384
  all: () => ['cells'],
@@ -1101,6 +1108,46 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1101
1108
  },
1102
1109
  };
1103
1110
  };
1111
+ /**
1112
+ * A spreadsheet: a space item whose content is a `@datalayer/cells`
1113
+ * workbook (`CellsWorkbook`), written through the documents routes the way
1114
+ * a Studio app's definition is. `workbook` is the parsed JSON when the
1115
+ * answer inlined it (`GET /lexicals/{uid}` does, a listing does not), and
1116
+ * `undefined` otherwise — never a guess, so an editor cannot open on a
1117
+ * listing's row and draw an empty grid it would then save.
1118
+ */
1119
+ const toSpreadsheet = (raw) => {
1120
+ let workbook = undefined;
1121
+ if (typeof raw.model_s === 'string' && raw.model_s.trim()) {
1122
+ workbook = JSON.parse(raw.model_s);
1123
+ }
1124
+ return {
1125
+ id: raw.uid,
1126
+ type: 'spreadsheet',
1127
+ name: raw.name_t,
1128
+ description: raw.description_t,
1129
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
1130
+ workbook,
1131
+ public: raw.is_public_b ?? false,
1132
+ creationDate: raw.creation_ts_dt
1133
+ ? new Date(raw.creation_ts_dt)
1134
+ : undefined,
1135
+ lastUpdateDate: raw.last_update_ts_dt
1136
+ ? new Date(raw.last_update_ts_dt)
1137
+ : undefined,
1138
+ lastPublicationDate: raw.published_ts_dt
1139
+ ? new Date(raw.published_ts_dt)
1140
+ : undefined,
1141
+ owner: toItemOwner(raw),
1142
+ space: {
1143
+ handle: raw.handle_s,
1144
+ },
1145
+ organization: {
1146
+ id: raw.organization_uid_s,
1147
+ handle: raw.organization_handle_s,
1148
+ },
1149
+ };
1150
+ };
1104
1151
  const toDocument = (doc) => {
1105
1152
  const owner = toItemOwner(doc);
1106
1153
  return {
@@ -1404,6 +1451,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1404
1451
  // read no type and no name, and drew itself as a nameless notebook.
1405
1452
  case 'deck':
1406
1453
  return toDeck(item);
1454
+ case 'spreadsheet':
1455
+ return toSpreadsheet(item);
1407
1456
  case 'document':
1408
1457
  return toDocument(item);
1409
1458
  case 'exercise':
@@ -2734,6 +2783,167 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2734
2783
  });
2735
2784
  };
2736
2785
  // ============================================================================
2786
+ // Spreadsheet Hooks
2787
+ // ============================================================================
2788
+ //
2789
+ // A spreadsheet is a space item of type `spreadsheet` whose content is a
2790
+ // `@datalayer/cells` workbook. It has no routes of its own: it is created,
2791
+ // read, renamed, saved and cloned through the documents routes
2792
+ // (`/lexicals`, `documentType=spreadsheet`), as a Studio app is, and Spacer
2793
+ // refuses a body that is not a workbook.
2794
+ /**
2795
+ * Get a spreadsheet by id, its workbook inlined as `workbook`.
2796
+ */
2797
+ const useSpreadsheet = (spreadsheetId) => {
2798
+ return useQuery({
2799
+ queryKey: queryKeys.spreadsheets.detail(spreadsheetId),
2800
+ queryFn: async () => {
2801
+ const resp = await requestDatalayer({
2802
+ url: `${configuration.spacerUrl}/api/spacer/v1/lexicals/${spreadsheetId}`,
2803
+ method: 'GET',
2804
+ });
2805
+ if (resp.success && resp.document) {
2806
+ if (resp.document.type_s !== 'spreadsheet') {
2807
+ throw new Error('This item is not a spreadsheet.');
2808
+ }
2809
+ return toSpreadsheet(resp.document);
2810
+ }
2811
+ if (resp?.code === 'FORBIDDEN') {
2812
+ throw new Error(resp.message || 'Not authorized to access this spreadsheet');
2813
+ }
2814
+ throw new Error(resp.message || 'Failed to fetch spreadsheet');
2815
+ },
2816
+ ...DEFAULT_QUERY_OPTIONS,
2817
+ enabled: !!spreadsheetId,
2818
+ });
2819
+ };
2820
+ /**
2821
+ * Get the spreadsheets of a space. A row carries no workbook: the detail
2822
+ * cache is not seeded from it.
2823
+ */
2824
+ const useSpreadsheetsBySpace = (spaceId) => {
2825
+ return useQuery({
2826
+ queryKey: queryKeys.spreadsheets.bySpace(spaceId),
2827
+ queryFn: async () => {
2828
+ const resp = await requestDatalayer({
2829
+ url: `${configuration.spacerUrl}/api/spacer/v1/spaces/${spaceId}/items/types/spreadsheet`,
2830
+ method: 'GET',
2831
+ });
2832
+ if (resp.success && resp.items) {
2833
+ return resp.items.map((raw) => toSpreadsheet(raw));
2834
+ }
2835
+ return [];
2836
+ },
2837
+ ...LIST_QUERY_OPTIONS,
2838
+ enabled: !!spaceId,
2839
+ });
2840
+ };
2841
+ /**
2842
+ * Create a spreadsheet from a workbook: the starter one, or one the
2843
+ * person brings. Sent as the uploaded file, which Spacer checks.
2844
+ */
2845
+ const useCreateSpreadsheet = () => {
2846
+ return useMutation({
2847
+ mutationFn: async ({ spaceId, name, description, workbook, }) => {
2848
+ const formData = new FormData();
2849
+ formData.append('spaceId', spaceId);
2850
+ formData.append('documentType', 'spreadsheet');
2851
+ formData.append('name', name);
2852
+ formData.append('description', description || '');
2853
+ formData.append('file', new File([JSON.stringify(workbook)], `${name}.cells.json`, {
2854
+ type: 'application/json',
2855
+ }));
2856
+ return uploadDocument(formData);
2857
+ },
2858
+ onSuccess: resp => {
2859
+ if (resp.success && resp.document) {
2860
+ // The answer carries no model: the editor reads the spreadsheet
2861
+ // from the server rather than from a seeded, empty detail.
2862
+ queryClient.invalidateQueries({
2863
+ queryKey: queryKeys.spreadsheets.detail(resp.document.uid),
2864
+ });
2865
+ queryClient.refetchQueries({
2866
+ queryKey: queryKeys.spreadsheets.all(),
2867
+ });
2868
+ queryClient.refetchQueries({ queryKey: queryKeys.items.all() });
2869
+ }
2870
+ },
2871
+ });
2872
+ };
2873
+ /**
2874
+ * Update a spreadsheet's name and description.
2875
+ */
2876
+ const useUpdateSpreadsheet = () => {
2877
+ return useMutation({
2878
+ mutationFn: async ({ id, name, description, }) => {
2879
+ return requestDatalayer({
2880
+ url: `${configuration.spacerUrl}/api/spacer/v1/lexicals/${id}`,
2881
+ method: 'PUT',
2882
+ body: { name, description },
2883
+ });
2884
+ },
2885
+ onSuccess: (resp, spreadsheet) => {
2886
+ if (resp.success) {
2887
+ queryClient.invalidateQueries({
2888
+ queryKey: queryKeys.spreadsheets.detail(spreadsheet.id),
2889
+ });
2890
+ queryClient.invalidateQueries({
2891
+ queryKey: queryKeys.spreadsheets.all(),
2892
+ });
2893
+ }
2894
+ },
2895
+ });
2896
+ };
2897
+ /**
2898
+ * Replace a spreadsheet's workbook, whole. A refusal (a workbook Spacer
2899
+ * does not accept) is thrown, not answered as a success.
2900
+ */
2901
+ const useUpdateSpreadsheetModel = () => {
2902
+ return useMutation({
2903
+ mutationFn: async ({ id, workbook, }) => {
2904
+ const resp = await requestDatalayer({
2905
+ url: `${configuration.spacerUrl}/api/spacer/v1/lexicals/${id}/model`,
2906
+ method: 'PUT',
2907
+ body: { model: workbook },
2908
+ });
2909
+ if (!resp.success) {
2910
+ throw new Error(resp.message || 'The spreadsheet was not saved.');
2911
+ }
2912
+ return resp;
2913
+ },
2914
+ onSuccess: (_, variables) => {
2915
+ queryClient.invalidateQueries({
2916
+ queryKey: queryKeys.spreadsheets.detail(variables.id),
2917
+ });
2918
+ queryClient.invalidateQueries({
2919
+ queryKey: queryKeys.spreadsheets.all(),
2920
+ });
2921
+ },
2922
+ });
2923
+ };
2924
+ /**
2925
+ * Clone a spreadsheet into the caller's library: the reuse the library
2926
+ * counts for a published one.
2927
+ */
2928
+ const useCloneSpreadsheet = () => {
2929
+ return useMutation({
2930
+ mutationFn: async (spreadsheetId) => {
2931
+ return requestDatalayer({
2932
+ url: `${configuration.spacerUrl}/api/spacer/v1/lexicals/${spreadsheetId}/clone`,
2933
+ method: 'POST',
2934
+ });
2935
+ },
2936
+ onSuccess: resp => {
2937
+ if (resp.success && resp.document) {
2938
+ queryClient.refetchQueries({
2939
+ queryKey: queryKeys.spreadsheets.all(),
2940
+ });
2941
+ queryClient.refetchQueries({ queryKey: queryKeys.items.all() });
2942
+ }
2943
+ },
2944
+ });
2945
+ };
2946
+ // ============================================================================
2737
2947
  // Document Hooks
2738
2948
  // ============================================================================
2739
2949
  /**
@@ -3196,6 +3406,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
3196
3406
  queryKey: queryKeys.documents.detail(itemId),
3197
3407
  });
3198
3408
  queryClient.removeQueries({ queryKey: queryKeys.decks.detail(itemId) });
3409
+ queryClient.removeQueries({
3410
+ queryKey: queryKeys.spreadsheets.detail(itemId),
3411
+ });
3199
3412
  queryClient.removeQueries({ queryKey: queryKeys.cells.detail(itemId) });
3200
3413
  queryClient.removeQueries({
3201
3414
  queryKey: queryKeys.lessons.detail(itemId),
@@ -3210,6 +3423,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
3210
3423
  queryClient.invalidateQueries({ queryKey: queryKeys.notebooks.all() });
3211
3424
  queryClient.invalidateQueries({ queryKey: queryKeys.documents.all() });
3212
3425
  queryClient.invalidateQueries({ queryKey: queryKeys.decks.all() });
3426
+ queryClient.invalidateQueries({
3427
+ queryKey: queryKeys.spreadsheets.all(),
3428
+ });
3213
3429
  queryClient.invalidateQueries({ queryKey: queryKeys.cells.all() });
3214
3430
  queryClient.invalidateQueries({ queryKey: queryKeys.lessons.all() });
3215
3431
  queryClient.invalidateQueries({ queryKey: queryKeys.exercises.all() });
@@ -8692,6 +8908,13 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
8692
8908
  useSavedDeckVideo,
8693
8909
  useSaveDeckVideo,
8694
8910
  useDeleteDeckVideo,
8911
+ // Spreadsheets
8912
+ useSpreadsheet,
8913
+ useSpreadsheetsBySpace,
8914
+ useCreateSpreadsheet,
8915
+ useUpdateSpreadsheet,
8916
+ useUpdateSpreadsheetModel,
8917
+ useCloneSpreadsheet,
8695
8918
  // Documents
8696
8919
  useDocument,
8697
8920
  useDocumentsBySpace,
@@ -311,10 +311,11 @@ export function viewForRoute(route, current) {
311
311
  }
312
312
  /**
313
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
314
+ * Agentify sidebar — decks, spreadsheets (Cells), courses and what a course
315
+ * is made of. The
315
316
  * documentation (`/docs/…`) is never an account.
316
317
  */
317
- const AGENTIFY_SPACE_PAGE = /^\/(?!docs\/)[^/]+\/[^/]+\/(?:decks|courses|course|lessons|exercises|assignments)(?:\/|$)/;
318
+ const AGENTIFY_SPACE_PAGE = /^\/(?!docs\/)[^/]+\/[^/]+\/(?:decks|spreadsheets|courses|course|lessons|exercises|assignments)(?:\/|$)/;
318
319
  /** `/<account>/courses`: an account's courses, beside the Agentify sidebar. */
319
320
  const AGENTIFY_ACCOUNT_PAGE = /^\/(?!docs\/)[^/]+\/courses$/;
320
321
  /** Whether a page may be the one a shell returns to. */
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text } from '@primer/react';
3
3
  import { Box } from '@datalayer/primer-addons';
4
- import { UserAvatar } from '../../components/avatars/index.js';
5
- import { CopyToClipboardButton } from '../../components/buttons/index.js';
4
+ import { UserAvatar } from '../../components/avatars/UserAvatar.js';
5
+ import { CopyToClipboardButton } from '../../components/buttons/CopyToClipboardButton.js';
6
6
  /** One row of the list: its label, then its value. */
7
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
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" })] }) }))] })] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.2.24",
3
+ "version": "1.2.26",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",