@ethisyscore/plugin-ui 1.127.1 → 1.128.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1608,6 +1608,205 @@ function ConnectorMappingView({
1608
1608
  ] }) })
1609
1609
  ] });
1610
1610
  }
1611
+
1612
+ // src/components/connectors/requestTemplates.ts
1613
+ var PLACEHOLDER = /^\{\{([A-Za-z_][A-Za-z0-9_.-]*)\}\}$/;
1614
+ var MAX_REQUEST_TEMPLATES = 4;
1615
+ function parseRequestTemplate(text) {
1616
+ let parsed;
1617
+ try {
1618
+ parsed = JSON.parse(text);
1619
+ } catch {
1620
+ return { value: null, error: "Not valid JSON.", placeholders: [] };
1621
+ }
1622
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1623
+ return { value: null, error: "A template must be a JSON object.", placeholders: [] };
1624
+ }
1625
+ const placeholders = [];
1626
+ const embedded = [];
1627
+ walkStrings(parsed, (value) => {
1628
+ const match = PLACEHOLDER.exec(value);
1629
+ if (match) {
1630
+ if (!placeholders.includes(match[1])) placeholders.push(match[1]);
1631
+ } else if (value.includes("{{")) {
1632
+ embedded.push(value);
1633
+ }
1634
+ });
1635
+ return {
1636
+ value: parsed,
1637
+ error: embedded.length > 0 ? `A placeholder must be the whole value, like "{{companyName}}": "${embedded[0]}".` : null,
1638
+ placeholders
1639
+ };
1640
+ }
1641
+ function walkStrings(value, onString) {
1642
+ if (typeof value === "string") {
1643
+ onString(value);
1644
+ } else if (Array.isArray(value)) {
1645
+ value.forEach((item) => walkStrings(item, onString));
1646
+ } else if (value !== null && typeof value === "object") {
1647
+ Object.values(value).forEach((item) => walkStrings(item, onString));
1648
+ }
1649
+ }
1650
+ var MAX_ROW_FIELDS_PER_FIELD = 32;
1651
+ var MAX_ROW_FIELD_TEXT_LENGTH = 256;
1652
+ function rowFieldPathError(path) {
1653
+ const trimmed = path.trim();
1654
+ if (trimmed.length === 0) return null;
1655
+ if (!trimmed.startsWith("$.")) return 'A path must start with "$.", e.g. $.company_domain.';
1656
+ if (trimmed.length > MAX_ROW_FIELD_TEXT_LENGTH) return `A path can be at most ${MAX_ROW_FIELD_TEXT_LENGTH} characters.`;
1657
+ return null;
1658
+ }
1659
+ var REQUEST_TEMPLATES_CAPTION = "The first template whose placeholders all have values is sent as the request. Leave empty to use the field map.";
1660
+ function RequestTemplatesSection({
1661
+ rows,
1662
+ canonicalRequestFields,
1663
+ onChange,
1664
+ onAdd,
1665
+ onRemove,
1666
+ onMove
1667
+ }) {
1668
+ const canonicalNames = new Set(canonicalRequestFields.map((field) => field.name));
1669
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1670
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", component: "h2", className: "mb-2", children: "Request templates" }),
1671
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", className: "text-text-secondary mb-3 block", children: REQUEST_TEMPLATES_CAPTION }),
1672
+ rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", className: "text-text-disabled italic mb-3", children: "No request templates configured." }) : /* @__PURE__ */ jsxRuntime.jsx(material.Box, { className: "flex flex-col gap-3 mb-3", children: rows.map((row, index) => {
1673
+ const parsed = row.text.trim().length === 0 ? null : parseRequestTemplate(row.text);
1674
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { className: "flex flex-col gap-1", children: [
1675
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { className: "flex flex-row items-start gap-2", children: [
1676
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { variant: "filled", size: "medium", className: "surface mb-0 flex-1", children: [
1677
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: `Template ${index + 1}` }),
1678
+ /* @__PURE__ */ jsxRuntime.jsx(
1679
+ material.FilledInput,
1680
+ {
1681
+ multiline: true,
1682
+ minRows: 4,
1683
+ value: row.text,
1684
+ onChange: (e) => onChange(index, e.target.value),
1685
+ inputProps: { "aria-label": `Request template ${index + 1}` },
1686
+ sx: { fontFamily: "monospace" }
1687
+ }
1688
+ )
1689
+ ] }),
1690
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { className: "flex flex-col", children: [
1691
+ /* @__PURE__ */ jsxRuntime.jsx(
1692
+ material.IconButton,
1693
+ {
1694
+ "aria-label": `Move template ${index + 1} up`,
1695
+ color: "inherit",
1696
+ disabled: index === 0,
1697
+ onClick: () => onMove(index, -1),
1698
+ children: /* @__PURE__ */ jsxRuntime.jsx(NiChevronUpSmall, { size: "small" })
1699
+ }
1700
+ ),
1701
+ /* @__PURE__ */ jsxRuntime.jsx(
1702
+ material.IconButton,
1703
+ {
1704
+ "aria-label": `Move template ${index + 1} down`,
1705
+ color: "inherit",
1706
+ disabled: index === rows.length - 1,
1707
+ onClick: () => onMove(index, 1),
1708
+ children: /* @__PURE__ */ jsxRuntime.jsx(NiChevronDownSmall, { size: "small" })
1709
+ }
1710
+ ),
1711
+ /* @__PURE__ */ jsxRuntime.jsx(material.IconButton, { "aria-label": `Remove template ${index + 1}`, color: "inherit", onClick: () => onRemove(index), children: /* @__PURE__ */ jsxRuntime.jsx(NiBinEmpty, { size: "small" }) })
1712
+ ] })
1713
+ ] }),
1714
+ parsed?.error && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "error", role: "alert", children: parsed.error }),
1715
+ parsed && parsed.placeholders.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(material.Box, { className: "flex flex-wrap gap-1", children: parsed.placeholders.map(
1716
+ (name) => canonicalNames.has(name) ? /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { label: `{{${name}}}`, size: "small", variant: "outlined" }, name) : /* @__PURE__ */ jsxRuntime.jsx(
1717
+ material.Chip,
1718
+ {
1719
+ label: `{{${name}}} is not a request field`,
1720
+ size: "small",
1721
+ color: "error",
1722
+ variant: "outlined"
1723
+ },
1724
+ name
1725
+ )
1726
+ ) })
1727
+ ] }, row.id);
1728
+ }) }),
1729
+ /* @__PURE__ */ jsxRuntime.jsx(
1730
+ material.Button,
1731
+ {
1732
+ variant: "outlined",
1733
+ color: "grey",
1734
+ size: "small",
1735
+ onClick: onAdd,
1736
+ disabled: rows.length >= MAX_REQUEST_TEMPLATES,
1737
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(NiPlus, { size: "small" }),
1738
+ sx: { alignSelf: "flex-start" },
1739
+ children: "Add template"
1740
+ }
1741
+ )
1742
+ ] });
1743
+ }
1744
+ function RowFieldsSection({ objectFields, rows, onChange, onAdd, onRemove }) {
1745
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: objectFields.map((field) => {
1746
+ const fieldRows = rows[field.name] ?? [];
1747
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { className: "flex flex-col gap-2 mb-3", children: [
1748
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", component: "h2", children: `Row fields \u2014 ${field.name}` }),
1749
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", className: "text-text-secondary block", children: [
1750
+ "Rename the fields inside each row of this response. Each path is read from one row, e.g.",
1751
+ " ",
1752
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "$.company_domain" }),
1753
+ "; once you list any, the fields you do not list are dropped."
1754
+ ] }),
1755
+ fieldRows.map((row, index) => /* @__PURE__ */ jsxRuntime.jsxs(material.Box, { className: "flex flex-row items-start gap-3", children: [
1756
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { variant: "filled", size: "medium", className: "surface mb-0", sx: { width: 200, flexShrink: 0 }, children: [
1757
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: "Row field" }),
1758
+ /* @__PURE__ */ jsxRuntime.jsx(
1759
+ material.FilledInput,
1760
+ {
1761
+ value: row.key,
1762
+ onChange: (e) => onChange(field.name, index, { key: e.target.value }),
1763
+ inputProps: { "aria-label": `Row field name ${index + 1} for ${field.name}`, maxLength: MAX_ROW_FIELD_TEXT_LENGTH },
1764
+ placeholder: "e.g. domain"
1765
+ }
1766
+ )
1767
+ ] }),
1768
+ /* @__PURE__ */ jsxRuntime.jsxs(material.FormControl, { variant: "filled", size: "medium", className: "surface mb-0 flex-1", children: [
1769
+ /* @__PURE__ */ jsxRuntime.jsx(material.InputLabel, { children: "JSON path in the row" }),
1770
+ /* @__PURE__ */ jsxRuntime.jsx(
1771
+ material.FilledInput,
1772
+ {
1773
+ value: row.path,
1774
+ onChange: (e) => onChange(field.name, index, { path: e.target.value }),
1775
+ inputProps: { "aria-label": `Row field path ${index + 1} for ${field.name}`, maxLength: MAX_ROW_FIELD_TEXT_LENGTH },
1776
+ error: rowFieldPathError(row.path) !== null,
1777
+ placeholder: "e.g. $.company_domain"
1778
+ }
1779
+ ),
1780
+ rowFieldPathError(row.path) && /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "error", role: "alert", children: rowFieldPathError(row.path) })
1781
+ ] }),
1782
+ /* @__PURE__ */ jsxRuntime.jsx(
1783
+ material.IconButton,
1784
+ {
1785
+ "aria-label": `Remove row field ${index + 1} for ${field.name}`,
1786
+ color: "inherit",
1787
+ onClick: () => onRemove(field.name, index),
1788
+ sx: { mt: 1 },
1789
+ children: /* @__PURE__ */ jsxRuntime.jsx(NiBinEmpty, { size: "small" })
1790
+ }
1791
+ )
1792
+ ] }, row.id)),
1793
+ /* @__PURE__ */ jsxRuntime.jsx(
1794
+ material.Button,
1795
+ {
1796
+ variant: "outlined",
1797
+ color: "grey",
1798
+ size: "small",
1799
+ onClick: () => onAdd(field.name),
1800
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(NiPlus, { size: "small" }),
1801
+ disabled: fieldRows.length >= MAX_ROW_FIELDS_PER_FIELD,
1802
+ sx: { alignSelf: "flex-start" },
1803
+ "aria-label": `Add row field to ${field.name}`,
1804
+ children: "Add row field"
1805
+ }
1806
+ )
1807
+ ] }, field.name);
1808
+ }) });
1809
+ }
1611
1810
  var SUPPRESS_REQUEST_FIELD_VALUE = "(suppress)";
