@thatopen/services 0.0.4 → 0.0.6

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 (140) hide show
  1. package/README.md +24 -263
  2. package/dist/cli/commands/swap.d.ts +3 -0
  3. package/dist/cli/lib/beta.d.ts +2 -0
  4. package/dist/cli/lib/config.d.ts +2 -0
  5. package/dist/cli.js +85 -126
  6. package/dist/core/examples/apps.d.ts +1 -0
  7. package/dist/core/examples/components.d.ts +1 -0
  8. package/dist/core/examples/execution.d.ts +1 -0
  9. package/dist/core/examples/file-metadata.d.ts +1 -0
  10. package/dist/core/examples/files.d.ts +1 -0
  11. package/dist/core/examples/folders.d.ts +1 -0
  12. package/dist/core/examples/hidden-files.d.ts +1 -0
  13. package/dist/core/examples/icons.d.ts +1 -0
  14. package/dist/core/examples/permissions.d.ts +1 -0
  15. package/dist/types/items.d.ts +2 -0
  16. package/docs/builtin/paths.json +54 -0
  17. package/docs/cli/create.md +28 -0
  18. package/docs/cli/local-server.md +38 -0
  19. package/docs/cli/login.md +32 -0
  20. package/docs/cli/paths.json +30 -0
  21. package/docs/cli/publish.md +31 -0
  22. package/docs/cli/run.md +26 -0
  23. package/docs/cli/serve.md +43 -0
  24. package/docs/cli/swap.md +30 -0
  25. package/docs/client/paths.json +38 -0
  26. package/package.json +8 -10
  27. package/resources/AGENTS.md +39 -0
  28. package/src/built-in/AppManager/example.ts +86 -0
  29. package/src/built-in/CDEManager/example.ts +214 -0
  30. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -0
  31. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -0
  32. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -0
  33. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -0
  34. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -0
  35. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -0
  36. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -0
  37. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -0
  38. package/src/built-in/FileList/example.ts +107 -0
  39. package/src/built-in/TabbedNavigation/example.ts +115 -0
  40. package/src/built-in/ViewportsManager/example.ts +106 -0
  41. package/src/cli/templates/{shared/app → app}/vite.config.js +42 -23
  42. package/src/cli/templates/{shared/cloud → cloud-component}/vite.config.js +46 -27
  43. package/src/cli/templates/shared/AGENTS.md +8 -30
  44. package/src/cli/templates/shared/CLAUDE.md +1 -1
  45. package/AGENTS.md +0 -86
  46. package/CONTEXT.md +0 -258
  47. package/dist/built-in/index.d.ts +0 -723
  48. package/dist/cli/commands/create-tests.d.ts +0 -3
  49. package/dist/cli/commands/serve-tests.d.ts +0 -3
  50. package/docs/access-backend-data.md +0 -19
  51. package/docs/app-architecture.md +0 -94
  52. package/docs/app-layout.md +0 -139
  53. package/docs/app-wiring.md +0 -123
  54. package/docs/bim-components/coding-conventions.md +0 -128
  55. package/docs/bim-components/element-collections.md +0 -69
  56. package/docs/bim-components/expose-events.md +0 -84
  57. package/docs/bim-components/library-examples.md +0 -28
  58. package/docs/bim-components/observable-collections.md +0 -83
  59. package/docs/bim-components/overview.md +0 -160
  60. package/docs/bim-components/per-frame-updates.md +0 -20
  61. package/docs/bim-components/save-and-restore-state.md +0 -21
  62. package/docs/bim-components/setup-and-cleanup.md +0 -64
  63. package/docs/bim-components/type-conventions.md +0 -49
  64. package/docs/bim-components/user-driven-object-creation.md +0 -56
  65. package/docs/cli-setup.md +0 -54
  66. package/docs/cloud-components.md +0 -74
  67. package/docs/connect-logic-to-ui.md +0 -113
  68. package/docs/previewing.md +0 -45
  69. package/docs/publishing.md +0 -35
  70. package/docs/scaffolding.md +0 -71
  71. package/docs/ui-components/async-actions.md +0 -18
  72. package/docs/ui-components/confirmation-dialog.md +0 -39
  73. package/docs/ui-components/data-table.md +0 -185
  74. package/docs/ui-components/display-text.md +0 -39
  75. package/docs/ui-components/inline-form.md +0 -44
  76. package/docs/ui-components/library-examples.md +0 -24
  77. package/docs/ui-components/overview.md +0 -194
  78. package/docs/ui-components/rendering-patterns.md +0 -129
  79. package/docs/ui-components/sections-layout.md +0 -123
  80. package/docs/update-grid-elements.md +0 -46
  81. package/docs/using-colors.md +0 -32
  82. package/docs/using-icons.md +0 -37
  83. package/src/cli/templates/bim/src/bim-components/CloudRunner/src/index.ts +0 -1
  84. package/src/cli/templates/bim/src/bim-components/CloudRunner/src/types.ts +0 -5
  85. package/src/cli/templates/bim/src/bim-components/index.ts +0 -1
  86. package/src/cli/templates/bim/src/globals.ts +0 -1
  87. package/src/cli/templates/bim/src/setups/index.ts +0 -3
  88. package/src/cli/templates/bim/src/ui-components/app-info-section/index.ts +0 -26
  89. package/src/cli/templates/bim/src/ui-components/app-info-section/src/index.ts +0 -1
  90. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/index.ts +0 -37
  91. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/src/index.ts +0 -1
  92. package/src/cli/templates/bim/src/ui-components/index.ts +0 -2
  93. package/src/cli/templates/bim-beta/CONTEXT.md +0 -241
  94. package/src/cli/templates/bim-beta/package.json +0 -25
  95. package/src/cli/templates/bim-beta/src/app.ts +0 -16
  96. package/src/cli/templates/bim-beta/src/bim-components/CloudRunner/index.ts +0 -91
  97. package/src/cli/templates/bim-beta/src/main.ts +0 -93
  98. package/src/cli/templates/bim-beta/src/setups/cloud-runner.ts +0 -13
  99. package/src/cli/templates/bim-beta/src/setups/ui-manager.ts +0 -27
  100. package/src/cli/templates/bim-beta/src/setups/viewports-manager.ts +0 -22
  101. package/src/cli/templates/bim-beta/src/ui-components/app-info-section/src/types.ts +0 -15
  102. package/src/cli/templates/bim-beta/src/ui-components/cloud-runner-section/src/types.ts +0 -14
  103. package/src/cli/templates/cloud/_thatopen +0 -5
  104. package/src/cli/templates/cloud/package.json +0 -22
  105. package/src/cli/templates/cloud-test/CONTEXT.md +0 -56
  106. package/src/cli/templates/cloud-test/src/main.ts +0 -565
  107. package/src/cli/templates/default/CONTEXT.md +0 -92
  108. package/src/cli/templates/default/package.json +0 -18
  109. package/src/cli/templates/default/src/main.ts +0 -62
  110. package/src/cli/templates/test/CONTEXT.md +0 -53
  111. package/src/cli/templates/test/package.json +0 -25
  112. package/src/cli/templates/test/src/main.ts +0 -955
  113. /package/src/cli/templates/{bim/CONTEXT.md → app/README.md} +0 -0
  114. /package/src/cli/templates/{shared/app → app}/index.html +0 -0
  115. /package/src/cli/templates/{bim → app}/package.json +0 -0
  116. /package/src/cli/templates/{bim → app}/src/app.ts +0 -0
  117. /package/src/cli/templates/{bim → app}/src/bim-components/CloudRunner/index.ts +0 -0
  118. /package/src/cli/templates/{bim-beta → app}/src/bim-components/CloudRunner/src/index.ts +0 -0
  119. /package/src/cli/templates/{bim-beta → app}/src/bim-components/CloudRunner/src/types.ts +0 -0
  120. /package/src/cli/templates/{bim-beta → app}/src/bim-components/index.ts +0 -0
  121. /package/src/cli/templates/{bim-beta → app}/src/globals.ts +0 -0
  122. /package/src/cli/templates/{bim → app}/src/main.ts +0 -0
  123. /package/src/cli/templates/{bim → app}/src/setups/cloud-runner.ts +0 -0
  124. /package/src/cli/templates/{bim-beta → app}/src/setups/index.ts +0 -0
  125. /package/src/cli/templates/{bim → app}/src/setups/ui-manager.ts +0 -0
  126. /package/src/cli/templates/{bim → app}/src/setups/viewports-manager.ts +0 -0
  127. /package/src/cli/templates/{bim-beta → app}/src/ui-components/app-info-section/index.ts +0 -0
  128. /package/src/cli/templates/{bim-beta → app}/src/ui-components/app-info-section/src/index.ts +0 -0
  129. /package/src/cli/templates/{bim → app}/src/ui-components/app-info-section/src/types.ts +0 -0
  130. /package/src/cli/templates/{bim-beta → app}/src/ui-components/cloud-runner-section/index.ts +0 -0
  131. /package/src/cli/templates/{bim-beta → app}/src/ui-components/cloud-runner-section/src/index.ts +0 -0
  132. /package/src/cli/templates/{bim → app}/src/ui-components/cloud-runner-section/src/types.ts +0 -0
  133. /package/src/cli/templates/{bim-beta → app}/src/ui-components/index.ts +0 -0
  134. /package/src/cli/templates/{shared/app → app}/tsconfig.json +0 -0
  135. /package/src/cli/templates/{cloud/CONTEXT.md → cloud-component/README.md} +0 -0
  136. /package/src/cli/templates/{cloud-test → cloud-component}/_thatopen +0 -0
  137. /package/src/cli/templates/{cloud → cloud-component}/declarations.json +0 -0
  138. /package/src/cli/templates/{cloud-test → cloud-component}/package.json +0 -0
  139. /package/src/cli/templates/{cloud → cloud-component}/src/main.ts +0 -0
  140. /package/src/cli/templates/{shared/cloud → cloud-component}/tsconfig.json +0 -0
