@thatopen/services 0.0.3 → 0.0.5

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 -89
  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,230 @@
1
+ /* MD
2
+ ## Folder management — create, rename, archive, and commit
3
+ ---
4
+ A project reaches the construction phase and the coordination team needs to
5
+ restructure the folder hierarchy: add a "Facades" discipline folder, rename
6
+ "Structural" to its official abbreviation, and archive two superseded folders that
7
+ have been merged. Doing these one at a time through separate API calls means the
8
+ folder tree flickers between invalid states as each change lands — a freshly created
9
+ folder appears before its sibling is renamed, and the archived folders briefly
10
+ disappear before the new one is visible.
11
+
12
+ CDEManager stages all folder changes in memory first. Adds, renames, and archives
13
+ accumulate in a `pendingChanges` list before anything is sent to the backend. The
14
+ full diff is readable from the machine state at any point, so the UI can preview
15
+ exactly what the folder tree will look like after the commit. Cancelling throws away
16
+ all staged changes in one step. Saving applies everything in the right dependency
17
+ order and fires a single completion event with the counts.
18
+
19
+ This tutorial covers entering folder edit mode; staging new folders with a temporary
20
+ ID that gets resolved to a real ID on save; staging renames and observing how
21
+ renaming back to the original value automatically removes the change; the
22
+ archive-folder toggle behavior, where sending the event a second time unstages the
23
+ archive; removing a staged temp folder and all its staged children in one event;
24
+ reverting all staged changes for a single folder; and committing the full batch.
25
+
26
+ By the end, you'll have a working demonstration of every folder-management state
27
+ machine flow — stage, inspect, revert, and commit — against mock data with no
28
+ backend connection needed for the staging sections.
29
+ */
30
+
31
+ import * as OBC from "@thatopen/components";
32
+ // In production platform apps, CDEManager is loaded through the platform client:
33
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
34
+ // Here we import directly from source for local testing.
35
+ import { CDEManager } from "../../index";
36
+
37
+ /* MD
38
+ ### 🛠️ Setting up
39
+ The save path requires a live client context — `_saveFolderChanges` returns early if
40
+ no context is set and the machine would stay stuck in `savingFolderChanges` forever.
41
+ We keep the mock context active throughout the entire example so the commit section
42
+ works correctly.
43
+ */
44
+
45
+ const components = new OBC.Components();
46
+ const cde = components.get(CDEManager);
47
+
48
+ const output = document.getElementById("output") as HTMLPreElement;
49
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
50
+
51
+ let folderIdx = 0;
52
+ const mockClient = {
53
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
54
+ getProjectData: async () => ({ project: { title: "" }, users: [] }),
55
+ listFolders: async () => [],
56
+ archiveFolder: async (_id: string) => ({}),
57
+ updateFolder: async (_id: string, _updates: any) => ({}),
58
+ createFolder: async (name: string, parentId?: string) => ({
59
+ _id: `created-${folderIdx++}`, name, parentId: parentId ?? null,
60
+ }),
61
+ };
62
+
63
+ cde.ctx = { client: mockClient as any, projectId: "mock" };
64
+ await cde.loadFolders();
65
+
66
+ const folders = [
67
+ { _id: "folder-str", name: "Structural", parentId: null },
68
+ { _id: "folder-arch", name: "Architecture", parentId: null },
69
+ { _id: "folder-mep", name: "MEP", parentId: "folder-arch" },
70
+ ];
71
+ cde.folders = folders as any;
72
+
73
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
74
+ cde.onFolderChangesCompleted.add(({ created, renamed, archived }) =>
75
+ log(` [onFolderChangesCompleted] created: ${created}, renamed: ${renamed}, archived: ${archived}`)
76
+ );
77
+
78
+ /* MD
79
+ ### 📁 Staging folder changes
80
+ New folders are added with a temporary ID assigned by the caller — the UI can use it
81
+ to identify the row in the pending list before the real ID arrives from the backend.
82
+ Renames and archives for existing folders are keyed by their real ID. All three
83
+ change kinds accumulate in the same `pendingChanges` array, so the UI can render a
84
+ single diff view for the whole operation.
85
+
86
+ Cancelling discards everything and returns to idle. Unlike file bulk editing, folder
87
+ editing has no concept of "return to previous selection" — it always returns to idle.
88
+ */
89
+
90
+ log("\n--- stage changes and cancel ---");
91
+ cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
92
+
93
+ // New root-level folder.
94
+ cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades", name: "Facades", parentId: null });
95
+ // New folder nested inside the temp folder above.
96
+ cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades_sub", name: "Glazing", parentId: "temp_facades" });
97
+
98
+ // Rename an existing folder.
99
+ cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
100
+
101
+ // Archive an existing folder.
102
+ cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-mep" });
103
+
104
+ if (cde.machineState.kind === "editingFolders") {
105
+ const changes = cde.machineState.pendingChanges;
106
+ const creates = changes.filter(c => c.kind === "create").length;
107
+ const renames = changes.filter(c => c.kind === "rename").length;
108
+ const archives = changes.filter(c => c.kind === "archive").length;
109
+ log(` pending: ${creates} create(s), ${renames} rename(s), ${archives} archive(s)`);
110
+ }
111
+
112
+ cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
113
+ log(` state after cancel: ${cde.machineState.kind}`);
114
+ log(` folders unchanged: ${cde.folders.length}`);
115
+
116
+ /* MD
117
+ ### 🔍 Smart rename diff and archive toggle
118
+ Renaming an existing folder back to its current name automatically removes the
119
+ rename change from the pending list — the same smart diffing used for file metadata.
120
+ This keeps the pending list clean so the commit only sends real changes to the
121
+ backend.
122
+
123
+ Archiving the same existing folder twice toggles the staged archive off. This is
124
+ different from the file-editing archive, which needs an explicit unarchive event to
125
+ remove the mark. For folders, sending the archive event again is the undo gesture.
126
+ */
127
+
128
+ log("\n--- smart rename diff ---");
129
+ cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
130
+
131
+ cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
132
+ if (cde.machineState.kind === "editingFolders") {
133
+ log(` after rename "STR": ${cde.machineState.pendingChanges.filter(c => c.kind === "rename").length} rename(s)`);
134
+ }
135
+
136
+ // Renaming back to the original name removes the change automatically.
137
+ cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "Structural" });
138
+ if (cde.machineState.kind === "editingFolders") {
139
+ log(` after revert to "Structural": ${cde.machineState.pendingChanges.filter(c => c.kind === "rename").length} rename(s)`);
140
+ }
141
+
142
+ log("\n--- archive toggle ---");
143
+ cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-arch" });
144
+ if (cde.machineState.kind === "editingFolders") {
145
+ log(` after first ARCHIVE_FOLDER: ${cde.machineState.pendingChanges.filter(c => c.kind === "archive").length} archive(s)`);
146
+ }
147
+
148
+ cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-arch" });
149
+ if (cde.machineState.kind === "editingFolders") {
150
+ log(` after second ARCHIVE_FOLDER (toggle): ${cde.machineState.pendingChanges.filter(c => c.kind === "archive").length} archive(s)`);
151
+ }
152
+
153
+ cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
154
+
155
+ /* MD
156
+ ### 🗑️ Removing temp folders and reverting existing ones
157
+ When a staged new folder is archived or reset, it is removed from the pending list
158
+ entirely — there is nothing to archive on the backend yet because it was never
159
+ created. Any folders staged as children of the removed folder are removed along with
160
+ it, so the pending list never contains orphaned children.
161
+
162
+ For existing folders, a reset event clears all staged changes for that folder at
163
+ once — both a pending rename and a pending archive in the same step — without
164
+ affecting any other staged changes.
165
+ */
166
+
167
+ log("\n--- remove temp folder cascade ---");
168
+ cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
169
+
170
+ // Stage a temp parent with two children.
171
+ cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_p", name: "Parent", parentId: null });
172
+ cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_c1", name: "Child A", parentId: "temp_p" });
173
+ cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_c2", name: "Child B", parentId: "temp_p" });
174
+
175
+ if (cde.machineState.kind === "editingFolders") {
176
+ log(` staged before archive: ${cde.machineState.pendingChanges.length} change(s)`);
177
+ }
178
+
179
+ // Archiving a temp folder removes it AND all its staged children.
180
+ cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "temp_p" });
181
+
182
+ if (cde.machineState.kind === "editingFolders") {
183
+ log(` staged after archive temp parent: ${cde.machineState.pendingChanges.length} change(s)`);
184
+ }
185
+
186
+ log("\n--- reset existing folder ---");
187
+ cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
188
+ cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-str" });
189
+
190
+ if (cde.machineState.kind === "editingFolders") {
191
+ const changes = cde.machineState.pendingChanges;
192
+ log(` before reset: ${changes.filter(c => c.kind === "rename" || c.kind === "archive").length} change(s) for folder-str`);
193
+ }
194
+
195
+ // RESET_FOLDER removes all staged changes for folder-str in one event.
196
+ cde.sendMachineEvent({ type: "RESET_FOLDER", folderId: "folder-str" });
197
+
198
+ if (cde.machineState.kind === "editingFolders") {
199
+ const changes = cde.machineState.pendingChanges;
200
+ log(` after reset: ${changes.filter(c => c.kind === "rename" || c.kind === "archive").length} change(s) for folder-str`);
201
+ }
202
+
203
+ cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
204
+
205
+ /* MD
206
+ ### 💾 Committing folder changes
207
+ Saving applies all staged changes to the backend in the correct order: archives
208
+ first, then renames on folders that were not archived, then creates in parent-before-
209
+ child order so that nested new folders resolve to real IDs. After everything is
210
+ persisted, the folder list reloads from the backend and a single completion event
211
+ reports how many folders were created, renamed, and archived.
212
+ */
213
+
214
+ log("\n--- commit ---");
215
+ cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
216
+
217
+ cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades", name: "Facades", parentId: null });
218
+ cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
219
+ cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-mep" });
220
+
221
+ if (cde.machineState.kind === "editingFolders") {
222
+ log(` pending before save: ${cde.machineState.pendingChanges.length} change(s)`);
223
+ }
224
+
225
+ await new Promise<void>(resolve => {
226
+ cde.onFolderChangesCompleted.add(() => resolve());
227
+ cde.sendMachineEvent({ type: "SAVE_FOLDER_CHANGES" });
228
+ });
229
+
230
+ log(` state after commit: ${cde.machineState.kind}`);
@@ -0,0 +1,230 @@
1
+ /* MD
2
+ ## Metadata schema — define, stage, and publish field definitions
3
+ ---
4
+ Every project has its own vocabulary for describing files: discipline codes,
5
+ revision phases, area zones, review statuses. A metadata schema defines which
6
+ fields exist, what values they accept, and whether they are mandatory. Without a
7
+ structured schema, each user invents their own labels and the file list becomes
8
+ impossible to filter or report on consistently.
9
+
10
+ CDEManager stages schema changes in memory before anything is written. New fields
11
+ start with a placeholder name and get their permanent key from their label at save
12
+ time. Edits to existing fields are tracked per-key so the UI can show a diff, and
13
+ changes can be reverted field by field. Deletions are staged as null markers, which
14
+ lets the UI still show the field as "pending removal" rather than making it disappear
15
+ immediately.
16
+
17
+ This tutorial covers entering schema edit mode; adding new fields and updating their
18
+ definitions; observing that updating a field back to its original values removes it
19
+ from the pending list; staging a deletion and restoring it; and committing the full
20
+ batch, including how new field keys are derived from their label text.
21
+
22
+ By the end, you'll understand the full schema editing lifecycle and how the machine
23
+ state carries a complete diff — additions, updates, and deletions — before any
24
+ changes reach the backend.
25
+ */
26
+
27
+ import * as OBC from "@thatopen/components";
28
+ // In production platform apps, CDEManager is loaded through the platform client:
29
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
30
+ // Here we import directly from source for local testing.
31
+ import { CDEManager } from "../../index";
32
+ import type { CDEMetadataField } from "../../src/types";
33
+
34
+ /* MD
35
+ ### 🛠️ Setting up
36
+ Schema editing is gated by the manage permission. We use the mock context pattern
37
+ to load permissions, then clear the context — schema staging is entirely in-memory
38
+ and does not need a backend connection. Saving without a context skips persistence
39
+ but still applies the changes locally, which is enough to verify the machine behavior.
40
+ */
41
+
42
+ const components = new OBC.Components();
43
+ const cde = components.get(CDEManager);
44
+
45
+ const output = document.getElementById("output") as HTMLPreElement;
46
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
47
+
48
+ cde.ctx = {
49
+ client: {
50
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
51
+ getProjectData: async () => ({ project: { title: "" }, users: [] }),
52
+ listFolders: async () => [],
53
+ } as any,
54
+ projectId: "mock",
55
+ };
56
+ await cde.loadFolders();
57
+ cde.ctx = undefined;
58
+
59
+ // Seed an initial schema to work with.
60
+ cde.metadataSchema = {
61
+ discipline: { label: "Discipline", type: "string", required: true },
62
+ phase: { label: "Phase", type: "string", required: false },
63
+ };
64
+
65
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
66
+
67
+ /* MD
68
+ ### ➕ Adding and updating fields
69
+ New fields are added with a placeholder definition and a machine-generated temporary
70
+ key. The actual key is derived from the field label when the schema is saved, so the
71
+ caller only needs to set a meaningful label — not invent a key manually.
72
+
73
+ Any update to a field is stored as a replacement of the entire field definition under
74
+ that key. If all properties are changed back to the original values, the edit is
75
+ removed from the pending list automatically — no explicit undo step is needed.
76
+ */
77
+
78
+ log("\n--- add and update ---");
79
+ cde.sendMachineEvent({ type: "START_CONFIGURE_METADATA_SCHEMA" });
80
+
81
+ // New field starts as "New Field" with a temp key like "field_1234567890".
82
+ cde.sendMachineEvent({ type: "ADD_METADATA_FIELD" });
83
+
84
+ if (cde.machineState.kind === "configuringMetadataSchema") {
85
+ const tempKey = Object.keys(cde.machineState.pendingEdits).find(k => k.startsWith("field_"))!;
86
+ log(` new field temp key: ${tempKey.slice(0, 10)}…`);
87
+
88
+ // Give it a real label and type.
89
+ cde.sendMachineEvent({
90
+ type: "UPDATE_METADATA_FIELD",
91
+ key: tempKey,
92
+ value: { label: "Issue Phase", type: "list", required: false, options: [{ value: "open", label: "Open" }, { value: "closed", label: "Closed" }] },
93
+ });
94
+
95
+ const pending = Object.keys(cde.machineState.pendingEdits);
96
+ log(` pending fields after update: ${pending.length}`);
97
+ }
98
+
99
+ // Update an existing field — changes are tracked separately from new ones.
100
+ cde.sendMachineEvent({
101
+ type: "UPDATE_METADATA_FIELD",
102
+ key: "discipline",
103
+ value: { label: "Discipline", type: "string", required: false },
104
+ });
105
+
106
+ if (cde.machineState.kind === "configuringMetadataSchema") {
107
+ log(` discipline pending: ${Object.keys(cde.machineState.pendingEdits).includes("discipline")}`);
108
+ }
109
+
110
+ cde.sendMachineEvent({ type: "CANCEL_CONFIGURE_METADATA_SCHEMA" });
111
+
112
+ /* MD
113
+ ### 🔍 Smart update diff
114
+ Updating a field back to exactly its original values removes it from the pending
115
+ list. This keeps the diff clean — the UI doesn't need to track which changes were
116
+ reverted, it just reads the pending list.
117
+ */
118
+
119
+ log("\n--- smart diff on update ---");
120
+ cde.sendMachineEvent({ type: "START_CONFIGURE_METADATA_SCHEMA" });
121
+
122
+ // Change discipline's required flag.
123
+ cde.sendMachineEvent({
124
+ type: "UPDATE_METADATA_FIELD",
125
+ key: "discipline",
126
+ value: { label: "Discipline", type: "string", required: false },
127
+ });
128
+ if (cde.machineState.kind === "configuringMetadataSchema") {
129
+ log(` discipline in pending: ${Object.keys(cde.machineState.pendingEdits).includes("discipline")}`);
130
+ }
131
+
132
+ // Revert to the original — removes it from the pending list automatically.
133
+ cde.sendMachineEvent({
134
+ type: "UPDATE_METADATA_FIELD",
135
+ key: "discipline",
136
+ value: { label: "Discipline", type: "string", required: true },
137
+ });
138
+ if (cde.machineState.kind === "configuringMetadataSchema") {
139
+ log(` discipline in pending after revert: ${Object.keys(cde.machineState.pendingEdits).includes("discipline")}`);
140
+ }
141
+
142
+ cde.sendMachineEvent({ type: "CANCEL_CONFIGURE_METADATA_SCHEMA" });
143
+
144
+ /* MD
145
+ ### 🗑️ Deleting and restoring fields
146
+ Deleting an existing field marks it as null in the pending list rather than removing
147
+ it outright, so the UI can still show it as "pending deletion" and let the user
148
+ change their mind. Restoring removes the null marker — the field is back to its
149
+ current state as if it was never staged for deletion.
150
+
151
+ Deleting a field that was just added (and therefore does not yet exist in the schema)
152
+ removes it from the pending list entirely — there is nothing to mark for deletion
153
+ because there is nothing to undo.
154
+ */
155
+
156
+ log("\n--- delete and restore ---");
157
+ cde.sendMachineEvent({ type: "START_CONFIGURE_METADATA_SCHEMA" });
158
+
159
+ // Stage deletion of an existing field.
160
+ cde.sendMachineEvent({ type: "DELETE_METADATA_FIELD", key: "phase" });
161
+ if (cde.machineState.kind === "configuringMetadataSchema") {
162
+ log(` phase value in pending: ${JSON.stringify(cde.machineState.pendingEdits["phase"])}`);
163
+ }
164
+
165
+ // Restore it — null marker removed.
166
+ cde.sendMachineEvent({ type: "RESTORE_METADATA_FIELD", key: "phase" });
167
+ if (cde.machineState.kind === "configuringMetadataSchema") {
168
+ log(` phase in pending after restore: ${"phase" in cde.machineState.pendingEdits}`);
169
+ }
170
+
171
+ // Add a new field, then delete it — removes it entirely (no null marker needed).
172
+ cde.sendMachineEvent({ type: "ADD_METADATA_FIELD" });
173
+ const newTempKey = Object.keys(
174
+ (cde.machineState as any).pendingEdits
175
+ ).find((k: string) => k.startsWith("field_"))!;
176
+ cde.sendMachineEvent({ type: "DELETE_METADATA_FIELD", key: newTempKey });
177
+ if (cde.machineState.kind === "configuringMetadataSchema") {
178
+ log(` pending after deleting new field: ${Object.keys(cde.machineState.pendingEdits).length}`);
179
+ }
180
+
181
+ cde.sendMachineEvent({ type: "CANCEL_CONFIGURE_METADATA_SCHEMA" });
182
+
183
+ /* MD
184
+ ### 💾 Committing the schema
185
+ When the schema is saved, new fields get their permanent key derived from their label
186
+ — spaces become underscores, special characters are stripped. If the derived key
187
+ would collide with an existing one, a numeric suffix is appended automatically.
188
+ After the commit, the pending list is empty and the schema reflects all the staged
189
+ changes.
190
+ */
191
+
192
+ log("\n--- commit ---");
193
+ cde.sendMachineEvent({ type: "START_CONFIGURE_METADATA_SCHEMA" });
194
+
195
+ // Add a new field: label "Issue Phase" → key will become "issue_phase".
196
+ cde.sendMachineEvent({ type: "ADD_METADATA_FIELD" });
197
+ const commitTempKey = Object.keys(
198
+ (cde.machineState as any).pendingEdits
199
+ ).find((k: string) => k.startsWith("field_"))!;
200
+ cde.sendMachineEvent({
201
+ type: "UPDATE_METADATA_FIELD",
202
+ key: commitTempKey,
203
+ value: { label: "Issue Phase", type: "string", required: false },
204
+ });
205
+
206
+ // Update an existing field.
207
+ cde.sendMachineEvent({
208
+ type: "UPDATE_METADATA_FIELD",
209
+ key: "discipline",
210
+ value: { label: "Discipline", type: "string", required: false },
211
+ });
212
+
213
+ // Stage deletion of "phase".
214
+ cde.sendMachineEvent({ type: "DELETE_METADATA_FIELD", key: "phase" });
215
+
216
+ if (cde.machineState.kind === "configuringMetadataSchema") {
217
+ log(` pending before save: ${Object.keys(cde.machineState.pendingEdits).length} change(s)`);
218
+ }
219
+
220
+ // Save applies changes locally even without a backend connection.
221
+ await new Promise<void>(resolve => {
222
+ cde.onMachineStateChanged.add((s) => { if (s.kind === "idle") resolve(); });
223
+ cde.sendMachineEvent({ type: "SAVE_METADATA_SCHEMA" });
224
+ });
225
+
226
+ log(` state after save: ${cde.machineState.kind}`);
227
+ log(` schema keys: [${Object.keys(cde.metadataSchema).join(", ")}]`);
228
+ log(` discipline.required: ${cde.metadataSchema["discipline"]?.required}`);
229
+ log(` issue_phase exists: ${"issue_phase" in cde.metadataSchema}`);
230
+ log(` phase removed: ${"phase" in cde.metadataSchema ? "no" : "yes"}`);
@@ -0,0 +1,166 @@
1
+ /* MD
2
+ ## Metadata views — organize fields into named table layouts
3
+ ---
4
+ A structural engineer and an MEP coordinator look at the same file list but care
5
+ about completely different metadata. Showing every field to everyone produces a
6
+ wide table that is hard to navigate. Metadata views let each role define which
7
+ fields appear in which order — saving the layout so the table opens correctly next
8
+ time without requiring a manual setup every session.
9
+
10
+ CDEManager stores a list of named views, each carrying an ordered set of field keys.
11
+ An active view filters both which columns are visible and which metadata groupings
12
+ appear in the sidebar. Changes to the view list are staged in memory first, just like
13
+ schema changes, and saved in one batch — so the user can add, rename, and remove
14
+ views without any partial state reaching the backend.
15
+
16
+ This tutorial covers initializing views from a saved configuration; switching the
17
+ active view and reading the result; adding, updating, and removing views in the
18
+ pending list; and committing the full batch, which saves to the user's personal
19
+ config and fires an event with the final view list.
20
+
21
+ By the end, you'll know how to load, display, edit, and persist named metadata
22
+ layouts through the state machine.
23
+ */
24
+
25
+ import * as OBC from "@thatopen/components";
26
+ // In production platform apps, CDEManager is loaded through the platform client:
27
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
28
+ // Here we import directly from source for local testing.
29
+ import { CDEManager } from "../../index";
30
+ import type { CDEMetadataView } from "../../src/types";
31
+
32
+ /* MD
33
+ ### 🛠️ Setting up
34
+ Loading views from a saved config and switching the active view don't require
35
+ permissions. We still simulate the startup step to load permissions so the save
36
+ path works later, then clear the context — staging is in-memory only.
37
+ */
38
+
39
+ const components = new OBC.Components();
40
+ const cde = components.get(CDEManager);
41
+
42
+ const output = document.getElementById("output") as HTMLPreElement;
43
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
44
+
45
+ cde.ctx = {
46
+ client: {
47
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
48
+ getProjectData: async () => ({ project: { title: "" }, users: [] }),
49
+ listFolders: async () => [],
50
+ } as any,
51
+ projectId: "mock",
52
+ };
53
+ await cde.loadFolders();
54
+ cde.ctx = undefined;
55
+
56
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
57
+ cde.onMetadataViewsChanged.add((views) => log(` [onMetadataViewsChanged] ${views.length} view(s)`));
58
+
59
+ /* MD
60
+ ### 📋 Initializing views from config
61
+ After loading from the backend, views and the last active view ID are pushed into
62
+ CDEManager through `initMetadataViews`. This populates the internal view list and
63
+ fires the views-changed event so any subscribed component can render immediately.
64
+ The active view controls which fields `getSelectedFiles` uses for grouping when
65
+ metadata mode is on.
66
+ */
67
+
68
+ log("\n--- init views ---");
69
+ const savedViews: CDEMetadataView[] = [
70
+ { id: "view-str", name: "Structural", fields: ["discipline", "phase"] },
71
+ { id: "view-mep", name: "MEP", fields: ["discipline", "zone"] },
72
+ ];
73
+ cde.initMetadataViews(savedViews, "view-str");
74
+
75
+ log(` metadataViews: ${cde.metadataViews.length}`);
76
+ log(` activeMetadataViewId: ${cde.activeMetadataViewId}`);
77
+ log(` activeMetadataView name: ${cde.activeMetadataView?.name}`);
78
+
79
+ /* MD
80
+ ### 🔀 Switching the active view
81
+ The active view can be changed from idle, a selection, or while the viewer is open.
82
+ Switching resets the selected metadata group so the table doesn't show a stale
83
+ filter from the previous view's grouping. The transition doesn't change the machine
84
+ state kind — it's a targeted property update, not a full state change.
85
+ */
86
+
87
+ log("\n--- switch active view ---");
88
+ cde.sendMachineEvent({ type: "SET_ACTIVE_METADATA_VIEW", id: "view-mep" });
89
+ log(` active: ${cde.activeMetadataView?.name}`);
90
+
91
+ cde.sendMachineEvent({ type: "SET_ACTIVE_METADATA_VIEW", id: "view-str" });
92
+ log(` active: ${cde.activeMetadataView?.name}`);
93
+
94
+ /* MD
95
+ ### ✏️ Staging view changes
96
+ Adding, updating, and removing views all operate on the `pendingViews` list inside
97
+ the machine state. The committed view list is unchanged until the save fires. Adding
98
+ a view generates a stable random ID immediately, so the UI can reference it for
99
+ subsequent updates within the same session without waiting for a server response.
100
+ */
101
+
102
+ log("\n--- stage: add, update, delete ---");
103
+ cde.sendMachineEvent({ type: "START_CONFIGURE_METADATA_VIEWS" });
104
+
105
+ // Add a new view — gets a UUID immediately.
106
+ cde.sendMachineEvent({ type: "ADD_METADATA_VIEW" });
107
+
108
+ if (cde.machineState.kind === "configuringMetadataViews") {
109
+ const newView = cde.machineState.pendingViews.find(v => v.name === "New View")!;
110
+ log(` new view id: ${newView.id.slice(0, 8)}…`);
111
+
112
+ // Name it and set its fields.
113
+ cde.sendMachineEvent({
114
+ type: "UPDATE_METADATA_VIEW",
115
+ id: newView.id,
116
+ view: { ...newView, name: "Civil", fields: ["discipline", "zone", "level"] },
117
+ });
118
+
119
+ log(` views in pending: ${cde.machineState.pendingViews.length}`);
120
+ log(` updated view name: ${cde.machineState.pendingViews.find(v => v.id === newView.id)?.name}`);
121
+ }
122
+
123
+ // Remove an existing view from the pending list.
124
+ cde.sendMachineEvent({ type: "DELETE_METADATA_VIEW", id: "view-mep" });
125
+ if (cde.machineState.kind === "configuringMetadataViews") {
126
+ log(` views after delete: ${cde.machineState.pendingViews.length}`);
127
+ }
128
+
129
+ cde.sendMachineEvent({ type: "CANCEL_CONFIGURE_METADATA_VIEWS" });
130
+ // Cancelled — committed view list is unchanged.
131
+ log(` committed views after cancel: ${cde.metadataViews.length}`);
132
+
133
+ /* MD
134
+ ### 💾 Committing views
135
+ Saving replaces the committed view list with the pending one and fires the views-
136
+ changed event. The user's personal config is updated at the same time if the account
137
+ has create permission — the same config that `loadUserConfig` reads back on the
138
+ next session load. Without a real user identity in the mock, the persistence step is
139
+ silently skipped but the local update still applies.
140
+ */
141
+
142
+ log("\n--- commit ---");
143
+ cde.sendMachineEvent({ type: "START_CONFIGURE_METADATA_VIEWS" });
144
+
145
+ cde.sendMachineEvent({ type: "ADD_METADATA_VIEW" });
146
+ if (cde.machineState.kind === "configuringMetadataViews") {
147
+ const civil = cde.machineState.pendingViews.find(v => v.name === "New View")!;
148
+ cde.sendMachineEvent({
149
+ type: "UPDATE_METADATA_VIEW",
150
+ id: civil.id,
151
+ view: { ...civil, name: "Civil", fields: ["discipline", "zone"] },
152
+ });
153
+ }
154
+ cde.sendMachineEvent({ type: "DELETE_METADATA_VIEW", id: "view-mep" });
155
+
156
+ if (cde.machineState.kind === "configuringMetadataViews") {
157
+ log(` pending: ${cde.machineState.pendingViews.length} view(s)`);
158
+ }
159
+
160
+ await new Promise<void>(resolve => {
161
+ cde.onMachineStateChanged.add((s) => { if (s.kind === "idle") resolve(); });
162
+ cde.sendMachineEvent({ type: "SAVE_METADATA_VIEWS" });
163
+ });
164
+
165
+ log(` state after save: ${cde.machineState.kind}`);
166
+ log(` committed views: ${cde.metadataViews.map(v => v.name).join(", ")}`);