瀏覽代碼

Implement block previews in StreamField block chooser

Sage Abdullah 3 月之前
父節點
當前提交
d25a67a4b8

+ 1 - 0
.stylelintrc.js

@@ -56,6 +56,7 @@ module.exports = {
           'none',
           'unset',
           'transparent',
+          'normal',
           // System colors for forced-colors styling.
           // See https://drafts.csswg.org/css-color-4/#css-system-colors.
           'Canvas',

+ 1 - 0
client/scss/core.scss

@@ -80,6 +80,7 @@ These are classes for components.
 @import '../src/components/LoadingSpinner/LoadingSpinner';
 @import '../src/components/PublicationStatus/PublicationStatus';
 @import '../src/components/ComboBox/ComboBox';
+@import '../src/components/ComboBoxPreview/ComboBoxPreview';
 @import '../src/components/PageExplorer/PageExplorer';
 @import '../src/components/CommentApp/main';
 

+ 11 - 4
client/src/components/ComboBox/ComboBox.scss

@@ -3,15 +3,21 @@
 $spacing: theme('spacing.[2.5]');
 $spacing-sm: theme('spacing.5');
 
-.w-combobox {
-  width: min(400px, 80vw);
+.w-combobox-container {
   @include dark-theme() {
     background-color: theme('colors.surface-tooltip');
   }
+
+  display: grid;
+  grid-template-columns: 1fr;
+  @include media-breakpoint-up(sm) {
+    grid-template-columns: min(400px, 80vw) 1fr;
+  }
+
+  min-height: min(320px, 70vh);
   background: theme('colors.surface-page');
   color: theme('colors.text-context');
   border-radius: theme('borderRadius.DEFAULT');
-  font-size: theme('fontSize.18');
   box-shadow: theme('boxShadow.md');
   outline: 10px solid transparent;
 }
@@ -19,6 +25,7 @@ $spacing-sm: theme('spacing.5');
 .w-combobox__field {
   padding: $spacing;
   padding-bottom: 0;
+  font-size: theme('fontSize.18');
 
   @include media-breakpoint-up(sm) {
     padding: $spacing-sm;
@@ -48,7 +55,7 @@ $spacing-sm: theme('spacing.5');
   padding-top: 0;
 
   @include media-breakpoint-up(sm) {
-    width: 400px;
+    width: 100%;
     padding: $spacing-sm;
     padding-top: 0;
   }

+ 102 - 81
client/src/components/ComboBox/ComboBox.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
 import { useCombobox, UseComboboxStateChange } from 'downshift';
 
 import { gettext } from '../../utils/gettext';
+import ComboBoxPreview from '../ComboBoxPreview/ComboBoxPreview';
 import Icon from '../Icon/Icon';
 
 import findMatches from './findMatches';
@@ -21,6 +22,7 @@ export interface ComboBoxItem {
   label?: string | null;
   description?: string | null;
   icon?: string | JSX.Element | null;
+  blockDefId?: string;
   category?: string;
   render?: (props: { option: ComboBoxItem }) => JSX.Element | string;
 }
@@ -84,6 +86,7 @@ export default function ComboBox<ComboBoxOption extends ComboBoxItem>({
     getMenuProps,
     getInputProps,
     getItemProps,
+    highlightedIndex,
     setHighlightedIndex,
     setInputValue,
     openMenu,
@@ -151,6 +154,9 @@ export default function ComboBox<ComboBoxOption extends ComboBoxItem>({
     },
   });
 
+  const [lastHighlightedIndex, setLastHighlightedIndex] =
+    useState(highlightedIndex);
+
   useEffect(() => {
     if (inputValue) {
       openMenu();
@@ -170,96 +176,111 @@ export default function ComboBox<ComboBoxOption extends ComboBoxItem>({
     }
   }, [inputValue]);
 
+  if (
+    inputItems[highlightedIndex] &&
+    highlightedIndex !== lastHighlightedIndex
+  ) {
+    setLastHighlightedIndex(highlightedIndex);
+  }
+
+  const selectedBlock =
+    inputItems[highlightedIndex] || inputItems[lastHighlightedIndex];
+
   return (
-    <div className="w-combobox">
-      {/* downshift does the label-field association itself. */}
-      {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
-      <label {...getLabelProps()} className="w-sr-only">
-        {label}
-      </label>
-      <div className="w-combobox__field">
-        <input
-          {...getInputProps()}
-          type="text"
-          // Prevent the field from receiving focus if it’s not visible.
-          disabled={inlineCombobox}
-          placeholder={placeholder}
-        />
-      </div>
-      {noResults ? (
-        <div className="w-combobox__status">{noResultsText}</div>
-      ) : null}
-      <div {...getMenuProps()} className="w-combobox__menu">
-        {categories.map((category) => {
-          const categoryItems = (category.items || []).filter((item) =>
-            inputItems.find((i) => i.type === item.type),
-          );
-          const itemColumns = Math.ceil(categoryItems.length / 2);
+    <div className="w-combobox-container">
+      <div className="w-combobox">
+        {/* downshift does the label-field association itself. */}
+        {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+        <label {...getLabelProps()} className="w-sr-only">
+          {label}
+        </label>
+        <div className="w-combobox__field">
+          <input
+            {...getInputProps()}
+            type="text"
+            // Prevent the field from receiving focus if it’s not visible.
+            disabled={inlineCombobox}
+            placeholder={placeholder}
+          />
+        </div>
+        {noResults ? (
+          <div className="w-combobox__status">{noResultsText}</div>
+        ) : null}
+        <div {...getMenuProps()} className="w-combobox__menu">
+          {categories.map((category) => {
+            const categoryItems = (category.items || []).filter((item) =>
+              inputItems.find((i) => i.type === item.type),
+            );
+            const itemColumns = Math.ceil(categoryItems.length / 2);
 
-          if (categoryItems.length === 0) {
-            return null;
-          }
+            if (categoryItems.length === 0) {
+              return null;
+            }
 
-          return (
-            <div className="w-combobox__optgroup" key={category.type}>
-              {category.label ? (
-                <div className="w-combobox__optgroup-label">
-                  {category.label}
-                </div>
-              ) : null}
-              {categoryItems.map((item, index) => {
-                const itemLabel = getItemLabel(item.type, item);
-                const description = getItemDescription(item);
-                const itemIndex = inputItems.findIndex(
-                  (i) => i.type === item.type,
-                );
-                const itemColumn = index + 1 <= itemColumns ? 1 : 2;
-                const hasIcon =
-                  typeof item.icon !== 'undefined' && item.icon !== null;
-                let icon: JSX.Element | null | undefined = null;
+            return (
+              <div className="w-combobox__optgroup" key={category.type}>
+                {category.label ? (
+                  <div className="w-combobox__optgroup-label">
+                    {category.label}
+                  </div>
+                ) : null}
+                {categoryItems.map((item, index) => {
+                  const itemLabel = getItemLabel(item.type, item);
+                  const description = getItemDescription(item);
+                  const itemIndex = inputItems.findIndex(
+                    (i) => i.type === item.type,
+                  );
+                  const itemColumn = index + 1 <= itemColumns ? 1 : 2;
+                  const hasIcon =
+                    typeof item.icon !== 'undefined' && item.icon !== null;
+                  let icon: JSX.Element | null | undefined = null;
 
-                if (hasIcon) {
-                  if (Array.isArray(item.icon)) {
-                    icon = (
-                      <Icon name="custom" viewBox="0 0 1024 1024">
-                        {item.icon.map((pathData: string) => (
-                          <path key={pathData} d={pathData} />
-                        ))}
-                      </Icon>
-                    );
-                  } else {
-                    icon =
-                      typeof item.icon === 'string' ? (
-                        <Icon name={item.icon} />
-                      ) : (
-                        item.icon
+                  if (hasIcon) {
+                    if (Array.isArray(item.icon)) {
+                      icon = (
+                        <Icon name="custom" viewBox="0 0 1024 1024">
+                          {item.icon.map((pathData: string) => (
+                            <path key={pathData} d={pathData} />
+                          ))}
+                        </Icon>
                       );
+                    } else {
+                      icon =
+                        typeof item.icon === 'string' ? (
+                          <Icon name={item.icon} />
+                        ) : (
+                          item.icon
+                        );
+                    }
                   }
-                }
 
-                return (
-                  <div
-                    key={item.type}
-                    {...getItemProps({ item, index: itemIndex })}
-                    className={`w-combobox__option w-combobox__option--col${itemColumn}`}
-                  >
-                    <div className="w-combobox__option-icon">
-                      {icon}
-                      {/* Support for rich text options using text as an icon (for example "B" for bold). */}
-                      {itemLabel && !hasIcon ? <span>{itemLabel}</span> : null}
-                    </div>
-                    <div className="w-combobox__option-text">
-                      {item.render
-                        ? item.render({ option: item })
-                        : description}
+                  return (
+                    <div
+                      key={item.type}
+                      {...getItemProps({ item, index: itemIndex })}
+                      className={`w-combobox__option w-combobox__option--col${itemColumn}`}
+                    >
+                      <div className="w-combobox__option-icon">
+                        {icon}
+                        {/* Support for rich text options using text as an icon (for example "B" for bold). */}
+                        {itemLabel && !hasIcon ? (
+                          <span>{itemLabel}</span>
+                        ) : null}
+                      </div>
+                      <div className="w-combobox__option-text">
+                        {item.render
+                          ? item.render({ option: item })
+                          : description}
+                      </div>
                     </div>
-                  </div>
-                );
-              })}
-            </div>
-          );
-        })}
+                  );
+                })}
+              </div>
+            );
+          })}
+        </div>
       </div>
+      {selectedBlock ? <ComboBoxPreview item={selectedBlock} /> : null}
     </div>
   );
 }

+ 153 - 149
client/src/components/ComboBox/__snapshots__/ComboBox.test.tsx.snap

@@ -2,182 +2,186 @@
 
 exports[`ComboBox rendering matches the snapshot 1`] = `
 <div
-  className="w-combobox"
+  className="w-combobox-container"
 >
-  <label
-    className="w-sr-only"
-    htmlFor="downshift-1-input"
-    id="downshift-1-label"
-  >
-    Search options…
-  </label>
-  <div
-    className="w-combobox__field"
-  >
-    <input
-      aria-activedescendant=""
-      aria-autocomplete="list"
-      aria-controls="downshift-1-menu"
-      aria-expanded={false}
-      aria-labelledby="downshift-1-label"
-      autoComplete="off"
-      disabled={false}
-      id="downshift-1-input"
-      onBlur={[Function]}
-      onChange={[Function]}
-      onFocus={[Function]}
-      onKeyDown={[Function]}
-      placeholder="Search options…"
-      role="combobox"
-      type="text"
-      value=""
-    />
-  </div>
   <div
-    aria-labelledby="downshift-1-label"
-    className="w-combobox__menu"
-    id="downshift-1-menu"
-    onMouseLeave={[Function]}
-    role="listbox"
+    className="w-combobox"
   >
+    <label
+      className="w-sr-only"
+      htmlFor="downshift-1-input"
+      id="downshift-1-label"
+    >
+      Search options…
+    </label>
     <div
-      className="w-combobox__optgroup"
-      key="blockTypes"
+      className="w-combobox__field"
+    >
+      <input
+        aria-activedescendant=""
+        aria-autocomplete="list"
+        aria-controls="downshift-1-menu"
+        aria-expanded={false}
+        aria-labelledby="downshift-1-label"
+        autoComplete="off"
+        disabled={false}
+        id="downshift-1-input"
+        onBlur={[Function]}
+        onChange={[Function]}
+        onFocus={[Function]}
+        onKeyDown={[Function]}
+        placeholder="Search options…"
+        role="combobox"
+        type="text"
+        value=""
+      />
+    </div>
+    <div
+      aria-labelledby="downshift-1-label"
+      className="w-combobox__menu"
+      id="downshift-1-menu"
+      onMouseLeave={[Function]}
+      role="listbox"
     >
       <div
-        className="w-combobox__optgroup-label"
-      >
-        Blocks
-      </div>
-      <div
-        aria-selected="false"
-        className="w-combobox__option w-combobox__option--col1"
-        id="downshift-1-item-0"
-        key="blockquote"
-        onClick={[Function]}
-        onMouseDown={[Function]}
-        onMouseMove={[Function]}
-        role="option"
+        className="w-combobox__optgroup"
+        key="blockTypes"
       >
         <div
-          className="w-combobox__option-icon"
-        >
-          <Icon
-            name="blockquote"
-          />
-        </div>
-        <div
-          className="w-combobox__option-text"
+          className="w-combobox__optgroup-label"
         >
-          Blockquote
+          Blocks
         </div>
-      </div>
-      <div
-        aria-selected="false"
-        className="w-combobox__option w-combobox__option--col1"
-        id="downshift-1-item-1"
-        key="paragraph"
-        onClick={[Function]}
-        onMouseDown={[Function]}
-        onMouseMove={[Function]}
-        role="option"
-      >
         <div
-          className="w-combobox__option-icon"
+          aria-selected="false"
+          className="w-combobox__option w-combobox__option--col1"
+          id="downshift-1-item-0"
+          key="blockquote"
+          onClick={[Function]}
+          onMouseDown={[Function]}
+          onMouseMove={[Function]}
+          role="option"
         >
-          <span
-            className="my-icon"
+          <div
+            className="w-combobox__option-icon"
           >
-            P
-          </span>
-        </div>
-        <div
-          className="w-combobox__option-text"
-        >
-          Paragraph
-        </div>
-      </div>
-      <div
-        aria-selected="false"
-        className="w-combobox__option w-combobox__option--col1"
-        id="downshift-1-item-2"
-        key="heading-one"
-        onClick={[Function]}
-        onMouseDown={[Function]}
-        onMouseMove={[Function]}
-        role="option"
-      >
-        <div
-          className="w-combobox__option-icon"
-        >
-          <Icon
-            name="custom"
-            viewBox="0 0 1024 1024"
-          >
-            <path
-              d="M 83.625 "
-              key="M 83.625 "
-            />
-            <path
-              d="L 232.535156 "
-              key="L 232.535156 "
+            <Icon
+              name="blockquote"
             />
-          </Icon>
+          </div>
+          <div
+            className="w-combobox__option-text"
+          >
+            Blockquote
+          </div>
         </div>
         <div
-          className="w-combobox__option-text"
+          aria-selected="false"
+          className="w-combobox__option w-combobox__option--col1"
+          id="downshift-1-item-1"
+          key="paragraph"
+          onClick={[Function]}
+          onMouseDown={[Function]}
+          onMouseMove={[Function]}
+          role="option"
         >
-          Heading 1
-        </div>
-      </div>
-      <div
-        aria-selected="false"
-        className="w-combobox__option w-combobox__option--col2"
-        id="downshift-1-item-3"
-        key="heading-two"
-        onClick={[Function]}
-        onMouseDown={[Function]}
-        onMouseMove={[Function]}
-        role="option"
-      >
-        <div
-          className="w-combobox__option-icon"
-        >
-          <span>
-            H2
-          </span>
+          <div
+            className="w-combobox__option-icon"
+          >
+            <span
+              className="my-icon"
+            >
+              P
+            </span>
+          </div>
+          <div
+            className="w-combobox__option-text"
+          >
+            Paragraph
+          </div>
         </div>
         <div
-          className="w-combobox__option-text"
+          aria-selected="false"
+          className="w-combobox__option w-combobox__option--col1"
+          id="downshift-1-item-2"
+          key="heading-one"
+          onClick={[Function]}
+          onMouseDown={[Function]}
+          onMouseMove={[Function]}
+          role="option"
         >
-          <span
-            className="custom-text"
+          <div
+            className="w-combobox__option-icon"
           >
-            H2
-          </span>
+            <Icon
+              name="custom"
+              viewBox="0 0 1024 1024"
+            >
+              <path
+                d="M 83.625 "
+                key="M 83.625 "
+              />
+              <path
+                d="L 232.535156 "
+                key="L 232.535156 "
+              />
+            </Icon>
+          </div>
+          <div
+            className="w-combobox__option-text"
+          >
+            Heading 1
+          </div>
         </div>
-      </div>
-      <div
-        aria-selected="false"
-        className="w-combobox__option w-combobox__option--col2"
-        id="downshift-1-item-4"
-        key="link"
-        onClick={[Function]}
-        onMouseDown={[Function]}
-        onMouseMove={[Function]}
-        role="option"
-      >
         <div
-          className="w-combobox__option-icon"
+          aria-selected="false"
+          className="w-combobox__option w-combobox__option--col2"
+          id="downshift-1-item-3"
+          key="heading-two"
+          onClick={[Function]}
+          onMouseDown={[Function]}
+          onMouseMove={[Function]}
+          role="option"
         >
-          <span>
-            🔗
-          </span>
+          <div
+            className="w-combobox__option-icon"
+          >
+            <span>
+              H2
+            </span>
+          </div>
+          <div
+            className="w-combobox__option-text"
+          >
+            <span
+              className="custom-text"
+            >
+              H2
+            </span>
+          </div>
         </div>
         <div
-          className="w-combobox__option-text"
+          aria-selected="false"
+          className="w-combobox__option w-combobox__option--col2"
+          id="downshift-1-item-4"
+          key="link"
+          onClick={[Function]}
+          onMouseDown={[Function]}
+          onMouseMove={[Function]}
+          role="option"
         >
-          Link
+          <div
+            className="w-combobox__option-icon"
+          >
+            <span>
+              🔗
+            </span>
+          </div>
+          <div
+            className="w-combobox__option-text"
+          >
+            Link
+          </div>
         </div>
       </div>
     </div>

+ 41 - 0
client/src/components/ComboBoxPreview/ComboBoxPreview.scss

@@ -0,0 +1,41 @@
+.w-combobox-preview {
+  padding: theme('spacing.5');
+  display: grid;
+  grid-template-rows: 6fr 4fr;
+  gap: theme('spacing.5');
+  background-color: theme('colors.surface-header');
+  border-block-start: 1px solid theme('colors.border-furniture');
+  border-end-end-radius: inherit;
+  border-end-start-radius: inherit;
+  @include media-breakpoint-up(sm) {
+    border-block-start: 0;
+    border-start-end-radius: inherit;
+    border-end-start-radius: 0;
+    border-inline-start: 1px solid theme('colors.border-furniture');
+  }
+}
+
+.w-combobox-preview__iframe {
+  width: 100%;
+  height: 100%;
+  border: 1px solid theme('colors.border-furniture');
+  border-radius: theme('borderRadius.sm');
+
+  // Ensure iframe is always opaque
+  color-scheme: normal;
+  background-color: Canvas;
+
+  @include more-contrast() {
+    border-color: theme('colors.border-furniture-more-contrast');
+  }
+}
+
+.w-combobox-preview__label {
+  @apply w-label-1;
+}
+
+.w-combobox-preview__description {
+  @apply w-help-text;
+  margin-top: theme('spacing.3');
+  margin-bottom: 0;
+}

+ 35 - 0
client/src/components/ComboBoxPreview/ComboBoxPreview.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+import { WAGTAIL_CONFIG } from '../../config/wagtailConfig';
+
+interface ComboBoxItem {
+  label?: string | null;
+  description?: string | null;
+  // icon?: string | JSX.Element | null;
+  blockDefId?: string;
+}
+
+export interface ComboBoxPreviewProps {
+  item: ComboBoxItem;
+}
+
+export default function ComboBoxPreview({
+  item: { label, description, blockDefId },
+}: ComboBoxPreviewProps) {
+  const previewURL = blockDefId
+    ? new URL(WAGTAIL_CONFIG.ADMIN_URLS.BLOCK_PREVIEW, window.location.href)
+    : undefined;
+  previewURL?.searchParams.append('id', blockDefId || '');
+  return (
+    <div className="w-combobox-preview">
+      <iframe
+        className="w-combobox-preview__iframe"
+        title="Preview"
+        src={previewURL?.toString()}
+      />
+      <div className="w-combobox-preview__label">{label}</div>
+      {description ? (
+        <p className="w-combobox-preview__description">{description}</p>
+      ) : null}
+    </div>
+  );
+}

+ 3 - 0
client/src/components/StreamField/blocks/StreamBlock.js

@@ -104,6 +104,7 @@ class StreamBlockMenu extends BaseInsertionControl {
       content: this.combobox,
       trigger: 'click',
       interactive: true,
+      maxWidth: 'none',
       theme: 'dropdown',
       arrow: false,
       placement: 'bottom',
@@ -120,7 +121,9 @@ class StreamBlockMenu extends BaseInsertionControl {
       const groupItems = blockDefs.map((blockDef) => ({
         type: blockDef.name,
         label: blockDef.meta.label,
+        description: blockDef.meta.description,
         icon: blockDef.meta.icon,
+        blockDefId: blockDef.meta.blockDefId,
       }));
 
       return {

+ 1 - 1
client/src/components/StreamField/blocks/__snapshots__/StreamBlock.test.js.snap

@@ -751,7 +751,7 @@ exports[`telepath: wagtail.blocks.StreamBlock it renders menus on opening 1`] =
         <button type="button" title="Insert a block" class="c-sf-add-button" aria-expanded="true">
           <svg class="icon icon-plus" aria-hidden="true"><use href="#icon-plus"></use></svg>
         </button>
-      <div data-tippy-root="" id="tippy-5" style="z-index: 9999; visibility: visible; transition: none; position: absolute; left: 0px; top: 0px; margin: 0px;"><div class="tippy-box" data-state="hidden" tabindex="-1" data-theme="dropdown" data-animation="fade" style="max-width: 350px; transition-duration: 0ms;" role="tooltip"><div class="tippy-content" data-state="hidden" style="transition-duration: 0ms;"><div><div class="w-combobox"><label id="downshift-0-label" for="downshift-0-input" class="w-sr-only">Search options…</label><div class="w-combobox__field"><input aria-activedescendant="" aria-autocomplete="list" aria-controls="downshift-0-menu" aria-expanded="false" aria-labelledby="downshift-0-label" autocomplete="off" id="downshift-0-input" role="combobox" type="text" placeholder="Search options…" value=""></div><div id="downshift-0-menu" role="listbox" aria-labelledby="downshift-0-label" class="w-combobox__menu"><div class="w-combobox__optgroup"><div role="option" aria-selected="false" id="downshift-0-item-0" class="w-combobox__option w-combobox__option--col1"><div class="w-combobox__option-icon"><svg class="icon icon-placeholder" aria-hidden="true"><use href="#icon-placeholder"></use></svg></div><div class="w-combobox__option-text">Test Block A</div></div><div role="option" aria-selected="false" id="downshift-0-item-1" class="w-combobox__option w-combobox__option--col2"><div class="w-combobox__option-icon"><svg class="icon icon-pilcrow" aria-hidden="true"><use href="#icon-pilcrow"></use></svg></div><div class="w-combobox__option-text">Test Block B</div></div></div></div></div></div></div></div></div></div><div data-streamfield-child="" data-contentpath="2">
+      <div data-tippy-root="" id="tippy-5" style="z-index: 9999; visibility: visible; transition: none; position: absolute; left: 0px; top: 0px; margin: 0px;"><div class="tippy-box" data-state="hidden" tabindex="-1" data-theme="dropdown" data-animation="fade" style="max-width: none; transition-duration: 0ms;" role="tooltip"><div class="tippy-content" data-state="hidden" style="transition-duration: 0ms;"><div><div class="w-combobox-container"><div class="w-combobox"><label id="downshift-0-label" for="downshift-0-input" class="w-sr-only">Search options…</label><div class="w-combobox__field"><input aria-activedescendant="" aria-autocomplete="list" aria-controls="downshift-0-menu" aria-expanded="false" aria-labelledby="downshift-0-label" autocomplete="off" id="downshift-0-input" role="combobox" type="text" placeholder="Search options…" value=""></div><div id="downshift-0-menu" role="listbox" aria-labelledby="downshift-0-label" class="w-combobox__menu"><div class="w-combobox__optgroup"><div role="option" aria-selected="false" id="downshift-0-item-0" class="w-combobox__option w-combobox__option--col1"><div class="w-combobox__option-icon"><svg class="icon icon-placeholder" aria-hidden="true"><use href="#icon-placeholder"></use></svg></div><div class="w-combobox__option-text">Test Block A</div></div><div role="option" aria-selected="false" id="downshift-0-item-1" class="w-combobox__option w-combobox__option--col2"><div class="w-combobox__option-icon"><svg class="icon icon-pilcrow" aria-hidden="true"><use href="#icon-pilcrow"></use></svg></div><div class="w-combobox__option-text">Test Block B</div></div></div></div></div></div></div></div></div></div></div><div data-streamfield-child="" data-contentpath="2">
         <input type="hidden" name="the-prefix-1-deleted" value="">
         <input type="hidden" name="the-prefix-1-order" value="1">
         <input type="hidden" name="the-prefix-1-type" value="test_block_b">

+ 1 - 0
client/src/custom.d.ts

@@ -8,6 +8,7 @@ export interface WagtailConfig {
   ADMIN_URLS: {
     DISMISSIBLES: string;
     PAGES: string;
+    BLOCK_PREVIEW: string;
   };
   CSRF_HEADER_NAME: string;
   CSRF_TOKEN: string;

+ 1 - 0
wagtail/admin/templatetags/wagtailadmin_tags.py

@@ -983,6 +983,7 @@ def wagtail_config(context):
         "ADMIN_URLS": {
             "DISMISSIBLES": reverse("wagtailadmin_dismissibles"),
             "PAGES": reverse("wagtailadmin_explore_root"),
+            "BLOCK_PREVIEW": reverse("wagtailadmin_block_preview"),
         },
         "I18N_ENABLED": i18n_enabled(),
         "LOCALES": locales(serialize=False),