@milaboratories/pl-middle-layer 1.71.16 → 1.73.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.
Files changed (85) hide show
  1. package/dist/index.cjs +10 -5
  2. package/dist/index.d.ts +5 -4
  3. package/dist/index.js +3 -2
  4. package/dist/middle_layer/build_stamp.cjs +1 -1
  5. package/dist/middle_layer/build_stamp.js +1 -1
  6. package/dist/middle_layer/folders.cjs +636 -0
  7. package/dist/middle_layer/folders.cjs.map +1 -0
  8. package/dist/middle_layer/folders.d.ts +39 -0
  9. package/dist/middle_layer/folders.d.ts.map +1 -0
  10. package/dist/middle_layer/folders.js +617 -0
  11. package/dist/middle_layer/folders.js.map +1 -0
  12. package/dist/middle_layer/index.cjs +3 -0
  13. package/dist/middle_layer/index.d.ts +4 -3
  14. package/dist/middle_layer/index.js +2 -1
  15. package/dist/middle_layer/middle_layer.cjs +585 -370
  16. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  17. package/dist/middle_layer/middle_layer.d.ts +215 -122
  18. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  19. package/dist/middle_layer/middle_layer.js +593 -378
  20. package/dist/middle_layer/middle_layer.js.map +1 -1
  21. package/dist/middle_layer/project_list.cjs +36 -19
  22. package/dist/middle_layer/project_list.cjs.map +1 -1
  23. package/dist/middle_layer/project_list.d.ts +1 -1
  24. package/dist/middle_layer/project_list.d.ts.map +1 -1
  25. package/dist/middle_layer/project_list.js +37 -21
  26. package/dist/middle_layer/project_list.js.map +1 -1
  27. package/dist/middle_layer/sharing_list.cjs +123 -93
  28. package/dist/middle_layer/sharing_list.cjs.map +1 -1
  29. package/dist/middle_layer/sharing_list.d.ts +43 -19
  30. package/dist/middle_layer/sharing_list.d.ts.map +1 -1
  31. package/dist/middle_layer/sharing_list.js +123 -93
  32. package/dist/middle_layer/sharing_list.js.map +1 -1
  33. package/dist/middle_layer/template_list.cjs +45 -20
  34. package/dist/middle_layer/template_list.cjs.map +1 -1
  35. package/dist/middle_layer/template_list.d.ts +7 -15
  36. package/dist/middle_layer/template_list.d.ts.map +1 -1
  37. package/dist/middle_layer/template_list.js +44 -21
  38. package/dist/middle_layer/template_list.js.map +1 -1
  39. package/dist/model/index.cjs +7 -5
  40. package/dist/model/index.d.ts +2 -2
  41. package/dist/model/index.js +2 -2
  42. package/dist/model/sharing_model.cjs +46 -20
  43. package/dist/model/sharing_model.cjs.map +1 -1
  44. package/dist/model/sharing_model.d.ts +125 -55
  45. package/dist/model/sharing_model.d.ts.map +1 -1
  46. package/dist/model/sharing_model.js +40 -16
  47. package/dist/model/sharing_model.js.map +1 -1
  48. package/dist/mutator/list.cjs +30 -0
  49. package/dist/mutator/list.cjs.map +1 -0
  50. package/dist/mutator/list.js +29 -0
  51. package/dist/mutator/list.js.map +1 -0
  52. package/dist/mutator/project.cjs +14 -1
  53. package/dist/mutator/project.cjs.map +1 -1
  54. package/dist/mutator/project.d.ts.map +1 -1
  55. package/dist/mutator/project.js +15 -2
  56. package/dist/mutator/project.js.map +1 -1
  57. package/dist/mutator/sharing.cjs +130 -76
  58. package/dist/mutator/sharing.cjs.map +1 -1
  59. package/dist/mutator/sharing.js +130 -76
  60. package/dist/mutator/sharing.js.map +1 -1
  61. package/dist/mutator/template.cjs +24 -17
  62. package/dist/mutator/template.cjs.map +1 -1
  63. package/dist/mutator/template.js +26 -20
  64. package/dist/mutator/template.js.map +1 -1
  65. package/package.json +17 -17
  66. package/src/middle_layer/folders.test.ts +1076 -0
  67. package/src/middle_layer/folders.ts +1069 -0
  68. package/src/middle_layer/folders_read.test.ts +359 -0
  69. package/src/middle_layer/index.ts +3 -2
  70. package/src/middle_layer/middle_layer.ts +813 -547
  71. package/src/middle_layer/project_list.test.ts +183 -0
  72. package/src/middle_layer/project_list.ts +58 -21
  73. package/src/middle_layer/sharing.test.ts +183 -0
  74. package/src/middle_layer/sharing_list.ts +208 -152
  75. package/src/middle_layer/template_list.test.ts +148 -0
  76. package/src/middle_layer/template_list.ts +72 -32
  77. package/src/middle_layer/templates.test.ts +76 -45
  78. package/src/model/sharing_model.test.ts +69 -2
  79. package/src/model/sharing_model.ts +161 -73
  80. package/src/mutator/list.ts +36 -0
  81. package/src/mutator/project-v3.test.ts +3 -1
  82. package/src/mutator/project.ts +13 -2
  83. package/src/mutator/sharing.ts +193 -120
  84. package/src/mutator/template.ts +37 -29
  85. package/src/test/with_ml.ts +73 -16