1612
1811
  function isSuppressed(value) {
1613
1812
  return value?.trim().toLowerCase() === SUPPRESS_REQUEST_FIELD_VALUE;
@@ -1625,6 +1824,8 @@ function ConnectorMappingTranslationEditorView({
1625
1824
  requestFieldMap,
1626
1825
  responseFieldPaths,
1627
1826
  staticRequestFields,
1827
+ requestTemplates,
1828
+ responseItemFieldPaths,
1628
1829
  onSave,
1629
1830
  onCancel,
1630
1831
  onTestMapping,
@@ -1652,6 +1853,52 @@ function ConnectorMappingTranslationEditorView({
1652
1853
  const [rawJsonExpanded, setRawJsonExpanded] = react.useState(false);
1653
1854
  const [suggestionError, setSuggestionError] = react.useState(null);
1654
1855
  const [isSuggesting, setIsSuggesting] = react.useState(false);
1856
+ const showTemplates = requestTemplates !== void 0;
1857
+ const objectResponseFields = canonicalResponseFields.filter((field) => field.kind === "Object");
1858
+ const showRowFields = responseItemFieldPaths !== void 0 && objectResponseFields.length > 0;
1859
+ const templateRowIdRef = react.useRef(0);
1860
+ const [templateRows, setTemplateRows] = react.useState(
1861
+ () => (requestTemplates ?? []).flatMap((template) => {
1862
+ let text;
1863
+ try {
1864
+ text = JSON.stringify(template, null, 2);
1865
+ } catch {
1866
+ text = void 0;
1867
+ }
1868
+ return text === void 0 ? [] : [{ id: templateRowIdRef.current++, text }];
1869
+ })
1870
+ );
1871
+ const rowFieldIdRef = react.useRef(0);
1872
+ const [rowFieldRows, setRowFieldRows] = react.useState(
1873
+ () => Object.fromEntries(
1874
+ Object.entries(responseItemFieldPaths ?? {}).map(([field, paths]) => [
1875
+ field,
1876
+ Object.entries(paths).map(([key, path]) => ({ id: rowFieldIdRef.current++, key, path }))
1877
+ ])
1878
+ )
1879
+ );
1880
+ const hasInvalidTemplate = showTemplates && templateRows.some((row) => row.text.trim().length > 0 && parseRequestTemplate(row.text).error !== null) || showRowFields && objectResponseFields.some((field) => (rowFieldRows[field.name] ?? []).some((row) => rowFieldPathError(row.path) !== null));
1881
+ const handleTemplateChange = (index, text) => setTemplateRows((prev) => prev.map((row, i) => i === index ? { ...row, text } : row));
1882
+ const handleAddTemplate = () => setTemplateRows(
1883
+ (prev) => prev.length >= MAX_REQUEST_TEMPLATES ? prev : [...prev, { id: templateRowIdRef.current++, text: "" }]
1884
+ );
1885
+ const handleRemoveTemplate = (index) => setTemplateRows((prev) => prev.filter((_, i) => i !== index));
1886
+ const handleMoveTemplate = (index, direction) => setTemplateRows((prev) => {
1887
+ const target = index + direction;
1888
+ if (target < 0 || target >= prev.length) return prev;
1889
+ const next = [...prev];
1890
+ [next[index], next[target]] = [next[target], next[index]];
1891
+ return next;
1892
+ });
1893
+ const handleRowFieldChange = (field, index, patch) => setRowFieldRows((prev) => ({
1894
+ ...prev,
1895
+ [field]: (prev[field] ?? []).map((row, i) => i === index ? { ...row, ...patch } : row)
1896
+ }));
1897
+ const handleAddRowField = (field) => setRowFieldRows((prev) => ({
1898
+ ...prev,
1899
+ [field]: [...prev[field] ?? [], { id: rowFieldIdRef.current++, key: "", path: "" }]
1900
+ }));
1901
+ const handleRemoveRowField = (field, index) => setRowFieldRows((prev) => ({ ...prev, [field]: (prev[field] ?? []).filter((_, i) => i !== index) }));
1655
1902
  const handleRequestChange = (canonicalName, value) => {
1656
1903
  setRequestValues((prev) => ({ ...prev, [canonicalName]: value }));
1657
1904
  };
@@ -1677,11 +1924,17 @@ function ConnectorMappingTranslationEditorView({
1677
1924
  const requestMap = stripEmpty(requestValues);
1678
1925
  const responseMap = stripEmpty(responseValues);
1679
1926
  const staticMap = staticRowsToMap(staticRows);
1680
- onSave(
1681
- Object.keys(requestMap).length === 0 ? null : requestMap,
1682
- Object.keys(responseMap).length === 0 ? null : responseMap,
1683
- Object.keys(staticMap).length === 0 ? null : staticMap
1684
- );
1927
+ const request = Object.keys(requestMap).length === 0 ? null : requestMap;
1928
+ const response = Object.keys(responseMap).length === 0 ? null : responseMap;
1929
+ const statics = Object.keys(staticMap).length === 0 ? null : staticMap;
1930
+ if (!showTemplates && !showRowFields) {
1931
+ onSave(request, response, statics);
1932
+ return;
1933
+ }
1934
+ const extras = {};
1935
+ if (showTemplates) extras.requestTemplates = templateRowsToWire(templateRows);
1936
+ if (showRowFields) extras.responseItemFieldPaths = rowFieldRowsToWire(rowFieldRows, objectResponseFields);
1937
+ onSave(request, response, statics, extras);
1685
1938
  };
1686
1939
  const handleTest = async () => {
1687
1940
  if (!onTestMapping) return;
@@ -1857,6 +2110,33 @@ function ConnectorMappingTranslationEditorView({
1857
2110
  children: "Add parameter"
1858
2111
  }
1859
2112
  ),
2113
+ showTemplates && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2114
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, { className: "my-4" }),
2115
+ /* @__PURE__ */ jsxRuntime.jsx(
2116
+ RequestTemplatesSection,
2117
+ {
2118
+ rows: templateRows,
2119
+ canonicalRequestFields,
2120
+ onChange: handleTemplateChange,
2121
+ onAdd: handleAddTemplate,
2122
+ onRemove: handleRemoveTemplate,
2123
+ onMove: handleMoveTemplate
2124
+ }
2125
+ )
2126
+ ] }),
2127
+ showRowFields && objectResponseFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2128
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, { className: "my-4" }),
2129
+ /* @__PURE__ */ jsxRuntime.jsx(
2130
+ RowFieldsSection,
2131
+ {
2132
+ objectFields: objectResponseFields,
2133
+ rows: rowFieldRows,
2134
+ onChange: handleRowFieldChange,
2135
+ onAdd: handleAddRowField,
2136
+ onRemove: handleRemoveRowField
2137
+ }
2138
+ )
2139
+ ] }),
1860
2140
  (onTestMapping || onSuggestTranslation) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1861
