@things-factory/ops-master 10.1.34 → 10.1.36

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 (151) hide show
  1. package/dist-client/master-change-approval.d.ts +7 -0
  2. package/dist-client/master-change-approval.js +16 -0
  3. package/dist-client/master-change-approval.js.map +1 -1
  4. package/dist-client/pages/master/applied-when-dialog.d.ts +45 -0
  5. package/dist-client/pages/master/applied-when-dialog.js +139 -0
  6. package/dist-client/pages/master/applied-when-dialog.js.map +1 -0
  7. package/dist-client/pages/master/assignment-fields.d.ts +28 -0
  8. package/dist-client/pages/master/assignment-fields.js +55 -0
  9. package/dist-client/pages/master/assignment-fields.js.map +1 -0
  10. package/dist-client/pages/master/equipment.js +1 -0
  11. package/dist-client/pages/master/equipment.js.map +1 -1
  12. package/dist-client/pages/master/master-page-base.d.ts +14 -0
  13. package/dist-client/pages/master/master-page-base.js +40 -4
  14. package/dist-client/pages/master/master-page-base.js.map +1 -1
  15. package/dist-client/pages/master/material-definition.d.ts +4 -11
  16. package/dist-client/pages/master/material-definition.js +5 -53
  17. package/dist-client/pages/master/material-definition.js.map +1 -1
  18. package/dist-client/pages/master/material-revision-fields.d.ts +2 -0
  19. package/dist-client/pages/master/material-revision-fields.js +43 -0
  20. package/dist-client/pages/master/material-revision-fields.js.map +1 -0
  21. package/dist-client/pages/master/operational-location.js +1 -0
  22. package/dist-client/pages/master/operational-location.js.map +1 -1
  23. package/dist-client/pages/master/recipe-definition.d.ts +4 -135
  24. package/dist-client/pages/master/recipe-definition.js +7 -397
  25. package/dist-client/pages/master/recipe-definition.js.map +1 -1
  26. package/dist-client/pages/master/revision-comparison.d.ts +48 -0
  27. package/dist-client/pages/master/revision-comparison.js +553 -0
  28. package/dist-client/pages/master/revision-comparison.js.map +1 -0
  29. package/dist-client/pages/master/revision-view.d.ts +19 -0
  30. package/dist-client/pages/master/revision-view.js +133 -0
  31. package/dist-client/pages/master/revision-view.js.map +1 -0
  32. package/dist-client/pages/master/revision-workspace.d.ts +50 -0
  33. package/dist-client/pages/master/revision-workspace.js +752 -0
  34. package/dist-client/pages/master/revision-workspace.js.map +1 -0
  35. package/dist-client/pages/master/test-specification.d.ts +4 -29
  36. package/dist-client/pages/master/test-specification.js +5 -225
  37. package/dist-client/pages/master/test-specification.js.map +1 -1
  38. package/dist-client/tsconfig.tsbuildinfo +1 -1
  39. package/dist-server/service/assignment/applied-when.d.ts +24 -0
  40. package/dist-server/service/assignment/applied-when.js +16 -0
  41. package/dist-server/service/assignment/applied-when.js.map +1 -0
  42. package/dist-server/service/assignment/assignment-claim.d.ts +111 -0
  43. package/dist-server/service/assignment/assignment-claim.js +160 -0
  44. package/dist-server/service/assignment/assignment-claim.js.map +1 -0
  45. package/dist-server/service/assignment/assignment-history.d.ts +87 -0
  46. package/dist-server/service/assignment/assignment-history.js +250 -0
  47. package/dist-server/service/assignment/assignment-history.js.map +1 -0
  48. package/dist-server/service/assignment/assignment-write.d.ts +25 -0
  49. package/dist-server/service/assignment/assignment-write.js +69 -0
  50. package/dist-server/service/assignment/assignment-write.js.map +1 -0
  51. package/dist-server/service/equipment/equipment-master-resolver.d.ts +9 -1
  52. package/dist-server/service/equipment/equipment-master-resolver.js +6 -3
  53. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -1
  54. package/dist-server/service/index.d.ts +15 -2
  55. package/dist-server/service/index.js +13 -0
  56. package/dist-server/service/index.js.map +1 -1
  57. package/dist-server/service/location/location-master-resolver.d.ts +4 -1
  58. package/dist-server/service/location/location-master-resolver.js +6 -3
  59. package/dist-server/service/location/location-master-resolver.js.map +1 -1
  60. package/dist-server/service/master-axis.js +1 -1
  61. package/dist-server/service/master-axis.js.map +1 -1
  62. package/dist-server/service/master-change-policy.js +7 -2
  63. package/dist-server/service/master-change-policy.js.map +1 -1
  64. package/dist-server/service/master-conformance-resolver.js +11 -1
  65. package/dist-server/service/master-conformance-resolver.js.map +1 -1
  66. package/dist-server/service/master-crud.d.ts +11 -0
  67. package/dist-server/service/master-crud.js +15 -1
  68. package/dist-server/service/master-crud.js.map +1 -1
  69. package/dist-server/service/master-list.js +6 -0
  70. package/dist-server/service/master-list.js.map +1 -1
  71. package/dist-server/service/material/index.d.ts +3 -2
  72. package/dist-server/service/material/index.js +4 -2
  73. package/dist-server/service/material/index.js.map +1 -1
  74. package/dist-server/service/material/material-axis-status.d.ts +8 -0
  75. package/dist-server/service/material/material-axis-status.js +19 -0
  76. package/dist-server/service/material/material-axis-status.js.map +1 -0
  77. package/dist-server/service/material/material-definition.d.ts +3 -0
  78. package/dist-server/service/material/material-definition.js +13 -0
  79. package/dist-server/service/material/material-definition.js.map +1 -1
  80. package/dist-server/service/material/material-effective-list.d.ts +39 -0
  81. package/dist-server/service/material/material-effective-list.js +54 -0
  82. package/dist-server/service/material/material-effective-list.js.map +1 -0
  83. package/dist-server/service/material/material-master-resolver.js +4 -10
  84. package/dist-server/service/material/material-master-resolver.js.map +1 -1
  85. package/dist-server/service/material/material-query.js +9 -2
  86. package/dist-server/service/material/material-query.js.map +1 -1
  87. package/dist-server/service/material/material-revision-content.d.ts +10 -0
  88. package/dist-server/service/material/material-revision-content.js +92 -0
  89. package/dist-server/service/material/material-revision-content.js.map +1 -0
  90. package/dist-server/service/material/material-revision-resolver.d.ts +54 -0
  91. package/dist-server/service/material/material-revision-resolver.js +76 -0
  92. package/dist-server/service/material/material-revision-resolver.js.map +1 -0
  93. package/dist-server/service/material/material-revision-service.d.ts +144 -0
  94. package/dist-server/service/material/material-revision-service.js +547 -0
  95. package/dist-server/service/material/material-revision-service.js.map +1 -0
  96. package/dist-server/service/material/material-revision.d.ts +84 -0
  97. package/dist-server/service/material/material-revision.js +339 -0
  98. package/dist-server/service/material/material-revision.js.map +1 -0
  99. package/dist-server/service/operation/operation-definition.js +1 -0
  100. package/dist-server/service/operation/operation-definition.js.map +1 -1
  101. package/dist-server/service/operation/operation-master-resolver.js +2 -5
  102. package/dist-server/service/operation/operation-master-resolver.js.map +1 -1
  103. package/dist-server/service/recipe/index.d.ts +3 -2
  104. package/dist-server/service/recipe/index.js +4 -2
  105. package/dist-server/service/recipe/index.js.map +1 -1
  106. package/dist-server/service/recipe/recipe-definition.d.ts +4 -0
  107. package/dist-server/service/recipe/recipe-definition.js +14 -1
  108. package/dist-server/service/recipe/recipe-definition.js.map +1 -1
  109. package/dist-server/service/recipe/recipe-effective-list.d.ts +49 -0
  110. package/dist-server/service/recipe/recipe-effective-list.js +96 -0
  111. package/dist-server/service/recipe/recipe-effective-list.js.map +1 -0
  112. package/dist-server/service/recipe/recipe-query.js +9 -2
  113. package/dist-server/service/recipe/recipe-query.js.map +1 -1
  114. package/dist-server/service/recipe/recipe-revision-content.d.ts +32 -0
  115. package/dist-server/service/recipe/recipe-revision-content.js +65 -0
  116. package/dist-server/service/recipe/recipe-revision-content.js.map +1 -0
  117. package/dist-server/service/recipe/recipe-revision-line.d.ts +30 -0
  118. package/dist-server/service/recipe/recipe-revision-line.js +95 -0
  119. package/dist-server/service/recipe/recipe-revision-line.js.map +1 -0
  120. package/dist-server/service/recipe/recipe-revision-resolver.d.ts +59 -0
  121. package/dist-server/service/recipe/recipe-revision-resolver.js +76 -0
  122. package/dist-server/service/recipe/recipe-revision-resolver.js.map +1 -0
  123. package/dist-server/service/recipe/recipe-revision-service.d.ts +147 -0
  124. package/dist-server/service/recipe/recipe-revision-service.js +604 -0
  125. package/dist-server/service/recipe/recipe-revision-service.js.map +1 -0
  126. package/dist-server/service/recipe/recipe-revision.d.ts +100 -0
  127. package/dist-server/service/recipe/recipe-revision.js +373 -0
  128. package/dist-server/service/recipe/recipe-revision.js.map +1 -0
  129. package/dist-server/service/recipe/route-definition.js +1 -0
  130. package/dist-server/service/recipe/route-definition.js.map +1 -1
  131. package/dist-server/service/resource/class-members.js +9 -0
  132. package/dist-server/service/resource/class-members.js.map +1 -1
  133. package/dist-server/service/status/master-status.js +2 -1
  134. package/dist-server/service/status/master-status.js.map +1 -1
  135. package/dist-server/service/test-spec/test-spec-effective-list.d.ts +2 -0
  136. package/dist-server/service/test-spec/test-spec-effective-list.js +3 -1
  137. package/dist-server/service/test-spec/test-spec-effective-list.js.map +1 -1
  138. package/dist-server/service/test-spec/test-spec-revision-resolver.d.ts +1 -0
  139. package/dist-server/service/test-spec/test-spec-revision-service.d.ts +21 -0
  140. package/dist-server/service/test-spec/test-spec-revision-service.js +33 -0
  141. package/dist-server/service/test-spec/test-spec-revision-service.js.map +1 -1
  142. package/dist-server/service/test-spec/test-spec-revision.d.ts +2 -0
  143. package/dist-server/service/test-spec/test-spec-revision.js +6 -1
  144. package/dist-server/service/test-spec/test-spec-revision.js.map +1 -1
  145. package/dist-server/service/test-spec/test-specification.d.ts +2 -0
  146. package/dist-server/service/test-spec/test-specification.js +4 -0
  147. package/dist-server/service/test-spec/test-specification.js.map +1 -1
  148. package/dist-server/tsconfig.tsbuildinfo +1 -1
  149. package/package.json +4 -3
  150. package/translations/en.json +148 -0
  151. package/translations/ko.json +148 -0