@@ -1 +1 @@
1
- {"version":3,"file":"project_list.cjs","names":["treeFilter","SynchronizedTreeState","Computable","ProjectMetaKey","ProjectCreatedTimestamp","ProjectLastModifiedTimestamp"],"sources":["../../src/middle_layer/project_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport type { WatchableValue } from \"@milaboratories/computable\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { ProjectId, ProjectListEntry } from \"../model/project_model\";\nimport {\n ProjectCreatedTimestamp,\n ProjectLastModifiedTimestamp,\n ProjectMetaKey,\n} from \"../model/project_model\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\n\nexport const ProjectsField = \"projects\";\nexport const ProjectsResourceType: ResourceType = { name: \"Projects\", version: \"1\" };\n\n/**\n * Resolves the projects-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link ProjectsField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no projects list yet, e.g. copying a project\n * into another user's root during admin impersonation.\n */\nexport async function ensureProjectListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const projectsField = field(tx.clientRoot, ProjectsField);\n tx.createField(projectsField, \"Dynamic\");\n const fData = await tx.getField(projectsField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(ProjectsResourceType);\n tx.lock(ref);\n tx.setField(projectsField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const ProjectsListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, ProjectsResourceType)) return [];\n return resource.fields;\n};\n\nexport const projectsListFieldFilter: Filter = treeFilter.resourceTypeEq(\"Projects\");\n\nexport async function createProjectList(\n pl: PlClient,\n rid: SignedResourceId,\n openedProjects: WatchableValue<ProjectId[]>,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<ProjectListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: ProjectsListTreePruningFunction,\n fieldFilter: projectsListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n const oProjects = openedProjects.getValue(ctx);\n if (node === undefined) return undefined;\n const result: ProjectListEntry[] = [];\n\n // Projects list resource keeps projects assigned to fields. Each field name is project's UUID\n for (const field of node.listDynamicFields()) {\n const prj = node.traverse(field);\n if (prj === undefined) continue;\n const meta = notEmpty(prj.getKeyValueAsJson<ProjectMeta>(ProjectMetaKey));\n const created = notEmpty(prj.getKeyValueAsJson<number>(ProjectCreatedTimestamp));\n const lastModified = notEmpty(prj.getKeyValueAsJson<number>(ProjectLastModifiedTimestamp));\n const projectId = resourceIdToString(prj.id) as ProjectId;\n result.push({\n id: projectId,\n created: new Date(created),\n lastModified: new Date(lastModified),\n opened: oProjects.indexOf(projectId) >= 0,\n meta,\n });\n }\n result.sort((p) => -p.lastModified.valueOf());\n return result;\n }).withStableType();\n\n return { computable: c, tree };\n}\n"],"mappings":";;;;;;AA6BA,MAAa,gBAAgB;AAC7B,MAAa,uBAAqC;CAAE,MAAM;CAAY,SAAS;AAAI;;;;;;;AAQnF,eAAsB,qBAAqB,IAA8C;CACvF,MAAM,iBAAA,GAAA,0BAAA,MAAA,CAAsB,GAAG,YAAY,aAAa;CACxD,GAAG,YAAY,eAAe,SAAS;CACvC,MAAM,QAAQ,MAAM,GAAG,SAAS,aAAa;CAC7C,KAAA,GAAA,0BAAA,uBAAA,CAA2B,MAAM,KAAK,GAAG;EACvC,MAAM,MAAM,GAAG,gBAAgB,oBAAoB;EACnD,GAAG,KAAK,GAAG;EACX,GAAG,SAAS,eAAe,GAAG;EAC9B,OAAO,MAAM,IAAI;CACnB;CACA,OAAO,MAAM;AACf;AAEA,MAAa,mCAAoD,aAAa;CAC5E,IAAI,EAAA,GAAA,0BAAA,mBAAA,CAAoB,SAAS,MAAM,oBAAoB,GAAG,OAAO,CAAC;CACtE,OAAO,SAAS;AAClB;AAEA,MAAa,0BAAkCA,0BAAAA,WAAW,eAAe,UAAU;AAEnF,eAAsB,kBACpB,IACA,KACA,gBACA,KACiD;CACjD,MAAM,OAAO,MAAMC,wBAAAA,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CA4BA,OAAO;EAAE,YA1BCC,2BAAAA,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,MAAM,YAAY,eAAe,SAAS,GAAG;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,MAAM,SAA6B,CAAC;GAGpC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;IAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;IAC/B,IAAI,QAAQ,KAAA,GAAW;IACvB,MAAM,QAAA,GAAA,2BAAA,SAAA,CAAgB,IAAI,kBAA+BC,sBAAAA,cAAc,CAAC;IACxE,MAAM,WAAA,GAAA,2BAAA,SAAA,CAAmB,IAAI,kBAA0BC,sBAAAA,uBAAuB,CAAC;IAC/E,MAAM,gBAAA,GAAA,2BAAA,SAAA,CAAwB,IAAI,kBAA0BC,sBAAAA,4BAA4B,CAAC;IACzF,MAAM,aAAA,GAAA,0BAAA,mBAAA,CAA+B,IAAI,EAAE;IAC3C,OAAO,KAAK;KACV,IAAI;KACJ,SAAS,IAAI,KAAK,OAAO;KACzB,cAAc,IAAI,KAAK,YAAY;KACnC,QAAQ,UAAU,QAAQ,SAAS,KAAK;KACxC;IACF,CAAC;GACH;GACA,OAAO,MAAM,MAAM,CAAC,EAAE,aAAa,QAAQ,CAAC;GAC5C,OAAO;EACT,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B"}
1
+ {"version":3,"file":"project_list.cjs","names":["treeFilter","SynchronizedTreeState","Computable","ProjectResourceType","ProjectMetaKey","ProjectCreatedTimestamp","ProjectLastModifiedTimestamp"],"sources":["../../src/middle_layer/project_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport type { WatchableValue } from \"@milaboratories/computable\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { ProjectId, ProjectListEntry } from \"../model/project_model\";\nimport {\n ProjectCreatedTimestamp,\n ProjectLastModifiedTimestamp,\n ProjectMetaKey,\n ProjectResourceType,\n} from \"../model/project_model\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\nimport { asProjectId } from \"@milaboratories/pl-model-common\";\n\nexport const ProjectsField = \"projects\";\nexport const ProjectsResourceType: ResourceType = { name: \"Projects\", version: \"1\" };\n\n/**\n * Resolves the projects-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link ProjectsField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no projects list yet, e.g. copying a project\n * into another user's root during admin impersonation.\n */\nexport async function ensureProjectListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const projectsField = field(tx.clientRoot, ProjectsField);\n tx.createField(projectsField, \"Dynamic\");\n const fData = await tx.getField(projectsField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(ProjectsResourceType);\n tx.lock(ref);\n tx.setField(projectsField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const ProjectsListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, ProjectsResourceType)) return [];\n return resource.fields;\n};\n\nexport const projectsListFieldFilter: Filter = treeFilter.resourceTypeEq(\"Projects\");\n\nexport async function createProjectList(\n pl: PlClient,\n rid: SignedResourceId,\n openedProjects: WatchableValue<ProjectId[]>,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<ProjectListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: ProjectsListTreePruningFunction,\n fieldFilter: projectsListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n const oProjects = openedProjects.getValue(ctx);\n if (node === undefined) return undefined;\n return projectListEntries(node, oProjects);\n }).withStableType();\n\n return { computable: c, tree };\n}\n\n/** The part of a tree node this reader needs from a single entry of the projects list. */\ninterface ProjectListEntryNode {\n readonly id: SignedResourceId;\n readonly resourceType: ResourceType;\n getKeyValueAsJson<T>(key: string): T | undefined;\n}\n\n/** The part of a tree node this reader needs from the projects-list resource itself. */\ninterface ProjectsListNode {\n listDynamicFields(): string[];\n traverse(fieldName: string): ProjectListEntryNode | undefined;\n}\n\n/**\n * Builds the project list out of the projects-list resource, most recently modified first.\n *\n * Skips, rather than throws on, anything that is not a fully synced project. The projects\n * resource carries the user's entire project list, so one unexpected sibling field — or one\n * project whose metadata has not arrived yet — must cost that entry and nothing else. Both the\n * traversal and the metadata reads are watched, so an entry appears as soon as it syncs.\n *\n * The resource type is matched by name only: the point is to exclude foreign resources, not to\n * hide a project stored under an earlier resource-type version.\n */\nexport function projectListEntries(\n node: ProjectsListNode,\n openedProjects: readonly ProjectId[],\n): ProjectListEntry[] {\n const result: ProjectListEntry[] = [];\n\n // Projects list resource keeps projects assigned to fields. Each field name is project's UUID\n for (const field of node.listDynamicFields()) {\n const prj = node.traverse(field);\n if (prj === undefined) continue;\n if (prj.resourceType.name !== ProjectResourceType.name) continue;\n\n const meta = prj.getKeyValueAsJson<ProjectMeta>(ProjectMetaKey);\n const created = prj.getKeyValueAsJson<number>(ProjectCreatedTimestamp);\n const lastModified = prj.getKeyValueAsJson<number>(ProjectLastModifiedTimestamp);\n if (meta === undefined || created === undefined || lastModified === undefined) continue;\n\n const projectId = asProjectId(resourceIdToString(prj.id));\n result.push({\n id: projectId,\n created: new Date(created),\n lastModified: new Date(lastModified),\n opened: openedProjects.indexOf(projectId) >= 0,\n meta,\n });\n }\n\n result.sort((a, b) => b.lastModified.valueOf() - a.lastModified.valueOf());\n return result;\n}\n"],"mappings":";;;;;;AA8BA,MAAa,gBAAgB;AAC7B,MAAa,uBAAqC;CAAE,MAAM;CAAY,SAAS;AAAI;;;;;;;AAQnF,eAAsB,qBAAqB,IAA8C;CACvF,MAAM,iBAAA,GAAA,0BAAA,MAAA,CAAsB,GAAG,YAAY,aAAa;CACxD,GAAG,YAAY,eAAe,SAAS;CACvC,MAAM,QAAQ,MAAM,GAAG,SAAS,aAAa;CAC7C,KAAA,GAAA,0BAAA,uBAAA,CAA2B,MAAM,KAAK,GAAG;EACvC,MAAM,MAAM,GAAG,gBAAgB,oBAAoB;EACnD,GAAG,KAAK,GAAG;EACX,GAAG,SAAS,eAAe,GAAG;EAC9B,OAAO,MAAM,IAAI;CACnB;CACA,OAAO,MAAM;AACf;AAEA,MAAa,mCAAoD,aAAa;CAC5E,IAAI,EAAA,GAAA,0BAAA,mBAAA,CAAoB,SAAS,MAAM,oBAAoB,GAAG,OAAO,CAAC;CACtE,OAAO,SAAS;AAClB;AAEA,MAAa,0BAAkCA,0BAAAA,WAAW,eAAe,UAAU;AAEnF,eAAsB,kBACpB,IACA,KACA,gBACA,KACiD;CACjD,MAAM,OAAO,MAAMC,wBAAAA,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CASA,OAAO;EAAE,YAPCC,2BAAAA,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,MAAM,YAAY,eAAe,SAAS,GAAG;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,OAAO,mBAAmB,MAAM,SAAS;EAC3C,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B;;;;;;;;;;;;AA0BA,SAAgB,mBACd,MACA,gBACoB;CACpB,MAAM,SAA6B,CAAC;CAGpC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;EAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;EAC/B,IAAI,QAAQ,KAAA,GAAW;EACvB,IAAI,IAAI,aAAa,SAASC,sBAAAA,oBAAoB,MAAM;EAExD,MAAM,OAAO,IAAI,kBAA+BC,sBAAAA,cAAc;EAC9D,MAAM,UAAU,IAAI,kBAA0BC,sBAAAA,uBAAuB;EACrE,MAAM,eAAe,IAAI,kBAA0BC,sBAAAA,4BAA4B;EAC/E,IAAI,SAAS,KAAA,KAAa,YAAY,KAAA,KAAa,iBAAiB,KAAA,GAAW;EAE/E,MAAM,aAAA,GAAA,gCAAA,YAAA,EAAA,GAAA,0BAAA,mBAAA,CAA2C,IAAI,EAAE,CAAC;EACxD,OAAO,KAAK;GACV,IAAI;GACJ,SAAS,IAAI,KAAK,OAAO;GACzB,cAAc,IAAI,KAAK,YAAY;GACnC,QAAQ,eAAe,QAAQ,SAAS,KAAK;GAC7C;EACF,CAAC;CACH;CAEA,OAAO,MAAM,GAAG,MAAM,EAAE,aAAa,QAAQ,IAAI,EAAE,aAAa,QAAQ,CAAC;CACzE,OAAO;AACT"}
@@ -1,5 +1,5 @@
1
- import "./types.js";
2
1
  import "../model/project_model.js";
