@milaboratories/pl-middle-layer 1.72.0 → 1.73.1
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/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/folders.cjs +14 -9
- package/dist/middle_layer/folders.cjs.map +1 -1
- package/dist/middle_layer/folders.js +14 -10
- package/dist/middle_layer/folders.js.map +1 -1
- package/dist/middle_layer/middle_layer.cjs +17 -17
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +11 -11
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +18 -18
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/package.json +14 -14
- package/src/middle_layer/folders.test.ts +22 -14
- package/src/middle_layer/folders.ts +37 -27
- package/src/middle_layer/middle_layer.ts +18 -18
- package/src/middle_layer/templates.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-middle-layer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.73.1",
|
|
4
4
|
"description": "Pl Middle Layer",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -30,26 +30,26 @@
|
|
|
30
30
|
"utility-types": "^3.11.0",
|
|
31
31
|
"yaml": "^2.8.0",
|
|
32
32
|
"zod": "~3.25.76",
|
|
33
|
-
"@milaboratories/pf-driver": "1.9.5",
|
|
34
33
|
"@milaboratories/columns-collection-driver": "0.2.7",
|
|
35
34
|
"@milaboratories/computable": "2.9.8",
|
|
35
|
+
"@milaboratories/pf-driver": "1.9.6",
|
|
36
36
|
"@milaboratories/helpers": "1.14.5",
|
|
37
|
-
"@milaboratories/pf-spec-driver": "1.5.
|
|
38
|
-
"@milaboratories/pl-crash-recorder": "0.3.3",
|
|
39
|
-
"@milaboratories/pl-client": "3.17.3",
|
|
37
|
+
"@milaboratories/pf-spec-driver": "1.5.6",
|
|
40
38
|
"@milaboratories/pl-deployments": "3.0.19",
|
|
41
|
-
"@milaboratories/pl-
|
|
42
|
-
"@milaboratories/pl-
|
|
39
|
+
"@milaboratories/pl-crash-recorder": "0.3.3",
|
|
40
|
+
"@milaboratories/pl-drivers": "1.16.27",
|
|
41
|
+
"@milaboratories/pl-client": "3.17.4",
|
|
43
42
|
"@milaboratories/pl-http": "1.2.4",
|
|
44
|
-
"@milaboratories/pl-
|
|
43
|
+
"@milaboratories/pl-errors": "1.4.45",
|
|
44
|
+
"@milaboratories/pl-model-backend": "1.4.30",
|
|
45
45
|
"@milaboratories/pl-model-common": "1.51.0",
|
|
46
|
-
"@milaboratories/pl-model-middle-layer": "1.
|
|
47
|
-
"@milaboratories/pl-tree": "1.15.
|
|
46
|
+
"@milaboratories/pl-model-middle-layer": "1.34.0",
|
|
47
|
+
"@milaboratories/pl-tree": "1.15.5",
|
|
48
48
|
"@milaboratories/resolve-helper": "1.1.3",
|
|
49
49
|
"@milaboratories/ts-helpers": "1.8.6",
|
|
50
|
-
"@platforma-sdk/block-tools": "2.16.
|
|
51
|
-
"@platforma-sdk/
|
|
52
|
-
"@platforma-sdk/
|
|
50
|
+
"@platforma-sdk/block-tools": "2.16.5",
|
|
51
|
+
"@platforma-sdk/model": "1.84.3",
|
|
52
|
+
"@platforma-sdk/workflow-tengo": "6.12.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "~24.5.2",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"@vitest/coverage-istanbul": "^4.1.3",
|
|
58
58
|
"typescript": "7.0.2",
|
|
59
59
|
"vitest": "^4.1.3",
|
|
60
|
-
"@milaboratories/build-configs": "2.0.2",
|
|
61
60
|
"@milaboratories/ts-builder": "1.7.2",
|
|
61
|
+
"@milaboratories/build-configs": "2.0.2",
|
|
62
62
|
"@milaboratories/ts-configs": "1.4.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
@@ -79,18 +79,20 @@ test("a typed name is stored trimmed, and a blank folder name is refused", async
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("a name a human typed is rejected when
|
|
82
|
+
test("a name a human typed is rejected when another folder carries it, whatever its case", async () => {
|
|
83
83
|
await withMl(async (ml) => {
|
|
84
84
|
await ml.createFolder("Samples");
|
|
85
|
-
await expect(ml.createFolder("samples")).rejects.toThrow(
|
|
85
|
+
await expect(ml.createFolder("samples")).rejects.toThrow(
|
|
86
|
+
'A folder named "samples" is already here.',
|
|
87
|
+
);
|
|
86
88
|
|
|
87
|
-
// A project
|
|
89
|
+
// A project is another kind of item: its name is no obstacle to a folder's.
|
|
88
90
|
await ml.createProject({ label: "Alpha" });
|
|
89
91
|
await untilListing(ml, (l) => l.projects.length === 1);
|
|
90
|
-
await
|
|
92
|
+
await ml.createFolder("alpha");
|
|
91
93
|
|
|
92
94
|
const other = await ml.createFolder("Other");
|
|
93
|
-
await expect(ml.renameFolder(other, "SAMPLES")).rejects.toThrow(/already
|
|
95
|
+
await expect(ml.renameFolder(other, "SAMPLES")).rejects.toThrow(/folder named .* already here/);
|
|
94
96
|
|
|
95
97
|
// Renaming a folder to what it is already called is not a collision with itself.
|
|
96
98
|
await ml.renameFolder(other, "Other");
|
|
@@ -625,16 +627,22 @@ test("renaming a project onto a sibling's name is refused, in whatever case", as
|
|
|
625
627
|
await move(ml, other, folder);
|
|
626
628
|
await untilListing(ml, (l) => l.projects.filter((p) => p.folder === folder).length === 2);
|
|
627
629
|
|
|
628
|
-
await expect(ml.setProjectMeta(other, { label: "Alpha" })).rejects.toThrow(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
await ml.
|
|
632
|
-
|
|
630
|
+
await expect(ml.setProjectMeta(other, { label: "Alpha" })).rejects.toThrow(
|
|
631
|
+
'A project named "Alpha" is already here.',
|
|
632
|
+
);
|
|
633
|
+
await expect(ml.setProjectMeta(other, { label: "ALPHA" })).rejects.toThrow(
|
|
634
|
+
/project named .* already here/,
|
|
635
|
+
);
|
|
633
636
|
|
|
634
|
-
// Nothing was written by
|
|
637
|
+
// Nothing was written by either refusal.
|
|
635
638
|
const listing = await untilListing(ml, (l) => l.projects.length === 2);
|
|
636
639
|
expect(labelOf(listing, other)).toBe("Beta");
|
|
637
640
|
expect(labelOf(listing, resident)).toBe("Alpha");
|
|
641
|
+
|
|
642
|
+
// A folder is another kind of item: a project may take the name of one beside it.
|
|
643
|
+
await ml.createFolder("Archive", folder);
|
|
644
|
+
await ml.setProjectMeta(other, { label: "archive" });
|
|
645
|
+
await untilListing(ml, (l) => labelOf(l, other) === "archive");
|
|
638
646
|
});
|
|
639
647
|
});
|
|
640
648
|
|
|
@@ -858,8 +866,8 @@ test("a template saved from a project lands beside it", async () => {
|
|
|
858
866
|
const listing = await untilListing(ml, (l) => l.templates.length === 1);
|
|
859
867
|
expect(listing.templates[0].id).toBe(saved.templateId);
|
|
860
868
|
expect(listing.templates[0].folder).toBe(folder);
|
|
861
|
-
// The project beside it
|
|
862
|
-
expect(listing.templates[0].label).toBe("Alpha
|
|
869
|
+
// The project beside it answers to that label too, but a project is another kind of item.
|
|
870
|
+
expect(listing.templates[0].label).toBe("Alpha");
|
|
863
871
|
});
|
|
864
872
|
});
|
|
865
873
|
|
|
@@ -913,7 +921,7 @@ test("duplicating a folder rebuilds its subtree beside it, descriptions and all"
|
|
|
913
921
|
|
|
914
922
|
const copiedTemplate = listing.templates.find((t) => t.id !== saved.templateId);
|
|
915
923
|
expect(copiedTemplate?.folder).toBe(copiedInner.id);
|
|
916
|
-
expect(copiedTemplate?.label).toBe("Alpha
|
|
924
|
+
expect(copiedTemplate?.label).toBe("Alpha");
|
|
917
925
|
expect(copiedTemplate?.description).toBe("Reusable");
|
|
918
926
|
});
|
|
919
927
|
});
|
|
@@ -376,26 +376,28 @@ export interface FoldersTx {
|
|
|
376
376
|
/** Resource id of every template the list holds under a label. */
|
|
377
377
|
readonly templateRids: ReadonlyMap<TemplateId, SignedResourceId>;
|
|
378
378
|
/**
|
|
379
|
-
* Names already taken in a folder, or at the top level, for choosing one
|
|
380
|
-
* anything is created.
|
|
379
|
+
* Names already taken by items of one kind in a folder, or at the top level, for choosing one
|
|
380
|
+
* that is free before anything of that kind is created. Each kind has a namespace of its own.
|
|
381
381
|
*
|
|
382
382
|
* Empty when the folder document is not ours to read: nothing is then known about who sits
|
|
383
383
|
* where, and inventing collisions out of that would rename copies for no reason.
|
|
384
384
|
*/
|
|
385
|
-
namesTakenIn(folder: FolderId | undefined): string[];
|
|
386
|
-
/** Names already taken where `project` sits — where something made from it
|
|
387
|
-
* the same reason {@link namesTakenIn} can be. */
|
|
388
|
-
namesTakenBeside(project: ProjectId): string[];
|
|
385
|
+
namesTakenIn(folder: FolderId | undefined, kind: FoldersItem["kind"]): string[];
|
|
386
|
+
/** Names already taken by items of one kind where `project` sits — where something made from it
|
|
387
|
+
* lands. Empty for the same reason {@link namesTakenIn} can be. */
|
|
388
|
+
namesTakenBeside(project: ProjectId, kind: FoldersItem["kind"]): string[];
|
|
389
389
|
/** The folder holding a project, or undefined at the top level. */
|
|
390
390
|
folderOf(project: ProjectId): FolderId | undefined;
|
|
391
391
|
/**
|
|
392
|
-
* Refuses a name for a project or template rename when
|
|
392
|
+
* Refuses a name for a project or template rename when a sibling of the same kind already
|
|
393
|
+
* carries it. A folder, a project and a template beside each other may share a name; two
|
|
394
|
+
* projects, or two templates, may not.
|
|
393
395
|
*
|
|
394
396
|
* Renaming a project or a template is not a folder operation and does not go through
|
|
395
|
-
* {@link withFolders} — it writes the item's own metadata. But
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
397
|
+
* {@link withFolders} — it writes the item's own metadata. But the folder rule governs its
|
|
398
|
+
* name, so without this check a user could rename two projects sitting in the same folder to
|
|
399
|
+
* the same name, and the uniqueness the rest of the feature relies on would be false from the
|
|
400
|
+
* day it shipped.
|
|
399
401
|
*
|
|
400
402
|
* The name is checked against the state the rename actually commits against, since the read
|
|
401
403
|
* happened in the rename's own transaction. Four things it deliberately does not do:
|
|
@@ -424,8 +426,8 @@ export interface FoldersTx {
|
|
|
424
426
|
*
|
|
425
427
|
* What travels is the shape of a subtree, not its identity: the incoming folder keys are the
|
|
426
428
|
* caller's and mean nothing here, so a fresh id is minted for each. Only the root is renamed to
|
|
427
|
-
* be free among the destination
|
|
428
|
-
* each other, and they came in already distinct.
|
|
429
|
+
* be free among the folders in the destination — the folders inside it are only ever compared
|
|
430
|
+
* with each other, and they came in already distinct.
|
|
429
431
|
*
|
|
430
432
|
* @returns `unwritable` when the document is not ours to rewrite and nothing was grafted.
|
|
431
433
|
*/
|
|
@@ -468,8 +470,8 @@ export async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promi
|
|
|
468
470
|
throw new Error(`Folder ${folder} does not exist.`);
|
|
469
471
|
};
|
|
470
472
|
|
|
471
|
-
const namesTakenIn = (folder: FolderId | undefined): string[] =>
|
|
472
|
-
read.decoded.writable ? foldersSiblingNames(view, folder) : [];
|
|
473
|
+
const namesTakenIn = (folder: FolderId | undefined, kind: FoldersItem["kind"]): string[] =>
|
|
474
|
+
read.decoded.writable ? foldersSiblingNames(view, folder, [], { kind }) : [];
|
|
473
475
|
|
|
474
476
|
const folderOf = (project: ProjectId): FolderId | undefined =>
|
|
475
477
|
view.projects.find((candidate) => candidate.id === project)?.folder;
|
|
@@ -481,7 +483,7 @@ export async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promi
|
|
|
481
483
|
projectRids: read.projectRids,
|
|
482
484
|
templateRids: read.templateRids,
|
|
483
485
|
namesTakenIn,
|
|
484
|
-
namesTakenBeside: (project) => namesTakenIn(folderOf(project)),
|
|
486
|
+
namesTakenBeside: (project, kind) => namesTakenIn(folderOf(project), kind),
|
|
485
487
|
folderOf,
|
|
486
488
|
|
|
487
489
|
assertNameFree(item, name) {
|
|
@@ -497,8 +499,8 @@ export async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promi
|
|
|
497
499
|
// name goes through even where a pre-existing duplicate sits beside it.
|
|
498
500
|
if (foldersNameTaken(name, [placed.name])) return;
|
|
499
501
|
|
|
500
|
-
const siblings = foldersSiblingNames(view, placed.folder, [item.id]);
|
|
501
|
-
if (foldersNameTaken(name, siblings)) throw new Error(
|
|
502
|
+
const siblings = foldersSiblingNames(view, placed.folder, [item.id], { kind: item.kind });
|
|
503
|
+
if (foldersNameTaken(name, siblings)) throw new Error(nameTakenMessage(item.kind, name));
|
|
502
504
|
},
|
|
503
505
|
|
|
504
506
|
place(items, folder) {
|
|
@@ -520,7 +522,10 @@ export async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promi
|
|
|
520
522
|
|
|
521
523
|
const root: FoldersGraft["folders"][string] | undefined = subtree.folders[subtree.root];
|
|
522
524
|
if (root === undefined) throw new Error(`The subtree holds no folder ${subtree.root}.`);
|
|
523
|
-
const rootName = foldersUniqueName(
|
|
525
|
+
const rootName = foldersUniqueName(
|
|
526
|
+
root.name,
|
|
527
|
+
foldersSiblingNames(view, destination, [], { kind: "folder" }),
|
|
528
|
+
);
|
|
524
529
|
|
|
525
530
|
const incoming = Object.keys(subtree.folders);
|
|
526
531
|
const minted = new Map<string, FolderId>();
|
|
@@ -560,9 +565,9 @@ export async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promi
|
|
|
560
565
|
/**
|
|
561
566
|
* Creates a folder and returns its id.
|
|
562
567
|
*
|
|
563
|
-
* The name is typed by a human, so a name
|
|
564
|
-
* quietly suffixed — a text field that disagrees with what was typed is worse
|
|
565
|
-
* no.
|
|
568
|
+
* The name is typed by a human, so a name another folder in the destination already carries is
|
|
569
|
+
* rejected rather than quietly suffixed — a text field that disagrees with what was typed is worse
|
|
570
|
+
* than one that says no. A project or a template of that name beside it is no obstacle.
|
|
566
571
|
*/
|
|
567
572
|
export async function createFolder(
|
|
568
573
|
pl: PlClient,
|
|
@@ -580,8 +585,8 @@ export async function createFolder(
|
|
|
580
585
|
await withFolders(pl, "MLCreateFolder", rids, (view) => {
|
|
581
586
|
if (parent !== undefined && !view.folders.some((folder) => folder.id === parent))
|
|
582
587
|
throw new Error(`Folder ${parent} does not exist.`);
|
|
583
|
-
if (foldersNameTaken(wanted, foldersSiblingNames(view, parent)))
|
|
584
|
-
throw new Error(
|
|
588
|
+
if (foldersNameTaken(wanted, foldersSiblingNames(view, parent, [], { kind: "folder" })))
|
|
589
|
+
throw new Error(nameTakenMessage("folder", wanted));
|
|
585
590
|
|
|
586
591
|
const base = foldersDocumentFromView(view);
|
|
587
592
|
return {
|
|
@@ -613,8 +618,8 @@ export async function renameFolder(
|
|
|
613
618
|
const target = view.folders.find((candidate) => candidate.id === folder);
|
|
614
619
|
if (target === undefined) throw new Error(`Folder ${folder} does not exist.`);
|
|
615
620
|
|
|
616
|
-
const siblings = foldersSiblingNames(view, target.parent, [folder]);
|
|
617
|
-
if (foldersNameTaken(wanted, siblings)) throw new Error(
|
|
621
|
+
const siblings = foldersSiblingNames(view, target.parent, [folder], { kind: "folder" });
|
|
622
|
+
if (foldersNameTaken(wanted, siblings)) throw new Error(nameTakenMessage("folder", wanted));
|
|
618
623
|
|
|
619
624
|
const base = foldersDocumentFromView(view);
|
|
620
625
|
return {
|
|
@@ -797,6 +802,11 @@ export async function resetFolders(pl: PlClient, rids: FoldersRids): Promise<voi
|
|
|
797
802
|
);
|
|
798
803
|
}
|
|
799
804
|
|
|
805
|
+
/** Why a typed name is refused: another item of the same kind beside it already carries it. */
|
|
806
|
+
export function nameTakenMessage(kind: FoldersItem["kind"], name: string): string {
|
|
807
|
+
return `A ${kind} named "${name}" is already here.`;
|
|
808
|
+
}
|
|
809
|
+
|
|
800
810
|
//
|
|
801
811
|
// Internals
|
|
802
812
|
//
|
|
@@ -898,7 +908,7 @@ async function readFolders(tx: PlTransaction, rids: FoldersRids): Promise<Folder
|
|
|
898
908
|
projects.push({ id, name: meta.label });
|
|
899
909
|
});
|
|
900
910
|
|
|
901
|
-
// Labels as well as ids: a template is
|
|
911
|
+
// Labels as well as ids: a template's name is held to the folder rule, among templates.
|
|
902
912
|
const templateRids = new Map<TemplateId, SignedResourceId>();
|
|
903
913
|
const templates: FoldersTemplateInput[] = [];
|
|
904
914
|
[...listedTemplates.values()].forEach(({ rid }, index) => {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
createFolderList,
|
|
25
25
|
deleteFolder,
|
|
26
26
|
moveFolderItems,
|
|
27
|
+
nameTakenMessage,
|
|
27
28
|
openFoldersTx,
|
|
28
29
|
previewFolderDeletion,
|
|
29
30
|
previewFoldersMove,
|
|
@@ -413,9 +414,9 @@ export class MiddleLayer {
|
|
|
413
414
|
* different places: a rename that carried a stale description alongside the new name would
|
|
414
415
|
* undo a description edit that happened in between, and the reverse.
|
|
415
416
|
*
|
|
416
|
-
* The label is stored trimmed, and
|
|
417
|
-
*
|
|
418
|
-
* human typed it, and a name that silently becomes a different name is worse than one that is
|
|
417
|
+
* The label is stored trimmed, and the folder rule governs it, so a label another project
|
|
418
|
+
* inside the same folder already carries is refused rather than written — a folder or a
|
|
419
|
+
* template of that name beside it is no obstacle. A human typed it, and a name that silently becomes a different name is worse than one that is
|
|
419
420
|
* turned down. The check and the write share one transaction, so two renames racing for the
|
|
420
421
|
* same name cannot both win. Duplicates an account already holds are tolerated and never
|
|
421
422
|
* rewritten; see the name check of {@link openFoldersTx}.
|
|
@@ -459,7 +460,7 @@ export class MiddleLayer {
|
|
|
459
460
|
return id;
|
|
460
461
|
}
|
|
461
462
|
|
|
462
|
-
/** Renames a folder. A name
|
|
463
|
+
/** Renames a folder. A name another folder beside it already carries is rejected. */
|
|
463
464
|
public async renameFolder(folder: FolderId, name: string): Promise<void> {
|
|
464
465
|
await renameFolder(this.pl, this.foldersRids, folder, name);
|
|
465
466
|
await this.foldersTree.refreshState();
|
|
@@ -552,7 +553,7 @@ export class MiddleLayer {
|
|
|
552
553
|
* in them, and a copy of every template.
|
|
553
554
|
*
|
|
554
555
|
* The copy lands beside the source, so only its root needs a name of its own — `X (Copy)`.
|
|
555
|
-
* Nothing inside is renamed: names are compared among siblings, and a copied folder's children
|
|
556
|
+
* Nothing inside is renamed: names are compared among siblings of one kind, and a copied folder's children
|
|
556
557
|
* are only ever compared with each other, where they came in distinct already.
|
|
557
558
|
*
|
|
558
559
|
* The subtree is read first and rebuilt in one write transaction, so the folders and everything
|
|
@@ -887,10 +888,10 @@ export class MiddleLayer {
|
|
|
887
888
|
* A block that cannot be expressed as a template entry stores nothing at all, and every
|
|
888
889
|
* such block is reported — fixing an unexportable project takes one pass, not one per block.
|
|
889
890
|
*
|
|
890
|
-
* The template lands beside its project, and is named by the rule
|
|
891
|
-
* by: a label the caller chose is stored trimmed and refused if
|
|
892
|
-
* without one the project's own label is taken — suffixed
|
|
893
|
-
* answers to it.
|
|
891
|
+
* The template lands beside its project, and is named by the rule templates there are named
|
|
892
|
+
* by: a label the caller chose is stored trimmed and refused if another template there already
|
|
893
|
+
* carries it, and without one the project's own label is taken — suffixed only when a template
|
|
894
|
+
* there already answers to it. The project is not a template, so its own name is no obstacle.
|
|
894
895
|
*
|
|
895
896
|
* The template's description is the one the caller gives, stored trimmed; blank means none.
|
|
896
897
|
* Without one the template is stored undescribed, whatever the project's own description says.
|
|
@@ -912,11 +913,10 @@ export class MiddleLayer {
|
|
|
912
913
|
const signedRid = await this.pl.withWriteTx("MLSaveProjectAsTemplate", async (tx) => {
|
|
913
914
|
const meta = await tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey);
|
|
914
915
|
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
915
|
-
const taken = tree.namesTakenBeside(projectId);
|
|
916
|
+
const taken = tree.namesTakenBeside(projectId, "template");
|
|
916
917
|
if (wanted !== undefined && foldersNameTaken(wanted, taken))
|
|
917
|
-
throw new Error(
|
|
918
|
-
|
|
919
|
-
const name = wanted ?? foldersUniqueName(meta.label, [meta.label, ...taken]);
|
|
918
|
+
throw new Error(nameTakenMessage("template", wanted));
|
|
919
|
+
const name = wanted ?? foldersUniqueName(meta.label, taken);
|
|
920
920
|
|
|
921
921
|
const tpl = createTemplate(
|
|
922
922
|
tx,
|
|
@@ -947,8 +947,8 @@ export class MiddleLayer {
|
|
|
947
947
|
* Changes a template's label. The stored document is immutable and stays untouched —
|
|
948
948
|
* improving a template means saving a new one.
|
|
949
949
|
*
|
|
950
|
-
* The label is stored trimmed.
|
|
951
|
-
*
|
|
950
|
+
* The label is stored trimmed. One another template beside it already carries is refused, in
|
|
951
|
+
* the transaction that writes it; a folder or a project of that name beside it is no obstacle.
|
|
952
952
|
*/
|
|
953
953
|
public async renameTemplate(id: TemplateId, label: string): Promise<void> {
|
|
954
954
|
const rid = await this.resolveTemplateId(id);
|
|
@@ -1115,7 +1115,7 @@ export class MiddleLayer {
|
|
|
1115
1115
|
rename === undefined
|
|
1116
1116
|
? foldersUniqueName(sourceMeta.label, [
|
|
1117
1117
|
sourceMeta.label,
|
|
1118
|
-
...tree.namesTakenBeside(srcProjectId),
|
|
1118
|
+
...tree.namesTakenBeside(srcProjectId, "project"),
|
|
1119
1119
|
])
|
|
1120
1120
|
: rename(sourceMeta.label, await existingProjectLabels(tx, this.projectListResourceId));
|
|
1121
1121
|
|
|
@@ -1492,7 +1492,7 @@ export class MiddleLayer {
|
|
|
1492
1492
|
tx,
|
|
1493
1493
|
this.templateListResourceId,
|
|
1494
1494
|
{
|
|
1495
|
-
label: foldersUniqueName(payload.label, tree.namesTakenIn(destination)),
|
|
1495
|
+
label: foldersUniqueName(payload.label, tree.namesTakenIn(destination, "template")),
|
|
1496
1496
|
description: payload.description,
|
|
1497
1497
|
},
|
|
1498
1498
|
{ schemaVersion: 1, document: payload.document, sender: payload.from },
|
|
@@ -1577,7 +1577,7 @@ export class MiddleLayer {
|
|
|
1577
1577
|
// Scoped to where the copies are going, because that is the only place their names have
|
|
1578
1578
|
// to be free. `taken` grows as the pack is copied, so two projects of one name inside a
|
|
1579
1579
|
// single share do not land on top of each other either.
|
|
1580
|
-
const taken = [...tree.namesTakenIn(destination)];
|
|
1580
|
+
const taken = [...tree.namesTakenIn(destination, "project")];
|
|
1581
1581
|
const created = await copyEnvelopeProjectsIntoList(
|
|
1582
1582
|
tx,
|
|
1583
1583
|
envelope.rid,
|
|
@@ -153,7 +153,7 @@ test("copying a template share lands it among the recipient's templates and buil
|
|
|
153
153
|
const list = await awaitTemplateList(ml, (l) => l.length === 2);
|
|
154
154
|
const copied = list.find((t) => t.id === outcome.templates[0]);
|
|
155
155
|
if (copied === undefined) throw new Error("the copied template is not in the list");
|
|
156
|
-
// The original is at the top level too, and one folder never holds two
|
|
156
|
+
// The original is at the top level too, and one folder never holds two templates of one name.
|
|
157
157
|
expect(copied.label).toBe("A pipeline (Copy)");
|
|
158
158
|
// Who sent it, kept as the copy's provenance; the donor's own source project is not part of
|
|
159
159
|
// the payload and does not travel.
|