@beyondwork/docx-react-component 1.0.31 → 1.0.32
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.
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/api/public-types.ts +16 -1
- package/src/api/session-state.ts +2 -0
- package/src/io/docx-session.ts +16 -3
- package/src/io/ooxml/parse-footnotes.ts +23 -33
- package/src/io/ooxml/parse-headers-footers.ts +20 -21
- package/src/io/ooxml/workflow-payload.ts +311 -8
- package/src/model/snapshot.ts +113 -1
- package/src/runtime/document-runtime.ts +207 -33
- package/src/runtime/surface-projection.ts +156 -7
- package/src/ui/WordReviewEditor.tsx +13 -5
- package/src/ui/editor-surface-controller.tsx +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +4 -1
- package/src/ui/headless/selection-tool-types.ts +1 -2
- package/src/ui/workflow-surface-blocked-rails.ts +19 -1
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +1 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +1 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +88 -14
- package/src/ui-tailwind/editor-surface/pm-schema.ts +29 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +12 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +20 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
- package/src/ui-tailwind/theme/editor-theme.css +8 -0
|
@@ -4,9 +4,13 @@ import type {
|
|
|
4
4
|
OpcRelationship,
|
|
5
5
|
} from "./part-manifest.ts";
|
|
6
6
|
import type {
|
|
7
|
+
EditorAnchorProjection,
|
|
8
|
+
WorkflowOverlay,
|
|
7
9
|
WorkflowMetadataDefinition,
|
|
8
10
|
WorkflowMetadataEntry,
|
|
9
11
|
WorkflowMetadataSnapshot,
|
|
12
|
+
WorkflowScope,
|
|
13
|
+
WorkflowWorkItem,
|
|
10
14
|
} from "../../api/public-types.ts";
|
|
11
15
|
|
|
12
16
|
export const WORKFLOW_PAYLOAD_PART_PATH = "/customXml/item1.xml";
|
|
@@ -17,6 +21,8 @@ export const WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE =
|
|
|
17
21
|
"application/vnd.openxmlformats-officedocument.customXmlProperties+xml";
|
|
18
22
|
export const WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE =
|
|
19
23
|
"application/vnd.openxmlformats-officedocument.custom-properties+xml";
|
|
24
|
+
export const WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE =
|
|
25
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml";
|
|
20
26
|
export const WORKFLOW_PAYLOAD_ITEM_PROPS_RELATIONSHIP_TYPE =
|
|
21
27
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps";
|
|
22
28
|
export const WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE =
|
|
@@ -30,6 +36,11 @@ type EmbeddedWorkflowPayloadDescriptor = {
|
|
|
30
36
|
itemPropsPartPath: string;
|
|
31
37
|
};
|
|
32
38
|
|
|
39
|
+
export interface WorkflowPayloadEnvelope {
|
|
40
|
+
workflowMetadata: WorkflowMetadataSnapshot;
|
|
41
|
+
workflowOverlay?: WorkflowOverlay;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
export function getDocumentBackedWorkflowMetadata(
|
|
34
45
|
snapshot: WorkflowMetadataSnapshot | undefined,
|
|
35
46
|
): WorkflowMetadataSnapshot {
|
|
@@ -51,6 +62,7 @@ export function getDocumentBackedWorkflowMetadata(
|
|
|
51
62
|
export function buildWorkflowPayloadParts(input: {
|
|
52
63
|
sourcePackage: OpcPackage;
|
|
53
64
|
workflowMetadata: WorkflowMetadataSnapshot | undefined;
|
|
65
|
+
workflowOverlay?: WorkflowOverlay;
|
|
54
66
|
documentId: string;
|
|
55
67
|
createdAt: string;
|
|
56
68
|
updatedAt: string;
|
|
@@ -65,7 +77,20 @@ export function buildWorkflowPayloadParts(input: {
|
|
|
65
77
|
descriptor: EmbeddedWorkflowPayloadDescriptor;
|
|
66
78
|
} | null {
|
|
67
79
|
const workflowMetadata = getDocumentBackedWorkflowMetadata(input.workflowMetadata);
|
|
68
|
-
|
|
80
|
+
const existingPayloadPartPath = resolvePayloadPartPath(input.sourcePackage);
|
|
81
|
+
const existingPayloadEnvelope = existingPayloadPartPath
|
|
82
|
+
? parseWorkflowPayloadEnvelopeFromPackage(input.sourcePackage)
|
|
83
|
+
: undefined;
|
|
84
|
+
const sourceHasRuntimeOwnedPayloadContent = Boolean(
|
|
85
|
+
existingPayloadEnvelope?.workflowOverlay ||
|
|
86
|
+
(existingPayloadEnvelope?.workflowMetadata.definitions.length ?? 0) > 0,
|
|
87
|
+
);
|
|
88
|
+
if (
|
|
89
|
+
workflowMetadata.definitions.length === 0 &&
|
|
90
|
+
workflowMetadata.entries.length === 0 &&
|
|
91
|
+
!input.workflowOverlay &&
|
|
92
|
+
!sourceHasRuntimeOwnedPayloadContent
|
|
93
|
+
) {
|
|
69
94
|
return null;
|
|
70
95
|
}
|
|
71
96
|
|
|
@@ -76,6 +101,8 @@ export function buildWorkflowPayloadParts(input: {
|
|
|
76
101
|
updatedAt: input.updatedAt,
|
|
77
102
|
producerVersion: input.producerVersion,
|
|
78
103
|
workflowMetadata,
|
|
104
|
+
workflowOverlay: input.workflowOverlay,
|
|
105
|
+
preservedExtensionsXml: getPreservedExtensionsXml(input.sourcePackage, descriptor.payloadPartPath),
|
|
79
106
|
});
|
|
80
107
|
const itemPropsXml = buildItemPropsXml(descriptor.itemId);
|
|
81
108
|
const customPropertiesXml = buildCustomPropertiesXml(descriptor, workflowMetadata, input.sourcePackage);
|
|
@@ -101,6 +128,12 @@ export function buildWorkflowPayloadParts(input: {
|
|
|
101
128
|
export function parseWorkflowPayloadFromPackage(
|
|
102
129
|
sourcePackage: OpcPackage,
|
|
103
130
|
): WorkflowMetadataSnapshot | undefined {
|
|
131
|
+
return parseWorkflowPayloadEnvelopeFromPackage(sourcePackage)?.workflowMetadata;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parseWorkflowPayloadEnvelopeFromPackage(
|
|
135
|
+
sourcePackage: OpcPackage,
|
|
136
|
+
): WorkflowPayloadEnvelope | undefined {
|
|
104
137
|
const payloadPartPath = resolvePayloadPartPath(sourcePackage);
|
|
105
138
|
if (!payloadPartPath) {
|
|
106
139
|
return undefined;
|
|
@@ -120,6 +153,7 @@ export function parseWorkflowPayloadFromPackage(
|
|
|
120
153
|
if (!attributes.key) {
|
|
121
154
|
continue;
|
|
122
155
|
}
|
|
156
|
+
|
|
123
157
|
if (attributes.key.startsWith("definition.")) {
|
|
124
158
|
const metadataId = attributes.key.replace(/^definition\./, "");
|
|
125
159
|
const kind = attributes.ns ?? "tag-category";
|
|
@@ -133,7 +167,7 @@ export function parseWorkflowPayloadFromPackage(
|
|
|
133
167
|
label,
|
|
134
168
|
...(attributes.source ? { color: attributes.source } : {}),
|
|
135
169
|
...(attributes.status ? { icon: attributes.status } : {}),
|
|
136
|
-
persistence: "document-metadata",
|
|
170
|
+
persistence: "document-metadata" as const,
|
|
137
171
|
});
|
|
138
172
|
continue;
|
|
139
173
|
}
|
|
@@ -153,10 +187,10 @@ export function parseWorkflowPayloadFromPackage(
|
|
|
153
187
|
entryId,
|
|
154
188
|
metadataId,
|
|
155
189
|
anchor: {
|
|
156
|
-
kind: "range",
|
|
190
|
+
kind: "range" as const,
|
|
157
191
|
from: 0,
|
|
158
192
|
to: 0,
|
|
159
|
-
assoc: { start: -1, end: 1 },
|
|
193
|
+
assoc: { start: -1 as const, end: 1 as const },
|
|
160
194
|
},
|
|
161
195
|
storyTarget: parseWorkflowStoryTarget(attributes),
|
|
162
196
|
value: parseWorkflowMetadataValue(payloadText, attributes.valueType),
|
|
@@ -168,8 +202,11 @@ export function parseWorkflowPayloadFromPackage(
|
|
|
168
202
|
}
|
|
169
203
|
|
|
170
204
|
return {
|
|
171
|
-
|
|
172
|
-
|
|
205
|
+
workflowMetadata: {
|
|
206
|
+
definitions,
|
|
207
|
+
entries,
|
|
208
|
+
},
|
|
209
|
+
workflowOverlay: parseWorkflowOverlay(xml),
|
|
173
210
|
};
|
|
174
211
|
}
|
|
175
212
|
|
|
@@ -179,6 +216,8 @@ function buildPayloadXml(input: {
|
|
|
179
216
|
updatedAt: string;
|
|
180
217
|
producerVersion: string;
|
|
181
218
|
workflowMetadata: WorkflowMetadataSnapshot;
|
|
219
|
+
workflowOverlay?: WorkflowOverlay;
|
|
220
|
+
preservedExtensionsXml: string;
|
|
182
221
|
}): string {
|
|
183
222
|
const definitionEntriesXml = input.workflowMetadata.definitions
|
|
184
223
|
.map((definition) => [
|
|
@@ -212,6 +251,11 @@ function buildPayloadXml(input: {
|
|
|
212
251
|
.filter((value) => value.length > 0)
|
|
213
252
|
.join("\n");
|
|
214
253
|
|
|
254
|
+
const workblockXml = buildWorkblockExtensionXml(input.workflowOverlay);
|
|
255
|
+
const extensionsXml = [workblockXml, input.preservedExtensionsXml]
|
|
256
|
+
.filter((value) => value.trim().length > 0)
|
|
257
|
+
.join("\n");
|
|
258
|
+
|
|
215
259
|
return [
|
|
216
260
|
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
217
261
|
`<bw:workflowPayload xmlns:bw="urn:beyondwork:workflow-payload:1" version="1.0" payloadId="${escapeXml(input.descriptor.payloadId)}" itemId="${escapeXml(input.descriptor.itemId)}" documentId="${escapeXml(input.descriptor.documentId)}" createdAt="${escapeXml(input.createdAt)}" updatedAt="${escapeXml(input.updatedAt)}">`,
|
|
@@ -223,7 +267,9 @@ function buildPayloadXml(input: {
|
|
|
223
267
|
definitionEntriesXml ? indentLines(definitionEntriesXml, 4) : "",
|
|
224
268
|
metadataEntriesXml ? indentLines(metadataEntriesXml, 4) : "",
|
|
225
269
|
` </bw:metadata>`,
|
|
226
|
-
|
|
270
|
+
extensionsXml
|
|
271
|
+
? ` <bw:extensions>\n${indentLines(extensionsXml, 4)}\n </bw:extensions>`
|
|
272
|
+
: ` <bw:extensions />`,
|
|
227
273
|
`</bw:workflowPayload>`,
|
|
228
274
|
].filter((line) => line.length > 0).join("\n");
|
|
229
275
|
}
|
|
@@ -283,6 +329,129 @@ function buildCustomProperty(pid: number, name: string, value: string): string {
|
|
|
283
329
|
return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${escapeXml(name)}"><vt:lpwstr>${escapeXml(value)}</vt:lpwstr></property>`;
|
|
284
330
|
}
|
|
285
331
|
|
|
332
|
+
function buildWorkblockExtensionXml(workflowOverlay: WorkflowOverlay | undefined): string {
|
|
333
|
+
if (!workflowOverlay) {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
const workItemsXml = (workflowOverlay.workItems ?? [])
|
|
337
|
+
.map((workItem) => [
|
|
338
|
+
`<bw:workItem`,
|
|
339
|
+
` id="${escapeXml(workItem.workItemId)}"`,
|
|
340
|
+
` title="${escapeXml(workItem.title)}"`,
|
|
341
|
+
workItem.status ? ` status="${escapeXml(workItem.status)}"` : "",
|
|
342
|
+
workItem.domain ? ` domain="${escapeXml(workItem.domain)}"` : "",
|
|
343
|
+
`>`,
|
|
344
|
+
workItem.scopeIds.map((scopeId) => ` <bw:scopeRef ref="${escapeXml(scopeId)}" />`).join("\n"),
|
|
345
|
+
`</bw:workItem>`,
|
|
346
|
+
].join(""))
|
|
347
|
+
.join("\n");
|
|
348
|
+
const scopesXml = workflowOverlay.scopes
|
|
349
|
+
.map((scope) => buildWorkflowScopeXml(scope))
|
|
350
|
+
.join("\n");
|
|
351
|
+
|
|
352
|
+
return [
|
|
353
|
+
`<bw:workblock>`,
|
|
354
|
+
[
|
|
355
|
+
`<bw:workflowOverlay`,
|
|
356
|
+
` overlayVersion="${escapeXml(workflowOverlay.overlayVersion)}"`,
|
|
357
|
+
workflowOverlay.activeWorkItemId
|
|
358
|
+
? ` activeWorkItemId="${escapeXml(workflowOverlay.activeWorkItemId)}"`
|
|
359
|
+
: "",
|
|
360
|
+
`>`,
|
|
361
|
+
].join(""),
|
|
362
|
+
` <bw:workItems>`,
|
|
363
|
+
workItemsXml ? indentLines(workItemsXml, 4) : "",
|
|
364
|
+
` </bw:workItems>`,
|
|
365
|
+
` <bw:scopes>`,
|
|
366
|
+
scopesXml ? indentLines(scopesXml, 4) : "",
|
|
367
|
+
` </bw:scopes>`,
|
|
368
|
+
`</bw:workflowOverlay>`,
|
|
369
|
+
`</bw:workblock>`,
|
|
370
|
+
].filter((line) => line.length > 0).join("\n");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildWorkflowScopeXml(scope: WorkflowScope): string {
|
|
374
|
+
return [
|
|
375
|
+
`<bw:scope`,
|
|
376
|
+
` id="${escapeXml(scope.scopeId)}"`,
|
|
377
|
+
` mode="${escapeXml(scope.mode)}"`,
|
|
378
|
+
scope.workItemId ? ` workItemRef="${escapeXml(scope.workItemId)}"` : "",
|
|
379
|
+
scope.label ? ` label="${escapeXml(scope.label)}"` : "",
|
|
380
|
+
scope.domain ? ` domain="${escapeXml(scope.domain)}"` : "",
|
|
381
|
+
`>`,
|
|
382
|
+
indentLines(buildWorkflowAnchorXml(scope.anchor, scope.storyTarget), 2),
|
|
383
|
+
`</bw:scope>`,
|
|
384
|
+
].join("");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildWorkflowAnchorXml(
|
|
388
|
+
anchor: EditorAnchorProjection,
|
|
389
|
+
storyTarget: WorkflowScope["storyTarget"],
|
|
390
|
+
): string {
|
|
391
|
+
const resolvedStoryTarget = storyTarget ?? { kind: "main" as const };
|
|
392
|
+
const range = anchor.kind === "range"
|
|
393
|
+
? { from: anchor.from, to: anchor.to }
|
|
394
|
+
: anchor.kind === "node"
|
|
395
|
+
? { from: anchor.at, to: anchor.at }
|
|
396
|
+
: { from: anchor.lastKnownRange.from, to: anchor.lastKnownRange.to };
|
|
397
|
+
|
|
398
|
+
return [
|
|
399
|
+
`<bw:anchor${serializeOverlayStoryTargetAttributes(resolvedStoryTarget)} targetKind="text">`,
|
|
400
|
+
` <bw:selector type="position-range" start="${String(range.from)}" end="${String(range.to)}" />`,
|
|
401
|
+
` <bw:selector type="binding-hash" alg="sha256" value="${escapeXml(createAnchorBindingHash(resolvedStoryTarget, range.from, range.to))}" />`,
|
|
402
|
+
`</bw:anchor>`,
|
|
403
|
+
].join("\n");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function serializeOverlayStoryTargetAttributes(target: WorkflowScope["storyTarget"]): string {
|
|
407
|
+
const storyTarget = target ?? { kind: "main" as const };
|
|
408
|
+
switch (storyTarget.kind) {
|
|
409
|
+
case "main":
|
|
410
|
+
return ` story="main"`;
|
|
411
|
+
case "header":
|
|
412
|
+
case "footer":
|
|
413
|
+
return [
|
|
414
|
+
` story="${escapeXml(storyTarget.kind)}"`,
|
|
415
|
+
` relationshipId="${escapeXml(storyTarget.relationshipId)}"`,
|
|
416
|
+
` variant="${escapeXml(storyTarget.variant)}"`,
|
|
417
|
+
typeof storyTarget.sectionIndex === "number"
|
|
418
|
+
? ` sectionIndex="${String(storyTarget.sectionIndex)}"`
|
|
419
|
+
: "",
|
|
420
|
+
].join("");
|
|
421
|
+
case "footnote":
|
|
422
|
+
case "endnote":
|
|
423
|
+
return [
|
|
424
|
+
` story="${escapeXml(storyTarget.kind)}"`,
|
|
425
|
+
` noteId="${escapeXml(storyTarget.noteId)}"`,
|
|
426
|
+
].join("");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function createAnchorBindingHash(
|
|
431
|
+
storyTarget: NonNullable<WorkflowScope["storyTarget"]>,
|
|
432
|
+
from: number,
|
|
433
|
+
to: number,
|
|
434
|
+
): string {
|
|
435
|
+
return `${serializeStoryTargetKey(storyTarget)}:${from}:${to}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getPreservedExtensionsXml(sourcePackage: OpcPackage, payloadPartPath: string): string {
|
|
439
|
+
const payloadPart = sourcePackage.parts.get(payloadPartPath);
|
|
440
|
+
if (!payloadPart) {
|
|
441
|
+
return "";
|
|
442
|
+
}
|
|
443
|
+
const xml = decodeUtf8(payloadPart.bytes);
|
|
444
|
+
const match = xml.match(/<bw:extensions\b[^>]*>([\s\S]*?)<\/bw:extensions>/u);
|
|
445
|
+
if (!match) {
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
448
|
+
return stripWorkblockExtension(match[1] ?? "").trim();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function stripWorkblockExtension(extensionsInnerXml: string): string {
|
|
452
|
+
return extensionsInnerXml.replace(/<bw:workblock\b[\s\S]*?<\/bw:workblock>/gu, "").trim();
|
|
453
|
+
}
|
|
454
|
+
|
|
286
455
|
function serializeWorkflowMetadataValue(
|
|
287
456
|
value: WorkflowMetadataEntry["value"],
|
|
288
457
|
): { type: "json"; text: string } {
|
|
@@ -361,6 +530,140 @@ function serializeWorkflowStoryTarget(target: WorkflowMetadataEntry["storyTarget
|
|
|
361
530
|
}
|
|
362
531
|
}
|
|
363
532
|
|
|
533
|
+
function parseWorkflowOverlay(xml: string): WorkflowOverlay | undefined {
|
|
534
|
+
const overlayMatch = xml.match(/<bw:workflowOverlay\b([^>]*)>([\s\S]*?)<\/bw:workflowOverlay>/u);
|
|
535
|
+
if (!overlayMatch) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
const overlayAttributes = parseAttributes(overlayMatch[1] ?? "");
|
|
539
|
+
const overlayBody = overlayMatch[2] ?? "";
|
|
540
|
+
const scopes = [...overlayBody.matchAll(/<bw:scope\b([^>]*)>([\s\S]*?)<\/bw:scope>/gu)]
|
|
541
|
+
.map((match) => parseWorkflowScope(match[1] ?? "", match[2] ?? ""))
|
|
542
|
+
.filter((scope): scope is WorkflowScope => scope !== null);
|
|
543
|
+
const workItems = [...overlayBody.matchAll(/<bw:workItem\b([^>]*)>([\s\S]*?)<\/bw:workItem>/gu)]
|
|
544
|
+
.map((match) => parseWorkflowWorkItem(match[1] ?? "", match[2] ?? ""))
|
|
545
|
+
.filter((item): item is WorkflowWorkItem => item !== null);
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
overlayVersion: (overlayAttributes.overlayVersion as WorkflowOverlay["overlayVersion"]) ?? "workflow-overlay/1",
|
|
549
|
+
activeWorkItemId: overlayAttributes.activeWorkItemId ?? null,
|
|
550
|
+
scopes,
|
|
551
|
+
...(workItems.length > 0 ? { workItems } : {}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function parseWorkflowScope(attributesSource: string, body: string): WorkflowScope | null {
|
|
556
|
+
const attributes = parseAttributes(attributesSource);
|
|
557
|
+
if (!attributes.id || !attributes.mode) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
const anchorMatch = body.match(/<bw:anchor\b([^>]*)>([\s\S]*?)<\/bw:anchor>/u);
|
|
561
|
+
const anchor = anchorMatch
|
|
562
|
+
? parseWorkflowOverlayAnchor(anchorMatch[1] ?? "", anchorMatch[2] ?? "")
|
|
563
|
+
: createDefaultRangeAnchor(0, 0);
|
|
564
|
+
return {
|
|
565
|
+
scopeId: attributes.id,
|
|
566
|
+
mode: attributes.mode as WorkflowScope["mode"],
|
|
567
|
+
anchor,
|
|
568
|
+
storyTarget: anchorMatch ? parseOverlayStoryTarget(parseAttributes(anchorMatch[1] ?? "")) : { kind: "main" },
|
|
569
|
+
workItemId: attributes.workItemRef,
|
|
570
|
+
label: attributes.label,
|
|
571
|
+
domain: attributes.domain as WorkflowScope["domain"],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function parseWorkflowWorkItem(attributesSource: string, body: string): WorkflowWorkItem | null {
|
|
576
|
+
const attributes = parseAttributes(attributesSource);
|
|
577
|
+
if (!attributes.id || !attributes.title) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const scopeIds = [...body.matchAll(/<bw:scopeRef\b([^>]*)\/>/gu)]
|
|
581
|
+
.map((match) => parseAttributes(match[1] ?? "").ref ?? "")
|
|
582
|
+
.filter((value) => value.length > 0);
|
|
583
|
+
return {
|
|
584
|
+
workItemId: attributes.id,
|
|
585
|
+
title: attributes.title,
|
|
586
|
+
status: attributes.status as WorkflowWorkItem["status"],
|
|
587
|
+
domain: attributes.domain as WorkflowWorkItem["domain"],
|
|
588
|
+
scopeIds,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function parseWorkflowOverlayAnchor(
|
|
593
|
+
attributesSource: string,
|
|
594
|
+
body: string,
|
|
595
|
+
): EditorAnchorProjection {
|
|
596
|
+
const selectorMatches = [...body.matchAll(/<bw:selector\b([^>]*)\/>/gu)];
|
|
597
|
+
const positionSelector = selectorMatches
|
|
598
|
+
.map((match) => parseAttributes(match[1] ?? ""))
|
|
599
|
+
.find((attributes) => attributes.type === "position-range");
|
|
600
|
+
if (positionSelector?.start !== undefined && positionSelector.end !== undefined) {
|
|
601
|
+
return createDefaultRangeAnchor(Number(positionSelector.start), Number(positionSelector.end));
|
|
602
|
+
}
|
|
603
|
+
const anchorAttributes = parseAttributes(attributesSource);
|
|
604
|
+
if (anchorAttributes.targetKind === "node" && anchorAttributes.at !== undefined) {
|
|
605
|
+
return {
|
|
606
|
+
kind: "node",
|
|
607
|
+
at: Number(anchorAttributes.at),
|
|
608
|
+
assoc: 1,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return createDefaultRangeAnchor(0, 0);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function parseOverlayStoryTarget(attributes: Record<string, string>): WorkflowScope["storyTarget"] {
|
|
615
|
+
switch (attributes.story) {
|
|
616
|
+
case "header":
|
|
617
|
+
case "footer":
|
|
618
|
+
if (!attributes.relationshipId || !attributes.variant) {
|
|
619
|
+
return { kind: "main" };
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
kind: attributes.story,
|
|
623
|
+
relationshipId: attributes.relationshipId,
|
|
624
|
+
variant: attributes.variant as "default" | "first" | "even",
|
|
625
|
+
sectionIndex: attributes.sectionIndex !== undefined ? Number(attributes.sectionIndex) : undefined,
|
|
626
|
+
};
|
|
627
|
+
case "footnote":
|
|
628
|
+
case "endnote":
|
|
629
|
+
if (!attributes.noteId) {
|
|
630
|
+
return { kind: "main" };
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
kind: attributes.story,
|
|
634
|
+
noteId: attributes.noteId,
|
|
635
|
+
};
|
|
636
|
+
case "main":
|
|
637
|
+
default:
|
|
638
|
+
return { kind: "main" };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function createDefaultRangeAnchor(from: number, to: number): EditorAnchorProjection {
|
|
643
|
+
return {
|
|
644
|
+
kind: "range",
|
|
645
|
+
from,
|
|
646
|
+
to,
|
|
647
|
+
assoc: {
|
|
648
|
+
start: -1,
|
|
649
|
+
end: 1,
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function serializeStoryTargetKey(storyTarget: NonNullable<WorkflowScope["storyTarget"]>): string {
|
|
655
|
+
switch (storyTarget.kind) {
|
|
656
|
+
case "main":
|
|
657
|
+
return "main";
|
|
658
|
+
case "header":
|
|
659
|
+
case "footer":
|
|
660
|
+
return `${storyTarget.kind}:${storyTarget.relationshipId}:${storyTarget.variant}:${storyTarget.sectionIndex ?? "none"}`;
|
|
661
|
+
case "footnote":
|
|
662
|
+
case "endnote":
|
|
663
|
+
return `${storyTarget.kind}:${storyTarget.noteId}`;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
364
667
|
function parseWorkflowStoryTarget(
|
|
365
668
|
attributes: Record<string, string>,
|
|
366
669
|
): WorkflowMetadataEntry["storyTarget"] {
|
|
@@ -423,7 +726,7 @@ function resolveDescriptor(sourcePackage: OpcPackage, documentId: string): Embed
|
|
|
423
726
|
};
|
|
424
727
|
}
|
|
425
728
|
|
|
426
|
-
function resolvePayloadPartPath(sourcePackage: OpcPackage): string | null {
|
|
729
|
+
export function resolvePayloadPartPath(sourcePackage: OpcPackage): string | null {
|
|
427
730
|
const mirroredItemId = readCustomPropertyValue(
|
|
428
731
|
sourcePackage,
|
|
429
732
|
"BwWorkflowPayloadItemId",
|
package/src/model/snapshot.ts
CHANGED
|
@@ -122,6 +122,33 @@ export interface PersistedWorkflowMetadataSnapshot {
|
|
|
122
122
|
entries: PersistedWorkflowMetadataEntry[];
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export interface PersistedWorkflowScope {
|
|
126
|
+
scopeId: string;
|
|
127
|
+
mode: "edit" | "suggest" | "comment" | "view";
|
|
128
|
+
anchor: Record<string, unknown>;
|
|
129
|
+
storyTarget?: Record<string, unknown>;
|
|
130
|
+
workItemId?: string;
|
|
131
|
+
label?: string;
|
|
132
|
+
domain?: "legal" | "commercial" | "finance" | "other";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PersistedWorkflowWorkItem {
|
|
136
|
+
workItemId: string;
|
|
137
|
+
title: string;
|
|
138
|
+
description?: string;
|
|
139
|
+
domain?: "legal" | "commercial" | "finance" | "other";
|
|
140
|
+
status?: "pending" | "active" | "done" | "blocked";
|
|
141
|
+
scopeIds: string[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface PersistedWorkflowOverlay {
|
|
145
|
+
overlayVersion: "workflow-overlay/1";
|
|
146
|
+
candidates?: Array<Record<string, unknown>>;
|
|
147
|
+
scopes: PersistedWorkflowScope[];
|
|
148
|
+
workItems?: PersistedWorkflowWorkItem[];
|
|
149
|
+
activeWorkItemId?: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
export interface PersistedEditorSnapshot {
|
|
126
153
|
snapshotVersion: PersistedEditorSnapshotVersion;
|
|
127
154
|
schemaVersion: typeof CDS_SCHEMA_VERSION;
|
|
@@ -136,6 +163,7 @@ export interface PersistedEditorSnapshot {
|
|
|
136
163
|
warningLog: EditorWarning[];
|
|
137
164
|
protectionSnapshot?: ProtectionSnapshotRecord;
|
|
138
165
|
sourcePackage?: PersistedSourcePackage;
|
|
166
|
+
workflowOverlay?: PersistedWorkflowOverlay;
|
|
139
167
|
workflowMetadata?: PersistedWorkflowMetadataSnapshot;
|
|
140
168
|
}
|
|
141
169
|
|
|
@@ -153,7 +181,12 @@ const SNAPSHOT_REQUIRED_TOP_LEVEL_KEYS = [
|
|
|
153
181
|
"warningLog",
|
|
154
182
|
] as const;
|
|
155
183
|
|
|
156
|
-
const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = [
|
|
184
|
+
const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = [
|
|
185
|
+
"sourcePackage",
|
|
186
|
+
"protectionSnapshot",
|
|
187
|
+
"workflowOverlay",
|
|
188
|
+
"workflowMetadata",
|
|
189
|
+
] as const;
|
|
157
190
|
|
|
158
191
|
export interface ProtectionRangeRecord {
|
|
159
192
|
rangeId: string;
|
|
@@ -308,6 +341,9 @@ export function validatePersistedEditorSnapshot(
|
|
|
308
341
|
if (record.protectionSnapshot !== undefined) {
|
|
309
342
|
validateProtectionSnapshot(record.protectionSnapshot, "$.protectionSnapshot", issues);
|
|
310
343
|
}
|
|
344
|
+
if (record.workflowOverlay !== undefined) {
|
|
345
|
+
validateWorkflowOverlay(record.workflowOverlay, "$.workflowOverlay", issues);
|
|
346
|
+
}
|
|
311
347
|
if (record.workflowMetadata !== undefined) {
|
|
312
348
|
validateWorkflowMetadataSnapshot(record.workflowMetadata, "$.workflowMetadata", issues);
|
|
313
349
|
}
|
|
@@ -642,6 +678,82 @@ function validateProtectionSnapshot(
|
|
|
642
678
|
}
|
|
643
679
|
}
|
|
644
680
|
|
|
681
|
+
function validateWorkflowOverlay(
|
|
682
|
+
value: unknown,
|
|
683
|
+
path: string,
|
|
684
|
+
issues: ModelValidationIssue[],
|
|
685
|
+
): void {
|
|
686
|
+
const record = asPlainObject(value, path, issues);
|
|
687
|
+
if (!record) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
expectExactString(record.overlayVersion, "workflow-overlay/1", `${path}.overlayVersion`, issues);
|
|
691
|
+
if (!Array.isArray(record.scopes)) {
|
|
692
|
+
issues.push({ path: `${path}.scopes`, message: "scopes must be an array." });
|
|
693
|
+
} else {
|
|
694
|
+
record.scopes.forEach((scope, index) => validateWorkflowScope(scope, `${path}.scopes[${index}]`, issues));
|
|
695
|
+
}
|
|
696
|
+
if (record.workItems !== undefined) {
|
|
697
|
+
if (!Array.isArray(record.workItems)) {
|
|
698
|
+
issues.push({ path: `${path}.workItems`, message: "workItems must be an array." });
|
|
699
|
+
} else {
|
|
700
|
+
record.workItems.forEach((workItem, index) =>
|
|
701
|
+
validateWorkflowWorkItem(workItem, `${path}.workItems[${index}]`, issues),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (record.activeWorkItemId !== undefined && record.activeWorkItemId !== null) {
|
|
706
|
+
expectString(record.activeWorkItemId, `${path}.activeWorkItemId`, issues);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function validateWorkflowScope(
|
|
711
|
+
value: unknown,
|
|
712
|
+
path: string,
|
|
713
|
+
issues: ModelValidationIssue[],
|
|
714
|
+
): void {
|
|
715
|
+
const record = asPlainObject(value, path, issues);
|
|
716
|
+
if (!record) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
expectString(record.scopeId, `${path}.scopeId`, issues);
|
|
720
|
+
if (
|
|
721
|
+
record.mode !== "edit" &&
|
|
722
|
+
record.mode !== "suggest" &&
|
|
723
|
+
record.mode !== "comment" &&
|
|
724
|
+
record.mode !== "view"
|
|
725
|
+
) {
|
|
726
|
+
issues.push({ path: `${path}.mode`, message: "mode must be edit, suggest, comment, or view." });
|
|
727
|
+
}
|
|
728
|
+
asPlainObject(record.anchor, `${path}.anchor`, issues);
|
|
729
|
+
if (record.workItemId !== undefined) {
|
|
730
|
+
expectString(record.workItemId, `${path}.workItemId`, issues);
|
|
731
|
+
}
|
|
732
|
+
if (record.label !== undefined) {
|
|
733
|
+
expectString(record.label, `${path}.label`, issues);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function validateWorkflowWorkItem(
|
|
738
|
+
value: unknown,
|
|
739
|
+
path: string,
|
|
740
|
+
issues: ModelValidationIssue[],
|
|
741
|
+
): void {
|
|
742
|
+
const record = asPlainObject(value, path, issues);
|
|
743
|
+
if (!record) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
expectString(record.workItemId, `${path}.workItemId`, issues);
|
|
747
|
+
expectString(record.title, `${path}.title`, issues);
|
|
748
|
+
if (!Array.isArray(record.scopeIds)) {
|
|
749
|
+
issues.push({ path: `${path}.scopeIds`, message: "scopeIds must be an array." });
|
|
750
|
+
} else {
|
|
751
|
+
record.scopeIds.forEach((scopeId, index) => {
|
|
752
|
+
expectString(scopeId, `${path}.scopeIds[${index}]`, issues);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
645
757
|
function validateWorkflowMetadataSnapshot(
|
|
646
758
|
value: unknown,
|
|
647
759
|
path: string,
|