2
+ import "./types.js";
3
3
  import "./middle_layer.js";
4
4
  import { PlClient, PlTransaction, ResourceType, SignedResourceId } from "@milaboratories/pl-client";
5
5
  import "@milaboratories/pl-tree";
@@ -1 +1 @@
1
- {"version":3,"file":"project_list.d.ts","names":[],"sources":["../../src/middle_layer/project_list.ts"],"mappings":";;;;;;;qBA6Ba;qBACA,sBAAsB"}
1
+ {"version":3,"file":"project_list.d.ts","names":[],"sources":["../../src/middle_layer/project_list.ts"],"mappings":";;;;;;;qBA8Ba;qBACA,sBAAsB"}
@@ -1,5 +1,5 @@
1
- import { ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey } from "../model/project_model.js";
2
- import { notEmpty } from "@milaboratories/ts-helpers";
1
+ import { ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType } from "../model/project_model.js";
2
+ import { asProjectId } from "@milaboratories/pl-model-common";
3
3
  import { field, isNullSignedResourceId, resourceIdToString, resourceTypesEqual, treeFilter } from "@milaboratories/pl-client";
4
4
  import { SynchronizedTreeState } from "@milaboratories/pl-tree";
5
5
  import { Computable } from "@milaboratories/computable";
@@ -43,29 +43,45 @@ async function createProjectList(pl, rid, openedProjects, env) {
43
43
  const node = ctx.accessor(tree.entry()).node();
44
44
  const oProjects = openedProjects.getValue(ctx);
45
45
  if (node === void 0) return void 0;
46
- const result = [];
47
- for (const field of node.listDynamicFields()) {
48
- const prj = node.traverse(field);
49
- if (prj === void 0) continue;
50
- const meta = notEmpty(prj.getKeyValueAsJson(ProjectMetaKey));
51
- const created = notEmpty(prj.getKeyValueAsJson(ProjectCreatedTimestamp));
52
- const lastModified = notEmpty(prj.getKeyValueAsJson(ProjectLastModifiedTimestamp));
53
- const projectId = resourceIdToString(prj.id);
54
- result.push({
55
- id: projectId,
56
- created: new Date(created),
57
- lastModified: new Date(lastModified),
58
- opened: oProjects.indexOf(projectId) >= 0,
59
- meta
60
- });
61
- }
62
- result.sort((p) => -p.lastModified.valueOf());
63
- return result;
46
+ return projectListEntries(node, oProjects);
64
47
  }).withStableType(),
65
48
  tree
66
49
  };
67
50
  }
51
+ /**
52
+ * Builds the project list out of the projects-list resource, most recently modified first.
53
+ *
54
+ * Skips, rather than throws on, anything that is not a fully synced project. The projects
55
+ * resource carries the user's entire project list, so one unexpected sibling field — or one
56
+ * project whose metadata has not arrived yet — must cost that entry and nothing else. Both the
57
+ * traversal and the metadata reads are watched, so an entry appears as soon as it syncs.
58
+ *
59
+ * The resource type is matched by name only: the point is to exclude foreign resources, not to
60
+ * hide a project stored under an earlier resource-type version.
61
+ */
62
+ function projectListEntries(node, openedProjects) {
63
+ const result = [];
64
+ for (const field of node.listDynamicFields()) {
65
+ const prj = node.traverse(field);
66
+ if (prj === void 0) continue;
67
+ if (prj.resourceType.name !== ProjectResourceType.name) continue;
68
+ const meta = prj.getKeyValueAsJson(ProjectMetaKey);
69
+ const created = prj.getKeyValueAsJson(ProjectCreatedTimestamp);
70
+ const lastModified = prj.getKeyValueAsJson(ProjectLastModifiedTimestamp);
71
+ if (meta === void 0 || created === void 0 || lastModified === void 0) continue;
72
+ const projectId = asProjectId(resourceIdToString(prj.id));
73
+ result.push({
74
+ id: projectId,
75
+ created: new Date(created),
76
+ lastModified: new Date(lastModified),
77
+ opened: openedProjects.indexOf(projectId) >= 0,
78
+ meta
79
+ });
80
+ }
81
+ result.sort((a, b) => b.lastModified.valueOf() - a.lastModified.valueOf());
82
+ return result;
83
+ }
68
84
  //#endregion
