From 0c8a9ba1e96f37125219fd794c4317754a2dacf9 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Wed, 15 Jul 2026 13:11:31 +0200 Subject: [PATCH] Fix editor crash for widgets disabled in editor Since packs are only loaded for editor-enabled widgets, the editor preview must not try to render widgets that are disabled in the editor. Otherwise their widget type is not registered and rendering fails, breaking the editor for entries using such a widget. Exclude editor-disabled widgets from the preview so they no longer run inside the editor, matching the intent of disabling them there. REDMINE-21330 --- app/helpers/pageflow/widgets_helper.rb | 3 ++- .../package/spec/entryState/widgets-spec.js | 24 +++++++++++++++++ .../src/entryState/watchCollections.js | 5 +++- .../collections/widgetsCollection-spec.js | 27 +++++++++++++++---- package/src/editor/api/WidgetType.js | 1 + .../editor/collections/WidgetsCollection.js | 11 +++++--- spec/helpers/pageflow/widgets_helper_spec.rb | 14 ++++++++++ 7 files changed, 74 insertions(+), 11 deletions(-) diff --git a/app/helpers/pageflow/widgets_helper.rb b/app/helpers/pageflow/widgets_helper.rb index 30f8c73e83..e4a97114b0 100644 --- a/app/helpers/pageflow/widgets_helper.rb +++ b/app/helpers/pageflow/widgets_helper.rb @@ -33,7 +33,8 @@ def widget_types_json_seeds(config) result[role] << { name: widget_type.name, translationKey: widget_type.translation_key, - insertPoint: widget_type.insert_point + insertPoint: widget_type.insert_point, + enabledInEditor: widget_type.enabled_in_editor? } end }.to_json.html_safe diff --git a/entry_types/scrolled/package/spec/entryState/widgets-spec.js b/entry_types/scrolled/package/spec/entryState/widgets-spec.js index 4b3ca416eb..99c65a67bd 100644 --- a/entry_types/scrolled/package/spec/entryState/widgets-spec.js +++ b/entry_types/scrolled/package/spec/entryState/widgets-spec.js @@ -59,6 +59,30 @@ describe('useWidget', () => { expect(widget).toBeUndefined(); }); + it('filters out react widgets disabled in editor', () => { + const {result} = renderHookInEntry(() => useWidget({role: 'consent'}), { + setup: dispatch => + watchCollections( + factories.entry(ScrolledEntry, {}, { + widgetTypes: factories.widgetTypes([{ + role: 'consent', + name: 'consent_dialog', + insertPoint: 'react', + enabledInEditor: false + }]), + widgetsAttributes: [{ + type_name: 'consent_dialog', + role: 'consent' + }], + entryTypeSeed: normalizeSeed() + }), + {dispatch} + ) + }); + + expect(result.current).toBeUndefined(); + }); + it('reads data from seed', () => { const {result} = renderHookInEntry( () => useWidget({role: 'navigation'}), diff --git a/entry_types/scrolled/package/src/entryState/watchCollections.js b/entry_types/scrolled/package/src/entryState/watchCollections.js index 1b1979e504..328753b939 100644 --- a/entry_types/scrolled/package/src/entryState/watchCollections.js +++ b/entry_types/scrolled/package/src/entryState/watchCollections.js @@ -47,7 +47,10 @@ export function watchCollections(entry, {dispatch}) { dispatch })); - teardownFns.push(watchCollection(widgets.withInsertPoint('react'), { + // Only sync widgets whose frontend packs the server loads in the editor. + // Editor-disabled widgets have no registered widget type here, so rendering + // them in the preview would fail. + teardownFns.push(watchCollection(widgets.withWidgetType({insertPoint: 'react', enabledInEditor: true}), { name: 'widgets', attributes: [{typeName: 'type_name'}, 'role', {permaId: 'role'}], keyAttribute: 'permaId', diff --git a/package/spec/editor/collections/widgetsCollection-spec.js b/package/spec/editor/collections/widgetsCollection-spec.js index 416f316e8e..8a65f96016 100644 --- a/package/spec/editor/collections/widgetsCollection-spec.js +++ b/package/spec/editor/collections/widgetsCollection-spec.js @@ -3,7 +3,7 @@ import {CheckBoxInputView, ConfigurationEditorTabView} from 'pageflow/ui'; import {factories} from '$support'; describe('WidgetsCollection', () => { - it('supports gettting subset collection for insert point', () => { + it('supports getting subset collection by widget type property', () => { const widgetTypes = factories.widgetTypes([ {role: 'navigation', name: 'some_navigation_bar', insertPoint: 'react'}, {role: 'consent', name: 'some_consent_provider', insertPoint: 'bottom_of_entry'} @@ -14,11 +14,28 @@ describe('WidgetsCollection', () => { ], {widgetTypes}); expect( - widgets.withInsertPoint('react').pluck('type_name') + widgets.withWidgetType({insertPoint: 'react'}).pluck('type_name') ).toEqual(['some_navigation_bar']); }); - it('keeps insert point subset collection up to date when type name changes', () => { + it('supports filtering by multiple widget type properties', () => { + const widgetTypes = factories.widgetTypes([ + {role: 'consent', name: 'consent_bar', insertPoint: 'react'}, + {role: 'consent', name: 'consent_dialog', insertPoint: 'react', + enabledInEditor: false} + ]); + const widgets = new WidgetsCollection([ + {type_name: 'consent_bar'}, + {type_name: 'consent_dialog'}, + ], {widgetTypes}); + + expect( + widgets.withWidgetType({insertPoint: 'react', enabledInEditor: true}) + .pluck('type_name') + ).toEqual(['consent_bar']); + }); + + it('keeps subset collection up to date when type name changes', () => { const widgetTypes = factories.widgetTypes([ {role: 'consent', name: 'consent_bar', insertPoint: 'react'}, {role: 'consent', name: 'some_consent_provider', insertPoint: 'bottom_of_entry'} @@ -28,7 +45,7 @@ describe('WidgetsCollection', () => { ], {widgetTypes}); widgets.subject = factories.entry(); - const subsetCollection = widgets.withInsertPoint('react'); + const subsetCollection = widgets.withWidgetType({insertPoint: 'react'}); widgets.first().set('type_name', 'consent_bar'); expect(subsetCollection.pluck('type_name')).toEqual(['consent_bar']); @@ -44,7 +61,7 @@ describe('WidgetsCollection', () => { ], {widgetTypes}); widgets.subject = factories.entry(); - const subsetCollection = widgets.withInsertPoint('react'); + const subsetCollection = widgets.withWidgetType({insertPoint: 'react'}); expect(subsetCollection.pluck('type_name')).toEqual(['consent_bar']); }); diff --git a/package/src/editor/api/WidgetType.js b/package/src/editor/api/WidgetType.js index ed6cc6d439..4410567691 100644 --- a/package/src/editor/api/WidgetType.js +++ b/package/src/editor/api/WidgetType.js @@ -6,6 +6,7 @@ export const WidgetType = Object.extend({ this.name = serverSideConfig.name; this.translationKey = serverSideConfig.translationKey; this.insertPoint = serverSideConfig.insertPoint; + this.enabledInEditor = serverSideConfig.enabledInEditor !== false; this.configurationEditorView = clientSideConfig.configurationEditorView; this.configurationEditorTabViewGroups = clientSideConfig.configurationEditorTabViewGroups || {}; this.isOptional = clientSideConfig.isOptional; diff --git a/package/src/editor/collections/WidgetsCollection.js b/package/src/editor/collections/WidgetsCollection.js index c55db289a4..05ce70d493 100644 --- a/package/src/editor/collections/WidgetsCollection.js +++ b/package/src/editor/collections/WidgetsCollection.js @@ -49,13 +49,16 @@ export const WidgetsCollection = Backbone.Collection.extend({ this.each(widget => widget.defineConfigurationEditorTabViewGroups(groups)); }, - withInsertPoint(insertPoint) { + withWidgetType(properties) { return new SubsetCollection({ parent: this, watchAttribute: 'type_name', - filter: widget => ( - widget.widgetType() && widget.widgetType().insertPoint === insertPoint - ) + filter: widget => { + const widgetType = widget.widgetType(); + + return !!widgetType && + Object.keys(properties).every(name => widgetType[name] === properties[name]); + } }); } }); diff --git a/spec/helpers/pageflow/widgets_helper_spec.rb b/spec/helpers/pageflow/widgets_helper_spec.rb index 71334ba8b2..0a8dc64438 100644 --- a/spec/helpers/pageflow/widgets_helper_spec.rb +++ b/spec/helpers/pageflow/widgets_helper_spec.rb @@ -229,6 +229,20 @@ module Pageflow .to eq('pageflow.fancy_bar.widget_type_name') expect(result['navigation'][0]['insertPoint']).to eq('react') end + + it 'renders enabledInEditor by role' do + widget_type = TestWidgetType.new(name: 'fancy_bar', + roles: ['navigation'], + enabled_in_editor: false) + pageflow_configure do |config| + config.widget_types.clear + config.widget_types.register(widget_type) + end + + result = JSON.parse(helper.widget_types_json_seeds(Pageflow.config)) + + expect(result['navigation'][0]['enabledInEditor']).to eq(false) + end end describe '#widgets_json_seeds' do