brilliant-directories-mcp 6.59.621 → 6.59.623
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/index.js +234 -14
- package/openapi/bd-api.json +13 -5
- package/openapi/design-settings-map.json +1 -1
- package/openapi/mcp-instructions.md +1 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -672,6 +672,20 @@ function pcssSplitCommas(sel) {
|
|
|
672
672
|
return parts;
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
+
// A rule whose selector ends in a bare `*` also matches a widget's own <style>/<script> tags; giving them a display prints their code on the page.
|
|
676
|
+
function pcssStarDisplay(css) {
|
|
677
|
+
const re = /([^{}]+)\{([^{}]*)\}/g;
|
|
678
|
+
let m;
|
|
679
|
+
while ((m = re.exec(css))) {
|
|
680
|
+
const sel = m[1].replace(/\s+/g, " ").trim();
|
|
681
|
+
if (sel.startsWith("@") || !/(?:^|;)\s*display\s*:\s*(?!none\b)/i.test(m[2])) continue;
|
|
682
|
+
if (pcssSplitCommas(sel).some((part) => /(?:^|[\s>+~])\*\s*$/.test(part))) {
|
|
683
|
+
return `\`${sel.slice(0, 160)}\` ends in \`*\` and sets display: that also matches a widget's own <style> and <script> tags and prints their code on the page. End it with a class, or with \`*:not(style):not(script)\`.`;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
675
689
|
// Checks 1 to 6 of the tool's contract. Returns { entries } or { refused: [messages] }.
|
|
676
690
|
function pcssValidate(args) {
|
|
677
691
|
const refused = [];
|
|
@@ -716,6 +730,8 @@ function pcssValidate(args) {
|
|
|
716
730
|
refused.push(`The css for \`${selector}\` contains ${bad.map((b) => "`" + b + "`").join(", ")}.` + (bad.includes("//") ? " Use a root-relative /path for images." : ""));
|
|
717
731
|
continue;
|
|
718
732
|
}
|
|
733
|
+
const star = pcssStarDisplay(`${selector}{${css}}`);
|
|
734
|
+
if (star) { refused.push(star); continue; }
|
|
719
735
|
const key = pcssNorm(media, true) + " | " + pcssNorm(selector, false);
|
|
720
736
|
if (seen.has(key)) {
|
|
721
737
|
refused.push(`\`${selector}\` appears twice in this call.`);
|
|
@@ -1262,6 +1278,17 @@ function dsBy(ds, by, values) {
|
|
|
1262
1278
|
return kind || by.else;
|
|
1263
1279
|
}
|
|
1264
1280
|
|
|
1281
|
+
// BD's Homepage widget gives each row a class from its stored value (hs-recent-members); a site copy of that widget may not.
|
|
1282
|
+
function dsRowCss(ds, b, values, css) {
|
|
1283
|
+
const want = dsExpand(ds.m, b.widget);
|
|
1284
|
+
const rb = ds.m.blocks.find((x) => x.id === "home.rows");
|
|
1285
|
+
const n = rb ? rb.rows.find((r) => dsRowValue(ds.m, values[r]) === want) : undefined;
|
|
1286
|
+
let v = n ? String(values[n]) : b.id === "content.1" ? "Homepage Content" : want;
|
|
1287
|
+
for (const p of ["Bootstrap Theme - Display - ", "Bootstrap Theme - Homepage - "]) v = v.split(p).join("");
|
|
1288
|
+
const cls = "hs-" + v.replace(/[A-Z]/g, (c) => c.toLowerCase()).replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1289
|
+
return css.replace(/\.homepage-sections > (div:has\([^()]*\)|\.custom-homepage-content( \([^()]*\))?)/g, `.homepage-sections > .${cls}`);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1265
1292
|
// The block's selectors for the site's current state: per hero type (css_by), plus the inline title colour and the Card View photo.
|
|
1266
1293
|
function dsCss(ds, b, values) {
|
|
1267
1294
|
let css = b.css_by ? dsBy(ds, b.css_by, values) : b.css || null;
|
|
@@ -1407,6 +1434,35 @@ async function dsfSiteWidgets(key, get) {
|
|
|
1407
1434
|
return p;
|
|
1408
1435
|
}
|
|
1409
1436
|
|
|
1437
|
+
// The site's copy of a widget, from the site widget list already read (no request), or null.
|
|
1438
|
+
async function dsfCachedCopy(key, name) {
|
|
1439
|
+
const hit = _dsfWidgetCache.get(key);
|
|
1440
|
+
if (!hit || Date.now() - hit.at >= DS_TTL_MS) return null;
|
|
1441
|
+
const ws = await hit.p;
|
|
1442
|
+
const w = ws && ws.byName ? ws.byName.get(name) : null;
|
|
1443
|
+
return w ? Number(w.widget_id) : null;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// BD's widgets behind the homepage sections, name -> row with code. One list request, kept as long as the read cache.
|
|
1447
|
+
const _dsfMasterCache = new Map();
|
|
1448
|
+
async function dsfMasters(key, names, get) {
|
|
1449
|
+
if (!names.length) return new Map();
|
|
1450
|
+
const k = `${key}\n${names.join("\n")}`;
|
|
1451
|
+
const hit = _dsfMasterCache.get(k);
|
|
1452
|
+
if (hit && Date.now() - hit.at < DS_TTL_MS) return hit.p;
|
|
1453
|
+
const p = (async () => {
|
|
1454
|
+
const r = await get("/api/v2/data_widgets/get", { property: "widget_name", property_value: names.join(","), property_operator: "in", limit: 50 });
|
|
1455
|
+
if (!r || r.status !== 200 || !r.body || !Array.isArray(r.body.message)) return null;
|
|
1456
|
+
const out = new Map();
|
|
1457
|
+
for (const w of r.body.message) if (w && udsYes(w.is_default) && names.includes(w.widget_name) && !out.has(w.widget_name)) out.set(w.widget_name, w);
|
|
1458
|
+
return out;
|
|
1459
|
+
})().catch(() => null);
|
|
1460
|
+
const entry = { at: Date.now(), p };
|
|
1461
|
+
_dsfMasterCache.set(k, entry);
|
|
1462
|
+
p.then((r) => { if (!r && _dsfMasterCache.get(k) === entry) _dsfMasterCache.delete(k); });
|
|
1463
|
+
return p;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1410
1466
|
async function dsfHomePage(get) {
|
|
1411
1467
|
const r = await get("/api/v2/list_seo/get", { property: "seo_type", property_value: "home", limit: 1 });
|
|
1412
1468
|
const row = r && r.status === 200 && r.body && Array.isArray(r.body.message) ? r.body.message[0] : null;
|
|
@@ -1473,10 +1529,22 @@ function dsfCore(words) {
|
|
|
1473
1529
|
});
|
|
1474
1530
|
return out;
|
|
1475
1531
|
}
|
|
1532
|
+
// PHP in widget code: "<?php" opens no tag, and the ">" of "?>", "->" or "=>" closes none.
|
|
1476
1533
|
function dsfInTag(raw, at) {
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1534
|
+
let lt = raw.lastIndexOf("<", at);
|
|
1535
|
+
while (lt >= 0 && raw[lt + 1] === "?") lt = lt > 0 ? raw.lastIndexOf("<", lt - 1) : -1;
|
|
1536
|
+
if (lt < 0 || !/^<[\/!a-zA-Z]/.test(raw.slice(lt, lt + 2))) return false;
|
|
1537
|
+
let gt = raw.lastIndexOf(">", at - 1);
|
|
1538
|
+
while (gt > lt && (raw[gt - 1] === "?" || raw[gt - 1] === "=" || (raw[gt - 1] === "-" && raw[gt - 2] !== "-"))) gt = raw.lastIndexOf(">", gt - 1);
|
|
1539
|
+
return gt < lt;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// A hit inside a %%%label%%% token is the whole token: the code holds the token, and replacing part of it breaks it.
|
|
1543
|
+
function dsfToken(raw, sp) {
|
|
1544
|
+
const s = raw.lastIndexOf("%%%", sp[0]);
|
|
1545
|
+
if (s < 0) return null;
|
|
1546
|
+
const m = /^%%%([A-Za-z0-9_]+)(~[a-z]+)?%%%/.exec(raw.slice(s, s + 120));
|
|
1547
|
+
return m && s + m[0].length >= sp[1] ? { sp: [s, s + m[0].length], name: m[1] } : null;
|
|
1480
1548
|
}
|
|
1481
1549
|
|
|
1482
1550
|
const DSF_BLOCK_TAG = /^<\/?(br|p|div|li|ul|ol|h[1-6]|tr|td|th|section|article|header|footer|table|tbody|blockquote|hr)\b/i;
|
|
@@ -1619,11 +1687,17 @@ async function dsfFind(ds, got, text, key, get) {
|
|
|
1619
1687
|
const placed = new Map(dir.homepage.rows.filter((r) => r.block).map((r) => [r.block, r.row]));
|
|
1620
1688
|
const heroShown = !/^(Blank)$/.test(String(values.custom_30 || "")) && !/Map/.test(String(values.custom_30 || ""));
|
|
1621
1689
|
const t0 = Date.now();
|
|
1622
|
-
const
|
|
1690
|
+
const secRows = [];
|
|
1691
|
+
for (const r of dir.homepage.rows) {
|
|
1692
|
+
const b = /^section\./.test(r.block || "") ? m.blocks.find((x) => x.id === r.block) : null;
|
|
1693
|
+
if (b && b.widget) secRows.push({ name: dsExpand(m, b.widget), row: r.row, block: r.block });
|
|
1694
|
+
}
|
|
1695
|
+
const [ws, home, mi, mn, bd] = await Promise.all([
|
|
1623
1696
|
dsfSiteWidgets(key, get),
|
|
1624
1697
|
dsfHomePage(get),
|
|
1625
1698
|
get("/api/v2/menu_items/get", { limit: 100 }),
|
|
1626
1699
|
get("/api/v2/menus/get", { limit: 100 }),
|
|
1700
|
+
dsfMasters(key, Array.from(new Set(secRows.map((x) => x.name))), get),
|
|
1627
1701
|
]);
|
|
1628
1702
|
const sources = [];
|
|
1629
1703
|
const searched = [];
|
|
@@ -1660,6 +1734,11 @@ async function dsfFind(ds, got, text, key, get) {
|
|
|
1660
1734
|
const ccRow = placed.get("content.1");
|
|
1661
1735
|
if (home) for (const n of dsfRefs(home.content)) want(n, ccRow === undefined ? null : ccRow, "content.1", undefined, 0);
|
|
1662
1736
|
for (const n of DS_EXTRA) if (heroShown && (!ws.byName || ws.byName.has(n))) want(n, null, "hero.cta", undefined, 0);
|
|
1737
|
+
const bdw = [];
|
|
1738
|
+
for (const x of secRows) {
|
|
1739
|
+
if (ws.byName && ws.byName.has(x.name)) want(x.name, x.row, x.block, undefined, 0);
|
|
1740
|
+
else if (bd && bd.has(x.name) && !bdw.some((y) => y.w.widget_name === x.name)) bdw.push({ w: bd.get(x.name), row: x.row, block: x.block, master: true });
|
|
1741
|
+
}
|
|
1663
1742
|
for (let i = 0; i < widgets.length; i++) {
|
|
1664
1743
|
const x = widgets[i];
|
|
1665
1744
|
if (x.depth >= 2) continue;
|
|
@@ -1668,13 +1747,15 @@ async function dsfFind(ds, got, text, key, get) {
|
|
|
1668
1747
|
const skipped = widgets.length > DSF_MAX_WIDGETS ? widgets.splice(DSF_MAX_WIDGETS).map((x) => x.w.widget_name) : [];
|
|
1669
1748
|
const addWidget = (x, shown) => {
|
|
1670
1749
|
x.shown = shown;
|
|
1671
|
-
const base = { widget_id: Number(x.w.widget_id), widget: x.w.widget_name, row: x.row === undefined ? null : x.row, block: x.block };
|
|
1750
|
+
const base = x.master ? { widget: x.w.widget_name, bd_widget: true, row: x.row === undefined ? null : x.row, block: x.block } : { widget_id: Number(x.w.widget_id), widget: x.w.widget_name, row: x.row === undefined ? null : x.row, block: x.block };
|
|
1672
1751
|
if (x.via) base.via = x.via;
|
|
1673
1752
|
for (const f of DSF_CODE) if (x.w[f]) sources.push({ hit: Object.assign({}, base, { field: f }), raw: String(x.w[f]), markup: f === "widget_data", kind: f === "widget_data" ? "text" : "code", shown, widget: x });
|
|
1674
1753
|
};
|
|
1675
1754
|
for (const x of widgets) addWidget(x, x.row !== null || x.block === "hero.cta");
|
|
1676
1755
|
if (ws.byName) searched.push(`${widgets.length} site widgets on the homepage (rows, hero hooks, nested)`);
|
|
1677
1756
|
else searched.push("site widgets: the widget list could not be read");
|
|
1757
|
+
for (const x of bdw) addWidget(x, true);
|
|
1758
|
+
if (bdw.length) searched.push(`${bdw.length} BD section widgets on the homepage (no site copy)`);
|
|
1678
1759
|
if (home) {
|
|
1679
1760
|
for (const f of DSF_PAGE_FIELDS) {
|
|
1680
1761
|
if (!home[f]) continue;
|
|
@@ -1699,10 +1780,13 @@ async function dsfFind(ds, got, text, key, get) {
|
|
|
1699
1780
|
let spans = mode === "loose" ? dsfSpans(src.raw, null, "loose", W) : dsfSpans(src.raw, rawNeedle, "raw");
|
|
1700
1781
|
if (!spans.length && mode !== "loose" && src.markup && needle) spans = dsfSpans(src.raw, needle, "vis");
|
|
1701
1782
|
if (!spans.length) continue;
|
|
1783
|
+
const tok = new Map();
|
|
1784
|
+
if (src.widget) spans = spans.map((sp) => { const t = dsfToken(src.raw, sp); if (!t) return sp; tok.set(t.sp[0], t.name); return t.sp; }).filter((sp, i, all) => all.findIndex((o) => o[0] === sp[0]) === i);
|
|
1702
1785
|
const texts = spans.filter((sp) => src.kind === "text" && !(src.markup && dsfInTag(src.raw, sp[0])));
|
|
1703
1786
|
const pick = texts.length ? texts.slice(0, 3) : [spans[0]];
|
|
1704
1787
|
for (const sp of pick) {
|
|
1705
1788
|
const h = Object.assign({}, src.hit, dsfOccur(src.raw, sp, texts.length > 0 && src.markup));
|
|
1789
|
+
if (tok.has(sp[0])) h.text_label = tok.get(sp[0]);
|
|
1706
1790
|
if (mode === "loose") h.loose = true;
|
|
1707
1791
|
if (!src.shown) h.shown = false;
|
|
1708
1792
|
if (!texts.length && spans.length > 1 && !h.count && !h.of) h.count = spans.length;
|
|
@@ -1714,7 +1798,7 @@ async function dsfFind(ds, got, text, key, get) {
|
|
|
1714
1798
|
return hits;
|
|
1715
1799
|
};
|
|
1716
1800
|
const wbase = (x) => {
|
|
1717
|
-
const b = { widget_id: Number(x.w.widget_id), widget: x.w.widget_name, row: x.row === undefined ? null : x.row, block: x.block };
|
|
1801
|
+
const b = x.master ? { widget: x.w.widget_name, bd_widget: true, row: x.row === undefined ? null : x.row, block: x.block } : { widget_id: Number(x.w.widget_id), widget: x.w.widget_name, row: x.row === undefined ? null : x.row, block: x.block };
|
|
1718
1802
|
if (x.via) b.via = x.via;
|
|
1719
1803
|
if (x.shown === false) b.shown = false;
|
|
1720
1804
|
return b;
|
|
@@ -1769,7 +1853,20 @@ async function dsfFind(ds, got, text, key, get) {
|
|
|
1769
1853
|
}
|
|
1770
1854
|
return assemble(hits, named, looseNames);
|
|
1771
1855
|
};
|
|
1772
|
-
let hits = search(sources, widgets);
|
|
1856
|
+
let hits = search(sources, widgets.concat(bdw));
|
|
1857
|
+
// A text label also matches its token where BD prints it: any number of %, ~na/~ao/~as/~ut, any case (core's pattern).
|
|
1858
|
+
const labs = Array.from(new Set(hits.filter((h) => h.text_label).map((h) => String(h.text_label).toLowerCase())));
|
|
1859
|
+
if (labs.length) {
|
|
1860
|
+
const hk = (h) => JSON.stringify([h.setting, h.page, h.widget_id, h.widget, h.menu_item_id, h.field, h.match, h.nth]);
|
|
1861
|
+
const have = new Set(hits.map(hk));
|
|
1862
|
+
const re = new RegExp(`(?<!['0-9])%+(${labs.join("|")})(~na|~ao|~as|~ut)?%+(?:(?!['0-9])|(?='s))`, "gi");
|
|
1863
|
+
for (const src of sources) for (const x of String(src.raw).matchAll(re)) {
|
|
1864
|
+
const h = Object.assign({}, src.hit, dsfOccur(src.raw, [x.index, x.index + x[0].length], src.kind === "text" && src.markup && !dsfInTag(src.raw, x.index)), { text_label: x[1] });
|
|
1865
|
+
if (!src.shown) h.shown = false;
|
|
1866
|
+
if (!have.has(hk(h))) { have.add(hk(h)); hits.push(h); }
|
|
1867
|
+
}
|
|
1868
|
+
hits.sort((a, b) => (a.shown === false ? 1 : 0) - (b.shown === false ? 1 : 0));
|
|
1869
|
+
}
|
|
1773
1870
|
if (!hits.length && ws.byName) {
|
|
1774
1871
|
const rest = [];
|
|
1775
1872
|
for (const [name, w] of ws.byName) if (/^homepage/i.test(name) && !seen.has(name)) rest.push({ w, row: null, block: null });
|
|
@@ -1806,6 +1903,8 @@ function dsfEditLine(hits) {
|
|
|
1806
1903
|
if (w.some((h) => !h.match)) t += "; by name or code: getWidget include_code=1 first";
|
|
1807
1904
|
say.push(t);
|
|
1808
1905
|
}
|
|
1906
|
+
const bw = hits.filter((h) => h.bd_widget);
|
|
1907
|
+
if (bw.length) say.push("BD section widget: updateWidget {widget_name, customize:true, replace:[{field, find: match, with}]} makes this site's own copy with the change" + (bw.some((h) => h.of > 1 || h.count > 1) ? DSF_ALL_SAY : ""));
|
|
1809
1908
|
if (hits.some((h) => h.setting)) say.push("setting: updateDesignSettings {settings: {name: value}}" + (hits.some((h) => h.text_label) ? " (literal text replaces the label)" : ""));
|
|
1810
1909
|
const pc = hits.filter((h) => h.page && h.field === "content");
|
|
1811
1910
|
if (pc.length) say.push("home page content: updateWebPage {seo_id, replace:[{find: match, with}]}, no read needed, returns an undo" + (pc.some((h) => h.of > 1 || h.count > 1) ? DSF_ALL_SAY : ""));
|
|
@@ -1824,6 +1923,14 @@ function dsfSlideNotes(b, out, addons) {
|
|
|
1824
1923
|
if (b.id === "hero.background") for (const s of out.settings || []) if (s.name === "custom_76" && s.note) s.note = s.note.replace(DSF_SLIDE_76, on ? DSF_SLIDE_76_ON : DSF_SLIDE_76_OFF);
|
|
1825
1924
|
}
|
|
1826
1925
|
|
|
1926
|
+
// updateWidget on BD's own widget: `customize` creates this site's copy under the same name (it then replaces BD's widget on
|
|
1927
|
+
// this site), as the admin's Customize does: every column of BD's row, with the replacements applied.
|
|
1928
|
+
const WC_CAS = "Bootstrap Theme - Custom Appearance Settings";
|
|
1929
|
+
const WC_COPY = ["widget_name", "widget_type", "widget_data", "widget_style", "widget_javascript", "widget_class", "widget_viewport", "short_code", "div_id", "ssl_enabled", "mobile_enabled", "widget_html_element", "widget_settings", "widget_values"];
|
|
1930
|
+
const WC_CAS_SAY = `${WC_CAS} is never customised or edited: BD updates it often. Override what it prints with CSS.`;
|
|
1931
|
+
const WC_DONE = "This site's own copy of BD's widget, made with your changes; it no longer receives BD's updates to that widget. undo (deleteWidget) removes the copy and the site shows BD's widget again.";
|
|
1932
|
+
const WC_NO_ID = "updateWidget needs widget_id; for BD's own widget send widget_name with replace and customize:true.";
|
|
1933
|
+
|
|
1827
1934
|
// updateWidget `replace`: exact substitutions on the stored code fields, all or nothing; `all` changes every occurrence.
|
|
1828
1935
|
// Returns {problems} or {fields: {name: new value}, replaced, undo}. The new values then go through the ordinary updateWidget path.
|
|
1829
1936
|
const DSF_REPLACE_FIELDS = ["widget_data", "widget_style", "widget_javascript"];
|
|
@@ -1973,15 +2080,89 @@ function replaceUndoAsSent(rp, args) {
|
|
|
1973
2080
|
return { [key]: rp.undo[key], replace: list };
|
|
1974
2081
|
}
|
|
1975
2082
|
|
|
2083
|
+
function wcNeeds(name) {
|
|
2084
|
+
return `${name} is BD's widget: add customize:true, which creates this site's own copy of BD's widget with your changes; that copy stops receiving BD's updates to it.`;
|
|
2085
|
+
}
|
|
2086
|
+
function wcPick(r, name) {
|
|
2087
|
+
const b = r && r.status === 200 ? r.body : null;
|
|
2088
|
+
if (!b || typeof b !== "object" || !(b.status === "success" || / not found$/.test(String(b.message)))) return null;
|
|
2089
|
+
const rows = Array.isArray(b.message) ? b.message : [];
|
|
2090
|
+
return { site: rows.find((w) => w && w.widget_name === name && w.widget_id && !udsYes(w.is_default)) || null, master: rows.find((w) => w && w.widget_name === name && udsYes(w.is_default)) || null };
|
|
2091
|
+
}
|
|
2092
|
+
function wcPlan(master, name, list) {
|
|
2093
|
+
const plan = dsfReplacePlan(master, list);
|
|
2094
|
+
if (plan.problems) return plan;
|
|
2095
|
+
const fields = {};
|
|
2096
|
+
for (const f of WC_COPY) if (master[f] !== null && master[f] !== undefined && master[f] !== "") fields[f] = String(master[f]);
|
|
2097
|
+
Object.assign(fields, plan.fields);
|
|
2098
|
+
fields.widget_name = name;
|
|
2099
|
+
fields.bootstrap_enabled = 4;
|
|
2100
|
+
return { customize: { name, fields, replaced: plan.replaced } };
|
|
2101
|
+
}
|
|
2102
|
+
async function widgetCustomize(config, c) {
|
|
2103
|
+
const body = Object.assign({}, c.fields);
|
|
2104
|
+
sanitizeScaffoldingInArgs(body);
|
|
2105
|
+
stripWidgetWrapperTagsInArgs(body);
|
|
2106
|
+
autoDefaultSystemTimestamps("createWidget", body, await getSiteTimezoneCached(config.apiUrl, config.apiKey));
|
|
2107
|
+
let r = null;
|
|
2108
|
+
try { r = await makeRequest(config, "POST", "/api/v2/data_widgets/create", null, body); } catch (e) { return { status: "error", message: `Network error: ${(e && e.message) || String(e)}` }; }
|
|
2109
|
+
const b = r && r.body;
|
|
2110
|
+
const row = b && typeof b === "object" && b.status === "success" ? (Array.isArray(b.message) ? b.message[0] : b.message) : null;
|
|
2111
|
+
if (!row || typeof row !== "object" || !row.widget_id) return { status: "error", message: `The copy was not created: ${b && typeof b === "object" && b.message ? (typeof b.message === "string" ? b.message : JSON.stringify(b.message)) : `HTTP ${r && r.status}`}` };
|
|
2112
|
+
const wid = Number(row.widget_id);
|
|
2113
|
+
const ref = await autoRefreshCache(config, AUTO_REFRESH_SCOPE.createWidget);
|
|
2114
|
+
const out = { status: "success", message: { widget_id: wid, widget_name: c.name }, customized: WC_DONE, replaced: c.replaced, undo: { deleteWidget: { widget_id: wid } }, auto_cache_refreshed: ref.ok };
|
|
2115
|
+
if (!ref.ok) out.auto_cache_refresh_error = ref.message;
|
|
2116
|
+
return out;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// getWidget / renderWidget by widget_name alone: BD's get/{id} needs an id and BD's render reads only the site's own
|
|
2120
|
+
// widgets, so the name is looked up in the merged list. null: BD answers as before.
|
|
2121
|
+
const WN_RENDER = "BD's own widget renders only on the page that uses it; read its code with getWidget {widget_name, include_code:1}.";
|
|
2122
|
+
function wnAnswer(found, tool, name, includeFlags) {
|
|
2123
|
+
if (tool === "renderWidget") return found && !found.site && found.master ? { status: "error", message: WN_RENDER } : null;
|
|
2124
|
+
if (!found) return { status: "error", message: `Widget ${name} could not be read.` };
|
|
2125
|
+
const row = found.site || found.master;
|
|
2126
|
+
if (!row) return { status: "error", message: `No site widget or BD widget is named ${name}.` };
|
|
2127
|
+
return applyWidgetLean({ status: "success", message: [Object.assign({}, row)] }, includeFlags);
|
|
2128
|
+
}
|
|
2129
|
+
async function widgetByName(config, tool, name, includeFlags) {
|
|
2130
|
+
let r = null;
|
|
2131
|
+
try { r = await makeRequest(config, "GET", "/api/v2/data_widgets/get", { property: "widget_name", property_value: name, limit: 10 }, null); } catch { r = null; }
|
|
2132
|
+
return wnAnswer(wcPick(r, name), tool, name, includeFlags);
|
|
2133
|
+
}
|
|
1976
2134
|
async function widgetReplacePrepare(config, args) {
|
|
1977
|
-
const
|
|
1978
|
-
|
|
2135
|
+
const customize = udsYes(args.customize);
|
|
2136
|
+
delete args.customize;
|
|
1979
2137
|
if (Array.isArray(args.replace)) for (const f of DSF_REPLACE_FIELDS) if (Object.prototype.hasOwnProperty.call(args, f) && args.replace.some((e) => e && e.field === f)) return { problems: [`${f}: send the whole field or replace entries for it, not both.`] };
|
|
1980
|
-
let
|
|
1981
|
-
|
|
1982
|
-
|
|
2138
|
+
let id = args.widget_id;
|
|
2139
|
+
let row = null;
|
|
2140
|
+
if ((id === undefined || id === null || id === "") && typeof args.widget_name === "string" && args.widget_name.trim()) {
|
|
2141
|
+
const name = args.widget_name.trim();
|
|
2142
|
+
if (name.toLowerCase() === WC_CAS.toLowerCase()) return { problems: [WC_CAS_SAY] };
|
|
2143
|
+
let r = null;
|
|
2144
|
+
try { r = await makeRequest(config, "GET", "/api/v2/data_widgets/get", { property: "widget_name", property_value: name, limit: 10 }, null); } catch { r = null; }
|
|
2145
|
+
const found = wcPick(r, name);
|
|
2146
|
+
if (!found) return { problems: [`Widget ${name} could not be read; replace needs the stored widget.`] };
|
|
2147
|
+
if (found.site) {
|
|
2148
|
+
row = found.site;
|
|
2149
|
+
id = row.widget_id;
|
|
2150
|
+
args.widget_id = Number(id);
|
|
2151
|
+
delete args.widget_name;
|
|
2152
|
+
} else if (found.master) {
|
|
2153
|
+
if (!customize) return { problems: [wcNeeds(name)] };
|
|
2154
|
+
return wcPlan(found.master, name, args.replace);
|
|
2155
|
+
} else return { problems: [`No site widget or BD widget is named ${name}.`] };
|
|
2156
|
+
}
|
|
2157
|
+
if (id === undefined || id === null || !/^\d+$/.test(String(id))) return { problems: ["replace needs widget_id, or widget_name for BD's widget."] };
|
|
2158
|
+
if (!row) {
|
|
2159
|
+
let r = null;
|
|
2160
|
+
try { r = await makeRequest(config, "GET", `/api/v2/data_widgets/get/${id}`, null, null); } catch { r = null; }
|
|
2161
|
+
row = r && r.status === 200 && r.body && r.body.status === "success" ? (Array.isArray(r.body.message) ? r.body.message[0] : r.body.message) : null;
|
|
2162
|
+
}
|
|
1983
2163
|
if (!row || typeof row !== "object" || !row.widget_id) return { problems: [`Widget ${id} could not be read; replace needs the stored widget.`] };
|
|
1984
2164
|
if (udsYes(row.is_default)) return { problems: [`Widget ${id} is a BD default, not a site widget.`] };
|
|
2165
|
+
if (String(row.widget_name || "").trim().toLowerCase() === WC_CAS.toLowerCase()) return { problems: [WC_CAS_SAY] };
|
|
1985
2166
|
const plan = dsfReplacePlan(row, args.replace);
|
|
1986
2167
|
if (plan.problems) return plan;
|
|
1987
2168
|
delete args.replace;
|
|
@@ -2234,6 +2415,12 @@ function dsValues(config, ds) {
|
|
|
2234
2415
|
return p;
|
|
2235
2416
|
}
|
|
2236
2417
|
|
|
2418
|
+
// The hero type's widget (custom_30). The hide types print none; the map types print the maps add-on's widgets instead.
|
|
2419
|
+
function dsHeroWidget(ds, values) {
|
|
2420
|
+
const k = dsMeta(ds, values, "custom_30", "kind");
|
|
2421
|
+
return k && k !== "hide" && k !== "map" ? dsRead(ds, values, "custom_30") || null : null;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2237
2424
|
async function getDesignSettings(config, args) {
|
|
2238
2425
|
let ds;
|
|
2239
2426
|
try {
|
|
@@ -2264,7 +2451,15 @@ async function getDesignSettings(config, args) {
|
|
|
2264
2451
|
if (got.error) return { status: "error", message: got.error };
|
|
2265
2452
|
if (one) return dsOne(ds, one, q, got);
|
|
2266
2453
|
if (!b) return dsfOverview(ds, got, dsDirectory(ds, got), dsCacheKey(config), get);
|
|
2267
|
-
|
|
2454
|
+
let out = dsBlock(ds, b, got);
|
|
2455
|
+
if (out.css && b.widget && !(await dsfCachedCopy(dsCacheKey(config), "Bootstrap Theme - Homepage"))) out.css = dsRowCss(ds, b, got.values, out.css);
|
|
2456
|
+
const hw = /^hero\.(primary|cta)/.test(b.id) ? b.widget || dsHeroWidget(ds, got.values) : null;
|
|
2457
|
+
if ((/^section\./.test(b.id) && b.widget) || hw) {
|
|
2458
|
+
const name = dsExpand(ds.m, hw || b.widget);
|
|
2459
|
+
out = Object.assign({ block: out.block, title: out.title, widget: name }, out);
|
|
2460
|
+
const copy = await dsfCachedCopy(dsCacheKey(config), name);
|
|
2461
|
+
if (copy) out.widget_copy = `This site's own copy: widget_id ${copy}; updateWidget replace edits it.`;
|
|
2462
|
+
}
|
|
2268
2463
|
if (b.id === "hero.cta") out.hero_extra_content = await dsHeroExtra(config);
|
|
2269
2464
|
return out;
|
|
2270
2465
|
}
|
|
@@ -2279,6 +2474,7 @@ const UDS_MAX = 100;
|
|
|
2279
2474
|
const UDS_NOTE_PAGE = "A Design Settings page already open in a browser puts back the values it loaded on its next save: reload it before saving there.";
|
|
2280
2475
|
const UDS_CC = "Custom Content holds one widget reference. Create a site widget and send [widget=Name].";
|
|
2281
2476
|
const UDS_CC_HTML = "This value is editor HTML. Custom Content holds one widget reference: put the HTML in a site widget and send [widget=Name].";
|
|
2477
|
+
const UDS_ROW_WRAP = "Row widget: own wrapper (max-width, margin auto, side padding); no negative margin into neighbouring rows. Live counts in its PHP: bd_controller::user(WEBSITE_DB)->getCount('user_id', array(array('column' => 'active', 'value' => 2))); same on data_posts (post_status 1) and list_professions.";
|
|
2282
2478
|
const UDS_ROW_WIDGET = "To show a site widget as a section, set a free Custom Content block (homepage_content_2 to 10) to [widget=Name] and place that block with home_rows.";
|
|
2283
2479
|
const UDS_UNSIGNED = "This undo was not issued by this tool for this site; send settings instead.";
|
|
2284
2480
|
const UDS_LOST = "The site's response was lost; the result was confirmed by reading the settings back.";
|
|
@@ -2615,6 +2811,7 @@ function udsAdvice(ds, idx, plan, R, addons) {
|
|
|
2615
2811
|
if (also.length) notes.push(`${x.label}: the same section is also in row ${also.join(" and ")}; the page shows it in each row.`);
|
|
2616
2812
|
}
|
|
2617
2813
|
}
|
|
2814
|
+
if (plan.some((e) => DS_CC_REF.test(e.name) && dsfCcRef(e.value).name)) notes.push(UDS_ROW_WRAP);
|
|
2618
2815
|
return notes;
|
|
2619
2816
|
}
|
|
2620
2817
|
|
|
@@ -3275,7 +3472,8 @@ function buildTools(spec) {
|
|
|
3275
3472
|
if (p.in === "path") {
|
|
3276
3473
|
properties[p.name] = p.schema || { type: "string" };
|
|
3277
3474
|
if (p.description) properties[p.name].description = p.description;
|
|
3278
|
-
|
|
3475
|
+
// getWidget also reads by widget_name (wrapper), so its widget_id is optional.
|
|
3476
|
+
if (!(op.operationId === "getWidget" && p.name === "widget_id")) required.push(p.name);
|
|
3279
3477
|
} else if (p.in === "query" && !["limit", "page", "property", "property_value", "property_operator", "order_column", "order_type"].includes(p.name)) {
|
|
3280
3478
|
// Non-standard query params (like form_name).
|
|
3281
3479
|
// The skip-list above is BD's standard pagination/filter param
|
|
@@ -4721,6 +4919,7 @@ function applyWriteLean(toolName, body, sentKeys) {
|
|
|
4721
4919
|
// createWebPage / updateWebPage -> scope=web_pages (page render cache;
|
|
4722
4920
|
// also covers list_seo hero-EAV)
|
|
4723
4921
|
// createWidget / updateWidget -> scope=data_widgets
|
|
4922
|
+
// deleteWidget -> "" (the site shows BD's widget again only after a refresh)
|
|
4724
4923
|
// updatePostType -> "" (full refresh; no targeted scope
|
|
4725
4924
|
// exists for post-type config)
|
|
4726
4925
|
//
|
|
@@ -4757,6 +4956,7 @@ const AUTO_REFRESH_SCOPE = {
|
|
|
4757
4956
|
updateWebPage: "",
|
|
4758
4957
|
createWidget: "",
|
|
4759
4958
|
updateWidget: "",
|
|
4959
|
+
deleteWidget: "",
|
|
4760
4960
|
updatePostType: "",
|
|
4761
4961
|
createMenu: "",
|
|
4762
4962
|
updateMenu: "",
|
|
@@ -8720,13 +8920,33 @@ async function main() {
|
|
|
8720
8920
|
if (name === "updateWidget" && args && typeof args === "object" && args.replace !== undefined) {
|
|
8721
8921
|
const rp = await widgetReplacePrepare(config, args);
|
|
8722
8922
|
if (rp.problems) return { content: [{ type: "text", text: JSON.stringify({ status: "refused", problems: rp.problems }) }], isError: true };
|
|
8923
|
+
if (rp.customize) {
|
|
8924
|
+
const res = await widgetCustomize(config, rp.customize);
|
|
8925
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], isError: res.status !== "success" };
|
|
8926
|
+
}
|
|
8723
8927
|
_widgetReplace = rp;
|
|
8928
|
+
} else if (name === "updateWidget" && args && typeof args === "object") {
|
|
8929
|
+
delete args.customize;
|
|
8930
|
+
if (args.widget_id === undefined || args.widget_id === null || args.widget_id === "") return { content: [{ type: "text", text: JSON.stringify({ status: "refused", problems: [WC_NO_ID] }) }], isError: true };
|
|
8931
|
+
}
|
|
8932
|
+
if ((name === "getWidget" || name === "renderWidget") && args && typeof args === "object") {
|
|
8933
|
+
const noId = args.widget_id === undefined || args.widget_id === null || args.widget_id === "";
|
|
8934
|
+
const wn = typeof args.widget_name === "string" ? args.widget_name.trim() : "";
|
|
8935
|
+
if (name === "getWidget" && !noId) delete args.widget_name;
|
|
8936
|
+
else if (noId && wn) {
|
|
8937
|
+
const res = await widgetByName(config, name, wn, includeFlags);
|
|
8938
|
+
if (res) return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], isError: res.status !== "success" };
|
|
8939
|
+
} else if (name === "getWidget") return { content: [{ type: "text", text: JSON.stringify({ status: "refused", problems: ["getWidget needs widget_id or widget_name."] }) }], isError: true };
|
|
8724
8940
|
}
|
|
8725
8941
|
if (name === "updateWebPage" && args && typeof args === "object" && args.replace !== undefined) {
|
|
8726
8942
|
const rp = await webPageReplacePrepare(config, args);
|
|
8727
8943
|
if (rp.problems) return { content: [{ type: "text", text: JSON.stringify({ status: "refused", problems: rp.problems }) }], isError: true };
|
|
8728
8944
|
_widgetReplace = rp;
|
|
8729
8945
|
}
|
|
8946
|
+
if ((name === "createWebPage" || name === "updateWebPage") && args && typeof args.content_css === "string") {
|
|
8947
|
+
const star = pcssStarDisplay(args.content_css);
|
|
8948
|
+
if (star) return { content: [{ type: "text", text: JSON.stringify({ status: "refused", problems: [star] }) }], isError: true };
|
|
8949
|
+
}
|
|
8730
8950
|
|
|
8731
8951
|
// Sanitize agent-scaffolding tokens from content-field values before
|
|
8732
8952
|
// they reach BD. Belt-and-suspenders with the docs rule (which tells
|
package/openapi/bd-api.json
CHANGED
|
@@ -5540,6 +5540,14 @@
|
|
|
5540
5540
|
1
|
|
5541
5541
|
]
|
|
5542
5542
|
}
|
|
5543
|
+
},
|
|
5544
|
+
{
|
|
5545
|
+
"name": "widget_name",
|
|
5546
|
+
"in": "query",
|
|
5547
|
+
"description": "Instead of widget_id: the exact name. Returns the site's copy, else BD's widget.",
|
|
5548
|
+
"schema": {
|
|
5549
|
+
"type": "string"
|
|
5550
|
+
}
|
|
5543
5551
|
}
|
|
5544
5552
|
],
|
|
5545
5553
|
"responses": {
|
|
@@ -5554,7 +5562,7 @@
|
|
|
5554
5562
|
}
|
|
5555
5563
|
}
|
|
5556
5564
|
},
|
|
5557
|
-
"description": "Fetch a single widget record by `widget_id`. Read-only.\n\n**Lean-by-default keep-list:** returns only `widget_id`, `widget_name`, `widget_type`, `short_code`, `date_updated`, `revision_timestamp`, `is_default`. The code fields (`widget_data`, `widget_style`, `widget_javascript`) are stripped — restore with `include_code=1`.\n\n**Use when:** you have a `widget_id` (from `listWidgets` or admin) and want the widget's SOURCE code to edit or audit. To preview the rendered widget on the front-end, embed it on a page via `[widget=Name]` shortcode and view the page.\n\n**Required:** `widget_id`
|
|
5565
|
+
"description": "Fetch a single widget record by `widget_id`. Read-only.\n\n**Lean-by-default keep-list:** returns only `widget_id`, `widget_name`, `widget_type`, `short_code`, `date_updated`, `revision_timestamp`, `is_default`. The code fields (`widget_data`, `widget_style`, `widget_javascript`) are stripped — restore with `include_code=1`.\n\n**Use when:** you have a `widget_id` (from `listWidgets` or admin) and want the widget's SOURCE code to edit or audit. To preview the rendered widget on the front-end, embed it on a page via `[widget=Name]` shortcode and view the page.\n\n**Required:** `widget_id` or `widget_name`.\n\n**See also:** `listWidgets` (enumerate), `updateWidget` (modify).\n\n**Returns:** `{ status: \"success\", message: [{...record}] }` - the `message` array contains 1 record with all widget fields (`widget_data` = HTML, `widget_style` = CSS, `widget_javascript` = JS, plus metadata).\n\n\nFor the full field list, see `listWidgets`.\n\n"
|
|
5558
5566
|
}
|
|
5559
5567
|
},
|
|
5560
5568
|
"/api/v2/data_widgets/create": {
|
|
@@ -5622,9 +5630,6 @@
|
|
|
5622
5630
|
"application/x-www-form-urlencoded": {
|
|
5623
5631
|
"schema": {
|
|
5624
5632
|
"type": "object",
|
|
5625
|
-
"required": [
|
|
5626
|
-
"widget_id"
|
|
5627
|
-
],
|
|
5628
5633
|
"properties": {
|
|
5629
5634
|
"widget_id": {
|
|
5630
5635
|
"type": "integer"
|
|
@@ -5647,6 +5652,9 @@
|
|
|
5647
5652
|
"_clear_fields": {
|
|
5648
5653
|
"$ref": "#/components/schemas/_ClearFields"
|
|
5649
5654
|
},
|
|
5655
|
+
"customize": {
|
|
5656
|
+
"type": "boolean"
|
|
5657
|
+
},
|
|
5650
5658
|
"replace": {
|
|
5651
5659
|
"type": "array",
|
|
5652
5660
|
"description": "Change part of a code field: [{field, find, with}]. If find occurs more than once, add nth (1 = first) or all:true. All or nothing; returns an undo.",
|
|
@@ -5699,7 +5707,7 @@
|
|
|
5699
5707
|
}
|
|
5700
5708
|
}
|
|
5701
5709
|
},
|
|
5702
|
-
"description": "Update an existing widget by `widget_id`. Fields omitted are untouched. Writes live data. Small edit: `replace` (no need to read or resend the field).\n\n**Cache refresh is automatic.** Response includes `auto_cache_refreshed: true` after successful writes; no manual `refreshSiteCache` call needed. If `auto_cache_refreshed: false`, check `auto_cache_refresh_error` and retry `refreshSiteCache` once.\n\n**Use when:** editing widget HTML (`widget_data`), CSS (`widget_style`), JS (`widget_javascript`), or metadata. Any page or email referencing this widget via `[widget=Name]` shortcode will render the updated content on next view.\n\n**Required:** `widget_id
|
|
5710
|
+
"description": "Update an existing widget by `widget_id`. Fields omitted are untouched. Writes live data. Small edit: `replace` (no need to read or resend the field).\n\n**Cache refresh is automatic.** Response includes `auto_cache_refreshed: true` after successful writes; no manual `refreshSiteCache` call needed. If `auto_cache_refreshed: false`, check `auto_cache_refresh_error` and retry `refreshSiteCache` once.\n\n**Use when:** editing widget HTML (`widget_data`), CSS (`widget_style`), JS (`widget_javascript`), or metadata. Any page or email referencing this widget via `[widget=Name]` shortcode will render the updated content on next view.\n\n**Required:** `widget_id`; for BD's own widget, `widget_name` + `replace` + `customize: true` (makes this site's copy).\n\n**Common edits:**\n\n- Content: `widget_data`, `widget_style`, `widget_javascript`\n\n- Framework: `mobile_enabled`, `ssl_enabled`\n\n**Renaming via `widget_name`:** **DO NOT pass `widget_name` unless the user explicitly asks to rename the widget.** Renaming a widget breaks every `[widget=Name]` shortcode reference to its old name on every page/email — silently. If the user does ask: same format rules as create (`[A-Za-z0-9 -+_]+`, runtime-rejected on bad chars); on collision follow the auto-suffix flow (`-v2`, `-v3`, ... up to `-v10`).\n\n**See also:** `createWidget` (add new), `deleteWidget` (remove).\n\n**Returns:** `{ status: \"success\", message: {...updatedRecord}, auto_cache_refreshed: true|false, auto_cache_refresh_error?: \"...\" }`.\n\n\nFor the full field list, see `listWidgets`.\n\n"
|
|
5703
5711
|
}
|
|
5704
5712
|
},
|
|
5705
5713
|
"/api/v2/data_widgets/delete": {
|