@edgedev/create-edge-app 1.2.34 → 1.2.35
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/README.md +1 -0
- package/agents.md +95 -2
- package/deploy.sh +60 -1
- package/edge/components/cms/block.vue +763 -301
- package/edge/components/cms/blockEditor.vue +342 -29
- package/edge/components/cms/blockPicker.vue +3 -3
- package/edge/components/cms/blocksManager.vue +48 -13
- package/edge/components/cms/htmlContent.vue +601 -10
- package/edge/components/cms/init_blocks/contact_us.html +55 -47
- package/edge/components/cms/init_blocks/newsletter.html +56 -96
- package/edge/components/cms/menu.vue +92 -31
- package/edge/components/cms/page.vue +165 -50
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +15 -4
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge-pull.sh +16 -2
- package/edge-push.sh +9 -1
- package/edge-remote.sh +20 -0
- package/edge-status.sh +9 -5
- package/edge-update-all.sh +127 -0
- package/package.json +1 -1
|
@@ -120,6 +120,11 @@ const BLOCK_CONTENT_SNIPPETS = [
|
|
|
120
120
|
snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
|
|
121
121
|
description: 'Simple text field placeholder',
|
|
122
122
|
},
|
|
123
|
+
{
|
|
124
|
+
label: 'Text with Options',
|
|
125
|
+
snippet: '{{{#text {"field":"fieldName","title":"Field Label","option":{"field":"fieldName","options":[{"title":"Option 1","name":"option1"},{"title":"Option 2","name":"option2"}],"optionsKey":"title","optionsValue":"name"},"value":"option1"}}}}',
|
|
126
|
+
description: 'Text field with selectable options',
|
|
127
|
+
},
|
|
123
128
|
{
|
|
124
129
|
label: 'Text Area',
|
|
125
130
|
snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
|
|
@@ -898,6 +903,7 @@ const exportCurrentBlock = () => {
|
|
|
898
903
|
:theme="theme"
|
|
899
904
|
:edit-mode="true"
|
|
900
905
|
:contain-fixed="true"
|
|
906
|
+
:disable-interactive-preview-in-edit="false"
|
|
901
907
|
:allow-delete="false"
|
|
902
908
|
:viewport-mode="previewViewportMode"
|
|
903
909
|
:block-id="state.previewBlock.id"
|
|
@@ -922,13 +928,16 @@ const exportCurrentBlock = () => {
|
|
|
922
928
|
</SheetHeader>
|
|
923
929
|
<div class="px-6 pb-6">
|
|
924
930
|
<Tabs class="w-full" default-value="guide">
|
|
925
|
-
<TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-
|
|
931
|
+
<TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-5">
|
|
926
932
|
<TabsTrigger value="guide" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
927
933
|
Block Guide
|
|
928
934
|
</TabsTrigger>
|
|
929
935
|
<TabsTrigger value="carousel" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
930
936
|
Carousel Usage
|
|
931
937
|
</TabsTrigger>
|
|
938
|
+
<TabsTrigger value="form-helpers" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
939
|
+
Form Helpers
|
|
940
|
+
</TabsTrigger>
|
|
932
941
|
<TabsTrigger value="nav-bar" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
933
942
|
Nav Bar
|
|
934
943
|
</TabsTrigger>
|
|
@@ -959,6 +968,7 @@ const exportCurrentBlock = () => {
|
|
|
959
968
|
<a href="#arrays-filters" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Filters</a>
|
|
960
969
|
<a href="#conditionals" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Conditionals</a>
|
|
961
970
|
<a href="#subarrays" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Subarrays</a>
|
|
971
|
+
<a href="#entries" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Entries</a>
|
|
962
972
|
<a href="#rendering-rules" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Rendering</a>
|
|
963
973
|
<a href="#loading-tokens" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Loading</a>
|
|
964
974
|
<a href="#validation" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Validation</a>
|
|
@@ -1243,6 +1253,29 @@ const exportCurrentBlock = () => {
|
|
|
1243
1253
|
</p>
|
|
1244
1254
|
</section>
|
|
1245
1255
|
|
|
1256
|
+
<section id="entries" class="space-y-3">
|
|
1257
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1258
|
+
Entries (Object Key/Value Loops)
|
|
1259
|
+
</h3>
|
|
1260
|
+
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>{{{#entries:pair {"field":"settings","value":{"theme":"dark","ctaText":"Contact Us"}}}}}
|
|
1261
|
+
<div><strong>{{pair.key}}</strong>: {{pair.value}}</div>
|
|
1262
|
+
{{{/entries}}}
|
|
1263
|
+
|
|
1264
|
+
{{{#entries:group {"field":"groupedItems","value":{"featured":["One","Two"],"archive":["Three"]}}}}}
|
|
1265
|
+
<h4>{{group.key}}</h4>
|
|
1266
|
+
{{{#subarray:child {"field":"item.value","value":[]}}}}
|
|
1267
|
+
<div>{{child}}</div>
|
|
1268
|
+
{{{/subarray}}}
|
|
1269
|
+
{{{/entries}}}</code></pre>
|
|
1270
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1271
|
+
<div><code>entries</code> loops object fields instead of arrays.</div>
|
|
1272
|
+
<div>Use it at the root or inside other loops; it does not need to be inside <code>subarray</code>.</div>
|
|
1273
|
+
<div>Each iteration exposes <code>item.key</code> and <code>item.value</code>, plus alias access like <code v-pre>{{pair.key}}</code>.</div>
|
|
1274
|
+
<div>If a value is an array, use nested <code>subarray</code> on <code>item.value</code>.</div>
|
|
1275
|
+
<div>If <code>field</code> is not an object, it renders nothing.</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
</section>
|
|
1278
|
+
|
|
1246
1279
|
<section id="rendering-rules" class="space-y-2">
|
|
1247
1280
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1248
1281
|
Rendering Rules
|
|
@@ -1372,7 +1405,6 @@ const exportCurrentBlock = () => {
|
|
|
1372
1405
|
</h3>
|
|
1373
1406
|
<p class="text-sm text-foreground">
|
|
1374
1407
|
Add <code>data-carousel</code> markup to any CMS block and the runtime auto-initializes Embla on the client.
|
|
1375
|
-
This is initialized in <code>htmlContent.vue</code> and works inside raw block HTML.
|
|
1376
1408
|
</p>
|
|
1377
1409
|
</section>
|
|
1378
1410
|
|
|
@@ -1488,6 +1520,14 @@ const exportCurrentBlock = () => {
|
|
|
1488
1520
|
<div><code>cms-nav-overlay</code>: backdrop click-to-close (optional but recommended).</div>
|
|
1489
1521
|
<div><code>cms-nav-close</code>: explicit close button in panel (optional).</div>
|
|
1490
1522
|
<div><code>cms-nav-link</code>: links that should close panel on click (optional).</div>
|
|
1523
|
+
<div><code>cms-nav-folder</code>: desktop folder wrapper for dropdown behavior (recommended).</div>
|
|
1524
|
+
<div><code>cms-nav-folder-toggle</code>: desktop folder trigger link/button (recommended).</div>
|
|
1525
|
+
<div><code>cms-nav-folder-menu</code>: desktop dropdown menu panel for folder items (recommended).</div>
|
|
1526
|
+
<div><code>cms-nav-main</code>: optional hook for scroll/sticky/hide classes (defaults to first <code><nav></code>).</div>
|
|
1527
|
+
<div><code>cms-nav-pos-right</code>, <code>cms-nav-pos-left</code>, <code>cms-nav-pos-center</code>: helper classes for menu position behavior.</div>
|
|
1528
|
+
<div><code>cms-nav-layout</code>, <code>cms-nav-logo</code>, <code>cms-nav-desktop</code>: optional structure hooks for precise layout mapping.</div>
|
|
1529
|
+
<div><code>cms-nav-sticky</code>: force sticky top behavior even if your nav did not include fixed classes.</div>
|
|
1530
|
+
<div><code>cms-nav-hide-on-down</code>: hide nav on scroll down, show on scroll up.</div>
|
|
1491
1531
|
</div>
|
|
1492
1532
|
</section>
|
|
1493
1533
|
|
|
@@ -1499,6 +1539,12 @@ const exportCurrentBlock = () => {
|
|
|
1499
1539
|
<div><code>data-cms-nav-open="true"</code> to start open.</div>
|
|
1500
1540
|
<div><code>data-cms-nav-open-class="your-class"</code> to change the root open class (default <code>is-open</code>).</div>
|
|
1501
1541
|
<div><code>data-cms-nav-close-on-link="false"</code> to keep panel open after link clicks.</div>
|
|
1542
|
+
<div><code>data-cms-nav-position="right|left|center"</code> as an alternative to helper classes.</div>
|
|
1543
|
+
<div><code>data-cms-nav-scrolled-class</code> / <code>data-cms-nav-top-class</code>: classes toggled on nav main target.</div>
|
|
1544
|
+
<div><code>data-cms-nav-scrolled-row-class</code> / <code>data-cms-nav-top-row-class</code>: classes toggled on <code>cms-nav-layout</code> for shrink/expand.</div>
|
|
1545
|
+
<div><code>data-cms-nav-scroll-threshold</code>: px before “scrolled” classes apply (default 10).</div>
|
|
1546
|
+
<div><code>data-cms-nav-hide-on-down="true"</code>, <code>data-cms-nav-hide-threshold</code> (default 80), <code>data-cms-nav-hide-delta</code> (default 6).</div>
|
|
1547
|
+
<div><code>data-cms-nav-hidden-class</code> / <code>data-cms-nav-visible-class</code> / <code>data-cms-nav-transition-class</code> for hide/show animation control.</div>
|
|
1502
1548
|
</div>
|
|
1503
1549
|
</section>
|
|
1504
1550
|
|
|
@@ -1506,26 +1552,65 @@ const exportCurrentBlock = () => {
|
|
|
1506
1552
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1507
1553
|
Nav Block Template (Copy / Paste)
|
|
1508
1554
|
</h3>
|
|
1509
|
-
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code><div class="cms-nav-root" data-cms-nav-root data-cms-nav-close-on-link="true">
|
|
1510
|
-
|
|
1511
|
-
|
|
1555
|
+
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code><div class="cms-nav-root cms-nav-sticky" data-cms-nav-root data-cms-nav-position="{{{#text {"field":"navPosition","title":"Menu Position","option":{"field":"navPosition","options":[{"title":"Right","name":"right"},{"title":"Left","name":"left"},{"title":"Center","name":"center"}],"optionsKey":"title","optionsValue":"name"},"value":"right"}}}}" data-cms-nav-close-on-link="true" data-cms-nav-top-class="bg-transparent border-transparent" data-cms-nav-scrolled-class="bg-navBg/80 backdrop-blur-lg shadow-lg" data-cms-nav-top-row-class="h-[64px] md:h-[88px] py-6 md:py-8" data-cms-nav-scrolled-row-class="h-[56px] md:h-[68px] py-5 md:py-4">
|
|
1556
|
+
{{{#array {"field":"siteDoc","collection":{"path":"sites","uniqueKey":"{orgId}","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
|
|
1557
|
+
<nav class="cms-nav-main fixed inset-x-0 top-0 z-30 w-full bg-transparent text-navText">
|
|
1512
1558
|
<div class="relative w-full px-6 md:px-12">
|
|
1513
|
-
<div class="flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8">
|
|
1514
|
-
<a href="/" class="cursor-pointer text-xl text-navText">
|
|
1515
|
-
|
|
1559
|
+
<div class="cms-nav-layout flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8">
|
|
1560
|
+
<a href="/" class="cms-nav-logo cursor-pointer text-xl text-navText">
|
|
1561
|
+
{{{#if {"cond":"item.logoLight"}}}}
|
|
1562
|
+
<img src="{{item.logoLight}}" class="h-[56px] md:h-[72px] py-3" />
|
|
1563
|
+
{{{#else}}}
|
|
1564
|
+
<img src="{{item.logo}}" class="h-[56px] md:h-[72px] py-3" />
|
|
1565
|
+
{{{/if}}}
|
|
1516
1566
|
</a>
|
|
1517
1567
|
|
|
1518
|
-
<div class="ml-auto flex items-center gap-2">
|
|
1519
|
-
<ul class="hidden lg:flex items-center
|
|
1520
|
-
{{{#subarray:
|
|
1521
|
-
<li class="relative group">
|
|
1522
|
-
{{{#if {"cond":"
|
|
1523
|
-
<a href="{{
|
|
1568
|
+
<div class="cms-nav-desktop ml-auto flex items-center gap-2">
|
|
1569
|
+
<ul class="hidden lg:flex items-center gap-x-[20px] pt-1 text-sm uppercase tracking-widest list-none m-0 p-0 [&>li]:m-0 [&>li>a]:m-0">
|
|
1570
|
+
{{{#subarray:menuItem {"field":"item.menus.Site Root","limit":5,"value":[]}}}}
|
|
1571
|
+
<li class="relative group cms-nav-folder" data-cms-nav-folder>
|
|
1572
|
+
{{{#if {"cond":"menuItem.item.type == 'external'"}}}}
|
|
1573
|
+
<a href="{{menuItem.item.url}}" class="cursor-pointer">{{menuItem.name}}</a>
|
|
1524
1574
|
{{{#else}}}
|
|
1525
|
-
{{{#if {"cond":"
|
|
1526
|
-
|
|
1575
|
+
{{{#if {"cond":"menuItem.item == '[object Object]'"}}}}
|
|
1576
|
+
{{{#entries:folderEntry {"field":"menuItem.item","value":{}}}}}
|
|
1577
|
+
{{{#if {"cond":"folderEntry.key == 'home'"}}}}
|
|
1578
|
+
<a href="/" class="cms-nav-folder-toggle cursor-pointer text-sideNavText" data-cms-nav-folder-toggle>{{menuItem.menuTitle}}</a>
|
|
1527
1579
|
{{{#else}}}
|
|
1528
|
-
<a href="/{{
|
|
1580
|
+
<a href="/{{folderEntry.key}}" class="cms-nav-folder-toggle cursor-pointer text-sideNavText" data-cms-nav-folder-toggle>{{menuItem.menuTitle}}</a>
|
|
1581
|
+
{{{/if}}}
|
|
1582
|
+
<div class="cms-nav-folder-menu absolute left-0 top-full z-40 hidden min-w-max whitespace-nowrap bg-sideNavBg text-sideNavText py-2 text-left px-12 normal-case tracking-normal shadow-xl" data-cms-nav-folder-menu>
|
|
1583
|
+
<ul>
|
|
1584
|
+
{{{#subarray:folderChild {"field":"item.value","value":[]}}}}
|
|
1585
|
+
<li class="py-1">
|
|
1586
|
+
{{{#if {"cond":"folderChild.item.type == 'external'"}}}}
|
|
1587
|
+
<a href="{{folderChild.item.url}}" class="block cursor-pointer whitespace-nowrap text-sideNavText">{{folderChild.name}}</a>
|
|
1588
|
+
{{{#else}}}
|
|
1589
|
+
{{{#if {"cond":"folderChild.menuTitle"}}}}
|
|
1590
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="block cursor-pointer whitespace-nowrap text-sideNavText">{{folderChild.menuTitle}}</a>
|
|
1591
|
+
{{{#else}}}
|
|
1592
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="block cursor-pointer whitespace-nowrap text-sideNavText">{{folderChild.name}}</a>
|
|
1593
|
+
{{{/if}}}
|
|
1594
|
+
{{{/if}}}
|
|
1595
|
+
</li>
|
|
1596
|
+
{{{/subarray}}}
|
|
1597
|
+
</ul>
|
|
1598
|
+
</div>
|
|
1599
|
+
{{{/entries}}}
|
|
1600
|
+
{{{#else}}}
|
|
1601
|
+
{{{#if {"cond":"menuItem.name == 'home'"}}}}
|
|
1602
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1603
|
+
<a href="/" class="cursor-pointer">{{menuItem.menuTitle}}</a>
|
|
1604
|
+
{{{#else}}}
|
|
1605
|
+
<a href="/" class="cursor-pointer">{{menuItem.name}}</a>
|
|
1606
|
+
{{{/if}}}
|
|
1607
|
+
{{{#else}}}
|
|
1608
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1609
|
+
<a href="/{{menuItem.name}}" class="cursor-pointer">{{menuItem.menuTitle}}</a>
|
|
1610
|
+
{{{#else}}}
|
|
1611
|
+
<a href="/{{menuItem.name}}" class="cursor-pointer">{{menuItem.name}}</a>
|
|
1612
|
+
{{{/if}}}
|
|
1613
|
+
{{{/if}}}
|
|
1529
1614
|
{{{/if}}}
|
|
1530
1615
|
{{{/if}}}
|
|
1531
1616
|
</li>
|
|
@@ -1540,34 +1625,118 @@ const exportCurrentBlock = () => {
|
|
|
1540
1625
|
</div>
|
|
1541
1626
|
</div>
|
|
1542
1627
|
</div>
|
|
1543
|
-
{{{/array}}}
|
|
1544
1628
|
</nav>
|
|
1545
1629
|
|
|
1546
1630
|
<div class="cms-nav-overlay fixed inset-0 z-[110] bg-black/50 transition-opacity duration-300 opacity-0 pointer-events-none"></div>
|
|
1547
1631
|
|
|
1548
1632
|
<aside class="cms-nav-panel fixed inset-y-0 right-0 z-[120] w-full max-w-md bg-sideNavBg text-sideNavText transition-all duration-300 translate-x-full opacity-0 pointer-events-none">
|
|
1549
|
-
<div class="relative h-full overflow-y-auto px-8 py-10">
|
|
1633
|
+
<div class="relative flex h-full flex-col overflow-y-auto px-8 py-10 text-center">
|
|
1550
1634
|
<button type="button" class="cms-nav-close absolute right-6 top-6 text-4xl text-sideNavText">&times;</button>
|
|
1551
1635
|
|
|
1552
|
-
<
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
<
|
|
1636
|
+
<div class="mb-8 mt-2 flex items-center justify-center gap-4">
|
|
1637
|
+
<a href="/" class="flex items-center gap-4 text-navText">
|
|
1638
|
+
<img src="{{item.logo}}" class="h-[30px] w-auto max-w-full object-contain" />
|
|
1639
|
+
{{{#if {"cond":"item.brandLogoDark"}}}}
|
|
1640
|
+
<span class="h-10 w-px bg-black" aria-hidden="true"></span>
|
|
1641
|
+
<img src="{{item.brandLogoDark}}" class="h-[30px] w-auto max-w-full object-contain" />
|
|
1642
|
+
{{{/if}}}
|
|
1643
|
+
</a>
|
|
1644
|
+
</div>
|
|
1645
|
+
|
|
1646
|
+
<ul class="w-full space-y-4 border-b border-black pb-4 uppercase">
|
|
1647
|
+
{{{#subarray:menuItem {"field":"item.menus.Site Root","value":[]}}}}
|
|
1648
|
+
<li class="border-t border-black pt-4">
|
|
1649
|
+
{{{#if {"cond":"menuItem.item.type == 'external'"}}}}
|
|
1650
|
+
<a href="{{menuItem.item.url}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.name}}</a>
|
|
1651
|
+
{{{#else}}}
|
|
1652
|
+
{{{#if {"cond":"menuItem.item == '[object Object]'"}}}}
|
|
1653
|
+
{{{#entries:folderEntry {"field":"menuItem.item","value":{}}}}}
|
|
1654
|
+
{{{#if {"cond":"folderEntry.key == 'home'"}}}}
|
|
1655
|
+
<a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1558
1656
|
{{{#else}}}
|
|
1559
|
-
{{
|
|
1560
|
-
|
|
1657
|
+
<a href="/{{folderEntry.key}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1658
|
+
{{{/if}}}
|
|
1659
|
+
<ul class="mt-2 space-y-2 border-l border-black/40 pl-4">
|
|
1660
|
+
{{{#subarray:folderChild {"field":"item.value","value":[]}}}}
|
|
1661
|
+
<li>
|
|
1662
|
+
{{{#if {"cond":"folderChild.item.type == 'external'"}}}}
|
|
1663
|
+
<a href="{{folderChild.item.url}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs">{{folderChild.name}}</a>
|
|
1664
|
+
{{{#else}}}
|
|
1665
|
+
{{{#if {"cond":"folderChild.menuTitle"}}}}
|
|
1666
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs">{{folderChild.menuTitle}}</a>
|
|
1667
|
+
{{{#else}}}
|
|
1668
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs">{{folderChild.name}}</a>
|
|
1669
|
+
{{{/if}}}
|
|
1670
|
+
{{{/if}}}
|
|
1671
|
+
</li>
|
|
1672
|
+
{{{/subarray}}}
|
|
1673
|
+
</ul>
|
|
1674
|
+
{{{/entries}}}
|
|
1675
|
+
{{{#else}}}
|
|
1676
|
+
{{{#if {"cond":"menuItem.name == 'home'"}}}}
|
|
1677
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1678
|
+
<a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1679
|
+
{{{#else}}}
|
|
1680
|
+
<a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.name}}</a>
|
|
1681
|
+
{{{/if}}}
|
|
1561
1682
|
{{{#else}}}
|
|
1562
|
-
|
|
1683
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1684
|
+
<a href="/{{menuItem.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1685
|
+
{{{#else}}}
|
|
1686
|
+
<a href="/{{menuItem.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.name}}</a>
|
|
1687
|
+
{{{/if}}}
|
|
1688
|
+
{{{/if}}}
|
|
1563
1689
|
{{{/if}}}
|
|
1564
1690
|
{{{/if}}}
|
|
1565
1691
|
</li>
|
|
1566
1692
|
{{{/subarray}}}
|
|
1567
|
-
{{{/array}}}
|
|
1568
1693
|
</ul>
|
|
1694
|
+
|
|
1695
|
+
<div class="mt-10 flex w-full items-center justify-center gap-4">
|
|
1696
|
+
{{{#if {"cond":"item.socialFacebook"}}}}
|
|
1697
|
+
<a href="{{item.socialFacebook}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1698
|
+
<span class="sr-only">Facebook</span>
|
|
1699
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1700
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
|
1701
|
+
<path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4 .4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z"></path>
|
|
1702
|
+
</svg>
|
|
1703
|
+
</span>
|
|
1704
|
+
</a>
|
|
1705
|
+
{{{/if}}}
|
|
1706
|
+
{{{#if {"cond":"item.socialInstagram"}}}}
|
|
1707
|
+
<a href="{{item.socialInstagram}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1708
|
+
<span class="sr-only">Instagram</span>
|
|
1709
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1710
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
1711
|
+
<path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"></path>
|
|
1712
|
+
</svg>
|
|
1713
|
+
</span>
|
|
1714
|
+
</a>
|
|
1715
|
+
{{{/if}}}
|
|
1716
|
+
{{{#if {"cond":"item.socialLinkedIn"}}}}
|
|
1717
|
+
<a href="{{item.socialLinkedIn}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1718
|
+
<span class="sr-only">LinkedIn</span>
|
|
1719
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1720
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
1721
|
+
<path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"></path>
|
|
1722
|
+
</svg>
|
|
1723
|
+
</span>
|
|
1724
|
+
</a>
|
|
1725
|
+
{{{/if}}}
|
|
1726
|
+
{{{#if {"cond":"item.socialYouTube"}}}}
|
|
1727
|
+
<a href="{{item.socialYouTube}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1728
|
+
<span class="sr-only">YouTube</span>
|
|
1729
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1730
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
|
1731
|
+
<path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"></path>
|
|
1732
|
+
</svg>
|
|
1733
|
+
</span>
|
|
1734
|
+
</a>
|
|
1735
|
+
{{{/if}}}
|
|
1736
|
+
</div>
|
|
1569
1737
|
</div>
|
|
1570
1738
|
</aside>
|
|
1739
|
+
{{{/array}}}
|
|
1571
1740
|
</div></code></pre>
|
|
1572
1741
|
</section>
|
|
1573
1742
|
|
|
@@ -1579,8 +1748,152 @@ const exportCurrentBlock = () => {
|
|
|
1579
1748
|
<div>Clicking the nav button opens the slide-out in Block Editor preview and Page Preview mode.</div>
|
|
1580
1749
|
<div>Interactive nav elements do not trigger “Edit Block”. Clicking outside them still opens the editor in edit mode.</div>
|
|
1581
1750
|
<div>In CMS preview, fixed nav and panel are contained to the preview surface by the block wrapper.</div>
|
|
1751
|
+
<div><code>cms-nav-pos-left</code> also switches the slide-out panel to the left side.</div>
|
|
1752
|
+
</div>
|
|
1753
|
+
</section>
|
|
1754
|
+
</div>
|
|
1755
|
+
</div>
|
|
1756
|
+
</TabsContent>
|
|
1757
|
+
|
|
1758
|
+
<TabsContent value="form-helpers">
|
|
1759
|
+
<div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
|
|
1760
|
+
<div class="space-y-6">
|
|
1761
|
+
<section class="space-y-2">
|
|
1762
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1763
|
+
What This Does
|
|
1764
|
+
</h3>
|
|
1765
|
+
<p class="text-sm text-foreground">
|
|
1766
|
+
Add helper classes or data attributes to a CMS block form, and the client runtime will submit to
|
|
1767
|
+
<code>/api/contact</code> with anti-bot checks and submit history tracking.
|
|
1768
|
+
</p>
|
|
1769
|
+
</section>
|
|
1770
|
+
|
|
1771
|
+
<section class="space-y-2">
|
|
1772
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1773
|
+
CMS Preview Scope
|
|
1774
|
+
</h3>
|
|
1775
|
+
<p class="text-sm text-foreground">
|
|
1776
|
+
In Block Editor, this is for structure and messaging preview only. Use it to verify markup and required-state UX,
|
|
1777
|
+
not to validate end-to-end delivery.
|
|
1778
|
+
</p>
|
|
1779
|
+
</section>
|
|
1780
|
+
|
|
1781
|
+
<section class="space-y-2">
|
|
1782
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1783
|
+
Helper Contract
|
|
1784
|
+
</h3>
|
|
1785
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1786
|
+
<div><code>form.cms-form</code> or <code>form[data-cms-form]</code>: form root.</div>
|
|
1787
|
+
<div><code>.cms-form-required</code> or <code>[data-cms-required="true"]</code>: required field markers.</div>
|
|
1788
|
+
<div><code>.cms-form-submit</code> or <code>[data-cms-form-submit]</code>: submit button.</div>
|
|
1789
|
+
<div><code>.cms-form-message</code> or <code>[data-cms-form-message]</code>: status/error message container.</div>
|
|
1790
|
+
</div>
|
|
1791
|
+
</section>
|
|
1792
|
+
|
|
1793
|
+
<section class="space-y-2">
|
|
1794
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1795
|
+
Defaults + Messages
|
|
1796
|
+
</h3>
|
|
1797
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1798
|
+
<div>Default endpoint: <code>/api/contact</code>.</div>
|
|
1799
|
+
<div><code>data-cms-success-message</code>: override success copy.</div>
|
|
1800
|
+
<div><code>data-cms-error-message</code>: override error copy.</div>
|
|
1801
|
+
<div><code>data-cms-required-message</code>: override required-field copy.</div>
|
|
1582
1802
|
</div>
|
|
1583
1803
|
</section>
|
|
1804
|
+
|
|
1805
|
+
<section class="space-y-2">
|
|
1806
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1807
|
+
Context IDs
|
|
1808
|
+
</h3>
|
|
1809
|
+
<p class="text-sm text-foreground">
|
|
1810
|
+
Block/Page/Site/Org IDs are inherited from the CMS HTML wrapper automatically, so forms in blocks
|
|
1811
|
+
do not need manual context wiring.
|
|
1812
|
+
</p>
|
|
1813
|
+
</section>
|
|
1814
|
+
|
|
1815
|
+
<section class="space-y-3">
|
|
1816
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1817
|
+
Contact Form Example (Block HTML)
|
|
1818
|
+
</h3>
|
|
1819
|
+
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code><section
|
|
1820
|
+
class="relative cms-block cms-block-contact-form-placeholder rounded-2xl border border-dashed border-slate-300 bg-slate-50/70 px-4 py-6 sm:px-6 sm:py-8"
|
|
1821
|
+
data-block-type="contact-form-placeholder"
|
|
1822
|
+
>
|
|
1823
|
+
<div class="mx-auto max-w-3xl pt-6">
|
|
1824
|
+
<div class="mb-6 space-y-2 text-center sm:text-left">
|
|
1825
|
+
<h2 class="text-xl font-semibold text-slate-900">
|
|
1826
|
+
{{{#text {"field":"formHeader","title":"Form Header","value":"Contact Us"}}}}
|
|
1827
|
+
</h2>
|
|
1828
|
+
<p class="text-sm text-slate-600">
|
|
1829
|
+
{{{#text {"field":"formSubheader","title":"Form Subheader","value":"Subheader content"}}}}
|
|
1830
|
+
</p>
|
|
1831
|
+
</div>
|
|
1832
|
+
|
|
1833
|
+
<form
|
|
1834
|
+
class="cms-form space-y-4"
|
|
1835
|
+
data-cms-form
|
|
1836
|
+
data-cms-required-message="Please complete all required fields."
|
|
1837
|
+
data-cms-success-message="Thanks! Your message has been sent."
|
|
1838
|
+
data-cms-error-message="Sorry, we could not send your message. Please try again."
|
|
1839
|
+
data-cms-success-class="cms-form-message cms-form-message-success"
|
|
1840
|
+
data-cms-error-class="cms-form-message cms-form-message-error"
|
|
1841
|
+
data-cms-invalid-class="cms-form-field-invalid"
|
|
1842
|
+
data-cms-working-class="cms-form-submitting"
|
|
1843
|
+
>
|
|
1844
|
+
<!-- Honeypot (optional, used by helper if present) -->
|
|
1845
|
+
<div class="pointer-events-none absolute -left-[9999px] top-auto h-px w-px overflow-hidden opacity-0" aria-hidden="true">
|
|
1846
|
+
<label for="cms-company">Company</label>
|
|
1847
|
+
<input id="cms-company" name="company" type="text" tabindex="-1" autocomplete="off" />
|
|
1848
|
+
</div>
|
|
1849
|
+
|
|
1850
|
+
<div class="space-y-4">
|
|
1851
|
+
{{{#array {"field":"formFields","schema":[{"field":"fieldName","type":"text","title":"Field Label"},{"field":"fieldType","type":"option","title":"Field Type","option":{"optionsKey":"title","optionsValue":"value","options":[{"title":"Text","value":"text"},{"title":"Email","value":"email"},{"title":"Phone","value":"tel"},{"title":"Textarea","value":"textarea"}]},"value":"text"},{"field":"fieldRequired","type":"option","title":"Required","option":{"optionsKey":"title","optionsValue":"value","options":[{"title":"Yes","value":"true"},{"title":"No","value":"false"}]},"value":"true"}],"value":[{"fieldName":"Name","fieldType":"text","fieldRequired":"true"},{"fieldName":"Email","fieldType":"email","fieldRequired":"true"},{"fieldName":"Message","fieldType":"textarea","fieldRequired":"true"}]}}}}
|
|
1852
|
+
<div class="space-y-1">
|
|
1853
|
+
<label class="text-xs font-medium uppercase tracking-wide text-slate-600">
|
|
1854
|
+
{{item.fieldName}}
|
|
1855
|
+
</label>
|
|
1856
|
+
|
|
1857
|
+
{{{#if {"cond":"item.fieldType == 'textarea'"}}}}
|
|
1858
|
+
<textarea
|
|
1859
|
+
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
|
|
1860
|
+
data-cms-required="{{item.fieldRequired}}"
|
|
1861
|
+
name="{{item.fieldName}}"
|
|
1862
|
+
placeholder="{{item.fieldName}}"
|
|
1863
|
+
rows="6"
|
|
1864
|
+
></textarea>
|
|
1865
|
+
{{{#else}}}
|
|
1866
|
+
<input
|
|
1867
|
+
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
|
|
1868
|
+
data-cms-required="{{item.fieldRequired}}"
|
|
1869
|
+
type="{{item.fieldType}}"
|
|
1870
|
+
name="{{item.fieldName}}"
|
|
1871
|
+
placeholder="{{item.fieldName}}"
|
|
1872
|
+
/>
|
|
1873
|
+
{{{/if}}}
|
|
1874
|
+
</div>
|
|
1875
|
+
{{{/array}}}
|
|
1876
|
+
</div>
|
|
1877
|
+
|
|
1878
|
+
<div class="mt-6">
|
|
1879
|
+
<button
|
|
1880
|
+
type="submit"
|
|
1881
|
+
class="cms-form-submit inline-flex w-full items-center justify-center rounded-lg bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
|
|
1882
|
+
data-cms-form-submit
|
|
1883
|
+
>
|
|
1884
|
+
{{{#text {"field":"buttonText","title":"Button Text","value":"Send Message"}}}}
|
|
1885
|
+
</button>
|
|
1886
|
+
</div>
|
|
1887
|
+
|
|
1888
|
+
<p class="cms-form-message hidden text-sm" data-cms-form-message></p>
|
|
1889
|
+
</form>
|
|
1890
|
+
|
|
1891
|
+
<div class="hidden">
|
|
1892
|
+
{{{#text {"field":"emailTo","title":"Email To","value":"test@testing.com"}}}}
|
|
1893
|
+
</div>
|
|
1894
|
+
</div>
|
|
1895
|
+
</section></code></pre>
|
|
1896
|
+
</section>
|
|
1584
1897
|
</div>
|
|
1585
1898
|
</div>
|
|
1586
1899
|
</TabsContent>
|
|
@@ -334,7 +334,7 @@ const clearTagFilters = () => {
|
|
|
334
334
|
</script>
|
|
335
335
|
|
|
336
336
|
<template>
|
|
337
|
-
<div v-if="props.blockOverride" :class="previewSurfaceClass(blockOverridePreviewType)">
|
|
337
|
+
<div v-if="props.blockOverride" class="pointer-events-none" :class="previewSurfaceClass(blockOverridePreviewType)">
|
|
338
338
|
<edge-cms-block-api
|
|
339
339
|
:content="props.blockOverride.content"
|
|
340
340
|
:values="props.blockOverride.values"
|
|
@@ -386,7 +386,7 @@ const clearTagFilters = () => {
|
|
|
386
386
|
<div class="scale-wrapper">
|
|
387
387
|
<div
|
|
388
388
|
:ref="el => setInnerRef(block.docId, el)"
|
|
389
|
-
class="scale-inner scale p-4"
|
|
389
|
+
class="scale-inner scale p-4 pointer-events-none"
|
|
390
390
|
:class="previewSurfaceClass(block.previewType)"
|
|
391
391
|
:data-block-id="block.docId"
|
|
392
392
|
>
|
|
@@ -463,7 +463,7 @@ const clearTagFilters = () => {
|
|
|
463
463
|
<div class="scale-wrapper">
|
|
464
464
|
<div
|
|
465
465
|
:ref="el => setInnerRef(block.docId, el)"
|
|
466
|
-
class="scale-inner scale p-4"
|
|
466
|
+
class="scale-inner scale p-4 pointer-events-none"
|
|
467
467
|
:class="previewSurfaceClass(block.previewType)"
|
|
468
468
|
:data-block-id="block.docId"
|
|
469
469
|
>
|
|
@@ -43,7 +43,8 @@ const router = useRouter()
|
|
|
43
43
|
const blockImportInputRef = ref(null)
|
|
44
44
|
const blockImportDocIdResolver = ref(null)
|
|
45
45
|
const blockImportConflictResolver = ref(null)
|
|
46
|
-
const
|
|
46
|
+
const DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE = 'Failed to import block JSON.'
|
|
47
|
+
const OPTIONAL_BLOCK_IMPORT_KEYS = new Set(['previewType'])
|
|
47
48
|
|
|
48
49
|
const seedInitialBlocks = async () => {
|
|
49
50
|
console.log('Seeding initial blocks...')
|
|
@@ -415,7 +416,7 @@ const readTextFile = file => new Promise((resolve, reject) => {
|
|
|
415
416
|
|
|
416
417
|
const normalizeImportedDoc = (payload, fallbackDocId = '') => {
|
|
417
418
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
418
|
-
throw new Error(
|
|
419
|
+
throw new Error('Invalid JSON payload. Expected an object.')
|
|
419
420
|
|
|
420
421
|
if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
|
|
421
422
|
const normalized = { ...payload.document }
|
|
@@ -454,30 +455,48 @@ const getBlockDocDefaults = () => getDocDefaultsFromSchema(blockNewDocSchema.val
|
|
|
454
455
|
|
|
455
456
|
const validateImportedBlockDoc = (doc) => {
|
|
456
457
|
if (!isPlainObject(doc))
|
|
457
|
-
throw new Error(
|
|
458
|
+
throw new Error('Invalid block document. Expected an object.')
|
|
458
459
|
|
|
459
460
|
const requiredKeys = Object.keys(blockNewDocSchema.value || {})
|
|
461
|
+
.filter(key => !OPTIONAL_BLOCK_IMPORT_KEYS.has(key))
|
|
460
462
|
const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
|
|
461
463
|
if (missing.length)
|
|
462
|
-
throw new Error(
|
|
464
|
+
throw new Error(`Missing required block key(s): ${missing.join(', ')}`)
|
|
463
465
|
|
|
464
466
|
return doc
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
const validateImportedBlockThemes = (doc) => {
|
|
470
|
+
if (Object.prototype.hasOwnProperty.call(doc || {}, 'themes') && !Array.isArray(doc?.themes)) {
|
|
471
|
+
throw new Error('Invalid "themes" value. Expected an array of theme docIds.')
|
|
472
|
+
}
|
|
473
|
+
|
|
468
474
|
const importedThemes = Array.isArray(doc?.themes) ? doc.themes : []
|
|
469
475
|
if (!importedThemes.length)
|
|
470
476
|
return doc
|
|
471
477
|
|
|
472
478
|
const orgThemes = edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`] || {}
|
|
479
|
+
const missingThemeIds = []
|
|
480
|
+
let hasEmptyThemeId = false
|
|
473
481
|
const normalizedThemes = []
|
|
474
482
|
for (const themeId of importedThemes) {
|
|
475
483
|
const normalizedThemeId = String(themeId || '').trim()
|
|
476
|
-
if (!normalizedThemeId
|
|
477
|
-
|
|
484
|
+
if (!normalizedThemeId) {
|
|
485
|
+
hasEmptyThemeId = true
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
if (!orgThemes[normalizedThemeId]) {
|
|
489
|
+
missingThemeIds.push(normalizedThemeId)
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
478
492
|
normalizedThemes.push(normalizedThemeId)
|
|
479
493
|
}
|
|
480
494
|
|
|
495
|
+
if (hasEmptyThemeId)
|
|
496
|
+
throw new Error('Themes include an empty theme id.')
|
|
497
|
+
if (missingThemeIds.length)
|
|
498
|
+
throw new Error(`Theme id(s) not found in this organization: ${[...new Set(missingThemeIds)].join(', ')}`)
|
|
499
|
+
|
|
481
500
|
doc.themes = [...new Set(normalizedThemes)]
|
|
482
501
|
return doc
|
|
483
502
|
}
|
|
@@ -553,10 +572,29 @@ const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
|
|
|
553
572
|
}
|
|
554
573
|
|
|
555
574
|
const openImportErrorDialog = (message) => {
|
|
556
|
-
state.importErrorMessage = String(message ||
|
|
575
|
+
state.importErrorMessage = String(message || DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE)
|
|
557
576
|
state.importErrorDialogOpen = true
|
|
558
577
|
}
|
|
559
578
|
|
|
579
|
+
const getBlockImportFailureReason = (error, message) => {
|
|
580
|
+
if (error instanceof SyntaxError)
|
|
581
|
+
return `Invalid JSON syntax: ${String(error?.message || 'Unable to parse JSON.')}`
|
|
582
|
+
if (/^Import canceled\./i.test(message))
|
|
583
|
+
return message
|
|
584
|
+
return message
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const logBlockImportFailure = (file, error, message) => {
|
|
588
|
+
const fileName = String(file?.name || 'unknown-file')
|
|
589
|
+
const reason = getBlockImportFailureReason(error, message)
|
|
590
|
+
console.error(`[BlocksManager] Block import failed for "${fileName}". Reason: ${reason}`, {
|
|
591
|
+
fileName,
|
|
592
|
+
reason,
|
|
593
|
+
errorMessage: message,
|
|
594
|
+
error,
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
560
598
|
const triggerBlockImport = () => {
|
|
561
599
|
blockImportInputRef.value?.click()
|
|
562
600
|
}
|
|
@@ -614,14 +652,11 @@ const handleBlockImport = async (event) => {
|
|
|
614
652
|
await importSingleBlockFile(file, existingBlocks)
|
|
615
653
|
}
|
|
616
654
|
catch (error) {
|
|
617
|
-
|
|
618
|
-
|
|
655
|
+
const message = error?.message || DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE
|
|
656
|
+
logBlockImportFailure(file, error, message)
|
|
619
657
|
if (/^Import canceled\./i.test(message))
|
|
620
658
|
continue
|
|
621
|
-
|
|
622
|
-
openImportErrorDialog(INVALID_BLOCK_IMPORT_MESSAGE)
|
|
623
|
-
else
|
|
624
|
-
openImportErrorDialog(message)
|
|
659
|
+
openImportErrorDialog(getBlockImportFailureReason(error, message))
|
|
625
660
|
}
|
|
626
661
|
}
|
|
627
662
|
}
|