69
- export { ProjectsField, ProjectsListTreePruningFunction, ProjectsResourceType, createProjectList, ensureProjectListRid, projectsListFieldFilter };
85
+ export { ProjectsField, ProjectsListTreePruningFunction, ProjectsResourceType, createProjectList, ensureProjectListRid, projectListEntries, projectsListFieldFilter };
70
86
 
71
87
  //# sourceMappingURL=project_list.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project_list.js","names":[],"sources":["../../src/middle_layer/project_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport type { WatchableValue } from \"@milaboratories/computable\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { ProjectId, ProjectListEntry } from \"../model/project_model\";\nimport {\n ProjectCreatedTimestamp,\n ProjectLastModifiedTimestamp,\n ProjectMetaKey,\n} from \"../model/project_model\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\n\nexport const ProjectsField = \"projects\";\nexport const ProjectsResourceType: ResourceType = { name: \"Projects\", version: \"1\" };\n\n/**\n * Resolves the projects-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link ProjectsField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no projects list yet, e.g. copying a project\n * into another user's root during admin impersonation.\n */\nexport async function ensureProjectListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const projectsField = field(tx.clientRoot, ProjectsField);\n tx.createField(projectsField, \"Dynamic\");\n const fData = await tx.getField(projectsField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(ProjectsResourceType);\n tx.lock(ref);\n tx.setField(projectsField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const ProjectsListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, ProjectsResourceType)) return [];\n return resource.fields;\n};\n\nexport const projectsListFieldFilter: Filter = treeFilter.resourceTypeEq(\"Projects\");\n\nexport async function createProjectList(\n pl: PlClient,\n rid: SignedResourceId,\n openedProjects: WatchableValue<ProjectId[]>,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<ProjectListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: ProjectsListTreePruningFunction,\n fieldFilter: projectsListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n const oProjects = openedProjects.getValue(ctx);\n if (node === undefined) return undefined;\n const result: ProjectListEntry[] = [];\n\n // Projects list resource keeps projects assigned to fields. Each field name is project's UUID\n for (const field of node.listDynamicFields()) {\n const prj = node.traverse(field);\n if (prj === undefined) continue;\n const meta = notEmpty(prj.getKeyValueAsJson<ProjectMeta>(ProjectMetaKey));\n const created = notEmpty(prj.getKeyValueAsJson<number>(ProjectCreatedTimestamp));\n const lastModified = notEmpty(prj.getKeyValueAsJson<number>(ProjectLastModifiedTimestamp));\n const projectId = resourceIdToString(prj.id) as ProjectId;\n result.push({\n id: projectId,\n created: new Date(created),\n lastModified: new Date(lastModified),\n opened: oProjects.indexOf(projectId) >= 0,\n meta,\n });\n }\n result.sort((p) => -p.lastModified.valueOf());\n return result;\n }).withStableType();\n\n return { computable: c, tree };\n}\n"],"mappings":";;;;;;AA6BA,MAAa,gBAAgB;AAC7B,MAAa,uBAAqC;CAAE,MAAM;CAAY,SAAS;AAAI;;;;;;;AAQnF,eAAsB,qBAAqB,IAA8C;CACvF,MAAM,gBAAgB,MAAM,GAAG,YAAY,aAAa;CACxD,GAAG,YAAY,eAAe,SAAS;CACvC,MAAM,QAAQ,MAAM,GAAG,SAAS,aAAa;CAC7C,IAAI,uBAAuB,MAAM,KAAK,GAAG;EACvC,MAAM,MAAM,GAAG,gBAAgB,oBAAoB;EACnD,GAAG,KAAK,GAAG;EACX,GAAG,SAAS,eAAe,GAAG;EAC9B,OAAO,MAAM,IAAI;CACnB;CACA,OAAO,MAAM;AACf;AAEA,MAAa,mCAAoD,aAAa;CAC5E,IAAI,CAAC,mBAAmB,SAAS,MAAM,oBAAoB,GAAG,OAAO,CAAC;CACtE,OAAO,SAAS;AAClB;AAEA,MAAa,0BAAkC,WAAW,eAAe,UAAU;AAEnF,eAAsB,kBACpB,IACA,KACA,gBACA,KACiD;CACjD,MAAM,OAAO,MAAM,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CA4BA,OAAO;EAAE,YA1BC,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,MAAM,YAAY,eAAe,SAAS,GAAG;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,MAAM,SAA6B,CAAC;GAGpC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;IAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;IAC/B,IAAI,QAAQ,KAAA,GAAW;IACvB,MAAM,OAAO,SAAS,IAAI,kBAA+B,cAAc,CAAC;IACxE,MAAM,UAAU,SAAS,IAAI,kBAA0B,uBAAuB,CAAC;IAC/E,MAAM,eAAe,SAAS,IAAI,kBAA0B,4BAA4B,CAAC;IACzF,MAAM,YAAY,mBAAmB,IAAI,EAAE;IAC3C,OAAO,KAAK;KACV,IAAI;KACJ,SAAS,IAAI,KAAK,OAAO;KACzB,cAAc,IAAI,KAAK,YAAY;KACnC,QAAQ,UAAU,QAAQ,SAAS,KAAK;KACxC;IACF,CAAC;GACH;GACA,OAAO,MAAM,MAAM,CAAC,EAAE,aAAa,QAAQ,CAAC;GAC5C,OAAO;EACT,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B"}
