@datalayer/core 1.1.66 → 1.2.10

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.
Files changed (210) hide show
  1. package/README.md +41 -18
  2. package/lib/api/constants.d.ts +1 -1
  3. package/lib/api/constants.js +1 -1
  4. package/lib/api/contents/attachments.js +3 -7
  5. package/lib/api/contents/datasets.d.ts +48 -0
  6. package/lib/api/contents/datasets.js +44 -7
  7. package/lib/api/contents/runtimeMounts.js +1 -1
  8. package/lib/api/contents/sandboxUid.d.ts +13 -45
  9. package/lib/api/contents/sandboxUid.js +14 -65
  10. package/lib/api/evals/client.d.ts +402 -0
  11. package/lib/api/evals/client.js +359 -0
  12. package/lib/api/evals/index.d.ts +10 -0
  13. package/lib/api/evals/index.js +18 -0
  14. package/lib/api/evals/request.d.ts +43 -0
  15. package/lib/api/evals/request.js +67 -0
  16. package/lib/api/evals/status.d.ts +27 -0
  17. package/lib/api/evals/status.js +122 -0
  18. package/lib/api/evals/types.d.ts +1335 -0
  19. package/lib/api/evals/types.js +5 -0
  20. package/lib/api/iam/connectedAgents.d.ts +29 -0
  21. package/lib/api/iam/connectedAgents.js +35 -0
  22. package/lib/api/iam/identityProviders.d.ts +76 -0
  23. package/lib/api/iam/identityProviders.js +187 -0
  24. package/lib/api/iam/index.d.ts +1 -0
  25. package/lib/api/iam/index.js +1 -0
  26. package/lib/api/iam/mcpPolicy.d.ts +102 -1
  27. package/lib/api/iam/mcpPolicy.js +38 -2
  28. package/lib/api/iam/profile.d.ts +17 -1
  29. package/lib/api/iam/profile.js +27 -0
  30. package/lib/api/iam/scimTokens.d.ts +79 -0
  31. package/lib/api/iam/scimTokens.js +141 -0
  32. package/lib/api/iam/secrets.js +9 -21
  33. package/lib/api/iam/trials.d.ts +38 -0
  34. package/lib/api/iam/trials.js +66 -0
  35. package/lib/api/index.d.ts +4 -0
  36. package/lib/api/index.js +4 -0
  37. package/lib/api/mcp/observability.d.ts +25 -9
  38. package/lib/api/mcp/observability.js +63 -50
  39. package/lib/api/orchestration/client.d.ts +53 -0
  40. package/lib/api/orchestration/client.js +105 -0
  41. package/lib/api/orchestration/events.d.ts +88 -0
  42. package/lib/api/orchestration/events.js +245 -0
  43. package/lib/api/orchestration/generated.d.ts +693 -0
  44. package/lib/api/orchestration/generated.js +687 -0
  45. package/lib/api/orchestration/index.d.ts +30 -0
  46. package/lib/api/orchestration/index.js +34 -0
  47. package/lib/api/orchestration/lifecycle.d.ts +39 -0
  48. package/lib/api/orchestration/lifecycle.js +57 -0
  49. package/lib/api/orchestration/measures.d.ts +55 -0
  50. package/lib/api/orchestration/measures.js +99 -0
  51. package/lib/api/otel/dashboards.d.ts +56 -0
  52. package/lib/api/otel/dashboards.js +50 -0
  53. package/lib/api/otel/index.d.ts +1 -0
  54. package/lib/api/otel/index.js +1 -0
  55. package/lib/api/otel/metrics.d.ts +16 -0
  56. package/lib/api/otel/metrics.js +15 -0
  57. package/lib/api/scheduler/client.d.ts +32 -0
  58. package/lib/api/scheduler/client.js +42 -0
  59. package/lib/api/scheduler/index.d.ts +8 -0
  60. package/lib/api/scheduler/index.js +16 -0
  61. package/lib/api/scheduler/request.d.ts +28 -0
  62. package/lib/api/scheduler/request.js +50 -0
  63. package/lib/api/scheduler/types.d.ts +88 -0
  64. package/lib/api/scheduler/types.js +5 -0
  65. package/lib/api/spacer/comments.d.ts +97 -0
  66. package/lib/api/spacer/comments.js +45 -0
  67. package/lib/api/spacer/index.d.ts +9 -0
  68. package/lib/api/spacer/index.js +17 -0
  69. package/lib/api/spacer/notebooks.d.ts +33 -0
  70. package/lib/api/spacer/notebooks.js +39 -0
  71. package/lib/api/spacer/request.d.ts +25 -0
  72. package/lib/api/spacer/request.js +46 -0
  73. package/lib/api/spacer/spaces.d.ts +68 -0
  74. package/lib/api/spacer/spaces.js +45 -0
  75. package/lib/api/utils/validation.d.ts +4 -4
  76. package/lib/api/utils/validation.js +10 -7
  77. package/lib/client/constants.d.ts +1 -0
  78. package/lib/client/constants.js +1 -0
  79. package/lib/components/animation/AnimatedText.d.ts +1 -44
  80. package/lib/components/animation/AnimatedText.js +6 -122
  81. package/lib/components/anonymous/AnonymousKeyExpired.d.ts +56 -0
  82. package/lib/components/anonymous/AnonymousKeyExpired.js +96 -0
  83. package/lib/components/anonymous/AnonymousKeyTimer.d.ts +63 -0
  84. package/lib/components/anonymous/AnonymousKeyTimer.js +140 -0
  85. package/lib/components/anonymous/index.d.ts +7 -0
  86. package/lib/components/anonymous/index.js +15 -0
  87. package/lib/components/avatars/UserAvatar.d.ts +7 -1
  88. package/lib/components/avatars/UserAvatar.js +15 -1
  89. package/lib/components/billing/BillingEntitySelect.d.ts +6 -0
  90. package/lib/components/billing/BillingEntitySelect.js +49 -10
  91. package/lib/components/billing/eligibility.d.ts +22 -0
  92. package/lib/components/billing/eligibility.js +27 -0
  93. package/lib/components/billing/index.d.ts +1 -0
  94. package/lib/components/billing/index.js +1 -0
  95. package/lib/components/collaboration/LiveEditorCollaborators.d.ts +5 -0
  96. package/lib/components/collaboration/LiveEditorCollaborators.js +3 -1
  97. package/lib/components/display/DatalayerBox.d.ts +2 -3
  98. package/lib/components/display/DatalayerBox.js +1 -1
  99. package/lib/components/display/NavLink.js +3 -2
  100. package/lib/components/labels/StatusLabels.d.ts +2 -10
  101. package/lib/components/labels/StatusLabels.js +2 -26
  102. package/lib/components/principal/PrincipalAppearance.d.ts +21 -42
  103. package/lib/components/principal/PrincipalAppearance.js +232 -174
  104. package/lib/components/principal/PrincipalAvatar.d.ts +8 -1
  105. package/lib/components/principal/PrincipalAvatar.js +2 -2
  106. package/lib/components/principal/PrincipalDetailsOverlay.d.ts +8 -1
  107. package/lib/components/principal/PrincipalDetailsOverlay.js +2 -2
  108. package/lib/components/screencapture/Screencapture.js +46 -3
  109. package/lib/components/sharing/ShareAccessComponent.d.ts +45 -4
  110. package/lib/components/sharing/ShareAccessComponent.js +110 -78
  111. package/lib/components/sharing/SharingEditor.d.ts +5 -1
  112. package/lib/components/sharing/SharingEditor.js +9 -4
  113. package/lib/components/sharing/index.d.ts +1 -0
  114. package/lib/components/sharing/index.js +1 -0
  115. package/lib/components/sharing/sandboxSharing.d.ts +26 -0
  116. package/lib/components/sharing/sandboxSharing.js +41 -0
  117. package/lib/components/spaces/SpaceDetailsOverlay.d.ts +39 -0
  118. package/lib/components/spaces/SpaceDetailsOverlay.js +80 -0
  119. package/lib/components/spaces/SpaceDisplay.d.ts +38 -0
  120. package/lib/components/spaces/SpaceDisplay.js +92 -0
  121. package/lib/components/spaces/index.d.ts +4 -0
  122. package/lib/components/spaces/index.js +7 -0
  123. package/lib/components/spaces/spaceDisplayModel.d.ts +93 -0
  124. package/lib/components/spaces/spaceDisplayModel.js +170 -0
  125. package/lib/hooks/cacheConverters.d.ts +0 -7
  126. package/lib/hooks/cacheConverters.js +0 -25
  127. package/lib/hooks/index.d.ts +1 -0
  128. package/lib/hooks/index.js +1 -0
  129. package/lib/hooks/useBillingEntityStore.js +3 -0
  130. package/lib/hooks/useCache.d.ts +168 -25
  131. package/lib/hooks/useCache.js +1424 -307
  132. package/lib/hooks/useContents.d.ts +22 -1
  133. package/lib/hooks/useContents.js +90 -21
  134. package/lib/hooks/useMcp.d.ts +87 -1
  135. package/lib/hooks/useMcp.js +247 -12
  136. package/lib/hooks/useNavigate.d.ts +41 -3
  137. package/lib/hooks/useNavigate.js +106 -52
  138. package/lib/hooks/usePrincipalCacheStore.js +3 -0
  139. package/lib/hooks/usePrincipalStore.js +3 -0
  140. package/lib/hooks/useSpaceCacheStore.d.ts +50 -0
  141. package/lib/hooks/useSpaceCacheStore.js +85 -0
  142. package/lib/models/CreditsDTO.d.ts +6 -5
  143. package/lib/models/CreditsDTO.js +7 -5
  144. package/lib/models/McpBinding.d.ts +6 -0
  145. package/lib/models/Organization.d.ts +69 -0
  146. package/lib/models/Organization.js +96 -0
  147. package/lib/models/Secret.d.ts +1 -2
  148. package/lib/models/Secret.js +2 -18
  149. package/lib/models/StartedBy.d.ts +34 -0
  150. package/lib/models/StartedBy.js +36 -0
  151. package/lib/models/Team.d.ts +2 -0
  152. package/lib/models/Team.js +2 -0
  153. package/lib/models/User.d.ts +4 -0
  154. package/lib/models/User.js +3 -0
  155. package/lib/models/UserSettings.d.ts +13 -0
  156. package/lib/models/UserSettings.js +9 -0
  157. package/lib/models/contents/__fixtures__/v1-contracts.json +158 -30
  158. package/lib/navigation/components.js +2 -3
  159. package/lib/routes/publicPaths.js +4 -0
  160. package/lib/state/index.d.ts +1 -0
  161. package/lib/state/index.js +1 -0
  162. package/lib/state/sessionEnd.d.ts +55 -0
  163. package/lib/state/sessionEnd.js +162 -0
  164. package/lib/state/substates/CoreState.js +3 -3
  165. package/lib/state/substates/IAMState.js +7 -0
  166. package/lib/state/substates/LayoutState.js +12 -0
  167. package/lib/state/substates/ProfileState.d.ts +1 -0
  168. package/lib/state/substates/ProfileState.js +1 -0
  169. package/lib/utils/Lazy.d.ts +20 -0
  170. package/lib/utils/Lazy.js +32 -6
  171. package/lib/utils/Screencapture.js +3 -0
  172. package/lib/utils/WithSuspense.d.ts +11 -1
  173. package/lib/utils/WithSuspense.js +11 -1
  174. package/lib/views/mcp/AdmittedClients.d.ts +44 -0
  175. package/lib/views/mcp/AdmittedClients.js +59 -0
  176. package/lib/views/mcp/ApprovalQueue.d.ts +66 -0
  177. package/lib/views/mcp/ApprovalQueue.js +95 -0
  178. package/lib/views/mcp/ConnectedAgents.js +38 -3
  179. package/lib/views/mcp/EnterpriseConsole.d.ts +1 -1
  180. package/lib/views/mcp/EnterpriseConsole.js +23 -1
  181. package/lib/views/mcp/IdentityProviders.d.ts +33 -0
  182. package/lib/views/mcp/IdentityProviders.js +299 -0
  183. package/lib/views/mcp/McpDashboard.js +90 -7
  184. package/lib/views/mcp/McpObservability.d.ts +30 -1
  185. package/lib/views/mcp/McpObservability.js +105 -19
  186. package/lib/views/mcp/NotebookRuns.d.ts +39 -0
  187. package/lib/views/mcp/NotebookRuns.js +44 -0
  188. package/lib/views/mcp/OrganizationPolicy.js +2 -1
  189. package/lib/views/mcp/PersonalPolicy.js +2 -1
  190. package/lib/views/mcp/PolicyForm.d.ts +17 -1
  191. package/lib/views/mcp/PolicyForm.js +25 -3
  192. package/lib/views/mcp/PolicyHistory.d.ts +13 -0
  193. package/lib/views/mcp/PolicyHistory.js +14 -1
  194. package/lib/views/mcp/RunDetail.d.ts +87 -0
  195. package/lib/views/mcp/RunDetail.js +186 -0
  196. package/lib/views/mcp/ScimProvisioning.d.ts +54 -0
  197. package/lib/views/mcp/ScimProvisioning.js +173 -0
  198. package/lib/views/mcp/TeamPolicies.js +66 -5
  199. package/lib/views/mcp/ToolAccess.d.ts +60 -0
  200. package/lib/views/mcp/ToolAccess.js +101 -0
  201. package/lib/views/mcp/TraceTimeline.d.ts +81 -0
  202. package/lib/views/mcp/TraceTimeline.js +132 -0
  203. package/lib/views/mcp/index.d.ts +5 -0
  204. package/lib/views/mcp/index.js +5 -0
  205. package/lib/views/otel/simpleAuthStore.js +3 -0
  206. package/lib/views/secrets/SecretEdit.js +3 -2
  207. package/lib/views/secrets/SecretNew.js +5 -1
  208. package/package.json +269 -264
  209. package/scripts/generate-mcp-types.py +1 -1
  210. package/scripts/generate-orchestration-types.py +920 -0
