@aglyn/plugins-data 1.0.0-beta.244 → 1.0.0-beta.247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -10
- package/src/lib/components/dataset-field-inputs.component.d.ts +37 -0
- package/src/lib/components/dataset-field-inputs.component.js +243 -0
- package/src/lib/components/dataset-field-inputs.component.js.map +1 -0
- package/src/lib/components/host-datasets-card.component.js +149 -136
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +3 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/form-target/form-dataset-binding.js +3 -2
- package/src/lib/form-target/form-dataset-binding.js.map +1 -1
- package/src/lib/model/dataset-image-field.d.ts +5 -0
- package/src/lib/model/dataset-image-field.js +8 -3
- package/src/lib/model/dataset-image-field.js.map +1 -1
- package/src/lib/model/dataset-page-field.d.ts +42 -0
- package/src/lib/model/dataset-page-field.js +57 -0
- package/src/lib/model/dataset-page-field.js.map +1 -0
- package/src/lib/plugin.js +12 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/dataset-ai-capability.js +4 -2
- package/src/lib/server/dataset-ai-capability.js.map +1 -1
- package/src/lib/server/dataset-drafts.d.ts +6 -1
- package/src/lib/server/dataset-drafts.js +11 -2
- package/src/lib/server/dataset-drafts.js.map +1 -1
- package/src/lib/server/dataset-record-index.d.ts +6 -0
- package/src/lib/server/dataset-record-index.js +43 -3
- package/src/lib/server/dataset-record-index.js.map +1 -1
|
@@ -39,12 +39,13 @@ const text = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max
|
|
|
39
39
|
if (!datasetId && !datasetName) return null;
|
|
40
40
|
const fieldMap = {};
|
|
41
41
|
for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)){
|
|
42
|
-
var _ref;
|
|
42
|
+
var _ref, _fieldProps_fieldName;
|
|
43
43
|
var _nodes_fieldNodeId;
|
|
44
44
|
const fieldProps = (_ref = nodes == null ? void 0 : (_nodes_fieldNodeId = nodes[fieldNodeId]) == null ? void 0 : _nodes_fieldNodeId.props) != null ? _ref : {};
|
|
45
45
|
const datasetFieldId = text(fieldProps['datasetFieldId'], 64);
|
|
46
46
|
if (!datasetFieldId) continue;
|
|
47
|
-
|
|
47
|
+
// `fieldName` on the all-in-one Form Field, `name` on MUI's inputs.
|
|
48
|
+
const submittedKey = text((_fieldProps_fieldName = fieldProps['fieldName']) != null ? _fieldProps_fieldName : fieldProps['name'], 64) || 'field';
|
|
48
49
|
if (!(submittedKey in fieldMap) && Object.keys(fieldMap).length >= FORM_DATASET_BINDING_MAX_FIELDS) {
|
|
49
50
|
continue;
|
|
50
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/form-dataset-binding.ts"],"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\nimport type { AglynNodeSchema, NodeId } from '@aglyn/aglyn'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n} from '@aglyn/aglyn/app-utils/forms'\n\n/**\n * Where a form's submissions are also written as dataset records, read off\n * the form as a page renders it.\n */\nexport interface FormDatasetBinding {\n /** The dataset, by id, so renaming it never breaks the binding (AGL-556). */\n datasetId?: string\n /** The legacy by-name binding, honored when no id is set (AGL-141). */\n datasetName?: string\n /** Submitted field name → the dataset field id the value is stored under. */\n fieldMap: Record<string, string>\n}\n\n/** The most fields a submission may carry, and so the most a map can name. */\nexport const FORM_DATASET_BINDING_MAX_FIELDS = 20\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The dataset binding a `form` node declares, or null when it declares none.\n *\n * The map is built the way the browser builds the submission: every\n * `formField` under the form, in reading order, keyed by the name the field\n * submits under (`fieldName`, or `field` when it has none), and a later field\n * under the same name replaces an earlier one. A field with no\n * `datasetFieldId` maps nothing and its value falls back to matching dataset\n * fields by name.\n *\n * Read from the COMPOSED tree, so a form grafted from a layout, a reusable\n * component or a form entity is read exactly as the page renders it.\n */\nexport function formDatasetBindingOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n formNodeId: NodeId,\n): FormDatasetBinding | null {\n const node = nodes?.[formNodeId]\n if (node?.componentId !== FORM_COMPONENT_ID) return null\n const props = (node.props ?? {}) as Record<string, unknown>\n const datasetId = text(props['datasetId'], 128)\n const datasetName = text(props['datasetName'], 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)) {\n const fieldProps = (nodes?.[fieldNodeId]?.props ?? {}) as Record<\n string,\n unknown\n >\n const datasetFieldId = text(fieldProps['datasetFieldId'], 64)\n if (!datasetFieldId) continue\n const submittedKey
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/form-dataset-binding.ts"],"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\nimport type { AglynNodeSchema, NodeId } from '@aglyn/aglyn'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n} from '@aglyn/aglyn/app-utils/forms'\n\n/**\n * Where a form's submissions are also written as dataset records, read off\n * the form as a page renders it.\n */\nexport interface FormDatasetBinding {\n /** The dataset, by id, so renaming it never breaks the binding (AGL-556). */\n datasetId?: string\n /** The legacy by-name binding, honored when no id is set (AGL-141). */\n datasetName?: string\n /** Submitted field name → the dataset field id the value is stored under. */\n fieldMap: Record<string, string>\n}\n\n/** The most fields a submission may carry, and so the most a map can name. */\nexport const FORM_DATASET_BINDING_MAX_FIELDS = 20\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The dataset binding a `form` node declares, or null when it declares none.\n *\n * The map is built the way the browser builds the submission: every\n * `formField` under the form, in reading order, keyed by the name the field\n * submits under (`fieldName`, or `field` when it has none), and a later field\n * under the same name replaces an earlier one. A field with no\n * `datasetFieldId` maps nothing and its value falls back to matching dataset\n * fields by name.\n *\n * Read from the COMPOSED tree, so a form grafted from a layout, a reusable\n * component or a form entity is read exactly as the page renders it.\n */\nexport function formDatasetBindingOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n formNodeId: NodeId,\n): FormDatasetBinding | null {\n const node = nodes?.[formNodeId]\n if (node?.componentId !== FORM_COMPONENT_ID) return null\n const props = (node.props ?? {}) as Record<string, unknown>\n const datasetId = text(props['datasetId'], 128)\n const datasetName = text(props['datasetName'], 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)) {\n const fieldProps = (nodes?.[fieldNodeId]?.props ?? {}) as Record<\n string,\n unknown\n >\n const datasetFieldId = text(fieldProps['datasetFieldId'], 64)\n if (!datasetFieldId) continue\n // `fieldName` on the all-in-one Form Field, `name` on MUI's inputs.\n const submittedKey =\n text(fieldProps['fieldName'] ?? fieldProps['name'], 64) || 'field'\n if (\n !(submittedKey in fieldMap) &&\n Object.keys(fieldMap).length >= FORM_DATASET_BINDING_MAX_FIELDS\n ) {\n continue\n }\n fieldMap[submittedKey] = datasetFieldId\n }\n return {\n ...(datasetId ? { datasetId } : {}),\n ...(datasetName ? { datasetName } : {}),\n fieldMap,\n }\n}\n\n/** Every `form` node in a composed tree that declares a dataset binding. */\nexport function formDatasetBindingsOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n): Array<{ nodeId: NodeId; binding: FormDatasetBinding }> {\n const found: Array<{ nodeId: NodeId; binding: FormDatasetBinding }> = []\n for (const [nodeId, node] of Object.entries(nodes ?? {})) {\n if (node?.componentId !== FORM_COMPONENT_ID) continue\n const binding = formDatasetBindingOf(nodes, nodeId as NodeId)\n if (binding) found.push({ nodeId: nodeId as NodeId, binding })\n }\n return found\n}\n"],"names":["collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_DATASET_BINDING_MAX_FIELDS","text","value","max","trim","slice","formDatasetBindingOf","nodes","formNodeId","node","componentId","props","datasetId","datasetName","fieldMap","fieldNodeId","fieldProps","datasetFieldId","submittedKey","Object","keys","length","formDatasetBindingsOf","found","nodeId","entries","binding","push"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,uBAAuB,EACvBC,iBAAiB,QACZ,+BAA8B;AAerC,4EAA4E,GAC5E,OAAO,MAAMC,kCAAkC,GAAE;AAEjD,MAAMC,OAAO,CAACC,OAAgBC,MAC5B,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,KAAK,CAAC,GAAGF,OAAO;AAE3D;;;;;;;;;;;;CAYC,GACD,OAAO,SAASG,qBACdC,KAAqE,EACrEC,UAAkB;QAIHC;IAFf,MAAMA,OAAOF,yBAAAA,KAAO,CAACC,WAAW;IAChC,IAAIC,CAAAA,wBAAAA,KAAMC,WAAW,MAAKX,mBAAmB,OAAO;IACpD,MAAMY,SAASF,cAAAA,KAAKE,KAAK,YAAVF,cAAc,CAAC;IAC9B,MAAMG,YAAYX,KAAKU,KAAK,CAAC,YAAY,EAAE;IAC3C,MAAME,cAAcZ,KAAKU,KAAK,CAAC,cAAc,EAAE;IAC/C,IAAI,CAACC,aAAa,CAACC,aAAa,OAAO;IACvC,MAAMC,WAAmC,CAAC;IAC1C,KAAK,MAAMC,eAAejB,wBAAwBS,OAAOC,YAAa;kBAS7DQ;YARaT;QAApB,MAAMS,qBAAcT,0BAAAA,qBAAAA,KAAO,CAACQ,YAAY,qBAApBR,mBAAsBI,KAAK,mBAAI,CAAC;QAIpD,MAAMM,iBAAiBhB,KAAKe,UAAU,CAAC,iBAAiB,EAAE;QAC1D,IAAI,CAACC,gBAAgB;QACrB,oEAAoE;QACpE,MAAMC,eACJjB,MAAKe,wBAAAA,UAAU,CAAC,YAAY,YAAvBA,wBAA2BA,UAAU,CAAC,OAAO,EAAE,OAAO;QAC7D,IACE,CAAEE,CAAAA,gBAAgBJ,QAAO,KACzBK,OAAOC,IAAI,CAACN,UAAUO,MAAM,IAAIrB,iCAChC;YACA;QACF;QACAc,QAAQ,CAACI,aAAa,GAAGD;IAC3B;IACA,OAAO,aACDL,YAAY;QAAEA;IAAU,IAAI,CAAC,GAC7BC,cAAc;QAAEA;IAAY,IAAI,CAAC;QACrCC;;AAEJ;AAEA,0EAA0E,GAC1E,OAAO,SAASQ,sBACdf,KAAqE;IAErE,MAAMgB,QAAgE,EAAE;IACxE,KAAK,MAAM,CAACC,QAAQf,KAAK,IAAIU,OAAOM,OAAO,CAAClB,gBAAAA,QAAS,CAAC,GAAI;QACxD,IAAIE,CAAAA,wBAAAA,KAAMC,WAAW,MAAKX,mBAAmB;QAC7C,MAAM2B,UAAUpB,qBAAqBC,OAAOiB;QAC5C,IAAIE,SAASH,MAAMI,IAAI,CAAC;YAAEH,QAAQA;YAAkBE;QAAQ;IAC9D;IACA,OAAOH;AACT"}
|
|
@@ -26,6 +26,11 @@ import type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'
|
|
|
26
26
|
* the form a picker writes into a product), a `media:` reference, another
|
|
27
27
|
* path on the site, or an https address. Nothing else is a picture a page can
|
|
28
28
|
* draw, so nothing else is stored.
|
|
29
|
+
*
|
|
30
|
+
* A `media:` reference is read by `parseMediaRef`, the grammar that minted
|
|
31
|
+
* it, so a photo from the organization library (`media:org:{orgId}/{id}`,
|
|
32
|
+
* whose scope holds a colon) and a content-pinned one (`…@{hash}`) are the
|
|
33
|
+
* same photo as a site's own (AGL-3763).
|
|
29
34
|
*/
|
|
30
35
|
/** The custom field type a record's photo rides. Persisted on field definitions — never rename. */
|
|
31
36
|
export declare const DATASET_IMAGE_FIELD_TYPE = "image";
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { parseMediaRef } from "@aglyn/aglyn/app-utils/media-ref";
|
|
17
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
17
18
|
/**
|
|
18
19
|
* A RECORD'S PHOTO (AGL-3616): the "Image" field type, riding `text` storage.
|
|
19
20
|
*
|
|
@@ -25,13 +26,17 @@
|
|
|
25
26
|
* the form a picker writes into a product), a `media:` reference, another
|
|
26
27
|
* path on the site, or an https address. Nothing else is a picture a page can
|
|
27
28
|
* draw, so nothing else is stored.
|
|
29
|
+
*
|
|
30
|
+
* A `media:` reference is read by `parseMediaRef`, the grammar that minted
|
|
31
|
+
* it, so a photo from the organization library (`media:org:{orgId}/{id}`,
|
|
32
|
+
* whose scope holds a colon) and a content-pinned one (`…@{hash}`) are the
|
|
33
|
+
* same photo as a site's own (AGL-3763).
|
|
28
34
|
*/ /** The custom field type a record's photo rides. Persisted on field definitions — never rename. */ export const DATASET_IMAGE_FIELD_TYPE = 'image';
|
|
29
35
|
/** The longest photo address kept. */ export const DATASET_IMAGE_VALUE_MAX = 2000;
|
|
30
|
-
const MEDIA_REF = /^media:[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+(?:@[A-Za-z0-9]+)?$/;
|
|
31
36
|
/** Whether a value is a photo address a record may hold. */ export function isDatasetImageValue(value) {
|
|
32
37
|
if (typeof value !== 'string' || !value || value.length > DATASET_IMAGE_VALUE_MAX) return false;
|
|
33
38
|
if (/[\s"'<>\\]/.test(value)) return false;
|
|
34
|
-
if (value.startsWith('media:')) return
|
|
39
|
+
if (value.startsWith('media:')) return parseMediaRef(value) !== null;
|
|
35
40
|
if (value.startsWith('/')) return !value.startsWith('//');
|
|
36
41
|
return /^https:\/\/[^/]+/.test(value);
|
|
37
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-image-field.ts"],"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\nimport type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * A RECORD'S PHOTO (AGL-3616): the \"Image\" field type, riding `text` storage.\n *\n * A portfolio's pieces, a team, a menu, a studio's classes are lists a\n * visitor looks at before reading, so their records carry a photo, and the\n * page that repeats over them binds it as an Image's `src`\n * (`{{item.<field>}}`). The value is an address the site's own Image element\n * shows: the media library's CDN path (`/api/media/cdn/{scope}/{mediaId}`,\n * the form a picker writes into a product), a `media:` reference, another\n * path on the site, or an https address. Nothing else is a picture a page can\n * draw, so nothing else is stored.\n */\n\n/** The custom field type a record's photo rides. Persisted on field definitions — never rename. */\nexport const DATASET_IMAGE_FIELD_TYPE = 'image'\n\n/** The longest photo address kept. */\nexport const DATASET_IMAGE_VALUE_MAX = 2_000\n\
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-image-field.ts"],"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\nimport { parseMediaRef } from '@aglyn/aglyn/app-utils/media-ref'\nimport type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * A RECORD'S PHOTO (AGL-3616): the \"Image\" field type, riding `text` storage.\n *\n * A portfolio's pieces, a team, a menu, a studio's classes are lists a\n * visitor looks at before reading, so their records carry a photo, and the\n * page that repeats over them binds it as an Image's `src`\n * (`{{item.<field>}}`). The value is an address the site's own Image element\n * shows: the media library's CDN path (`/api/media/cdn/{scope}/{mediaId}`,\n * the form a picker writes into a product), a `media:` reference, another\n * path on the site, or an https address. Nothing else is a picture a page can\n * draw, so nothing else is stored.\n *\n * A `media:` reference is read by `parseMediaRef`, the grammar that minted\n * it, so a photo from the organization library (`media:org:{orgId}/{id}`,\n * whose scope holds a colon) and a content-pinned one (`…@{hash}`) are the\n * same photo as a site's own (AGL-3763).\n */\n\n/** The custom field type a record's photo rides. Persisted on field definitions — never rename. */\nexport const DATASET_IMAGE_FIELD_TYPE = 'image'\n\n/** The longest photo address kept. */\nexport const DATASET_IMAGE_VALUE_MAX = 2_000\n\n/** Whether a value is a photo address a record may hold. */\nexport function isDatasetImageValue(value: unknown): value is string {\n if (typeof value !== 'string' || !value || value.length > DATASET_IMAGE_VALUE_MAX) return false\n if (/[\\s\"'<>\\\\]/.test(value)) return false\n if (value.startsWith('media:')) return parseMediaRef(value) !== null\n if (value.startsWith('/')) return !value.startsWith('//')\n return /^https:\\/\\/[^/]+/.test(value)\n}\n\nexport const DATASET_IMAGE_FIELD: CustomFieldType = {\n name: DATASET_IMAGE_FIELD_TYPE,\n pluginId: BUNDLE_ID,\n label: 'Image',\n baseType: 'text',\n description: 'A photo from the media library, or an https link to one',\n validate: (value) => (isDatasetImageValue(value) ? null : 'Use a photo from the media library or an https link'),\n}\n"],"names":["parseMediaRef","BUNDLE_ID","DATASET_IMAGE_FIELD_TYPE","DATASET_IMAGE_VALUE_MAX","isDatasetImageValue","value","length","test","startsWith","DATASET_IMAGE_FIELD","name","pluginId","label","baseType","description","validate"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,mCAAkC;AAEhE,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;;;;;;CAgBC,GAED,iGAAiG,GACjG,OAAO,MAAMC,2BAA2B,QAAO;AAE/C,oCAAoC,GACpC,OAAO,MAAMC,0BAA0B,KAAK;AAE5C,0DAA0D,GAC1D,OAAO,SAASC,oBAAoBC,KAAc;IAChD,IAAI,OAAOA,UAAU,YAAY,CAACA,SAASA,MAAMC,MAAM,GAAGH,yBAAyB,OAAO;IAC1F,IAAI,aAAaI,IAAI,CAACF,QAAQ,OAAO;IACrC,IAAIA,MAAMG,UAAU,CAAC,WAAW,OAAOR,cAAcK,WAAW;IAChE,IAAIA,MAAMG,UAAU,CAAC,MAAM,OAAO,CAACH,MAAMG,UAAU,CAAC;IACpD,OAAO,mBAAmBD,IAAI,CAACF;AACjC;AAEA,OAAO,MAAMI,sBAAuC;IAClDC,MAAMR;IACNS,UAAUV;IACVW,OAAO;IACPC,UAAU;IACVC,aAAa;IACbC,UAAU,CAACV,QAAWD,oBAAoBC,SAAS,OAAO;AAC5D,EAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields';
|
|
18
|
+
/**
|
|
19
|
+
* A RECORD'S PAGE (AGL-3763): the "Page" field type, riding `text` storage.
|
|
20
|
+
*
|
|
21
|
+
* A service, a location or a team member often has a page of its own that
|
|
22
|
+
* was built as an ordinary page, and the list that repeats over the records
|
|
23
|
+
* links each card to it. The owner picks that page from the site's pages;
|
|
24
|
+
* nobody types an address.
|
|
25
|
+
*
|
|
26
|
+
* The value is the page's link reference, `screen:{screenId}` — the form the
|
|
27
|
+
* Besigner's own page pickers write into a `Link` prop. Every linking element
|
|
28
|
+
* already resolves that form from either of its link slots through the
|
|
29
|
+
* routing map (`splitLinkValue`), so a Button whose link is
|
|
30
|
+
* `{{item.<field>}}` follows the page to its CURRENT address: renaming the
|
|
31
|
+
* page or moving it under another one never breaks the records that name it.
|
|
32
|
+
* A bare id would not do — in an href slot it reads as a relative path.
|
|
33
|
+
*/
|
|
34
|
+
/** The custom field type a record's page rides. Persisted on field definitions — never rename. */
|
|
35
|
+
export declare const DATASET_PAGE_FIELD_TYPE = "page";
|
|
36
|
+
/** The page a stored value names, or undefined when it names none. */
|
|
37
|
+
export declare function datasetPageScreenId(value: unknown): string | undefined;
|
|
38
|
+
/** Whether a value is a page reference a record may hold, exactly as stored. */
|
|
39
|
+
export declare function isDatasetPageValue(value: unknown): value is string;
|
|
40
|
+
/** The stored form of a picked page. */
|
|
41
|
+
export declare function datasetPageValue(screenId: string): string;
|
|
42
|
+
export declare const DATASET_PAGE_FIELD: CustomFieldType;
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
*/ import { SCREEN_LINK_VALUE_PREFIX, formatScreenLinkValue, linkTargetKind, parseScreenLinkValue } from "@aglyn/aglyn/app-utils/screen-link-value";
|
|
17
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
18
|
+
/**
|
|
19
|
+
* A RECORD'S PAGE (AGL-3763): the "Page" field type, riding `text` storage.
|
|
20
|
+
*
|
|
21
|
+
* A service, a location or a team member often has a page of its own that
|
|
22
|
+
* was built as an ordinary page, and the list that repeats over the records
|
|
23
|
+
* links each card to it. The owner picks that page from the site's pages;
|
|
24
|
+
* nobody types an address.
|
|
25
|
+
*
|
|
26
|
+
* The value is the page's link reference, `screen:{screenId}` — the form the
|
|
27
|
+
* Besigner's own page pickers write into a `Link` prop. Every linking element
|
|
28
|
+
* already resolves that form from either of its link slots through the
|
|
29
|
+
* routing map (`splitLinkValue`), so a Button whose link is
|
|
30
|
+
* `{{item.<field>}}` follows the page to its CURRENT address: renaming the
|
|
31
|
+
* page or moving it under another one never breaks the records that name it.
|
|
32
|
+
* A bare id would not do — in an href slot it reads as a relative path.
|
|
33
|
+
*/ /** The custom field type a record's page rides. Persisted on field definitions — never rename. */ export const DATASET_PAGE_FIELD_TYPE = 'page';
|
|
34
|
+
/** A screen id as the console mints them: one path-free segment. */ const SCREEN_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
35
|
+
/** The page a stored value names, or undefined when it names none. */ export function datasetPageScreenId(value) {
|
|
36
|
+
if (typeof value !== 'string' || !value.startsWith(SCREEN_LINK_VALUE_PREFIX)) return undefined;
|
|
37
|
+
const key = parseScreenLinkValue(value);
|
|
38
|
+
if (!key || linkTargetKind(key) !== 'screen' || !SCREEN_ID.test(key)) return undefined;
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
/** Whether a value is a page reference a record may hold, exactly as stored. */ export function isDatasetPageValue(value) {
|
|
42
|
+
const screenId = datasetPageScreenId(value);
|
|
43
|
+
return screenId !== undefined && value === datasetPageValue(screenId);
|
|
44
|
+
}
|
|
45
|
+
/** The stored form of a picked page. */ export function datasetPageValue(screenId) {
|
|
46
|
+
return formatScreenLinkValue(screenId);
|
|
47
|
+
}
|
|
48
|
+
export const DATASET_PAGE_FIELD = {
|
|
49
|
+
name: DATASET_PAGE_FIELD_TYPE,
|
|
50
|
+
pluginId: BUNDLE_ID,
|
|
51
|
+
label: 'Page',
|
|
52
|
+
baseType: 'text',
|
|
53
|
+
description: 'One of the site’s pages, picked from a list — a link to it follows the page wherever it moves',
|
|
54
|
+
validate: (value)=>isDatasetPageValue(value) ? null : 'Pick one of the site’s pages'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=dataset-page-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-page-field.ts"],"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\nimport {\n SCREEN_LINK_VALUE_PREFIX,\n formatScreenLinkValue,\n linkTargetKind,\n parseScreenLinkValue,\n} from '@aglyn/aglyn/app-utils/screen-link-value'\nimport type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * A RECORD'S PAGE (AGL-3763): the \"Page\" field type, riding `text` storage.\n *\n * A service, a location or a team member often has a page of its own that\n * was built as an ordinary page, and the list that repeats over the records\n * links each card to it. The owner picks that page from the site's pages;\n * nobody types an address.\n *\n * The value is the page's link reference, `screen:{screenId}` — the form the\n * Besigner's own page pickers write into a `Link` prop. Every linking element\n * already resolves that form from either of its link slots through the\n * routing map (`splitLinkValue`), so a Button whose link is\n * `{{item.<field>}}` follows the page to its CURRENT address: renaming the\n * page or moving it under another one never breaks the records that name it.\n * A bare id would not do — in an href slot it reads as a relative path.\n */\n\n/** The custom field type a record's page rides. Persisted on field definitions — never rename. */\nexport const DATASET_PAGE_FIELD_TYPE = 'page'\n\n/** A screen id as the console mints them: one path-free segment. */\nconst SCREEN_ID = /^[A-Za-z0-9_-]{1,128}$/\n\n/** The page a stored value names, or undefined when it names none. */\nexport function datasetPageScreenId(value: unknown): string | undefined {\n if (typeof value !== 'string' || !value.startsWith(SCREEN_LINK_VALUE_PREFIX)) return undefined\n const key = parseScreenLinkValue(value)\n if (!key || linkTargetKind(key) !== 'screen' || !SCREEN_ID.test(key)) return undefined\n return key\n}\n\n/** Whether a value is a page reference a record may hold, exactly as stored. */\nexport function isDatasetPageValue(value: unknown): value is string {\n const screenId = datasetPageScreenId(value)\n return screenId !== undefined && value === datasetPageValue(screenId)\n}\n\n/** The stored form of a picked page. */\nexport function datasetPageValue(screenId: string): string {\n return formatScreenLinkValue(screenId)\n}\n\nexport const DATASET_PAGE_FIELD: CustomFieldType = {\n name: DATASET_PAGE_FIELD_TYPE,\n pluginId: BUNDLE_ID,\n label: 'Page',\n baseType: 'text',\n description: 'One of the site’s pages, picked from a list — a link to it follows the page wherever it moves',\n validate: (value) => (isDatasetPageValue(value) ? null : 'Pick one of the site’s pages'),\n}\n"],"names":["SCREEN_LINK_VALUE_PREFIX","formatScreenLinkValue","linkTargetKind","parseScreenLinkValue","BUNDLE_ID","DATASET_PAGE_FIELD_TYPE","SCREEN_ID","datasetPageScreenId","value","startsWith","undefined","key","test","isDatasetPageValue","screenId","datasetPageValue","DATASET_PAGE_FIELD","name","pluginId","label","baseType","description","validate"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,qBAAqB,EACrBC,cAAc,EACdC,oBAAoB,QACf,2CAA0C;AAEjD,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;;;;;CAeC,GAED,gGAAgG,GAChG,OAAO,MAAMC,0BAA0B,OAAM;AAE7C,kEAAkE,GAClE,MAAMC,YAAY;AAElB,oEAAoE,GACpE,OAAO,SAASC,oBAAoBC,KAAc;IAChD,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAMC,UAAU,CAACT,2BAA2B,OAAOU;IACrF,MAAMC,MAAMR,qBAAqBK;IACjC,IAAI,CAACG,OAAOT,eAAeS,SAAS,YAAY,CAACL,UAAUM,IAAI,CAACD,MAAM,OAAOD;IAC7E,OAAOC;AACT;AAEA,8EAA8E,GAC9E,OAAO,SAASE,mBAAmBL,KAAc;IAC/C,MAAMM,WAAWP,oBAAoBC;IACrC,OAAOM,aAAaJ,aAAaF,UAAUO,iBAAiBD;AAC9D;AAEA,sCAAsC,GACtC,OAAO,SAASC,iBAAiBD,QAAgB;IAC/C,OAAOb,sBAAsBa;AAC/B;AAEA,OAAO,MAAME,qBAAsC;IACjDC,MAAMZ;IACNa,UAAUd;IACVe,OAAO;IACPC,UAAU;IACVC,aAAa;IACbC,UAAU,CAACd,QAAWK,mBAAmBL,SAAS,OAAO;AAC3D,EAAC"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -25,6 +26,8 @@ import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/pl
|
|
|
25
26
|
import { DATASET_TRANSFER_RESOURCE } from "./transfer/dataset-transfer-key.js";
|
|
26
27
|
import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
|
|
27
28
|
import { DATASET_IMAGE_FIELD } from "./model/dataset-image-field.js";
|
|
29
|
+
import { DATASET_PAGE_FIELD } from "./model/dataset-page-field.js";
|
|
30
|
+
import { DatasetImageFieldInput, DatasetPageFieldInput } from "./components/dataset-field-inputs.component.js";
|
|
28
31
|
import { RECORD_PAGE_ADDRESS_FIELD } from "./record-pages/record-pages.js";
|
|
29
32
|
import { RECORD_PAGE_SOURCE } from "./record-pages/record-page-source.js";
|
|
30
33
|
import { registerDataRecordRoutes } from "./model/data-record-routes.js";
|
|
@@ -55,8 +58,15 @@ import RecordTemplateSection from "./record-pages/record-template-section.compon
|
|
|
55
58
|
// The "Page address" field type (AGL-3475), for the schema dialog's type
|
|
56
59
|
// list and the record editor's checks.
|
|
57
60
|
Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD);
|
|
58
|
-
// A record's photo (AGL-3616), which a page's Image binds
|
|
59
|
-
|
|
61
|
+
// A record's photo (AGL-3616), which a page's Image binds, and its page
|
|
62
|
+
// (AGL-3763), which a Button or Link binds — each chosen from a picker in
|
|
63
|
+
// the record editor, never typed.
|
|
64
|
+
Aglyn.registerCustomFieldType(_extends({}, DATASET_IMAGE_FIELD, {
|
|
65
|
+
Input: DatasetImageFieldInput
|
|
66
|
+
}));
|
|
67
|
+
Aglyn.registerCustomFieldType(_extends({}, DATASET_PAGE_FIELD, {
|
|
68
|
+
Input: DatasetPageFieldInput
|
|
69
|
+
}));
|
|
60
70
|
// A record template draws on the canvas for one of its records (AGL-3475).
|
|
61
71
|
registerPageRecordSource(RECORD_PAGE_SOURCE);
|
|
62
72
|
// The workspace's datasets, for another plugin's picker (AGL-3080).
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.ts"],"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\nimport HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { registerPageRecordSource } from '@aglyn/aglyn/app-utils/page-record-sources'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { DATASET_TRANSFER_RESOURCE } from './transfer/dataset-transfer-key'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\nimport { DATASET_IMAGE_FIELD } from './model/dataset-image-field'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { RECORD_PAGE_SOURCE } from './record-pages/record-page-source'\nimport { registerDataRecordRoutes } from './model/data-record-routes'\nimport RecordTemplateRowChip from './record-pages/record-template-row-chip.component'\nimport RecordTemplateSection from './record-pages/record-template-section.component'\n\n/** Code-split: the Data console page only loads when opened. */\nconst DataConsolePage = lazy(() => import('./components/data-console-page'))\n\n/**\n * Data feature plugin (AGL-395). Console-only — datasets are org-shared\n * document collections consumed by repeatable components at render time\n * through binding resolution, not a canvas element of their own, so there\n * is no UI bundle. The console half declares the host Data nav + page\n * through the ConsoleExtension registry, gated by the `dataStore`\n * entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route\n * that imports {@link HostDatasetsCard} directly.\n *\n * Record pages (AGL-3475) add a section to the besigner's Page Properties, a\n * chip to the Pages list's rows, and the page-record source that draws a\n * record template on the canvas for one of its records.\n *\n * It also registers datasets as a repeat source (AGL-3111), which is what puts\n * \"Repeat over dataset\" on every element's Attributes panel and draws a\n * repeat's copies from the real rows on the besigner canvas. Registered here,\n * inside the function the loader calls by name, and never by a module's own\n * load: this package declares no side effects, so a bundler deletes a module\n * imported only to run one (AGL-3025).\n */\nexport function registerDataConsole(): void {\n registerRepeatSource(DATASET_REPEAT_SOURCE)\n // The \"Page address\" field type (AGL-3475), for the schema dialog's type\n // list and the record editor's checks.\n Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // A record's photo (AGL-3616), which a page's Image binds.\n Aglyn.registerCustomFieldType(DATASET_IMAGE_FIELD)\n // A record template draws on the canvas for one of its records (AGL-3475).\n registerPageRecordSource(RECORD_PAGE_SOURCE)\n // The workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\n // …and where one is read, for a surface that links it (AGL-3616): an AI\n // job's dataset opens on the Data page.\n registerDataRecordRoutes()\n // A dataset's records in the import wizard and the export dialog\n // (AGL-3530); the Data card names the dataset when it opens them.\n registerPluginTransferResourceUi(\n DATASET_TRANSFER_RESOURCE,\n // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).\n { label: 'Dataset records', icon: { path: mdiDatabaseOutline.path }, instancesFrom: 'dataset' },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Org datasets card (AGL-419): org/data renders it through the\n // 'orgData' widget slot.\n widgets: [\n {\n slot: 'orgData',\n widgetId: 'data-org-datasets',\n Component: HostDatasetsCard,\n },\n // Record pages (AGL-3475): the template's settings in Page Properties,\n // and its chip in the Pages list.\n {\n slot: 'besignerPageProperties',\n widgetId: 'data-record-template',\n Component: RecordTemplateSection,\n },\n {\n slot: 'hostScreenRow',\n widgetId: 'data-record-template-chip',\n Component: RecordTemplateRowChip,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Data',\n featureFlag: 'dataStore',\n navItems: [\n {\n label: 'Data',\n href: '/data',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-data',\n icon: { path: mdiDatabaseOutline.path },\n header: {\n title: 'Data',\n icon: { path: mdiDatabaseOutline.path },\n docsTopic: 'datasets',\n },\n Component: DataConsolePage,\n },\n ],\n })\n}\n\nexport { default as HostDatasetsCard } from './components/host-datasets-card.component'\nexport type { HostDatasetsCardProps } from './components/host-datasets-card.component'\n"],"names":["HostDatasetsCard","Aglyn","registerRepeatSource","mdiDatabaseOutline","lazy","BUNDLE_ID","registerDatasetRecordList","registerPageRecordSource","registerPluginTransferResourceUi","DATASET_TRANSFER_RESOURCE","DATASET_REPEAT_SOURCE","DATASET_IMAGE_FIELD","RECORD_PAGE_ADDRESS_FIELD","RECORD_PAGE_SOURCE","registerDataRecordRoutes","RecordTemplateRowChip","RecordTemplateSection","DataConsolePage","registerDataConsole","registerCustomFieldType","label","icon","path","instancesFrom","pluginId","registerConsoleExtension","widgets","slot","widgetId","Component","displayName","featureFlag","navItems","href","navTabId","header","title","docsTopic","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,OAAOA,sBAAsB,+CAA2C;AACxE,YAAYC,WAAW,eAAc;AACrC,SAASC,oBAAoB,QAAQ,wCAAuC;AAC5E,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,wBAAwB,QAAQ,6CAA4C;AACrF,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,yBAAyB,QAAQ,qCAAiC;AAC3E,SAASC,qBAAqB,QAAQ,oCAAgC;AACtE,SAASC,mBAAmB,QAAQ,iCAA6B;AACjE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,kBAAkB,QAAQ,uCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA4B;AACrE,OAAOC,2BAA2B,uDAAmD;AACrF,OAAOC,2BAA2B,sDAAkD;AAEpF,8DAA8D,GAC9D,MAAMC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.ts"],"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\nimport HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { registerPageRecordSource } from '@aglyn/aglyn/app-utils/page-record-sources'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { DATASET_TRANSFER_RESOURCE } from './transfer/dataset-transfer-key'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\nimport { DATASET_IMAGE_FIELD } from './model/dataset-image-field'\nimport { DATASET_PAGE_FIELD } from './model/dataset-page-field'\nimport { DatasetImageFieldInput, DatasetPageFieldInput } from './components/dataset-field-inputs.component'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { RECORD_PAGE_SOURCE } from './record-pages/record-page-source'\nimport { registerDataRecordRoutes } from './model/data-record-routes'\nimport RecordTemplateRowChip from './record-pages/record-template-row-chip.component'\nimport RecordTemplateSection from './record-pages/record-template-section.component'\n\n/** Code-split: the Data console page only loads when opened. */\nconst DataConsolePage = lazy(() => import('./components/data-console-page'))\n\n/**\n * Data feature plugin (AGL-395). Console-only — datasets are org-shared\n * document collections consumed by repeatable components at render time\n * through binding resolution, not a canvas element of their own, so there\n * is no UI bundle. The console half declares the host Data nav + page\n * through the ConsoleExtension registry, gated by the `dataStore`\n * entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route\n * that imports {@link HostDatasetsCard} directly.\n *\n * Record pages (AGL-3475) add a section to the besigner's Page Properties, a\n * chip to the Pages list's rows, and the page-record source that draws a\n * record template on the canvas for one of its records.\n *\n * It also registers datasets as a repeat source (AGL-3111), which is what puts\n * \"Repeat over dataset\" on every element's Attributes panel and draws a\n * repeat's copies from the real rows on the besigner canvas. Registered here,\n * inside the function the loader calls by name, and never by a module's own\n * load: this package declares no side effects, so a bundler deletes a module\n * imported only to run one (AGL-3025).\n */\nexport function registerDataConsole(): void {\n registerRepeatSource(DATASET_REPEAT_SOURCE)\n // The \"Page address\" field type (AGL-3475), for the schema dialog's type\n // list and the record editor's checks.\n Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // A record's photo (AGL-3616), which a page's Image binds, and its page\n // (AGL-3763), which a Button or Link binds — each chosen from a picker in\n // the record editor, never typed.\n Aglyn.registerCustomFieldType({ ...DATASET_IMAGE_FIELD, Input: DatasetImageFieldInput })\n Aglyn.registerCustomFieldType({ ...DATASET_PAGE_FIELD, Input: DatasetPageFieldInput })\n // A record template draws on the canvas for one of its records (AGL-3475).\n registerPageRecordSource(RECORD_PAGE_SOURCE)\n // The workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\n // …and where one is read, for a surface that links it (AGL-3616): an AI\n // job's dataset opens on the Data page.\n registerDataRecordRoutes()\n // A dataset's records in the import wizard and the export dialog\n // (AGL-3530); the Data card names the dataset when it opens them.\n registerPluginTransferResourceUi(\n DATASET_TRANSFER_RESOURCE,\n // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).\n { label: 'Dataset records', icon: { path: mdiDatabaseOutline.path }, instancesFrom: 'dataset' },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Org datasets card (AGL-419): org/data renders it through the\n // 'orgData' widget slot.\n widgets: [\n {\n slot: 'orgData',\n widgetId: 'data-org-datasets',\n Component: HostDatasetsCard,\n },\n // Record pages (AGL-3475): the template's settings in Page Properties,\n // and its chip in the Pages list.\n {\n slot: 'besignerPageProperties',\n widgetId: 'data-record-template',\n Component: RecordTemplateSection,\n },\n {\n slot: 'hostScreenRow',\n widgetId: 'data-record-template-chip',\n Component: RecordTemplateRowChip,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Data',\n featureFlag: 'dataStore',\n navItems: [\n {\n label: 'Data',\n href: '/data',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-data',\n icon: { path: mdiDatabaseOutline.path },\n header: {\n title: 'Data',\n icon: { path: mdiDatabaseOutline.path },\n docsTopic: 'datasets',\n },\n Component: DataConsolePage,\n },\n ],\n })\n}\n\nexport { default as HostDatasetsCard } from './components/host-datasets-card.component'\nexport type { HostDatasetsCardProps } from './components/host-datasets-card.component'\n"],"names":["HostDatasetsCard","Aglyn","registerRepeatSource","mdiDatabaseOutline","lazy","BUNDLE_ID","registerDatasetRecordList","registerPageRecordSource","registerPluginTransferResourceUi","DATASET_TRANSFER_RESOURCE","DATASET_REPEAT_SOURCE","DATASET_IMAGE_FIELD","DATASET_PAGE_FIELD","DatasetImageFieldInput","DatasetPageFieldInput","RECORD_PAGE_ADDRESS_FIELD","RECORD_PAGE_SOURCE","registerDataRecordRoutes","RecordTemplateRowChip","RecordTemplateSection","DataConsolePage","registerDataConsole","registerCustomFieldType","Input","label","icon","path","instancesFrom","pluginId","registerConsoleExtension","widgets","slot","widgetId","Component","displayName","featureFlag","navItems","href","navTabId","header","title","docsTopic","default"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,OAAOA,sBAAsB,+CAA2C;AACxE,YAAYC,WAAW,eAAc;AACrC,SAASC,oBAAoB,QAAQ,wCAAuC;AAC5E,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,wBAAwB,QAAQ,6CAA4C;AACrF,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,yBAAyB,QAAQ,qCAAiC;AAC3E,SAASC,qBAAqB,QAAQ,oCAAgC;AACtE,SAASC,mBAAmB,QAAQ,iCAA6B;AACjE,SAASC,kBAAkB,QAAQ,gCAA4B;AAC/D,SAASC,sBAAsB,EAAEC,qBAAqB,QAAQ,iDAA6C;AAC3G,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,kBAAkB,QAAQ,uCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA4B;AACrE,OAAOC,2BAA2B,uDAAmD;AACrF,OAAOC,2BAA2B,sDAAkD;AAEpF,8DAA8D,GAC9D,MAAMC,kBAAkBhB,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASiB;IACdnB,qBAAqBQ;IACrB,yEAAyE;IACzE,uCAAuC;IACvCT,MAAMqB,uBAAuB,CAACP;IAC9B,wEAAwE;IACxE,0EAA0E;IAC1E,kCAAkC;IAClCd,MAAMqB,uBAAuB,CAAC,aAAKX;QAAqBY,OAAOV;;IAC/DZ,MAAMqB,uBAAuB,CAAC,aAAKV;QAAoBW,OAAOT;;IAC9D,2EAA2E;IAC3EP,yBAAyBS;IACzB,oEAAoE;IACpEV;IACA,wEAAwE;IACxE,wCAAwC;IACxCW;IACA,iEAAiE;IACjE,kEAAkE;IAClET,iCACEC,2BACA,wFAAwF;IACxF;QAAEe,OAAO;QAAmBC,MAAM;YAAEC,MAAMvB,mBAAmBuB,IAAI;QAAC;QAAGC,eAAe;IAAU,GAC9F;QAAEC,UAAUvB;IAAU;IAExBJ,MAAM4B,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAWjC;YACb;YACA,uEAAuE;YACvE,kCAAkC;YAClC;gBACE+B,MAAM;gBACNC,UAAU;gBACVC,WAAWd;YACb;YACA;gBACEY,MAAM;gBACNC,UAAU;gBACVC,WAAWf;YACb;SACD;QACDU,UAAUvB;QACV6B,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEZ,OAAO;gBACPa,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVb,MAAM;oBAAEC,MAAMvB,mBAAmBuB,IAAI;gBAAC;gBACtCa,QAAQ;oBACNC,OAAO;oBACPf,MAAM;wBAAEC,MAAMvB,mBAAmBuB,IAAI;oBAAC;oBACtCe,WAAW;gBACb;gBACAR,WAAWb;YACb;SACD;IACH;AACF;AAEA,SAASsB,WAAW1C,gBAAgB,QAAQ,+CAA2C"}
|
|
@@ -62,7 +62,9 @@ const TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES);
|
|
|
62
62
|
fields.forEach((field, index)=>{
|
|
63
63
|
var _values_index;
|
|
64
64
|
const value = (_values_index = values[index]) != null ? _values_index : '';
|
|
65
|
-
|
|
65
|
+
// A page is picked by the owner from the site's pages (AGL-3763); the
|
|
66
|
+
// planner has no page ids to give, so it never seeds one.
|
|
67
|
+
if (!value || field.type === 'page') return;
|
|
66
68
|
record[field.name] = field.type === 'list' ? value.split(LIST_SEPARATOR).filter(Boolean) : value;
|
|
67
69
|
});
|
|
68
70
|
return record;
|
|
@@ -94,7 +96,7 @@ export const datasetAiCapability = {
|
|
|
94
96
|
},
|
|
95
97
|
fields: {
|
|
96
98
|
type: 'array',
|
|
97
|
-
description: `Each field as Name:type, type one of ${TYPES.join('|')}; a date or time is text, written the way a visitor reads it. The first text field names each record.`,
|
|
99
|
+
description: `Each field as Name:type, type one of ${TYPES.join('|')}; a date or time is text, written the way a visitor reads it; a page field links each record to one of the site's pages, which the owner picks, so leave its values empty. The first text field names each record.`,
|
|
98
100
|
items: {
|
|
99
101
|
type: 'string',
|
|
100
102
|
maxLength: DATASET_DRAFT_FIELD_NAME_MAX + 10
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-ai-capability.ts"],"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\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n DATASET_DRAFT_FIELD_NAME_MAX,\n DATASET_DRAFT_FIELD_TYPES,\n DATASET_DRAFT_FIELDS_MAX,\n DATASET_DRAFT_NAME_MAX,\n DATASET_DRAFT_RECORDS_MAX,\n DATASET_DRAFT_RESOURCE,\n type DatasetDraftFieldType,\n} from './dataset-drafts'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN DATA (AGL-3616): a dataset with typed fields\n * and its first records.\n *\n * The capability the AI plugin's build planner offers for \"add a menu page\"\n * or \"list our team\": structured content kept as records rather than written\n * into a page, so the owner edits one row and every page listing it follows.\n * Its item is written by this plugin's `dataset` draft writer, so every rule\n * is the writer's: `dataStore` (Starter and up), `datasetsPerOrg`,\n * `release_data_store`, the member's role, and the model every record is\n * held to.\n *\n * The arguments are flat, as the contract requires: each field as\n * `Name:type`, and each record as its values in field order joined by ` | `,\n * which `draftContent` turns into the writer's records. The planner fills\n * them in, so no model runs (`estimateCredits` 0).\n */\n\n/** The operation's name in a build plan. */\nexport const DATASET_AI_OP = 'dataset'\n\n/** What separates one value of a record from the next in its argument. */\nexport const DATASET_AI_VALUE_SEPARATOR = ' | '\n\n/** What separates the items of a list value inside one record value. */\nconst LIST_SEPARATOR = /\\s*;\\s*/\n\n/** The longest one record's argument may be. */\nconst DATASET_AI_RECORD_MAX = 600\n\nconst TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES) as DatasetDraftFieldType[]\n\n/** `\"Price:number\"` → `{ name: 'Price', type: 'number' }`; a type it does not know reads as text. */\nexport function datasetAiFieldOf(spec: string): { name: string; type: DatasetDraftFieldType } {\n const at = spec.lastIndexOf(':')\n const type = at > 0 ? spec.slice(at + 1).trim().toLowerCase() : ''\n if (at > 0 && (TYPES as readonly string[]).includes(type)) {\n return { name: spec.slice(0, at).trim(), type: type as DatasetDraftFieldType }\n }\n return { name: spec.trim(), type: 'text' }\n}\n\n/** The writer's content for an item. Pure. */\nexport function datasetDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n const specs = Array.isArray(args['fields']) ? (args['fields'] as readonly string[]) : []\n const fields = specs.map(datasetAiFieldOf).filter((field) => field.name)\n const rows = Array.isArray(args['records']) ? (args['records'] as readonly string[]) : []\n const records = rows.map((row) => {\n const values = row.split(DATASET_AI_VALUE_SEPARATOR.trim()).map((value) => value.trim())\n const record: Record<string, unknown> = {}\n fields.forEach((field, index) => {\n const value = values[index] ?? ''\n if (!value) return\n record[field.name] = field.type === 'list' ? value.split(LIST_SEPARATOR).filter(Boolean) : value\n })\n return record\n })\n const address = typeof args['pageAddressFrom'] === 'string' ? args['pageAddressFrom'].trim() : ''\n return {\n ...(typeof args['name'] === 'string' ? { name: args['name'] } : {}),\n fields,\n records: records.filter((record) => Object.keys(record).length),\n ...(address ? { pageAddressFrom: address } : {}),\n }\n}\n\nexport const datasetAiCapability: PluginAiCapability = {\n op: DATASET_AI_OP,\n noun: 'dataset',\n where: 'Data, as a dataset with its first records',\n intents: [\n 'a list of like things kept as records the owner edits in one place — a menu, a team, services, portfolio pieces, events, FAQs, locations — with typed fields and its first records from the request',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'What the dataset is called, e.g. \"Menu\" or \"Team\".', maxLength: DATASET_DRAFT_NAME_MAX },\n fields: {\n type: 'array',\n description: `Each field as Name:type, type one of ${TYPES.join('|')}; a date or time is text, written the way a visitor reads it. The first text field names each record.`,\n items: { type: 'string', maxLength: DATASET_DRAFT_FIELD_NAME_MAX + 10 },\n maxItems: DATASET_DRAFT_FIELDS_MAX,\n },\n records: {\n type: 'array',\n description:\n `The first records, each its values in field order joined by \"${DATASET_AI_VALUE_SEPARATOR}\" (a list's items joined by \"; \"), from what the request says. ` +\n 'Leave a value empty rather than invent it: never a review, testimonial, rating, quote, price or person’s name the request does not give.',\n items: { type: 'string', maxLength: DATASET_AI_RECORD_MAX },\n maxItems: DATASET_DRAFT_RECORDS_MAX,\n },\n pageAddressFrom: {\n type: 'string',\n description: 'Only where a page shows each record at its own address: the text field its address is made from, e.g. \"Name\".',\n maxLength: DATASET_DRAFT_FIELD_NAME_MAX,\n },\n },\n required: ['name', 'fields'],\n additionalProperties: false,\n },\n maxPerPlan: 3,\n // Free includes no data store (`datasetsPerOrg: 0`).\n freeAllowed: false,\n feature: 'dataStore',\n quota: 'datasetsPerOrg',\n draftResource: DATASET_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => datasetDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console surface calls it beside the writer,\n * since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerDatasetAiCapability(): void {\n registerPluginAiCapability(datasetAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","DATASET_DRAFT_FIELD_NAME_MAX","DATASET_DRAFT_FIELD_TYPES","DATASET_DRAFT_FIELDS_MAX","DATASET_DRAFT_NAME_MAX","DATASET_DRAFT_RECORDS_MAX","DATASET_DRAFT_RESOURCE","DATASET_AI_OP","DATASET_AI_VALUE_SEPARATOR","LIST_SEPARATOR","DATASET_AI_RECORD_MAX","TYPES","Object","keys","datasetAiFieldOf","spec","at","lastIndexOf","type","slice","trim","toLowerCase","includes","name","datasetDraftContentFromArgs","args","specs","Array","isArray","fields","map","filter","field","rows","records","row","values","split","value","record","forEach","index","Boolean","address","length","pageAddressFrom","datasetAiCapability","op","noun","where","intents","argsSchema","properties","description","maxLength","join","items","maxItems","required","additionalProperties","maxPerPlan","freeAllowed","feature","quota","draftResource","estimateCredits","degrade","draftContent","item","registerDatasetAiCapability","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-ai-capability.ts"],"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\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n DATASET_DRAFT_FIELD_NAME_MAX,\n DATASET_DRAFT_FIELD_TYPES,\n DATASET_DRAFT_FIELDS_MAX,\n DATASET_DRAFT_NAME_MAX,\n DATASET_DRAFT_RECORDS_MAX,\n DATASET_DRAFT_RESOURCE,\n type DatasetDraftFieldType,\n} from './dataset-drafts'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN DATA (AGL-3616): a dataset with typed fields\n * and its first records.\n *\n * The capability the AI plugin's build planner offers for \"add a menu page\"\n * or \"list our team\": structured content kept as records rather than written\n * into a page, so the owner edits one row and every page listing it follows.\n * Its item is written by this plugin's `dataset` draft writer, so every rule\n * is the writer's: `dataStore` (Starter and up), `datasetsPerOrg`,\n * `release_data_store`, the member's role, and the model every record is\n * held to.\n *\n * The arguments are flat, as the contract requires: each field as\n * `Name:type`, and each record as its values in field order joined by ` | `,\n * which `draftContent` turns into the writer's records. The planner fills\n * them in, so no model runs (`estimateCredits` 0).\n */\n\n/** The operation's name in a build plan. */\nexport const DATASET_AI_OP = 'dataset'\n\n/** What separates one value of a record from the next in its argument. */\nexport const DATASET_AI_VALUE_SEPARATOR = ' | '\n\n/** What separates the items of a list value inside one record value. */\nconst LIST_SEPARATOR = /\\s*;\\s*/\n\n/** The longest one record's argument may be. */\nconst DATASET_AI_RECORD_MAX = 600\n\nconst TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES) as DatasetDraftFieldType[]\n\n/** `\"Price:number\"` → `{ name: 'Price', type: 'number' }`; a type it does not know reads as text. */\nexport function datasetAiFieldOf(spec: string): { name: string; type: DatasetDraftFieldType } {\n const at = spec.lastIndexOf(':')\n const type = at > 0 ? spec.slice(at + 1).trim().toLowerCase() : ''\n if (at > 0 && (TYPES as readonly string[]).includes(type)) {\n return { name: spec.slice(0, at).trim(), type: type as DatasetDraftFieldType }\n }\n return { name: spec.trim(), type: 'text' }\n}\n\n/** The writer's content for an item. Pure. */\nexport function datasetDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n const specs = Array.isArray(args['fields']) ? (args['fields'] as readonly string[]) : []\n const fields = specs.map(datasetAiFieldOf).filter((field) => field.name)\n const rows = Array.isArray(args['records']) ? (args['records'] as readonly string[]) : []\n const records = rows.map((row) => {\n const values = row.split(DATASET_AI_VALUE_SEPARATOR.trim()).map((value) => value.trim())\n const record: Record<string, unknown> = {}\n fields.forEach((field, index) => {\n const value = values[index] ?? ''\n // A page is picked by the owner from the site's pages (AGL-3763); the\n // planner has no page ids to give, so it never seeds one.\n if (!value || field.type === 'page') return\n record[field.name] = field.type === 'list' ? value.split(LIST_SEPARATOR).filter(Boolean) : value\n })\n return record\n })\n const address = typeof args['pageAddressFrom'] === 'string' ? args['pageAddressFrom'].trim() : ''\n return {\n ...(typeof args['name'] === 'string' ? { name: args['name'] } : {}),\n fields,\n records: records.filter((record) => Object.keys(record).length),\n ...(address ? { pageAddressFrom: address } : {}),\n }\n}\n\nexport const datasetAiCapability: PluginAiCapability = {\n op: DATASET_AI_OP,\n noun: 'dataset',\n where: 'Data, as a dataset with its first records',\n intents: [\n 'a list of like things kept as records the owner edits in one place — a menu, a team, services, portfolio pieces, events, FAQs, locations — with typed fields and its first records from the request',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'What the dataset is called, e.g. \"Menu\" or \"Team\".', maxLength: DATASET_DRAFT_NAME_MAX },\n fields: {\n type: 'array',\n description: `Each field as Name:type, type one of ${TYPES.join('|')}; a date or time is text, written the way a visitor reads it; a page field links each record to one of the site's pages, which the owner picks, so leave its values empty. The first text field names each record.`,\n items: { type: 'string', maxLength: DATASET_DRAFT_FIELD_NAME_MAX + 10 },\n maxItems: DATASET_DRAFT_FIELDS_MAX,\n },\n records: {\n type: 'array',\n description:\n `The first records, each its values in field order joined by \"${DATASET_AI_VALUE_SEPARATOR}\" (a list's items joined by \"; \"), from what the request says. ` +\n 'Leave a value empty rather than invent it: never a review, testimonial, rating, quote, price or person’s name the request does not give.',\n items: { type: 'string', maxLength: DATASET_AI_RECORD_MAX },\n maxItems: DATASET_DRAFT_RECORDS_MAX,\n },\n pageAddressFrom: {\n type: 'string',\n description: 'Only where a page shows each record at its own address: the text field its address is made from, e.g. \"Name\".',\n maxLength: DATASET_DRAFT_FIELD_NAME_MAX,\n },\n },\n required: ['name', 'fields'],\n additionalProperties: false,\n },\n maxPerPlan: 3,\n // Free includes no data store (`datasetsPerOrg: 0`).\n freeAllowed: false,\n feature: 'dataStore',\n quota: 'datasetsPerOrg',\n draftResource: DATASET_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => datasetDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console surface calls it beside the writer,\n * since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerDatasetAiCapability(): void {\n registerPluginAiCapability(datasetAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","DATASET_DRAFT_FIELD_NAME_MAX","DATASET_DRAFT_FIELD_TYPES","DATASET_DRAFT_FIELDS_MAX","DATASET_DRAFT_NAME_MAX","DATASET_DRAFT_RECORDS_MAX","DATASET_DRAFT_RESOURCE","DATASET_AI_OP","DATASET_AI_VALUE_SEPARATOR","LIST_SEPARATOR","DATASET_AI_RECORD_MAX","TYPES","Object","keys","datasetAiFieldOf","spec","at","lastIndexOf","type","slice","trim","toLowerCase","includes","name","datasetDraftContentFromArgs","args","specs","Array","isArray","fields","map","filter","field","rows","records","row","values","split","value","record","forEach","index","Boolean","address","length","pageAddressFrom","datasetAiCapability","op","noun","where","intents","argsSchema","properties","description","maxLength","join","items","maxItems","required","additionalProperties","maxPerPlan","freeAllowed","feature","quota","draftResource","estimateCredits","degrade","draftContent","item","registerDatasetAiCapability","pluginId"],"mappings":"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"}
|
|
@@ -47,6 +47,9 @@ import { type DatasetFieldType, type DatasetModel } from '../model/dataset-model
|
|
|
47
47
|
* "Image" custom type, each value an address the site's Image element
|
|
48
48
|
* shows (`isDatasetImageValue`), so a portfolio's pieces or a team keep
|
|
49
49
|
* the photo the page that lists them binds.
|
|
50
|
+
* - A PAGE is a `page` field (AGL-3763): text storage riding the "Page"
|
|
51
|
+
* custom type, each value one of the site's pages as `screen:{id}`
|
|
52
|
+
* (`isDatasetPageValue`), which a Button or Link binds.
|
|
50
53
|
*
|
|
51
54
|
* Nothing is published. The writer touches the one new dataset and its
|
|
52
55
|
* records; a page shows them only where a person (or a job's draft page)
|
|
@@ -71,7 +74,8 @@ export declare const DATASET_DRAFT_LIST_MAX = 12;
|
|
|
71
74
|
* each one is stored as. A date or a time is written as text the way a
|
|
72
75
|
* visitor reads it ("Saturday 8 November, 7 pm"): a stored timestamp renders
|
|
73
76
|
* as a number in a repeat's `{{item.<field>}}`. An image is text holding a
|
|
74
|
-
* photo's address, under the "Image" custom type (AGL-3616)
|
|
77
|
+
* photo's address, under the "Image" custom type (AGL-3616), and a page is
|
|
78
|
+
* text holding a page's reference, under the "Page" custom type (AGL-3763).
|
|
75
79
|
*/
|
|
76
80
|
export declare const DATASET_DRAFT_FIELD_TYPES: {
|
|
77
81
|
readonly text: "text";
|
|
@@ -80,6 +84,7 @@ export declare const DATASET_DRAFT_FIELD_TYPES: {
|
|
|
80
84
|
readonly boolean: "bool";
|
|
81
85
|
readonly list: "sorted";
|
|
82
86
|
readonly image: "text";
|
|
87
|
+
readonly page: "text";
|
|
83
88
|
};
|
|
84
89
|
export type DatasetDraftFieldType = keyof typeof DATASET_DRAFT_FIELD_TYPES;
|
|
85
90
|
/** The datasets route's refusals, word for word. */
|
|
@@ -26,6 +26,7 @@ import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/rel
|
|
|
26
26
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
27
27
|
import { coerceDocumentValues, datasetIntegrityFields, defaultDatasetFieldId, validateDocument } from "../model/dataset-models.js";
|
|
28
28
|
import { DATASET_IMAGE_FIELD_TYPE, isDatasetImageValue } from "../model/dataset-image-field.js";
|
|
29
|
+
import { DATASET_PAGE_FIELD_TYPE, isDatasetPageValue } from "../model/dataset-page-field.js";
|
|
29
30
|
import { RECORD_PAGE_ADDRESS_FIELD_TYPE, RECORD_PAGE_ADDRESS_MAX, uniqueRecordAddresses } from "../record-pages/record-pages.js";
|
|
30
31
|
/** The resource name this writer is registered under. */ export const DATASET_DRAFT_RESOURCE = 'dataset';
|
|
31
32
|
/** The longest dataset name, as the datasets route keeps it. */ export const DATASET_DRAFT_NAME_MAX = 120;
|
|
@@ -39,14 +40,16 @@ export const DATASET_DRAFT_FIELD_NAME_MAX = 60;
|
|
|
39
40
|
* each one is stored as. A date or a time is written as text the way a
|
|
40
41
|
* visitor reads it ("Saturday 8 November, 7 pm"): a stored timestamp renders
|
|
41
42
|
* as a number in a repeat's `{{item.<field>}}`. An image is text holding a
|
|
42
|
-
* photo's address, under the "Image" custom type (AGL-3616)
|
|
43
|
+
* photo's address, under the "Image" custom type (AGL-3616), and a page is
|
|
44
|
+
* text holding a page's reference, under the "Page" custom type (AGL-3763).
|
|
43
45
|
*/ export const DATASET_DRAFT_FIELD_TYPES = {
|
|
44
46
|
text: 'text',
|
|
45
47
|
number: 'float',
|
|
46
48
|
integer: 'int32',
|
|
47
49
|
boolean: 'bool',
|
|
48
50
|
list: 'sorted',
|
|
49
|
-
image: 'text'
|
|
51
|
+
image: 'text',
|
|
52
|
+
page: 'text'
|
|
50
53
|
};
|
|
51
54
|
const DRAFT_TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES);
|
|
52
55
|
/** The datasets route's refusals, word for word. */ export const DATASET_DRAFT_ROLE_REFUSAL = 'Editing org data requires the editor role';
|
|
@@ -123,6 +126,8 @@ function isRecord(value) {
|
|
|
123
126
|
type: DATASET_DRAFT_FIELD_TYPES[rawType]
|
|
124
127
|
}, rawType === 'image' ? {
|
|
125
128
|
customType: DATASET_IMAGE_FIELD_TYPE
|
|
129
|
+
} : {}, rawType === 'page' ? {
|
|
130
|
+
customType: DATASET_PAGE_FIELD_TYPE
|
|
126
131
|
} : {}, isRecord(raw) && raw['required'] === true ? {
|
|
127
132
|
required: true
|
|
128
133
|
} : {});
|
|
@@ -185,6 +190,10 @@ function isRecord(value) {
|
|
|
185
190
|
if ((field == null ? void 0 : field.customType) === DATASET_IMAGE_FIELD_TYPE && !errors[id] && !isDatasetImageValue(value)) {
|
|
186
191
|
errors[id] = `${field.name} is not a photo from the media library or an https link`;
|
|
187
192
|
}
|
|
193
|
+
// …and a page is one of the site's pages, by reference (AGL-3763).
|
|
194
|
+
if ((field == null ? void 0 : field.customType) === DATASET_PAGE_FIELD_TYPE && !errors[id] && !isDatasetPageValue(value)) {
|
|
195
|
+
errors[id] = `${field.name} is not one of the site’s pages`;
|
|
196
|
+
}
|
|
188
197
|
}
|
|
189
198
|
for (const error of Object.values(errors))problems.push(`Record ${index + 1}: ${error}`);
|
|
190
199
|
if (!Object.keys(values).length) problems.push(`Record ${index + 1} has no values`);
|