@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,230 +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"}`);
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"}`);