@thatopen/services 0.3.15 → 0.4.0

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 (68) hide show
  1. package/README.md +62 -62
  2. package/dist/built-in/index.d.ts +165 -2
  3. package/dist/cli.js +27 -13
  4. package/dist/core/client.d.ts +1 -0
  5. package/dist/core/platform-client.d.ts +7 -0
  6. package/dist/index.cjs.js +2 -2
  7. package/dist/index.es.js +396 -374
  8. package/dist/vite-env.d.ts +1 -1
  9. package/docs/builtin/paths.json +94 -86
  10. package/docs/cli/create.md +29 -29
  11. package/docs/cli/local-server.md +38 -38
  12. package/docs/cli/login.md +32 -32
  13. package/docs/cli/publish.md +31 -31
  14. package/docs/cli/run.md +26 -26
  15. package/docs/cli/serve.md +43 -43
  16. package/docs/cli/swap.md +30 -30
  17. package/package.json +4 -4
  18. package/resources/AGENTS.md +40 -40
  19. package/src/built-in/CDEManager/example.ts +214 -214
  20. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  21. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  22. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  23. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  24. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  25. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  26. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  27. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  28. package/src/built-in/ClashesManager/example.ts +283 -283
  29. package/src/built-in/UIManager/src/app/example.ts +97 -97
  30. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
  31. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
  32. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
  33. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
  34. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
  35. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
  36. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
  37. package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -0
  38. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
  39. package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -160
  40. package/src/built-in/UIManager/src/models-list/example.ts +122 -122
  41. package/src/built-in/UIManager/src/topics-panel/example.ts +170 -0
  42. package/src/built-in/index.ts +1048 -881
  43. package/src/cli/templates/app/index.html +35 -35
  44. package/src/cli/templates/app/package.json +32 -32
  45. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  48. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  49. package/src/cli/templates/app/src/globals.ts +1 -1
  50. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
  51. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
  52. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
  53. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
  54. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
  55. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
  56. package/src/cli/templates/app/src/ui-components/index.ts +2 -2
  57. package/src/cli/templates/app/tsconfig.json +16 -16
  58. package/src/cli/templates/app/vite.config.js +57 -57
  59. package/src/cli/templates/cloud-component/README.md +205 -205
  60. package/src/cli/templates/cloud-component/_thatopen +5 -5
  61. package/src/cli/templates/cloud-component/declarations.json +4 -4
  62. package/src/cli/templates/cloud-component/package.json +21 -21
  63. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  64. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  65. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  66. package/src/cli/templates/shared/AGENTS.md +8 -8
  67. package/src/cli/templates/shared/CLAUDE.md +1 -1
  68. package/src/cli/templates/shared/_gitignore +5 -5
@@ -1,295 +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
- }
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
+ }