@@ -0,0 +1,295 @@
1
+ /* MD
2
+ ## File operations — inline edit, rename, view, download, archive
3
+ ---
4
+ BIM coordinators reviewing a model drop repeat the same small actions throughout
5
+ their session: correcting a discipline tag that changed after a scope revision,
6
+ renaming a file to match the agreed convention, opening it alongside the structural
7
+ model to check for clashes, handing it off to a sub-consultant, or removing a
8
+ superseded version from the live set. If every panel in the platform handles these
9
+ separately, two panels looking at the same file at the same time can show
10
+ contradictory states — one says "uploading", another still shows the old name.
11
+
12
+ CDEManager gives all of these operations a single shared state. Every action moves
13
+ through a known transition so any part of the platform — the file table, the viewer,
14
+ the side panel — reacts to the same source of truth without coordinating with every
15
+ other panel individually.
16
+
17
+ This tutorial covers selecting files and editing a metadata value inline; cancelling
18
+ an in-progress edit; committing the edit and reading the updated value back from the
19
+ state; renaming a file optimistically and observing the local update; opening files
20
+ in the viewer, adding a second file to the active view, simulating load completion,
21
+ and removing files from the view one at a time; closing the viewer while keeping the
22
+ selection intact; and, with live credentials, downloading a selection and archiving a
23
+ file through the state machine.
24
+
25
+ By the end, you'll have a working demonstration of every per-file state machine flow
26
+ — inline edit, rename, viewer, download, and archive — with all mock-data flows
27
+ running without any backend connection.
28
+ */
29
+
30
+ import * as OBC from "@thatopen/components";
31
+ // In production platform apps, CDEManager is loaded through the platform client:
32
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
33
+ // Here we import directly from source for local testing.
34
+ import { CDEManager } from "../../index";
35
+ import type { CDEFile } from "../../src/types";
36
+
37
+ /* MD
38
+ ### 🛠️ Setting up
39
+ Most flows in this example run against mock data without a backend connection.
40
+ However, operations like editing metadata and renaming files are permission-gated —
41
+ the platform normally loads those permissions when the app starts. We simulate that
42
+ startup step here so the demo flows work in isolation. We also register viewer groups
43
+ before setting any files because the transition into the viewer validates the group
44
+ configuration at the moment of the event.
45
+ */
46
+
47
+ const components = new OBC.Components();
48
+ const cde = components.get(CDEManager);
49
+
50
+ const output = document.getElementById("output") as HTMLPreElement;
51
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
52
+
53
+ // The "ifc/frag" group allows multiple files open at the same time; "pdf" does not.
54
+ cde.viewerGroups = [
55
+ { extensions: ["ifc", "frag"], multiple: true },
56
+ { extensions: ["pdf"] },
57
+ ];
58
+
59
+ // Simulate the platform startup step that loads user permissions.
60
+ // Without this, permission-gated events (edit, rename, archive) are silently ignored.
61
+ cde.ctx = {
62
+ client: {
63
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
64
+ getProjectData: async () => ({ project: { title: "" }, users: [] }),
65
+ listFolders: async () => [],
66
+ updateFile: async (id: string) => ({ item: { _id: id } }),
67
+ updateFileVersionMetadata: async () => ({}),
68
+ } as any,
69
+ projectId: "mock",
70
+ };
71
+ await cde.loadFolders();
72
+ // Clear the context after loading permissions — mock flows don't make API calls.
73
+ // With a live client, operations like rename would also confirm via onRenameCompleted.
74
+ cde.ctx = undefined;
75
+
76
+ const now = new Date();
77
+ const mockFiles: CDEFile[] = [
78
+ {
79
+ _id: "f1", name: "column.ifc", folderId: null,
80
+ createdBy: "alice", createdAt: now,
81
+ metadata: { discipline: "STR", phase: "DD" },
82
+ } as CDEFile,
83
+ {
84
+ _id: "f2", name: "foundations.ifc", folderId: null,
85
+ createdBy: "bob", createdAt: now,
86
+ metadata: { discipline: "STR", phase: "CD" },
87
+ } as CDEFile,
88
+ ];
89
+ cde.files = mockFiles;
90
+
91
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
92
+ cde.onFilesChanged.add((files) => log(` [onFilesChanged] ${files.length} file(s)`));
93
+ cde.onRenameCompleted.add(({ success, error }) =>
94
+ log(success ? " [onRenameCompleted] server confirmed" : ` [onRenameCompleted] rolled back — ${error}`)
95
+ );
96
+ cde.onDownloadReady.add(({ blob, filename }) =>
97
+ log(` [onDownloadReady] "${filename}" — ${blob.size} bytes`)
98
+ );
99
+
100
+ /* MD
101
+ ### ✏️ Inline metadata edit
102
+ When a coordinator spots a wrong discipline tag, they should be able to correct it
103
+ right in the file row rather than navigating to a separate form. The edit stays
104
+ pending — visible as an unsaved diff — so the UI can show exactly what will change
105
+ before anything is written.
106
+
107
+ Entering the edit state carries the selected files along, so the form always knows
108
+ which file it is editing. Cancelling discards all pending values and returns to the
109
+ exact same selection — nothing is sent to the backend.
110
+ */
111
+
112
+ log("\n--- inline edit: cancel ---");
113
+ cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0]] });
114
+ cde.sendMachineEvent({ type: "START_EDIT" });
115
+ cde.sendMachineEvent({ type: "UPDATE_METADATA", key: "phase", value: "SD" });
116
+
117
+ if (cde.machineState.kind === "editing") {
118
+ log(` pending phase: ${cde.machineState.pendingMetadata.phase}`);
119
+ }
120
+
121
+ cde.sendMachineEvent({ type: "CANCEL_EDIT" });
122
+ // The original mockFiles[0] reference is unchanged — nothing was applied.
123
+ log(` phase after cancel: ${mockFiles[0].metadata.phase}`);
124
+
125
+ /* MD
126
+ Committing the edit applies the pending values to the file in memory and returns to
127
+ the selection, with the file already reflecting its new values. With a live backend,
128
+ the changes are persisted before the transition completes; without one, the local
129
+ update still happens. Either way, the updated file is in the machine state's
130
+ selection — not in the original array reference.
131
+ */
132
+
133
+ log("\n--- inline edit: save ---");
134
+ // Re-enter from the current "selected" state.
135
+ cde.sendMachineEvent({ type: "START_EDIT" });
136
+ cde.sendMachineEvent({ type: "UPDATE_METADATA", key: "phase", value: "SD" });
137
+ cde.sendMachineEvent({ type: "SAVE" });
138
+
139
+ // _saveFileMetadata without a client applies the edit locally and returns synchronously.
140
+ if (cde.machineState.kind === "selected") {
141
+ log(` phase after save: ${cde.machineState.files[0].metadata.phase}`);
142
+ }
143
+
144
+ /* MD
145
+ ### 🏷️ Optimistic rename
146
+ Renaming a file can't wait for a server round-trip — the user should see the new
147
+ name appear immediately. The rename applies locally the moment it is triggered, then
148
+ a confirmation arrives once the backend has saved it. If the server rejects the
149
+ change, the name reverts automatically and a rollback event is fired so the UI can
150
+ notify the user.
151
+
152
+ Without a live backend, the local rename still applies and the file list updates —
153
+ only the server confirmation step is skipped, so the rollback path is never reached.
154
+ */
155
+
156
+ log("\n--- rename immediate ---");
157
+ cde.sendMachineEvent({ type: "RENAME_FILE_IMMEDIATE", fileId: "f1", name: "column-v2.ifc" });
158
+
159
+ const renamedFile = cde.files.find(f => f._id === "f1")!;
160
+ log(` local name: ${renamedFile.name}`);
161
+ // onRenameCompleted fires only with a live client — skipped here.
162
+
163
+ // Restore the original name so the view-flow demo works with "column.ifc".
164
+ cde.sendMachineEvent({ type: "RENAME_FILE_IMMEDIATE", fileId: "f1", name: "column.ifc" });
165
+
166
+ /* MD
167
+ ### 👁️ Viewer
168
+ Opening files in the viewer is a state transition, not a direct method call. This
169
+ keeps the viewer component decoupled from the file table — any part of the platform
170
+ can request a view by sending an event, and the viewer just reacts to the state
171
+ change. No direct reference from one component to the other is required.
172
+
173
+ When a viewer group supports multiple files, a second can be added while the first
174
+ is still loading. Each file tracks its own load status independently, so the viewer
175
+ can show a spinner per model without needing its own state management.
176
+ */
177
+
178
+ log("\n--- view: open and add ---");
179
+ cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0]] });
180
+ cde.sendMachineEvent({ type: "VIEW" });
181
+
182
+ // The viewer calls this once its geometry is ready — here we simulate it.
183
+ cde.sendMachineEvent({ type: "SET_VIEW_FILE_STATUS", fileId: "f1", status: "ready" });
184
+
185
+ cde.sendMachineEvent({ type: "ADD_TO_VIEW", file: mockFiles[1] });
186
+
187
+ if (cde.machineState.kind === "viewing") {
188
+ log(` files in viewer: ${cde.machineState.files.length}`);
189
+ log(` f1: ${cde.machineState.fileStatus["f1"]}, f2: ${cde.machineState.fileStatus["f2"]}`);
190
+ }
191
+
192
+ cde.sendMachineEvent({ type: "SET_VIEW_FILE_STATUS", fileId: "f2", status: "ready" });
193
+
194
+ /* MD
195
+ Removing the last file from the viewer closes it entirely — the state moves to idle
196
+ rather than back to a selection, since there is nothing left to select. Removing one
197
+ of several files leaves the rest open.
198
+
199
+ Alternatively, closing the viewer explicitly returns to the selection state with the
200
+ same files still highlighted — useful when the user wants to go back to the file
201
+ table without losing which row was active.
202
+ */
203
+
204
+ log("\n--- view: remove one by one ---");
205
+ cde.sendMachineEvent({ type: "REMOVE_FROM_VIEW", fileId: "f1" });
206
+ log(` after removing f1: ${cde.machineState.kind}`);
207
+
208
+ cde.sendMachineEvent({ type: "REMOVE_FROM_VIEW", fileId: "f2" });
209
+ log(` after removing last: ${cde.machineState.kind}`);
210
+
211
+ log("\n--- view: close keeps selection ---");
212
+ cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0]] });
213
+ cde.sendMachineEvent({ type: "VIEW" });
214
+ cde.sendMachineEvent({ type: "CLOSE_VIEW" });
215
+ log(` state: ${cde.machineState.kind}`);
216
+ if (cde.machineState.kind === "selected") {
217
+ log(` selection preserved: ${cde.machineState.files.length} file(s)`);
218
+ }
219
+
220
+ /* MD
221
+ ### 🌐 Download and archive (requires credentials)
222
+ Download has to fetch file content from the backend, and archive needs the account
223
+ to have delete access on the project — so both flows require live credentials. Pass
224
+ them as query parameters to run this section:
225
+
226
+ example.html?accessToken=<token>&apiUrl=https://...&projectId=<id>
227
+
228
+ We create a throwaway file here so the demo is self-contained and leaves no
229
+ permanent changes in the project.
230
+ */
231
+
232
+ const params = new URLSearchParams(location.search);
233
+ const accessToken = params.get("accessToken");
234
+ const apiUrl = params.get("apiUrl");
235
+ const projectId = params.get("projectId");
236
+
237
+ if (accessToken && apiUrl && projectId) {
238
+ const { EngineServicesClient } = await import("thatopen-services");
239
+ const client = new EngineServicesClient(accessToken, apiUrl);
240
+ // Replace the mock context with the real one.
241
+ cde.ctx = { client, projectId };
242
+
243
+ // loadFolders also resolves user permissions — needed before archive.
244
+ await cde.loadFolders();
245
+ log(`\nProject: "${cde.projectName}", canDelete: ${cde.userPermissions.canDelete}`);
246
+
247
+ // Create a throwaway file so the demo doesn't touch existing project data.
248
+ const blob = new Blob([JSON.stringify({ demo: true })], { type: "application/json" });
249
+ const { item } = await client.createFile({
250
+ file: blob, name: `__example-test__-${Date.now()}.json`, versionTag: "v1", projectId,
251
+ });
252
+ const liveFile: CDEFile = {
253
+ ...item,
254
+ createdAt: new Date(item.createdAt),
255
+ createdBy: "",
256
+ metadata: {},
257
+ } as CDEFile;
258
+ cde.files = [liveFile];
259
+ log(`Created test file: ${item._id}`);
260
+
261
+ // --- Download ---
262
+ log("\n--- download ---");
263
+ cde.sendMachineEvent({ type: "SELECT", files: [liveFile] });
264
+ // onDownloadReady fires with { blob, filename } — multiple selected files are zipped.
265
+ await new Promise<void>(resolve => {
266
+ cde.onDownloadReady.add(() => resolve());
267
+ cde.sendMachineEvent({ type: "DOWNLOAD" });
268
+ });
269
+
270
+ // --- Archive ---
271
+ // ARCHIVE_FILE is silently ignored if the account lacks delete permission.
272
+ if (cde.userPermissions.canDelete) {
273
+ log("\n--- archive ---");
274
+ cde.sendMachineEvent({ type: "SELECT", files: [liveFile] });
275
+ await new Promise<void>(resolve => {
276
+ cde.onMachineStateChanged.add((s) => {
277
+ if (s.kind === "idle" || s.kind === "error") resolve();
278
+ });
279
+ cde.sendMachineEvent({ type: "ARCHIVE_FILE" });
280
+ });
281
+
282
+ if (cde.machineState.kind === "error") {
283
+ log(` archive failed: ${cde.machineState.reason}`);
284
+ cde.sendMachineEvent({ type: "DISMISS" });
285
+ await client.archiveFile(item._id as string);
286
+ } else {
287
+ log(` archived. Files in cde: ${cde.files.length}`);
288
+ }
289
+ } else {
290
+ log("Archive skipped — no delete permission. Cleaning up directly.");
291
+ await client.archiveFile(item._id as string);
292
+ }
293
+ } else {
294
+ log("\nDownload and archive skipped — pass ?accessToken=&apiUrl=&projectId= to run.");
295
+ }
@@ -0,0 +1,210 @@
1
+ /* MD
2
+ ## File uploads, versioning, and the download cache
3
+ ---
4
+ A site coordinator drops a batch of files onto the CDE every Monday after the
5
+ weekend review: some are entirely new models, some are revised versions of files
6
+ already in the project, and sometimes the metadata group they upload into should
7
+ be stamped automatically onto the new files. If the platform makes the coordinator
8
+ sort this out manually, it slows the handoff and introduces naming errors. And if
9
+ an upload fails halfway through a batch, the file table should land in a safe state
10
+ rather than freeze at a loading spinner.
11
+
12
+ CDEManager's upload method compares each incoming file against the current list by
13
+ base name and extension. Files with no match are created from scratch; files that
14
+ match an existing entry produce a new version instead. When a metadata group is
15
+ active, the group's values are stamped onto every file in the batch without any
16
+ extra action from the user. The state machine moves through a dedicated uploading
17
+ state so any component in the platform can show a progress indicator by reading a
18
+ single source of truth.
19
+
20
+ This tutorial covers uploading new files and reading the result from the
21
+ upload-completed event; uploading a file whose name matches an existing entry and
22
+ confirming it is versioned rather than duplicated; attaching metadata automatically
23
+ when the upload runs inside an active metadata group; recovering cleanly from an
24
+ upload error through the state machine; and reading file content through the
25
+ in-memory cache that avoids re-fetching the same version twice.
26
+
27
+ By the end, you'll have a working demonstration of the full upload lifecycle —
28
+ create, version, auto-metadata, error recovery — plus the cache hit and miss
29
+ behavior that the viewer relies on when loading models.
30
+ */
31
+
32
+ import * as OBC from "@thatopen/components";
33
+ // In production platform apps, CDEManager is loaded through the platform client:
34
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
35
+ // Here we import directly from source for local testing.
36
+ import { CDEManager } from "../../index";
37
+ import type { CDEFile } from "../../src/types";
38
+
39
+ /* MD
40
+ ### 🛠️ Setting up
41
+ All flows in this example use a minimal mock client so they run without a backend
42
+ connection. Unlike mock flows that clear the client context after loading permissions,
43
+ upload and download operations require an active client throughout — so the context
44
+ stays set for the full example.
45
+ */
46
+
47
+ const components = new OBC.Components();
48
+ const cde = components.get(CDEManager);
49
+
50
+ const output = document.getElementById("output") as HTMLPreElement;
51
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
52
+
53
+ let fileCounter = 0;
54
+ // Track created version records so the versioning demo can find them.
55
+ const mockVersions: Record<string, { tag: string }[]> = {};
56
+
57
+ const mockClient = {
58
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
59
+ getProjectData: async () => ({ project: { title: "Demo Project" }, users: [] }),
60
+ listFolders: async () => [],
61
+ createFile: async ({ name, versionTag, parentFolderId }: any) => {
62
+ const id = `file-${fileCounter++}`;
63
+ mockVersions[id] = [{ tag: versionTag }];
64
+ return {
65
+ item: { _id: id, name, folderId: parentFolderId ?? null, createdAt: new Date().toISOString(), createdBy: "mock-user", itemType: "FILE" },
66
+ version: { tag: versionTag },
67
+ };
68
+ },
69
+ listVersions: async (fileId: string) => mockVersions[fileId] ?? [],
70
+ createVersion: async (fileId: string, _file: File, tag: string) => {
71
+ mockVersions[fileId] = [...(mockVersions[fileId] ?? []), { tag }];
72
+ return { tag };
73
+ },
74
+ updateFileVersionMetadata: async () => ({}),
75
+ downloadFile: async (_id: string) => ({ arrayBuffer: async () => new ArrayBuffer(4096) }),
76
+ archiveFile: async () => ({}),
77
+ };
78
+
79
+ cde.ctx = { client: mockClient as any, projectId: "mock" };
80
+ await cde.loadFolders();
81
+
82
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
83
+
84
+ /* MD
85
+ ### 📤 Uploading new files
86
+ Calling the upload method transitions the machine to an uploading state, carries
87
+ out the operation, then returns to idle and fires a completion event. The event
88
+ payload separates newly created files from versioned ones, so the UI can show a
89
+ targeted confirmation — "2 files added" versus "1 file updated" — without parsing
90
+ the whole file list.
91
+ */
92
+
93
+ cde.onUploadCompleted.add(({ created, versioned }) => {
94
+ log(` [onUploadCompleted] created: ${created.length}, versioned: ${versioned.length}`);
95
+ for (const f of created) log(` + ${f.name}`);
96
+ for (const f of versioned) log(` ↑ ${f.name}`);
97
+ });
98
+
99
+ log("\n--- upload: create new files ---");
100
+ const f1 = new File([new ArrayBuffer(100)], "column.ifc", { type: "application/octet-stream" });
101
+ const f2 = new File([new ArrayBuffer(50)], "report.pdf");
102
+ await cde.uploadFiles([f1, f2]);
103
+ log(` cde.files after upload: ${cde.files.length}`);
104
+
105
+ /* MD
106
+ ### 🔄 Versioning an existing file
107
+ When the incoming file's base name and extension match an entry already in the list,
108
+ the upload method creates a new version rather than a duplicate. The file counter in
109
+ the version tag is computed from the existing version history, so the tag always
110
+ follows on from whatever the project already has.
111
+ */
112
+
113
+ log("\n--- upload: version existing file ---");
114
+ // "column.ifc" is already in cde.files from the previous upload.
115
+ const f1v2 = new File([new ArrayBuffer(120)], "column.ifc", { type: "application/octet-stream" });
116
+ await cde.uploadFiles([f1v2]);
117
+ log(` cde.files after version upload: ${cde.files.length}`);
118
+
119
+ const colFile = cde.files.find(f => f.name === "column.ifc")!;
120
+ log(` column.ifc version tag: ${(colFile as any).versionTag}`);
121
+
122
+ /* MD
123
+ ### 🏷️ Auto-metadata from a metadata group
124
+ When metadata grouping is active and a group is selected, every file uploaded in
125
+ that session inherits the group's metadata values automatically. The coordinator
126
+ selects the "Structural SD phase" group, drops files, and every new file arrives
127
+ already tagged — no post-upload editing required.
128
+ */
129
+
130
+ log("\n--- upload: auto-metadata ---");
131
+ cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "metadata" });
132
+ cde.selectedMetadataGroup = { discipline: "STR", phase: "SD" };
133
+
134
+ const f3 = new File([new ArrayBuffer(80)], "foundations-v2.ifc", { type: "application/octet-stream" });
135
+ await cde.uploadFiles([f3]);
136
+
137
+ const uploaded = cde.files.find(f => f.name === "foundations-v2.ifc")!;
138
+ log(` discipline: ${uploaded.metadata.discipline}, phase: ${uploaded.metadata.phase}`);
139
+
140
+ // Reset back to groups view for subsequent sections.
141
+ cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "groups" });
142
+ cde.selectedMetadataGroup = null;
143
+
144
+ /* MD
145
+ ### ⚠️ Recovering from an upload error
146
+ If the backend rejects the upload, the machine moves to an error state rather than
147
+ silently returning to idle. The error state carries the reason message so the UI can
148
+ surface it, and sending a dismiss event returns to idle so the user can try again.
149
+ No manual state cleanup is needed.
150
+ */
151
+
152
+ log("\n--- upload: error recovery ---");
153
+ cde.ctx = {
154
+ client: {
155
+ ...mockClient,
156
+ createFile: async () => { throw new Error("Upload failed — server unavailable"); },
157
+ } as any,
158
+ projectId: "mock",
159
+ };
160
+
161
+ const fBroken = new File([new ArrayBuffer(10)], "broken.ifc", { type: "application/octet-stream" });
162
+ await cde.uploadFiles([fBroken]);
163
+
164
+ if (cde.machineState.kind === "error") {
165
+ log(` error reason: ${cde.machineState.reason}`);
166
+ cde.sendMachineEvent({ type: "DISMISS" });
167
+ }
168
+ log(` state after dismiss: ${cde.machineState.kind}`);
169
+
170
+ // Restore the working mock client.
171
+ cde.ctx = { client: mockClient as any, projectId: "mock" };
172
+
173
+ /* MD
174
+ ### 💾 File download cache
175
+ The download method on CDEManager keeps a buffer cache in memory so the viewer
176
+ does not re-fetch the same file version twice during a session. The cache logs each
177
+ access — a miss triggers a download, a hit returns the stored buffer immediately.
178
+ Only extensions in `cacheExtensions` are stored; all others are downloaded every
179
+ time so the cache stays focused on the large binary files that benefit most from it.
180
+
181
+ Note: files uploaded in the same session are already placed in the cache by the
182
+ upload method. The demo below uses a file injected directly — simulating a file
183
+ that arrived from a previous session load — to show a clean miss on the first
184
+ access.
185
+ */
186
+
187
+ log("\n--- download cache ---");
188
+
189
+ // A file loaded from the backend (not uploaded this session) starts with no cache entry.
190
+ const serverFile = {
191
+ _id: "server-ifc", name: "arch.ifc", folderId: null, versionTag: "v3",
192
+ createdBy: "carol", createdAt: new Date(), metadata: {},
193
+ } as CDEFile;
194
+ cde.files = [...cde.files, serverFile];
195
+
196
+ // First access — cache miss, buffer fetched and stored.
197
+ const buf1 = await cde.downloadFile(serverFile);
198
+ log(` arch.ifc first access (miss): ${buf1.byteLength} bytes`);
199
+
200
+ // Second access — cache hit, no network call.
201
+ const buf2 = await cde.downloadFile(serverFile);
202
+ log(` arch.ifc second access (hit): ${buf2.byteLength} bytes`);
203
+
204
+ // PDF extension is not in cacheExtensions — fetched but not stored.
205
+ const pdfFile = cde.files.find(f => f.name === "report.pdf")!;
206
+ const bufPdf = await cde.downloadFile(pdfFile);
207
+ log(` report.pdf access (skip): ${bufPdf.byteLength} bytes`);
208
+
209
+ log(`\n cacheExtensions: [${[...cde.cacheExtensions].join(", ")}]`);
210
+ log(` cacheMaxBytes: ${(cde.cacheMaxBytes / 1024 / 1024).toFixed(0)} MB`);