1
+ {"version":3,"file":"project_list.js","names":[],"sources":["../../src/middle_layer/project_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport type { WatchableValue } from \"@milaboratories/computable\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { ProjectId, ProjectListEntry } from \"../model/project_model\";\nimport {\n ProjectCreatedTimestamp,\n ProjectLastModifiedTimestamp,\n ProjectMetaKey,\n ProjectResourceType,\n} from \"../model/project_model\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\nimport { asProjectId } from \"@milaboratories/pl-model-common\";\n\nexport const ProjectsField = \"projects\";\nexport const ProjectsResourceType: ResourceType = { name: \"Projects\", version: \"1\" };\n\n/**\n * Resolves the projects-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link ProjectsField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no projects list yet, e.g. copying a project\n * into another user's root during admin impersonation.\n */\nexport async function ensureProjectListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const projectsField = field(tx.clientRoot, ProjectsField);\n tx.createField(projectsField, \"Dynamic\");\n const fData = await tx.getField(projectsField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(ProjectsResourceType);\n tx.lock(ref);\n tx.setField(projectsField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const ProjectsListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, ProjectsResourceType)) return [];\n return resource.fields;\n};\n\nexport const projectsListFieldFilter: Filter = treeFilter.resourceTypeEq(\"Projects\");\n\nexport async function createProjectList(\n pl: PlClient,\n rid: SignedResourceId,\n openedProjects: WatchableValue<ProjectId[]>,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<ProjectListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: ProjectsListTreePruningFunction,\n fieldFilter: projectsListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n const oProjects = openedProjects.getValue(ctx);\n if (node === undefined) return undefined;\n return projectListEntries(node, oProjects);\n }).withStableType();\n\n return { computable: c, tree };\n}\n\n/** The part of a tree node this reader needs from a single entry of the projects list. */\ninterface ProjectListEntryNode {\n readonly id: SignedResourceId;\n readonly resourceType: ResourceType;\n getKeyValueAsJson<T>(key: string): T | undefined;\n}\n\n/** The part of a tree node this reader needs from the projects-list resource itself. */\ninterface ProjectsListNode {\n listDynamicFields(): string[];\n traverse(fieldName: string): ProjectListEntryNode | undefined;\n}\n\n/**\n * Builds the project list out of the projects-list resource, most recently modified first.\n *\n * Skips, rather than throws on, anything that is not a fully synced project. The projects\n * resource carries the user's entire project list, so one unexpected sibling field — or one\n * project whose metadata has not arrived yet — must cost that entry and nothing else. Both the\n * traversal and the metadata reads are watched, so an entry appears as soon as it syncs.\n *\n * The resource type is matched by name only: the point is to exclude foreign resources, not to\n * hide a project stored under an earlier resource-type version.\n */\nexport function projectListEntries(\n node: ProjectsListNode,\n openedProjects: readonly ProjectId[],\n): ProjectListEntry[] {\n const result: ProjectListEntry[] = [];\n\n // Projects list resource keeps projects assigned to fields. Each field name is project's UUID\n for (const field of node.listDynamicFields()) {\n const prj = node.traverse(field);\n if (prj === undefined) continue;\n if (prj.resourceType.name !== ProjectResourceType.name) continue;\n\n const meta = prj.getKeyValueAsJson<ProjectMeta>(ProjectMetaKey);\n const created = prj.getKeyValueAsJson<number>(ProjectCreatedTimestamp);\n const lastModified = prj.getKeyValueAsJson<number>(ProjectLastModifiedTimestamp);\n if (meta === undefined || created === undefined || lastModified === undefined) continue;\n\n const projectId = asProjectId(resourceIdToString(prj.id));\n result.push({\n id: projectId,\n created: new Date(created),\n lastModified: new Date(lastModified),\n opened: openedProjects.indexOf(projectId) >= 0,\n meta,\n });\n }\n\n result.sort((a, b) => b.lastModified.valueOf() - a.lastModified.valueOf());\n return result;\n}\n"],"mappings":";;;;;;AA8BA,MAAa,gBAAgB;AAC7B,MAAa,uBAAqC;CAAE,MAAM;CAAY,SAAS;AAAI;;;;;;;AAQnF,eAAsB,qBAAqB,IAA8C;CACvF,MAAM,gBAAgB,MAAM,GAAG,YAAY,aAAa;CACxD,GAAG,YAAY,eAAe,SAAS;CACvC,MAAM,QAAQ,MAAM,GAAG,SAAS,aAAa;CAC7C,IAAI,uBAAuB,MAAM,KAAK,GAAG;EACvC,MAAM,MAAM,GAAG,gBAAgB,oBAAoB;EACnD,GAAG,KAAK,GAAG;EACX,GAAG,SAAS,eAAe,GAAG;EAC9B,OAAO,MAAM,IAAI;CACnB;CACA,OAAO,MAAM;AACf;AAEA,MAAa,mCAAoD,aAAa;CAC5E,IAAI,CAAC,mBAAmB,SAAS,MAAM,oBAAoB,GAAG,OAAO,CAAC;CACtE,OAAO,SAAS;AAClB;AAEA,MAAa,0BAAkC,WAAW,eAAe,UAAU;AAEnF,eAAsB,kBACpB,IACA,KACA,gBACA,KACiD;CACjD,MAAM,OAAO,MAAM,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CASA,OAAO;EAAE,YAPC,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,MAAM,YAAY,eAAe,SAAS,GAAG;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,OAAO,mBAAmB,MAAM,SAAS;EAC3C,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B;;;;;;;;;;;;AA0BA,SAAgB,mBACd,MACA,gBACoB;CACpB,MAAM,SAA6B,CAAC;CAGpC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;EAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;EAC/B,IAAI,QAAQ,KAAA,GAAW;EACvB,IAAI,IAAI,aAAa,SAAS,oBAAoB,MAAM;EAExD,MAAM,OAAO,IAAI,kBAA+B,cAAc;EAC9D,MAAM,UAAU,IAAI,kBAA0B,uBAAuB;EACrE,MAAM,eAAe,IAAI,kBAA0B,4BAA4B;EAC/E,IAAI,SAAS,KAAA,KAAa,YAAY,KAAA,KAAa,iBAAiB,KAAA,GAAW;EAE/E,MAAM,YAAY,YAAY,mBAAmB,IAAI,EAAE,CAAC;EACxD,OAAO,KAAK;GACV,IAAI;GACJ,SAAS,IAAI,KAAK,OAAO;GACzB,cAAc,IAAI,KAAK,YAAY;GACnC,QAAQ,eAAe,QAAQ,SAAS,KAAK;GAC7C;EACF,CAAC;CACH;CAEA,OAAO,MAAM,GAAG,MAAM,EAAE,aAAa,QAAQ,IAAI,EAAE,aAAa,QAAQ,CAAC;CACzE,OAAO;AACT"}
@@ -3,28 +3,11 @@ let _milaboratories_pl_client = require("@milaboratories/pl-client");
3
3
  let _milaboratories_pl_tree = require("@milaboratories/pl-tree");
4
4
  let _milaboratories_computable = require("@milaboratories/computable");
5
5
  //#region src/middle_layer/sharing_list.ts
6
- /** Per-project view for the donor's change UI, from the envelope's `projects` payload; empty
7
- * for a payload that carries no project. */
8
- function envelopeProjects(data) {
9
- return Object.values(require_sharing_model.envelopeProjectMap(data)).map((p) => ({
10
- projectId: p.source,
11
- label: p.label,
12
- updatedAt: p.updatedAt
13
- }));
14
- }
15
- const SharingOutboxPruningFunction = (resource) => {
16
- if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharingOutboxResourceType) && !(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharedEnvelopeResourceType)) return [];
17
- return resource.fields;
18
- };
19
- const SharingOutboxFieldFilter = _milaboratories_pl_client.treeFilter.or(_milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharingOutboxResourceType.name), _milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharedEnvelopeResourceType.name));
20
6
  /**
21
7
  * Reactive view of the donor's own outbox. Reads each live envelope's immutable
22
- * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then
23
- * enriches each share's full recipient list via `ListGrants` on the envelope (`["*"]` for an
24
- * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is
25
- * exactly who reads this view.
26
- *
27
- * API-level only in M1 (no UI).
8
+ * {@link EnvelopeData} from the tree, then enriches each share's full recipient list via
9
+ * `ListGrants` on the envelope (`["*"]` for an everyone-share). `ListGrants` is gated backend-side
10
+ * to the envelope owner (the donor), which is exactly who reads this view.
28
11
  */