@@ -38,13 +38,14 @@
38
38
  */
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
- import { asPage, asSpace } from './cacheConverters.js';
41
+ import { asSpace } from './cacheConverters.js';
42
42
  import { useCoreStore, useIAMStore, profileStore } from '../state/index.js';
43
43
  import { asDisplayName, namesAsInitials, asArray } from '../utils/index.js';
44
44
  import { useDatalayer } from './useDatalayer.js';
45
45
  import { useAuthorization } from './useAuthorization.js';
46
46
  import { useUploadForm } from './useUpload.js';
47
47
  import { OUTPUTSHOT_PLACEHOLDER_DEFAULT_SVG } from './assets/index.js';
48
+ import { forgetSessionState } from '../state/sessionEnd.js';
48
49
  // Kept for potential future use
49
50
  // Default query options for all queries
50
51
  const DEFAULT_QUERY_OPTIONS = {
@@ -57,6 +58,53 @@ const DEFAULT_QUERY_OPTIONS = {
57
58
  // Ensure queries prioritize cache over network when data is fresh
58
59
  networkMode: 'online',
59
60
  };
61
+ /**
62
+ * Refresh what a visitor sees, not just what the editor is looking at.
63
+ *
64
+ * `invalidateQueries` marks queries stale and refetches the **active** ones —
65
+ * the ones some mounted component is watching. Everything else is left to
66
+ * refetch when it is next mounted, which is the usual and correct behaviour
67
+ * and does not work here.
68
+ *
69
+ * It does not work because the app's `QueryClient` is built with
70
+ * `refetchOnMount: false`. A query that mounts holding stale data renders it
71
+ * and asks for nothing. So an administrator on `/admin/library/featured`
72
+ * would reorder the ribbon, watch the admin list reorder itself correctly —
73
+ * that query is active — and then find the home page still showing the old
74
+ * order, because the ribbon was not mounted when the invalidation happened
75
+ * and would not ask again when it was. Only a full page reload, which throws
76
+ * the whole cache away, put it right.
77
+ *
78
+ * `refetchType: 'all'` fetches the inactive ones too, there and then. It
79
+ * belongs on the handful of mutations that change what somebody *else's*
80
+ * screen shows, and nowhere else: for an ordinary edit, refetching every
81
+ * dormant query in the cache is a burst of requests for pages nobody is on.
82
+ */
83
+ const invalidateEverywhere = (client, queryKey) => {
84
+ void client.invalidateQueries({ queryKey, refetchType: 'all' });
85
+ };
86
+ /**
87
+ * Forget everything a session was told, without orphaning what is on screen.
88
+ *
89
+ * `queryClient.clear()` is the obvious way to end a session and the wrong
90
+ * one. It destroys the query objects themselves, and an observer whose query
91
+ * is destroyed while its fetch is in flight is never answered and never asks
92
+ * again: it sits at `isLoading` for as long as the page stays open.
93
+ *
94
+ * Signing out is exactly that race. Ending the session swaps the whole tree
95
+ * for the anonymous one, which mounts the home page and starts fetching the
96
+ * featured ribbon; the logout response then lands on top of that fetch. The
97
+ * ribbon stayed a row of empty placeholders until the reader reloaded.
98
+ *
99
+ * So: drop what nobody is looking at, and reset what is on screen. A reset
100
+ * forgets the answer the person who has left was given and asks the question
101
+ * again as whoever is here now, which is what signing out means for a page
102
+ * that is still open.
103
+ */
104
+ export const forgetSession = (client) => {
105
+ client.removeQueries({ type: 'inactive' });
106
+ client.resetQueries({ type: 'active' });
107
+ };
60
108
  // ============================================================================
61
109
  // Query Key Factories
62
110
  // ============================================================================
@@ -132,14 +180,26 @@ export const queryKeys = {
132
180
  mcpCall: (sessionUid, callUid) => [...queryKeys.contents.mcpCalls(sessionUid), callUid],
133
181
  mcpApprovals: () => [...queryKeys.contents.all(), 'mcp-approvals'],
134
182
  mcpApprovalList: (filters) => [...queryKeys.contents.mcpApprovals(), 'list', filters ?? {}],
135
- datasourceSchema: (sourceUid) => [...queryKeys.contents.source(sourceUid), 'datasource', 'schema'],
136
- datasourceCapabilities: (sourceUid) => [...queryKeys.contents.source(sourceUid), 'datasource', 'capabilities'],
183
+ datasourceSchema: (sourceUid) => [
184
+ ...queryKeys.contents.source(sourceUid),
185
+ 'datasource',
186
+ 'schema',
187
+ ],
188
+ datasourceCapabilities: (sourceUid) => [
189
+ ...queryKeys.contents.source(sourceUid),
190
+ 'datasource',
191
+ 'capabilities',
192
+ ],
137
193
  datasourceQueries: (sourceUid) => [...queryKeys.contents.source(sourceUid), 'queries'],
138
194
  // A query is addressed by its own uid, not under its source: a
139
195
  // notebook reconnects to one by uid alone.
140
196
  queries: () => [...queryKeys.contents.all(), 'queries'],
141
197
  query: (queryUid) => [...queryKeys.contents.queries(), queryUid],
142
- dataserverStatus: (sourceUid) => [...queryKeys.contents.source(sourceUid), 'dataserver', 'status'],
198
+ dataserverStatus: (sourceUid) => [
199
+ ...queryKeys.contents.source(sourceUid),
200
+ 'dataserver',
201
+ 'status',
202
+ ],
143
203
  },
144
204
  // The Jupyter MCP Server: what the agents did, and what they may do
145
205
  mcp: {
@@ -147,7 +207,12 @@ export const queryKeys = {
147
207
  tasks: () => [...queryKeys.mcp.all(), 'tasks'],
148
208
  taskList: (filters) => [...queryKeys.mcp.tasks(), 'list', filters ?? {}],
149
209
  task: (taskUid) => [...queryKeys.mcp.tasks(), taskUid],
150
- notebookTasks: (notebookUid, filters) => [...queryKeys.mcp.tasks(), 'notebook', notebookUid, filters ?? {}],
210
+ notebookTasks: (notebookUid, filters) => [
211
+ ...queryKeys.mcp.tasks(),
212
+ 'notebook',
213
+ notebookUid,
214
+ filters ?? {},
215
+ ],
151
216
  bindings: () => [...queryKeys.mcp.all(), 'bindings'],
152
217
  bindingList: (filters) => [...queryKeys.mcp.bindings(), 'list', filters ?? {}],
153
218
  activity: (filters) => [...queryKeys.mcp.all(), 'activity', filters ?? {}],
@@ -163,6 +228,12 @@ export const queryKeys = {
163
228
  // shared entry would show one under the other's name.
164
229
  policyLayer: (scope, subjectUid) => [...queryKeys.mcp.all(), 'policy-layer', scope, subjectUid],
165
230
  alertRules: (orgUid) => [...queryKeys.mcp.all(), 'alert-rules', orgUid],
231
+ identityProviders: (orgUid) => [...queryKeys.mcp.all(), 'identity-providers', orgUid],
232
+ scimTokens: (orgUid) => [...queryKeys.mcp.all(), 'scim-tokens', orgUid],
233
+ // Keyed by scope *and* subject, like `policyLayer`: an organization's
234
+ // admitted clients and a team's are different lists, and a shared entry
235
+ // would show one under the other's name.
236
+ admittedClients: (scope, subjectUid) => [...queryKeys.mcp.all(), 'admitted-clients', scope, subjectUid],
166
237
  forwarding: (orgUid) => [...queryKeys.mcp.all(), 'audit-forwarding', orgUid],
167
238
  auditSettings: (orgUid) => [...queryKeys.mcp.all(), 'audit-settings', orgUid],
168
239
  organizationTeams: (orgUid) => [...queryKeys.mcp.all(), 'organization-teams', orgUid],
@@ -177,8 +248,20 @@ export const queryKeys = {
177
248
  workflows: () => [...queryKeys.mcp.operations(), 'workflows'],
178
249
  gatewayVersion: () => [...queryKeys.mcp.all(), 'version'],
179
250
  // The Enterprise console: one answer per organization, narrowed by team.
180
- orgOverview: (orgUid, filters) => [...queryKeys.mcp.all(), 'organizations', orgUid, 'overview', filters ?? {}],
181
- orgUsage: (orgUid, filters) => [...queryKeys.mcp.all(), 'organizations', orgUid, 'usage', filters ?? {}],
251
+ orgOverview: (orgUid, filters) => [
252
+ ...queryKeys.mcp.all(),
253
+ 'organizations',
254
+ orgUid,
255
+ 'overview',
256
+ filters ?? {},
257
+ ],
258
+ orgUsage: (orgUid, filters) => [
259
+ ...queryKeys.mcp.all(),
260
+ 'organizations',
261
+ orgUid,
262
+ 'usage',
263
+ filters ?? {},
264
+ ],
182
265
  },
183
266
  // Authentication & Profile
184
267
  auth: {
@@ -262,6 +345,14 @@ export const queryKeys = {
262
345
  bySpace: (spaceId) => [...queryKeys.documents.all(), 'space', spaceId],
263
346
  model: (documentId) => [...queryKeys.documents.detail(documentId), 'model'],
264
347
  },
348
+ // Decks
349
+ decks: {
350
+ all: () => ['decks'],
351
+ details: () => [...queryKeys.decks.all(), 'detail'],
352
+ detail: (id) => [...queryKeys.decks.details(), id],
353
+ bySpace: (spaceId) => [...queryKeys.decks.all(), 'space', spaceId],
354
+ model: (deckId) => [...queryKeys.decks.detail(deckId), 'model'],
355
+ },
265
356
  // Cells
266
357
  cells: {
267
358
  all: () => ['cells'],
@@ -308,12 +399,6 @@ export const queryKeys = {
308
399
  detail: (id) => [...queryKeys.environments.details(), id],
309
400
  bySpace: (spaceId) => [...queryKeys.environments.all(), 'space', spaceId],
310
401
  },
311
- // Pages
312
- pages: {
313
- all: () => ['pages'],
314
- details: () => [...queryKeys.pages.all(), 'detail'],
315
- detail: (id) => [...queryKeys.pages.details(), id],
316
- },
317
402
  // Secrets
318
403
  secrets: {
319
404
  all: () => ['secrets'],
@@ -371,8 +456,16 @@ export const queryKeys = {
371
456
  items: {
372
457
  all: () => ['items'],
373
458
  public: () => [...queryKeys.items.all(), 'public'],
459
+ publicItem: (uid) => [...queryKeys.items.all(), 'public', uid],
374
460
  bySpace: (spaceId) => [...queryKeys.items.all(), 'space', spaceId],
375
461
  search: (opts) => [...queryKeys.items.all(), 'search', opts],
462
+ // The library: what the home page, the profile pages and "my orbits"
463
+ // read, each invalidated by the mutation that changes it.
464
+ featured: () => [...queryKeys.items.all(), 'featured'],
465
+ orbits: () => [...queryKeys.items.all(), 'orbits'],
466
+ orbiters: (uid) => [...queryKeys.items.all(), 'orbiters', uid],
467
+ derivations: (uid) => [...queryKeys.items.all(), 'derivations', uid],
468
+ byOwner: (handle) => [...queryKeys.items.all(), 'owner', handle],
376
469
  },
377
470
  // Layout
378
471
  layout: {
@@ -453,6 +546,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
453
546
  const { isLoading: notebookUploadLoading, uploadAndSubmit: uploadNotebook, progress: notebookUploadProgress, reset: resetNotebookUpload, } = useUploadForm(`${coreStore.configuration.spacerUrl}/api/spacer/v1/notebooks`);
454
547
  // Hook for document upload/creation
455
548
  const { isLoading: documentUploadLoading, uploadAndSubmit: uploadDocument, progress: documentUploadProgress, reset: resetDocumentUpload, } = useUploadForm(`${coreStore.configuration.spacerUrl}/api/spacer/v1/lexicals`);
549
+ // Hook for deck upload/creation
550
+ const { isLoading: deckUploadLoading, uploadAndSubmit: uploadDeck, progress: deckUploadProgress, reset: resetDeckUpload, } = useUploadForm(`${coreStore.configuration.spacerUrl}/api/spacer/v1/decks`);
456
551
  // ============================================================================
457
552
  // Transformation Functions (kept from original useCache)
458
553
  // Note: These functions use 'any' because they handle dynamic API responses
@@ -552,11 +647,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
552
647
  const toSpace = (spc) => {
553
648
  return asSpace(spc);
554
649
  };
555
- const toPage = (s) => {
556
- if (s) {
557
- return asPage(s);
558
- }
559
- };
560
650
  const toSecret = (s) => {
561
651
  if (s) {
562
652
  return asSecret(s);
@@ -582,6 +672,228 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
582
672
  * is available so the UI hydration hooks (e.g. `useUsersByUids`) can
583
673
  * still fill in display data downstream.
584
674
  */
675
+ /**
676
+ * What the library knows about an artifact, on top of what its own service
677
+ * stores: how many people orbit it, whether this visitor does, how often it
678
+ * has been reused, whether it is featured, and what it looks like.
679
+ *
680
+ * Applied in one place — `toItem`, the library and search path — rather than
681
+ * in each mapper: a notebook read from its space has none of this, and
682
+ * inventing zeros there would say "nobody orbits it" where the truth is
683
+ * "this was not the library answering".
684
+ */
685
+ const withLibraryMeta = (raw) => {
686
+ if (!raw || typeof raw !== 'object') {
687
+ return {};
688
+ }
689
+ const meta = {};
690
+ if (raw.orbits !== undefined)
691
+ meta.orbits = Number(raw.orbits) || 0;
692
+ if (raw.orbited !== undefined)
693
+ meta.orbited = Boolean(raw.orbited);
694
+ if (raw.clones !== undefined)
695
+ meta.clones = Number(raw.clones) || 0;
696
+ if (raw.instantiations !== undefined)
697
+ meta.instantiations = Number(raw.instantiations) || 0;
698
+ if (raw.featured !== undefined)
699
+ meta.featured = Boolean(raw.featured);
700
+ if (raw.featuredRank !== undefined)
701
+ meta.featuredRank = Number(raw.featuredRank);
702
+ if (raw.reuse !== undefined)
703
+ meta.reuse = raw.reuse;
704
+ if (raw.publisher !== undefined)
705
+ meta.publisher = raw.publisher;
706
+ if (raw.image !== undefined)
707
+ meta.image = raw.image;
708
+ /*
709
+ * Where the artifact can be opened for editing.
710
+ *
711
+ * An item is a nested child of its space and carries no handle of its
712
+ * own, so the library resolves the space when it projects one. Without
713
+ * these two a page can link to the public reading of an artifact and has
714
+ * no way to offer the editable one.
715
+ */
716
+ if (raw.space_handle_s)
717
+ meta.spaceHandle = raw.space_handle_s;
718
+ if (raw.space_owner_handle_s)
719
+ meta.spaceOwnerHandle = raw.space_owner_handle_s;
720
+ /*
721
+ * The absolute address anyone can read it at.
722
+ *
723
+ * Built by the library, not here, because it needs the host the site is
724
+ * served from and the browser only knows the host it happens to be on —
725
+ * which on a development server is `localhost:3063`, the one address that
726
+ * is no use to whoever the link is being copied for.
727
+ */
728
+ if (raw.publicUrl)
729
+ meta.publicUrl = raw.publicUrl;
730
+ return meta;
731
+ };
732
+ /**
733
+ * A published dataset. Keyed on the dataset it is a publication of, not on
734
+ * the publication: republishing mints a new immutable revision, and the
735
+ * library entry — with the orbits it has collected — is the dataset's.
736
+ */
737
+ const toDataset = (raw) => {
738
+ const owner = toItemOwner(raw);
739
+ return {
740
+ id: raw.uid,
741
+ type: 'dataset',
742
+ name: raw.name_t,
743
+ description: raw.description_t,
744
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
745
+ public: raw.is_public_b ?? true,
746
+ license: raw.license_s,
747
+ fileCount: raw.file_count_i,
748
+ totalSize: raw.total_size_l,
749
+ publicationId: raw.latest_publication_uid_s,
750
+ revisionId: raw.revision_uid_s,
751
+ creationDate: raw.creation_ts_dt
752
+ ? new Date(raw.creation_ts_dt)
753
+ : undefined,
754
+ owner,
755
+ };
756
+ };
757
+ /**
758
+ * An agent as a library artifact: published by Datalayer rather than by a
759
+ * user, and reused by instantiating a runtime from its specification rather
760
+ * than by cloning a document. The specification itself lives in the
761
+ * application's own build; this is only what the library shows of it.
762
+ */
763
+ /**
764
+ * A published Data Server: its catalog, never its address.
765
+ *
766
+ * What the library holds is what it answers for — the connector kinds and
767
+ * the relations they name. Reusing one attaches it as a Datasource; there
768
+ * is nothing to copy, because a Data Server is a process somebody runs.
769
+ */
770
+ const toDataserver = (raw) => {
771
+ const owner = toItemOwner(raw);
772
+ return {
773
+ id: raw.uid,
774
+ type: 'dataserver',
775
+ name: raw.name_t,
776
+ description: raw.description_t,
777
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
778
+ public: raw.is_public_b ?? true,
779
+ connectors: Array.isArray(raw.connectors_ss) ? raw.connectors_ss : [],
780
+ relations: Array.isArray(raw.relations_ss) ? raw.relations_ss : [],
781
+ publicationId: raw.latest_publication_uid_s,
782
+ creationDate: raw.creation_ts_dt
783
+ ? new Date(raw.creation_ts_dt)
784
+ : undefined,
785
+ owner,
786
+ };
787
+ };
788
+ const toAgent = (raw) => {
789
+ return {
790
+ id: raw.uid,
791
+ type: 'agent',
792
+ name: raw.name_t,
793
+ description: raw.description_t,
794
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
795
+ public: raw.is_public_b ?? true,
796
+ domain: raw.domain_s,
797
+ icon: raw.icon_s,
798
+ emoji: raw.emoji_s,
799
+ model: raw.model_s,
800
+ specId: String(raw.uid || '').replace(/^agent:/, ''),
801
+ specVersion: raw.spec_version_s,
802
+ publisher: raw.publisher_s || 'datalayer',
803
+ creationDate: raw.creation_ts_dt
804
+ ? new Date(raw.creation_ts_dt)
805
+ : undefined,
806
+ };
807
+ };
808
+ /**
809
+ * A published benchmark report (B5-01): what it is about and where it
810
+ * stands, and the document it is written in, which the library reads for it
811
+ * and hands over as `model_s` — the field a document's reader looks for.
812
+ */
813
+ const toReport = (raw) => {
814
+ return {
815
+ id: raw.uid,
816
+ type: 'report',
817
+ name: raw.name_t,
818
+ description: raw.description_t,
819
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
820
+ public: raw.is_public_b ?? true,
821
+ state: raw.state_s,
822
+ version: raw.version_i,
823
+ runCount: raw.run_count_i,
824
+ evalsetId: raw.evalset_uid_s,
825
+ report: raw.report,
826
+ model_s: raw.model_s,
827
+ creationDate: raw.creation_ts_dt
828
+ ? new Date(raw.creation_ts_dt)
829
+ : undefined,
830
+ lastPublicationDate: raw.published_ts_dt
831
+ ? new Date(raw.published_ts_dt)
832
+ : undefined,
833
+ owner: toItemOwner(raw),
834
+ };
835
+ };
836
+ /**
837
+ * A published investigation (B5-01): its scope, where it stands and what was
838
+ * decided, and the document it is written in, as a report's is.
839
+ */
840
+ const toInvestigation = (raw) => {
841
+ return {
842
+ id: raw.uid,
843
+ type: 'investigation',
844
+ name: raw.name_t,
845
+ description: raw.description_t,
846
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
847
+ public: raw.is_public_b ?? true,
848
+ scope: raw.scope_s,
849
+ status: raw.status_s,
850
+ evalsetId: raw.evalset_uid_s,
851
+ investigation: raw.investigation,
852
+ model_s: raw.model_s,
853
+ creationDate: raw.creation_ts_dt
854
+ ? new Date(raw.creation_ts_dt)
855
+ : undefined,
856
+ lastPublicationDate: raw.published_ts_dt
857
+ ? new Date(raw.published_ts_dt)
858
+ : undefined,
859
+ owner: toItemOwner(raw),
860
+ };
861
+ };
862
+ /** An evaluator of the platform's catalogue (B5-01), published as an agent is. */
863
+ const toEvaluator = (raw) => {
864
+ return {
865
+ id: raw.uid,
866
+ type: 'evaluator',
867
+ name: raw.name_t,
868
+ description: raw.description_t,
869
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
870
+ public: raw.is_public_b ?? true,
871
+ specId: String(raw.uid || '').replace(/^evaluator:/, ''),
872
+ specVersion: raw.spec_version_s,
873
+ publisher: raw.publisher_s || 'datalayer',
874
+ creationDate: raw.creation_ts_dt
875
+ ? new Date(raw.creation_ts_dt)
876
+ : undefined,
877
+ };
878
+ };
879
+ /**
880
+ * A card of the site's own in the featured ribbon — the run the home page is
881
+ * written about, say. The library knows its name and whether it is
882
+ * featured; what it draws and where it leads are in the application's build,
883
+ * keyed by `cardId`.
884
+ */
885
+ const toSiteCard = (raw) => {
886
+ return {
887
+ id: raw.uid,
888
+ type: 'sitecard',
889
+ name: raw.name_t,
890
+ description: raw.description_t,
891
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
892
+ public: raw.is_public_b ?? true,
893
+ cardId: String(raw.uid || '').replace(/^sitecard:/, ''),
894
+ publisher: raw.publisher_s || 'datalayer',
895
+ };
896
+ };
585
897
  const toItemOwner = (raw) => {
586
898
  const uid = String(raw?.creator_uid_s ??
587
899
  raw?.creator_uid ??
@@ -593,6 +905,15 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
593
905
  const lastName = String(raw?.creator_last_name_t ?? raw?.last_name_t ?? '').trim();
594
906
  const email = String(raw?.creator_email_s ?? raw?.email_s ?? '').trim();
595
907
  const displayName = firstName || lastName ? asDisplayName(firstName, lastName) : handle;
908
+ /*
909
+ * The face, which the library has been projecting all along and nothing
910
+ * was reading. Without it every published artifact was attributed to a
911
+ * pair of initials, on its own public page as much as anywhere else —
912
+ * the owner is right there in the projection, and this is the one place
913
+ * it was being dropped.
914
+ */
915
+ const avatarUrl = String(raw?.creator_avatar_url_s ?? raw?.avatar_url_s ?? '').trim();
916
+ const avatarIcon = String(raw?.creator_avatar_icon_s ?? raw?.avatar_icon_s ?? '').trim();
596
917
  return {
597
918
  id: uid,
598
919
  handle,
@@ -601,6 +922,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
601
922
  lastName,
602
923
  initials: namesAsInitials(firstName, lastName),
603
924
  displayName,
925
+ avatarUrl: avatarUrl || undefined,
926
+ avatarIcon: avatarIcon || undefined,
604
927
  roles: [],
605
928
  iamProviders: [],
606
929
  setRoles: () => { },
@@ -619,6 +942,17 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
619
942
  type: 'cell',
620
943
  name: cl.name_t,
621
944
  description: cl.description_t,
945
+ /*
946
+ * The words its owner filed it under.
947
+ *
948
+ * Every artifact type the library holds carries `tags_ss`, and the six
949
+ * space items — notebook, document, cell, lesson, exercise, assignment
950
+ * — were the six that never read it. So a notebook could be published
951
+ * under tags, be found by them in a search, and arrive at the card that
952
+ * shows them with none: the field was dropped here, one layer below
953
+ * anything that could notice.
954
+ */
955
+ tags: Array.isArray(cl.tags_ss) ? cl.tags_ss : [],
622
956
  source: cl.source_t,
623
957
  creationDate: new Date(cl.creation_ts_dt),
624
958
  public: cl.is_public_b ?? false,
@@ -644,6 +978,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
644
978
  type: 'notebook',
645
979
  name: raw_notebook.name_t,
646
980
  description: raw_notebook.description_t,
981
+ tags: Array.isArray(raw_notebook.tags_ss) ? raw_notebook.tags_ss : [],
647
982
  nbformat: raw_notebook.model_s
648
983
  ? JSON.parse(raw_notebook.model_s)
649
984
  : undefined,
@@ -666,6 +1001,49 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
666
1001
  },
667
1002
  };
668
1003
  };
1004
+ /**
1005
+ * Put a deck in the detail cache — but only a whole one.
1006
+ *
1007
+ * A listing answers with the space's items, and an item carries no
1008
+ * `model_s`: the specification lives in S3 and only `GET /decks/{uid}`
1009
+ * inlines it. Seeding the detail cache from such a row left `useDeck`
1010
+ * answering instantly with a deck whose `spec` is undefined — and since
1011
+ * these queries stay fresh for five minutes and do not refetch on mount,
1012
+ * an editor opened from the table drew nothing until the tab was reloaded.
1013
+ * A deck without its specification is not the deck this cache promises.
1014
+ */
1015
+ const seedDeckDetail = (deck) => {
1016
+ if (deck?.spec) {
1017
+ queryClient.setQueryData(queryKeys.decks.detail(deck.id), deck);
1018
+ }
1019
+ };
1020
+ const toDeck = (raw) => {
1021
+ const owner = toItemOwner(raw);
1022
+ return {
1023
+ id: raw.uid,
1024
+ type: 'deck',
1025
+ name: raw.name_t,
1026
+ description: raw.description_t,
1027
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
1028
+ spec: raw.model_s ? JSON.parse(raw.model_s) : undefined,
1029
+ public: raw.is_public_b ?? false,
1030
+ creationDate: new Date(raw.creation_ts_dt),
1031
+ lastUpdateDate: raw.last_update_ts_dt
1032
+ ? new Date(raw.last_update_ts_dt)
1033
+ : undefined,
1034
+ lastPublicationDate: raw.creation_ts_dt
1035
+ ? new Date(raw.creation_ts_dt)
1036
+ : undefined,
1037
+ owner,
1038
+ space: {
1039
+ handle: raw.handle_s,
1040
+ },
1041
+ organization: {
1042
+ id: raw.organization_uid_s,
1043
+ handle: raw.organization_handle_s,
1044
+ },
1045
+ };
1046
+ };
669
1047
  const toDocument = (doc) => {
670
1048
  const owner = toItemOwner(doc);
671
1049
  return {
@@ -673,6 +1051,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
673
1051
  type: 'document',
674
1052
  name: doc.name_t,
675
1053
  description: doc.description_t,
1054
+ tags: Array.isArray(doc.tags_ss) ? doc.tags_ss : [],
676
1055
  model: doc.model_s ? JSON.parse(doc.model_s) : undefined,
677
1056
  public: doc.is_public_b ?? false,
678
1057
  creationDate: new Date(doc.creation_ts_dt),
@@ -699,6 +1078,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
699
1078
  type: 'lesson',
700
1079
  name: raw_lesson.name_t,
701
1080
  description: raw_lesson.description_t,
1081
+ tags: Array.isArray(raw_lesson.tags_ss) ? raw_lesson.tags_ss : [],
702
1082
  nbformat: raw_lesson.model_s ? JSON.parse(raw_lesson.model_s) : undefined,
703
1083
  public: raw_lesson.is_public_b ?? false,
704
1084
  creationDate: new Date(raw_lesson.creation_ts_dt),
@@ -726,6 +1106,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
726
1106
  type: 'exercise',
727
1107
  name: ex.name_t,
728
1108
  description: ex.description_t,
1109
+ tags: Array.isArray(ex.tags_ss) ? ex.tags_ss : [],
729
1110
  help: ex.help_t,
730
1111
  codePre: ex.code_pre_t,
731
1112
  codeQuestion: ex.code_question_t,
@@ -771,6 +1152,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
771
1152
  type: 'assignment',
772
1153
  name: raw_assignment.name_t,
773
1154
  description: raw_assignment.description_t,
1155
+ tags: Array.isArray(raw_assignment.tags_ss) ? raw_assignment.tags_ss : [],
774
1156
  nbformat: raw_assignment.model_s
775
1157
  ? JSON.parse(raw_assignment.model_s)
776
1158
  : undefined,
@@ -818,6 +1200,13 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
818
1200
  };
819
1201
  const toEvalset = (raw_evalset) => {
820
1202
  const owner = toItemOwner(raw_evalset);
1203
+ const numberOrUndefined = (value) => {
1204
+ if (value === null || value === undefined || value === '') {
1205
+ return undefined;
1206
+ }
1207
+ const parsed = Number(value);
1208
+ return Number.isFinite(parsed) ? parsed : undefined;
1209
+ };
821
1210
  return {
822
1211
  id: raw_evalset.uid,
823
1212
  type: 'evalset',
@@ -831,6 +1220,70 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
831
1220
  ? new Date(raw_evalset.last_update_ts_dt)
832
1221
  : undefined,
833
1222
  tags: Array.isArray(raw_evalset.tags_ss) ? raw_evalset.tags_ss : [],
1223
+ /*
1224
+ * What the runs of a benchmark say about it, projected onto its
1225
+ * library card whenever a run moves (BENCHMARK.md, B2-10 and B1-09).
1226
+ *
1227
+ * These were dropped here, so a card in the Library or on the home
1228
+ * page could show a name and a description and nothing that would
1229
+ * help anybody choose between two benchmarks — how big it is, what it
1230
+ * has been run on, how well anyone has done, what a run tends to
1231
+ * cost. The projection has been writing them all along.
1232
+ */
1233
+ category: raw_evalset.category_s || undefined,
1234
+ caseCount: numberOrUndefined(raw_evalset.case_count_i),
1235
+ runCount: numberOrUndefined(raw_evalset.run_count_i),
1236
+ latestPassRate: numberOrUndefined(raw_evalset.latest_pass_rate_f),
1237
+ bestPassRate: numberOrUndefined(raw_evalset.best_pass_rate_f),
1238
+ lastRunStatus: raw_evalset.last_run_status_s || undefined,
1239
+ lastRunAt: raw_evalset.last_run_at_dt
1240
+ ? new Date(raw_evalset.last_run_at_dt)
1241
+ : undefined,
1242
+ estimatedCost: numberOrUndefined(raw_evalset.estimated_cost_f),
1243
+ environment: raw_evalset.environment_s || undefined,
1244
+ subjectRefs: Array.isArray(raw_evalset.subject_refs_ss)
1245
+ ? raw_evalset.subject_refs_ss
1246
+ : [],
1247
+ owner,
1248
+ };
1249
+ };
1250
+ /**
1251
+ * A published environment version as a library artifact (PLAN_ENV E2-16).
1252
+ *
1253
+ * Keyed on the version uid, so one published version is one card, and read
1254
+ * from the publication's immutable snapshot: its name, title, README (the
1255
+ * description), the licenses its SBOM names (the tags), its size class and
1256
+ * the variants it was built for. Republishing mints a new version — and a
1257
+ * new card — rather than moving this one.
1258
+ */
1259
+ const toEnvironmentArtifact = (raw) => {
1260
+ const owner = toItemOwner(raw);
1261
+ const numberOrUndefined = (value) => {
1262
+ if (value === null || value === undefined || value === '') {
1263
+ return undefined;
1264
+ }
1265
+ const parsed = Number(value);
1266
+ return Number.isFinite(parsed) ? parsed : undefined;
1267
+ };
1268
+ return {
1269
+ id: raw.uid,
1270
+ type: 'environment',
1271
+ name: raw.name_t,
1272
+ title: raw.title_t || undefined,
1273
+ description: raw.description_t,
1274
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
1275
+ licenses: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
1276
+ public: raw.is_public_b ?? true,
1277
+ environmentUid: raw.environment_uid_s || undefined,
1278
+ sizeClass: raw.size_class_s || undefined,
1279
+ variants: Array.isArray(raw.variants_ss) ? raw.variants_ss : [],
1280
+ versionNumber: numberOrUndefined(raw.version_number_i),
1281
+ creationDate: raw.creation_ts_dt
1282
+ ? new Date(raw.creation_ts_dt)
1283
+ : undefined,
1284
+ lastUpdateDate: raw.source_update_ts_dt
1285
+ ? new Date(raw.source_update_ts_dt)
1286
+ : undefined,
834
1287
  owner,
835
1288
  };
836
1289
  };
@@ -839,6 +1292,13 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
839
1292
  console.error('No type_s found on item', item);
840
1293
  return {};
841
1294
  }
1295
+ // The library answers with the same field names a space item has, plus
1296
+ // what only it knows; the type mapper reads the former and
1297
+ // `withLibraryMeta` adds the latter, so one artifact has one shape
1298
+ // wherever it was searched.
1299
+ return { ...toTypedItem(item), ...withLibraryMeta(item) };
1300
+ };
1301
+ const toTypedItem = (item) => {
842
1302
  switch (item.type_s) {
843
1303
  case 'assignment':
844
1304
  return toAssignment(item);
@@ -883,6 +1343,10 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
883
1343
  },
884
1344
  };
885
1345
  }
1346
+ // Without this the mapper answered `{}` for a deck: the card then
1347
+ // read no type and no name, and drew itself as a nameless notebook.
1348
+ case 'deck':
1349
+ return toDeck(item);
886
1350
  case 'document':
887
1351
  return toDocument(item);
888
1352
  case 'exercise':
@@ -891,10 +1355,24 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
891
1355
  return toLesson(item);
892
1356
  case 'notebook':
893
1357
  return toNotebook(item);
894
- case 'page':
895
- return toPage(item);
896
1358
  case 'evalset':
897
1359
  return toEvalset(item);
1360
+ case 'environment':
1361
+ return toEnvironmentArtifact(item);
1362
+ case 'dataset':
1363
+ return toDataset(item);
1364
+ case 'agent':
1365
+ return toAgent(item);
1366
+ case 'dataserver':
1367
+ return toDataserver(item);
1368
+ case 'report':
1369
+ return toReport(item);
1370
+ case 'investigation':
1371
+ return toInvestigation(item);
1372
+ case 'evaluator':
1373
+ return toEvaluator(item);
1374
+ case 'sitecard':
1375
+ return toSiteCard(item);
898
1376
  default:
899
1377
  return {};
900
1378
  }
@@ -1020,8 +1498,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1020
1498
  });
1021
1499
  },
1022
1500
  onSuccess: () => {
1023
- // Clear all queries on logout
1024
- queryClient.clear();
1501
+ forgetSession(queryClient);
1502
+ forgetSessionState();
1025
1503
  },
1026
1504
  });
1027
1505
  };
@@ -1058,11 +1536,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1058
1536
  */
1059
1537
  const useUpdateMe = () => {
1060
1538
  return useMutation({
1061
- mutationFn: async ({ email, firstName, lastName, avatarIcon, banner, }) => {
1539
+ mutationFn: async ({ email, firstName, lastName, avatarIcon, banner, showOrbits, }) => {
1062
1540
  return requestDatalayer({
1063
1541
  url: `${configuration.iamUrl}/api/iam/v1/me`,
1064
1542
  method: 'PUT',
1065
- body: { email, firstName, lastName, avatarIcon, banner },
1543
+ body: { email, firstName, lastName, avatarIcon, banner, showOrbits },
1066
1544
  });
1067
1545
  },
1068
1546
  onSuccess: () => {
@@ -1296,19 +1774,33 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1296
1774
  const useUpdateUserSettings = () => {
1297
1775
  return useMutation({
1298
1776
  mutationFn: async ({ userId, settings, }) => {
1777
+ // Only the keys the caller actually set. IAM merges what it
1778
+ // receives into the settings document, so sending a key means
1779
+ // changing it — an undefined one is dropped by `JSON.stringify`
1780
+ // and leaves the stored value alone.
1781
+ const body = {};
1782
+ if (settings.aiAgentsUrl !== undefined) {
1783
+ body.aiAgents_url_s = settings.aiAgentsUrl;
1784
+ }
1785
+ if (settings.canInvite !== undefined) {
1786
+ body.can_invite_b = settings.canInvite;
1787
+ }
1788
+ if (settings.docsInPlace !== undefined) {
1789
+ body.docs_in_place_b = settings.docsInPlace;
1790
+ }
1299
1791
  return requestDatalayer({
1300
1792
  url: `${configuration.iamUrl}/api/iam/v1/users/${userId}/settings`,
1301
1793
  method: 'PUT',
1302
- body: {
1303
- aiAgents_url_s: settings.aiAgentsUrl,
1304
- can_invite_b: settings.canInvite || false,
1305
- },
1794
+ body,
1306
1795
  });
1307
1796
  },
1308
1797
  onSuccess: (_, variables) => {
1309
1798
  queryClient.invalidateQueries({
1310
1799
  queryKey: queryKeys.users.detail(variables.userId),
1311
1800
  });
1801
+ // The signed-in user carries these settings, and the application
1802
+ // shell reads them from there.
1803
+ queryClient.invalidateQueries({ queryKey: queryKeys.auth.me() });
1312
1804
  },
1313
1805
  });
1314
1806
  };
@@ -1938,6 +2430,148 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1938
2430
  },
1939
2431
  });
1940
2432
  };
2433
+ /**
2434
+ * Get a deck by id, its specification inlined as `spec`.
2435
+ */
2436
+ const useDeck = (deckId) => {
2437
+ return useQuery({
2438
+ queryKey: queryKeys.decks.detail(deckId),
2439
+ queryFn: async () => {
2440
+ const resp = await requestDatalayer({
2441
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${deckId}`,
2442
+ method: 'GET',
2443
+ });
2444
+ if (resp.success && resp.deck) {
2445
+ return toDeck(resp.deck);
2446
+ }
2447
+ if (resp?.code === 'FORBIDDEN') {
2448
+ throw new Error(resp.message || 'Not authorized to access this deck');
2449
+ }
2450
+ throw new Error(resp.message || 'Failed to fetch deck');
2451
+ },
2452
+ ...DEFAULT_QUERY_OPTIONS,
2453
+ enabled: !!deckId,
2454
+ });
2455
+ };
2456
+ /**
2457
+ * Get decks by space
2458
+ */
2459
+ const useDecksBySpace = (spaceId) => {
2460
+ return useQuery({
2461
+ queryKey: queryKeys.decks.bySpace(spaceId),
2462
+ queryFn: async () => {
2463
+ const resp = await requestDatalayer({
2464
+ url: `${configuration.spacerUrl}/api/spacer/v1/spaces/${spaceId}/items/types/deck`,
2465
+ method: 'GET',
2466
+ });
2467
+ if (resp.success && resp.items) {
2468
+ return resp.items.map((raw) => {
2469
+ const deck = toDeck(raw);
2470
+ seedDeckDetail(deck);
2471
+ return deck;
2472
+ });
2473
+ }
2474
+ return [];
2475
+ },
2476
+ ...DEFAULT_QUERY_OPTIONS,
2477
+ enabled: !!spaceId,
2478
+ });
2479
+ };
2480
+ /**
2481
+ * Create a deck — from an uploaded specification, or the starter one.
2482
+ */
2483
+ const useCreateDeck = () => {
2484
+ return useMutation({
2485
+ mutationFn: async ({ spaceId, name, description, deckType = 'deck', file, }) => {
2486
+ const formData = new FormData();
2487
+ formData.append('space_id', spaceId);
2488
+ // Backward compatibility for endpoints still expecting camelCase.
2489
+ formData.append('spaceId', spaceId);
2490
+ formData.append('deckType', deckType);
2491
+ formData.append('name', name);
2492
+ formData.append('description', description || '');
2493
+ if (file) {
2494
+ formData.append('file', file);
2495
+ }
2496
+ return uploadDeck(formData);
2497
+ },
2498
+ onSuccess: resp => {
2499
+ if (resp.success && resp.deck) {
2500
+ const deck = toDeck(resp.deck);
2501
+ seedDeckDetail(deck);
2502
+ queryClient.refetchQueries({ queryKey: queryKeys.decks.all() });
2503
+ queryClient.refetchQueries({ queryKey: queryKeys.items.all() });
2504
+ }
2505
+ },
2506
+ });
2507
+ };
2508
+ /**
2509
+ * Update a deck's name and description
2510
+ */
2511
+ const useUpdateDeck = () => {
2512
+ return useMutation({
2513
+ mutationFn: async ({ id, name, description, }) => {
2514
+ return requestDatalayer({
2515
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${id}`,
2516
+ method: 'PUT',
2517
+ body: { name, description },
2518
+ });
2519
+ },
2520
+ onSuccess: (resp, deck) => {
2521
+ if (resp.success) {
2522
+ queryClient.invalidateQueries({
2523
+ queryKey: queryKeys.decks.detail(deck.id),
2524
+ });
2525
+ queryClient.invalidateQueries({ queryKey: queryKeys.decks.all() });
2526
+ }
2527
+ },
2528
+ });
2529
+ };
2530
+ /**
2531
+ * Replace a deck's specification, whole.
2532
+ */
2533
+ const useUpdateDeckModel = () => {
2534
+ return useMutation({
2535
+ mutationFn: async ({ deckId, spec, }) => {
2536
+ return requestDatalayer({
2537
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${deckId}/model`,
2538
+ method: 'PUT',
2539
+ body: { spec },
2540
+ });
2541
+ },
2542
+ onSuccess: (_, variables) => {
2543
+ queryClient.invalidateQueries({
2544
+ queryKey: queryKeys.decks.detail(variables.deckId),
2545
+ });
2546
+ queryClient.invalidateQueries({
2547
+ queryKey: queryKeys.decks.model(variables.deckId),
2548
+ });
2549
+ queryClient.invalidateQueries({ queryKey: queryKeys.decks.all() });
2550
+ },
2551
+ });
2552
+ };
2553
+ /**
2554
+ * Clone a deck into the caller's library (or a space they may change).
2555
+ * The spacer writes the specification again under the new uid.
2556
+ */
2557
+ const useCloneDeck = () => {
2558
+ return useMutation({
2559
+ mutationFn: async (deckId) => {
2560
+ return requestDatalayer({
2561
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${deckId}/clone`,
2562
+ method: 'POST',
2563
+ });
2564
+ },
2565
+ onSuccess: resp => {
2566
+ if (resp.success && resp.deck) {
2567
+ const deck = toDeck(resp.deck);
2568
+ seedDeckDetail(deck);
2569
+ queryClient.refetchQueries({ queryKey: queryKeys.decks.all() });
2570
+ queryClient.refetchQueries({ queryKey: queryKeys.items.all() });
2571
+ }
2572
+ },
2573
+ });
2574
+ };
1941
2575
  /**
1942
2576
  * Clone notebook
1943
2577
  */
@@ -2133,191 +2767,51 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2133
2767
  },
2134
2768
  });
