@beyondwork/docx-react-component 1.0.41 → 1.0.42
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/package.json +13 -1
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +347 -4
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +568 -1
- package/src/index.ts +118 -1
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +271 -0
- package/src/io/ooxml/workflow-payload.ts +146 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +280 -93
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +122 -12
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +230 -34
- package/src/runtime/layout/public-facet.ts +185 -13
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +587 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +11 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +32 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-path validator for bw:workflowPayload 1.0 / 1.1.
|
|
3
|
+
*
|
|
4
|
+
* This is a structural sanity check that runs inline on every import.
|
|
5
|
+
* It does NOT replace the Railway OOXML validator — that one checks
|
|
6
|
+
* package-level concerns (part types, relationship sanity, Word-spec
|
|
7
|
+
* compliance). This validator catches malformed bw:workflowPayload
|
|
8
|
+
* content that the parser would silently strip. Results surface to
|
|
9
|
+
* callers so tooling can log, display, or assert on them.
|
|
10
|
+
*
|
|
11
|
+
* Spec: docs/reference/bw-schema-spec.md §8.2.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ValidatorIssueCode =
|
|
15
|
+
| "unknown_enum_value"
|
|
16
|
+
| "external_entry_missing_ref"
|
|
17
|
+
| "external_entry_body_ignored"
|
|
18
|
+
| "external_field_missing_ref"
|
|
19
|
+
| "external_field_body_ignored"
|
|
20
|
+
| "internal_entry_unexpected_storage_ref"
|
|
21
|
+
| "internal_field_unexpected_storage_ref"
|
|
22
|
+
| "unsupported_version";
|
|
23
|
+
|
|
24
|
+
export type ValidatorIssueSeverity = "error" | "warning";
|
|
25
|
+
|
|
26
|
+
export interface ValidatorIssue {
|
|
27
|
+
code: ValidatorIssueCode;
|
|
28
|
+
path: string; // e.g. "bw:entry[@key='e1']"
|
|
29
|
+
severity: ValidatorIssueSeverity;
|
|
30
|
+
value?: string; // the invalid value when relevant
|
|
31
|
+
detail?: string; // optional extra context
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const OVERLAY_PERSISTENCE_VALUES = ["internal", "external"] as const;
|
|
35
|
+
const SCOPE_PERSISTENCE_VALUES = ["internal", "external", "inherit"] as const;
|
|
36
|
+
|
|
37
|
+
export function validateWorkflowPayloadEnvelope(xml: string): ValidatorIssue[] {
|
|
38
|
+
const issues: ValidatorIssue[] = [];
|
|
39
|
+
|
|
40
|
+
// Root version check.
|
|
41
|
+
const versionMatch = xml.match(/<bw:workflowPayload\b[^>]*\sversion="([^"]+)"/u);
|
|
42
|
+
if (versionMatch && !/^1\.[0-9]+$/.test(versionMatch[1])) {
|
|
43
|
+
issues.push({
|
|
44
|
+
code: "unsupported_version",
|
|
45
|
+
path: "bw:workflowPayload/@version",
|
|
46
|
+
value: versionMatch[1],
|
|
47
|
+
severity: "warning",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Overlay-level metadataPersistence.
|
|
52
|
+
const overlayMatch = xml.match(/<bw:workflowOverlay\b([^>]*)>/u);
|
|
53
|
+
let overlayEnum: "internal" | "external" | undefined;
|
|
54
|
+
if (overlayMatch) {
|
|
55
|
+
const attrs = parseAttrs(overlayMatch[1]);
|
|
56
|
+
if (attrs.metadataPersistence !== undefined) {
|
|
57
|
+
if (
|
|
58
|
+
!(OVERLAY_PERSISTENCE_VALUES as readonly string[]).includes(
|
|
59
|
+
attrs.metadataPersistence,
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
issues.push({
|
|
63
|
+
code: "unknown_enum_value",
|
|
64
|
+
path: "bw:workflowOverlay/@metadataPersistence",
|
|
65
|
+
value: attrs.metadataPersistence,
|
|
66
|
+
severity: "warning",
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
overlayEnum = attrs.metadataPersistence as "internal" | "external";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Each scope + its fields.
|
|
75
|
+
for (const scope of findAll(xml, /<bw:scope\b([^>]*?)>([\s\S]*?)<\/bw:scope>/gu)) {
|
|
76
|
+
const scopeAttrs = parseAttrs(scope.attrs);
|
|
77
|
+
const scopeId = scopeAttrs.id ?? "?";
|
|
78
|
+
let scopePersistence: string | undefined = scopeAttrs.metadataPersistence;
|
|
79
|
+
if (scopePersistence !== undefined) {
|
|
80
|
+
if (
|
|
81
|
+
!(SCOPE_PERSISTENCE_VALUES as readonly string[]).includes(scopePersistence)
|
|
82
|
+
) {
|
|
83
|
+
issues.push({
|
|
84
|
+
code: "unknown_enum_value",
|
|
85
|
+
path: `bw:scope[@id='${scopeId}']/@metadataPersistence`,
|
|
86
|
+
value: scopePersistence,
|
|
87
|
+
severity: "warning",
|
|
88
|
+
});
|
|
89
|
+
scopePersistence = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// <bw:field>, two forms: self-closing and with body.
|
|
94
|
+
for (const field of findFields(scope.body)) {
|
|
95
|
+
const fieldAttrs = parseAttrs(field.attrs);
|
|
96
|
+
const fieldKey = fieldAttrs.key ?? "?";
|
|
97
|
+
|
|
98
|
+
let fieldPersistence: string | undefined = fieldAttrs.metadataPersistence;
|
|
99
|
+
if (
|
|
100
|
+
fieldPersistence !== undefined &&
|
|
101
|
+
!(SCOPE_PERSISTENCE_VALUES as readonly string[]).includes(fieldPersistence)
|
|
102
|
+
) {
|
|
103
|
+
issues.push({
|
|
104
|
+
code: "unknown_enum_value",
|
|
105
|
+
path: `bw:scope[@id='${scopeId}']/bw:field[@key='${fieldKey}']/@metadataPersistence`,
|
|
106
|
+
value: fieldPersistence,
|
|
107
|
+
severity: "warning",
|
|
108
|
+
});
|
|
109
|
+
fieldPersistence = undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const effective = resolveEffective({
|
|
113
|
+
overlay: overlayEnum,
|
|
114
|
+
scope: scopePersistence,
|
|
115
|
+
field: fieldPersistence,
|
|
116
|
+
});
|
|
117
|
+
const hasRef = Boolean(fieldAttrs.storageRef);
|
|
118
|
+
const hasBody = field.body.trim().length > 0;
|
|
119
|
+
|
|
120
|
+
if (effective === "external") {
|
|
121
|
+
if (!hasRef) {
|
|
122
|
+
issues.push({
|
|
123
|
+
code: "external_field_missing_ref",
|
|
124
|
+
path: `bw:scope[@id='${scopeId}']/bw:field[@key='${fieldKey}']`,
|
|
125
|
+
severity: "error",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (hasBody) {
|
|
129
|
+
issues.push({
|
|
130
|
+
code: "external_field_body_ignored",
|
|
131
|
+
path: `bw:scope[@id='${scopeId}']/bw:field[@key='${fieldKey}']`,
|
|
132
|
+
severity: "warning",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else if (effective === "internal" && hasRef) {
|
|
136
|
+
// Internal-mode fields carry their value inline. A `storageRef` on
|
|
137
|
+
// an internal field is a stale pointer from a persistence-mode
|
|
138
|
+
// downgrade and will confuse hosts that branch on its presence.
|
|
139
|
+
issues.push({
|
|
140
|
+
code: "internal_field_unexpected_storage_ref",
|
|
141
|
+
path: `bw:scope[@id='${scopeId}']/bw:field[@key='${fieldKey}']`,
|
|
142
|
+
severity: "warning",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Top-level entries (bw:metadata body, outside scopes).
|
|
149
|
+
// These resolve against overlay-only (no scope context available unless
|
|
150
|
+
// the entry carries its own `scope="scope:{id}"` attribute, in which case
|
|
151
|
+
// a matching scope's override would apply — we keep this simple and just
|
|
152
|
+
// use the overlay default for entries).
|
|
153
|
+
for (const entry of findEntries(xml)) {
|
|
154
|
+
const entryAttrs = parseAttrs(entry.attrs);
|
|
155
|
+
const entryKey = entryAttrs.key ?? "?";
|
|
156
|
+
|
|
157
|
+
// Definition rows (key="definition.*") are schema descriptors, not data
|
|
158
|
+
// entries. Their body is always the human-readable label (per §4.1) and
|
|
159
|
+
// they are never subject to external-persistence rules (§4.3 applies only
|
|
160
|
+
// to data rows, §4.2). Skip persistence checks for them.
|
|
161
|
+
if (entryKey.startsWith("definition.")) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let entryPersistence: string | undefined = entryAttrs.metadataPersistence;
|
|
166
|
+
if (
|
|
167
|
+
entryPersistence !== undefined &&
|
|
168
|
+
!(SCOPE_PERSISTENCE_VALUES as readonly string[]).includes(entryPersistence)
|
|
169
|
+
) {
|
|
170
|
+
issues.push({
|
|
171
|
+
code: "unknown_enum_value",
|
|
172
|
+
path: `bw:entry[@key='${entryKey}']/@metadataPersistence`,
|
|
173
|
+
value: entryPersistence,
|
|
174
|
+
severity: "warning",
|
|
175
|
+
});
|
|
176
|
+
entryPersistence = undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const effective = resolveEffective({
|
|
180
|
+
overlay: overlayEnum,
|
|
181
|
+
scope: undefined,
|
|
182
|
+
field: entryPersistence,
|
|
183
|
+
});
|
|
184
|
+
const hasRef = Boolean(entryAttrs.storageRef);
|
|
185
|
+
const hasBody = entry.body.trim().length > 0;
|
|
186
|
+
|
|
187
|
+
if (effective === "external") {
|
|
188
|
+
if (!hasRef) {
|
|
189
|
+
issues.push({
|
|
190
|
+
code: "external_entry_missing_ref",
|
|
191
|
+
path: `bw:entry[@key='${entryKey}']`,
|
|
192
|
+
severity: "error",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (hasBody) {
|
|
196
|
+
issues.push({
|
|
197
|
+
code: "external_entry_body_ignored",
|
|
198
|
+
path: `bw:entry[@key='${entryKey}']`,
|
|
199
|
+
severity: "warning",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} else if (effective === "internal" && hasRef) {
|
|
203
|
+
// Internal-mode entries carry their value inline. A `storageRef` on
|
|
204
|
+
// an internal entry is a stale pointer from a persistence-mode
|
|
205
|
+
// downgrade and will confuse hosts that branch on its presence.
|
|
206
|
+
issues.push({
|
|
207
|
+
code: "internal_entry_unexpected_storage_ref",
|
|
208
|
+
path: `bw:entry[@key='${entryKey}']`,
|
|
209
|
+
severity: "warning",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return issues;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseAttrs(source: string): Record<string, string> {
|
|
218
|
+
const out: Record<string, string> = {};
|
|
219
|
+
for (const m of source.matchAll(/(\w+)="([^"]*)"/gu)) {
|
|
220
|
+
out[m[1]] = m[2];
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function* findAll(
|
|
226
|
+
xml: string,
|
|
227
|
+
re: RegExp,
|
|
228
|
+
): Generator<{ attrs: string; body: string }> {
|
|
229
|
+
for (const m of xml.matchAll(re)) {
|
|
230
|
+
yield { attrs: m[1] ?? "", body: m[2] ?? "" };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Emits each `<bw:field>` in the scope body, handling both self-closing
|
|
236
|
+
* and body-bearing forms.
|
|
237
|
+
*/
|
|
238
|
+
function* findFields(
|
|
239
|
+
scopeBody: string,
|
|
240
|
+
): Generator<{ attrs: string; body: string }> {
|
|
241
|
+
// Match both <bw:field .../> and <bw:field ...>body</bw:field>
|
|
242
|
+
const re = /<bw:field\b([^>]*?)(?:\/>|>([\s\S]*?)<\/bw:field>)/gu;
|
|
243
|
+
for (const m of scopeBody.matchAll(re)) {
|
|
244
|
+
yield { attrs: m[1] ?? "", body: m[2] ?? "" };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Emits each top-level `<bw:entry>` (inside `<bw:metadata>`), handling
|
|
250
|
+
* both self-closing and body-bearing forms. Scope-internal entries
|
|
251
|
+
* are handled by findFields — this only looks at the flat metadata list.
|
|
252
|
+
*/
|
|
253
|
+
function* findEntries(
|
|
254
|
+
xml: string,
|
|
255
|
+
): Generator<{ attrs: string; body: string }> {
|
|
256
|
+
const re = /<bw:entry\b([^>]*?)(?:\/>|>([\s\S]*?)<\/bw:entry>)/gu;
|
|
257
|
+
for (const m of xml.matchAll(re)) {
|
|
258
|
+
yield { attrs: m[1] ?? "", body: m[2] ?? "" };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resolveEffective(input: {
|
|
263
|
+
overlay?: "internal" | "external";
|
|
264
|
+
scope?: string;
|
|
265
|
+
field?: string;
|
|
266
|
+
}): "internal" | "external" {
|
|
267
|
+
if (input.field === "internal" || input.field === "external") return input.field;
|
|
268
|
+
if (input.scope === "internal" || input.scope === "external") return input.scope;
|
|
269
|
+
if (input.overlay === "external") return "external";
|
|
270
|
+
return "internal";
|
|
271
|
+
}
|
|
@@ -12,6 +12,29 @@ import type {
|
|
|
12
12
|
WorkflowScope,
|
|
13
13
|
WorkflowWorkItem,
|
|
14
14
|
} from "../../api/public-types.ts";
|
|
15
|
+
import {
|
|
16
|
+
validateWorkflowPayloadEnvelope,
|
|
17
|
+
type ValidatorIssue,
|
|
18
|
+
} from "./workflow-payload-validator.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Schema 1.1 parser helpers (fail-closed per spec §8.2)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function parseClosedEnum<T extends string>(
|
|
25
|
+
raw: string | undefined,
|
|
26
|
+
allowed: readonly T[],
|
|
27
|
+
): T | undefined {
|
|
28
|
+
if (raw === undefined) return undefined;
|
|
29
|
+
return (allowed as readonly string[]).includes(raw) ? (raw as T) : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseNonNegativeInt(raw: string | undefined): number | undefined {
|
|
33
|
+
if (raw === undefined) return undefined;
|
|
34
|
+
const n = Number(raw);
|
|
35
|
+
if (!Number.isInteger(n) || n < 0) return undefined;
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
15
38
|
|
|
16
39
|
export const WORKFLOW_PAYLOAD_PART_PATH = "/customXml/item1.xml";
|
|
17
40
|
export const WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH = "/customXml/itemProps1.xml";
|
|
@@ -39,6 +62,7 @@ type EmbeddedWorkflowPayloadDescriptor = {
|
|
|
39
62
|
export interface WorkflowPayloadEnvelope {
|
|
40
63
|
workflowMetadata: WorkflowMetadataSnapshot;
|
|
41
64
|
workflowOverlay?: WorkflowOverlay;
|
|
65
|
+
validatorIssues?: readonly ValidatorIssue[];
|
|
42
66
|
}
|
|
43
67
|
|
|
44
68
|
export function getDocumentBackedWorkflowMetadata(
|
|
@@ -144,7 +168,8 @@ export function parseWorkflowPayloadEnvelopeFromPackage(
|
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
const xml = decodeUtf8(payloadPart.bytes);
|
|
147
|
-
|
|
171
|
+
// Match both self-closing (<bw:entry .../>) and body-bearing (<bw:entry ...>body</bw:entry>)
|
|
172
|
+
const entryMatches = [...xml.matchAll(/<bw:entry\b([^>]*)(?:\/>|>([\s\S]*?)<\/bw:entry>)/g)];
|
|
148
173
|
const definitions: WorkflowMetadataDefinition[] = [];
|
|
149
174
|
const entries: WorkflowMetadataEntry[] = [];
|
|
150
175
|
|
|
@@ -183,6 +208,14 @@ export function parseWorkflowPayloadEnvelopeFromPackage(
|
|
|
183
208
|
if (!entryId || !metadataId) {
|
|
184
209
|
continue;
|
|
185
210
|
}
|
|
211
|
+
|
|
212
|
+
const entryMetadataPersistence = parseClosedEnum(
|
|
213
|
+
attributes.metadataPersistence,
|
|
214
|
+
["internal", "external", "inherit"] as const,
|
|
215
|
+
);
|
|
216
|
+
const entryStorageRef = attributes.storageRef;
|
|
217
|
+
const entryMetadataVersion = parseNonNegativeInt(attributes.metadataVersion);
|
|
218
|
+
|
|
186
219
|
entries.push({
|
|
187
220
|
entryId,
|
|
188
221
|
metadataId,
|
|
@@ -198,18 +231,62 @@ export function parseWorkflowPayloadEnvelopeFromPackage(
|
|
|
198
231
|
...(attributes.scope?.startsWith("workItem:")
|
|
199
232
|
? { workItemId: attributes.scope.slice("workItem:".length) }
|
|
200
233
|
: {}),
|
|
234
|
+
...(entryMetadataPersistence !== undefined ? { metadataPersistence: entryMetadataPersistence } : {}),
|
|
235
|
+
...(entryStorageRef !== undefined ? { storageRef: entryStorageRef } : {}),
|
|
236
|
+
...(entryMetadataVersion !== undefined ? { metadataVersion: entryMetadataVersion } : {}),
|
|
201
237
|
});
|
|
202
238
|
}
|
|
203
239
|
|
|
240
|
+
const validatorIssues = validateWorkflowPayloadEnvelope(xml);
|
|
204
241
|
return {
|
|
205
242
|
workflowMetadata: {
|
|
206
243
|
definitions,
|
|
207
244
|
entries,
|
|
208
245
|
},
|
|
209
246
|
workflowOverlay: parseWorkflowOverlay(xml),
|
|
247
|
+
...(validatorIssues.length > 0 ? { validatorIssues } : {}),
|
|
210
248
|
};
|
|
211
249
|
}
|
|
212
250
|
|
|
251
|
+
function needsSchemaV11(input: {
|
|
252
|
+
workflowMetadata: WorkflowMetadataSnapshot;
|
|
253
|
+
workflowOverlay?: WorkflowOverlay;
|
|
254
|
+
}): boolean {
|
|
255
|
+
if (input.workflowOverlay?.metadataPersistence !== undefined) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (
|
|
259
|
+
input.workflowOverlay?.scopes?.some(
|
|
260
|
+
(s) => s.metadataPersistence !== undefined,
|
|
261
|
+
)
|
|
262
|
+
) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
input.workflowOverlay?.scopes?.some((s) =>
|
|
267
|
+
s.metadata?.some(
|
|
268
|
+
(f) =>
|
|
269
|
+
f.metadataPersistence !== undefined ||
|
|
270
|
+
f.storageRef !== undefined ||
|
|
271
|
+
f.metadataVersion !== undefined,
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
input.workflowMetadata.entries.some(
|
|
279
|
+
(e) =>
|
|
280
|
+
e.metadataPersistence !== undefined ||
|
|
281
|
+
e.storageRef !== undefined ||
|
|
282
|
+
e.metadataVersion !== undefined,
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
213
290
|
function buildPayloadXml(input: {
|
|
214
291
|
descriptor: EmbeddedWorkflowPayloadDescriptor;
|
|
215
292
|
createdAt: string;
|
|
@@ -219,6 +296,8 @@ function buildPayloadXml(input: {
|
|
|
219
296
|
workflowOverlay?: WorkflowOverlay;
|
|
220
297
|
preservedExtensionsXml: string;
|
|
221
298
|
}): string {
|
|
299
|
+
const schemaVersion = needsSchemaV11(input) ? "1.1" : "1.0";
|
|
300
|
+
|
|
222
301
|
const definitionEntriesXml = input.workflowMetadata.definitions
|
|
223
302
|
.map((definition) => [
|
|
224
303
|
`<bw:entry`,
|
|
@@ -237,7 +316,7 @@ function buildPayloadXml(input: {
|
|
|
237
316
|
const serializedValue = serializeWorkflowMetadataValue(entry.value);
|
|
238
317
|
const storyTargetAttributes = serializeWorkflowStoryTarget(entry.storyTarget);
|
|
239
318
|
|
|
240
|
-
|
|
319
|
+
const baseAttrs = [
|
|
241
320
|
`<bw:entry`,
|
|
242
321
|
` key="${escapeXml(entry.entryId)}"`,
|
|
243
322
|
` metadataId="${escapeXml(entry.metadataId)}"`,
|
|
@@ -245,8 +324,24 @@ function buildPayloadXml(input: {
|
|
|
245
324
|
` valueType="${escapeXml(serializedValue.type)}"`,
|
|
246
325
|
` scope="${escapeXml(entry.scopeId ? `scope:${entry.scopeId}` : entry.workItemId ? `workItem:${entry.workItemId}` : "document")}"`,
|
|
247
326
|
storyTargetAttributes,
|
|
248
|
-
|
|
327
|
+
entry.metadataPersistence && entry.metadataPersistence !== "inherit"
|
|
328
|
+
? ` metadataPersistence="${escapeXml(entry.metadataPersistence)}"`
|
|
329
|
+
: "",
|
|
330
|
+
entry.metadataPersistence === "external" && entry.storageRef
|
|
331
|
+
? ` storageRef="${escapeXml(entry.storageRef)}"`
|
|
332
|
+
: "",
|
|
333
|
+
typeof entry.metadataVersion === "number" &&
|
|
334
|
+
Number.isInteger(entry.metadataVersion) &&
|
|
335
|
+
entry.metadataVersion >= 0
|
|
336
|
+
? ` metadataVersion="${entry.metadataVersion.toString()}"`
|
|
337
|
+
: "",
|
|
249
338
|
].join("");
|
|
339
|
+
|
|
340
|
+
if (entry.metadataPersistence === "external") {
|
|
341
|
+
return `${baseAttrs}/>`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return `${baseAttrs}>${escapeXml(serializedValue.text)}</bw:entry>`;
|
|
250
345
|
})
|
|
251
346
|
.filter((value) => value.length > 0)
|
|
252
347
|
.join("\n");
|
|
@@ -258,7 +353,7 @@ function buildPayloadXml(input: {
|
|
|
258
353
|
|
|
259
354
|
return [
|
|
260
355
|
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
261
|
-
`<bw:workflowPayload xmlns:bw="urn:beyondwork:workflow-payload:1" version="
|
|
356
|
+
`<bw:workflowPayload xmlns:bw="urn:beyondwork:workflow-payload:1" version="${schemaVersion}" payloadId="${escapeXml(input.descriptor.payloadId)}" itemId="${escapeXml(input.descriptor.itemId)}" documentId="${escapeXml(input.descriptor.documentId)}" createdAt="${escapeXml(input.createdAt)}" updatedAt="${escapeXml(input.updatedAt)}">`,
|
|
262
357
|
` <bw:manifest>`,
|
|
263
358
|
` <bw:producer name="@beyondwork/docx-react-component" version="${escapeXml(input.producerVersion)}" />`,
|
|
264
359
|
` <bw:compatibility rebindMode="best-effort" preserveUnknownExtensions="true" />`,
|
|
@@ -357,6 +452,9 @@ function buildWorkblockExtensionXml(workflowOverlay: WorkflowOverlay | undefined
|
|
|
357
452
|
workflowOverlay.activeWorkItemId
|
|
358
453
|
? ` activeWorkItemId="${escapeXml(workflowOverlay.activeWorkItemId)}"`
|
|
359
454
|
: "",
|
|
455
|
+
workflowOverlay.metadataPersistence
|
|
456
|
+
? ` metadataPersistence="${escapeXml(workflowOverlay.metadataPersistence)}"`
|
|
457
|
+
: "",
|
|
360
458
|
`>`,
|
|
361
459
|
].join(""),
|
|
362
460
|
` <bw:workItems>`,
|
|
@@ -380,6 +478,9 @@ function buildWorkflowScopeXml(scope: WorkflowScope): string {
|
|
|
380
478
|
scope.workItemId ? ` workItemRef="${escapeXml(scope.workItemId)}"` : "",
|
|
381
479
|
scope.label ? ` label="${escapeXml(scope.label)}"` : "",
|
|
382
480
|
scope.domain ? ` domain="${escapeXml(scope.domain)}"` : "",
|
|
481
|
+
scope.metadataPersistence && scope.metadataPersistence !== "inherit"
|
|
482
|
+
? ` metadataPersistence="${escapeXml(scope.metadataPersistence)}"`
|
|
483
|
+
: "",
|
|
383
484
|
`>`,
|
|
384
485
|
indentLines(buildWorkflowAnchorXml(scope.anchor, scope.storyTarget), 2),
|
|
385
486
|
scopeMetadataXml ? indentLines(scopeMetadataXml, 2) : "",
|
|
@@ -397,12 +498,28 @@ function buildWorkflowScopeMetadataXml(scopeMetadata: WorkflowScope["metadata"])
|
|
|
397
498
|
scopeMetadata
|
|
398
499
|
.map((field) => {
|
|
399
500
|
const serialized = serializeWorkflowScopeMetadataValue(field.valueType, field.value);
|
|
400
|
-
|
|
501
|
+
const baseAttrs = [
|
|
401
502
|
`<bw:field`,
|
|
402
503
|
` key="${escapeXml(field.key)}"`,
|
|
403
504
|
serialized.valueType ? ` type="${escapeXml(serialized.valueType)}"` : "",
|
|
404
|
-
|
|
505
|
+
field.metadataPersistence && field.metadataPersistence !== "inherit"
|
|
506
|
+
? ` metadataPersistence="${escapeXml(field.metadataPersistence)}"`
|
|
507
|
+
: "",
|
|
508
|
+
field.metadataPersistence === "external" && field.storageRef
|
|
509
|
+
? ` storageRef="${escapeXml(field.storageRef)}"`
|
|
510
|
+
: "",
|
|
511
|
+
typeof field.metadataVersion === "number" &&
|
|
512
|
+
Number.isInteger(field.metadataVersion) &&
|
|
513
|
+
field.metadataVersion >= 0
|
|
514
|
+
? ` metadataVersion="${field.metadataVersion.toString()}"`
|
|
515
|
+
: "",
|
|
405
516
|
].join("");
|
|
517
|
+
|
|
518
|
+
if (field.metadataPersistence === "external") {
|
|
519
|
+
return `${baseAttrs}/>`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return `${baseAttrs}>${escapeXml(serialized.text)}</bw:field>`;
|
|
406
523
|
})
|
|
407
524
|
.join("\n"),
|
|
408
525
|
2,
|
|
@@ -571,11 +688,17 @@ function parseWorkflowOverlay(xml: string): WorkflowOverlay | undefined {
|
|
|
571
688
|
.map((match) => parseWorkflowWorkItem(match[1] ?? "", match[2] ?? ""))
|
|
572
689
|
.filter((item): item is WorkflowWorkItem => item !== null);
|
|
573
690
|
|
|
691
|
+
const overlayMetadataPersistence = parseClosedEnum(
|
|
692
|
+
overlayAttributes.metadataPersistence,
|
|
693
|
+
["internal", "external"] as const,
|
|
694
|
+
);
|
|
695
|
+
|
|
574
696
|
return {
|
|
575
697
|
overlayVersion: (overlayAttributes.overlayVersion as WorkflowOverlay["overlayVersion"]) ?? "workflow-overlay/1",
|
|
576
698
|
activeWorkItemId: overlayAttributes.activeWorkItemId ?? null,
|
|
577
699
|
scopes,
|
|
578
700
|
...(workItems.length > 0 ? { workItems } : {}),
|
|
701
|
+
...(overlayMetadataPersistence !== undefined ? { metadataPersistence: overlayMetadataPersistence } : {}),
|
|
579
702
|
};
|
|
580
703
|
}
|
|
581
704
|
|
|
@@ -588,6 +711,11 @@ function parseWorkflowScope(attributesSource: string, body: string): WorkflowSco
|
|
|
588
711
|
const anchor = anchorMatch
|
|
589
712
|
? parseWorkflowOverlayAnchor(anchorMatch[1] ?? "", anchorMatch[2] ?? "")
|
|
590
713
|
: createDefaultRangeAnchor(0, 0);
|
|
714
|
+
const scopeMetadataPersistence = parseClosedEnum(
|
|
715
|
+
attributes.metadataPersistence,
|
|
716
|
+
["internal", "external", "inherit"] as const,
|
|
717
|
+
);
|
|
718
|
+
|
|
591
719
|
return {
|
|
592
720
|
scopeId: attributes.id,
|
|
593
721
|
version: attributes.version !== undefined ? Number(attributes.version) : undefined,
|
|
@@ -598,6 +726,7 @@ function parseWorkflowScope(attributesSource: string, body: string): WorkflowSco
|
|
|
598
726
|
label: attributes.label,
|
|
599
727
|
domain: attributes.domain as WorkflowScope["domain"],
|
|
600
728
|
metadata: parseWorkflowScopeMetadata(body),
|
|
729
|
+
...(scopeMetadataPersistence !== undefined ? { metadataPersistence: scopeMetadataPersistence } : {}),
|
|
601
730
|
};
|
|
602
731
|
}
|
|
603
732
|
|
|
@@ -606,7 +735,8 @@ function parseWorkflowScopeMetadata(body: string): WorkflowScope["metadata"] | u
|
|
|
606
735
|
if (!metadataMatch) {
|
|
607
736
|
return undefined;
|
|
608
737
|
}
|
|
609
|
-
|
|
738
|
+
// Match both self-closing (<bw:field .../>) and body-bearing (<bw:field ...>body</bw:field>)
|
|
739
|
+
const fields = [...(metadataMatch[1] ?? "").matchAll(/<bw:field\b([^>]*)(?:\/>|>([\s\S]*?)<\/bw:field>)/gu)]
|
|
610
740
|
.map((match) => parseWorkflowScopeMetadataField(match[1] ?? "", match[2] ?? ""))
|
|
611
741
|
.filter((field): field is NonNullable<WorkflowScope["metadata"]>[number] => field !== null);
|
|
612
742
|
return fields.length > 0 ? fields : undefined;
|
|
@@ -621,10 +751,19 @@ function parseWorkflowScopeMetadataField(
|
|
|
621
751
|
return null;
|
|
622
752
|
}
|
|
623
753
|
const valueType = attributes.type as NonNullable<WorkflowScope["metadata"]>[number]["valueType"] | undefined;
|
|
754
|
+
const fieldMetadataPersistence = parseClosedEnum(
|
|
755
|
+
attributes.metadataPersistence,
|
|
756
|
+
["internal", "external", "inherit"] as const,
|
|
757
|
+
);
|
|
758
|
+
const fieldStorageRef = attributes.storageRef;
|
|
759
|
+
const fieldMetadataVersion = parseNonNegativeInt(attributes.metadataVersion);
|
|
624
760
|
return {
|
|
625
761
|
key: attributes.key,
|
|
626
762
|
...(valueType ? { valueType } : {}),
|
|
627
763
|
value: parseWorkflowScopeMetadataValue(valueType, body),
|
|
764
|
+
...(fieldMetadataPersistence !== undefined ? { metadataPersistence: fieldMetadataPersistence } : {}),
|
|
765
|
+
...(fieldStorageRef !== undefined ? { storageRef: fieldStorageRef } : {}),
|
|
766
|
+
...(fieldMetadataVersion !== undefined ? { metadataVersion: fieldMetadataVersion } : {}),
|
|
628
767
|
};
|
|
629
768
|
}
|
|
630
769
|
|