29
12
  function createOutgoingSharesComputable(pl, tree) {
30
13
  return _milaboratories_computable.Computable.make((ctx) => {
@@ -37,17 +20,8 @@ function createOutgoingSharesComputable(pl, tree) {
37
20
  if (!(0, _milaboratories_pl_client.resourceTypesEqual)(envelope.resourceType, require_sharing_model.SharedEnvelopeResourceType)) continue;
38
21
  const data = require_sharing_model.normalizeEnvelopeData(envelope.getDataAsJson());
39
22
  if (data === void 0) continue;
40
- const responses = {};
41
- for (const f of envelope.listDynamicFields()) {
42
- if (!f.startsWith("acceptance/")) continue;
43
- const login = f.slice(require_sharing_model.AcceptanceFieldPrefix.length);
44
- const accData = envelope.traverse(f)?.getDataAsJson();
45
- if (accData === void 0) continue;
46
- responses[login] = {
47
- action: accData.action,
48
- timestamp: accData.timestamp
49
- };
50
- }
23
+ const folder = envelopeFolderSummary(data);
24
+ const description = envelopeDescription(data);
51
25
  drafts.push({
52
26
  shareId: data.shareId,
53
27
  sharedAt: data.sharedAt,
@@ -58,10 +32,11 @@ function createOutgoingSharesComputable(pl, tree) {
58
32
  projects: envelopeProjects(data),
59
33
  ...data.payload.kind === "template" ? { template: {
60
34
  label: data.payload.label,
61
- blockCount: data.payload.document.blocks.length
35
+ blockCount: data.payload.document.blocks.length,
36
+ ...data.payload.source === void 0 ? {} : { source: data.payload.source }
62
37
  } } : {},
63
- responsesAvailable: data.payload.kind !== "template",
64
- responses,
38
+ ...folder === void 0 ? {} : { folder },
39
+ ...description === void 0 ? {} : { description },
65
40
  envelopeRid: envelope.id
66
41
  });
67
42
  }
@@ -97,102 +72,73 @@ async function createOutgoingShares(pl, outboxRid, env) {
97
72
  tree
98
73
  };
99
74
  }
100
- const DecisionFieldPrefix = "decision/";
101
- const SharedEnvelopePruningFunction = (resource) => {
102
- if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharedEnvelopeResourceType)) return [];
103
- return resource.fields;
104
- };
105
- const PendingSharesFieldFilter = _milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharedEnvelopeResourceType.name);
106
75
  /**
107
- * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over
108
- * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.
109
- *
110
- * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —
111
- * the dedup is applied by the caller, which holds the SharingState tree.
76
+ * Creates the recipient's shared-resource discovery tree: a `{kind:'shared'}` seed over
77
+ * {@link SharedEnvelopeResourceType}. {@link createAvailableSharesComputable} and
78
+ * {@link createLiveEnvelopesComputable} both read it.
112
79
  */
113
- async function createPendingSharesTree(pl, env) {
80
+ async function createAvailableSharesTree(pl, env) {
114
81
  return await _milaboratories_pl_tree.SynchronizedTreeState.init(pl, {
115
82
  kind: "shared",
116
83
  resourceType: (0, _milaboratories_pl_client.resourceType)(require_sharing_model.SharedEnvelopeResourceType.name, require_sharing_model.SharedEnvelopeResourceType.version)
117
84
  }, {
118
85
  ...env.ops.defaultTreeOptions,
119
86
  pruning: SharedEnvelopePruningFunction,
120
- fieldFilter: PendingSharesFieldFilter
87
+ fieldFilter: AvailableSharesFieldFilter
121
88
  }, env.logger);
122
89
  }
123
90
  /**
124
- * Builds a Computable yielding the acceptor's currently-live envelopes, read from the
91
+ * Builds a Computable yielding the recipient's currently-live envelopes, read from the
125
92
  * shared-resource discovery tree the ML already maintains — the single discovery mechanism.
126
- * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no
127
- * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx
128
- * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.
93
+ * A copy out of a share `.getValue()`s this instead of re-streaming `ListUserResources`, so there
94
+ * is no second discovery path. The envelope's signed `rid` (from the tree node) is what the write
95
+ * tx needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.
129
96
  *
130
- * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed
131
- * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live
132
- * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.
97
+ * Yields a flat list; the caller dedups by `shareId` — at most one live envelope per shareId is
98
+ * expected (the donor keeps one), and a replace tears the old one down.
133
99
  */
134
100
  function createLiveEnvelopesComputable(sharedTree) {
135
- return _milaboratories_computable.Computable.make((ctx) => {
136
- const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();
137
- const result = [];
138
- for (const envelope of roots) {
139
- if (envelope === void 0) continue;
140
- if (!(0, _milaboratories_pl_client.resourceTypesEqual)(envelope.resourceType, require_sharing_model.SharedEnvelopeResourceType)) continue;
141
- const data = require_sharing_model.normalizeEnvelopeData(envelope.getDataAsJson());
142
- if (data === void 0) continue;
143
- result.push({
144
- rid: envelope.id,
145
- data
146
- });
147
- }
148
- return result;
149
- });
101
+ return _milaboratories_computable.Computable.make((ctx) => decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes()));
150
102
  }
151
103
  /**
152
- * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered
153
- * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's
154
- * SharingState). Both trees feed one Computable so it recomputes when either changes.
104
+ * Builds the {@link AvailableShare} computable over the shared-resource discovery tree, marking
105
+ * each entry with whether this user has hidden it (a hiddenField in their own SharingState).
106
+ * Both trees feed one Computable so it recomputes when either changes.
155
107
  *
156
108
  * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants
157
- * the everyone-user, so the donor discovers their own envelope as a pending share. There is no
158
- * scenario where a user accepts their own share, so they are dropped from the pending view.
109
+ * the everyone-user, so the donor discovers their own envelope here. A donor already owns what
110
+ * they shared, so their own envelopes are dropped from this view.
159
111
  */