2135
2769
  };
2136
- // ============================================================================
2137
- // Page Hooks
2138
- // ============================================================================
2139
- /**
2140
- * Get page by ID
2141
- */
2142
- const usePage = (pageId) => {
2143
- return useQuery({
2144
- queryKey: queryKeys.pages.detail(pageId),
2145
- queryFn: async () => {
2146
- const resp = await requestDatalayer({
2147
- url: `${configuration.libraryUrl}/api/library/v1/pages/${pageId}`,
2148
- method: 'GET',
2149
- });
2150
- if (resp.success && resp.page) {
2151
- return toPage(resp.page);
2152
- }
2153
- throw new Error(resp.message || 'Failed to fetch page');
2154
- },
2155
- ...DEFAULT_QUERY_OPTIONS,
2156
- enabled: !!pageId,
2157
- });
2158
- };
2159
2770
  /**
2160
- * Get all pages
2771
+ * Get all secrets
2161
2772
  */
2162
- const usePages = (scope) => {
2773
+ // TODO: Previously this hook pre-populated individual secret caches with setQueryData,
2774
+ // but this prevented useSecret from fetching fresh data (e.g., the value field).
2775
+ // Consider re-adding cache pre-population if the list endpoint returns full secret data,
2776
+ // or use a different query key pattern for partial vs full secret data.
2777
+ const useSecrets = (options) => {
2778
+ const principalUid = options?.principalUid;
2779
+ const principalKind = options?.principalKind;
2163
2780
  return useQuery({
2164
2781
  queryKey: [
2165
- ...queryKeys.pages.all(),
2166
- scope?.selectedPrincipalUid || '',
2167
- scope?.selectedPrincipalKind || '',
2782
+ ...queryKeys.secrets.all(),
2783
+ principalUid || 'self',
2784
+ principalKind || '',
2168
2785
  ],
2169
2786
  queryFn: async () => {
2170
- const params = new URLSearchParams();
2171
- if (scope?.selectedPrincipalUid) {
2172
- params.set('selected_principal_uid', scope.selectedPrincipalUid);
2173
- }
2174
- if (scope?.selectedPrincipalKind) {
2175
- params.set('selected_principal_kind', scope.selectedPrincipalKind);
2176
- }
2177
- const query = params.toString();
2178
2787
  const resp = await requestDatalayer({
2179
- url: `${configuration.libraryUrl}/api/library/v1/pages${query ? `?${query}` : ''}`,
2788
+ url: withAccountUidQuery(`${configuration.iamUrl}/api/iam/v1/secrets`, principalUid, principalKind),
2180
2789
  method: 'GET',
2181
2790
  });
2182
- if (resp.success && resp.pages) {
2183
- const pages = resp.pages
2184
- .map((p) => {
2185
- const page = toPage(p);
2186
- if (page) {
2187
- queryClient.setQueryData(queryKeys.pages.detail(page.id), page);
2188
- }
2189
- return page;
2190
- })
2791
+ if (resp.success && resp.secrets) {
2792
+ const secrets = resp.secrets
2793
+ .map((s) => toSecret(s))
2191
2794
  .filter(Boolean);
2192
- return pages;
2795
+ return secrets;
2193
2796
  }
2194
2797
  return [];
2195
2798
  },
2196
2799
  ...DEFAULT_QUERY_OPTIONS,
2800
+ refetchOnMount: true, // Override to refetch when stale after invalidation
2197
2801
  });
2198
2802
  };
2199
2803
  /**
2200
- * Create page
2804
+ * Create secret
2201
2805
  */
2202
- const useCreatePage = () => {
2806
+ const useCreateSecret = (options) => {
2807
+ const principalUid = options?.principalUid;
2808
+ const principalKind = options?.principalKind;
2203
2809
  return useMutation({
2204
- mutationFn: async (page) => {
2810
+ mutationFn: async (secret) => {
2205
2811
  return requestDatalayer({
2206
- url: `${configuration.libraryUrl}/api/library/v1/pages`,
2812
+ url: withAccountUidQuery(`${configuration.iamUrl}/api/iam/v1/secrets`, principalUid, principalKind),
2207
2813
  method: 'POST',
2208
- body: { ...page },
2209
- });
2210
- },
2211
- onSuccess: resp => {
2212
- if (resp.success && resp.page) {
2213
- const page = toPage(resp.page);
2214
- if (page) {
2215
- // Set detail cache
2216
- queryClient.setQueryData(queryKeys.pages.detail(page.id), page);
2217
- // Invalidate list to refetch
2218
- queryClient.invalidateQueries({
2219
- queryKey: queryKeys.pages.all(),
2220
- });
2221
- }
2222
- }
2223
- },
2224
- });
2225
- };
2226
- /**
2227
- * Update page
2228
- */
2229
- const useUpdatePage = () => {
2230
- return useMutation({
2231
- mutationFn: async (page) => {
2232
- return requestDatalayer({
2233
- url: `${configuration.libraryUrl}/api/library/v1/pages/${page.id}`,
2234
- method: 'PUT',
2235
- body: {
2236
- name: page.name,
2237
- description: page.description,
2238
- tags: page.tags,
2239
- },
2240
- });
2241
- },
2242
- onSuccess: (resp, page) => {
2243
- if (resp.success) {
2244
- // Invalidate detail and list queries
2245
- queryClient.invalidateQueries({
2246
- queryKey: queryKeys.pages.detail(page.id),
2247
- });
2248
- queryClient.invalidateQueries({
2249
- queryKey: queryKeys.pages.all(),
2250
- });
2251
- }
2252
- },
2253
- });
2254
- };
2255
- /**
2256
- * Delete page
2257
- */
2258
- const useDeletePage = () => {
2259
- return useMutation({
2260
- mutationFn: async (pageId) => {
2261
- return requestDatalayer({
2262
- url: `${configuration.libraryUrl}/api/library/v1/pages/${pageId}`,
2263
- method: 'DELETE',
2264
- });
2265
- },
2266
- onSuccess: (_, pageId) => {
2267
- // Remove from detail cache
2268
- queryClient.removeQueries({ queryKey: queryKeys.pages.detail(pageId) });
2269
- // Invalidate list to refetch
2270
- queryClient.invalidateQueries({
2271
- queryKey: queryKeys.pages.all(),
2272
- });
2273
- },
2274
- });
2275
- };
2276
- /**
2277
- * Get all secrets
2278
- */
2279
- // TODO: Previously this hook pre-populated individual secret caches with setQueryData,
2280
- // but this prevented useSecret from fetching fresh data (e.g., the value field).
2281
- // Consider re-adding cache pre-population if the list endpoint returns full secret data,
2282
- // or use a different query key pattern for partial vs full secret data.
2283
- const useSecrets = (options) => {
2284
- const principalUid = options?.principalUid;
2285
- const principalKind = options?.principalKind;
2286
- return useQuery({
2287
- queryKey: [
2288
- ...queryKeys.secrets.all(),
2289
- principalUid || 'self',
2290
- principalKind || '',
2291
- ],
2292
- queryFn: async () => {
2293
- const resp = await requestDatalayer({
2294
- url: withAccountUidQuery(`${configuration.iamUrl}/api/iam/v1/secrets`, principalUid, principalKind),
2295
- method: 'GET',
2296
- });
2297
- if (resp.success && resp.secrets) {
2298
- const secrets = resp.secrets
2299
- .map((s) => toSecret(s))
2300
- .filter(Boolean);
2301
- return secrets;
2302
- }
2303
- return [];
2304
- },
2305
- ...DEFAULT_QUERY_OPTIONS,
2306
- refetchOnMount: true, // Override to refetch when stale after invalidation
2307
- });
2308
- };
2309
- /**
2310
- * Create secret
2311
- */
2312
- const useCreateSecret = (options) => {
2313
- const principalUid = options?.principalUid;
2314
- const principalKind = options?.principalKind;
2315
- return useMutation({
2316
- mutationFn: async (secret) => {
2317
- return requestDatalayer({
2318
- url: withAccountUidQuery(`${configuration.iamUrl}/api/iam/v1/secrets`, principalUid, principalKind),
2319
- method: 'POST',
2320
- body: { ...secret },
2814
+ body: { ...secret },
2321
2815
  });
2322
2816
  },
2323
2817
  onSuccess: resp => {
@@ -2565,6 +3059,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2565
3059
  queryClient.removeQueries({
2566
3060
  queryKey: queryKeys.documents.detail(itemId),
2567
3061
  });
3062
+ queryClient.removeQueries({ queryKey: queryKeys.decks.detail(itemId) });
2568
3063
  queryClient.removeQueries({ queryKey: queryKeys.cells.detail(itemId) });
2569
3064
  queryClient.removeQueries({
2570
3065
  queryKey: queryKeys.lessons.detail(itemId),
@@ -2578,6 +3073,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2578
3073
  // Invalidate all artifact type queries
2579
3074
  queryClient.invalidateQueries({ queryKey: queryKeys.notebooks.all() });
2580
3075
  queryClient.invalidateQueries({ queryKey: queryKeys.documents.all() });
3076
+ queryClient.invalidateQueries({ queryKey: queryKeys.decks.all() });
2581
3077
  queryClient.invalidateQueries({ queryKey: queryKeys.cells.all() });
2582
3078
  queryClient.invalidateQueries({ queryKey: queryKeys.lessons.all() });
2583
3079
  queryClient.invalidateQueries({ queryKey: queryKeys.exercises.all() });
@@ -3525,25 +4021,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
3525
4021
  },
3526
4022
  });
3527
4023
  };
3528
- /**
3529
- * Get public courses
3530
- */
3531
- const usePublicCourses = () => {
3532
- return useQuery({
3533
- queryKey: ['courses', 'public'],
3534
- queryFn: async () => {
3535
- const resp = await requestDatalayer({
3536
- url: `${configuration.libraryUrl}/api/library/v1/courses/public`,
3537
- method: 'GET',
3538
- });
3539
- if (resp.success && resp.courses) {
3540
- return resp.courses.map((course) => toCourse(course));
3541
- }
3542
- return [];
3543
- },
3544
- ...DEFAULT_QUERY_OPTIONS,
3545
- });
3546
- };
3547
4024
  /**
3548
4025
  * Get instructor courses
3549
4026
  */
@@ -5830,35 +6307,721 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
5830
6307
  /**
5831
6308
  * Search public items (notebooks, documents, etc.)
5832
6309
  */
6310
+ /**
6311
+ * Every artifact type a library card is drawn for. One list, so a caller
6312
+ * that wants "everything" cannot accidentally ask for the six types someone
6313
+ * wrote out by hand two releases ago.
6314
+ *
6315
+ * `deck` is here because the administration page's "not in the ribbon" list
6316
+ * asks for this default: without it a published deck was in the library and
6317
+ * could be found by searching, but was never offered to be featured.
6318
+ */
6319
+ const LIBRARY_ARTIFACT_TYPES = [
6320
+ 'notebook',
6321
+ 'document',
6322
+ 'deck',
6323
+ 'cell',
6324
+ 'lesson',
6325
+ 'exercise',
6326
+ 'assignment',
6327
+ 'course',
6328
+ 'evalset',
6329
+ 'dataset',
6330
+ 'agent',
6331
+ ];
6332
+ const librarySearchQueryString = ({ q, types, max = 20, offset = 0, sort = 'relevance', featured = false, owner, }) => {
6333
+ const normalizedTypes = Array.from(new Set((types || [])
6334
+ .map(type => String(type || '')
6335
+ .trim()
6336
+ .toLowerCase())
6337
+ .filter(Boolean)
6338
+ .map(type => (type === 'eval' ? 'evalset' : type))));
6339
+ const params = {
6340
+ q: q || '*',
6341
+ types: (normalizedTypes.length > 0
6342
+ ? normalizedTypes
6343
+ : LIBRARY_ARTIFACT_TYPES).join(' '),
6344
+ max: String(max),
6345
+ offset: String(offset),
6346
+ sort,
6347
+ };
6348
+ if (featured) {
6349
+ params.featured = 'true';
6350
+ }
6351
+ if (owner) {
6352
+ params.owner = owner;
6353
+ }
6354
+ return Object.entries(params)
6355
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
6356
+ .join('&');
6357
+ };
6358
+ const searchLibrary = async (args) => {
6359
+ const resp = await requestDatalayer({
6360
+ url: `${configuration.libraryUrl}/api/library/v1/search?${librarySearchQueryString(args)}`,
6361
+ method: 'GET',
6362
+ });
6363
+ if (!resp.success) {
6364
+ throw new Error(resp.message || 'Failed to search the library');
6365
+ }
6366
+ return {
6367
+ items: (resp.items || [])
6368
+ .map((item) => toItem(item))
6369
+ .filter(Boolean),
6370
+ total: Number(resp.total ?? 0),
6371
+ offset: Number(resp.offset ?? args.offset ?? 0),
6372
+ };
6373
+ };
6374
+ /**
6375
+ * Search the library: one request, whatever mix of artifact types is asked
6376
+ * for, with the total so a caller can page without asking twice.
6377
+ *
6378
+ * Answers `{ items, total, offset }` rather than a bare array — a result
6379
+ * page without its total cannot be paged, and every caller was inventing
6380
+ * "there is probably more" from the length it got.
6381
+ */
5833
6382
  const useSearchPublicItems = () => {
5834
6383
  return useMutation({
5835
- mutationFn: async ({ q, types = ['notebook', 'document', 'cell', 'lesson', 'evalset', 'course'], max = 100, }) => {
5836
- const normalizedTypes = Array.from(new Set((types || [])
5837
- .map(type => String(type || '')
5838
- .trim()
5839
- .toLowerCase())
5840
- .filter(Boolean)
5841
- .map(type => (type === 'eval' ? 'evalset' : type))));
5842
- const queryString = Object.entries({
5843
- q: q || '*',
5844
- types: (normalizedTypes.length > 0
5845
- ? normalizedTypes
5846
- : ['notebook', 'document', 'cell', 'lesson', 'evalset', 'course']).join(' '),
5847
- max: max.toString(),
5848
- })
5849
- .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
5850
- .join('&');
6384
+ mutationFn: async (args) => searchLibrary(args),
6385
+ });
6386
+ };
6387
+ /**
6388
+ * Everything the library holds, as a query rather than a mutation.
6389
+ *
6390
+ * For the administration page, which has to show what *could* be featured
6391
+ * as well as what is. Publishing the agent catalogue puts artifacts in the
6392
+ * library; without this the page reported "6 agents published" and then
6393
+ * listed none of them, because the only list it had was the featured one.
6394
+ */
6395
+ const useLibraryArtifacts = (max = 100, options) => {
6396
+ return useQuery({
6397
+ queryKey: [...queryKeys.items.all(), 'library-artifacts', max],
6398
+ queryFn: async () => searchLibrary({ max, sort: 'recent' }),
6399
+ ...options,
6400
+ });
6401
+ };
6402
+ /**
6403
+ * The featured artifacts, for the home page's ribbon.
6404
+ *
6405
+ * Anonymous-safe: the library answers this one without a token, and the
6406
+ * service holds it for a minute, so a visit costs the platform nothing.
6407
+ */
6408
+ const useFeaturedItems = (max = 12, options) => {
6409
+ return useQuery({
6410
+ queryKey: [...queryKeys.items.featured(), max],
6411
+ queryFn: async () => {
6412
+ const resp = await requestDatalayer({
6413
+ url: `${configuration.libraryUrl}/api/library/v1/featured?max=${max}`,
6414
+ method: 'GET',
6415
+ });
6416
+ if (!resp.success) {
6417
+ throw new Error(resp.message || 'Failed to get the featured artifacts');
6418
+ }
6419
+ return (resp.items || [])
6420
+ .map((item) => toItem(item))
6421
+ .filter(Boolean);
6422
+ },
6423
+ staleTime: 60_000,
6424
+ ...options,
6425
+ });
6426
+ };
6427
+ /**
6428
+ * Everything an administrator has actually featured, and nothing else.
6429
+ *
6430
+ * Not `useFeaturedItems`. That one answers for the home page's ribbon: a
6431
+ * public request, capped at what a row can show, ordered for reading. This
6432
+ * one is what the administration page edits — everything featured, in rank
6433
+ * order, however long the list is and whoever it belongs to — so that the
6434
+ * page showing the ribbon's contents is showing the ribbon's contents and
6435
+ * not a page of it. `platform_admin` only.
6436
+ */
6437
+ const useAllFeaturedItems = (options) => {
6438
+ return useQuery({
6439
+ queryKey: [...queryKeys.items.featured(), 'all'],
6440
+ queryFn: async () => {
5851
6441
  const resp = await requestDatalayer({
5852
- url: `${configuration.libraryUrl}/api/library/v1/search?${queryString}`,
6442
+ url: `${configuration.libraryUrl}/api/library/v1/featured/all`,
5853
6443
  method: 'GET',
5854
6444
  });
5855
6445
  if (!resp.success) {
5856
- throw new Error(resp.message || 'Failed to search public items');
6446
+ throw new Error(resp.message || 'Failed to get the featured artifacts');
5857
6447
  }
5858
6448
  return (resp.items || [])
5859
6449
  .map((item) => toItem(item))
5860
6450
  .filter(Boolean);
5861
6451
  },
6452
+ ...options,
6453
+ });
6454
+ };
6455
+ /**
6456
+ * One public artifact by uid, with its content and its library metadata.
6457
+ *
6458
+ * The public pages used to `fetch` this endpoint themselves, which meant no
6459
+ * token was ever sent and the page could not know whether the visitor
6460
+ * orbits what they are reading.
6461
+ */
6462
+ const usePublicItem = (itemId, options) => {
6463
+ return useQuery({
6464
+ queryKey: queryKeys.items.publicItem(itemId || ''),
6465
+ queryFn: async () => {
6466
+ const resp = await requestDatalayer({
6467
+ url: `${configuration.libraryUrl}/api/library/v1/public/items/${encodeURIComponent(itemId)}`,
6468
+ method: 'GET',
6469
+ });
6470
+ if (!resp.success || !resp.item) {
6471
+ throw new Error(resp.message || 'Failed to get the public artifact');
6472
+ }
6473
+ return toItem(resp.item);
6474
+ },
6475
+ enabled: Boolean(itemId),
6476
+ ...options,
6477
+ });
6478
+ };
6479
+ /** The artifacts this account has published: a public profile page. */
6480
+ const useAccountPublications = (handle, options) => {
6481
+ return useQuery({
6482
+ queryKey: queryKeys.items.byOwner(handle || ''),
6483
+ queryFn: async () => searchLibrary({ owner: handle, max: 100, sort: 'recent' }),
6484
+ enabled: Boolean(handle),
6485
+ ...options,
6486
+ });
6487
+ };
6488
+ /** The artifacts the signed-in visitor orbits. */
6489
+ const useMyOrbits = (offset = 0, max = 20, options) => {
6490
+ return useQuery({
6491
+ queryKey: [...queryKeys.items.orbits(), offset, max],
6492
+ queryFn: async () => {
6493
+ const resp = await requestDatalayer({
6494
+ url: `${configuration.libraryUrl}/api/library/v1/orbits?offset=${offset}&max=${max}`,
6495
+ method: 'GET',
6496
+ });
6497
+ if (!resp.success) {
6498
+ throw new Error(resp.message || 'Failed to get the orbits');
6499
+ }
6500
+ return {
6501
+ items: (resp.items || [])
6502
+ .map((item) => toItem(item))
6503
+ .filter(Boolean),
6504
+ total: Number(resp.total ?? 0),
6505
+ };
6506
+ },
6507
+ ...options,
6508
+ });
6509
+ };
6510
+ /**
6511
+ * What one account orbits, on its public profile.
6512
+ *
6513
+ * Whether it publishes them at all is the service's answer, not this
6514
+ * caller's: `shown` says so, and an account that has not opted in reads the
6515
+ * same as one that orbits nothing.
6516
+ */
6517
+ const useAccountOrbits = (handle, offset = 0, max = 12, options) => {
6518
+ return useQuery({
6519
+ queryKey: [
6520
+ ...queryKeys.items.orbits(),
6521
+ 'account',
6522
+ handle || '',
6523
+ offset,
6524
+ max,
6525
+ ],
6526
+ queryFn: async () => {
6527
+ const resp = await requestDatalayer({
6528
+ url: `${configuration.libraryUrl}/api/library/v1/accounts/${encodeURIComponent(handle)}/orbits?offset=${offset}&max=${max}`,
6529
+ method: 'GET',
6530
+ });
6531
+ if (!resp.success) {
6532
+ throw new Error(resp.message || 'Failed to get the orbits');
6533
+ }
6534
+ return {
6535
+ items: (resp.items || [])
6536
+ .map((item) => toItem(item))
6537
+ .filter(Boolean),
6538
+ total: Number(resp.total ?? 0),
6539
+ shown: Boolean(resp.shown),
6540
+ };
6541
+ },
6542
+ enabled: Boolean(handle),
6543
+ ...options,
6544
+ });
6545
+ };
6546
+ /** Who orbits an artifact. */
6547
+ const useItemOrbiters = (itemId, offset = 0, max = 20, options) => {
6548
+ return useQuery({
6549
+ queryKey: [
6550
+ ...queryKeys.items.orbiters(itemId || ''),
6551
+ offset,
6552
+ max,
6553
+ ],
6554
+ queryFn: async () => {
6555
+ const resp = await requestDatalayer({
6556
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/orbiters?offset=${offset}&max=${max}`,
6557
+ method: 'GET',
6558
+ });
6559
+ if (!resp.success) {
6560
+ throw new Error(resp.message || 'Failed to get the orbiters');
6561
+ }
6562
+ return {
6563
+ orbiters: resp.orbiters || [],
6564
+ total: Number(resp.total ?? 0),
6565
+ };
6566
+ },
6567
+ enabled: Boolean(itemId),
6568
+ ...options,
6569
+ });
6570
+ };
6571
+ /**
6572
+ * What was made of a public artifact (B5-04): every derivation counted by
6573
+ * kind, and the derived artifacts that are public themselves, newest first.
6574
+ */
6575
+ const useItemDerivations = (itemId, offset = 0, max = 20, options) => {
6576
+ return useQuery({
6577
+ queryKey: [
6578
+ ...queryKeys.items.derivations(itemId || ''),
6579
+ offset,
6580
+ max,
6581
+ ],
6582
+ queryFn: async () => {
6583
+ const resp = await requestDatalayer({
6584
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/derivations?offset=${offset}&max=${max}`,
6585
+ method: 'GET',
6586
+ });
6587
+ if (!resp.success) {
6588
+ throw new Error(resp.message || 'Failed to get the derivations');
6589
+ }
6590
+ return {
6591
+ derivations: (resp.derivations || []).map((derivation) => ({
6592
+ kind: String(derivation.kind || ''),
6593
+ fromVersion: derivation.fromVersion ?? null,
6594
+ derivedAt: derivation.derivedAt,
6595
+ item: toItem(derivation.item),
6596
+ })),
6597
+ counts: (resp.counts || {}),
6598
+ total: Number(resp.total ?? 0),
6599
+ };
6600
+ },
6601
+ enabled: Boolean(itemId),
6602
+ ...options,
6603
+ });
6604
+ };
6605
+ /**
6606
+ * Show an artifact as orbited before the server has said so, and put it
6607
+ * back if the server disagrees.
6608
+ *
6609
+ * A star that waits for a round trip feels broken, and the count is on the
6610
+ * button the visitor just pressed. The server answers with a recount, which
6611
+ * is what finally lands in the cache.
6612
+ */
6613
+ const applyOrbitToCaches = (itemId, orbited, orbits) => {
6614
+ const patch = (item) => {
6615
+ if (!item || String(item.id ?? item.uid) !== String(itemId)) {
6616
+ return item;
6617
+ }
6618
+ const nextOrbits = orbits !== undefined
6619
+ ? orbits
6620
+ : Math.max(0, Number(item.orbits || 0) + (orbited ? 1 : -1));
6621
+ return { ...item, orbited, orbits: nextOrbits };
6622
+ };
6623
+ queryClient.setQueriesData({ queryKey: queryKeys.items.all() }, (data) => {
6624
+ if (!data) {
6625
+ return data;
6626
+ }
6627
+ if (Array.isArray(data)) {
6628
+ return data.map(patch);
6629
+ }
6630
+ if (Array.isArray(data.items)) {
6631
+ return { ...data, items: data.items.map(patch) };
6632
+ }
6633
+ if (data.id || data.uid) {
6634
+ return patch(data);
6635
+ }
6636
+ return data;
6637
+ });
6638
+ };
6639
+ const useOrbitItem = () => {
6640
+ return useMutation({
6641
+ mutationFn: async (itemId) => {
6642
+ const resp = await requestDatalayer({
6643
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/orbit`,
6644
+ method: 'PUT',
6645
+ });
6646
+ if (!resp.success) {
6647
+ throw new Error(resp.message || 'Failed to orbit the artifact');
6648
+ }
6649
+ return { itemId, orbits: Number(resp.orbits ?? 0), orbited: true };
6650
+ },
6651
+ onMutate: async (itemId) => {
6652
+ applyOrbitToCaches(itemId, true);
6653
+ return { itemId };
6654
+ },
6655
+ onError: (_error, itemId) => {
6656
+ applyOrbitToCaches(itemId, false);
6657
+ },
6658
+ onSuccess: result => {
6659
+ applyOrbitToCaches(result.itemId, true, result.orbits);
6660
+ queryClient.invalidateQueries({ queryKey: queryKeys.items.orbits() });
6661
+ },
6662
+ });
6663
+ };
6664
+ const useUnorbitItem = () => {
6665
+ return useMutation({
6666
+ mutationFn: async (itemId) => {
6667
+ const resp = await requestDatalayer({
6668
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/orbit`,
6669
+ method: 'DELETE',
6670
+ });
6671
+ if (!resp.success) {
6672
+ throw new Error(resp.message || 'Failed to leave the orbit');
6673
+ }
6674
+ return { itemId, orbits: Number(resp.orbits ?? 0), orbited: false };
6675
+ },
6676
+ onMutate: async (itemId) => {
6677
+ applyOrbitToCaches(itemId, false);
6678
+ return { itemId };
6679
+ },
6680
+ onError: (_error, itemId) => {
6681
+ applyOrbitToCaches(itemId, true);
6682
+ },
6683
+ onSuccess: result => {
6684
+ applyOrbitToCaches(result.itemId, false, result.orbits);
6685
+ queryClient.invalidateQueries({ queryKey: queryKeys.items.orbits() });
6686
+ },
6687
+ });
6688
+ };
6689
+ /** Feature an artifact on the home page. `platform_admin` only. */
6690
+ const useFeatureItem = () => {
6691
+ return useMutation({
6692
+ mutationFn: async ({ itemId, rank, }) => {
6693
+ const resp = await requestDatalayer({
6694
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/featured`,
6695
+ method: 'PUT',
6696
+ body: { rank: rank ?? null },
6697
+ // One toast, not two: the caller says which artifact failed and
6698
+ // why, so the generic report underneath is a duplicate.
6699
+ notifyOnError: false,
6700
+ });
6701
+ if (!resp.success) {
6702
+ throw new Error(resp.message || 'Failed to feature the artifact');
6703
+ }
6704
+ return resp;
6705
+ },
6706
+ onSuccess: () => {
6707
+ invalidateEverywhere(queryClient, queryKeys.items.all());
6708
+ },
6709
+ });
6710
+ };
6711
+ const useUnfeatureItem = () => {
6712
+ return useMutation({
6713
+ mutationFn: async (itemId) => {
6714
+ const resp = await requestDatalayer({
6715
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/featured`,
6716
+ method: 'DELETE',
6717
+ // One toast, not two: the caller says which artifact failed and
6718
+ // why, so the generic report underneath is a duplicate.
6719
+ notifyOnError: false,
6720
+ });
6721
+ if (!resp.success) {
6722
+ throw new Error(resp.message || 'Failed to unfeature the artifact');
6723
+ }
6724
+ return resp;
6725
+ },
6726
+ onSuccess: () => {
6727
+ invalidateEverywhere(queryClient, queryKeys.items.all());
6728
+ },
6729
+ });
6730
+ };
6731
+ const useSetFeaturedRank = () => {
6732
+ return useMutation({
6733
+ mutationFn: async ({ itemId, rank, }) => {
6734
+ const resp = await requestDatalayer({
6735
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/featured`,
6736
+ method: 'PATCH',
6737
+ body: { rank },
6738
+ // One toast, not two: the caller says which artifact failed and
6739
+ // why, so the generic report underneath is a duplicate.
6740
+ notifyOnError: false,
6741
+ });
6742
+ if (!resp.success) {
6743
+ throw new Error(resp.message || 'Failed to move the artifact');
6744
+ }
6745
+ return resp;
6746
+ },
6747
+ onSuccess: () => {
6748
+ invalidateEverywhere(queryClient, queryKeys.items.featured());
6749
+ },
6750
+ });
6751
+ };
6752
+ /**
6753
+ * Refresh everything that carries an artifact's public flag or its image.
6754
+ *
6755
+ * `items` and `spaces` cover the library listings, but an artifact opened in
6756
+ * an editor is read through its own per-type query — `notebooks.detail(id)`
6757
+ * for the editor, `notebooks.bySpace(id)` for the table — and neither
6758
+ * shares a prefix with either of those. Leaving them alone is what made the
6759
+ * Publish button keep saying "Publish" after publishing, and "Published"
6760
+ * after withdrawing: the mutation succeeded, the cached copy still held the
6761
+ * old `is_public_b`, and nothing asked for it again for five minutes.
6762
+ *
6763
+ * A course is a space, but it is not read through the `spaces` queries: it
6764
+ * has a namespace of its own, so publishing one from its header left the
6765
+ * course page holding the `public` flag it loaded with and the button
6766
+ * saying the opposite of what had just happened.
6767
+ *
6768
+ * **`refetchType: 'all'`, which is what makes any of the above work.**
6769
+ *
6770
+ * Naming a query is not the same as making it ask again. The default,
6771
+ * `'active'`, refetches the queries something is currently watching and
6772
+ * leaves the rest marked stale — right nearly everywhere, and wrong here,
6773
+ * because this application sets `refetchOnMount: false` (see
6774
+ * `DEFAULT_QUERY_OPTIONS`, and the client the web application builds). A
6775
+ * query that mounts holding stale data renders it and asks for nothing.
6776
+ *
6777
+ * So publishing worked exactly once. The editor it was published from was
6778
+ * mounted and flipped its button; the notebooks table, the library, the
6779
+ * search page and the artifact's own public page were not, and kept the
6780
+ * answer they already had. Come back to the editor after that and *it* is
6781
+ * now the page that was not mounted for the next withdrawal, so its button
6782
+ * is wrong too — which is what "it works the first time" means.
6783
+ *
6784
+ * The whole of each type's namespace, not only its `details()`. A table
6785
+ * lists its artifacts under `bySpace(spaceId)`, which shares no prefix with
6786
+ * `detail(id)` — so publishing from an editor and walking back to the table
6787
+ * showed the row exactly as it was, switch and all.
6788
+ *
6789
+ * The `model` queries are the one thing held out. A notebook's model is its
6790
+ * content, megabytes of it, and it lives *under* `detail(id)` — so
6791
+ * refetching a namespace would pull every open notebook's content over the
6792
+ * wire each time somebody pressed Publish. Whether an artifact is public
6793
+ * says nothing about what is in it.
6794
+ */
6795
+ const invalidateArtifactVisibility = () => {
6796
+ const refetchType = 'all';
6797
+ /** Everything about an artifact except what is written in it. */
6798
+ const notItsContent = (query) => query.queryKey[query.queryKey.length - 1] !== 'model';
6799
+ queryClient.invalidateQueries({
6800
+ queryKey: queryKeys.items.all(),
6801
+ refetchType,
6802
+ });
6803
+ queryClient.invalidateQueries({ queryKey: ['spaces'], refetchType });
6804
+ for (const namespace of [
6805
+ queryKeys.notebooks.all(),
6806
+ queryKeys.documents.all(),
6807
+ queryKeys.decks.all(),
6808
+ queryKeys.cells.all(),
6809
+ queryKeys.lessons.all(),
6810
+ queryKeys.exercises.all(),
6811
+ queryKeys.assignments.all(),
6812
+ queryKeys.courses.all(),
6813
+ ]) {
6814
+ queryClient.invalidateQueries({
6815
+ queryKey: namespace,
6816
+ refetchType,
6817
+ predicate: notItsContent,
6818
+ });
6819
+ }
6820
+ };
6821
+ /**
6822
+ * What an artifact looks like in the library: a capture the publisher took,
6823
+ * the box drawn for its kind, or an icon from the Datalayer set.
6824
+ */
6825
+ const useSetItemImage = () => {
6826
+ return useMutation({
6827
+ mutationFn: async ({ itemId, kind, ref, data, }) => {
6828
+ const resp = await requestDatalayer({
6829
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/image`,
6830
+ method: 'PUT',
6831
+ body: { kind, ref, data },
6832
+ });
6833
+ if (!resp.success) {
6834
+ throw new Error(resp.message || 'Failed to set the image');
6835
+ }
6836
+ return resp.image;
6837
+ },
6838
+ onSuccess: invalidateArtifactVisibility,
6839
+ });
6840
+ };
6841
+ const useResetItemImage = () => {
6842
+ return useMutation({
6843
+ mutationFn: async (itemId) => {
6844
+ const resp = await requestDatalayer({
6845
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/image`,
6846
+ method: 'DELETE',
6847
+ });
6848
+ if (!resp.success) {
6849
+ throw new Error(resp.message || 'Failed to reset the image');
6850
+ }
6851
+ return resp.image;
6852
+ },
6853
+ onSuccess: invalidateArtifactVisibility,
6854
+ });
6855
+ };
6856
+ /**
6857
+ * Tell the library that an artifact was reused: cloned into a space, or
6858
+ * instantiated as a runtime. The reuse itself belongs to the service that
6859
+ * performed it, so this is a count, not the act.
6860
+ */
6861
+ const useCountItemReuse = () => {
6862
+ return useMutation({
6863
+ mutationFn: async ({ itemId, kind, }) => {
6864
+ const resp = await requestDatalayer({
6865
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/reused`,
6866
+ method: 'POST',
6867
+ body: { kind },
6868
+ });
6869
+ if (!resp.success) {
6870
+ throw new Error(resp.message || 'Failed to count the reuse');
6871
+ }
6872
+ return resp;
6873
+ },
6874
+ });
6875
+ };
6876
+ /**
6877
+ * Publish the platform's own catalogue — the agents — into the library.
6878
+ * `platform_admin` only; the catalogue lives in this application's build.
6879
+ */
6880
+ const useSyncArtifactCatalogue = () => {
6881
+ return useMutation({
6882
+ mutationFn: async ({ artifacts, artifactType = 'agent', }) => {
6883
+ const resp = await requestDatalayer({
6884
+ url: `${configuration.libraryUrl}/api/library/v1/artifacts/sync`,
6885
+ method: 'PUT',
6886
+ body: { artifact_type: artifactType, artifacts },
6887
+ // One toast, not two: the caller says which artifact failed and
6888
+ // why, so the generic report underneath is a duplicate.
6889
+ notifyOnError: false,
6890
+ });
6891
+ if (!resp.success) {
6892
+ throw new Error(resp.message || 'Failed to synchronize the catalogue');
6893
+ }
6894
+ return resp;
6895
+ },
6896
+ onSuccess: () => {
6897
+ invalidateEverywhere(queryClient, queryKeys.items.all());
6898
+ },
6899
+ });
6900
+ };
6901
+ /**
6902
+ * Publish an artifact in the library: choose its image, then make it
6903
+ * public.
6904
+ *
6905
+ * The one place that publishes. It used to be a `ToggleSwitch` in each of
6906
+ * seven tables and three editors, each with its own copy of the mutation
6907
+ * and its own idea of what to roll back; publishing is an act with
6908
+ * consequences a switch cannot express — a picture to choose, a public page
6909
+ * that appears, a search it enters.
6910
+ *
6911
+ * The image is set first: a failure there leaves the artifact private, which
6912
+ * is recoverable by trying again, where the reverse would publish something
6913
+ * showing the wrong picture.
6914
+ */
6915
+ const usePublishArtifact = () => {
6916
+ const setImage = useSetItemImage();
6917
+ return useMutation({
6918
+ mutationFn: async ({ itemId, image, tags, description, }) => {
6919
+ /*
6920
+ * Publish first, then dress it.
6921
+ *
6922
+ * The image used to be set before the artifact was made public, so
6923
+ * that a picture that would not take left it private rather than
6924
+ * published with the wrong one. That order could never work: the
6925
+ * library indexes public artifacts, so a private one has no
6926
+ * projection, and setting an image on it answered "The artifact is
6927
+ * not found" — every first publish with a picture failed.
6928
+ *
6929
+ * The intent survives the reordering. The artifact goes public, which
6930
+ * is what creates the projection; if the picture then will not take,
6931
+ * it is withdrawn again and the message still tells the truth.
6932
+ */
6933
+ const resp = await requestDatalayer({
6934
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/public`,
6935
+ method: 'PATCH',
6936
+ body: {
6937
+ is_public: true,
6938
+ // Absent, not null: the service reads "leave it alone" from a
6939
+ // missing key, and a caller that does not know an artifact's
6940
+ // words must not be able to clear them by omission.
6941
+ ...(tags === undefined ? {} : { tags }),
6942
+ ...(description === undefined ? {} : { description }),
6943
+ },
6944
+ });
6945
+ if (!resp.success) {
6946
+ throw new Error(resp.message || 'Failed to publish the artifact');
6947
+ }
6948
+ if (image) {
6949
+ try {
6950
+ await setImage.mutateAsync({ itemId, ...image });
6951
+ }
6952
+ catch (error) {
6953
+ // Back to private, so the artifact is never left public wearing
6954
+ // the drawing the publisher did not choose.
6955
+ try {
6956
+ await requestDatalayer({
6957
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/public`,
6958
+ method: 'PATCH',
6959
+ body: { is_public: false },
6960
+ });
6961
+ }
6962
+ catch {
6963
+ // The withdrawal failed too; the error below is still the one
6964
+ // worth showing, and it is the more actionable of the two.
6965
+ }
6966
+ throw new Error(`The image could not be set, so the artifact stays private: ${error?.message || 'unknown error'}`, { cause: error });
6967
+ }
6968
+ }
6969
+ return resp;
6970
+ },
6971
+ onSuccess: invalidateArtifactVisibility,
6972
+ });
6973
+ };
6974
+ /**
6975
+ * Withdraw an artifact from the library. Its orbits are kept: publishing it
6976
+ * again brings them back.
6977
+ */
6978
+ /**
6979
+ * Rename a published artifact, or change what it says about itself.
6980
+ *
6981
+ * The words belong to the artifact rather than to the library — the
6982
+ * library holds a projection of them — so the library service writes the
6983
+ * artifact's own source and that write carries the change into the index.
6984
+ * Which is why this is one request and not two.
6985
+ *
6986
+ * Every field is optional and every one means "leave it alone" when
6987
+ * omitted, so a caller may change one without knowing the others.
6988
+ */
6989
+ const useUpdateArtifactDetails = () => {
6990
+ return useMutation({
6991
+ mutationFn: async ({ itemId, name, description, tags, }) => {
6992
+ const resp = await requestDatalayer({
6993
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}`,
6994
+ method: 'PATCH',
6995
+ body: {
6996
+ ...(name === undefined ? {} : { name }),
6997
+ ...(description === undefined ? {} : { description }),
6998
+ ...(tags === undefined ? {} : { tags }),
6999
+ },
7000
+ });
7001
+ if (!resp.success) {
7002
+ throw new Error(resp.message || 'Failed to update the artifact');
7003
+ }
7004
+ return resp;
7005
+ },
7006
+ // The same fan-out publishing uses: a rename changes the card in every
7007
+ // listing, the editor's own copy, and the artifact's public page.
7008
+ onSuccess: invalidateArtifactVisibility,
7009
+ });
7010
+ };
7011
+ const useUnpublishArtifact = () => {
7012
+ return useMutation({
7013
+ mutationFn: async (itemId) => {
7014
+ const resp = await requestDatalayer({
7015
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/public`,
7016
+ method: 'PATCH',
7017
+ body: { is_public: false },
7018
+ });
7019
+ if (!resp.success) {
7020
+ throw new Error(resp.message || 'Failed to withdraw the artifact');
7021
+ }
7022
+ return resp;
7023
+ },
7024
+ onSuccess: invalidateArtifactVisibility,
5862
7025
  });
5863
7026
  };
5864
7027
  // ============================================================================
@@ -6215,28 +7378,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6215
7378
  ...options,
6216
7379
  });
6217
7380
  };
