@aglyn/plugins-data 1.0.0-beta.221 → 1.0.0-beta.223

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 (135) hide show
  1. package/README.md +9 -6
  2. package/package.json +10 -10
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +1 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
  7. package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
  8. package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
  9. package/src/lib/artifact/dataset-schema.d.ts +136 -0
  10. package/src/lib/artifact/dataset-schema.js +232 -0
  11. package/src/lib/artifact/dataset-schema.js.map +1 -0
  12. package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
  13. package/src/lib/components/dataset-record-dialog.component.js +1 -1
  14. package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
  15. package/src/lib/components/dataset-record-filter.d.ts +1 -1
  16. package/src/lib/components/dataset-record-filter.js +2 -2
  17. package/src/lib/components/dataset-record-filter.js.map +1 -1
  18. package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
  19. package/src/lib/components/dataset-schema-dialog.component.js +55 -23
  20. package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
  21. package/src/lib/components/host-datasets-card.component.js +91 -403
  22. package/src/lib/components/host-datasets-card.component.js.map +1 -1
  23. package/src/lib/constants/bundle-common.d.ts +12 -0
  24. package/src/lib/constants/bundle-common.js +13 -0
  25. package/src/lib/constants/bundle-common.js.map +1 -1
  26. package/src/lib/declarations.console-server.d.ts +16 -1
  27. package/src/lib/declarations.console-server.js +34 -2
  28. package/src/lib/declarations.console-server.js.map +1 -1
  29. package/src/lib/declarations.server.d.ts +13 -0
  30. package/src/lib/declarations.server.js +48 -2
  31. package/src/lib/declarations.server.js.map +1 -1
  32. package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
  33. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
  34. package/src/lib/model/dataset-models.d.ts +392 -0
  35. package/src/lib/model/dataset-models.js +806 -0
  36. package/src/lib/model/dataset-models.js.map +1 -0
  37. package/src/lib/model/dataset-record-view.d.ts +1 -1
  38. package/src/lib/model/dataset-record-view.js +1 -2
  39. package/src/lib/model/dataset-record-view.js.map +1 -1
  40. package/src/lib/model/dataset-record.js +2 -2
  41. package/src/lib/model/dataset-record.js.map +1 -1
  42. package/src/lib/model/datasets.d.ts +162 -0
  43. package/src/lib/model/datasets.js +227 -0
  44. package/src/lib/model/datasets.js.map +1 -0
  45. package/src/lib/plugin.d.ts +4 -0
  46. package/src/lib/plugin.js +40 -0
  47. package/src/lib/plugin.js.map +1 -1
  48. package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
  49. package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
  50. package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
  51. package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
  52. package/src/lib/record-pages/record-page-read.server.js +175 -0
  53. package/src/lib/record-pages/record-page-read.server.js.map +1 -0
  54. package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
  55. package/src/lib/record-pages/record-page-resolver.server.js +131 -0
  56. package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
  57. package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
  58. package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
  59. package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
  60. package/src/lib/record-pages/record-page-source.d.ts +42 -0
  61. package/src/lib/record-pages/record-page-source.js +176 -0
  62. package/src/lib/record-pages/record-page-source.js.map +1 -0
  63. package/src/lib/record-pages/record-pages-route.d.ts +42 -0
  64. package/src/lib/record-pages/record-pages-route.js +348 -0
  65. package/src/lib/record-pages/record-pages-route.js.map +1 -0
  66. package/src/lib/record-pages/record-pages.d.ts +218 -0
  67. package/src/lib/record-pages/record-pages.js +351 -0
  68. package/src/lib/record-pages/record-pages.js.map +1 -0
  69. package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
  70. package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
  71. package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
  72. package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
  73. package/src/lib/record-pages/record-template-section.component.js +491 -0
  74. package/src/lib/record-pages/record-template-section.component.js.map +1 -0
  75. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
  76. package/src/lib/repeat/dataset-repeat-rows.js +30 -12
  77. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  78. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
  79. package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
  80. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
  81. package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
  82. package/src/lib/repeat/repeat-record-pages.js +64 -0
  83. package/src/lib/repeat/repeat-record-pages.js.map +1 -0
  84. package/src/lib/server/announce-dataset-records.d.ts +1 -1
  85. package/src/lib/server/announce-dataset-records.js +1 -1
  86. package/src/lib/server/announce-dataset-records.js.map +1 -1
  87. package/src/lib/server/api-v1/datasets.js +5 -3
  88. package/src/lib/server/api-v1/datasets.js.map +1 -1
  89. package/src/lib/server/dataset-figures.js +2 -2
  90. package/src/lib/server/dataset-figures.js.map +1 -1
  91. package/src/lib/server/dataset-live-pages.d.ts +147 -0
  92. package/src/lib/server/dataset-live-pages.js +436 -0
  93. package/src/lib/server/dataset-live-pages.js.map +1 -0
  94. package/src/lib/server/dataset-steps.server.d.ts +45 -0
  95. package/src/lib/server/dataset-steps.server.js +268 -0
  96. package/src/lib/server/dataset-steps.server.js.map +1 -0
  97. package/src/lib/server/datasets-route.d.ts +2 -0
  98. package/src/lib/server/datasets-route.js +102 -6
  99. package/src/lib/server/datasets-route.js.map +1 -1
  100. package/src/lib/server/resolve-dataset.d.ts +40 -0
  101. package/src/lib/server/resolve-dataset.js +64 -0
  102. package/src/lib/server/resolve-dataset.js.map +1 -0
  103. package/src/lib/server.d.ts +3 -2
  104. package/src/lib/server.js +8 -9
  105. package/src/lib/server.js.map +1 -1
  106. package/src/lib/site-bundle/datasets-package.d.ts +42 -0
  107. package/src/lib/site-bundle/datasets-package.js +95 -0
  108. package/src/lib/site-bundle/datasets-package.js.map +1 -0
  109. package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
  110. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
  111. package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
  112. package/src/lib/tenant-api.js +30 -0
  113. package/src/lib/tenant-api.js.map +1 -0
  114. package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
  115. package/src/lib/transfer/dataset-transfer-key.js +31 -0
  116. package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
  117. package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
  118. package/src/lib/transfer/dataset-transfer-model.js +536 -0
  119. package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
  120. package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
  121. package/src/lib/transfer/dataset-transfer-resource.js +41 -0
  122. package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
  123. package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
  124. package/src/lib/transfer/dataset-transfer.server.js +930 -0
  125. package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
  126. package/src/lib/model/dataset-csv.d.ts +0 -36
  127. package/src/lib/model/dataset-csv.js +0 -85
  128. package/src/lib/model/dataset-csv.js.map +0 -1
  129. package/src/lib/model/dataset-io.d.ts +0 -44
  130. package/src/lib/model/dataset-io.js +0 -85
  131. package/src/lib/model/dataset-io.js.map +0 -1
  132. package/src/lib/model/index.js.map +0 -1
  133. package/src/lib/server/datasets-export-route.d.ts +0 -57
  134. package/src/lib/server/datasets-export-route.js +0 -229
  135. package/src/lib/server/datasets-export-route.js.map +0 -1
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { ConsoleHostScreenRowZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins';
18
+ /**
19
+ * A Pages list row's record template chip (AGL-3475): how many record pages the
20
+ * page serves, with the base they live under in its tooltip — what an author
21
+ * scanning the list needs to know about a page that has no address of its own.
22
+ *
23
+ * The site's bindings come from one query however many rows ask, which the
24
+ * Firestore SDK shares between identical listeners; only a row that IS a
25
+ * record template reads anything more, and that is one count.
26
+ */
27
+ export declare function RecordTemplateRowChip(props: ConsoleHostScreenRowZoneProps): import("react").JSX.Element;
28
+ export default RecordTemplateRowChip;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx } from "react/jsx-runtime";
18
+ import { useFirestore, useOrgDataScope } from "@aglyn/tenant-feature-instance";
19
+ import { Chip, Tooltip } from "@mui/material";
20
+ import { collection, doc, getCountFromServer, getDoc } from "firebase/firestore";
21
+ import { useEffect, useState } from "react";
22
+ import { datasetDisplayName } from "../model/datasets.js";
23
+ import { useSiteRecordPageBindings } from "./record-page-source.js";
24
+ /**
25
+ * A Pages list row's record template chip (AGL-3475): how many record pages the
26
+ * page serves, with the base they live under in its tooltip — what an author
27
+ * scanning the list needs to know about a page that has no address of its own.
28
+ *
29
+ * The site's bindings come from one query however many rows ask, which the
30
+ * Firestore SDK shares between identical listeners; only a row that IS a
31
+ * record template reads anything more, and that is one count.
32
+ */ export function RecordTemplateRowChip(props) {
33
+ const { hostId, orgId, screenId } = props;
34
+ const { bindings } = useSiteRecordPageBindings(hostId);
35
+ const binding = bindings.find((one)=>one.screenId === screenId);
36
+ const firestore = useFirestore();
37
+ const { scope } = useOrgDataScope({
38
+ hostId,
39
+ orgId
40
+ });
41
+ const [summary, setSummary] = useState(null);
42
+ const datasetId = binding == null ? void 0 : binding.datasetId;
43
+ useEffect(()=>{
44
+ if (!datasetId || !scope) {
45
+ setSummary(null);
46
+ return undefined;
47
+ }
48
+ let active = true;
49
+ const datasetRef = doc(firestore, scope[0], scope[1], 'datasets', datasetId);
50
+ Promise.all([
51
+ getDoc(datasetRef),
52
+ getCountFromServer(collection(datasetRef, 'records'))
53
+ ]).then(([dataset, count])=>{
54
+ if (!active) return;
55
+ setSummary({
56
+ name: datasetDisplayName(dataset.data()),
57
+ count: count.data().count
58
+ });
59
+ }).catch((error)=>{
60
+ // A dataset this viewer may not read still gets its chip, unnumbered.
61
+ console.error(error);
62
+ if (active) setSummary(null);
63
+ });
64
+ return ()=>{
65
+ active = false;
66
+ };
67
+ }, [
68
+ firestore,
69
+ scope,
70
+ datasetId
71
+ ]);
72
+ if (!binding) return null;
73
+ // Short, because it shares the name's cell: the count is the news, and the
74
+ // tooltip says where the pages live.
75
+ const label = summary ? `${summary.count} record ${summary.count === 1 ? 'page' : 'pages'}` : 'Record template';
76
+ return /*#__PURE__*/ _jsx(Tooltip, {
77
+ title: `Record template${(summary == null ? void 0 : summary.name) ? ` of ${summary.name}` : ''}: served once per record at /${binding.base}/ and each record's page address`,
78
+ children: /*#__PURE__*/ _jsx(Chip, {
79
+ size: "small",
80
+ variant: "outlined",
81
+ label: label
82
+ })
83
+ });
84
+ }
85
+ export default RecordTemplateRowChip;
86
+
87
+ //# sourceMappingURL=record-template-row-chip.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-template-row-chip.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport type { ConsoleHostScreenRowZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { useFirestore, useOrgDataScope } from '@aglyn/tenant-feature-instance'\nimport { Chip, Tooltip } from '@mui/material'\nimport { collection, doc, getCountFromServer, getDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { datasetDisplayName, type HostDataset } from '../model/datasets'\nimport { useSiteRecordPageBindings } from './record-page-source'\n\n/**\n * A Pages list row's record template chip (AGL-3475): how many record pages the\n * page serves, with the base they live under in its tooltip — what an author\n * scanning the list needs to know about a page that has no address of its own.\n *\n * The site's bindings come from one query however many rows ask, which the\n * Firestore SDK shares between identical listeners; only a row that IS a\n * record template reads anything more, and that is one count.\n */\nexport function RecordTemplateRowChip(props: ConsoleHostScreenRowZoneProps) {\n const { hostId, orgId, screenId } = props\n const { bindings } = useSiteRecordPageBindings(hostId)\n const binding = bindings.find((one) => one.screenId === screenId)\n const firestore = useFirestore()\n const { scope } = useOrgDataScope({ hostId, orgId })\n const [summary, setSummary] = useState<{ name: string; count: number } | null>(null)\n const datasetId = binding?.datasetId\n\n useEffect(() => {\n if (!datasetId || !scope) {\n setSummary(null)\n return undefined\n }\n let active = true\n const datasetRef = doc(firestore, scope[0], scope[1], 'datasets', datasetId)\n Promise.all([getDoc(datasetRef), getCountFromServer(collection(datasetRef, 'records'))])\n .then(([dataset, count]) => {\n if (!active) return\n setSummary({\n name: datasetDisplayName(dataset.data() as HostDataset | undefined),\n count: count.data().count,\n })\n })\n .catch((error) => {\n // A dataset this viewer may not read still gets its chip, unnumbered.\n console.error(error)\n if (active) setSummary(null)\n })\n return () => {\n active = false\n }\n }, [firestore, scope, datasetId])\n\n if (!binding) return null\n // Short, because it shares the name's cell: the count is the news, and the\n // tooltip says where the pages live.\n const label = summary\n ? `${summary.count} record ${summary.count === 1 ? 'page' : 'pages'}`\n : 'Record template'\n return (\n <Tooltip\n title={`Record template${summary?.name ? ` of ${summary.name}` : ''}: served once per record at /${binding.base}/ and each record's page address`}\n >\n <Chip size=\"small\" variant=\"outlined\" label={label} />\n </Tooltip>\n )\n}\n\nexport default RecordTemplateRowChip\n"],"names":["useFirestore","useOrgDataScope","Chip","Tooltip","collection","doc","getCountFromServer","getDoc","useEffect","useState","datasetDisplayName","useSiteRecordPageBindings","RecordTemplateRowChip","props","hostId","orgId","screenId","bindings","binding","find","one","firestore","scope","summary","setSummary","datasetId","undefined","active","datasetRef","Promise","all","then","dataset","count","name","data","catch","error","console","label","title","base","size","variant"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAGA,SAASA,YAAY,EAAEC,eAAe,QAAQ,iCAAgC;AAC9E,SAASC,IAAI,EAAEC,OAAO,QAAQ,gBAAe;AAC7C,SAASC,UAAU,EAAEC,GAAG,EAAEC,kBAAkB,EAAEC,MAAM,QAAQ,qBAAoB;AAChF,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,kBAAkB,QAA0B,uBAAmB;AACxE,SAASC,yBAAyB,QAAQ,0BAAsB;AAEhE;;;;;;;;CAQC,GACD,OAAO,SAASC,sBAAsBC,KAAoC;IACxE,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAGH;IACpC,MAAM,EAAEI,QAAQ,EAAE,GAAGN,0BAA0BG;IAC/C,MAAMI,UAAUD,SAASE,IAAI,CAAC,CAACC,MAAQA,IAAIJ,QAAQ,KAAKA;IACxD,MAAMK,YAAYrB;IAClB,MAAM,EAAEsB,KAAK,EAAE,GAAGrB,gBAAgB;QAAEa;QAAQC;IAAM;IAClD,MAAM,CAACQ,SAASC,WAAW,GAAGf,SAAiD;IAC/E,MAAMgB,YAAYP,2BAAAA,QAASO,SAAS;IAEpCjB,UAAU;QACR,IAAI,CAACiB,aAAa,CAACH,OAAO;YACxBE,WAAW;YACX,OAAOE;QACT;QACA,IAAIC,SAAS;QACb,MAAMC,aAAavB,IAAIgB,WAAWC,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE,YAAYG;QAClEI,QAAQC,GAAG,CAAC;YAACvB,OAAOqB;YAAatB,mBAAmBF,WAAWwB,YAAY;SAAY,EACpFG,IAAI,CAAC,CAAC,CAACC,SAASC,MAAM;YACrB,IAAI,CAACN,QAAQ;YACbH,WAAW;gBACTU,MAAMxB,mBAAmBsB,QAAQG,IAAI;gBACrCF,OAAOA,MAAME,IAAI,GAAGF,KAAK;YAC3B;QACF,GACCG,KAAK,CAAC,CAACC;YACN,sEAAsE;YACtEC,QAAQD,KAAK,CAACA;YACd,IAAIV,QAAQH,WAAW;QACzB;QACF,OAAO;YACLG,SAAS;QACX;IACF,GAAG;QAACN;QAAWC;QAAOG;KAAU;IAEhC,IAAI,CAACP,SAAS,OAAO;IACrB,2EAA2E;IAC3E,qCAAqC;IACrC,MAAMqB,QAAQhB,UACV,GAAGA,QAAQU,KAAK,CAAC,QAAQ,EAAEV,QAAQU,KAAK,KAAK,IAAI,SAAS,SAAS,GACnE;IACJ,qBACE,KAAC9B;QACCqC,OAAO,CAAC,eAAe,EAAEjB,CAAAA,2BAAAA,QAASW,IAAI,IAAG,CAAC,IAAI,EAAEX,QAAQW,IAAI,EAAE,GAAG,GAAG,6BAA6B,EAAEhB,QAAQuB,IAAI,CAAC,gCAAgC,CAAC;kBAEjJ,cAAA,KAACvC;YAAKwC,MAAK;YAAQC,SAAQ;YAAWJ,OAAOA;;;AAGnD;AAEA,eAAe3B,sBAAqB"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { ConsoleBesignerPagePropertiesZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins';
18
+ /**
19
+ * Page Properties' "Record pages" section (AGL-3475): makes the page in the
20
+ * editor the record template of a dataset, so it is served once per record at
21
+ * `/{base}/{address}`, and picks the record the canvas draws it for.
22
+ *
23
+ * Saving a page that is not yet a template converts it first, through the
24
+ * platform's own route, after saying what that means: it stops answering at
25
+ * its own address and stops counting toward the plan's pages. The binding is
26
+ * then written by the record-pages route, which re-checks everything this
27
+ * section checks and the few things only the server can see.
28
+ */
29
+ export declare function RecordTemplateSection(props: ConsoleBesignerPagePropertiesZoneProps): import("react").JSX.Element;
30
+ export default RecordTemplateSection;
@@ -0,0 +1,491 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
19
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
+ import { checkEntitlement, scopeTokensForHost } from "@aglyn/aglyn";
21
+ import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
24
+ import { useFirestore, useFirestoreCollection, useOrgDataScope, useOrgPlan, useUser } from "@aglyn/tenant-feature-instance";
25
+ import { Button, InputAdornment, MenuItem, Stack, TextField, Typography } from "@mui/material";
26
+ import { collection, limit, query, where } from "firebase/firestore";
27
+ import { useCallback, useEffect, useMemo, useState } from "react";
28
+ import { effectiveDatasetModel } from "../model/dataset-models.js";
29
+ import { datasetDisplayName } from "../model/datasets.js";
30
+ import { isRecordAddressField, recordAddressFieldIds, recordPageBaseRefusal, recordPagePath } from "./record-pages.js";
31
+ import { useRecordPagePreview, useSiteRecordPageBindings } from "./record-page-source.js";
32
+ /** The optional field pickers, in the order the section offers them. */ const FIELD_PICKERS = [
33
+ {
34
+ key: 'titleField',
35
+ label: 'Page name from',
36
+ helper: 'Names each page — its tab title and breadcrumb. Defaults to the first text field.',
37
+ empty: 'First text field'
38
+ },
39
+ {
40
+ key: 'seoTitleField',
41
+ label: 'Search title from',
42
+ helper: 'Used exactly as written. Without one, the page name and the site title are used.',
43
+ empty: 'Page name'
44
+ },
45
+ {
46
+ key: 'seoDescriptionField',
47
+ label: 'Search description from',
48
+ helper: 'Without one, this page’s own description is used.',
49
+ empty: 'This page’s description'
50
+ },
51
+ {
52
+ key: 'seoImageField',
53
+ label: 'Sharing image from',
54
+ helper: 'A media field or an image URL. Without one, this page’s image is used.',
55
+ empty: 'This page’s image'
56
+ }
57
+ ];
58
+ const EMPTY_DRAFT = {
59
+ datasetId: '',
60
+ base: '',
61
+ slugField: ''
62
+ };
63
+ /**
64
+ * Page Properties' "Record pages" section (AGL-3475): makes the page in the
65
+ * editor the record template of a dataset, so it is served once per record at
66
+ * `/{base}/{address}`, and picks the record the canvas draws it for.
67
+ *
68
+ * Saving a page that is not yet a template converts it first, through the
69
+ * platform's own route, after saying what that means: it stops answering at
70
+ * its own address and stops counting toward the plan's pages. The binding is
71
+ * then written by the record-pages route, which re-checks everything this
72
+ * section checks and the few things only the server can see.
73
+ */ export function RecordTemplateSection(props) {
74
+ const { hostId, orgId, screenId, screenKind } = props;
75
+ const firestore = useFirestore();
76
+ const { data: user } = useUser();
77
+ const { enqueueSnackbar } = useSnackbar();
78
+ const { confirm } = useConfirmationContext();
79
+ const { org, ready: planReady } = useOrgPlan(hostId);
80
+ const { scope } = useOrgDataScope({
81
+ hostId,
82
+ orgId
83
+ });
84
+ const { bindings } = useSiteRecordPageBindings(hostId);
85
+ const current = bindings.find((binding)=>binding.screenId === screenId);
86
+ const preview = useRecordPagePreview({
87
+ hostId,
88
+ screenId
89
+ });
90
+ // The datasets THIS site may see, which are the ones a record page can
91
+ // render: the same scope the published page's reads apply.
92
+ const { data: datasetDocs } = useFirestoreCollection(()=>scope ? query(collection(firestore, scope[0], scope[1], 'datasets'), where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)), limit(100)) : null, [
93
+ firestore,
94
+ scope,
95
+ hostId
96
+ ], {
97
+ idField: '$id'
98
+ });
99
+ const datasets = useMemo(()=>(datasetDocs != null ? datasetDocs : []).filter((dataset)=>!dataset.deletedAt).sort((a, b)=>datasetDisplayName(a).localeCompare(datasetDisplayName(b))), [
100
+ datasetDocs
101
+ ]);
102
+ const [draft, setDraft] = useState(EMPTY_DRAFT);
103
+ const [sourceField, setSourceField] = useState('');
104
+ const [busy, setBusy] = useState(false);
105
+ useEffect(()=>{
106
+ if (current) {
107
+ const { screenId: _id } = current, stored = _object_without_properties_loose(current, [
108
+ "screenId"
109
+ ]);
110
+ setDraft(stored);
111
+ } else {
112
+ setDraft(EMPTY_DRAFT);
113
+ }
114
+ }, [
115
+ current
116
+ ]);
117
+ const dataset = datasets.find((one)=>one.$id === draft.datasetId);
118
+ const model = dataset ? effectiveDatasetModel(dataset) : undefined;
119
+ const addressFields = model ? recordAddressFieldIds(model) : [];
120
+ const textFields = model ? model.order.filter((fieldId)=>{
121
+ var _model_fields_fieldId;
122
+ return ((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.type) === 'text' && !isRecordAddressField(model.fields[fieldId]);
123
+ }) : [];
124
+ const baseRefusal = draft.base ? recordPageBaseRefusal(draft.base, {
125
+ otherBases: bindings.filter((binding)=>binding.screenId !== screenId).map((binding)=>binding.base)
126
+ }) : null;
127
+ // A dataset with exactly one address field needs no choice.
128
+ useEffect(()=>{
129
+ if (model && !draft.slugField && addressFields.length === 1) {
130
+ setDraft((previous)=>_extends({}, previous, {
131
+ slugField: addressFields[0]
132
+ }));
133
+ }
134
+ }, [
135
+ model,
136
+ draft.slugField,
137
+ addressFields
138
+ ]);
139
+ const post = useCallback(async (path, body)=>{
140
+ var _ref;
141
+ const response = await authorizedFetch(user, path, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json'
145
+ },
146
+ body: JSON.stringify(body)
147
+ });
148
+ const result = await response.json().catch(()=>({}));
149
+ if (!response.ok) throw new Error((_ref = result == null ? void 0 : result['error']) != null ? _ref : 'Something went wrong');
150
+ return result;
151
+ }, [
152
+ user
153
+ ]);
154
+ const handleAddAddresses = useCallback(async ()=>{
155
+ if (!dataset || !sourceField || !orgId) return;
156
+ setBusy(true);
157
+ try {
158
+ const result = await post('/api/orgs/datasets', {
159
+ orgId,
160
+ action: 'add-address-field',
161
+ datasetId: dataset.$id,
162
+ sourceField
163
+ });
164
+ setDraft((previous)=>{
165
+ var _result_fieldId;
166
+ return _extends({}, previous, {
167
+ slugField: String((_result_fieldId = result['fieldId']) != null ? _result_fieldId : '')
168
+ });
169
+ });
170
+ enqueueSnackbar(result['truncated'] ? `Gave ${result['filled']} records a page address — run it again for the rest` : `Gave ${result['filled']} records a page address`, {
171
+ variant: 'success'
172
+ });
173
+ } catch (error) {
174
+ enqueueSnackbar(error.message, {
175
+ variant: 'error'
176
+ });
177
+ } finally{
178
+ setBusy(false);
179
+ }
180
+ }, [
181
+ dataset,
182
+ sourceField,
183
+ orgId,
184
+ post,
185
+ enqueueSnackbar
186
+ ]);
187
+ const handleSave = useCallback(async ()=>{
188
+ if (screenKind !== 'template') {
189
+ const accepted = await confirm({
190
+ title: 'Make this page a record template?',
191
+ description: `Each record gets its own page at ${recordPagePath(draft.base || 'base', 'address')}, ` + 'drawn by this design. This page stops answering at its own address and no ' + 'longer counts toward your plan’s pages.',
192
+ confirmationText: 'Make it a template'
193
+ })// The dialog rejects on cancel.
194
+ .then(()=>true).catch(()=>false);
195
+ if (!accepted) return;
196
+ }
197
+ setBusy(true);
198
+ try {
199
+ if (screenKind !== 'template') {
200
+ await post('/api/hosts/screens', {
201
+ action: 'convert',
202
+ hostId,
203
+ id: screenId,
204
+ kind: 'template'
205
+ });
206
+ }
207
+ await post('/api/hosts/record-pages', _extends({
208
+ action: 'save',
209
+ hostId,
210
+ screenId
211
+ }, draft));
212
+ enqueueSnackbar('Record pages saved', {
213
+ variant: 'success'
214
+ });
215
+ } catch (error) {
216
+ enqueueSnackbar(error.message, {
217
+ variant: 'error'
218
+ });
219
+ } finally{
220
+ setBusy(false);
221
+ }
222
+ }, [
223
+ screenKind,
224
+ confirm,
225
+ draft,
226
+ post,
227
+ hostId,
228
+ screenId,
229
+ enqueueSnackbar
230
+ ]);
231
+ const handleRemove = useCallback(async ()=>{
232
+ var _ref;
233
+ const accepted = await confirm({
234
+ title: 'Stop serving record pages?',
235
+ description: `Every page under /${(_ref = current == null ? void 0 : current.base) != null ? _ref : ''}/ stops answering. This page stays a ` + 'template, so it doesn’t answer at its own address either.',
236
+ confirmationText: 'Stop serving them',
237
+ confirmationButtonProps: {
238
+ color: 'error'
239
+ }
240
+ }).then(()=>true).catch(()=>false);
241
+ if (!accepted) return;
242
+ setBusy(true);
243
+ try {
244
+ await post('/api/hosts/record-pages', {
245
+ action: 'remove',
246
+ hostId,
247
+ screenId
248
+ });
249
+ enqueueSnackbar('Record pages removed', {
250
+ variant: 'success'
251
+ });
252
+ } catch (error) {
253
+ enqueueSnackbar(error.message, {
254
+ variant: 'error'
255
+ });
256
+ setBusy(false);
257
+ return;
258
+ }
259
+ // The page is left a template, which nobody can visit. Making it a page
260
+ // again is offered once, here, and checked against the plan's pages by
261
+ // the platform's convert route, whose refusal names the count.
262
+ const restore = await confirm({
263
+ title: 'Make this a normal page again?',
264
+ description: 'It counts toward your plan’s pages and, if it was published, answers at its ' + 'own address again right away. Placeholders like {{item.name}} show as plain ' + 'text until you edit them out.',
265
+ confirmationText: 'Make it a page',
266
+ cancellationText: 'Keep it hidden'
267
+ }).then(()=>true).catch(()=>false);
268
+ if (restore) {
269
+ try {
270
+ await post('/api/hosts/screens', {
271
+ action: 'convert',
272
+ hostId,
273
+ id: screenId,
274
+ kind: 'page'
275
+ });
276
+ enqueueSnackbar('This is a normal page again', {
277
+ variant: 'success'
278
+ });
279
+ } catch (error) {
280
+ enqueueSnackbar(error.message, {
281
+ variant: 'error'
282
+ });
283
+ }
284
+ }
285
+ setBusy(false);
286
+ }, [
287
+ confirm,
288
+ current == null ? void 0 : current.base,
289
+ post,
290
+ hostId,
291
+ screenId,
292
+ enqueueSnackbar
293
+ ]);
294
+ if (planReady && !checkEntitlement(org, 'dataStore')) {
295
+ return /*#__PURE__*/ _jsxs(Stack, {
296
+ spacing: 1,
297
+ children: [
298
+ /*#__PURE__*/ _jsx(Typography, {
299
+ variant: "subtitle2",
300
+ children: 'Record pages'
301
+ }),
302
+ /*#__PURE__*/ _jsx(Typography, {
303
+ variant: "caption",
304
+ color: "text.secondary",
305
+ children: 'Serve this page once per record of a dataset — a page for every service or ' + 'location. Record pages need a Starter plan or higher.'
306
+ })
307
+ ]
308
+ });
309
+ }
310
+ const ready = preview.status === 'ready' ? preview : null;
311
+ return /*#__PURE__*/ _jsxs(Stack, {
312
+ spacing: 1.5,
313
+ children: [
314
+ /*#__PURE__*/ _jsx(Typography, {
315
+ variant: "subtitle2",
316
+ children: 'Record pages'
317
+ }),
318
+ /*#__PURE__*/ _jsx(Typography, {
319
+ variant: "caption",
320
+ color: "text.secondary",
321
+ children: current ? `This page is the template of every record of ${dataset ? datasetDisplayName(dataset) : 'its dataset'}, each at /${current.base}/ and its page address. Inside it, ` + '{{item.field}} fills in from the record.' : 'Serve this page once per record of a dataset, each at its own address — ' + '/services/roofing. Inside it, {{item.field}} fills in from the record, as in ' + 'a repeat.'
322
+ }),
323
+ /*#__PURE__*/ _jsx(TextField, {
324
+ select: true,
325
+ size: "small",
326
+ label: "Dataset",
327
+ value: draft.datasetId,
328
+ onChange: (event)=>setDraft(_extends({}, EMPTY_DRAFT, {
329
+ base: draft.base,
330
+ datasetId: event.target.value
331
+ })),
332
+ helperText: "Only datasets shared with this site are listed",
333
+ children: datasets.map((one)=>/*#__PURE__*/ _jsx(MenuItem, {
334
+ value: one.$id,
335
+ children: datasetDisplayName(one) || one.$id
336
+ }, one.$id))
337
+ }),
338
+ /*#__PURE__*/ _jsx(TextField, {
339
+ size: "small",
340
+ label: "Address",
341
+ value: draft.base,
342
+ onChange: (event)=>setDraft(_extends({}, draft, {
343
+ base: event.target.value
344
+ })),
345
+ error: Boolean(baseRefusal),
346
+ helperText: baseRefusal != null ? baseRefusal : draft.base ? `Records are served at /${draft.base.replace(/^\/+|\/+$/g, '')}/their-address` : 'One or more segments, like services or services/residential',
347
+ slotProps: {
348
+ input: {
349
+ startAdornment: /*#__PURE__*/ _jsx(InputAdornment, {
350
+ position: "start",
351
+ children: "/"
352
+ })
353
+ }
354
+ }
355
+ }),
356
+ model && addressFields.length ? /*#__PURE__*/ _jsx(TextField, {
357
+ select: true,
358
+ size: "small",
359
+ label: "Page address field",
360
+ value: draft.slugField,
361
+ onChange: (event)=>setDraft(_extends({}, draft, {
362
+ slugField: event.target.value
363
+ })),
364
+ helperText: "Each record's own segment of its address",
365
+ children: addressFields.map((fieldId)=>{
366
+ var _ref;
367
+ var _model_fields_fieldId;
368
+ return /*#__PURE__*/ _jsx(MenuItem, {
369
+ value: fieldId,
370
+ children: (_ref = (_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) != null ? _ref : fieldId
371
+ }, fieldId);
372
+ })
373
+ }) : model ? /*#__PURE__*/ _jsxs(Stack, {
374
+ spacing: 1,
375
+ children: [
376
+ /*#__PURE__*/ _jsx(Typography, {
377
+ variant: "caption",
378
+ color: "text.secondary",
379
+ children: 'This dataset has no Page address field yet. Make one from a text field: ' + 'every record gets an address made from its value (Roof Repair → ' + 'roof-repair), and new records get one as they are added. A record keeps its ' + 'address when it is renamed.'
380
+ }),
381
+ /*#__PURE__*/ _jsxs(Stack, {
382
+ direction: "row",
383
+ spacing: 1,
384
+ sx: {
385
+ alignItems: 'center'
386
+ },
387
+ children: [
388
+ /*#__PURE__*/ _jsx(TextField, {
389
+ select: true,
390
+ size: "small",
391
+ label: "Make addresses from",
392
+ value: sourceField,
393
+ onChange: (event)=>setSourceField(event.target.value),
394
+ sx: {
395
+ flex: 1
396
+ },
397
+ children: textFields.map((fieldId)=>{
398
+ var _ref;
399
+ var _model_fields_fieldId;
400
+ return /*#__PURE__*/ _jsx(MenuItem, {
401
+ value: fieldId,
402
+ children: (_ref = (_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) != null ? _ref : fieldId
403
+ }, fieldId);
404
+ })
405
+ }),
406
+ /*#__PURE__*/ _jsx(Button, {
407
+ size: "small",
408
+ variant: "outlined",
409
+ onClick: handleAddAddresses,
410
+ disabled: !sourceField || busy,
411
+ children: 'Make addresses'
412
+ })
413
+ ]
414
+ })
415
+ ]
416
+ }) : null,
417
+ model ? FIELD_PICKERS.map((picker)=>{
418
+ var _draft_picker_key;
419
+ return /*#__PURE__*/ _jsxs(TextField, {
420
+ select: true,
421
+ size: "small",
422
+ label: picker.label,
423
+ value: (_draft_picker_key = draft[picker.key]) != null ? _draft_picker_key : '',
424
+ onChange: (event)=>setDraft(_extends({}, draft, {
425
+ [picker.key]: event.target.value || undefined
426
+ })),
427
+ helperText: picker.helper,
428
+ // The empty choice is a real one — "use the default" — so it is
429
+ // shown rather than leaving the field looking blank.
430
+ slotProps: {
431
+ select: {
432
+ displayEmpty: true
433
+ },
434
+ inputLabel: {
435
+ shrink: true
436
+ }
437
+ },
438
+ children: [
439
+ /*#__PURE__*/ _jsx(MenuItem, {
440
+ value: "",
441
+ children: picker.empty
442
+ }),
443
+ model.order.map((fieldId)=>{
444
+ var _ref;
445
+ var _model_fields_fieldId;
446
+ return /*#__PURE__*/ _jsx(MenuItem, {
447
+ value: fieldId,
448
+ children: (_ref = (_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) != null ? _ref : fieldId
449
+ }, fieldId);
450
+ })
451
+ ]
452
+ }, picker.key);
453
+ }) : null,
454
+ ready ? /*#__PURE__*/ _jsx(TextField, {
455
+ select: true,
456
+ size: "small",
457
+ label: "Preview with",
458
+ value: ready.selectedId,
459
+ onChange: (event)=>ready.select(event.target.value),
460
+ helperText: `The canvas draws this page for one of ${ready.choices.length}${ready.choices.length >= 100 ? '+' : ''} records`,
461
+ children: ready.choices.map((choice)=>/*#__PURE__*/ _jsx(MenuItem, {
462
+ value: choice.id,
463
+ children: choice.label
464
+ }, choice.id))
465
+ }) : null,
466
+ /*#__PURE__*/ _jsxs(Stack, {
467
+ direction: "row",
468
+ spacing: 1,
469
+ children: [
470
+ /*#__PURE__*/ _jsx(Button, {
471
+ size: "small",
472
+ variant: "outlined",
473
+ onClick: handleSave,
474
+ disabled: busy || !draft.datasetId || !draft.base || !draft.slugField || Boolean(baseRefusal),
475
+ children: current ? 'Save record pages' : 'Serve record pages'
476
+ }),
477
+ current ? /*#__PURE__*/ _jsx(Button, {
478
+ size: "small",
479
+ color: "error",
480
+ onClick: handleRemove,
481
+ disabled: busy,
482
+ children: 'Stop serving'
483
+ }) : null
484
+ ]
485
+ })
486
+ ]
487
+ });
488
+ }
489
+ export default RecordTemplateSection;
490
+
491
+ //# sourceMappingURL=record-template-section.component.js.map