160
- function createPendingSharesComputable(sharedTree, sharingStateTree, currentUserLogin) {
112
+ function createAvailableSharesComputable(sharedTree, sharingStateTree, currentUserLogin) {
161
113
  return _milaboratories_computable.Computable.make((ctx) => {
162
- const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();
163
114
  const stateNode = ctx.accessor(sharingStateTree.entry()).node();
164
- const handled = /* @__PURE__ */ new Set();
115
+ const hidden = /* @__PURE__ */ new Set();
165
116
  if (stateNode !== void 0) {
166
- for (const f of stateNode.listDynamicFields()) if (f.startsWith(DecisionFieldPrefix)) handled.add(require_sharing_model.asShareId(f.slice(9)));
117
+ for (const f of stateNode.listDynamicFields()) if (require_sharing_model.isHiddenField(f)) hidden.add(require_sharing_model.hiddenFieldShareId(f));
167
118
  }
168
119
  const result = [];
169
- for (const envelope of roots) {
170
- if (envelope === void 0) continue;
171
- if (!(0, _milaboratories_pl_client.resourceTypesEqual)(envelope.resourceType, require_sharing_model.SharedEnvelopeResourceType)) continue;
172
- const data = require_sharing_model.normalizeEnvelopeData(envelope.getDataAsJson());
173
- if (data === void 0) continue;
174
- if (handled.has(data.shareId)) continue;
120
+ for (const { data } of decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes())) {
175
121
  if (currentUserLogin !== null && data.sender === currentUserLogin) continue;
176
122
  if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;
123
+ const folder = envelopeFolderSummary(data);
124
+ const description = envelopeDescription(data);
177
125
  result.push({
178
126
  shareId: data.shareId,
179
127
  sender: data.sender,
180
128
  title: data.title,
181
129
  mode: data.mode,
182
130
  payloadKind: data.payload.kind,
183
- grantedAt: data.sharedAt
131
+ grantedAt: data.sharedAt,
132
+ ...description === void 0 ? {} : { description },
133
+ ...folder === void 0 ? {} : { folder },
134
+ hidden: hidden.has(data.shareId)
184
135
  });
185
136
  }
186
137
  result.sort((a, b) => b.grantedAt - a.grantedAt);
187
138
  return result;
188
139
  });
189
140
  }
190
- const SharingStatePruningFunction = (resource) => {
191
- if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharingStateResourceType)) return [];
192
- return resource.fields;
193
- };
194
- const SharingStateFieldFilter = _milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharingStateResourceType.name);
195
- /** Creates the acceptor's SharingState synchronized tree (single explicit root). */
141
+ /** Creates the recipient's SharingState synchronized tree (single explicit root). */
196
142
  async function createSharingStateTree(pl, stateRid, env) {
197
143
  return await _milaboratories_pl_tree.SynchronizedTreeState.init(pl, stateRid, {
198
144
  ...env.ops.defaultTreeOptions,
@@ -200,12 +146,96 @@ async function createSharingStateTree(pl, stateRid, env) {
200
146
  fieldFilter: SharingStateFieldFilter
201
147
  }, env.logger);
202
148
  }
149
+ const SharingOutboxPruningFunction = (resource) => {
150
+ if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharingOutboxResourceType) && !(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharedEnvelopeResourceType)) return [];
151
+ return resource.fields;
152
+ };
153
+ const SharingOutboxFieldFilter = _milaboratories_pl_client.treeFilter.or(_milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharingOutboxResourceType.name), _milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharedEnvelopeResourceType.name));
154
+ const SharedEnvelopePruningFunction = (resource) => {
155
+ if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharedEnvelopeResourceType)) return [];
156
+ return resource.fields;
157
+ };
158
+ const AvailableSharesFieldFilter = _milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharedEnvelopeResourceType.name);
159
+ const SharingStatePruningFunction = (resource) => {
160
+ if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, require_sharing_model.SharingStateResourceType)) return [];
161
+ return resource.fields;
162
+ };
163
+ const SharingStateFieldFilter = _milaboratories_pl_client.treeFilter.resourceTypeEq(require_sharing_model.SharingStateResourceType.name);
164
+ /**
165
+ * Every envelope among the discovery tree's roots that this build can read, decoded. A root that
166
+ * is not a {@link SharedEnvelopeResourceType}, or whose schemaVersion or payload kind this build
167
+ * does not know, is left out rather than offered: there is nothing useful to do with a share that
168
+ * cannot be read.
169
+ */
170
+ function decodedEnvelopes(roots) {
171
+ const result = [];
172
+ for (const envelope of roots) {
173
+ if (envelope === void 0) continue;
174
+ if (!(0, _milaboratories_pl_client.resourceTypesEqual)(envelope.resourceType, require_sharing_model.SharedEnvelopeResourceType)) continue;
175
+ const data = require_sharing_model.normalizeEnvelopeData(envelope.getDataAsJson());
176
+ if (data === void 0) continue;
177
+ result.push({
178
+ rid: envelope.id,
179
+ data
180
+ });
181
+ }
182
+ return result;
183
+ }
184
+ /** Per-project view for the donor, from the envelope's `projects` payload; empty for a payload
185
+ * that carries no project. */
186
+ function envelopeProjects(data) {
187
+ return Object.values(require_sharing_model.envelopeProjectMap(data)).map((p) => ({
188
+ projectId: p.source,
189
+ label: p.label,
190
+ updatedAt: p.updatedAt
191
+ }));
192
+ }
193
+ /**
194
+ * What a folder share is, in the words both lists show it in: the shared folder's name and how
195
+ * much is inside. `undefined` for any other payload.
196
+ *
197
+ * The root folder is excluded from `folderCount` — it is the thing being described, not something
198
+ * inside it. A folder envelope with no single root is one nothing can be rebuilt from, and it is
199
+ * described as nothing rather than guessed at.
200
+ */
201
+ function envelopeFolderSummary(data) {
202
+ if (data.payload.kind !== "folder") return void 0;
203
+ const { folders, projects, templates } = data.payload;
204
+ const root = require_sharing_model.envelopeFolderRoot(folders);
205
+ if (root === void 0) return void 0;
206
+ return {
207
+ label: folders[root].name,
208
+ source: data.payload.source,
209
+ folderCount: Object.keys(folders).length - 1,
210
+ projectCount: Object.keys(projects).length,
211
+ templateCount: templates.length
212
+ };
213
+ }
214
+ /**
215
+ * What the shared thing says about itself: the template's description, the root folder's, or the
216
+ * one project's of a single-project share. `undefined` when it has none, for a pack of several
217
+ * projects, which describes no single thing, and for a folder envelope with no single root.
218
+ */
219
+ function envelopeDescription(data) {
220
+ const payload = data.payload;
221
+ switch (payload.kind) {
222
+ case "template": return payload.description;
223
+ case "folder": {
224
+ const root = require_sharing_model.envelopeFolderRoot(payload.folders);
225
+ return root === void 0 ? void 0 : payload.folders[root].description;
226
+ }
227
+ case "projects": {
228
+ const projects = Object.values(payload.projects);
229
+ return projects.length === 1 ? projects[0]?.description : void 0;
230
+ }
231
+ }
232
+ }
203
233
  //#endregion
234
+ exports.createAvailableSharesComputable = createAvailableSharesComputable;
235
+ exports.createAvailableSharesTree = createAvailableSharesTree;
204
236
  exports.createLiveEnvelopesComputable = createLiveEnvelopesComputable;
205
237
  exports.createOutgoingShares = createOutgoingShares;
206
238
  exports.createOutgoingSharesComputable = createOutgoingSharesComputable;
207
- exports.createPendingSharesComputable = createPendingSharesComputable;
208
- exports.createPendingSharesTree = createPendingSharesTree;
209
239
  exports.createSharingStateTree = createSharingStateTree;
210
240
 
