@milaboratories/pl-middle-layer 1.69.2 → 1.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +0 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/middle_layer.cjs +14 -45
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +8 -15
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +14 -45
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/template_list.cjs.map +1 -1
- package/dist/middle_layer/template_list.d.ts +1 -6
- package/dist/middle_layer/template_list.d.ts.map +1 -1
- package/dist/middle_layer/template_list.js.map +1 -1
- package/dist/model/index.cjs +0 -2
- package/dist/model/index.d.ts +1 -2
- package/dist/model/index.js +1 -2
- package/dist/model/template_export.cjs +5 -1
- package/dist/model/template_export.cjs.map +1 -1
- package/dist/model/template_export.d.ts +3 -0
- package/dist/model/template_export.d.ts.map +1 -1
- package/dist/model/template_export.js +5 -1
- package/dist/model/template_export.js.map +1 -1
- package/dist/model/template_serializer.cjs +2 -0
- package/dist/model/template_serializer.cjs.map +1 -1
- package/dist/model/template_serializer.d.ts.map +1 -1
- package/dist/model/template_serializer.js +2 -0
- package/dist/model/template_serializer.js.map +1 -1
- package/package.json +13 -13
- package/src/middle_layer/middle_layer.ts +21 -57
- package/src/middle_layer/template_list.ts +2 -5
- package/src/middle_layer/templates.test.ts +11 -20
- package/src/model/index.ts +0 -1
- package/src/model/template_export.test.ts +41 -5
- package/src/model/template_export.ts +11 -3
- package/src/model/template_serializer.test.ts +26 -3
- package/src/model/template_serializer.ts +2 -0
- package/dist/model/template_share.cjs +0 -42
- package/dist/model/template_share.cjs.map +0 -1
- package/dist/model/template_share.d.ts +0 -27
- package/dist/model/template_share.d.ts.map +0 -1
- package/dist/model/template_share.js +0 -42
- package/dist/model/template_share.js.map +0 -1
- package/src/model/template_share.test.ts +0 -78
- package/src/model/template_share.ts +0 -52
|
@@ -75,8 +75,6 @@ import {
|
|
|
75
75
|
type ShareProjectsOptions,
|
|
76
76
|
type ShareTemplateOptions,
|
|
77
77
|
} from "../model/sharing_model";
|
|
78
|
-
import type { TemplateShareProblem } from "../model/template_share";
|
|
79
|
-
import { unshareableTemplateEntries } from "../model/template_share";
|
|
80
78
|
import {
|
|
81
79
|
buildShareEnvelope,
|
|
82
80
|
buildTemplateShareEnvelope,
|
|
@@ -1002,9 +1000,10 @@ export class MiddleLayer {
|
|
|
1002
1000
|
* The cost of that is the donor's receipt: nobody can write an acceptance onto a read-only
|
|
1003
1001
|
* envelope, so a template share never reports who accepted it.
|
|
1004
1002
|
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1003
|
+
* Nothing about the document is checked: a stored template is shareable by virtue of existing.
|
|
1004
|
+
* An entry the recipient cannot resolve — a block installed from a folder on the sender's
|
|
1005
|
+
* machine, say — is theirs to see when they preview or apply it, where every unresolvable entry
|
|
1006
|
+
* is named anyway.
|
|
1008
1007
|
*
|
|
1009
1008
|
* @param id template to share
|
|
1010
1009
|
* @param options recipients XOR everyone, plus the title recipients see
|
|
@@ -1013,8 +1012,7 @@ export class MiddleLayer {
|
|
|
1013
1012
|
id: TemplateId,
|
|
1014
1013
|
options: ShareTemplateOptions,
|
|
1015
1014
|
): Promise<ShareTemplateOutcome> {
|
|
1016
|
-
const
|
|
1017
|
-
if (!loaded.ok) return { ok: false, problems: loaded.problems };
|
|
1015
|
+
const template = await this.loadTemplateForShare(id);
|
|
1018
1016
|
|
|
1019
1017
|
const everyone = "everyone" in options;
|
|
1020
1018
|
const sender = this.currentUserLogin ?? "";
|
|
@@ -1026,7 +1024,7 @@ export class MiddleLayer {
|
|
|
1026
1024
|
const { envelope, data } = buildTemplateShareEnvelope(
|
|
1027
1025
|
tx,
|
|
1028
1026
|
this.sharingOutboxResourceId,
|
|
1029
|
-
|
|
1027
|
+
template,
|
|
1030
1028
|
{ sender, title: options.title, expiresAt },
|
|
1031
1029
|
);
|
|
1032
1030
|
shareId = data.shareId;
|
|
@@ -1037,29 +1035,16 @@ export class MiddleLayer {
|
|
|
1037
1035
|
});
|
|
1038
1036
|
|
|
1039
1037
|
await this.sharingOutboxTree.refreshState();
|
|
1040
|
-
return {
|
|
1038
|
+
return { shareId: shareId! };
|
|
1041
1039
|
}
|
|
1042
1040
|
|
|
1043
|
-
/**
|
|
1044
|
-
*
|
|
1045
|
-
|
|
1046
|
-
* template itself instead of only when the user tries to share it.
|
|
1047
|
-
*/
|
|
1048
|
-
public async checkTemplateShareable(id: TemplateId): Promise<readonly TemplateShareProblem[]> {
|
|
1049
|
-
const stored = await this.getTemplateData(id);
|
|
1050
|
-
return unshareableTemplateEntries(stored.document);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/** The document and the label of a template that may be shared, or every entry that stops it.
|
|
1054
|
-
* The label is what the recipient's own list will show, so it travels with the document. */
|
|
1055
|
-
private async loadShareableTemplate(
|
|
1041
|
+
/** The document and the label of a template about to be shared. The label is what the
|
|
1042
|
+
* recipient's own list will show, so it travels with the document. */
|
|
1043
|
+
private async loadTemplateForShare(
|
|
1056
1044
|
id: TemplateId,
|
|
1057
|
-
): Promise<
|
|
1058
|
-
| { ok: true; template: { document: ProjectTemplateV1; label: string } }
|
|
1059
|
-
| { ok: false; problems: readonly TemplateShareProblem[] }
|
|
1060
|
-
> {
|
|
1045
|
+
): Promise<{ document: ProjectTemplateV1; label: string }> {
|
|
1061
1046
|
const rid = await this.resolveTemplateId(id);
|
|
1062
|
-
|
|
1047
|
+
return await this.pl.withReadTx("MLReadTemplateForShare", async (tx) => {
|
|
1063
1048
|
const rd = await tx.getResourceData(rid, false);
|
|
1064
1049
|
if (rd.data === undefined) throw new Error(`Template ${id} carries no document.`);
|
|
1065
1050
|
return {
|
|
@@ -1067,10 +1052,6 @@ export class MiddleLayer {
|
|
|
1067
1052
|
label: await tx.getKValueJson<string>(rid, TemplateLabelKey),
|
|
1068
1053
|
};
|
|
1069
1054
|
});
|
|
1070
|
-
|
|
1071
|
-
const problems = unshareableTemplateEntries(template.document);
|
|
1072
|
-
if (problems.length > 0) return { ok: false, problems };
|
|
1073
|
-
return { ok: true, template };
|
|
1074
1055
|
}
|
|
1075
1056
|
|
|
1076
1057
|
/**
|
|
@@ -1092,8 +1073,7 @@ export class MiddleLayer {
|
|
|
1092
1073
|
* `opts.templateId` is required for, and only used by, a share that carries a template: a stored
|
|
1093
1074
|
* template is immutable, so an improved one is a different template and the share cannot re-read
|
|
1094
1075
|
* the one it started from — the caller names the new target. Every other option means the same
|
|
1095
|
-
* thing for both kinds of share.
|
|
1096
|
-
* {@link checkTemplateShareable}) or this throws.
|
|
1076
|
+
* thing for both kinds of share.
|
|
1097
1077
|
*/
|
|
1098
1078
|
public async changeShare(
|
|
1099
1079
|
shareId: ShareId,
|
|
@@ -1105,14 +1085,9 @@ export class MiddleLayer {
|
|
|
1105
1085
|
templateId?: TemplateId;
|
|
1106
1086
|
} = {},
|
|
1107
1087
|
): Promise<void> {
|
|
1108
|
-
// Read outside the write tx: it is two round-trips of its own
|
|
1109
|
-
// must be raised before anything is torn down.
|
|
1088
|
+
// Read outside the write tx: it is two round-trips of its own.
|
|
1110
1089
|
const target =
|
|
1111
|
-
opts.templateId === undefined ? undefined : await this.
|
|
1112
|
-
if (target !== undefined && !target.ok)
|
|
1113
|
-
throw new Error(
|
|
1114
|
-
`changeShare: template ${opts.templateId} cannot be shared: ${describeShareProblems(target.problems)}`,
|
|
1115
|
-
);
|
|
1090
|
+
opts.templateId === undefined ? undefined : await this.loadTemplateForShare(opts.templateId);
|
|
1116
1091
|
|
|
1117
1092
|
await this.pl.withWriteTx("MLChangeShare", async (tx) => {
|
|
1118
1093
|
const old = await this.resolveOutboxEnvelope(tx, shareId);
|
|
@@ -1137,17 +1112,12 @@ export class MiddleLayer {
|
|
|
1137
1112
|
|
|
1138
1113
|
// Same shareId, same outbox field name — detach the old field before rebuilding, or they collide.
|
|
1139
1114
|
tx.removeField(field(this.sharingOutboxResourceId, old.fieldName));
|
|
1140
|
-
const { envelope } = buildTemplateShareEnvelope(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
title: opts.title === undefined ? old.data.title : opts.title.trim(),
|
|
1147
|
-
expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,
|
|
1148
|
-
shareId, // SAME shareId — the essence of change
|
|
1149
|
-
},
|
|
1150
|
-
);
|
|
1115
|
+
const { envelope } = buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, target, {
|
|
1116
|
+
sender: self,
|
|
1117
|
+
title: opts.title === undefined ? old.data.title : opts.title.trim(),
|
|
1118
|
+
expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,
|
|
1119
|
+
shareId, // SAME shareId — the essence of change
|
|
1120
|
+
});
|
|
1151
1121
|
|
|
1152
1122
|
// Nothing to transfer: a read-only grant cannot write an acceptance, so a template share
|
|
1153
1123
|
// never accumulated one.
|
|
@@ -1828,9 +1798,3 @@ export class MiddleLayer {
|
|
|
1828
1798
|
//
|
|
1829
1799
|
// Internals
|
|
1830
1800
|
//
|
|
1831
|
-
|
|
1832
|
-
/** Refusal reasons as one line, each naming the entry it belongs to, so a throw that escapes to a
|
|
1833
|
-
* log still says which block stands in the way. */
|
|
1834
|
-
function describeShareProblems(problems: readonly TemplateShareProblem[]): string {
|
|
1835
|
-
return problems.map((p) => `${p.entryId}: ${p.error}`).join("; ");
|
|
1836
|
-
}
|
|
@@ -20,7 +20,6 @@ import type { MiddleLayerEnvironment } from "./middle_layer";
|
|
|
20
20
|
import { notEmpty } from "@milaboratories/ts-helpers";
|
|
21
21
|
import type { Branded, ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
22
22
|
import type { TemplateExportProblem } from "../model/template_export";
|
|
23
|
-
import type { TemplateShareProblem } from "../model/template_share";
|
|
24
23
|
import type { ShareId } from "../model/sharing_model";
|
|
25
24
|
import type { AppliedEntry, TemplateApplyProblem } from "../model/template_apply";
|
|
26
25
|
import type { ProjectId } from "../model/project_model";
|
|
@@ -80,10 +79,8 @@ export type SaveProjectAsTemplateOutcome =
|
|
|
80
79
|
| { readonly ok: true; readonly templateId: TemplateId }
|
|
81
80
|
| { readonly ok: false; readonly problems: readonly TemplateExportProblem[] };
|
|
82
81
|
|
|
83
|
-
/** What sharing a stored template yields: the share's logical id
|
|
84
|
-
export type ShareTemplateOutcome =
|
|
85
|
-
| { readonly ok: true; readonly shareId: ShareId }
|
|
86
|
-
| { readonly ok: false; readonly problems: readonly TemplateShareProblem[] };
|
|
82
|
+
/** What sharing a stored template yields: the share's logical id. */
|
|
83
|
+
export type ShareTemplateOutcome = { readonly shareId: ShareId };
|
|
87
84
|
|
|
88
85
|
/**
|
|
89
86
|
* What applying a stored template yields.
|
|
@@ -91,26 +91,19 @@ test("an entry nothing can resolve creates no project, and every entry is report
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
test("a template holding a block from a folder on this machine is
|
|
94
|
+
test("a template holding a block from a folder on this machine is shared like any other", async () => {
|
|
95
95
|
await withMl(async (ml) => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
document: documentOf(entry("a"), entry("b", LOCAL_FOLDER)),
|
|
99
|
-
});
|
|
96
|
+
const document = documentOf(entry("a"), entry("b", LOCAL_FOLDER));
|
|
97
|
+
const stored = await storeTemplate(ml, "Built here", { schemaVersion: 1, document });
|
|
100
98
|
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
recipients: ["colleague"],
|
|
107
|
-
title: "Built here",
|
|
108
|
-
});
|
|
99
|
+
// A stored template is shareable by existing. Whether the recipient can resolve every entry
|
|
100
|
+
// is their question, answered where they preview or apply it — not a gate on sending.
|
|
101
|
+
// With everyone, like the other share tests here: a named recipient must already exist on
|
|
102
|
+
// the backend, and the test server has no second user.
|
|
103
|
+
const shared = await ml.shareTemplate(stored.id, { everyone: true, title: "Built here" });
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
// Refused before anything was written: no envelope, so nothing to revoke.
|
|
113
|
-
expect((await ml.outgoingShares.getValue()) ?? []).toStrictEqual([]);
|
|
105
|
+
const outgoing = await ml.outgoingShares.awaitStableValue();
|
|
106
|
+
expect((outgoing ?? []).map((s) => s.shareId)).toStrictEqual([shared.shareId]);
|
|
114
107
|
});
|
|
115
108
|
});
|
|
116
109
|
|
|
@@ -142,7 +135,6 @@ test("an accepted template share lands on the acceptor's shelf and builds nothin
|
|
|
142
135
|
});
|
|
143
136
|
|
|
144
137
|
const shared = await ml.shareTemplate(stored.id, { everyone: true, title: "A pipeline" });
|
|
145
|
-
if (!shared.ok) throw new Error(`share refused: ${JSON.stringify(shared.problems)}`);
|
|
146
138
|
|
|
147
139
|
const outcome = await ml.acceptShare([shared.shareId]);
|
|
148
140
|
|
|
@@ -175,7 +167,6 @@ test("a changed template share keeps its id, and whoever already responded is no
|
|
|
175
167
|
});
|
|
176
168
|
|
|
177
169
|
const shared = await ml.shareTemplate(first.id, { everyone: true, title: "First" });
|
|
178
|
-
if (!shared.ok) throw new Error(`share refused: ${JSON.stringify(shared.problems)}`);
|
|
179
170
|
|
|
180
171
|
// Someone responds to the share, which is what the replace below must not undo.
|
|
181
172
|
const accept = await ml.acceptShare([shared.shareId]);
|
|
@@ -186,7 +177,7 @@ test("a changed template share keeps its id, and whoever already responded is no
|
|
|
186
177
|
await ml.changeShare(shared.shareId, { templateId: second.id, title: "Second" });
|
|
187
178
|
|
|
188
179
|
const outgoing = (await ml.outgoingShares.getValue()) ?? [];
|
|
189
|
-
expect(outgoing.map((s) => s.shareId)).toStrictEqual([shared.shareId]);
|
|
180
|
+
expect((outgoing ?? []).map((s) => s.shareId)).toStrictEqual([shared.shareId]);
|
|
190
181
|
expect(outgoing[0]).toMatchObject({
|
|
191
182
|
payloadKind: "template",
|
|
192
183
|
title: "Second",
|
package/src/model/index.ts
CHANGED
|
@@ -48,4 +48,3 @@ export type { TemplateExportProblem } from "./template_export";
|
|
|
48
48
|
|
|
49
49
|
// The template share path. Whether a template may be shared at all is a question a UI asks about
|
|
50
50
|
// a template it is merely displaying, so the check and its problem type are public.
|
|
51
|
-
export { unshareableTemplateEntries, type TemplateShareProblem } from "./template_share";
|
|
@@ -24,6 +24,35 @@ function providerFrom(params: Record<string, TemplateParamsResult>) {
|
|
|
24
24
|
|
|
25
25
|
const ok = (value: unknown): TemplateParamsResult => ({ value });
|
|
26
26
|
|
|
27
|
+
describe("labels", () => {
|
|
28
|
+
test("an entry and a problem both carry the block's label, not its id", () => {
|
|
29
|
+
// `simpleStructure` labels every block after its id, which cannot tell a label read from
|
|
30
|
+
// the structure apart from an id copied into the field. Distinct labels here can.
|
|
31
|
+
const structure: ProjectStructure = {
|
|
32
|
+
groups: [
|
|
33
|
+
{
|
|
34
|
+
id: "g1",
|
|
35
|
+
label: "G1",
|
|
36
|
+
blocks: [
|
|
37
|
+
{ id: "b1", label: "MiXCR Clonotyping", renderingMode: "Heavy" },
|
|
38
|
+
{ id: "b2", label: "Clonotype Browser", renderingMode: "Heavy" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
const walk = walkProjectForTemplateExport(structure, providerFrom({ b1: ok({}) }));
|
|
44
|
+
|
|
45
|
+
expect(walk.entries).toEqual([{ blockId: "b1", blockLabel: "MiXCR Clonotyping", params: {} }]);
|
|
46
|
+
expect(walk.problems).toEqual([
|
|
47
|
+
{
|
|
48
|
+
blockId: "b2",
|
|
49
|
+
blockLabel: "Clonotype Browser",
|
|
50
|
+
error: "Block state is unavailable, so its template params could not be derived",
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
27
56
|
describe("order", () => {
|
|
28
57
|
test("entries come out in structure order — no sort, none needed", () => {
|
|
29
58
|
// The structure IS the topological order: a block can only legally reference
|
|
@@ -101,7 +130,7 @@ describe("collecting each block's descriptor output", () => {
|
|
|
101
130
|
providerFrom({ mixcr: ok(params) }),
|
|
102
131
|
);
|
|
103
132
|
|
|
104
|
-
expect(walk.entries).toEqual([{ blockId: "mixcr", params }]);
|
|
133
|
+
expect(walk.entries).toEqual([{ blockId: "mixcr", blockLabel: "mixcr", params }]);
|
|
105
134
|
});
|
|
106
135
|
|
|
107
136
|
test("a block with nothing to project yields empty params", () => {
|
|
@@ -112,7 +141,9 @@ describe("collecting each block's descriptor output", () => {
|
|
|
112
141
|
providerFrom({ "pool-explorer": ok({}) }),
|
|
113
142
|
);
|
|
114
143
|
|
|
115
|
-
expect(walk.entries).toEqual([
|
|
144
|
+
expect(walk.entries).toEqual([
|
|
145
|
+
{ blockId: "pool-explorer", blockLabel: "pool-explorer", params: {} },
|
|
146
|
+
]);
|
|
116
147
|
expect(walk.problems).toEqual([]);
|
|
117
148
|
});
|
|
118
149
|
});
|
|
@@ -145,7 +176,7 @@ describe("what the walk does with params", () => {
|
|
|
145
176
|
providerFrom({ block1: ok({}) }),
|
|
146
177
|
);
|
|
147
178
|
|
|
148
|
-
expect(walk.entries).toEqual([{ blockId: "block1", params: {} }]);
|
|
179
|
+
expect(walk.entries).toEqual([{ blockId: "block1", blockLabel: "block1", params: {} }]);
|
|
149
180
|
});
|
|
150
181
|
|
|
151
182
|
test("a wrapper's contents are never inspected, whatever they are", () => {
|
|
@@ -210,7 +241,7 @@ describe("problems", () => {
|
|
|
210
241
|
// Every offending block is reported at once rather than aborting on the
|
|
211
242
|
// first, so the user fixes them in one pass.
|
|
212
243
|
expect(walk.problems).toEqual([
|
|
213
|
-
{ blockId: "b", error: "templateParams() threw: not exportable yet" },
|
|
244
|
+
{ blockId: "b", blockLabel: "b", error: "templateParams() threw: not exportable yet" },
|
|
214
245
|
]);
|
|
215
246
|
expect(walk.entries.map((e) => e.blockId)).toEqual(["a", "c"]);
|
|
216
247
|
});
|
|
@@ -228,6 +259,7 @@ describe("problems", () => {
|
|
|
228
259
|
expect(walk.problems).toEqual([
|
|
229
260
|
{
|
|
230
261
|
blockId: "ghost",
|
|
262
|
+
blockLabel: "ghost",
|
|
231
263
|
error: "Block state is unavailable, so its template params could not be derived",
|
|
232
264
|
},
|
|
233
265
|
]);
|
|
@@ -249,7 +281,11 @@ describe("problems", () => {
|
|
|
249
281
|
|
|
250
282
|
expect(walk.entries).toEqual([]);
|
|
251
283
|
expect(walk.problems).toEqual([
|
|
252
|
-
{
|
|
284
|
+
{
|
|
285
|
+
blockId: "odd",
|
|
286
|
+
blockLabel: "odd",
|
|
287
|
+
error: `templateParams() must return an object, got ${expected}`,
|
|
288
|
+
},
|
|
253
289
|
]);
|
|
254
290
|
});
|
|
255
291
|
});
|
|
@@ -20,6 +20,9 @@ export type TemplateExportEntry = {
|
|
|
20
20
|
* already stored in params need no translation.
|
|
21
21
|
*/
|
|
22
22
|
readonly blockId: string;
|
|
23
|
+
/** The block's label from the project structure, carried so a problem found downstream can
|
|
24
|
+
* name the block to a person. Not written to the template. */
|
|
25
|
+
readonly blockLabel: string;
|
|
23
26
|
/**
|
|
24
27
|
* The block's params exactly as it projected them.
|
|
25
28
|
*
|
|
@@ -33,6 +36,9 @@ export type TemplateExportEntry = {
|
|
|
33
36
|
/** Why one block could not be exported. */
|
|
34
37
|
export type TemplateExportProblem = {
|
|
35
38
|
readonly blockId: string;
|
|
39
|
+
/** The block's label as the project structure holds it. An id means nothing to the person who
|
|
40
|
+
* pressed Export; this is what a UI shows. */
|
|
41
|
+
readonly blockLabel: string;
|
|
36
42
|
readonly error: string;
|
|
37
43
|
};
|
|
38
44
|
|
|
@@ -91,19 +97,20 @@ export function walkProjectForTemplateExport(
|
|
|
91
97
|
const entries: TemplateExportEntry[] = [];
|
|
92
98
|
const problems: TemplateExportProblem[] = [];
|
|
93
99
|
|
|
94
|
-
for (const { id } of allBlocks(structure)) {
|
|
100
|
+
for (const { id, label } of allBlocks(structure)) {
|
|
95
101
|
const derived = paramsProvider(id);
|
|
96
102
|
|
|
97
103
|
if (derived === undefined) {
|
|
98
104
|
problems.push({
|
|
99
105
|
blockId: id,
|
|
106
|
+
blockLabel: label,
|
|
100
107
|
error: "Block state is unavailable, so its template params could not be derived",
|
|
101
108
|
});
|
|
102
109
|
continue;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
if (derived.error !== undefined) {
|
|
106
|
-
problems.push({ blockId: id, error: derived.error });
|
|
113
|
+
problems.push({ blockId: id, blockLabel: label, error: derived.error });
|
|
107
114
|
continue;
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -117,12 +124,13 @@ export function walkProjectForTemplateExport(
|
|
|
117
124
|
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
118
125
|
problems.push({
|
|
119
126
|
blockId: id,
|
|
127
|
+
blockLabel: label,
|
|
120
128
|
error: `templateParams() must return an object, got ${typeName(params)}`,
|
|
121
129
|
});
|
|
122
130
|
continue;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
|
-
entries.push({ blockId: id, params: params as Record<string, unknown> });
|
|
133
|
+
entries.push({ blockId: id, blockLabel: label, params: params as Record<string, unknown> });
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
return { entries, problems };
|
|
@@ -198,6 +198,27 @@ describe("problems", () => {
|
|
|
198
198
|
expect(result.problems[0].error).toContain("declares no kind");
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
test("a problem raised here carries the block's label from the walk", () => {
|
|
202
|
+
// Labels distinct from ids, or the assertion could not tell a label carried through from
|
|
203
|
+
// an id copied into the field.
|
|
204
|
+
const structure: ProjectStructure = {
|
|
205
|
+
groups: [
|
|
206
|
+
{
|
|
207
|
+
id: "g1",
|
|
208
|
+
label: "G1",
|
|
209
|
+
blocks: [{ id: "legacy", label: "Old Aligner", renderingMode: "Heavy" }],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
const result = exportOf(structure, { legacy: ok({}) }, () => undefined);
|
|
214
|
+
|
|
215
|
+
expect(result.ok).toBe(false);
|
|
216
|
+
if (result.ok) return;
|
|
217
|
+
expect(result.problems).toEqual([
|
|
218
|
+
{ blockId: "legacy", blockLabel: "Old Aligner", error: expect.stringContaining("no kind") },
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
|
|
201
222
|
test("a malformed stored kind reference is a problem, not a throw", () => {
|
|
202
223
|
const result = exportOf(
|
|
203
224
|
simpleStructure("a"),
|
|
@@ -385,14 +406,16 @@ describe("assembleProjectTemplateV1", () => {
|
|
|
385
406
|
test("carries the walk's problems through unchanged", () => {
|
|
386
407
|
const { document, problems } = assembleProjectTemplateV1(
|
|
387
408
|
{
|
|
388
|
-
entries: [{ blockId: "a", params: {} }],
|
|
389
|
-
problems: [{ blockId: "ghost", error: "state unavailable" }],
|
|
409
|
+
entries: [{ blockId: "a", blockLabel: "a", params: {} }],
|
|
410
|
+
problems: [{ blockId: "ghost", blockLabel: "ghost", error: "state unavailable" }],
|
|
390
411
|
},
|
|
391
412
|
kindPerBlock,
|
|
392
413
|
() => registrySpec,
|
|
393
414
|
);
|
|
394
415
|
|
|
395
|
-
expect(problems).toEqual([
|
|
416
|
+
expect(problems).toEqual([
|
|
417
|
+
{ blockId: "ghost", blockLabel: "ghost", error: "state unavailable" },
|
|
418
|
+
]);
|
|
396
419
|
expect(document.blocks.map((b) => b.id)).toEqual(["a"]);
|
|
397
420
|
});
|
|
398
421
|
});
|
|
@@ -126,6 +126,7 @@ export function assembleProjectTemplateV1(
|
|
|
126
126
|
if (kind === undefined) {
|
|
127
127
|
problems.push({
|
|
128
128
|
blockId: entry.blockId,
|
|
129
|
+
blockLabel: entry.blockLabel,
|
|
129
130
|
error:
|
|
130
131
|
"Block declares no kind, so it cannot be written to a template: an entry's kind " +
|
|
131
132
|
"carries the params contract the entry is typed against",
|
|
@@ -143,6 +144,7 @@ export function assembleProjectTemplateV1(
|
|
|
143
144
|
} catch (e) {
|
|
144
145
|
problems.push({
|
|
145
146
|
blockId: entry.blockId,
|
|
147
|
+
blockLabel: entry.blockLabel,
|
|
146
148
|
error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,
|
|
147
149
|
});
|
|
148
150
|
continue;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
2
|
-
//#region src/model/template_share.ts
|
|
3
|
-
/**
|
|
4
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
5
|
-
* template that can be shared.
|
|
6
|
-
*
|
|
7
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
8
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
9
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
10
|
-
* as normal — only sharing it is refused.
|
|
11
|
-
*
|
|
12
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
13
|
-
* it anywhere, here included.
|
|
14
|
-
*
|
|
15
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
16
|
-
* of sending its user round the loop once per entry.
|
|
17
|
-
*/
|
|
18
|
-
function unshareableTemplateEntries(document) {
|
|
19
|
-
const problems = [];
|
|
20
|
-
for (const entry of document.blocks) {
|
|
21
|
-
if (entry.location === void 0) continue;
|
|
22
|
-
let scheme;
|
|
23
|
-
try {
|
|
24
|
-
scheme = (0, _milaboratories_pl_model_common.parseBlockPackLocation)(entry.location).scheme;
|
|
25
|
-
} catch (e) {
|
|
26
|
-
problems.push({
|
|
27
|
-
entryId: entry.id,
|
|
28
|
-
error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`
|
|
29
|
-
});
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (scheme === "file") problems.push({
|
|
33
|
-
entryId: entry.id,
|
|
34
|
-
error: `Block is installed from ${entry.location}, a folder on this machine — it resolves to nothing on the recipient's, so this template cannot be shared`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return problems;
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
exports.unshareableTemplateEntries = unshareableTemplateEntries;
|
|
41
|
-
|
|
42
|
-
//# sourceMappingURL=template_share.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template_share.cjs","names":[],"sources":["../../src/model/template_share.ts"],"sourcesContent":["import type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { parseBlockPackLocation } from \"@milaboratories/pl-model-common\";\n\n/** One template entry standing in the way of sharing the template, and why. */\nexport type TemplateShareProblem = {\n /** The template-local id of the entry the problem belongs to; on an exported template it is\n * the block's project-local uuid. */\n readonly entryId: string;\n readonly error: string;\n};\n\n/**\n * Every entry of a template that cannot travel to another machine, or an empty list for a\n * template that can be shared.\n *\n * An entry's `location` names a place rather than a name, and a `file:` place is a folder on\n * the author's own disk: a recipient resolving it finds nothing, or worse finds something\n * else. Such a template stays perfectly usable where it was made, so it is stored and applied\n * as normal — only sharing it is refused.\n *\n * A location whose scheme cannot be read is refused for the same reason: nothing can resolve\n * it anywhere, here included.\n *\n * Every offending entry is reported, not only the first, so a UI can name each block instead\n * of sending its user round the loop once per entry.\n */\nexport function unshareableTemplateEntries(\n document: ProjectTemplateV1,\n): readonly TemplateShareProblem[] {\n const problems: TemplateShareProblem[] = [];\n for (const entry of document.blocks) {\n if (entry.location === undefined) continue;\n let scheme: string;\n try {\n scheme = parseBlockPackLocation(entry.location).scheme;\n } catch (e) {\n problems.push({\n entryId: entry.id,\n error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n if (scheme === \"file\")\n problems.push({\n entryId: entry.id,\n error:\n `Block is installed from ${entry.location}, a folder on this machine — it resolves ` +\n \"to nothing on the recipient's, so this template cannot be shared\",\n });\n }\n return problems;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0BA,SAAgB,2BACd,UACiC;CACjC,MAAM,WAAmC,CAAC;CAC1C,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,IAAI,MAAM,aAAa,KAAA,GAAW;EAClC,IAAI;EACJ,IAAI;GACF,UAAA,GAAA,gCAAA,uBAAA,CAAgC,MAAM,QAAQ,CAAC,CAAC;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OAAO,2DAA2D,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GAC7G,CAAC;GACD;EACF;EACA,IAAI,WAAW,QACb,SAAS,KAAK;GACZ,SAAS,MAAM;GACf,OACE,2BAA2B,MAAM,SAAS;EAE9C,CAAC;CACL;CACA,OAAO;AACT"}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
2
|
-
//#region src/model/template_share.d.ts
|
|
3
|
-
/** One template entry standing in the way of sharing the template, and why. */
|
|
4
|
-
export type TemplateShareProblem = {
|
|
5
|
-
/** The template-local id of the entry the problem belongs to; on an exported template it is
|
|
6
|
-
* the block's project-local uuid. */
|
|
7
|
-
readonly entryId: string;
|
|
8
|
-
readonly error: string;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
12
|
-
* template that can be shared.
|
|
13
|
-
*
|
|
14
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
15
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
16
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
17
|
-
* as normal — only sharing it is refused.
|
|
18
|
-
*
|
|
19
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
20
|
-
* it anywhere, here included.
|
|
21
|
-
*
|
|
22
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
23
|
-
* of sending its user round the loop once per entry.
|
|
24
|
-
*/
|
|
25
|
-
export declare function unshareableTemplateEntries(document: ProjectTemplateV1): readonly TemplateShareProblem[];
|
|
26
|
-
//#endregion
|
|
27
|
-
//# sourceMappingURL=template_share.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template_share.d.ts","names":[],"sources":["../../src/model/template_share.ts"],"mappings":";;;YAIY;;;WAGD;WACA;;;;;;;;;;;;;;;;;wBAkBK,2BACd,UAAU,6BACA"}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { parseBlockPackLocation } from "@milaboratories/pl-model-common";
|
|
2
|
-
//#region src/model/template_share.ts
|
|
3
|
-
/**
|
|
4
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
5
|
-
* template that can be shared.
|
|
6
|
-
*
|
|
7
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
8
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
9
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
10
|
-
* as normal — only sharing it is refused.
|
|
11
|
-
*
|
|
12
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
13
|
-
* it anywhere, here included.
|
|
14
|
-
*
|
|
15
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
16
|
-
* of sending its user round the loop once per entry.
|
|
17
|
-
*/
|
|
18
|
-
function unshareableTemplateEntries(document) {
|
|
19
|
-
const problems = [];
|
|
20
|
-
for (const entry of document.blocks) {
|
|
21
|
-
if (entry.location === void 0) continue;
|
|
22
|
-
let scheme;
|
|
23
|
-
try {
|
|
24
|
-
scheme = parseBlockPackLocation(entry.location).scheme;
|
|
25
|
-
} catch (e) {
|
|
26
|
-
problems.push({
|
|
27
|
-
entryId: entry.id,
|
|
28
|
-
error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`
|
|
29
|
-
});
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (scheme === "file") problems.push({
|
|
33
|
-
entryId: entry.id,
|
|
34
|
-
error: `Block is installed from ${entry.location}, a folder on this machine — it resolves to nothing on the recipient's, so this template cannot be shared`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return problems;
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
export { unshareableTemplateEntries };
|
|
41
|
-
|
|
42
|
-
//# sourceMappingURL=template_share.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template_share.js","names":[],"sources":["../../src/model/template_share.ts"],"sourcesContent":["import type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { parseBlockPackLocation } from \"@milaboratories/pl-model-common\";\n\n/** One template entry standing in the way of sharing the template, and why. */\nexport type TemplateShareProblem = {\n /** The template-local id of the entry the problem belongs to; on an exported template it is\n * the block's project-local uuid. */\n readonly entryId: string;\n readonly error: string;\n};\n\n/**\n * Every entry of a template that cannot travel to another machine, or an empty list for a\n * template that can be shared.\n *\n * An entry's `location` names a place rather than a name, and a `file:` place is a folder on\n * the author's own disk: a recipient resolving it finds nothing, or worse finds something\n * else. Such a template stays perfectly usable where it was made, so it is stored and applied\n * as normal — only sharing it is refused.\n *\n * A location whose scheme cannot be read is refused for the same reason: nothing can resolve\n * it anywhere, here included.\n *\n * Every offending entry is reported, not only the first, so a UI can name each block instead\n * of sending its user round the loop once per entry.\n */\nexport function unshareableTemplateEntries(\n document: ProjectTemplateV1,\n): readonly TemplateShareProblem[] {\n const problems: TemplateShareProblem[] = [];\n for (const entry of document.blocks) {\n if (entry.location === undefined) continue;\n let scheme: string;\n try {\n scheme = parseBlockPackLocation(entry.location).scheme;\n } catch (e) {\n problems.push({\n entryId: entry.id,\n error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n if (scheme === \"file\")\n problems.push({\n entryId: entry.id,\n error:\n `Block is installed from ${entry.location}, a folder on this machine — it resolves ` +\n \"to nothing on the recipient's, so this template cannot be shared\",\n });\n }\n return problems;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0BA,SAAgB,2BACd,UACiC;CACjC,MAAM,WAAmC,CAAC;CAC1C,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,IAAI,MAAM,aAAa,KAAA,GAAW;EAClC,IAAI;EACJ,IAAI;GACF,SAAS,uBAAuB,MAAM,QAAQ,CAAC,CAAC;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OAAO,2DAA2D,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GAC7G,CAAC;GACD;EACF;EACA,IAAI,WAAW,QACb,SAAS,KAAK;GACZ,SAAS,MAAM;GACf,OACE,2BAA2B,MAAM,SAAS;EAE9C,CAAC;CACL;CACA,OAAO;AACT"}
|