6218
- /**
6219
- * Refresh public courses list
6220
- * @param options - Mutation options
6221
- */
6222
- const useRefreshPublicCourses = (options) => {
6223
- const queryClient = useQueryClient();
6224
- return useMutation({
6225
- mutationFn: async () => {
6226
- const resp = await requestDatalayer({
6227
- url: `${configuration.libraryUrl}/api/library/v1/courses/public`,
6228
- method: 'GET',
6229
- });
6230
- return resp;
6231
- },
6232
- onSuccess: () => {
6233
- queryClient.invalidateQueries({
6234
- queryKey: queryKeys.courses.public(),
6235
- });
6236
- },
6237
- ...options,
6238
- });
6239
- };
6240
7381
  /**
6241
7382
  * Refresh instructor courses list
6242
7383
  * @param options - Mutation options
@@ -6465,28 +7606,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6465
7606
  ...options,
6466
7607
  });
6467
7608
  };
6468
- /**
6469
- * Get public items (query hook)
6470
- * @param options - Query options
6471
- */
6472
- const usePublicItems = (options) => {
6473
- return useQuery({
6474
- queryKey: queryKeys.items.public(),
6475
- queryFn: async () => {
6476
- const resp = await requestDatalayer({
6477
- url: `${configuration.libraryUrl}/api/library/v1/search?q=*&max=-1`,
6478
- method: 'GET',
6479
- });
6480
- return {
6481
- ...resp,
6482
- items: (resp.items || [])
6483
- .map((item) => toItem(item))
6484
- .filter(Boolean),
6485
- };
6486
- },
6487
- ...options,
6488
- });
6489
- };
6490
7609
  /**
6491
7610
  * Get the current user's own public items (publications).
6492
7611
  * @param options - Query options
@@ -6509,31 +7628,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6509
7628
  ...options,
6510
7629
  });
6511
7630
  };
6512
- /**
6513
- * Refresh public items
6514
- * @param options - Mutation options
6515
- */
6516
- const useRefreshPublicItems = (options) => {
6517
- const queryClient = useQueryClient();
6518
- return useMutation({
6519
- mutationFn: async () => {
6520
- const resp = await requestDatalayer({
6521
- url: `${configuration.libraryUrl}/api/library/v1/search?q=*&max=-1`,
6522
- method: 'GET',
6523
- });
6524
- return {
6525
- ...resp,
6526
- items: (resp.items || [])
6527
- .map((item) => toItem(item))
6528
- .filter(Boolean),
6529
- };
6530
- },
6531
- onSuccess: () => {
6532
- queryClient.invalidateQueries({ queryKey: queryKeys.items.public() });
6533
- },
6534
- ...options,
6535
- });
6536
- };
6537
7631
  /**
6538
7632
  * Refresh space items
6539
7633
  * @param options - Mutation options
@@ -7311,7 +8405,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7311
8405
  // Courses
7312
8406
  useCourse,
7313
8407
  useUpdateCourse,
7314
- usePublicCourses,
7315
8408
  useInstructorCourses,
7316
8409
  useCourseEnrollments,
7317
8410
  useEnrollStudentToCourse,
@@ -7321,7 +8414,6 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7321
8414
  useConfirmCourseItemCompletion,
7322
8415
  useSetCourseItems,
7323
8416
  useRefreshCourse,
7324
- useRefreshPublicCourses,
7325
8417
  useRefreshInstructorCourses,
7326
8418
  useRefreshCoursesEnrollments,
7327
8419
  useRefreshStudent,
@@ -7334,6 +8426,13 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7334
8426
  useCloneNotebook,
7335
8427
  useRefreshNotebook,
7336
8428
  useRefreshSpaceNotebooks,
8429
+ // Decks
8430
+ useDeck,
8431
+ useDecksBySpace,
8432
+ useCreateDeck,
8433
+ useUpdateDeck,
8434
+ useUpdateDeckModel,
8435
+ useCloneDeck,
7337
8436
  // Documents
7338
8437
  useDocument,
7339
8438
  useDocumentsBySpace,
@@ -7387,18 +8486,33 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7387
8486
  useMakeItemPublic,
7388
8487
  useMakeItemPrivate,
7389
8488
  useSearchPublicItems,
7390
- usePublicItems,
8489
+ useLibraryArtifacts,
7391
8490
  usePublications,
7392
- useRefreshPublicItems,
8491
+ // Library (search, orbits, featuring, images, reuse)
8492
+ useFeaturedItems,
8493
+ useAllFeaturedItems,
8494
+ usePublicItem,
8495
+ useAccountPublications,
8496
+ useMyOrbits,
8497
+ useAccountOrbits,
8498
+ useItemOrbiters,
8499
+ useItemDerivations,
8500
+ useOrbitItem,
8501
+ useUnorbitItem,
8502
+ useFeatureItem,
8503
+ useUnfeatureItem,
8504
+ useSetFeaturedRank,
8505
+ useSetItemImage,
8506
+ useResetItemImage,
8507
+ useCountItemReuse,
8508
+ useSyncArtifactCatalogue,
8509
+ usePublishArtifact,
8510
+ useUnpublishArtifact,
8511
+ useUpdateArtifactDetails,
7393
8512
  useRefreshSpaceItems,
7394
8513
  useClearCachedPublicItems,
7395
8514
  useClearCachedItems,
7396
8515
  // Pages
7397
- usePage,
7398
- usePages,
7399
- useCreatePage,
7400
- useUpdatePage,
7401
- useDeletePage,
7402
8516
  // Secrets
7403
8517
  useSecret,
7404
8518
  useSecrets,
@@ -7497,6 +8611,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7497
8611
  documentUploadLoading,
7498
8612
  documentUploadProgress,
7499
8613
  resetDocumentUpload,
8614
+ deckUploadLoading,
8615
+ deckUploadProgress,
8616
+ resetDeckUpload,
7500
8617
  };
7501
8618
  };
7502
8619
  export default useCache;