211
241
  //# sourceMappingURL=sharing_list.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sharing_list.cjs","names":["envelopeProjectMap","SharingOutboxResourceType","SharedEnvelopeResourceType","treeFilter","Computable","normalizeEnvelopeData","AcceptanceFieldPrefix","SynchronizedTreeState","asShareId","SharingStateResourceType"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, or one template document. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. Empty for a template share. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent for a project share. */\n template?: { label: string; blockCount: number };\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Whether {@link responses} can ever be populated for this share. A template share is granted\n * read-only, so no recipient can record a reply on the envelope and the donor never learns who\n * accepted — a view must say so rather than render an empty response list as \"nobody yet\". */\n responsesAvailable: boolean;\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from the envelope's `projects` payload; empty\n * for a payload that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n /** What the offer carries, so the recipient is told what they are being offered — projects to\n * copy, or a template for their own shelf. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n },\n }\n : {}),\n responsesAvailable: data.payload.kind !== \"template\",\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n // Same recognise-or-hide rule as the pending view, and for the same envelope: hiding an\n // offer while accept could still resolve it would only move the problem.\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n // An envelope whose schemaVersion or payload kind this build does not know is hidden, not\n // offered: there is nothing useful to do with a share we cannot read.\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sharing_list.cjs","names":["Computable","SharedEnvelopeResourceType","normalizeEnvelopeData","SynchronizedTreeState","isHiddenField","hiddenFieldShareId","SharingOutboxResourceType","treeFilter","SharingStateResourceType","envelopeProjectMap","envelopeFolderRoot"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PlTreeNodeAccessor, PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId, TemplateId } from \"@milaboratories/pl-model-common\";\nimport type { FolderId } from \"@milaboratories/pl-model-middle-layer\";\nimport type {\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n hiddenFieldShareId,\n isHiddenField,\n envelopeFolderRoot,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** What a folder share carries, as both share lists show it. */\nexport interface EnvelopeFolderSummary {\n label: string;\n /** Donor's own id of the shared folder. */\n source: FolderId;\n folderCount: number;\n projectCount: number;\n templateCount: number;\n}\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // identity of the share; a share that replaces it gets its own\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, one template document, or a folder subtree. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack. `projectId` is the donor's source project id, which is\n * what the share dialog matches a project's prior shares on; `updatedAt` is when this\n * project's snapshot was taken. Empty for any share that is not a pack of projects. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent otherwise. `source` is the donor's own\n * template id, which is what the share dialog matches a template's prior shares on; a share\n * made before it was carried has none and matches nothing. */\n template?: { label: string; blockCount: number; source?: TemplateId };\n /** The shared folder, for a folder share; absent otherwise. `label` is the root folder's own\n * name, `source` the donor's own id for it — what a prior share of the same folder is matched\n * on — and the counts are of the whole subtree, the root folder itself excluded. */\n folder?: EnvelopeFolderSummary;\n /** What the shared thing says about itself: a template's, a folder's, or the one project's of\n * a single-project share. Absent for a pack of several, which has no one thing to describe. */\n description?: string;\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n}\n\n/**\n * Recipient-facing view of one share that is open to this user.\n *\n * A share is a shelf, not an invitation: it stays listed for as long as the envelope lives, and\n * the recipient may copy from it any number of times. {@link hidden} is that recipient's own\n * private choice to stop seeing it, and it can be undone.\n */\nexport interface AvailableShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode;\n /** What the share carries — projects to copy, a template to add to the recipient's templates,\n * or a folder subtree to rebuild in the recipient's tree. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n /** What the shared thing says about itself. See {@link OutgoingShare.description}. */\n description?: string;\n /** The shared folder, for a folder share; absent otherwise. See {@link OutgoingShare.folder}. */\n folder?: EnvelopeFolderSummary;\n /** Whether this recipient has put it out of sight. Hidden shares are listed, not dropped: the\n * view that shows them is what \"show hidden\" turns on. */\n hidden: boolean;\n}\n\n/** A live envelope discovered in the recipient's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. A copy out of a share finds it by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} from the tree, then enriches each share's full recipient list via\n * `ListGrants` on the envelope (`[\"*\"]` for an everyone-share). `ListGrants` is gated backend-side\n * to the envelope owner (the donor), which is exactly who reads this view.\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n ...(data.payload.source === undefined ? {} : { source: data.payload.source }),\n },\n }\n : {}),\n ...(folder === undefined ? {} : { folder }),\n ...(description === undefined ? {} : { description }),\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\n/**\n * Creates the recipient's shared-resource discovery tree: a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}. {@link createAvailableSharesComputable} and\n * {@link createLiveEnvelopesComputable} both read it.\n */\nexport async function createAvailableSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: AvailableSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/**\n * Builds a Computable yielding the recipient's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * A copy out of a share `.getValue()`s this instead of re-streaming `ListUserResources`, so there\n * is no second discovery path. The envelope's signed `rid` (from the tree node) is what the write\n * tx needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Yields a flat list; the caller dedups by `shareId` — at most one live envelope per shareId is\n * expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes()));\n}\n\n/**\n * Builds the {@link AvailableShare} computable over the shared-resource discovery tree, marking\n * each entry with whether this user has hidden it (a hiddenField in their own SharingState).\n * Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope here. A donor already owns what\n * they shared, so their own envelopes are dropped from this view.\n */\nexport function createAvailableSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<AvailableShare[] | undefined> {\n return Computable.make((ctx) => {\n // Shares this user has put out of sight, keyed on the share. A share replaced by a newer one\n // is a new share, so its replacement arrives unhidden.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const hidden = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (isHiddenField(f)) hidden.add(hiddenFieldShareId(f));\n\n const result: AvailableShare[] = [];\n for (const { data } of decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes())) {\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n ...(description === undefined ? {} : { description }),\n ...(folder === undefined ? {} : { folder }),\n hidden: hidden.has(data.shareId),\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\n/** Creates the recipient's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n//\n// Internals\n//\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst AvailableSharesFieldFilter: Filter = treeFilter.resourceTypeEq(\n SharedEnvelopeResourceType.name,\n);\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Every envelope among the discovery tree's roots that this build can read, decoded. A root that\n * is not a {@link SharedEnvelopeResourceType}, or whose schemaVersion or payload kind this build\n * does not know, is left out rather than offered: there is nothing useful to do with a share that\n * cannot be read.\n */\nfunction decodedEnvelopes(roots: readonly (PlTreeNodeAccessor | undefined)[]): LiveEnvelope[] {\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n}\n\n/** Per-project view for the donor, from the envelope's `projects` payload; empty for a payload\n * that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/**\n * What a folder share is, in the words both lists show it in: the shared folder's name and how\n * much is inside. `undefined` for any other payload.\n *\n * The root folder is excluded from `folderCount` — it is the thing being described, not something\n * inside it. A folder envelope with no single root is one nothing can be rebuilt from, and it is\n * described as nothing rather than guessed at.\n */\nfunction envelopeFolderSummary(data: EnvelopeData): EnvelopeFolderSummary | undefined {\n if (data.payload.kind !== \"folder\") return undefined;\n const { folders, projects, templates } = data.payload;\n const root = envelopeFolderRoot(folders);\n if (root === undefined) return undefined;\n return {\n label: folders[root].name,\n source: data.payload.source,\n folderCount: Object.keys(folders).length - 1,\n projectCount: Object.keys(projects).length,\n templateCount: templates.length,\n };\n}\n\n/**\n * What the shared thing says about itself: the template's description, the root folder's, or the\n * one project's of a single-project share. `undefined` when it has none, for a pack of several\n * projects, which describes no single thing, and for a folder envelope with no single root.\n */\nfunction envelopeDescription(data: EnvelopeData): string | undefined {\n const payload = data.payload;\n switch (payload.kind) {\n case \"template\":\n return payload.description;\n case \"folder\": {\n const root = envelopeFolderRoot(payload.folders);\n return root === undefined ? undefined : payload.folders[root].description;\n }\n case \"projects\": {\n const projects = Object.values(payload.projects);\n return projects.length === 1 ? projects[0]?.description : undefined;\n }\n }\n}\n"],"mappings":"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"}