<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Drupal Canvas</title>
    <description>Stay informed about developments in Drupal Canvas.</description>
    <link>https://www.drupal.org/project/drupal-canvas</link>
    <item>
      <title>#3583897: Page preview should not scroll to top in Workbench when HMR reloads page</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3583897.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867db534ea74</guid>
      <pubDate>Fri, 10 Apr 2026 14:17:31 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/61c33227b0892c00fe43fd3829af5b299b71ab64"&gt;61c3322&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3583897"&gt;#3583897&lt;/a&gt; · 1 contributor · 8 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 2&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When HMR triggers a re-render in the Canvas Workbench, the page preview iframe no longer resets its scroll position to the top. Scroll still resets when navigating to a different page or component. This is purely a Workbench developer-experience improvement with no effect on site visitors.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The preview iframe in Canvas Workbench stays mounted as the shell switches between pages and components. Previously, every re-render caused &lt;code&gt;window.scrollTo(0, 0)&lt;/code&gt;, which meant HMR updates (same target, new markup) also jumped the user back to the top.&lt;/p&gt;
&lt;p&gt;The fix introduces a &lt;code&gt;useRef&lt;/code&gt; (&lt;code&gt;lastPreviewTargetKeyRef&lt;/code&gt;) in &lt;code&gt;PreviewFrameApp&lt;/code&gt; and a &lt;code&gt;useLayoutEffect&lt;/code&gt; that compares a stable key derived from the render type and render ID. The helper &lt;code&gt;getPreviewTargetKey()&lt;/code&gt; in &lt;code&gt;preview-target-key.ts&lt;/code&gt; builds this key as &lt;code&gt;&amp;quot;${renderType}:${renderId}&amp;quot;&lt;/code&gt;. When the key changes (user navigated to a different preview target), scroll resets to top. When the key is the same (HMR update for the same target), the effect skips the scroll call, preserving the user's position.&lt;/p&gt;
&lt;p&gt;New tests in &lt;code&gt;PreviewFrameApp.test.tsx&lt;/code&gt; cover both paths: scroll-to-top on target change and scroll preservation on same-target re-render. The &lt;code&gt;jsdom&lt;/code&gt; devDependency was added to the workbench package to support the vitest jsdom environment these tests require. The &lt;code&gt;package-lock.json&lt;/code&gt; diff reflects this addition plus recalculated peer-dependency flags.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 9, 2026&lt;/li&gt;
&lt;li&gt;First commit: April 9, 2026&lt;/li&gt;
&lt;li&gt;Last commit: April 10, 2026 (1 day and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;lauriii (Acquia)&lt;/li&gt;
&lt;li&gt;balintbrews (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3576521: defineComponentCatalog function from drupal-canvas package doesn't resolve schema references for props</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3576521.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94b4f4f6803e</guid>
      <pubDate>Thu, 09 Apr 2026 20:29:15 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/068c0932ed9e5b75b41c71b2e1c6acf436c157f8"&gt;068c093&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3576521"&gt;#3576521&lt;/a&gt; · 2 contributors · 7 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;defineComponentCatalog&lt;/code&gt; function in the &lt;code&gt;drupal-canvas&lt;/code&gt; package failed when a component's props contained Canvas-specific &lt;code&gt;$ref&lt;/code&gt; URIs (e.g. &lt;code&gt;json-schema-definitions://canvas.module/image&lt;/code&gt;). Zod's &lt;code&gt;fromJSONSchema&lt;/code&gt; could not resolve these custom URIs, causing an error when building the catalog. This is now fixed so components using schema references in their props work correctly in the catalog.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Canvas component metadata can reference shared type definitions via custom &lt;code&gt;$ref&lt;/code&gt; URIs like &lt;code&gt;json-schema-definitions://canvas.module/image&lt;/code&gt;. The &lt;code&gt;defineComponentCatalog&lt;/code&gt; function (added in &lt;a href="https://www.drupal.org/node/3573867"&gt;#3573867&lt;/a&gt;) passes each component's props JSON schema through Zod's &lt;code&gt;fromJSONSchema&lt;/code&gt;, which only understands standard local JSON Pointer refs (&lt;code&gt;#/$defs/...&lt;/code&gt;), not Canvas-specific URIs. Any component using these references would fail.&lt;/p&gt;
&lt;p&gt;The fix adds a &lt;code&gt;rewriteCanvasRefs&lt;/code&gt; function in &lt;code&gt;json-render-utils.tsx&lt;/code&gt; that recursively walks schema objects and rewrites &lt;code&gt;$ref&lt;/code&gt; values starting with &lt;code&gt;json-schema-definitions://canvas.module/&lt;/code&gt; to local &lt;code&gt;#/$defs/&lt;/code&gt; pointers. The project's root &lt;code&gt;schema.json&lt;/code&gt; contains &lt;code&gt;$defs&lt;/code&gt; with shared type definitions (image, video, heading-element, etc.). These definitions are imported, themselves rewritten (since they can contain nested &lt;code&gt;$ref&lt;/code&gt; URIs, e.g. image references image-uri), and attached to each component's JSON schema as &lt;code&gt;$defs&lt;/code&gt; in &lt;code&gt;metadataToCatalogEntry&lt;/code&gt;. This gives &lt;code&gt;fromJSONSchema&lt;/code&gt; everything it needs to resolve the rewritten local pointers.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 2, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (1 month later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;wotnak (Smartbees)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3583947: Fix CLI push to not push files that are distributed with Canvas</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3583947.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867e68568a42</guid>
      <category>Site owners</category>
      <pubDate>Thu, 09 Apr 2026 19:04:05 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/e81f712d85532dcef71e95a7d5d0947609ee3675"&gt;e81f712&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3583947"&gt;#3583947&lt;/a&gt; · 2 contributors · 9 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Canvas CLI push command could accidentally bundle packages and alias imports that Canvas already provides in its global import map. This change prevents those files from being included in the push output. It also adds eslint rules that warn developers when they use the old &lt;code&gt;@/lib/utils&lt;/code&gt;, &lt;code&gt;@/lib/jsonapi-utils&lt;/code&gt;, or &lt;code&gt;@/lib/drupal-utils&lt;/code&gt; import paths, and offers auto-fixes to switch them to &lt;code&gt;drupal-canvas&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects site owners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Config change:&lt;/strong&gt; Six new entries added to &lt;code&gt;DRUPAL_CANVAS_EXTERNALS&lt;/code&gt;: &lt;code&gt;@tailwindcss/typography&lt;/code&gt;, &lt;code&gt;next-image-standalone&lt;/code&gt;, &lt;code&gt;@drupal-api-client/json-api-client&lt;/code&gt;, &lt;code&gt;@/lib/FormattedText&lt;/code&gt;, &lt;code&gt;@/lib/utils&lt;/code&gt;, &lt;code&gt;@/lib/jsonapi-utils&lt;/code&gt;, &lt;code&gt;@/lib/drupal-utils&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Canvas distributes a set of packages and aliased utility modules via its global import map. The CLI &lt;code&gt;push&lt;/code&gt; command builds and uploads local component code, but the &lt;code&gt;DRUPAL_CANVAS_EXTERNALS&lt;/code&gt; list in &lt;code&gt;vite-build-config.ts&lt;/code&gt; was incomplete. Packages like &lt;code&gt;@tailwindcss/typography&lt;/code&gt;, &lt;code&gt;next-image-standalone&lt;/code&gt;, and &lt;code&gt;@drupal-api-client/json-api-client&lt;/code&gt; were missing, along with four &lt;code&gt;@/&lt;/code&gt; alias paths (&lt;code&gt;@/lib/FormattedText&lt;/code&gt;, &lt;code&gt;@/lib/utils&lt;/code&gt;, &lt;code&gt;@/lib/jsonapi-utils&lt;/code&gt;, &lt;code&gt;@/lib/drupal-utils&lt;/code&gt;). This meant the build could bundle these into the push output even though Canvas already ships them.&lt;/p&gt;
&lt;p&gt;The fix adds these entries to &lt;code&gt;DRUPAL_CANVAS_EXTERNALS&lt;/code&gt;. That list is already used in two places for Vite's &lt;code&gt;external&lt;/code&gt; option (&lt;code&gt;build-vendor.ts&lt;/code&gt; and &lt;code&gt;build-local-import.ts&lt;/code&gt;), so adding entries there prevents bundling. The &lt;code&gt;collectImports&lt;/code&gt; function in &lt;code&gt;import-analyzer.ts&lt;/code&gt; now also checks alias imports against &lt;code&gt;DRUPAL_CANVAS_EXTERNALS&lt;/code&gt; and skips matches, so they do not end up in &lt;code&gt;aliasImports&lt;/code&gt; or &lt;code&gt;unresolvedAliasImports&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;eslint-config&lt;/code&gt; package's &lt;code&gt;component-imports&lt;/code&gt; rule gains three new checks for &lt;code&gt;@/lib/utils&lt;/code&gt;, &lt;code&gt;@/lib/jsonapi-utils&lt;/code&gt;, and &lt;code&gt;@/lib/drupal-utils&lt;/code&gt;. Each reports that the path is now provided by Canvas and auto-fixes the import to &lt;code&gt;'drupal-canvas'&lt;/code&gt;. The &lt;code&gt;@/lib/drupal-utils&lt;/code&gt; case has a special carve-out: if the import includes &lt;code&gt;sortMenu&lt;/code&gt;, the auto-fix is suppressed because that export was renamed to &lt;code&gt;sortLinksetMenu&lt;/code&gt; in the &lt;code&gt;drupal-canvas&lt;/code&gt; package. The existing &lt;code&gt;@/lib/FormattedText&lt;/code&gt; error message was also updated to clarify that the path is provided by Canvas and cannot be used for local files.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 9, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;wotnak (Smartbees)&lt;/li&gt;
&lt;li&gt;mglaman (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Comment #9 notes a release dependency: a new version of &lt;code&gt;eslint-config&lt;/code&gt; must be released alongside the new CLI version.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582273: Prevent Workbench preview from reloading when links are clicked</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3582273.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-951ade160c28</guid>
      <category>Module maintainers</category>
      <pubDate>Thu, 09 Apr 2026 12:32:28 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/79f0121a5be515ce0f6d7f52c3e653a5ca3b8a17"&gt;79f0121&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582273"&gt;#3582273&lt;/a&gt; · 3 contributors · 8 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Clicking a link inside the Workbench preview iframe no longer causes the iframe to reload and render the entire Workbench application inside itself. Instead, recognized Workbench routes (known pages and components) navigate within the app, and external or unrecognized links open in a new tab. Form submissions inside the preview are also blocked.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; New &lt;code&gt;shellPath&lt;/code&gt; optional field on &lt;code&gt;PreviewRenderRequest.payload&lt;/code&gt;; new &lt;code&gt;PreviewShellSync&lt;/code&gt; message type in the preview contract.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The Workbench preview renders components and pages inside a sandboxed iframe. When a user clicked a link in the previewed content, the iframe navigated normally, which served the Workbench shell HTML (because the dev server's SPA fallback returned &lt;code&gt;index.html&lt;/code&gt;), nesting the entire app inside its own preview frame.&lt;/p&gt;
&lt;p&gt;The initial proposal was to intercept all link clicks and prevent their default behavior, borrowing the approach from Canvas UI's preview mode. In comment #3, lauriii suggested that clicks should instead navigate within Workbench when the target is a recognized route, and open unrecognized or external links in a new tab. The implementation follows that direction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Navigation resolver.&lt;/strong&gt; A new pure function &lt;code&gt;resolveWorkbenchPreviewNavigation&lt;/code&gt; in &lt;code&gt;resolve-workbench-preview-navigation.ts&lt;/code&gt; decides whether a URL should produce an in-app &lt;code&gt;navigate&lt;/code&gt; or a new-window &lt;code&gt;open&lt;/code&gt;. It checks the URL against known page slugs, component IDs, and mock indices from the discovery result and preview manifest. Vite internals (&lt;code&gt;/@&lt;/code&gt;, &lt;code&gt;/node_modules/&lt;/code&gt;, &lt;code&gt;/__&lt;/code&gt;), cross-origin URLs, and &lt;code&gt;mailto:&lt;/code&gt;/&lt;code&gt;tel:&lt;/code&gt; links all fall through to &lt;code&gt;open&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Iframe click and form interception.&lt;/strong&gt; &lt;code&gt;PreviewFrameApp&lt;/code&gt; now fetches the discovery result and preview manifest on mount so it can build the navigation context. A document-level click handler (registered with &lt;code&gt;capture: true&lt;/code&gt;) intercepts anchor clicks, resolves them through the navigation function, and either calls &lt;code&gt;window.open&lt;/code&gt; for external links or uses react-router's &lt;code&gt;navigate&lt;/code&gt; for internal ones. A separate handler prevents all form submissions inside the iframe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shell-iframe sync via postMessage.&lt;/strong&gt; Two new sync mechanisms keep the parent shell and iframe router aligned. The parent shell now includes a &lt;code&gt;shellPath&lt;/code&gt; field in &lt;code&gt;PreviewRenderRequest&lt;/code&gt; messages, which the iframe's &lt;code&gt;MemoryRouter&lt;/code&gt; uses to stay on the correct route. In the other direction, a new &lt;code&gt;preview:shell-sync&lt;/code&gt; message type lets the iframe tell the parent shell to navigate when a user clicks an internal link in previewed content. The parent &lt;code&gt;App.tsx&lt;/code&gt; handles this message using a &lt;code&gt;locationRef&lt;/code&gt; pattern to avoid stale closure reads of the current location.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SPA fallback and plugin ordering.&lt;/strong&gt; &lt;code&gt;shouldServeWorkbenchIndexHtml&lt;/code&gt; was broadened from matching only &lt;code&gt;/component&lt;/code&gt; and &lt;code&gt;/page&lt;/code&gt; paths to serving &lt;code&gt;index.html&lt;/code&gt; for any extensionless path that is not an API endpoint, Vite internal, or non-HTML file under &lt;code&gt;/canvas/&lt;/code&gt;. To ensure this middleware runs before other plugins' middleware, the workbench plugin was moved to &lt;code&gt;enforce: 'pre'&lt;/code&gt; and placed first in the plugins array, and its middleware is registered at the top of &lt;code&gt;configureServer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Iframe sandbox.&lt;/strong&gt; The iframe's sandbox attribute gains &lt;code&gt;allow-popups&lt;/code&gt; so that &lt;code&gt;window.open&lt;/code&gt; calls from the click handler can open new tabs. The preview iframe entry point is wrapped in a &lt;code&gt;MemoryRouter&lt;/code&gt; with initial entry &lt;code&gt;/page&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 30, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 9, 2026 (9 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;balintbrews (Acquia)&lt;/li&gt;
&lt;li&gt;mayur-sose&lt;/li&gt;
&lt;li&gt;shubham.prakash (OpenSense Labs)&lt;/li&gt;
&lt;li&gt;lauriii (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;lauriii's comment #3 redirected the approach. The original plan was to simply block all link clicks in the preview (matching what Canvas UI does). lauriii suggested instead navigating within Workbench for recognized routes and opening unrecognized or external links in a new tab, which is what the final implementation does.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3580221: Add CLI push/pull for fonts</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3580221.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-9500af600ac5</guid>
      <category>Module maintainers</category>
      <category>Site owners</category>
      <pubDate>Wed, 08 Apr 2026 19:04:32 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/040e79d7fd1132ff61517e8a5cbfeac246218dc7"&gt;040e79d&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3580221"&gt;#3580221&lt;/a&gt; · 2 contributors · 8 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Canvas CLI can now push and pull Brand Kit fonts, allowing font configuration to be version-controlled and deployed like other Canvas assets. When a &lt;code&gt;canvas.brand-kit.json&lt;/code&gt; file is present in the project root, &lt;code&gt;canvas push&lt;/code&gt; resolves each font family (via &lt;code&gt;unifont&lt;/code&gt; for provider-based fonts, or a local file path), uploads the font files to the Drupal site, and syncs the full font list to the global Brand Kit. &lt;code&gt;canvas pull&lt;/code&gt; fetches fonts from the global Brand Kit, downloads the files into a local &lt;code&gt;fonts/&lt;/code&gt; directory, and writes local &lt;code&gt;src&lt;/code&gt; entries back to &lt;code&gt;canvas.brand-kit.json&lt;/code&gt;. Font sync is entirely opt-in: the file must be present for any font operations to run. The &lt;code&gt;administer brand kit&lt;/code&gt; permission is no longer hidden from the Drupal permissions page when the &lt;code&gt;canvas_dev_mode&lt;/code&gt; module is not installed.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers and site owners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Config change:&lt;/strong&gt; New optional &lt;code&gt;canvas.brand-kit.json&lt;/code&gt; file in the project root for font configuration; default CLI OAuth scope now includes &lt;code&gt;canvas:brand_kit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission change:&lt;/strong&gt; &lt;code&gt;administer brand kit&lt;/code&gt; permission is no longer hidden from the Drupal permissions page when &lt;code&gt;canvas_dev_mode&lt;/code&gt; is not installed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;createFontsPullTask&lt;/code&gt; export in &lt;code&gt;pull.ts&lt;/code&gt; and &lt;code&gt;pushFonts&lt;/code&gt; / &lt;code&gt;buildFontPushPlannedResults&lt;/code&gt; exports in &lt;code&gt;font-push.ts&lt;/code&gt;; new TypeScript interfaces &lt;code&gt;FontsConfig&lt;/code&gt;, &lt;code&gt;FontFamilyEntry&lt;/code&gt;, &lt;code&gt;FontDefaults&lt;/code&gt;, &lt;code&gt;FontAxisDefaults&lt;/code&gt;, &lt;code&gt;BrandKitConfigFile&lt;/code&gt; exported from &lt;code&gt;config.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The global Brand Kit entity stores font configuration site-wide. Previously it was editor-only; the CLI had no way to version or deploy fonts. This change adds font sync to both push and pull as an optional step gated on the presence of &lt;code&gt;canvas.brand-kit.json&lt;/code&gt; in the project root.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Config (&lt;code&gt;packages/cli/src/config.ts&lt;/code&gt;):&lt;/strong&gt; The &lt;code&gt;Config&lt;/code&gt; interface gains a &lt;code&gt;fonts?: FontsConfig&lt;/code&gt; field. At startup, &lt;code&gt;loadFontsFromBrandKitFile()&lt;/code&gt; reads and parses &lt;code&gt;canvas.brand-kit.json&lt;/code&gt; (if present) and stores the &lt;code&gt;fonts&lt;/code&gt; block. New TypeScript interfaces describe the full font config shape: &lt;code&gt;FontsConfig&lt;/code&gt;, &lt;code&gt;FontFamilyEntry&lt;/code&gt; (a union of &lt;code&gt;LocalFontFamilyEntry&lt;/code&gt; with a &lt;code&gt;src&lt;/code&gt; path and &lt;code&gt;ProviderFontFamilyEntry&lt;/code&gt; with an optional &lt;code&gt;provider&lt;/code&gt;), &lt;code&gt;FontDefaults&lt;/code&gt;, &lt;code&gt;FontProviderOptions&lt;/code&gt;, and &lt;code&gt;FontAxisDefaults&lt;/code&gt;. The &lt;code&gt;BRAND_KIT_CONFIG_FILENAME&lt;/code&gt; and &lt;code&gt;BRAND_KIT_GLOBAL_ID&lt;/code&gt; constants are exported. The default OAuth scope gains &lt;code&gt;canvas:brand_kit&lt;/code&gt; (was &lt;code&gt;canvas:js_component canvas:asset_library&lt;/code&gt;; now &lt;code&gt;canvas:js_component canvas:asset_library canvas:brand_kit&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Font push (&lt;code&gt;packages/cli/src/lib/fonts/font-push.ts&lt;/code&gt;):&lt;/strong&gt; &lt;code&gt;pushFonts()&lt;/code&gt; validates the config first (via &lt;code&gt;font-validate.ts&lt;/code&gt;), then fetches the current Brand Kit to diff against remote. For provider-based families it calls &lt;code&gt;createFontResolver()&lt;/code&gt; (a &lt;code&gt;unifont&lt;/code&gt; wrapper in &lt;code&gt;font-resolver.ts&lt;/code&gt;) and then &lt;code&gt;downloadResolvedFaces()&lt;/code&gt; to fetch font files to temp paths. For local &lt;code&gt;src&lt;/code&gt; entries it reads the file from disk. Variable font axes are extracted from the font binary using &lt;code&gt;opentype.js&lt;/code&gt; (decompressing woff2 with &lt;code&gt;woff2-encoder&lt;/code&gt; first) via &lt;code&gt;font-metadata.ts&lt;/code&gt;. Axis tags get human-readable display names (e.g. &lt;code&gt;wght&lt;/code&gt; → &lt;code&gt;&amp;quot;Weight&amp;quot;&lt;/code&gt;) for the Brand Kit UI. &lt;code&gt;axisDefaults&lt;/code&gt; overrides are clamped to each axis min/max. Only variants not already present on the remote Brand Kit are uploaded; push replaces the remote font set in full so fonts not in config are deleted. All outcomes (create/update/delete/unchanged) are reported. An empty &lt;code&gt;families&lt;/code&gt; array clears all fonts on the Brand Kit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Font pull (&lt;code&gt;packages/cli/src/lib/fonts/font-pull.ts&lt;/code&gt;):&lt;/strong&gt; &lt;code&gt;pullFonts()&lt;/code&gt; fetches the Brand Kit via &lt;code&gt;api.getBrandKit()&lt;/code&gt;, skips variants already represented in &lt;code&gt;canvas.brand-kit.json&lt;/code&gt; (matched by a normalized &lt;code&gt;family\0weight\0style&lt;/code&gt; key), and for each new variant downloads the file via &lt;code&gt;api.downloadFile()&lt;/code&gt; into a &lt;code&gt;fonts/&lt;/code&gt; directory with slugified filenames. Variable font weight ranges are reconstructed from stored axes (&lt;code&gt;wght&lt;/code&gt; min/max). &lt;code&gt;updateBrandKitConfig()&lt;/code&gt; merges new &lt;code&gt;LocalFontFamilyEntry&lt;/code&gt; objects into the existing file, preserving other top-level keys. &lt;code&gt;createFontsPullTask()&lt;/code&gt; in &lt;code&gt;pull.ts&lt;/code&gt; wraps this as a &lt;code&gt;PullTask&lt;/code&gt; that runs alongside components and global CSS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OAuth (&lt;code&gt;CanvasOauthAuthenticationProvider&lt;/code&gt;):&lt;/strong&gt; &lt;code&gt;BrandKit::ENTITY_TYPE_ID&lt;/code&gt; is added to the list of protected config entity types so the existing config entity REST endpoints (&lt;code&gt;canvas.api.config.get&lt;/code&gt;, &lt;code&gt;canvas.api.config.patch&lt;/code&gt;, etc.) accept OAuth tokens for Brand Kit operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;canvas.module&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;canvas_form_alter()&lt;/code&gt; is removed. This hook was hiding the &lt;code&gt;administer brand kit&lt;/code&gt; permission entry from the Drupal permissions UI when &lt;code&gt;canvas_dev_mode&lt;/code&gt; was not installed. Its removal means the permission is always visible.&lt;/p&gt;
&lt;p&gt;New npm dependencies added to the CLI: &lt;code&gt;unifont ^0.7.4&lt;/code&gt;, &lt;code&gt;opentype.js ^1.3.4&lt;/code&gt;, &lt;code&gt;woff2-encoder ^2.0.0&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;If you have &lt;code&gt;CANVAS_SCOPE&lt;/code&gt; set explicitly in &lt;code&gt;.env&lt;/code&gt; or your environment, add &lt;code&gt;canvas:brand_kit&lt;/code&gt; to enable font push/pull:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CANVAS_SCOPE=&amp;quot;canvas:js_component canvas:asset_library canvas:brand_kit&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;CANVAS_SCOPE&lt;/code&gt; is unset, the new default already includes &lt;code&gt;canvas:brand_kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To opt in to font sync, create &lt;code&gt;canvas.brand-kit.json&lt;/code&gt; in your project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;fonts&amp;quot;: {
    &amp;quot;families&amp;quot;: [
      {
        &amp;quot;name&amp;quot;: &amp;quot;Inter&amp;quot;,
        &amp;quot;provider&amp;quot;: &amp;quot;google&amp;quot;,
        &amp;quot;weights&amp;quot;: [&amp;quot;400&amp;quot;, &amp;quot;700&amp;quot;],
        &amp;quot;styles&amp;quot;: [&amp;quot;normal&amp;quot;, &amp;quot;italic&amp;quot;]
      },
      {
        &amp;quot;name&amp;quot;: &amp;quot;My Font&amp;quot;,
        &amp;quot;src&amp;quot;: &amp;quot;fonts/MyFont-Regular.woff2&amp;quot;,
        &amp;quot;weights&amp;quot;: [&amp;quot;400&amp;quot;],
        &amp;quot;styles&amp;quot;: [&amp;quot;normal&amp;quot;]
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No changes are required if you do not create &lt;code&gt;canvas.brand-kit.json&lt;/code&gt;; the push and pull commands continue to work as before.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (20 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;lauriii (Acquia)&lt;/li&gt;
&lt;li&gt;hooroomoo (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3583386: Code Component: Prop Type Change Fails to Reset Configuration</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3583386.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-86798539ea82</guid>
      <pubDate>Wed, 08 Apr 2026 12:49:31 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/e58c60d3476789589b9e189db7285e0cb8a1e615"&gt;e58c60d&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3583386"&gt;#3583386&lt;/a&gt; · 5 contributors · 12 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When using the Code Component editor, changing a prop's type (for example, from &amp;quot;Formatted Text&amp;quot; to &amp;quot;Link&amp;quot; or &amp;quot;Date and time&amp;quot;) did not clear the previous type's configuration fields. The &lt;code&gt;contentMediaType&lt;/code&gt; and &lt;code&gt;x-formatting-context&lt;/code&gt; fields set by the &amp;quot;Formatted Text&amp;quot; type were carried over into the new type's saved configuration. On page reload, the prop derivation logic would see those stale fields and incorrectly re-classify the prop as &lt;code&gt;formattedText&lt;/code&gt;, causing broken or inconsistent front-end rendering.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Prop types in the Code Component editor are &amp;quot;derived&amp;quot; -- the system inspects serialized JSON Schema fields (&lt;code&gt;format&lt;/code&gt;, &lt;code&gt;contentMediaType&lt;/code&gt;, &lt;code&gt;x-formatting-context&lt;/code&gt;, &lt;code&gt;enum&lt;/code&gt;, etc.) to determine which &lt;code&gt;derivedType&lt;/code&gt; a prop belongs to. The &lt;code&gt;formattedText&lt;/code&gt; type is identified by the presence of &lt;code&gt;contentMediaType: 'text/html'&lt;/code&gt; and &lt;code&gt;'x-formatting-context': 'block'&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When a user switched away from &lt;code&gt;formattedText&lt;/code&gt;, the &lt;code&gt;onValueChange&lt;/code&gt; handler in &lt;code&gt;Props.tsx&lt;/code&gt; dispatched an &lt;code&gt;updateProp&lt;/code&gt; action that reset &lt;code&gt;derivedType&lt;/code&gt;, &lt;code&gt;$ref&lt;/code&gt;, and &lt;code&gt;format&lt;/code&gt;, but did not clear &lt;code&gt;contentMediaType&lt;/code&gt; or &lt;code&gt;'x-formatting-context'&lt;/code&gt;. Those fields persisted in the Redux store and were serialized to config. On the next page load, &lt;code&gt;deserializeProps&lt;/code&gt; in &lt;code&gt;utils.ts&lt;/code&gt; re-derived the prop type from the saved fields and would classify it as &lt;code&gt;formattedText&lt;/code&gt; again because those stale fields were still present and took precedence over &lt;code&gt;format&lt;/code&gt; or &lt;code&gt;enum&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix has two parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prevention in &lt;code&gt;Props.tsx&lt;/code&gt;&lt;/strong&gt;: The &lt;code&gt;updateProp&lt;/code&gt; dispatch when switching types now explicitly sets &lt;code&gt;contentMediaType: undefined&lt;/code&gt; and &lt;code&gt;'x-formatting-context': undefined&lt;/code&gt;. A comment instructs future contributors to add any new type-specific fields here when introducing new prop types.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backwards compatibility in &lt;code&gt;deserializeProps&lt;/code&gt; (&lt;code&gt;utils.ts&lt;/code&gt;)&lt;/strong&gt;: A new guard detects already-corrupted saved configs -- specifically, props that derive as &lt;code&gt;formattedText&lt;/code&gt; but also carry a &lt;code&gt;format&lt;/code&gt; value or non-empty &lt;code&gt;enum&lt;/code&gt; (which belong to other types). When detected, &lt;code&gt;contentMediaType&lt;/code&gt; and &lt;code&gt;'x-formatting-context'&lt;/code&gt; are deleted from the deserialized prop (and from &lt;code&gt;items&lt;/code&gt; for multi-value props), and the correct &lt;code&gt;derivedType&lt;/code&gt; is re-derived by running the derivation logic without those stale fields. This repairs existing configs without requiring a data migration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tests cover all target types (Date and time, Link, Text, Integer, Number, Boolean, Image, Video, List: text, List: integer) for the prevention side, and date, date-time, link (uri and uri-reference), list:text, and multi-value array props for the backwards-compatibility repair side.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 7, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (1 day later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;chandu7929 (Acquia)&lt;/li&gt;
&lt;li&gt;isholgueras (Acquia)&lt;/li&gt;
&lt;li&gt;narendrar (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;The backwards-compatibility repair in &lt;code&gt;deserializeProps&lt;/code&gt; was not in the original bug report. The issue author described the forward-looking fix (clear fields on type switch), but the committed solution also added the &lt;code&gt;deserializeProps&lt;/code&gt; guard to repair already-saved corrupted configs, which was a meaningful design decision made during implementation.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3583230: Code-component editor: NULL value error when reordering multivalue prop with empty slots in limited mode</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3583230.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-8678a9bcf536</guid>
      <pubDate>Wed, 08 Apr 2026 12:30:37 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/91371bd68e8972b439f431054b8137541ad6aa54"&gt;91371bd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3583230"&gt;#3583230&lt;/a&gt; · 4 contributors · 15 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 5&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In the code-component editor, reordering a multivalue prop in &amp;quot;limited&amp;quot; mode (with some empty slots) after reloading from the backend caused a NULL value error on save. The backend saves only the filled values (e.g., 3 items for a limit of 5). When reloaded, the UI renders all 5 slots but the underlying array only has 3 items. Accessing the empty slots returned &lt;code&gt;undefined&lt;/code&gt;, which became &lt;code&gt;null&lt;/code&gt; during JSON serialization, and the backend rejected null for string fields. This bug only appeared after a component was saved and reloaded, not on the first save.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The root cause was a mismatch between the sortable list item count and the actual display array in limited mode. In &lt;code&gt;FormPropTypeArray.tsx&lt;/code&gt; and &lt;code&gt;FormPropTypeLink.tsx&lt;/code&gt;, the &lt;code&gt;PropValuesSortableList&lt;/code&gt; items were generated using &lt;code&gt;Array.from({ length: limitedCount })&lt;/code&gt; (5 indices), but the &lt;code&gt;displayArray&lt;/code&gt; backing those slots was not padded and only contained the filled values from the backend (3 items). When the drag handler called &lt;code&gt;arrayMove(displayArray, ...)&lt;/code&gt;, it operated on the unpadded 3-item array, so indices 3 and 4 resolved to &lt;code&gt;undefined&lt;/code&gt;. These &lt;code&gt;undefined&lt;/code&gt; values were then serialized to &lt;code&gt;null&lt;/code&gt; by &lt;code&gt;JSON.stringify&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix introduces a shared &lt;code&gt;createDisplayArray&lt;/code&gt; function in &lt;code&gt;ui/src/features/code-editor/utils/arrayPropUtils.ts&lt;/code&gt;. In limited mode, it pads the array to exactly &lt;code&gt;limitedCount&lt;/code&gt; items using &lt;code&gt;exampleArray[i] ?? ''&lt;/code&gt;, replacing missing entries with empty strings instead of leaving them &lt;code&gt;undefined&lt;/code&gt;. In unlimited mode, it keeps the existing behavior of falling back to &lt;code&gt;['']&lt;/code&gt; when the array is empty. All three affected form components (&lt;code&gt;FormPropTypeArray&lt;/code&gt;, &lt;code&gt;FormPropTypeDate&lt;/code&gt;, &lt;code&gt;FormPropTypeLink&lt;/code&gt;) now call this shared function via &lt;code&gt;useMemo&lt;/code&gt;. The &lt;code&gt;PropValuesSortableList&lt;/code&gt; items are also simplified to always derive from &lt;code&gt;displayArray.map((_, index) =&amp;gt; index)&lt;/code&gt; since &lt;code&gt;displayArray&lt;/code&gt; is now guaranteed to have the correct length. &lt;code&gt;FormPropTypeDate&lt;/code&gt; previously used &lt;code&gt;exampleArray[i] || ''&lt;/code&gt; (logical OR), which would have silently replaced &lt;code&gt;0&lt;/code&gt; with &lt;code&gt;''&lt;/code&gt;; the new &lt;code&gt;?? ''&lt;/code&gt; (nullish coalescing) is more correct.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 6, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 8, 2026 (2 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kunal.sachdev (Acquia)&lt;/li&gt;
&lt;li&gt;narendrar (Acquia)&lt;/li&gt;
&lt;li&gt;isholgueras (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582804: Add endpoint to retrieve a canvas_page by UUID</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3582804.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-8675f1d60c71</guid>
      <category>Module maintainers</category>
      <pubDate>Tue, 07 Apr 2026 21:24:25 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/52a5f2589486a3e00d19ce9656b53e3b64dcbd9f"&gt;52a5f25&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582804"&gt;#3582804&lt;/a&gt; · 2 contributors · 7 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;A new &lt;code&gt;GET /canvas/api/v0/content/canvas_page/by-uuid/{uuid}&lt;/code&gt; endpoint lets callers retrieve a &lt;code&gt;canvas_page&lt;/code&gt; by its UUID instead of its integer entity ID. It returns the same response as the existing &lt;code&gt;GET /canvas/api/v0/content/canvas_page/{id}&lt;/code&gt; endpoint. This is useful when the integer ID is not known but the UUID is.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;GET /canvas/api/v0/content/canvas_page/by-uuid/{uuid}&lt;/code&gt; endpoint retrieves a &lt;code&gt;canvas_page&lt;/code&gt; by UUID, returning the same response as the existing integer-ID endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The existing &lt;code&gt;canvas.api.content.get&lt;/code&gt; route requires an integer entity ID in the path. The new &lt;code&gt;canvas.api.content.get.by_uuid&lt;/code&gt; route at &lt;code&gt;/canvas/api/v0/content/canvas_page/by-uuid/{canvas_page}&lt;/code&gt; reuses the same controller method, &lt;code&gt;ApiContentControllers::get&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;UUID-to-entity resolution is handled by a new &lt;code&gt;CanvasEntityByUuidConverter&lt;/code&gt; param converter (&lt;code&gt;src/Routing/CanvasEntityByUuidConverter.php&lt;/code&gt;), registered as a &lt;code&gt;paramconverter&lt;/code&gt; service. It applies to any route parameter whose type starts with the &lt;code&gt;canvas_entity_by_uuid:&lt;/code&gt; prefix, then calls &lt;code&gt;EntityRepositoryInterface::loadEntityByUuid()&lt;/code&gt; to load the entity. If the entity is not found, it throws &lt;code&gt;ParamNotConvertedException&lt;/code&gt;, resulting in a 404 response.&lt;/p&gt;
&lt;p&gt;The route is added to the &lt;code&gt;$page_route_names&lt;/code&gt; list in &lt;code&gt;CanvasOauthAuthenticationProvider&lt;/code&gt; so that OAuth token authentication applies to it. OpenAPI documentation is also updated with a new &lt;code&gt;entityUuid&lt;/code&gt; path parameter component and a path entry for the new endpoint.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 2, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 7, 2026 (5 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;attilatilman (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3502691: Introduce pagination for HTTP API to get list of content(pages).</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3502691.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0eef9847b9ec</guid>
      <category>Module maintainers</category>
      <pubDate>Tue, 07 Apr 2026 16:53:16 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/8b4a449f6579174049e7c99922483db4858b666b"&gt;8b4a449&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3502691"&gt;#3502691&lt;/a&gt; · 6 contributors · 22 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 9&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The HTTP API endpoint for listing content pages (&lt;code&gt;/canvas/api/v0/content/{entity_type}&lt;/code&gt;) now supports offset-based pagination. Previously the endpoint returned a flat object keyed by entity ID with a hard cap of 50 items and no way to page beyond that. It now accepts &lt;code&gt;page[offset]&lt;/code&gt; and &lt;code&gt;page[limit]&lt;/code&gt; query parameters (limit capped at 50), and the response wraps results in a &lt;code&gt;data&lt;/code&gt; array with &lt;code&gt;meta.count&lt;/code&gt; (total items) and JSON:API-style &lt;code&gt;links&lt;/code&gt; for &lt;code&gt;self&lt;/code&gt;/&lt;code&gt;first&lt;/code&gt;/&lt;code&gt;prev&lt;/code&gt;/&lt;code&gt;next&lt;/code&gt;/&lt;code&gt;last&lt;/code&gt;. The CLI &lt;code&gt;listPages()&lt;/code&gt; command automatically follows &lt;code&gt;links.next&lt;/code&gt; until all pages are fetched, replacing the earlier 50-page warning that told users some pages may have been missed.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API change:&lt;/strong&gt; GET &lt;code&gt;/canvas/api/v0/content/{entity_type}&lt;/code&gt; response changed from a flat keyed object to &lt;code&gt;{data: [...], meta: {count}, links: {...}}&lt;/code&gt;; &lt;code&gt;page[offset]&lt;/code&gt; and &lt;code&gt;page[limit]&lt;/code&gt; query parameters added; search responses return &lt;code&gt;data&lt;/code&gt; only with no &lt;code&gt;meta&lt;/code&gt;/&lt;code&gt;links&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; New &lt;code&gt;OffsetPage&lt;/code&gt; value object (&lt;code&gt;Drupal\canvas\Resource\OffsetPage&lt;/code&gt;) for parsing &lt;code&gt;page[offset]&lt;/code&gt;/&lt;code&gt;page[limit]&lt;/code&gt; query parameters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The previous endpoint returned a flat JSON object keyed by entity ID (e.g. &lt;code&gt;{&amp;quot;1&amp;quot;: {...}, &amp;quot;2&amp;quot;: {...}}&lt;/code&gt;), always with a range of 0–50. There was no way to page past the first 50 results, and the CLI emitted a warning when it hit that cap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;OffsetPage&lt;/code&gt; value object&lt;/strong&gt; (&lt;code&gt;src/Resource/OffsetPage.php&lt;/code&gt;): A new final class that parses &lt;code&gt;page[offset]&lt;/code&gt; and &lt;code&gt;page[limit]&lt;/code&gt; from the request. &lt;code&gt;MAX_SIZE = 50&lt;/code&gt; clamps the limit; negative offsets are floored to 0.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ApiContentControllers::list()&lt;/code&gt;&lt;/strong&gt;: When no &lt;code&gt;search&lt;/code&gt; parameter is present, &lt;code&gt;OffsetPage::createFromRequest()&lt;/code&gt; is called to read offset/limit. A separate count query (&lt;code&gt;$storage-&amp;gt;getQuery()-&amp;gt;count()&lt;/code&gt;) runs first to populate &lt;code&gt;meta.count&lt;/code&gt;. The main query then uses &lt;code&gt;-&amp;gt;range($offset, $limit)&lt;/code&gt;. Results are placed in a &lt;code&gt;data&lt;/code&gt; array, and &lt;code&gt;buildPaginationLinks()&lt;/code&gt; produces the link set. The &lt;code&gt;url.query_args:page&lt;/code&gt; cache context is added only for non-search requests.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;search&lt;/code&gt; is present, pagination is intentionally skipped. This is because search combines stored entity query results with auto-save matches in memory, making offset-based pagination against only the stored query impractical. Search responses return &lt;code&gt;{data: [...]}&lt;/code&gt; with no &lt;code&gt;meta&lt;/code&gt; or &lt;code&gt;links&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;buildPaginationLinks()&lt;/code&gt;&lt;/strong&gt;: A private method that constructs JSON:API-style link objects. &lt;code&gt;self&lt;/code&gt; is always present; &lt;code&gt;first&lt;/code&gt;/&lt;code&gt;prev&lt;/code&gt; appear when &lt;code&gt;$offset &amp;gt; 0&lt;/code&gt;; &lt;code&gt;next&lt;/code&gt;/&lt;code&gt;last&lt;/code&gt; appear when &lt;code&gt;$offset + $limit &amp;lt; $total&lt;/code&gt;. &lt;code&gt;last_offset&lt;/code&gt; is computed as &lt;code&gt;floor(($total - 1) / $limit) * $limit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;executeQueryInRenderContext()&lt;/code&gt;&lt;/strong&gt;: Return type widened from &lt;code&gt;array&lt;/code&gt; to &lt;code&gt;array|int&lt;/code&gt; to accommodate the count query.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLI&lt;/strong&gt; (&lt;code&gt;packages/cli/src/services/api.ts&lt;/code&gt;): &lt;code&gt;listPages()&lt;/code&gt; now loops, following &lt;code&gt;body.links?.next?.href&lt;/code&gt; until &lt;code&gt;null&lt;/code&gt;. The 50-item warnings in &lt;code&gt;pull.ts&lt;/code&gt; and &lt;code&gt;push.ts&lt;/code&gt; are removed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UI&lt;/strong&gt; (&lt;code&gt;ui/src/services/content.ts&lt;/code&gt;): &lt;code&gt;ContentListResponse&lt;/code&gt; interface updated from &lt;code&gt;{[key: string]: ContentStub}&lt;/code&gt; to &lt;code&gt;{data: ContentStub[], meta?: {count: number}, links?: ...}&lt;/code&gt;. &lt;code&gt;transformResponse&lt;/code&gt; now returns &lt;code&gt;response.data&lt;/code&gt; instead of &lt;code&gt;Object.values(response)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docs/config-management.md&lt;/code&gt;&lt;/strong&gt;: The bullet &amp;quot;Canvas's HTTP API does not need pagination support&amp;quot; is narrowed to &amp;quot;Canvas's config entity HTTP API does not need pagination support&amp;quot; to reflect that content entity APIs do support pagination.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;GET /canvas/api/v0/content/{entity_type}&lt;/code&gt; response shape changed.&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;1&amp;quot;: { &amp;quot;id&amp;quot;: 1, &amp;quot;title&amp;quot;: &amp;quot;Home&amp;quot;, ... },
  &amp;quot;2&amp;quot;: { &amp;quot;id&amp;quot;: 2, &amp;quot;title&amp;quot;: &amp;quot;About&amp;quot;, ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;data&amp;quot;: [
    { &amp;quot;id&amp;quot;: 1, &amp;quot;title&amp;quot;: &amp;quot;Home&amp;quot;, ... },
    { &amp;quot;id&amp;quot;: 2, &amp;quot;title&amp;quot;: &amp;quot;About&amp;quot;, ... }
  ],
  &amp;quot;meta&amp;quot;: { &amp;quot;count&amp;quot;: 42 },
  &amp;quot;links&amp;quot;: {
    &amp;quot;self&amp;quot;: { &amp;quot;href&amp;quot;: &amp;quot;/canvas/api/v0/content/canvas_page?page[offset]=0&amp;amp;page[limit]=50&amp;quot; },
    &amp;quot;next&amp;quot;: { &amp;quot;href&amp;quot;: &amp;quot;/canvas/api/v0/content/canvas_page?page[offset]=50&amp;amp;page[limit]=50&amp;quot; },
    &amp;quot;last&amp;quot;: { &amp;quot;href&amp;quot;: &amp;quot;/canvas/api/v0/content/canvas_page?page[offset]=0&amp;amp;page[limit]=50&amp;quot; }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Search responses omit &lt;code&gt;meta&lt;/code&gt; and &lt;code&gt;links&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;data&amp;quot;: [
    { &amp;quot;id&amp;quot;: 1, &amp;quot;title&amp;quot;: &amp;quot;Home&amp;quot;, ... }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any code that treated the response as a keyed object must be updated to read from &lt;code&gt;response.data&lt;/code&gt;. To page through all results, follow &lt;code&gt;links.next.href&lt;/code&gt; until it is absent.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: January 28, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 7, 2026 (1 year later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;amangrover90 (Acquia)&lt;/li&gt;
&lt;li&gt;mglaman (Acquia)&lt;/li&gt;
&lt;li&gt;catch (Third and Grove)&lt;/li&gt;
&lt;li&gt;wim leers (Acquia)&lt;/li&gt;
&lt;li&gt;penyaskito (Acquia)&lt;/li&gt;
&lt;li&gt;lauriii (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;wim leers closed the issue in comment #7, arguing that search (&lt;a href="https://www.drupal.org/node/3518292"&gt;#3518292&lt;/a&gt;) made general pagination unnecessary and that it was better to search for content rather than page through all of it. The issue was reopened when &lt;a href="https://www.drupal.org/node/3575144"&gt;#3575144&lt;/a&gt; (Add unstable v0 external Canvas Page API endpoints) surfaced the need again. The doc comment in &lt;code&gt;config-management.md&lt;/code&gt; that had been used to justify closing it (&amp;quot;does not need pagination support&amp;quot;) was narrowed in this very diff to apply only to config entities.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3563780: Push command should respect dependency order between components</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3563780.md</link>
      <guid isPermaLink="false">019d857f-33e3-726a-abae-0f47aeb34e74</guid>
      <pubDate>Tue, 07 Apr 2026 13:48:49 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Major bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/f479c68c80fcb8de582ea730f01fced56355fd92"&gt;f479c68&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3563780"&gt;#3563780&lt;/a&gt; · 4 contributors · 13 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;When pushing multiple JavaScript components via the CLI, the push command sent components to the server in an arbitrary order. If a component that imports another component was pushed before its dependency existed on the server, the backend rejected it with a validation error. The push command now sorts components by dependency order before uploading, so dependencies are always created first and deleted last.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The Canvas CLI &lt;code&gt;push&lt;/code&gt; command builds and uploads &lt;code&gt;JavaScriptComponent&lt;/code&gt; config entities to the Drupal backend. The backend's &lt;code&gt;JavaScriptComponent::addJavaScriptComponentsDependencies()&lt;/code&gt; validates that every imported component already exists when a component is created or updated. If components are pushed in an order where a component that imports another arrives before its dependency, the server returns an error like &lt;code&gt;[importedJsComponents.0] The JavaScript component with the machine name 'foo_button' does not exist.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The fix has two parts. First, a new &lt;code&gt;sortByDependencies&lt;/code&gt; utility (&lt;code&gt;packages/cli/src/utils/dependency-sort.ts&lt;/code&gt;) implements Kahn's topological sort algorithm with wave/level tracking. It returns an array of waves, where each wave contains items that share the same dependency depth. Items within a wave can be processed in parallel; waves must be processed sequentially.&lt;/p&gt;
&lt;p&gt;Second, &lt;code&gt;uploadComponents&lt;/code&gt; in &lt;code&gt;prepare-push.ts&lt;/code&gt; was refactored. It now separates tasks into creates, updates, and deletes before processing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Creates&lt;/strong&gt;: sorted into dependency-first waves (leaf components first, composites last), with each wave processed in parallel via the existing &lt;code&gt;processInPool&lt;/code&gt; helper, waves run sequentially.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Updates&lt;/strong&gt;: processed in parallel as before, because the components already exist on the server and order does not matter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deletes&lt;/strong&gt;: also sorted by &lt;code&gt;sortByDependencies&lt;/code&gt;, then the wave array is reversed. This ensures dependents are deleted before their dependencies, preventing a &amp;quot;component still referenced&amp;quot; error from the backend. For components being deleted that have no local file, the code parses &lt;code&gt;sourceCodeJs&lt;/code&gt; from the server response using Babel's &lt;code&gt;parse&lt;/code&gt; and &lt;code&gt;getImportsFromAst&lt;/code&gt; to extract the dependency list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;importedJsComponents&lt;/code&gt; field was added to both the &lt;code&gt;ComponentUploadTask&lt;/code&gt; union type and the &lt;code&gt;PreparedComponent&lt;/code&gt; interface to carry dependency information through the pipeline.&lt;/p&gt;
&lt;p&gt;Circular dependencies are not detected; they are silently grouped into a final wave. Follow-up issue &lt;a href="https://www.drupal.org/node/3583474"&gt;#3583474&lt;/a&gt; tracks adding explicit circular dependency validation to the push command.&lt;/p&gt;
&lt;p&gt;The UI is not affected by this bug. When a component is first created through the Canvas UI, a &lt;code&gt;JavaScriptComponent&lt;/code&gt; entity is created immediately (empty), so any component that can be referenced via &lt;code&gt;@import&lt;/code&gt; in the UI already exists on the server.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: December 17, 2025&lt;/li&gt;
&lt;li&gt;Committed: April 7, 2026 (3 months later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;hooroomoo&lt;/li&gt;
&lt;li&gt;wotnak (Smartbees)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;A passing comment from &lt;code&gt;penyaskito&lt;/code&gt; asked whether the Canvas UI's auto-save publishing had the same ordering problem. &lt;code&gt;effulgentsia&lt;/code&gt; and &lt;code&gt;hooroomoo&lt;/code&gt; explained that it does not: the UI creates the config entity as soon as a component is first added, so any component that can be typed into an &lt;code&gt;@import&lt;/code&gt; statement already exists on the server. This confirmed the bug was CLI-only and scoped the fix accordingly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3581622: Create PredictableImageStyleItokTestTrait</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3581622.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-9511b453aeb0</guid>
      <pubDate>Tue, 07 Apr 2026 02:30:24 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/19c552ce4170630c85ba27ee8555c954bdfafbd8"&gt;19c552c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3581622"&gt;#3581622&lt;/a&gt; · 2 contributors · 13 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is a test-only cleanup with no user-facing effect. A block of code that fixed the private key and hash salt to produce predictable image style &lt;code&gt;itok&lt;/code&gt; values was duplicated in at least five kernel test files. That block is now consolidated into a new &lt;code&gt;PredictableImageStyleItokTestTrait&lt;/code&gt; in &lt;code&gt;tests/src/Kernel/Traits/&lt;/code&gt;. Because the new shared hash salt value is slightly different from the old per-file value, the hard-coded &lt;code&gt;itok&lt;/code&gt; strings in several test assertions were updated.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Image style URLs in Drupal include an &lt;code&gt;itok&lt;/code&gt; query parameter derived from a hash of the private key and hash salt. Kernel tests that assert on those URLs need a fixed, predictable &lt;code&gt;itok&lt;/code&gt;. The pattern used &lt;code&gt;ReflectionClass&lt;/code&gt; to override the &lt;code&gt;Settings&lt;/code&gt; singleton and set &lt;code&gt;system.private_key&lt;/code&gt; state to known values.&lt;/p&gt;
&lt;p&gt;This pattern was copy-pasted into five kernel test classes and base classes: &lt;code&gt;JsonapiSupportTest&lt;/code&gt;, &lt;code&gt;ParametrizedImageStyleTest&lt;/code&gt;, &lt;code&gt;GeneratedFieldExplicitInputUxComponentSourceBaseTestBase&lt;/code&gt;, &lt;code&gt;PropSourceTestBase&lt;/code&gt;, and &lt;code&gt;CanvasTwigExtensionFiltersTest&lt;/code&gt;. The issue asked to consolidate them into a single trait.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;PredictableImageStyleItokTestTrait&lt;/code&gt; (at &lt;code&gt;tests/src/Kernel/Traits/PredictableImageStyleItokTestTrait.php&lt;/code&gt;) exposes one method, &lt;code&gt;setupPredictableItok()&lt;/code&gt;. It sets the &lt;code&gt;system.private_key&lt;/code&gt; state to &lt;code&gt;'dynamic_image_style_private_key'&lt;/code&gt; and replaces the &lt;code&gt;Settings&lt;/code&gt; singleton with a new instance whose &lt;code&gt;hash_salt&lt;/code&gt; is &lt;code&gt;'dynamic_image_style_hash_salt_large_enough_for_simple_oauth'&lt;/code&gt;. That value is slightly longer than the previous per-file value (&lt;code&gt;'dynamic_image_style_hash_salt'&lt;/code&gt;), which changed the resulting &lt;code&gt;itok&lt;/code&gt; tokens and required updating the expected strings in several test data sets.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 26, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 7, 2026 (11 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;penyaskito (Acquia)&lt;/li&gt;
&lt;li&gt;sapnil_biswas&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;sapnil_biswas's initial patch included unrelated cypress/playwright changes. penyaskito asked for those to be reverted before the patch was accepted.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582772: Video multivalue props throw "Cannot read properties of undefined (reading 'endsWith')" error when switching to Limited mode</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3582772.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867319d9281b</guid>
      <pubDate>Mon, 06 Apr 2026 12:45:46 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/7de823805130cee1d363b39347649e3cf5915e1c"&gt;7de8238&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582772"&gt;#3582772&lt;/a&gt; · 4 contributors · 14 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 5&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This fixes a crash in the Canvas code component editor when switching video props with &amp;quot;Allow multiple values&amp;quot; enabled to &amp;quot;Limited&amp;quot; mode. The editor tried to initialize the video array with empty strings, but video props expect objects with a &lt;code&gt;src&lt;/code&gt; property. When serialization attempted to process these strings, it threw &amp;quot;Cannot read properties of undefined (reading 'endsWith')&amp;quot;. The fix ensures video and image props are initialized with empty arrays or existing valid objects instead of empty strings, and adds defensive filtering during serialization to skip invalid items.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The issue was a regression from &lt;a href="https://www.drupal.org/node/3581869"&gt;#3581869&lt;/a&gt;. When a multivalue prop switched to limited mode, the initialization code in Props.tsx called &lt;code&gt;createArrayWithCount&lt;/code&gt; with an empty string ('') as the default value for all prop types. This worked for primitive types like text and numbers, but video and image props expect objects with a &lt;code&gt;src&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;When the &lt;code&gt;serializeExample&lt;/code&gt; function in utils.ts tried to serialize these arrays, it called &lt;code&gt;serializeVideoSrc&lt;/code&gt; on the empty strings. This function (or code it calls) attempts to access properties like &lt;code&gt;src.endsWith()&lt;/code&gt;, which failed when the value was a string instead of an object.&lt;/p&gt;
&lt;p&gt;The fix modifies Props.tsx to detect video and image props by checking their &lt;code&gt;derivedType&lt;/code&gt; field. For these types, the limited mode initialization now preserves any existing valid objects from the current example array (using &lt;code&gt;slice(0, count)&lt;/code&gt;) or starts with an empty array, rather than filling with empty strings. This logic was added in two places: the value mode change handler when switching to limited mode, and the &lt;code&gt;createLimitedExample&lt;/code&gt; helper function used when adjusting the limited count.&lt;/p&gt;
&lt;p&gt;In utils.ts, the &lt;code&gt;serializeExample&lt;/code&gt; function adds defensive filtering for both video and image arrays. Before processing, it filters out any items that are not objects, lack a &lt;code&gt;src&lt;/code&gt; property, or have an empty &lt;code&gt;src&lt;/code&gt;. For videos, the filtered array is then mapped with &lt;code&gt;serializeVideoSrc&lt;/code&gt;. This ensures serialization doesn't crash even if invalid data enters the example array.&lt;/p&gt;
&lt;p&gt;The tests verify that &lt;code&gt;serializeProps&lt;/code&gt; handles arrays containing empty strings without throwing errors, and that all prop types can switch to limited mode successfully.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 2, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 6, 2026 (4 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kunal.sachdev (Acquia)&lt;/li&gt;
&lt;li&gt;narendrar (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;Comment #6 noted that Image props did not show the same error, questioning whether the fix should apply to them. Comment #7 confirmed Image props weren't failing but explained they need the same handling as Video props for consistency, since both are object types that could encounter the same issue.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582709: Provide an endpoint to identify that a push is starting and has stopped</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3582709.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-9520f12ac370</guid>
      <category>Module maintainers</category>
      <pubDate>Fri, 03 Apr 2026 14:34:35 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal feature request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/4a242b335c9a667a341de61c3781ffee625c858b"&gt;4a242b3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582709"&gt;#3582709&lt;/a&gt; · 1 contributor · 12 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Canvas CLI push command now signals the server when a push starts, completes, or fails. This allows the Canvas editor UI to display a notification showing the push is in progress, then update it to show success or failure when the push finishes. The endpoints also dispatch events that other systems can subscribe to.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; Three POST endpoints: &lt;code&gt;/canvas/api/v0/push/start&lt;/code&gt;, &lt;code&gt;/canvas/api/v0/push/complete&lt;/code&gt;, and &lt;code&gt;/canvas/api/v0/push/fail&lt;/code&gt; for CLI push lifecycle signaling&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The Canvas module has an existing notification system that displays messages in the editor UI. This change adds three REST endpoints that the CLI push command calls to signal lifecycle events.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ApiPushController&lt;/code&gt; provides three methods that each create a notification and dispatch an event. The &lt;code&gt;start()&lt;/code&gt; method creates a &amp;quot;processing&amp;quot; notification. The &lt;code&gt;complete()&lt;/code&gt; method creates a &amp;quot;success&amp;quot; notification. The &lt;code&gt;fail()&lt;/code&gt; method creates an &amp;quot;error&amp;quot; notification and accepts an optional JSON body with a &lt;code&gt;message&lt;/code&gt; field. All three use the same notification key (&lt;code&gt;cli-push&lt;/code&gt;), so the existing notification system's &lt;code&gt;KEY_REPLACE_TYPES&lt;/code&gt; logic automatically replaces the processing notification when complete or fail is called.&lt;/p&gt;
&lt;p&gt;Each method dispatches a &lt;code&gt;PushEvent&lt;/code&gt; containing a &lt;code&gt;PushStatus&lt;/code&gt; enum value (Started, Completed, or Failed) for extensibility.&lt;/p&gt;
&lt;p&gt;The CLI push command in &lt;code&gt;packages/cli/src/commands/push.ts&lt;/code&gt; calls &lt;code&gt;/push/start&lt;/code&gt; before beginning work and either &lt;code&gt;/push/complete&lt;/code&gt; or &lt;code&gt;/push/fail&lt;/code&gt; at the end. The calls are best-effort: they are wrapped in try-catch blocks and failures do not affect the push itself. Error handling was refactored to throw exceptions instead of calling &lt;code&gt;process.exit()&lt;/code&gt; directly, so the fail signal can be sent in the catch block.&lt;/p&gt;
&lt;p&gt;The OAuth authentication provider was updated to recognize these three routes alongside the existing artifact upload route.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 1, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 3, 2026 (1 day later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mglaman (Acquia)&lt;/li&gt;
&lt;li&gt;wotnak (Smartbees)&lt;/li&gt;
&lt;li&gt;penyaskito (Acquia)&lt;/li&gt;
&lt;li&gt;hooroomoo (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3581431: Error in link prop type when reordering list or select values</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3581431.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-950f3bced810</guid>
      <pubDate>Fri, 03 Apr 2026 10:29:14 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal bug report&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/4bbc7c870c7add408f217a94737ddf63d853b842"&gt;4bbc7c8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3581431"&gt;#3581431&lt;/a&gt; · 6 contributors · 26 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 8&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This fixes errors that occurred when reordering multi-value link or datetime fields in Canvas. The issue was that empty rows weren't being handled correctly during transformation to the backend format. Empty rows are now converted to null placeholders to preserve their position in the list, while trailing empty rows are removed. Records with weight values are now properly sorted before transformation.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The transforms in &lt;code&gt;ui/src/utils/transforms.ts&lt;/code&gt; convert form values to backend format. The &lt;code&gt;link&lt;/code&gt; and &lt;code&gt;dateTime&lt;/code&gt; transformers for multi-value fields had bugs when handling empty rows and reordering.&lt;/p&gt;
&lt;p&gt;Two new helper functions were introduced. &lt;code&gt;trimTrailingNulls&lt;/code&gt; maps empty strings to null so the backend can render filled rows at the correct positions, then removes trailing nulls because empty rows at the end have no positional significance. &lt;code&gt;normalizeMultipleRecords&lt;/code&gt; handles object and array inputs, sorts by &lt;code&gt;_weight&lt;/code&gt; (other widgets) or &lt;code&gt;weight&lt;/code&gt; (media library) fields, and keeps non-weighted values like &lt;code&gt;add_more&lt;/code&gt; after weighted records.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;link&lt;/code&gt; transformer now uses both helpers and adds a &lt;code&gt;hasStringUri&lt;/code&gt; type guard for safer type checking. The &lt;code&gt;dateTime&lt;/code&gt; transformer uses &lt;code&gt;trimTrailingNulls&lt;/code&gt; for multiple values. The &lt;code&gt;mainProperty&lt;/code&gt; function was updated to use &lt;code&gt;normalizeMultipleRecords&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tests verify position preservation (null placeholders for empty rows in the middle), trailing null removal, empty array return when all rows are empty, and weight-based sorting for both simple URIs and titled links.&lt;/p&gt;
&lt;p&gt;A second commit removed an array check at the start of &lt;code&gt;normalizeMultipleRecords&lt;/code&gt; that prevented arrays from being processed through the weight-sorting logic, which was blocking reordering from persisting for multi-value fields.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 25, 2026&lt;/li&gt;
&lt;li&gt;First commit: April 1, 2026 (6 days later)&lt;/li&gt;
&lt;li&gt;Last commit: April 3, 2026 (1 day and 2 commits later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;vipin.mittal18 (Acquia)&lt;/li&gt;
&lt;li&gt;chandu7929 (Acquia)&lt;/li&gt;
&lt;li&gt;narendrar (Acquia)&lt;/li&gt;
&lt;li&gt;utkarsh_33 (Acquia)&lt;/li&gt;
&lt;li&gt;bnjmnm (Acquia)&lt;/li&gt;
&lt;li&gt;tim.plunkett (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;bnjmnm requested explanatory comments be added to the merge request to distinguish moved code from new logic before approval. A regression was discovered after the first commit where reordering wasn't persisting, fixed in a second commit by removing a condition that blocked array inputs from weight sorting.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3580217: [Notifications] Toast notifications</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3580217.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94ffedd933ab</guid>
      <pubDate>Thu, 02 Apr 2026 22:07:06 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/521904bb25cf0f9d57402f0fd5148b959d79ffb1"&gt;521904b&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3580217"&gt;#3580217&lt;/a&gt; · 2 contributors · 8 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Canvas now shows toast notifications in the top-right corner when new notifications arrive after the page was opened. Toasts display the notification's title, message, and action links with type-specific styling (colored borders and backgrounds matching the Activity Center). They automatically dismiss after 15 seconds without marking the notification as read. Users can explicitly dismiss a toast by clicking X, which marks it as read, or click an action button to mark it as read and open the link in a new tab. Toasts only appear when dev mode is enabled.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The implementation uses custom React components rather than the existing Sonner library. The existing Sonner &lt;code&gt;Toaster&lt;/code&gt; in &lt;code&gt;Toast.tsx&lt;/code&gt; remains for the bottom-center saving overlay only. &lt;code&gt;NotificationToastManager&lt;/code&gt; is mounted at the app level in &lt;code&gt;App.tsx&lt;/code&gt;, gated by &lt;code&gt;drupalSettings.canvas.devMode&lt;/code&gt;. It subscribes to the RTK Query notifications cache (polling is configured on &lt;code&gt;NotificationBell&lt;/code&gt; via &lt;code&gt;useGetNotificationsQuery&lt;/code&gt;) and filters for notifications with &lt;code&gt;timestamp &amp;gt; pageOpenedAt&lt;/code&gt; from Redux state. The manager maintains a &lt;code&gt;visibleToasts&lt;/code&gt; state array and a &lt;code&gt;shownIds&lt;/code&gt; ref set for deduplication. When new notifications arrive, it adds them to &lt;code&gt;visibleToasts&lt;/code&gt; and starts a 15-second timer (&lt;code&gt;TOAST_DURATION&lt;/code&gt; constant in &lt;code&gt;constants.ts&lt;/code&gt;). Auto-dismiss removes the toast without calling &lt;code&gt;useMarkNotificationsReadMutation&lt;/code&gt;. Explicit dismiss (X button) and action button clicks both call &lt;code&gt;markRead&lt;/code&gt; with the notification ID and remove the toast. Action clicks also open the link via &lt;code&gt;window.open(href, '_blank', 'noopener,noreferrer')&lt;/code&gt;. &lt;code&gt;NotificationToast&lt;/code&gt; is a presentational component that renders the icon, title, message, action buttons, and dismiss button. It uses a &lt;code&gt;data-type&lt;/code&gt; attribute for CSS styling hooks. Type-specific colors are defined in &lt;code&gt;NotificationToast.module.css&lt;/code&gt;: warning uses &lt;code&gt;--amber-9&lt;/code&gt; border and &lt;code&gt;--amber-2&lt;/code&gt; background, error uses &lt;code&gt;--red-9&lt;/code&gt; border and &lt;code&gt;--red-3&lt;/code&gt; background, info and processing use &lt;code&gt;--blue-9&lt;/code&gt; border, and success uses &lt;code&gt;--green-9&lt;/code&gt; border. The processing type icon has a spin animation applied via the &lt;code&gt;styles.spin&lt;/code&gt; class. The toast container uses &lt;code&gt;position: fixed&lt;/code&gt; at &lt;code&gt;top: var(--space-9)&lt;/code&gt; and &lt;code&gt;right: var(--space-4)&lt;/code&gt; in &lt;code&gt;NotificationToastManager.module.css&lt;/code&gt;, stacking toasts vertically with &lt;code&gt;flex-direction: column&lt;/code&gt;. Component tests use Vitest with fake timers to verify timestamp filtering, deduplication, auto-dismiss timing, and the &lt;code&gt;markRead&lt;/code&gt; call on explicit dismiss. Playwright E2E tests seed notifications via Drush after page load and verify toast appearance, styling, dismiss behavior, and feature flag gating.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 2, 2026 (14 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;justafish (Acquia)&lt;/li&gt;
&lt;li&gt;lauriii (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582632: Transform authored prop values for pages to server side format before push</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3582632.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-951f3f38b519</guid>
      <pubDate>Thu, 02 Apr 2026 19:08:10 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/9f93a8986ff51be383e57a8b6f52ffb0a9217f2a"&gt;9f93a89&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582632"&gt;#3582632&lt;/a&gt; · 2 contributors · 7 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This change improves the Canvas CLI's pull/push workflow for pages. When pulling pages from the server, they are now stored locally with resolved prop values that match the format used for rendering. When pushing pages back to the server, the CLI automatically transforms these values to the backend format. This makes it easier to modify and render pages locally and work with coding agents, because the local files use the same prop value format as the json-render tooling.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Canvas backend stores certain prop types differently from how they're used when rendering. Formatted text props are stored as &lt;code&gt;{ value, format }&lt;/code&gt; objects on the server but rendered as plain strings. Link props are stored as &lt;code&gt;{ uri, options }&lt;/code&gt; objects but rendered as plain URL strings.&lt;/p&gt;
&lt;p&gt;The pull direction now stores &lt;code&gt;inputs_resolved&lt;/code&gt; instead of raw &lt;code&gt;inputs&lt;/code&gt; in local JSON files. The &lt;code&gt;pageToAuthoredSpec&lt;/code&gt; function uses &lt;code&gt;inputs_resolved&lt;/code&gt; when available. The &lt;code&gt;CanvasComponentTreeNode&lt;/code&gt; interface was updated to include an optional &lt;code&gt;inputs_resolved&lt;/code&gt; field.&lt;/p&gt;
&lt;p&gt;For push, a new transformation layer was added in &lt;code&gt;prop-transforms.ts&lt;/code&gt;. It implements a transformer pattern where each transformer has a &lt;code&gt;matches&lt;/code&gt; method to check if it applies to a prop schema and a &lt;code&gt;serialize&lt;/code&gt; method to convert the value. Two transformers were implemented:&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;formattedTextTransformer&lt;/code&gt; detects props with &lt;code&gt;contentMediaType: 'text/html'&lt;/code&gt; and wraps strings into &lt;code&gt;{ value, format }&lt;/code&gt; objects. The format is &lt;code&gt;canvas_html_inline&lt;/code&gt; or &lt;code&gt;canvas_html_block&lt;/code&gt; based on the &lt;code&gt;x-formatting-context&lt;/code&gt; schema property, defaulting to block when absent.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;linkTransformer&lt;/code&gt; detects props with JSON Schema &lt;code&gt;format&lt;/code&gt; of &lt;code&gt;uri&lt;/code&gt;, &lt;code&gt;uri-reference&lt;/code&gt;, &lt;code&gt;iri&lt;/code&gt;, or &lt;code&gt;iri-reference&lt;/code&gt;. It wraps strings into &lt;code&gt;{ uri, options: [] }&lt;/code&gt; objects. For &lt;code&gt;uri-reference&lt;/code&gt; and &lt;code&gt;iri-reference&lt;/code&gt; formats, relative paths without a scheme are prefixed with &lt;code&gt;internal:&lt;/code&gt; as expected by Drupal's link field storage.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;preparePages&lt;/code&gt; function called during push now loads component metadata, calls &lt;code&gt;serializeElementMapForServer&lt;/code&gt; to transform authored props to server format, then calls &lt;code&gt;authoredSpecToComponentTree&lt;/code&gt; to build the component tree sent to the server.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 1, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 2, 2026 (1 day later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;wotnak (Smartbees)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Collaboration&lt;/h3&gt;
&lt;p&gt;hooroomoo made their first commit to this issue's fork.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3582776: stylelint failures on HEAD</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3582776.md</link>
      <guid isPermaLink="false">019d857f-33e5-770a-9eef-867462f90fd3</guid>
      <pubDate>Thu, 02 Apr 2026 11:05:35 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/1d6cd36fabdd8bde60ee9d03f25c2fbb47579baf"&gt;1d6cd36&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3582776"&gt;#3582776&lt;/a&gt; · 2 contributors · 9 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This change fixes accumulated stylelint violations in CSS files and integrates stylelint into the automatic fix workflow. Stylelint had not been running as part of &lt;code&gt;npm run fix&lt;/code&gt;, so formatting violations accumulated over time and were tracked in a suppressions file. The CSS formatting changes (property order, color notation, etc.) do not affect visual output. This is purely internal code quality maintenance.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;Stylelint was not integrated into the &lt;code&gt;npm run fix&lt;/code&gt; command, so CSS formatting violations accumulated. A &lt;code&gt;stylelint-suppressions.json&lt;/code&gt; file tracked 307 violations across 34 files.&lt;/p&gt;
&lt;p&gt;The fix involved three parts. First, &lt;code&gt;.stylelintrc.json&lt;/code&gt; was updated to handle project-specific patterns: the &lt;code&gt;at-rule-no-unknown&lt;/code&gt; rule now ignores &lt;code&gt;@theme&lt;/code&gt;, &lt;code&gt;@custom-variant&lt;/code&gt;, &lt;code&gt;@utility&lt;/code&gt;, and &lt;code&gt;@slot&lt;/code&gt; at-rules, and &lt;code&gt;selector-pseudo-class-no-unknown&lt;/code&gt; now ignores the &lt;code&gt;:global&lt;/code&gt; pseudo-class used by CSS Modules. An override disables &lt;code&gt;nesting-selector-no-missing-scoping-root&lt;/code&gt; for &lt;code&gt;packages/workbench/src/client/**/*.css&lt;/code&gt;. The &lt;code&gt;.stylelintignore&lt;/code&gt; file excludes &lt;code&gt;ui/dist/**&lt;/code&gt; from linting.&lt;/p&gt;
&lt;p&gt;Second, &lt;code&gt;npm run lint:stylelint -- --fix&lt;/code&gt; was run across the codebase. This auto-formatted CSS files to comply with configured rules. Changes include property reordering (e.g., &lt;code&gt;display&lt;/code&gt; before &lt;code&gt;width&lt;/code&gt;), color function notation (&lt;code&gt;rgba()&lt;/code&gt; to &lt;code&gt;rgb()&lt;/code&gt; with alpha parameter, &lt;code&gt;hsla()&lt;/code&gt; to &lt;code&gt;hsl()&lt;/code&gt; with alpha), alpha value notation in &lt;code&gt;oklch()&lt;/code&gt; (decimal &lt;code&gt;0.145&lt;/code&gt; to percentage &lt;code&gt;14.5%&lt;/code&gt;), hex color shortening (&lt;code&gt;#ccccff&lt;/code&gt; to &lt;code&gt;#ccf&lt;/code&gt;), splitting single-line declaration blocks to multiple lines, and using &lt;code&gt;overflow-wrap&lt;/code&gt; instead of deprecated &lt;code&gt;word-break: break-word&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Third, the &lt;code&gt;fix&lt;/code&gt; script in &lt;code&gt;package.json&lt;/code&gt; now includes &lt;code&gt;&amp;amp;&amp;amp; npm run lint:stylelint -- --fix&lt;/code&gt;. The &lt;code&gt;stylelint-suppressions.json&lt;/code&gt; file was deleted since all violations are resolved.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: April 2, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 2, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;lauriii (Acquia)&lt;/li&gt;
&lt;li&gt;justafish (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3580210: [Notifications] REST API endpoints</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3580210.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94fb6bc43bfb</guid>
      <pubDate>Wed, 01 Apr 2026 20:03:21 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/a35f3db67081f6f656ddad391e47e87298ace5d4"&gt;a35f3db&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3580210"&gt;#3580210&lt;/a&gt; · 1 contributor · 6 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Canvas's notification system now has REST API endpoints that the Canvas UI uses to fetch and mark notifications as read. These endpoints are internal to Canvas and not intended for external use.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The Canvas module provides a notification system for background processes like GitHub sync. This change adds two REST endpoints so the Canvas UI can display and interact with those notifications.&lt;/p&gt;
&lt;p&gt;Two routes are added to &lt;code&gt;canvas.routing.yml&lt;/code&gt;: GET &lt;code&gt;/canvas/api/v0/notifications&lt;/code&gt; and POST &lt;code&gt;/canvas/api/v0/notifications/read&lt;/code&gt;. Both require &lt;code&gt;_canvas_authentication_required&lt;/code&gt; and &lt;code&gt;_canvas_ui_access&lt;/code&gt; checks.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ApiNotificationController&lt;/code&gt; implements both endpoints. The &lt;code&gt;list()&lt;/code&gt; method calls &lt;code&gt;CanvasNotificationHandler::getRecent()&lt;/code&gt; with the current user's ID and returns notifications wrapped in a &lt;code&gt;data&lt;/code&gt; object. Each notification includes &lt;code&gt;hasRead&lt;/code&gt; state specific to the requesting user. The &lt;code&gt;markRead()&lt;/code&gt; method decodes the request body, validates that an &lt;code&gt;ids&lt;/code&gt; array is present, calls &lt;code&gt;CanvasNotificationHandler::markRead()&lt;/code&gt; with the current user's ID and the notification IDs, and returns 204 No Content.&lt;/p&gt;
&lt;p&gt;The OpenAPI spec (&lt;code&gt;openapi.yml&lt;/code&gt;) defines both endpoints and adds a &lt;code&gt;Notification&lt;/code&gt; schema. The schema includes &lt;code&gt;id&lt;/code&gt; (UUID), &lt;code&gt;type&lt;/code&gt; (enum: processing, success, info, warning, error), &lt;code&gt;key&lt;/code&gt; (nullable string for deduplication), &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt; (Unix milliseconds), &lt;code&gt;hasRead&lt;/code&gt; (boolean), and &lt;code&gt;actions&lt;/code&gt; (nullable array of label/href objects).&lt;/p&gt;
&lt;p&gt;Tests validate response structure against the OpenAPI spec, per-user read state (one user marking a notification as read does not affect another user's read state), authentication requirements, 204 responses, read state persistence across requests, and request validation. The OpenAPI request validator rejects missing or invalid &lt;code&gt;ids&lt;/code&gt; fields before the controller runs.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 1, 2026 (13 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;justafish (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3580214: [Notifications] RTK Query API and Redux slice</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3580214.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94fd2718f0bf</guid>
      <pubDate>Wed, 01 Apr 2026 13:18:19 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/87b6bcb5511fddff1adb3d4844ab77de3886486b"&gt;87b6bcb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3580214"&gt;#3580214&lt;/a&gt; · 1 contributor · 6 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This adds the frontend data layer for the notification system. It includes an RTK Query API service with endpoints for fetching notifications and marking them as read, a Redux slice for UI state (page open timestamp and activity center visibility), utilities for sorting notifications and formatting timestamps, constants for polling intervals and auto-read notification types, and a document visibility hook. No UI components are built in this ticket.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No upgrade or configuration changes required.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The change creates the Redux and RTK Query infrastructure for a notification system that will be consumed by UI components in later tickets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RTK Query API&lt;/strong&gt; (&lt;code&gt;notificationsApi.ts&lt;/code&gt;) defines the &lt;code&gt;Notification&lt;/code&gt; type with fields for &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; ('processing' | 'success' | 'info' | 'warning' | 'error'), &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;hasRead&lt;/code&gt;, and optional &lt;code&gt;actions&lt;/code&gt;. Two endpoints are created: &lt;code&gt;getNotifications&lt;/code&gt; queries &lt;code&gt;/canvas/api/v0/notifications&lt;/code&gt;, and &lt;code&gt;markNotificationsRead&lt;/code&gt; posts to &lt;code&gt;/canvas/api/v0/notifications/read&lt;/code&gt; with an array of notification IDs. The mutation implements optimistic updates by dispatching &lt;code&gt;updateQueryData&lt;/code&gt; to immediately set &lt;code&gt;hasRead: true&lt;/code&gt; for the specified IDs in the cached query result, then undoing the patch if the server request fails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redux slice&lt;/strong&gt; (&lt;code&gt;notificationsSlice.ts&lt;/code&gt;) manages two pieces of state: &lt;code&gt;pageOpenedAt&lt;/code&gt; (initialized to &lt;code&gt;Date.now()&lt;/code&gt; when the slice is created) and &lt;code&gt;activityCenterOpen&lt;/code&gt; (boolean). It provides a &lt;code&gt;setActivityCenterOpen&lt;/code&gt; action and selectors for both state fields.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sorting utility&lt;/strong&gt; (&lt;code&gt;sortNotifications.ts&lt;/code&gt;) implements a four-tier priority system: processing notifications (priority 0), unread errors (priority 1), unread warnings (priority 2), and everything else (priority 3). Read errors and warnings drop to the lowest tier. Within each tier, notifications are sorted by timestamp descending (newest first).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Badge count utility&lt;/strong&gt; (&lt;code&gt;selectBadgeCount.ts&lt;/code&gt;) returns the count of all notifications where &lt;code&gt;hasRead&lt;/code&gt; is false, regardless of type.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timestamp utility&lt;/strong&gt; (&lt;code&gt;formatTimestamp.ts&lt;/code&gt;) computes the difference between &lt;code&gt;Date.now()&lt;/code&gt; and the notification timestamp and returns &amp;quot;Now&amp;quot; for less than 1 minute, &amp;quot;Xm ago&amp;quot; for less than 60 minutes, &amp;quot;Xh ago&amp;quot; for less than 24 hours, or &amp;quot;Xd ago&amp;quot; for 24 hours or more.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constants&lt;/strong&gt; (&lt;code&gt;constants.ts&lt;/code&gt;) defines &lt;code&gt;AUTO_READ_TYPES&lt;/code&gt; as a Set containing 'success' and 'info', which will be used by UI components to determine which notification types are automatically marked as read when displayed. Three polling interval constants are defined (2000ms, 10000ms, 300000ms) for future use by components that will determine polling frequency based on whether processing notifications are present and whether the browser tab is focused.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document visibility hook&lt;/strong&gt; (&lt;code&gt;useDocumentVisibility.ts&lt;/code&gt;) wraps &lt;code&gt;document.visibilityState&lt;/code&gt; with a &lt;code&gt;visibilitychange&lt;/code&gt; event listener and returns a boolean indicating whether the document is visible.&lt;/p&gt;
&lt;p&gt;Both the API and slice are registered in &lt;code&gt;store.ts&lt;/code&gt; by adding them to &lt;code&gt;combineSlices()&lt;/code&gt; and adding &lt;code&gt;notificationsApi.middleware&lt;/code&gt; to the middleware chain. Unit tests cover the sorting logic, badge count computation, timestamp formatting, and slice reducers.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 1, 2026 (13 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;justafish (Acquia)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>#3580212: [Notifications] cron cleanup</title>
      <link>https://github.com/dbuytaert/drupal-digests/blob/main/issues/drupal-canvas/3580212.md</link>
      <guid isPermaLink="false">019d857f-33e4-728b-bbba-94fcfa4167f6</guid>
      <category>Module maintainers</category>
      <pubDate>Wed, 01 Apr 2026 11:42:24 +0000</pubDate>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project:&lt;/strong&gt; Drupal Canvas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type:&lt;/strong&gt; Normal task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version:&lt;/strong&gt; 1.x-dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Fixed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diff:&lt;/strong&gt; &lt;a href="https://git.drupalcode.org/project/canvas/-/commit/838515287321cca88bfe46c3cfe4cfbc696b5d2c"&gt;8385152&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://www.drupal.org/node/3580212"&gt;#3580212&lt;/a&gt; · 1 contributor · 6 comments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Followers:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Canvas module now automatically cleans up notifications via cron. Processing notifications older than 30 minutes are deleted and replaced with error notifications titled &amp;quot;Operation timed out&amp;quot; so users know the operation did not complete. All notifications and read entries older than 30 days are deleted to prevent unbounded database growth. Both operations run automatically on every cron run.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Affects module maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New API:&lt;/strong&gt; Two new public methods on &lt;code&gt;CanvasNotificationHandler&lt;/code&gt;: &lt;code&gt;purgeStaleProcessing()&lt;/code&gt; and &lt;code&gt;deleteExpired()&lt;/code&gt; for manual cleanup triggers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The Canvas notification system stores notifications and read status in two database tables. Without cleanup, these tables would grow indefinitely. This change adds two cleanup operations that run on cron.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CanvasNotificationHandler&lt;/code&gt; gains two new public methods. &lt;code&gt;purgeStaleProcessing()&lt;/code&gt; queries for processing-type notifications with timestamps older than 30 minutes (using the &lt;code&gt;idx_type_timestamp&lt;/code&gt; index), creates an error notification for each with the same key and message but title &amp;quot;Operation timed out&amp;quot;, then deletes all stale processing notifications in a single query. Non-processing notifications are never affected by this operation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;deleteExpired()&lt;/code&gt; removes all notifications and read entries older than 30 days regardless of type. It uses a transaction to ensure both deletes succeed or both fail. Notifications are deleted first, then read entries, to prevent orphaned read entries. Both queries use the same pre-computed cutoff timestamp to ensure consistency.&lt;/p&gt;
&lt;p&gt;The timeout (30 minutes) and retention period (30 days) are stored as class constants &lt;code&gt;PROCESSING_TIMEOUT_MS&lt;/code&gt; and &lt;code&gt;RETENTION_MS&lt;/code&gt; on &lt;code&gt;CanvasNotificationHandler&lt;/code&gt;, not configuration values.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NotificationCronHook&lt;/code&gt; implements &lt;code&gt;hook_cron&lt;/code&gt; using the Hook attribute system. It injects &lt;code&gt;CanvasNotificationHandler&lt;/code&gt; and calls &lt;code&gt;purgeStaleProcessing()&lt;/code&gt; followed by &lt;code&gt;deleteExpired()&lt;/code&gt; on every cron run.&lt;/p&gt;
&lt;h2&gt;Contribution&lt;/h2&gt;
&lt;h3&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Opened: March 19, 2026&lt;/li&gt;
&lt;li&gt;Committed: April 1, 2026 (13 days later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key contributors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;justafish (Acquia)&lt;/li&gt;
&lt;li&gt;f.mazeikis&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This content is AI-generated and may contain errors. See &lt;a href="https://github.com/dbuytaert/drupal-digests/"&gt;Drupal Digests&lt;/a&gt; for more.&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
  </channel>
</rss>