@aglyn/besigner-ui 1.0.0-beta.184 → 1.0.0-beta.186
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 +17 -17
- package/src/lib/components/insert-token-menu.component.d.ts +3 -1
- package/src/lib/components/insert-token-menu.component.js +5 -2
- package/src/lib/components/insert-token-menu.component.js.map +1 -1
- package/src/lib/hooks/use-insert-token-options.js +2 -1
- package/src/lib/hooks/use-insert-token-options.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/besigner-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.186",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.
|
|
30
|
-
"@aglyn/aglyn-node-renderer": "1.0.0-beta.
|
|
31
|
-
"@aglyn/besigner": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-svg-icons": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-ui-json-editor": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-dom": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.186",
|
|
29
|
+
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.186",
|
|
30
|
+
"@aglyn/aglyn-node-renderer": "1.0.0-beta.186",
|
|
31
|
+
"@aglyn/besigner": "1.0.0-beta.186",
|
|
32
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.186",
|
|
33
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.186",
|
|
34
|
+
"@aglyn/shared-data-types": "1.0.0-beta.186",
|
|
35
|
+
"@aglyn/shared-svg-icons": "1.0.0-beta.186",
|
|
36
|
+
"@aglyn/shared-ui-json-editor": "1.0.0-beta.186",
|
|
37
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.186",
|
|
38
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.186",
|
|
39
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.186",
|
|
40
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.186",
|
|
41
|
+
"@aglyn/shared-util-dom": "1.0.0-beta.186",
|
|
42
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.186",
|
|
43
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.186",
|
|
44
44
|
"@dnd-kit/core": "^6.3.1",
|
|
45
45
|
"@dnd-kit/utilities": "^3.2.2",
|
|
46
46
|
"@react-aria/utils": "^3.34.1",
|
|
@@ -77,7 +77,9 @@ export interface InsertTokenMenuProps {
|
|
|
77
77
|
* serving both the per-field {x} adornment and the legacy element-level
|
|
78
78
|
* "Insert binding" button (AGL-100). Groups render in option order with
|
|
79
79
|
* subheaders; context-dependent groups carry a hint line saying where
|
|
80
|
-
* their tokens resolve. Search filters across label, group, and
|
|
80
|
+
* their tokens resolve. Search filters across label, group, preview, and the
|
|
81
|
+
* token itself — a property is found by the name its dialog shows
|
|
82
|
+
* (`{{prop.reason}}`) as well as by its label.
|
|
81
83
|
*/
|
|
82
84
|
export declare function InsertTokenMenu(props: InsertTokenMenuProps): import("react").JSX.Element;
|
|
83
85
|
export declare namespace InsertTokenMenu {
|
|
@@ -101,7 +101,9 @@ InsertTokenAdornment.displayName = 'InsertTokenAdornment';
|
|
|
101
101
|
* serving both the per-field {x} adornment and the legacy element-level
|
|
102
102
|
* "Insert binding" button (AGL-100). Groups render in option order with
|
|
103
103
|
* subheaders; context-dependent groups carry a hint line saying where
|
|
104
|
-
* their tokens resolve. Search filters across label, group, and
|
|
104
|
+
* their tokens resolve. Search filters across label, group, preview, and the
|
|
105
|
+
* token itself — a property is found by the name its dialog shows
|
|
106
|
+
* (`{{prop.reason}}`) as well as by its label.
|
|
105
107
|
*/ export function InsertTokenMenu(props) {
|
|
106
108
|
const { anchorEl, open, onClose, options, onInsert, emptyText } = props;
|
|
107
109
|
// Hosts can have hundreds of variables (AGL-186) — filter as you type.
|
|
@@ -118,7 +120,8 @@ InsertTokenAdornment.displayName = 'InsertTokenAdornment';
|
|
|
118
120
|
return options.filter((option)=>[
|
|
119
121
|
option.label,
|
|
120
122
|
option.group,
|
|
121
|
-
option.preview
|
|
123
|
+
option.preview,
|
|
124
|
+
option.token
|
|
122
125
|
].filter(Boolean).some((text)=>String(text).toLowerCase().includes(term)));
|
|
123
126
|
}, [
|
|
124
127
|
options,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/besigner/feature/designer/src/lib/components/insert-token-menu.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\nimport { mdiCodeBraces } from '@aglyn/shared-data-mdi'\nimport { HelpTip, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n Box,\n IconButton,\n InputAdornment,\n ListSubheader,\n Menu,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { type MouseEvent, useCallback, useMemo, useRef, useState } from 'react'\n\nimport type { BindingOption } from '../contexts/binding-picker-context'\nimport { besignerDocsUrl } from '../utils/docs-help'\n\n/**\n * Insert-token building blocks (AGL-583): the {x} input adornment that\n * opens the data-placeholder picker on token-capable attribute fields, the\n * grouped/searchable picker menu itself, and the pure caret-splice helper.\n * The element props form wires them to the form state so an insert behaves\n * exactly like typing the token by hand (raw `{{...}}` stays supported).\n */\n\n/**\n * Splices `token` into `text` at the captured selection, replacing any\n * selected range — the concat/builder behavior: existing text before and\n * after the caret is preserved. A null/undefined start means the field was\n * never focused, so the token appends at the end. Returns the new value\n * and the caret position just after the inserted token.\n */\nexport function insertTokenIntoText(\n text: string,\n token: string,\n start?: number | null,\n end?: number | null,\n): { value: string; caret: number } {\n const length = text.length\n const clamp = (value: number) => Math.min(Math.max(value, 0), length)\n const at = start == null ? length : clamp(start)\n const to = end == null ? at : Math.max(clamp(end), at)\n return {\n value: `${text.slice(0, at)}${token}${text.slice(to)}`,\n caret: at + token.length,\n }\n}\n\n/** The input element + selection captured when the adornment is pressed. */\nexport interface TokenFieldCapture {\n input: HTMLInputElement | HTMLTextAreaElement | null\n start: number | null\n end: number | null\n}\n\n/**\n * Reads the adornment's sibling input and, when it holds focus, its\n * selection. Selection is only trusted while the input is focused — an\n * unfocused input reports a stale 0/0 selection, and inserting there\n * would silently prepend; appending at the end is the safer default.\n */\nexport function captureAdornmentTarget(button: HTMLElement): TokenFieldCapture {\n const root = button.closest('.MuiInputBase-root')\n const input = root?.querySelector('input, textarea') as\n | HTMLInputElement\n | HTMLTextAreaElement\n | null\n const focused = Boolean(input) && document.activeElement === input\n return {\n input,\n start: focused ? input?.selectionStart ?? null : null,\n end: focused ? input?.selectionEnd ?? null : null,\n }\n}\n\nexport interface InsertTokenAdornmentProps {\n onOpen: (anchor: HTMLElement, capture: TokenFieldCapture) => void\n}\n\n/**\n * The small {x} button rendered as an end adornment on token-capable\n * attribute inputs. The caret is captured on MOUSEDOWN — before the click\n * would move focus — and the default is prevented so the input keeps both\n * focus and its visible selection while the picker opens.\n */\nexport function InsertTokenAdornment(props: InsertTokenAdornmentProps) {\n const { onOpen } = props\n const captureRef = useRef<TokenFieldCapture | null>(null)\n\n const handleMouseDown = useCallback((event: MouseEvent<HTMLElement>) => {\n captureRef.current = captureAdornmentTarget(event.currentTarget)\n event.preventDefault()\n }, [])\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLElement>) => {\n // Keyboard activation never fires mousedown — capture on demand\n // (the input isn't focused then, so the token appends at the end).\n const capture =\n captureRef.current ?? captureAdornmentTarget(event.currentTarget)\n captureRef.current = null\n onOpen(event.currentTarget, capture)\n },\n [onOpen],\n )\n\n return (\n <InputAdornment position=\"end\">\n <IconButton\n aria-label=\"Insert data token\"\n title=\"Insert data\"\n size=\"small\"\n edge=\"end\"\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n >\n <MdiIcon path={mdiCodeBraces.path} fontSize=\"small\" />\n </IconButton>\n </InputAdornment>\n )\n}\nInsertTokenAdornment.displayName = 'InsertTokenAdornment'\n\nexport interface InsertTokenMenuProps {\n anchorEl: HTMLElement | null\n open: boolean\n onClose: () => void\n options: BindingOption[]\n onInsert: (token: string) => void\n /**\n * What the menu says when there is nothing to offer at all, as opposed to\n * nothing matching a search. A field that takes only one kind of value\n * knows where more of that kind comes from, and should say so.\n */\n emptyText?: string\n}\n\n/**\n * The grouped, searchable data-placeholder picker (AGL-583) — one menu\n * serving both the per-field {x} adornment and the legacy element-level\n * \"Insert binding\" button (AGL-100). Groups render in option order with\n * subheaders; context-dependent groups carry a hint line saying where\n * their tokens resolve. Search filters across label, group, and preview.\n */\nexport function InsertTokenMenu(props: InsertTokenMenuProps) {\n const { anchorEl, open, onClose, options, onInsert, emptyText } = props\n // Hosts can have hundreds of variables (AGL-186) — filter as you type.\n const [search, setSearch] = useState('')\n\n const handleClose = useCallback(() => {\n setSearch('')\n onClose()\n }, [onClose])\n\n const visibleOptions = useMemo(() => {\n const term = search.trim().toLowerCase()\n if (!term) return options\n return options.filter((option) =>\n [option.label, option.group, option.preview]\n .filter(Boolean)\n .some((text) => String(text).toLowerCase().includes(term)),\n )\n }, [options, search])\n\n return (\n <Menu\n anchorEl={anchorEl}\n open={open}\n onClose={handleClose}\n // Focus goes back to the edited input (with the caret placed after\n // the inserted token), never to the adornment button.\n disableRestoreFocus\n slotProps={{ paper: { sx: { width: 340, maxWidth: '90vw' } } }}\n >\n {/* One help affordance for BOTH bindings entry points (AGL-2167):\n this menu is what the per-field {x} adornment opens and what the\n element-level \"Insert binding\" button opens, so the explanation\n of what a token is belongs here rather than duplicated at each. */}\n <Box\n sx={{\n px: 1.5,\n pb: 1,\n display: 'flex',\n alignItems: 'center',\n gap: 0.5,\n }}\n >\n <TextField\n size=\"small\"\n placeholder=\"Search data…\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n onKeyDown={(event) => event.stopPropagation()}\n autoFocus\n fullWidth\n />\n <HelpTip\n title=\"Bindings\"\n excerpt=\"Insert a variable or function token. It stays a rename-safe reference, and the canvas shows the resolved value until you toggle raw tokens.\"\n href={besignerDocsUrl('bindings', '#insert-a-variable')}\n sx={{ flexShrink: 0, fontSize: '0.9em' }}\n />\n </Box>\n {visibleOptions.length === 0 ? (\n <MenuItem disabled sx={{ whiteSpace: 'normal' }}>\n {options.length === 0 && emptyText ? emptyText : 'No data matches'}\n </MenuItem>\n ) : null}\n {visibleOptions.map((option, index) => {\n const previous = visibleOptions[index - 1]\n const newGroup = option.group && option.group !== previous?.group\n return [\n newGroup ? (\n <ListSubheader\n key={`${option.group}-header`}\n sx={{ lineHeight: option.groupHint ? 1.6 : undefined, py: option.groupHint ? 0.75 : undefined }}\n >\n {option.group}\n {/* Context hint (AGL-583): where these tokens resolve. */}\n {option.groupHint ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {option.groupHint}\n </Typography>\n ) : null}\n </ListSubheader>\n ) : null,\n <MenuItem\n key={`${option.group ?? ''}-${option.token}-${index}`}\n onClick={() => {\n setSearch('')\n onInsert(option.token)\n }}\n >\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\">{option.label}</Typography>\n {/* Live value preview (AGL-262) / token description. */}\n {option.preview ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ overflowWrap: 'break-word' }}\n >\n {option.preview}\n </Typography>\n ) : null}\n </Stack>\n </MenuItem>,\n ]\n })}\n </Menu>\n )\n}\nInsertTokenMenu.displayName = 'InsertTokenMenu'\n\nexport default InsertTokenMenu\n"],"names":["mdiCodeBraces","HelpTip","MdiIcon","Box","IconButton","InputAdornment","ListSubheader","Menu","MenuItem","Stack","TextField","Typography","useCallback","useMemo","useRef","useState","besignerDocsUrl","insertTokenIntoText","text","token","start","end","length","clamp","value","Math","min","max","at","to","slice","caret","captureAdornmentTarget","button","root","closest","input","querySelector","focused","Boolean","document","activeElement","selectionStart","selectionEnd","InsertTokenAdornment","props","onOpen","captureRef","handleMouseDown","event","current","currentTarget","preventDefault","handleClick","capture","position","aria-label","title","size","edge","onMouseDown","onClick","path","fontSize","displayName","InsertTokenMenu","anchorEl","open","onClose","options","onInsert","emptyText","search","setSearch","handleClose","visibleOptions","term","trim","toLowerCase","filter","option","label","group","preview","some","String","includes","disableRestoreFocus","slotProps","paper","sx","width","maxWidth","px","pb","display","alignItems","gap","placeholder","onChange","target","onKeyDown","stopPropagation","autoFocus","fullWidth","excerpt","href","flexShrink","disabled","whiteSpace","map","index","previous","newGroup","lineHeight","groupHint","undefined","py","variant","color","component","minWidth","overflowWrap"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/besigner/feature/designer/src/lib/components/insert-token-menu.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\nimport { mdiCodeBraces } from '@aglyn/shared-data-mdi'\nimport { HelpTip, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n Box,\n IconButton,\n InputAdornment,\n ListSubheader,\n Menu,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { type MouseEvent, useCallback, useMemo, useRef, useState } from 'react'\n\nimport type { BindingOption } from '../contexts/binding-picker-context'\nimport { besignerDocsUrl } from '../utils/docs-help'\n\n/**\n * Insert-token building blocks (AGL-583): the {x} input adornment that\n * opens the data-placeholder picker on token-capable attribute fields, the\n * grouped/searchable picker menu itself, and the pure caret-splice helper.\n * The element props form wires them to the form state so an insert behaves\n * exactly like typing the token by hand (raw `{{...}}` stays supported).\n */\n\n/**\n * Splices `token` into `text` at the captured selection, replacing any\n * selected range — the concat/builder behavior: existing text before and\n * after the caret is preserved. A null/undefined start means the field was\n * never focused, so the token appends at the end. Returns the new value\n * and the caret position just after the inserted token.\n */\nexport function insertTokenIntoText(\n text: string,\n token: string,\n start?: number | null,\n end?: number | null,\n): { value: string; caret: number } {\n const length = text.length\n const clamp = (value: number) => Math.min(Math.max(value, 0), length)\n const at = start == null ? length : clamp(start)\n const to = end == null ? at : Math.max(clamp(end), at)\n return {\n value: `${text.slice(0, at)}${token}${text.slice(to)}`,\n caret: at + token.length,\n }\n}\n\n/** The input element + selection captured when the adornment is pressed. */\nexport interface TokenFieldCapture {\n input: HTMLInputElement | HTMLTextAreaElement | null\n start: number | null\n end: number | null\n}\n\n/**\n * Reads the adornment's sibling input and, when it holds focus, its\n * selection. Selection is only trusted while the input is focused — an\n * unfocused input reports a stale 0/0 selection, and inserting there\n * would silently prepend; appending at the end is the safer default.\n */\nexport function captureAdornmentTarget(button: HTMLElement): TokenFieldCapture {\n const root = button.closest('.MuiInputBase-root')\n const input = root?.querySelector('input, textarea') as\n | HTMLInputElement\n | HTMLTextAreaElement\n | null\n const focused = Boolean(input) && document.activeElement === input\n return {\n input,\n start: focused ? input?.selectionStart ?? null : null,\n end: focused ? input?.selectionEnd ?? null : null,\n }\n}\n\nexport interface InsertTokenAdornmentProps {\n onOpen: (anchor: HTMLElement, capture: TokenFieldCapture) => void\n}\n\n/**\n * The small {x} button rendered as an end adornment on token-capable\n * attribute inputs. The caret is captured on MOUSEDOWN — before the click\n * would move focus — and the default is prevented so the input keeps both\n * focus and its visible selection while the picker opens.\n */\nexport function InsertTokenAdornment(props: InsertTokenAdornmentProps) {\n const { onOpen } = props\n const captureRef = useRef<TokenFieldCapture | null>(null)\n\n const handleMouseDown = useCallback((event: MouseEvent<HTMLElement>) => {\n captureRef.current = captureAdornmentTarget(event.currentTarget)\n event.preventDefault()\n }, [])\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLElement>) => {\n // Keyboard activation never fires mousedown — capture on demand\n // (the input isn't focused then, so the token appends at the end).\n const capture =\n captureRef.current ?? captureAdornmentTarget(event.currentTarget)\n captureRef.current = null\n onOpen(event.currentTarget, capture)\n },\n [onOpen],\n )\n\n return (\n <InputAdornment position=\"end\">\n <IconButton\n aria-label=\"Insert data token\"\n title=\"Insert data\"\n size=\"small\"\n edge=\"end\"\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n >\n <MdiIcon path={mdiCodeBraces.path} fontSize=\"small\" />\n </IconButton>\n </InputAdornment>\n )\n}\nInsertTokenAdornment.displayName = 'InsertTokenAdornment'\n\nexport interface InsertTokenMenuProps {\n anchorEl: HTMLElement | null\n open: boolean\n onClose: () => void\n options: BindingOption[]\n onInsert: (token: string) => void\n /**\n * What the menu says when there is nothing to offer at all, as opposed to\n * nothing matching a search. A field that takes only one kind of value\n * knows where more of that kind comes from, and should say so.\n */\n emptyText?: string\n}\n\n/**\n * The grouped, searchable data-placeholder picker (AGL-583) — one menu\n * serving both the per-field {x} adornment and the legacy element-level\n * \"Insert binding\" button (AGL-100). Groups render in option order with\n * subheaders; context-dependent groups carry a hint line saying where\n * their tokens resolve. Search filters across label, group, preview, and the\n * token itself — a property is found by the name its dialog shows\n * (`{{prop.reason}}`) as well as by its label.\n */\nexport function InsertTokenMenu(props: InsertTokenMenuProps) {\n const { anchorEl, open, onClose, options, onInsert, emptyText } = props\n // Hosts can have hundreds of variables (AGL-186) — filter as you type.\n const [search, setSearch] = useState('')\n\n const handleClose = useCallback(() => {\n setSearch('')\n onClose()\n }, [onClose])\n\n const visibleOptions = useMemo(() => {\n const term = search.trim().toLowerCase()\n if (!term) return options\n return options.filter((option) =>\n [option.label, option.group, option.preview, option.token]\n .filter(Boolean)\n .some((text) => String(text).toLowerCase().includes(term)),\n )\n }, [options, search])\n\n return (\n <Menu\n anchorEl={anchorEl}\n open={open}\n onClose={handleClose}\n // Focus goes back to the edited input (with the caret placed after\n // the inserted token), never to the adornment button.\n disableRestoreFocus\n slotProps={{ paper: { sx: { width: 340, maxWidth: '90vw' } } }}\n >\n {/* One help affordance for BOTH bindings entry points (AGL-2167):\n this menu is what the per-field {x} adornment opens and what the\n element-level \"Insert binding\" button opens, so the explanation\n of what a token is belongs here rather than duplicated at each. */}\n <Box\n sx={{\n px: 1.5,\n pb: 1,\n display: 'flex',\n alignItems: 'center',\n gap: 0.5,\n }}\n >\n <TextField\n size=\"small\"\n placeholder=\"Search data…\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n onKeyDown={(event) => event.stopPropagation()}\n autoFocus\n fullWidth\n />\n <HelpTip\n title=\"Bindings\"\n excerpt=\"Insert a variable or function token. It stays a rename-safe reference, and the canvas shows the resolved value until you toggle raw tokens.\"\n href={besignerDocsUrl('bindings', '#insert-a-variable')}\n sx={{ flexShrink: 0, fontSize: '0.9em' }}\n />\n </Box>\n {visibleOptions.length === 0 ? (\n <MenuItem disabled sx={{ whiteSpace: 'normal' }}>\n {options.length === 0 && emptyText ? emptyText : 'No data matches'}\n </MenuItem>\n ) : null}\n {visibleOptions.map((option, index) => {\n const previous = visibleOptions[index - 1]\n const newGroup = option.group && option.group !== previous?.group\n return [\n newGroup ? (\n <ListSubheader\n key={`${option.group}-header`}\n sx={{ lineHeight: option.groupHint ? 1.6 : undefined, py: option.groupHint ? 0.75 : undefined }}\n >\n {option.group}\n {/* Context hint (AGL-583): where these tokens resolve. */}\n {option.groupHint ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {option.groupHint}\n </Typography>\n ) : null}\n </ListSubheader>\n ) : null,\n <MenuItem\n key={`${option.group ?? ''}-${option.token}-${index}`}\n onClick={() => {\n setSearch('')\n onInsert(option.token)\n }}\n >\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\">{option.label}</Typography>\n {/* Live value preview (AGL-262) / token description. */}\n {option.preview ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ overflowWrap: 'break-word' }}\n >\n {option.preview}\n </Typography>\n ) : null}\n </Stack>\n </MenuItem>,\n ]\n })}\n </Menu>\n )\n}\nInsertTokenMenu.displayName = 'InsertTokenMenu'\n\nexport default InsertTokenMenu\n"],"names":["mdiCodeBraces","HelpTip","MdiIcon","Box","IconButton","InputAdornment","ListSubheader","Menu","MenuItem","Stack","TextField","Typography","useCallback","useMemo","useRef","useState","besignerDocsUrl","insertTokenIntoText","text","token","start","end","length","clamp","value","Math","min","max","at","to","slice","caret","captureAdornmentTarget","button","root","closest","input","querySelector","focused","Boolean","document","activeElement","selectionStart","selectionEnd","InsertTokenAdornment","props","onOpen","captureRef","handleMouseDown","event","current","currentTarget","preventDefault","handleClick","capture","position","aria-label","title","size","edge","onMouseDown","onClick","path","fontSize","displayName","InsertTokenMenu","anchorEl","open","onClose","options","onInsert","emptyText","search","setSearch","handleClose","visibleOptions","term","trim","toLowerCase","filter","option","label","group","preview","some","String","includes","disableRestoreFocus","slotProps","paper","sx","width","maxWidth","px","pb","display","alignItems","gap","placeholder","onChange","target","onKeyDown","stopPropagation","autoFocus","fullWidth","excerpt","href","flexShrink","disabled","whiteSpace","map","index","previous","newGroup","lineHeight","groupHint","undefined","py","variant","color","component","minWidth","overflowWrap"],"mappings":"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"}
|
|
@@ -31,7 +31,8 @@ import { BindingPickerContext } from "../contexts/binding-picker-context.js";
|
|
|
31
31
|
if (!COMPONENT_PROP_NAME_PATTERN.test((_ref = prop == null ? void 0 : prop.name) != null ? _ref : '')) continue;
|
|
32
32
|
options.push({
|
|
33
33
|
group: 'Properties',
|
|
34
|
-
groupHint: owner === 'layout' ? 'Set per screen in the Screen Properties of each screen using this layout' :
|
|
34
|
+
groupHint: owner === 'layout' ? 'Set per screen in the Screen Properties of each screen using this layout' : // (AGL-3287), and its values are set in each email it is in.
|
|
35
|
+
'Set in the Attributes panel wherever this component is used',
|
|
35
36
|
label: prop.label || prop.name,
|
|
36
37
|
token: `{{${COMPONENT_PROP_TOKEN_PREFIX}${prop.name}}}`,
|
|
37
38
|
preview: componentPropDefaultPreview(prop)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/besigner/feature/designer/src/lib/hooks/use-insert-token-options.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n AUTHOR_TOKEN_CATALOG,\n canvas,\n COLLECTION_ENTRIES_COMPONENT_ID,\n COLLECTION_TOKEN_CATALOG,\n COMPONENT_PROP_NAME_PATTERN,\n COMPONENT_PROP_TOKEN_PREFIX,\n datasetItemToken,\n EntityPickerContext,\n entityValueNeedsResolution,\n ENTRY_TOKEN_CATALOG,\n readYesNoValue,\n reusablePropDefaultValue,\n reusablePropHasAnswers,\n reusablePropValueClass,\n} from '@aglyn/aglyn'\nimport { useContext, useEffect, useMemo } from 'react'\n\nimport {\n BindingPickerContext,\n type BindingOption,\n} from '../contexts/binding-picker-context'\nimport type { TokenLabelContext } from '../utils/token-segments'\n\nexport interface InsertTokenOptionsResult {\n /**\n * The full grouped picker list (AGL-583): host bindings first (AGL-100),\n * then the data-placeholder catalogs. Entry/Collection stay browsable\n * even out of context — with a hint saying where they resolve — while\n * the Dataset item group only appears inside a resolvable repeatable.\n */\n options: BindingOption[]\n /** Display-name resolution inputs for pill rendering (AGL-586). */\n labelContext: TokenLabelContext\n}\n\n/**\n * A component's or layout's own declared props as picker options (AGL-1335).\n * A free-text field offers every one; a field with no text box is handed only\n * the properties it can be bound to (see `withPropertyBinding`).\n *\n * Names are filtered by the SAME pattern the graft requires, so a picker can\n * never offer a token that would not substitute.\n */\nexport function componentPropBindingOptions(\n componentProps: readonly Aglyn.ReusableComponentProp[] | undefined | null,\n owner: 'component' | 'layout' = 'component',\n): BindingOption[] {\n const options: BindingOption[] = []\n for (const prop of componentProps ?? []) {\n if (!COMPONENT_PROP_NAME_PATTERN.test(prop?.name ?? '')) continue\n options.push({\n group: 'Properties',\n groupHint:\n owner === 'layout'\n ? 'Set per screen in the Screen Properties of each screen using this layout'\n : 'Set per page in the Attributes panel of each instance',\n label: prop.label || prop.name,\n token: `{{${COMPONENT_PROP_TOKEN_PREFIX}${prop.name}}}`,\n preview: componentPropDefaultPreview(prop),\n })\n }\n return options\n}\n\n/** The picker's second line for a property: what a page that sets nothing gets. */\nexport function componentPropDefaultPreview(\n prop: Aglyn.ReusableComponentProp,\n): string {\n const valueClass = reusablePropValueClass(prop)\n if (valueClass === 'boolean') {\n // A yes/no has no \"nothing\": unset with no default substitutes `''`,\n // which every Yes / no reader takes for a no.\n return readYesNoValue(prop.defaultValue) === true\n ? 'Defaults to Yes'\n : 'Defaults to No'\n }\n if (valueClass === 'icon') {\n // An icon id is not something to read out; the picker shows the icon.\n return prop.defaultValue\n ? 'Defaults to the icon picked in Properties'\n : 'No default — shows no icon until a page picks one'\n }\n const fallback = reusablePropDefaultValue(prop)\n if (reusablePropHasAnswers(prop)) {\n // Named by the label a page author picks, not the value a field gets.\n const chosen = (Array.isArray(fallback) ? fallback : [fallback])\n .map((value) => {\n const answer = (prop.options ?? []).find(\n (option) => option?.value && option.value === value,\n )\n return answer ? answer.label || answer.value : undefined\n })\n .filter(Boolean)\n return chosen.length\n ? `Defaults to ${chosen.join(', ')}`\n : 'No default — the field keeps its own until a page chooses'\n }\n if (fallback === undefined) {\n return valueClass === 'text'\n ? 'No default — renders as nothing until a page sets it'\n : 'No default — the field keeps its own until a page sets it'\n }\n return `Defaults to \"${Array.isArray(fallback) ? fallback.join(', ') : String(fallback)}\"`\n}\n\n/**\n * Assembles the insert-picker options for a canvas node from ITS context\n * (AGL-583, extracted for AGL-586 so the attributes panel and the inline\n * text editor share one walk): the ancestor chain is scanned for a\n * repeatable container (dataset-item tokens need its model fields) and for\n * a Collection entries block (entry tokens resolve right there, not only\n * on entry pages). `repeatDataset` persists a dataset id OR a legacy\n * display name — either resolves against the entity picker options.\n */\nexport function useInsertTokenOptions(\n node?: Aglyn.NodeSchema<any> | null,\n): InsertTokenOptionsResult {\n const {\n options: bindingOptions,\n variables: bindingVariables,\n functions: bindingFunctions,\n componentProps,\n componentPropsOwner,\n } = useContext(BindingPickerContext)\n const entityOptions = useContext(EntityPickerContext)\n\n const insertContext = useMemo(() => {\n const nodes = (canvas.toJSON().nodes ?? {}) as Record<string, any>\n let repeatDatasetKey: string | undefined\n let inCollectionEntries = false\n let current = node?.$id ? nodes[node.$id] : undefined\n for (let hops = 0; current && hops < 100; hops += 1) {\n const props = current.props ?? {}\n if (\n !repeatDatasetKey &&\n typeof props.repeatDataset === 'string' &&\n props.repeatDataset.trim()\n ) {\n repeatDatasetKey = props.repeatDataset.trim()\n }\n if (current.componentId === COLLECTION_ENTRIES_COMPONENT_ID) {\n inCollectionEntries = true\n }\n current = current.parentId ? nodes[current.parentId] : undefined\n }\n const datasets = entityOptions.datasets ?? []\n /*\n * The browse window is a PAGE of the org's datasets, so a repeat bound to\n * one outside it matches nothing here — and this is where the token menu\n * would silently stop offering that dataset's fields on exactly the\n * orgs with enough datasets to need them. A resolution counts as a match,\n * which is the same keyed read the attributes panel already spends on a\n * picker's stored value.\n */\n const dataset = repeatDatasetKey\n ? datasets.find((candidate) => candidate.id === repeatDatasetKey) ??\n datasets.find((candidate) => candidate.label === repeatDatasetKey) ??\n entityOptions.resolved?.datasets?.[repeatDatasetKey] ??\n undefined\n : undefined\n return {\n inCollectionEntries,\n datasetLabel: dataset?.label,\n datasetFields: dataset\n ? entityOptions.datasetFields?.[dataset.id] ?? []\n : [],\n // Carried out of the walk so the effect below can ask for the dataset\n // list without repeating it.\n repeatDatasetKey,\n }\n }, [\n node,\n entityOptions.datasets,\n entityOptions.datasetFields,\n entityOptions.resolved,\n ])\n\n /**\n * Ask for the dataset list, and only when this node is inside a repeat\n * bound to one (AGL-703).\n *\n * This hook runs on every selection — the props form and the inline text\n * editor both call it — so requesting unconditionally would put the\n * provider back to reading datasets on every click. `repeatDatasetKey` is\n * the precise condition: without it there is no dataset to name and the\n * token menu offers none.\n */\n const requestEntities = entityOptions.request\n const resolveEntity = entityOptions.resolve\n useEffect(() => {\n const key = insertContext.repeatDatasetKey\n if (!key) return\n requestEntities?.('datasets')\n // Nothing to look up once the dataset is named. `repeatDataset` may hold\n // a legacy DISPLAY NAME, which the label match above answers from the\n // window — reading a document at that name as though it were an id would\n // be a read that could only ever miss.\n if (insertContext.datasetLabel) return\n const id = entityValueNeedsResolution(entityOptions, 'datasets', key)\n if (id) resolveEntity?.('datasets', id)\n }, [\n requestEntities,\n resolveEntity,\n entityOptions,\n insertContext.repeatDatasetKey,\n insertContext.datasetLabel,\n ])\n\n const options = useMemo(() => {\n const assembled: BindingOption[] = [...(bindingOptions ?? [])]\n /**\n * The component's own declared props (AGL-1335), first because they are\n * the nearest scope — inside a component definition, `{{prop.x}}` is\n * what the author reaches for most.\n *\n * They were absent from this picker entirely, so binding one meant\n * typing `{{prop.name}}` by hand while a `{}` button sat next to the\n * field implying otherwise.\n */\n assembled.push(\n ...componentPropBindingOptions(componentProps, componentPropsOwner),\n )\n const entryHint = insertContext.inCollectionEntries\n ? undefined\n : 'Resolves in Collection entries blocks and on entry pages'\n for (const entry of ENTRY_TOKEN_CATALOG) {\n assembled.push({\n group: 'Entry',\n label: entry.label,\n token: entry.token,\n preview: entry.description,\n ...(entryHint ? { groupHint: entryHint } : {}),\n })\n }\n for (const entry of COLLECTION_TOKEN_CATALOG) {\n assembled.push({\n group: 'Collection',\n label: entry.label,\n token: entry.token,\n preview: entry.description,\n groupHint: 'Resolves on collection pages',\n })\n }\n for (const entry of AUTHOR_TOKEN_CATALOG) {\n assembled.push({\n group: 'Author',\n label: entry.label,\n token: entry.token,\n preview: entry.description,\n groupHint: 'Resolves on author pages',\n })\n }\n for (const field of insertContext.datasetFields) {\n assembled.push({\n group: 'Dataset item',\n label: field.label,\n token: datasetItemToken(field.id),\n ...(insertContext.datasetLabel\n ? { groupHint: `From dataset \"${insertContext.datasetLabel}\"` }\n : {}),\n })\n }\n return assembled\n }, [bindingOptions, componentProps, componentPropsOwner, insertContext])\n\n const labelContext = useMemo<TokenLabelContext>(\n () => ({\n options,\n variables: bindingVariables as TokenLabelContext['variables'],\n functions: bindingFunctions as TokenLabelContext['functions'],\n datasetFields: insertContext.datasetFields,\n }),\n [options, bindingVariables, bindingFunctions, insertContext.datasetFields],\n )\n\n return useMemo(\n () => ({ options, labelContext }),\n [options, labelContext],\n )\n}\n\nexport default useInsertTokenOptions\n"],"names":["AUTHOR_TOKEN_CATALOG","canvas","COLLECTION_ENTRIES_COMPONENT_ID","COLLECTION_TOKEN_CATALOG","COMPONENT_PROP_NAME_PATTERN","COMPONENT_PROP_TOKEN_PREFIX","datasetItemToken","EntityPickerContext","entityValueNeedsResolution","ENTRY_TOKEN_CATALOG","readYesNoValue","reusablePropDefaultValue","reusablePropHasAnswers","reusablePropValueClass","useContext","useEffect","useMemo","BindingPickerContext","componentPropBindingOptions","componentProps","owner","options","prop","test","name","push","group","groupHint","label","token","preview","componentPropDefaultPreview","valueClass","defaultValue","fallback","chosen","Array","isArray","map","value","answer","find","option","undefined","filter","Boolean","length","join","String","useInsertTokenOptions","node","bindingOptions","variables","bindingVariables","functions","bindingFunctions","componentPropsOwner","entityOptions","insertContext","datasets","nodes","toJSON","repeatDatasetKey","inCollectionEntries","current","$id","hops","props","repeatDataset","trim","componentId","parentId","dataset","candidate","id","resolved","datasetLabel","datasetFields","requestEntities","request","resolveEntity","resolve","key","assembled","entryHint","entry","description","field","labelContext"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/besigner/feature/designer/src/lib/hooks/use-insert-token-options.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 * as Aglyn from '@aglyn/aglyn'\nimport {\n AUTHOR_TOKEN_CATALOG,\n canvas,\n COLLECTION_ENTRIES_COMPONENT_ID,\n COLLECTION_TOKEN_CATALOG,\n COMPONENT_PROP_NAME_PATTERN,\n COMPONENT_PROP_TOKEN_PREFIX,\n datasetItemToken,\n EntityPickerContext,\n entityValueNeedsResolution,\n ENTRY_TOKEN_CATALOG,\n readYesNoValue,\n reusablePropDefaultValue,\n reusablePropHasAnswers,\n reusablePropValueClass,\n} from '@aglyn/aglyn'\nimport { useContext, useEffect, useMemo } from 'react'\n\nimport {\n BindingPickerContext,\n type BindingOption,\n} from '../contexts/binding-picker-context'\nimport type { TokenLabelContext } from '../utils/token-segments'\n\nexport interface InsertTokenOptionsResult {\n /**\n * The full grouped picker list (AGL-583): host bindings first (AGL-100),\n * then the data-placeholder catalogs. Entry/Collection stay browsable\n * even out of context — with a hint saying where they resolve — while\n * the Dataset item group only appears inside a resolvable repeatable.\n */\n options: BindingOption[]\n /** Display-name resolution inputs for pill rendering (AGL-586). */\n labelContext: TokenLabelContext\n}\n\n/**\n * A component's or layout's own declared props as picker options (AGL-1335).\n * A free-text field offers every one; a field with no text box is handed only\n * the properties it can be bound to (see `withPropertyBinding`).\n *\n * Names are filtered by the SAME pattern the graft requires, so a picker can\n * never offer a token that would not substitute.\n */\nexport function componentPropBindingOptions(\n componentProps: readonly Aglyn.ReusableComponentProp[] | undefined | null,\n owner: 'component' | 'layout' = 'component',\n): BindingOption[] {\n const options: BindingOption[] = []\n for (const prop of componentProps ?? []) {\n if (!COMPONENT_PROP_NAME_PATTERN.test(prop?.name ?? '')) continue\n options.push({\n group: 'Properties',\n groupHint:\n owner === 'layout'\n ? 'Set per screen in the Screen Properties of each screen using this layout'\n : // Pages and emails alike: an email block is a component too\n // (AGL-3287), and its values are set in each email it is in.\n 'Set in the Attributes panel wherever this component is used',\n label: prop.label || prop.name,\n token: `{{${COMPONENT_PROP_TOKEN_PREFIX}${prop.name}}}`,\n preview: componentPropDefaultPreview(prop),\n })\n }\n return options\n}\n\n/** The picker's second line for a property: what a page that sets nothing gets. */\nexport function componentPropDefaultPreview(\n prop: Aglyn.ReusableComponentProp,\n): string {\n const valueClass = reusablePropValueClass(prop)\n if (valueClass === 'boolean') {\n // A yes/no has no \"nothing\": unset with no default substitutes `''`,\n // which every Yes / no reader takes for a no.\n return readYesNoValue(prop.defaultValue) === true\n ? 'Defaults to Yes'\n : 'Defaults to No'\n }\n if (valueClass === 'icon') {\n // An icon id is not something to read out; the picker shows the icon.\n return prop.defaultValue\n ? 'Defaults to the icon picked in Properties'\n : 'No default — shows no icon until a page picks one'\n }\n const fallback = reusablePropDefaultValue(prop)\n if (reusablePropHasAnswers(prop)) {\n // Named by the label a page author picks, not the value a field gets.\n const chosen = (Array.isArray(fallback) ? fallback : [fallback])\n .map((value) => {\n const answer = (prop.options ?? []).find(\n (option) => option?.value && option.value === value,\n )\n return answer ? answer.label || answer.value : undefined\n })\n .filter(Boolean)\n return chosen.length\n ? `Defaults to ${chosen.join(', ')}`\n : 'No default — the field keeps its own until a page chooses'\n }\n if (fallback === undefined) {\n return valueClass === 'text'\n ? 'No default — renders as nothing until a page sets it'\n : 'No default — the field keeps its own until a page sets it'\n }\n return `Defaults to \"${Array.isArray(fallback) ? fallback.join(', ') : String(fallback)}\"`\n}\n\n/**\n * Assembles the insert-picker options for a canvas node from ITS context\n * (AGL-583, extracted for AGL-586 so the attributes panel and the inline\n * text editor share one walk): the ancestor chain is scanned for a\n * repeatable container (dataset-item tokens need its model fields) and for\n * a Collection entries block (entry tokens resolve right there, not only\n * on entry pages). `repeatDataset` persists a dataset id OR a legacy\n * display name — either resolves against the entity picker options.\n */\nexport function useInsertTokenOptions(\n node?: Aglyn.NodeSchema<any> | null,\n): InsertTokenOptionsResult {\n const {\n options: bindingOptions,\n variables: bindingVariables,\n functions: bindingFunctions,\n componentProps,\n componentPropsOwner,\n } = useContext(BindingPickerContext)\n const entityOptions = useContext(EntityPickerContext)\n\n const insertContext = useMemo(() => {\n const nodes = (canvas.toJSON().nodes ?? {}) as Record<string, any>\n let repeatDatasetKey: string | undefined\n let inCollectionEntries = false\n let current = node?.$id ? nodes[node.$id] : undefined\n for (let hops = 0; current && hops < 100; hops += 1) {\n const props = current.props ?? {}\n if (\n !repeatDatasetKey &&\n typeof props.repeatDataset === 'string' &&\n props.repeatDataset.trim()\n ) {\n repeatDatasetKey = props.repeatDataset.trim()\n }\n if (current.componentId === COLLECTION_ENTRIES_COMPONENT_ID) {\n inCollectionEntries = true\n }\n current = current.parentId ? nodes[current.parentId] : undefined\n }\n const datasets = entityOptions.datasets ?? []\n /*\n * The browse window is a PAGE of the org's datasets, so a repeat bound to\n * one outside it matches nothing here — and this is where the token menu\n * would silently stop offering that dataset's fields on exactly the\n * orgs with enough datasets to need them. A resolution counts as a match,\n * which is the same keyed read the attributes panel already spends on a\n * picker's stored value.\n */\n const dataset = repeatDatasetKey\n ? datasets.find((candidate) => candidate.id === repeatDatasetKey) ??\n datasets.find((candidate) => candidate.label === repeatDatasetKey) ??\n entityOptions.resolved?.datasets?.[repeatDatasetKey] ??\n undefined\n : undefined\n return {\n inCollectionEntries,\n datasetLabel: dataset?.label,\n datasetFields: dataset\n ? entityOptions.datasetFields?.[dataset.id] ?? []\n : [],\n // Carried out of the walk so the effect below can ask for the dataset\n // list without repeating it.\n repeatDatasetKey,\n }\n }, [\n node,\n entityOptions.datasets,\n entityOptions.datasetFields,\n entityOptions.resolved,\n ])\n\n /**\n * Ask for the dataset list, and only when this node is inside a repeat\n * bound to one (AGL-703).\n *\n * This hook runs on every selection — the props form and the inline text\n * editor both call it — so requesting unconditionally would put the\n * provider back to reading datasets on every click. `repeatDatasetKey` is\n * the precise condition: without it there is no dataset to name and the\n * token menu offers none.\n */\n const requestEntities = entityOptions.request\n const resolveEntity = entityOptions.resolve\n useEffect(() => {\n const key = insertContext.repeatDatasetKey\n if (!key) return\n requestEntities?.('datasets')\n // Nothing to look up once the dataset is named. `repeatDataset` may hold\n // a legacy DISPLAY NAME, which the label match above answers from the\n // window — reading a document at that name as though it were an id would\n // be a read that could only ever miss.\n if (insertContext.datasetLabel) return\n const id = entityValueNeedsResolution(entityOptions, 'datasets', key)\n if (id) resolveEntity?.('datasets', id)\n }, [\n requestEntities,\n resolveEntity,\n entityOptions,\n insertContext.repeatDatasetKey,\n insertContext.datasetLabel,\n ])\n\n const options = useMemo(() => {\n const assembled: BindingOption[] = [...(bindingOptions ?? [])]\n /**\n * The component's own declared props (AGL-1335), first because they are\n * the nearest scope — inside a component definition, `{{prop.x}}` is\n * what the author reaches for most.\n *\n * They were absent from this picker entirely, so binding one meant\n * typing `{{prop.name}}` by hand while a `{}` button sat next to the\n * field implying otherwise.\n */\n assembled.push(\n ...componentPropBindingOptions(componentProps, componentPropsOwner),\n )\n const entryHint = insertContext.inCollectionEntries\n ? undefined\n : 'Resolves in Collection entries blocks and on entry pages'\n for (const entry of ENTRY_TOKEN_CATALOG) {\n assembled.push({\n group: 'Entry',\n label: entry.label,\n token: entry.token,\n preview: entry.description,\n ...(entryHint ? { groupHint: entryHint } : {}),\n })\n }\n for (const entry of COLLECTION_TOKEN_CATALOG) {\n assembled.push({\n group: 'Collection',\n label: entry.label,\n token: entry.token,\n preview: entry.description,\n groupHint: 'Resolves on collection pages',\n })\n }\n for (const entry of AUTHOR_TOKEN_CATALOG) {\n assembled.push({\n group: 'Author',\n label: entry.label,\n token: entry.token,\n preview: entry.description,\n groupHint: 'Resolves on author pages',\n })\n }\n for (const field of insertContext.datasetFields) {\n assembled.push({\n group: 'Dataset item',\n label: field.label,\n token: datasetItemToken(field.id),\n ...(insertContext.datasetLabel\n ? { groupHint: `From dataset \"${insertContext.datasetLabel}\"` }\n : {}),\n })\n }\n return assembled\n }, [bindingOptions, componentProps, componentPropsOwner, insertContext])\n\n const labelContext = useMemo<TokenLabelContext>(\n () => ({\n options,\n variables: bindingVariables as TokenLabelContext['variables'],\n functions: bindingFunctions as TokenLabelContext['functions'],\n datasetFields: insertContext.datasetFields,\n }),\n [options, bindingVariables, bindingFunctions, insertContext.datasetFields],\n )\n\n return useMemo(\n () => ({ options, labelContext }),\n [options, labelContext],\n )\n}\n\nexport default useInsertTokenOptions\n"],"names":["AUTHOR_TOKEN_CATALOG","canvas","COLLECTION_ENTRIES_COMPONENT_ID","COLLECTION_TOKEN_CATALOG","COMPONENT_PROP_NAME_PATTERN","COMPONENT_PROP_TOKEN_PREFIX","datasetItemToken","EntityPickerContext","entityValueNeedsResolution","ENTRY_TOKEN_CATALOG","readYesNoValue","reusablePropDefaultValue","reusablePropHasAnswers","reusablePropValueClass","useContext","useEffect","useMemo","BindingPickerContext","componentPropBindingOptions","componentProps","owner","options","prop","test","name","push","group","groupHint","label","token","preview","componentPropDefaultPreview","valueClass","defaultValue","fallback","chosen","Array","isArray","map","value","answer","find","option","undefined","filter","Boolean","length","join","String","useInsertTokenOptions","node","bindingOptions","variables","bindingVariables","functions","bindingFunctions","componentPropsOwner","entityOptions","insertContext","datasets","nodes","toJSON","repeatDatasetKey","inCollectionEntries","current","$id","hops","props","repeatDataset","trim","componentId","parentId","dataset","candidate","id","resolved","datasetLabel","datasetFields","requestEntities","request","resolveEntity","resolve","key","assembled","entryHint","entry","description","field","labelContext"],"mappings":"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"}
|