@@ -0,0 +1,752 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@operato/data-grist/ox-grist.js';
3
+ import gql from 'graphql-tag';
4
+ import { html, css, nothing } from 'lit';
5
+ import { state } from 'lit/decorators.js';
6
+ import { client } from '@operato/graphql';
7
+ import { OxPrompt } from '@operato/popup';
8
+ import { IDENTITY, MasterPageBase } from './master-page-base.js';
9
+ import { requestMasterChangeApproval, masterChangeStatus } from '../../master-change-approval.js';
10
+ import { revisionText as t, revisionDate as date, revisionStyles } from './revision-comparison.js';
11
+ import { materialRevisionFields } from './material-revision-fields.js';
12
+ import { localDateTime, isoFromLocal } from './revision-view.js';
13
+ /** Shared workflow, not shared domain payload. Both consumers keep their existing page/URL identities. */
14
+ export class RevisionWorkspace extends MasterPageBase {
15
+ constructor() {
16
+ super(...arguments);
17
+ this.newName = '';
18
+ this.busy = false;
19
+ this.dirty = false;
20
+ this.failure = '';
21
+ this.message = '';
22
+ this.statuses = {};
23
+ this.serial = 0;
24
+ }
25
+ static { this.styles = [
26
+ ...MasterPageBase.styles,
27
+ revisionStyles,
28
+ css `
29
+ :host {
30
+ display: flex;
31
+ flex-direction: column;
32
+ min-height: 0;
33
+ }
34
+ ox-grist {
35
+ flex: 1;
36
+ min-height: 240px;
37
+ }
38
+ ox-grist[hidden] {
39
+ display: none;
40
+ }
41
+ .workspace {
42
+ overflow: auto;
43
+ flex: 1;
44
+ padding: 20px 24px;
45
+ }
46
+ .bar {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 12px;
50
+ flex-wrap: wrap;
51
+ }
52
+ .bar h2 {
53
+ margin: 8px 0;
54
+ font-size: var(--ui-t-title, 20px);
55
+ flex: 1;
56
+ overflow-wrap: anywhere;
57
+ }
58
+ .summary {
59
+ display: grid;
60
+ grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
61
+ gap: 16px;
62
+ margin: 20px 0;
63
+ }
64
+ .card {
65
+ padding: 16px;
66
+ border: 1px solid var(--ui-line);
67
+ border-radius: 6px;
68
+ background: var(--md-sys-color-surface);
69
+ }
70
+ .card h3 {
71
+ margin: 0 0 10px;
72
+ }
73
+ .card p {
74
+ margin: 8px 0;
75
+ }
76
+ .current {
77
+ border-inline-start: 4px solid var(--md-sys-color-primary);
78
+ }
79
+ .fields {
80
+ display: grid;
81
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
82
+ gap: 16px;
83
+ margin: 16px 0;
84
+ }
85
+ label {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 6px;
89
+ font-size: var(--ui-t-small, 13px);
90
+ }
91
+ input,
92
+ textarea {
93
+ box-sizing: border-box;
94
+ width: 100%;
95
+ padding: 8px;
96
+ border: 1px solid var(--ui-line);
97
+ border-radius: 4px;
98
+ font: inherit;
99
+ background: var(--md-sys-color-surface);
100
+ color: inherit;
101
+ }
102
+ td input {
103
+ min-width: 100px;
104
+ }
105
+ textarea {
106
+ min-height: 70px;
107
+ }
108
+ .actions {
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ gap: 12px;
112
+ margin-top: 20px;
113
+ align-items: center;
114
+ }
115
+ .notice {
116
+ padding: 12px;
117
+ background: var(--md-sys-color-surface-container);
118
+ border-radius: 4px;
119
+ }
120
+ .failure {
121
+ padding: 12px;
122
+ border-inline-start: 4px solid var(--md-sys-color-error);
123
+ color: var(--md-sys-color-error);
124
+ }
125
+ .empty {
126
+ padding: 24px;
127
+ }
128
+ .hint {
129
+ font-size: var(--ui-t-small, 13px);
130
+ }
131
+ .editor {
132
+ border-top: 1px solid var(--ui-line);
133
+ margin-top: 20px;
134
+ padding-top: 12px;
135
+ }
136
+ @media (max-width: 700px) {
137
+ .workspace {
138
+ padding: 12px;
139
+ }
140
+ .summary {
141
+ grid-template-columns: 1fr;
142
+ }
143
+ .fields {
144
+ grid-template-columns: 1fr;
145
+ }
146
+ }
147
+ `
148
+ ]; }
149
+ get recipe() {
150
+ return this.axis === 'recipeDefinition';
151
+ }
152
+ get material() { return this.axis === 'materialDefinition'; }
153
+ get names() {
154
+ if (this.material)
155
+ return { noun: 'material', list: 'materialDefinitions', overview: 'materialDefinitionRevisions', subject: 'materialId', create: 'createMaterialDefinitionDraft', edit: 'editMaterialDefinitionDraft', publish: 'publishMaterialDefinitionDraft' };
156
+ return this.recipe
157
+ ? {
158
+ noun: 'recipe',
159
+ list: 'recipeDefinitions',
160
+ overview: 'recipeRevisions',
161
+ subject: 'recipeId',
162
+ create: 'createRecipeDraft',
163
+ edit: 'editRecipeDraft',
164
+ publish: 'publishRecipeDraft'
165
+ }
166
+ : {
167
+ noun: 'spec',
168
+ list: 'testSpecifications',
169
+ overview: 'testSpecificationRevisions',
170
+ subject: 'specId',
171
+ create: 'createTestSpecificationDraft',
172
+ edit: 'editTestSpecificationDraft',
173
+ publish: 'publishTestSpecificationDraft'
174
+ };
175
+ }
176
+ get spec() {
177
+ return {
178
+ key: this.axis,
179
+ title: this.material ? 'material' : this.recipe ? 'recipe' : 'test-specification',
180
+ help: 'ops-master/' + (this.material ? 'material-definition' : this.recipe ? 'recipe-definition' : 'test-specification'),
181
+ query: this.names.list,
182
+ mutation: this.material ? 'updateMultipleMaterialDefinition' : this.recipe ? 'updateMultipleRecipeDefinition' : 'updateMultipleTestSpecification',
183
+ columns: [
184
+ ...IDENTITY,
185
+ ...(this.material ? [
186
+ { name: 'rootClasses', label: 'material-type', type: 'list', width: 120 },
187
+ { name: 'classPath', label: 'class-lineage', type: 'list', width: 180 },
188
+ { name: 'madeBy', label: 'made-by', type: 'list', width: 180 },
189
+ { name: 'usedIn', label: 'used-in', type: 'list', width: 180 }
190
+ ] : []),
191
+ { name: 'revisionNumber', label: 'revision', type: 'number' },
192
+ { name: 'effectiveStart', label: 'effective-start', type: 'date' }
193
+ ].map(column => ({ ...column, readonly: true }))
194
+ };
195
+ }
196
+ get pathSegmentNarrowsList() {
197
+ return false;
198
+ }
199
+ get context() {
200
+ return {
201
+ ...super.context,
202
+ importable: undefined,
203
+ exportable: this.overview || this.draft ? undefined : super.context.exportable,
204
+ search: this.overview || this.draft ? undefined : super.context.search,
205
+ description: t('purpose'),
206
+ actions: this.draft && !this.draft.frozenAt
207
+ ? [
208
+ {
209
+ title: t(this.approvalRequired ? 'request-approval' : 'publish'),
210
+ icon: 'done',
211
+ emphasis: { raised: true },
212
+ disabled: this.busy || this.approvalRequired === undefined,
213
+ action: () => this.submit()
214
+ }
215
+ ]
216
+ : this.overview || this.draft
217
+ ? []
218
+ : [
219
+ {
220
+ title: t(this.material ? 'new-material' : this.recipe ? 'new-recipe' : 'new-spec'),
221
+ icon: 'add',
222
+ emphasis: { raised: true },
223
+ action: () => this.startNew()
224
+ }
225
+ ]
226
+ };
227
+ }
228
+ updated(changes) {
229
+ super.updated(changes);
230
+ if (this.active && ['overview', 'draft', 'busy', 'approvalRequired'].some(key => changes.has(key)))
231
+ this.updateContext();
232
+ }
233
+ configOf() {
234
+ const config = super.configOf();
235
+ return { ...config, rows: { ...config.rows, appendable: false, selectable: { multiple: false } } };
236
+ }
237
+ async pageInitialized() {
238
+ await super.pageInitialized();
239
+ await this.openResource(this.lifecycle?.resourceId);
240
+ }
241
+ async pageUpdated(changes, after, before) {
242
+ await super.pageUpdated();
243
+ if (after?.resourceId !== before?.resourceId)
244
+ await this.openResource(after?.resourceId);
245
+ }
246
+ async openResource(key) {
247
+ if (!key)
248
+ return;
249
+ await this.run(async () => {
250
+ const response = await client.query({
251
+ query: gql(`query($filters:[Filter!]){${this.names.list}(filters:$filters){items{id}}}`),
252
+ variables: { filters: [{ name: 'name', operator: 'eq', value: key }] },
253
+ fetchPolicy: 'no-cache'
254
+ });
255
+ this.assert(response);
256
+ const row = response.data[this.names.list].items[0];
257
+ if (row)
258
+ await this.load(row.id);
259
+ });
260
+ }
261
+ assert(response) {
262
+ if (response.errors?.length)
263
+ throw new Error(response.errors.map((e) => e.message).join('\n'));
264
+ if (!response.data)
265
+ throw new Error(t('no-response'));
266
+ }
267
+ async run(work) {
268
+ if (this.busy)
269
+ return;
270
+ this.busy = true;
271
+ this.failure = '';
272
+ try {
273
+ await work();
274
+ }
275
+ catch (error) {
276
+ this.failure = error.message;
277
+ }
278
+ finally {
279
+ this.busy = false;
280
+ }
281
+ }
282
+ async canLeave() {
283
+ return (!this.dirty ||
284
+ !!(await OxPrompt.open({
285
+ title: t('unsaved'),
286
+ text: t('discard-question'),
287
+ confirmButton: { text: t('discard') },
288
+ cancelButton: { text: t('keep-editing') }
289
+ })));
290
+ }
291
+ blank() {
292
+ return {
293
+ version: '1',
294
+ description: '',
295
+ materialDefinitionName: '',
296
+ effectiveFrom: new Date().toISOString(),
297
+ reason: '',
298
+ materialClassIds: [], properties: [], testSpecificationIds: [],
299
+ criteria: [],
300
+ inputs: [],
301
+ outputs: []
302
+ };
303
+ }
304
+ async policy() {
305
+ const response = await client.query({
306
+ query: gql `
307
+ query ($axis: String!) {
308
+ masterChangePolicy(axis: $axis)
309
+ }
310
+ `,
311
+ variables: { axis: this.axis },
312
+ fetchPolicy: 'no-cache'
313
+ });
314
+ this.assert(response);
315
+ this.approvalRequired = response.data.masterChangePolicy.approvalRequired;
316
+ }
317
+ async startNew() {
318
+ if (!(await this.canLeave()))
319
+ return;
320
+ await this.run(async () => {
321
+ ++this.serial;
322
+ this.overview = undefined;
323
+ this.draft = this.blank();
324
+ this.newName = '';
325
+ this.dirty = false;
326
+ this.message = '';
327
+ await this.policy();
328
+ });
329
+ }
330
+ async pick(event) {
331
+ const row = event.detail?.records?.[0] ?? event.detail?.added?.[0];
332
+ if (!row?.id || !(await this.canLeave()))
333
+ return;
334
+ await this.run(async () => {
335
+ this.draft = undefined;
336
+ this.dirty = false;
337
+ this.message = '';
338
+ await this.load(row.id);
339
+ });
340
+ }
341
+ async load(id) {
342
+ const serial = ++this.serial;
343
+ const response = await client.query({
344
+ query: gql(`query($id:String!){${this.names.overview}(${this.names.subject}:$id)}`),
345
+ variables: { id },
346
+ fetchPolicy: 'no-cache'
347
+ });
348
+ this.assert(response);
349
+ if (serial !== this.serial)
350
+ return;
351
+ this.overview = response.data[this.names.overview];
352
+ await this.policy();
353
+ const statuses = {};
354
+ await Promise.all(this.overview.revisions
355
+ .filter((row) => row.activityInstanceId)
356
+ .map(async (row) => {
357
+ try {
358
+ statuses[row.id] = await masterChangeStatus(this, row.activityInstanceId);
359
+ }
360
+ catch (error) {
361
+ statuses[row.id] = { state: 'unknown', comment: error.message };
362
+ }
363
+ }));
364
+ if (serial === this.serial)
365
+ this.statuses = statuses;
366
+ }
367
+ get currentSegment() {
368
+ return this.overview?.schedule.find((s) => s.state === 'current');
369
+ }
370
+ get current() {
371
+ return this.overview?.revisions.find((r) => r.id === this.currentSegment?.revisionId);
372
+ }
373
+ get comparisonBase() {
374
+ if (this.draft?.id && (!this.dirty || this.draft.frozenAt))
375
+ return this.overview?.revisions.find((r) => r.id === this.draft.baseRevisionId);
376
+ const at = this.draft?.effectiveFrom;
377
+ const segment = this.overview?.schedule.find((s) => s.validFrom <= at && (!s.validTo || at < s.validTo));
378
+ return this.overview?.revisions.find((r) => r.id === segment?.revisionId);
379
+ }
380
+ stateOf(row) {
381
+ const segment = this.overview?.schedule.find((s) => s.revisionId === row.id);
382
+ return (segment?.state ??
383
+ (row.activityInstanceId ? (this.statuses[row.id]?.state ?? 'unknown') : row.frozenAt ? 'approved' : 'draft'));
384
+ }
385
+ async openRevision(row, fork = false) {
386
+ if (!(await this.canLeave()))
387
+ return;
388
+ this.draft = structuredClone(row ?? this.blank());
389
+ if (fork)
390
+ this.draft = {
391
+ ...this.draft,
392
+ id: undefined,
393
+ revisionNumber: undefined,
394
+ frozenAt: undefined,
395
+ activityInstanceId: undefined,
396
+ editGeneration: undefined,
397
+ baseRevisionId: undefined,
398
+ effectiveFrom: new Date().toISOString(),
399
+ reason: ''
400
+ };
401
+ this.dirty = false;
402
+ this.message = '';
403
+ await this.updateComplete;
404
+ this.renderRoot.querySelector('.editor')?.scrollIntoView({ block: 'start', behavior: 'smooth' });
405
+ }
406
+ change(field, value) {
407
+ this.draft = { ...this.draft, [field]: value };
408
+ this.dirty = true;
409
+ this.message = '';
410
+ }
411
+ line(section, index, field, value) {
412
+ const rows = this.draft[section].map((row) => ({ ...row, limit: { ...row.limit } }));
413
+ const numeric = ['qty', 'minimum', 'maximum'].includes(field);
414
+ const next = numeric ? (value === '' ? undefined : Number(value)) : value;
415
+ if (section === 'criteria' && ['minimum', 'maximum', 'uom'].includes(field))
416
+ rows[index].limit[field] = next;
417
+ else
418
+ rows[index][field] = next;
419
+ this.change(section, rows);
420
+ }
421
+ async save() {
422
+ await this.run(async () => {
423
+ await this.persist();
424
+ this.message = t('saved');
425
+ });
426
+ }
427
+ async persist() {
428
+ if (!this.draft || this.draft.frozenAt)
429
+ return;
430
+ const invalid = this.renderRoot.querySelector('input:invalid,textarea:invalid');
431
+ if (invalid) {
432
+ invalid.reportValidity();
433
+ throw new Error(t('required-fields'));
434
+ }
435
+ const { version, description, criteria, inputs, outputs, materialDefinitionName, route, effectiveFrom, reason } = this.draft;
436
+ const content = this.material
437
+ ? { description, materialClassIds: this.draft.materialClassIds, properties: this.draft.properties, testSpecificationIds: this.draft.testSpecificationIds, effectiveFrom, reason }
438
+ : this.recipe
439
+ ? { description, inputs, outputs, materialDefinitionName, route: route || null, effectiveFrom, reason }
440
+ : { version, description, criteria, effectiveFrom, reason };
441
+ const n = this.names;
442
+ const response = this.draft.id
443
+ ? await client.mutate({
444
+ mutation: gql(`mutation($id:String!,$generation:Int!,$content:Object!){${n.edit}(revisionId:$id,editGeneration:$generation,content:$content)}`),
445
+ variables: { id: this.draft.id, generation: this.draft.editGeneration, content }
446
+ })
447
+ : await client.mutate({
448
+ mutation: gql(`mutation($subject:String,$name:String,$content:Object!){${n.create}(${n.subject}:$subject,name:$name,content:$content)}`),
449
+ variables: { subject: this.overview?.[n.subject], name: this.overview ? undefined : this.newName, content }
450
+ });
451
+ this.assert(response);
452
+ this.draft = response.data[n.edit] ?? response.data[n.create];
453
+ this.dirty = false;
454
+ await this.load(this.draft[n.subject]);
455
+ await this.grist?.fetch();
456
+ }
457
+ async submit() {
458
+ await this.run(async () => {
459
+ await this.persist();
460
+ if (!this.draft?.id || this.draft.frozenAt)
461
+ return;
462
+ await this.policy();
463
+ if (this.approvalRequired)
464
+ await requestMasterChangeApproval(this, {
465
+ axis: this.axis,
466
+ revisionId: this.draft.id,
467
+ editGeneration: this.draft.editGeneration
468
+ });
469
+ else {
470
+ const response = await client.mutate({
471
+ mutation: gql(`mutation($id:String!,$generation:Int!){${this.names.publish}(revisionId:$id,editGeneration:$generation)}`),
472
+ variables: { id: this.draft.id, generation: this.draft.editGeneration }
473
+ });
474
+ this.assert(response);
475
+ }
476
+ const id = this.draft.id;
477
+ await this.load(this.draft[this.names.subject]);
478
+ this.draft = this.overview.revisions.find((r) => r.id === id);
479
+ await this.grist?.fetch();
480
+ this.message = t(this.approvalRequired ? 'submitted' : 'published');
481
+ });
482
+ }
483
+ content(row, editable = false) {
484
+ if (this.material)
485
+ return materialRevisionFields(row, editable ? (field, value) => this.change(field, value) : undefined);
486
+ return (this.recipe ? ['inputs', 'outputs'] : ['criteria']).map(section => {
487
+ const fields = section === 'criteria'
488
+ ? ['id', 'description', 'evaluatedPropertyId', 'minimum', 'maximum', 'uom', 'expression']
489
+ : section === 'inputs'
490
+ ? ['material', 'qty', 'operation']
491
+ : ['material', 'qty'];
492
+ return html `<h3>${t(section)}</h3>
493
+ <div class="scroll">
494
+ <table>
495
+ <thead>
496
+ <tr>
497
+ ${fields.map(field => html `<th>${t(field)}</th>`)}${editable ? html `<th>${t('actions')}</th>` : nothing}
498
+ </tr>
499
+ </thead>
500
+ <tbody>
501
+ ${(row[section] ?? []).map((line, index) => html `<tr>
502
+ ${fields.map(field => {
503
+ const value = section === 'criteria' && ['minimum', 'maximum', 'uom'].includes(field)
504
+ ? line.limit?.[field]
505
+ : line[field];
506
+ return html `<td>
507
+ ${editable ? html `<input aria-label=${t(section) + ' ' + (index + 1) + ' ' + t(field)} .value=${String(value ?? '')} type=${['qty', 'minimum', 'maximum'].includes(field) ? 'number' : 'text'} step="any" ?required=${['id', 'material', 'qty', 'evaluatedPropertyId'].includes(field)} @input=${(e) => this.line(section, index, field, e.target.value)} />` : (value ?? '—')}
508
+ </td>`;
509
+ })}${editable
510
+ ? html `<td>
511
+ <button
512
+ aria-label=${t('remove-row', { n: index + 1 })}
513
+ @click=${() => this.change(section, row[section].filter((_, i) => i !== index))}
514
+ >
515
+ ${t('remove')}
516
+ </button>
517
+ </td>`
518
+ : nothing}
519
+ </tr>`)}
520
+ </tbody>
521
+ </table>
522
+ </div>
523
+ ${editable ? html `<button @click=${() => this.change(section, [...row[section], section === 'criteria' ? { id: '', description: '', limit: {} } : { lineKey: crypto.randomUUID(), material: '', qty: 1 }])}>${t('add-' + section)}</button>` : nothing}
524
+ ${section === 'criteria' && row.criteria?.some((c) => c.expression) ? html `<p class="notice">${t('expression-preserved')}</p>` : nothing}`;
525
+ });
526
+ }
527
+ status(row) {
528
+ const state = this.stateOf(row), decision = this.statuses[row.id];
529
+ return html `<span class="pill">${t(state)}</span
530
+ >${decision?.who ? html `<span> · ${decision.who}</span>` : ''}${decision?.comment ? html `<p>${decision.comment}</p>` : ''}${decision?.open ? html `<button @click=${() => decision.open()}>${t('approval-detail')}</button>` : ''}`;
531
+ }
532
+ render() {
533
+ const current = this.current;
534
+ const next = this.overview?.schedule.filter((s) => s.state === 'scheduled') ?? [];
535
+ const changes = this.overview?.revisions.filter((r) => !this.overview.schedule.some((s) => s.revisionId === r.id)) ?? [];
536
+ const readonly = !!this.draft?.frozenAt || this.busy;
537
+ return html ` <ox-grist
538
+ ?hidden=${!!this.overview || !!this.draft}
539
+ .mode=${this.mode}
540
+ .config=${this.gristConfig}
541
+ .fetchHandler=${this.fetchHandler.bind(this)}
542
+ @select-record-change=${this.pick.bind(this)}
543
+ ></ox-grist>
544
+ ${this.overview || this.draft
545
+ ? html `<main class="workspace" aria-busy=${this.busy}>
546
+ <div class="bar">
547
+ <button
548
+ ?disabled=${this.busy}
549
+ @click=${async () => {
550
+ if (await this.canLeave()) {
551
+ this.overview = undefined;
552
+ this.draft = undefined;
553
+ this.dirty = false;
554
+ await this.grist?.fetch();
555
+ }
556
+ }}
557
+ >
558
+ ${t('back-list')}
559
+ </button>
560
+ <h2>${this.overview?.name ?? t(this.material ? 'new-material' : this.recipe ? 'new-recipe' : 'new-spec')}</h2>
561
+ ${this.overview
562
+ ? html `<button
563
+ ?disabled=${this.busy}
564
+ @click=${() => this.run(async () => {
565
+ this.message = '';
566
+ await this.load(this.overview[this.names.subject]);
567
+ await this.grist?.fetch();
568
+ })}
569
+ >
570
+ ${t('refresh')}</button
571
+ ><button
572
+ class="primary"
573
+ ?disabled=${this.busy}
574
+ @click=${() => this.openRevision(current, true)}
575
+ >
576
+ ${t('create-change')}
577
+ </button>`
578
+ : ''}
579
+ </div>
580
+ ${this.overview
581
+ ? html `<div class="summary">
582
+ <section class="card current">
583
+ <h3>${t('current')}</h3>
584
+ ${current
585
+ ? html `<strong>${t('revision-value', { n: current.revisionNumber })}</strong>
586
+ <p>
587
+ ${date(this.currentSegment.validFrom)} →
588
+ ${this.currentSegment.validTo ? date(this.currentSegment.validTo) : t('until-change')}
589
+ </p>
590
+ <button @click=${() => this.openRevision(current)}>${t('view-current')}</button>`
591
+ : html `<p>${t('no-current')}</p>`}
592
+ </section>
593
+ <section class="card">
594
+ <h3>${t('changes-in-progress')} · ${changes.length}</h3>
595
+ ${changes.length ? changes.map((row) => html `<p><button @click=${() => this.openRevision(row)}>${t('revision-value', { n: row.revisionNumber })}</button> ${this.status(row)}</p>`) : html `<p class="muted">${t('no-drafts')}</p>`}
596
+ </section>
597
+ <section class="card">
598
+ <h3>${t('scheduled')}</h3>
599
+ ${next.length
600
+ ? next.map((segment) => {
601
+ const row = this.overview.revisions.find((r) => r.id === segment.revisionId);
602
+ return html `<p>
603
+ <button @click=${() => this.openRevision(row)}>
604
+ ${t('revision-value', { n: row.revisionNumber })}
605
+ </button>
606
+ · ${date(segment.validFrom)}
607
+ </p>`;
608
+ })
609
+ : html `<p class="muted">${t('no-scheduled')}</p>`}
610
+ </section>
611
+ </div>
612
+ ${!this.draft && current
613
+ ? html `<h3>${t('current-content')}</h3>
614
+ <p>${current.description}</p>
615
+ ${this.content(current)}`
616
+ : ''}
617
+ <details>
618
+ <summary>${t('history')}</summary>
619
+ <table>
620
+ <thead>
621
+ <tr>
622
+ <th>${t('revision')}</th>
623
+ <th>${t('state')}</th>
624
+ <th>${t('effectiveFrom')}</th>
625
+ <th>${t('reason')}</th>
626
+ </tr>
627
+ </thead>
628
+ <tbody>
629
+ ${[...this.overview.revisions].reverse().map((row) => html `<tr>
630
+ <td><button @click=${() => this.openRevision(row)}>${row.revisionNumber}</button></td>
631
+ <td>${this.status(row)}</td>
632
+ <td>${date(row.effectiveFrom)}</td>
633
+ <td>${row.reason}</td>
634
+ </tr>`)}
635
+ </tbody>
636
+ </table>
637
+ </details>`
638
+ : ''}
639
+ ${this.draft
640
+ ? html `<section class="editor">
641
+ <div class="bar">
642
+ <h3>
643
+ ${this.draft.id ? t('revision-value', { n: this.draft.revisionNumber }) : t('new-change')}
644
+ </h3>
645
+ ${this.draft.id ? this.status(this.draft) : ''}<button
646
+ ?disabled=${this.busy}
647
+ @click=${async () => {
648
+ if (await this.canLeave()) {
649
+ this.draft = undefined;
650
+ this.dirty = false;
651
+ }
652
+ }}
653
+ >
654
+ ${t('close-editor')}
655
+ </button>
656
+ </div>
657
+ <p class="notice">${t(this.material ? 'material-impact' : this.recipe ? 'recipe-impact' : 'spec-impact')}</p>
658
+ <div class="fields">
659
+ ${!this.overview
660
+ ? html `<label
661
+ >${t('name')}<input
662
+ required
663
+ .value=${this.newName}
664
+ @input=${(e) => {
665
+ this.newName = e.target.value;
666
+ this.dirty = true;
667
+ }}
668
+ /></label>`
669
+ : ''}
670
+ ${['description', ...(this.material ? [] : this.recipe ? ['materialDefinitionName', 'route'] : ['version'])].map(field => html `<label>${t(field)}<input ?disabled=${readonly} ?required=${field === 'materialDefinitionName' || field === 'version'} .value=${this.draft[field] ?? ''} @input=${(e) => this.change(field, e.target.value)} /></label>`)}
671
+ <label
672
+ >${t('effectiveFrom')}<input
673
+ type="datetime-local"
674
+ required
675
+ ?disabled=${readonly}
676
+ .value=${localDateTime(this.draft.effectiveFrom)}
677
+ @change=${(e) => {
678
+ try {
679
+ this.change('effectiveFrom', isoFromLocal(e.target.value));
680
+ }
681
+ catch {
682
+ this.failure = t('invalid-date');
683
+ }
684
+ }}
685
+ /><span class="muted hint"
686
+ >${t('time-zone')} · ${Intl.DateTimeFormat().resolvedOptions().timeZone}</span
687
+ ></label
688
+ >
689
+ <label
690
+ >${t('reason')}<textarea
691
+ required
692
+ ?disabled=${readonly}
693
+ .value=${this.draft.reason ?? ''}
694
+ @input=${(e) => this.change('reason', e.target.value)}
695
+ ></textarea>
696
+ </label>
697
+ </div>
698
+ ${this.content(this.draft, !readonly)}
699
+ <ops-master-revision-comparison
700
+ .axis=${this.axis}
701
+ .before=${this.comparisonBase}
702
+ .after=${this.draft}
703
+ ></ops-master-revision-comparison>
704
+ <div class="actions">
705
+ ${this.draft.frozenAt ? html `<button ?disabled=${this.busy} @click=${() => this.openRevision(this.draft, true)}>${t('create-change')}</button>` : html `<button ?disabled=${this.busy} @click=${() => this.save()}>${t('save-draft')}</button>`}${this.dirty ? html `<span>${t('unsaved')}</span>` : ''}
706
+ </div>
707
+ </section>`
708
+ : ''}
709
+ ${this.message ? html `<p role="status" class="notice">${this.message}</p>` : ''}${this.failure ? html `<p role="alert" class="failure">${this.failure}</p>` : ''}
710
+ </main>`
711
+ : this.failure
712
+ ? html `<p role="alert" class="failure">${this.failure}</p>`
713
+ : nothing}`;
714
+ }
715
+ }
716
+ __decorate([
717
+ state(),
718
+ __metadata("design:type", Object)
719
+ ], RevisionWorkspace.prototype, "overview", void 0);
720
+ __decorate([
721
+ state(),
722
+ __metadata("design:type", Object)
723
+ ], RevisionWorkspace.prototype, "draft", void 0);
724
+ __decorate([
725
+ state(),
726
+ __metadata("design:type", Object)
727
+ ], RevisionWorkspace.prototype, "newName", void 0);
728
+ __decorate([
729
+ state(),
730
+ __metadata("design:type", Object)
731
+ ], RevisionWorkspace.prototype, "busy", void 0);
732
+ __decorate([
733
+ state(),
734
+ __metadata("design:type", Object)
735
+ ], RevisionWorkspace.prototype, "dirty", void 0);
736
+ __decorate([
737
+ state(),
738
+ __metadata("design:type", Object)
739
+ ], RevisionWorkspace.prototype, "failure", void 0);
740
+ __decorate([
741
+ state(),
742
+ __metadata("design:type", Object)
743
+ ], RevisionWorkspace.prototype, "message", void 0);
744
+ __decorate([
745
+ state(),
746
+ __metadata("design:type", Object)
747
+ ], RevisionWorkspace.prototype, "approvalRequired", void 0);
748
+ __decorate([
749
+ state(),
750
+ __metadata("design:type", Object)
751
+ ], RevisionWorkspace.prototype, "statuses", void 0);
752
+ //# sourceMappingURL=revision-workspace.js.map