2141
  /* @__PURE__ */ jsxRuntime.jsx(material.Divider, { className: "my-4" }),
1862
2142
  /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", component: "h2", className: "mb-2", children: "Test with sample inputs" }),
@@ -2012,6 +2292,7 @@ function ConnectorMappingTranslationEditorView({
2012
2292
  color: "primary",
2013
2293
  onClick: handleSave,
2014
2294
  "data-testid": "save-translation-button",
2295
+ disabled: hasInvalidTemplate,
2015
2296
  children: "Save"
2016
2297
  }
2017
2298
  )
@@ -2071,22 +2352,27 @@ function FieldRow({
2071
2352
  ] });
2072
2353
  }
2073
2354
  function stripEmpty(values) {
2074
- const out = {};
2075
- for (const [key, value] of Object.entries(values)) {
2076
- if (value.trim().length > 0) {
2077
- out[key] = value.trim();
2078
- }
2079
- }
2080
- return out;
2355
+ return Object.fromEntries(
2356
+ Object.entries(values).filter(([, value]) => value.trim().length > 0).map(([key, value]) => [key, value.trim()])
2357
+ );
2081
2358
  }
2082
2359
  function staticRowsToMap(rows) {
2083
- const out = {};
2084
- for (const { key, value } of rows) {
2085
- if (key.trim().length > 0) {
2086
- out[key.trim()] = value;
2087
- }
2088
- }
2089
- return out;
2360
+ return Object.fromEntries(rows.filter(({ key }) => key.trim().length > 0).map(({ key, value }) => [key.trim(), value]));
2361
+ }
2362
+ function templateRowsToWire(rows) {
2363
+ const templates = rows.filter((row) => row.text.trim().length > 0).map((row) => parseRequestTemplate(row.text).value).filter((value) => value !== null);
2364
+ return templates.length === 0 ? null : templates;
2365
+ }
2366
+ function rowFieldRowsToWire(rows, objectFields) {
2367
+ const out = Object.fromEntries(
2368
+ objectFields.map((field) => {
2369
+ const paths = Object.fromEntries(
2370
+ (rows[field.name] ?? []).filter(({ key, path }) => key.trim().length > 0 && path.trim().length > 0).map(({ key, path }) => [key.trim(), path.trim()])
2371
+ );
2372
+ return [field.name, paths];
2373
+ }).filter(([, paths]) => Object.keys(paths).length > 0)
2374
+ );
2375
+ return Object.keys(out).length === 0 ? null : out;
2090
2376
  }
2091
2377
 
2092
2378
  // src/components/connectors/mappingQueryKeys.ts