From d2d77d2cb11bdc03ee435e7b2c76307a8f4f6e9a Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Fri, 14 Aug 2026 13:42:11 +0200 Subject: [PATCH 1/3] Support seeding files of arbitrary file types Resolve file types via their collection name from the registry instead of going through `Pageflow::BuiltInFileType`, and pick up any attribute ending in `_files`. Plugin defined file types like `lottie_files` can now be seeded without the seeds DSL knowing about them. --- .../scrolled/lib/pageflow_scrolled/seeds.rb | 92 +++++++++++-------- .../spec/pageflow_scrolled/seeds_spec.rb | 51 ++++++++++ 2 files changed, 107 insertions(+), 36 deletions(-) diff --git a/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb b/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb index 532d34bb5a..62673431d6 100644 --- a/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb +++ b/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb @@ -24,6 +24,12 @@ module Seeds # @option attributes [Hash] :video_files A hash mapping video # names used in properties like `backdrop.video` to urls. # @option attributes [Hash] :text_track_files A hash mapping text track files to urls. + # + # Any further attribute ending in `_files` is interpreted as the + # collection name of a registered file type. Passing `lottie_files`, + # for example, creates files of the file type registered with + # collection name `lottie_files`. + # # @yield [entry] a block to be called before the entry is saved # @param [Hash] options options for entry and files creation # @option options [Boolean] :skip_encoding @@ -36,12 +42,10 @@ def sample_scrolled_entry(attributes:, options: {}) .first if entry.nil? - entry = Pageflow::Entry.create!(type_name: 'scrolled', - **attributes.except(:chapters, - :image_files, - :video_files, - :audio_files, - :text_track_files)) do |created_entry| + entry = Pageflow::Entry.create!( + type_name: 'scrolled', + **attributes.except(:chapters, *file_collection_names(attributes)) + ) do |created_entry| created_entry.site = attributes.fetch(:account).default_site say_creating_scrolled_entry(created_entry) @@ -50,29 +54,8 @@ def sample_scrolled_entry(attributes:, options: {}) draft_entry = Pageflow::DraftEntry.new(entry) - image_files_by_name = create_files(draft_entry, - :image, - attributes.fetch(:image_files, {})) - - video_files_by_name = create_files(draft_entry, - :video, - attributes.fetch(:video_files, {}), - skip_encoding: options.fetch(:skip_encoding, false)) - - audio_files_by_name = create_files(draft_entry, - :audio, - attributes.fetch(:audio_files, {}), - skip_encoding: options.fetch(:skip_encoding, false)) - - files_by_name = image_files_by_name.merge(video_files_by_name).merge(audio_files_by_name) - - # rewrite parent file references to actual ids - text_tracks_by_name = attributes.fetch(:text_track_files, {}) - text_tracks_by_name.each_value do |text_track_config| - parent_file = files_by_name.fetch(text_track_config['parent_file_id']) - text_track_config['parent_file_id'] = parent_file.id - end - create_files(draft_entry, :text_track, text_tracks_by_name) + files_by_name = create_top_level_files(draft_entry, attributes, options) + create_text_track_files(draft_entry, attributes, files_by_name) attributes[:chapters].each_with_index do |chapter_config, i| create_chapter(entry, chapter_config, i, files_by_name) @@ -92,24 +75,53 @@ def say_creating_scrolled_entry(entry) say(" sample scrolled entry '#{entry.title}'\n") end - def create_files(draft_entry, file_type, file_data_by_name, skip_encoding: false) + def file_collection_names(attributes) + attributes.keys.select { |name| name.to_s.end_with?('_files') } + end + + def create_top_level_files(draft_entry, attributes, options) + collection_names = file_collection_names(attributes) - [:text_track_files] + + collection_names.reduce({}) do |files_by_name, collection_name| + files_by_name.merge( + create_files(draft_entry, + collection_name.to_s, + attributes.fetch(collection_name), + skip_encoding: options.fetch(:skip_encoding, false)) + ) + end + end + + def create_text_track_files(draft_entry, attributes, files_by_name) + text_tracks_by_name = attributes.fetch(:text_track_files, {}) + + text_tracks_by_name.each_value do |text_track_config| + parent_file = files_by_name.fetch(text_track_config['parent_file_id']) + text_track_config['parent_file_id'] = parent_file.id + end + + create_files(draft_entry, 'text_track_files', text_tracks_by_name) + end + + def create_files(draft_entry, collection_name, file_data_by_name, skip_encoding: false) + file_type = Pageflow.config.file_types.find_by_collection_name!(collection_name) + file_data_by_name.transform_values do |data| - say(" creating #{file_type} file from #{data['url']}") + say(" creating #{collection_name.delete_suffix('_files')} file from #{data['url']}") - file_state = %i[image text_track].include?(file_type) ? 'processed' : 'uploading' uri = URI.parse(data['url']) - file = draft_entry.create_file!(Pageflow::BuiltInFileType.send(file_type), - state: file_state, + file = draft_entry.create_file!(file_type, + state: initial_file_state(collection_name), attachment: uri, display_name: File.basename(uri.path, '*'), configuration: data['configuration'], parent_file_model_type: data['parent_file_model_type'], parent_file_id: data['parent_file_id'], **data.slice('width', 'height').symbolize_keys) - if %i[audio video].include?(file_type) + if %w[audio_files video_files].include?(collection_name) if skip_encoding file.update!(state: 'encoded') - if file_type.eql?(:video) + if collection_name == 'video_files' file.update!(output_presences: { 'dash-playlist' => true, 'hls-playlist' => true, @@ -133,6 +145,14 @@ def create_files(draft_entry, file_type, file_data_by_name, skip_encoding: false end end + def initial_file_state(collection_name) + case collection_name + when 'image_files', 'text_track_files' then 'processed' + when 'audio_files', 'video_files' then 'uploading' + else 'uploaded' + end + end + def create_chapter(entry, chapter_config, position, files_by_name) section_configs = chapter_config.delete('sections') || [] chapter = Chapter.create!( diff --git a/entry_types/scrolled/spec/pageflow_scrolled/seeds_spec.rb b/entry_types/scrolled/spec/pageflow_scrolled/seeds_spec.rb index 6451ac7ef7..42d28246bb 100644 --- a/entry_types/scrolled/spec/pageflow_scrolled/seeds_spec.rb +++ b/entry_types/scrolled/spec/pageflow_scrolled/seeds_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'pageflow/test_uploadable_file' module PageflowScrolled module SeedsDsl @@ -561,6 +562,56 @@ module SeedsDsl end end + context 'files of custom file types' do + before do + pageflow_configure do |config| + config.file_types.register( + Pageflow::FileType.new(model: 'Pageflow::TestUploadableFile', + collection_name: 'test_files') + ) + end + + stub_request(:get, /example.com/) + .to_return(status: 200, + body: File.read('spec/fixtures/image.jpg'), + headers: {'Content-Type' => 'image/jpg'}) + end + + it 'creates file for collection name of registered file type' do + entry = SeedsDsl.sample_scrolled_entry(attributes: { + account: create(:account), + title: 'Example', + test_files: { + 'some-file' => { + 'url' => 'https://example.com/some.jpg' + } + }, + chapters: [] + }) + + file = entry.draft.find_files(Pageflow::TestUploadableFile).first + + expect(file.display_name).to eq('some.jpg') + end + + it 'marks file as uploaded' do + entry = SeedsDsl.sample_scrolled_entry(attributes: { + account: create(:account), + title: 'Example', + test_files: { + 'some-file' => { + 'url' => 'https://example.com/some.jpg' + } + }, + chapters: [] + }) + + file = entry.draft.find_files(Pageflow::TestUploadableFile).first + + expect(file.state).to eq('uploaded') + end + end + it 'allows overriding attributes in block' do account = create(:account) site = create(:site, account:) From 4844c31c2df79fca552a571602bdf3fe7a69bd49 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Fri, 14 Aug 2026 13:43:11 +0200 Subject: [PATCH 2/3] Add example lottie file to storybook seed Enable the lottie animation feature for the storybook seed account. File types registered inside a feature only end up in the seed's url templates if the feature is enabled for the entry. --- .../tasks/pageflow_scrolled/storybook.rake | 13 +++++++++++ .../pageflow_scrolled/storybook_tasks_spec.rb | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/entry_types/scrolled/lib/tasks/pageflow_scrolled/storybook.rake b/entry_types/scrolled/lib/tasks/pageflow_scrolled/storybook.rake index 70a8f390de..d7a7877db9 100644 --- a/entry_types/scrolled/lib/tasks/pageflow_scrolled/storybook.rake +++ b/entry_types/scrolled/lib/tasks/pageflow_scrolled/storybook.rake @@ -41,6 +41,11 @@ namespace :pageflow_scrolled do attributes: { title: 'Storybook seed', account:, + # File types registered inside a feature are only included in + # the seed's model types and url templates if the feature is + # enabled. Enabled on the entry, which is recreated on each + # run, since the account is only seeded once. + features_configuration: {'lottie_animation_content_element' => true}, chapters: [], image_files: { turtle: { @@ -96,6 +101,14 @@ namespace :pageflow_scrolled do } }.stringify_keys }.stringify_keys, + lottie_files: { + animation: { + url: 'https://s3-eu-west-1.amazonaws.com/de.codevise.pageflow.development/pageflow-next/seed-assets/lottie_animations/animation.lottie', + configuration: { + testReferenceName: 'lottieAnimation' + } + }.stringify_keys + }, text_track_files: { sample: { url: 'https://s3-eu-west-1.amazonaws.com/de.codevise.pageflow.development/pageflow-next/seed-assets/text_tracks/sample.vtt', diff --git a/entry_types/scrolled/spec/tasks/pageflow_scrolled/storybook_tasks_spec.rb b/entry_types/scrolled/spec/tasks/pageflow_scrolled/storybook_tasks_spec.rb index 56373ec70f..fc583b3e73 100644 --- a/entry_types/scrolled/spec/tasks/pageflow_scrolled/storybook_tasks_spec.rb +++ b/entry_types/scrolled/spec/tasks/pageflow_scrolled/storybook_tasks_spec.rb @@ -65,6 +65,29 @@ expect(seed_json).to include('"testReferenceName":"turtle"') end + it 'writes seed containing url templates for feature specific file types' do + rake 'pageflow_scrolled:storybook:seed:create_entry' + rake 'pageflow_scrolled:storybook:seed:generate_json', test_output_dir + + seed_json = File.read(File.join(test_output_dir, 'seed.json')) + + expect(seed_json).to include('"testReferenceName":"lottieAnimation"') + expect(seed_json).to include_json(config: {fileUrlTemplates: {lottieFiles: {}}}) + end + + it 'writes seed containing feature specific file types if account already exists' do + create(:account, name: 'storybook-seed') + + rake 'pageflow_scrolled:storybook:seed:create_entry' + rake 'pageflow_scrolled:storybook:seed:generate_json', test_output_dir + + seed_json = File.read(File.join(test_output_dir, 'seed.json')) + + expect(seed_json).to include_json( + config: {fileModelTypes: {lottieFiles: 'PageflowScrolled::LottieFile'}} + ) + end + it 'sets locale to entry locale' do rake 'pageflow_scrolled:storybook:seed:create_entry' Pageflow::Entry.where(title: 'Storybook seed').first.draft.update(locale: 'fr') From 4af78819aa6d90ead7797511af591c0390a3071c Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Fri, 14 Aug 2026 13:50:50 +0200 Subject: [PATCH 3/3] Add storybook stories for lottie animation element Since the element ships as a separate frontend pack, the stories import its own `frontend` module instead of the aggregate content element registry. Storybook's webpack config needs the same rule for the dotLottie WebAssembly module that host applications get via `config/webpack.js`. Lottie files are also included in the collections that inline file rights stories apply rights to. --- .../scrolled/package/.storybook/main.js | 12 ++++++ .../spec/support/__spec__/stories-spec.js | 27 +++++++++++++ .../scrolled/package/spec/support/stories.js | 11 ++++-- .../lottieAnimation/stories.js | 39 +++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 entry_types/scrolled/package/src/contentElements/lottieAnimation/stories.js diff --git a/entry_types/scrolled/package/.storybook/main.js b/entry_types/scrolled/package/.storybook/main.js index 46e1178c3c..53149a0f90 100644 --- a/entry_types/scrolled/package/.storybook/main.js +++ b/entry_types/scrolled/package/.storybook/main.js @@ -29,6 +29,7 @@ module.exports = { ) ), reactSvgLoader(), + wasmLoader() ] }, resolve: { @@ -63,6 +64,17 @@ function reactSvgLoader() { }; } +function wasmLoader() { + // Emit the WebAssembly module of the dotLottie player as asset, so + // that it does not have to be loaded from a CDN. See + // `config/webpack.js` for the equivalent rule used by host + // applications. + return { + test: /\.wasm$/, + type: 'asset/resource' + }; +} + function removeSvgFromFileLoader(rules) { return rules.map(rule => { if (!rule.test || !rule.test.toString().includes('svg|')) { diff --git a/entry_types/scrolled/package/spec/support/__spec__/stories-spec.js b/entry_types/scrolled/package/spec/support/__spec__/stories-spec.js index 2b9453f432..ca803b9834 100644 --- a/entry_types/scrolled/package/spec/support/__spec__/stories-spec.js +++ b/entry_types/scrolled/package/spec/support/__spec__/stories-spec.js @@ -191,6 +191,33 @@ describe('exampleStories', () => { })); }); + it('applies inline file rights to lottie files', () => { + stubSeedFixture(normalizeSeed({ + lottieFiles: [ + {id: 10, permaId: 1} + ] + })); + + const stories = exampleStories({ + typeName: 'test', + inlineFileRights: true, + baseConfiguration: {} + }); + + expect(stories).toContainEqual(expect.objectContaining({ + title: 'Inline File Rights - Icon', + seed: expect.objectContaining({ + collections: expect.objectContaining({ + lottieFiles: expect.arrayContaining([ + expect.objectContaining({ + configuration: expect.objectContaining({rights_display: 'inline'}) + }) + ]) + }) + }) + })); + }); + it('supports adding story for inline file rights', () => { stubSeedFixture(normalizeSeed({ imageFiles: [ diff --git a/entry_types/scrolled/package/spec/support/stories.js b/entry_types/scrolled/package/spec/support/stories.js index fba97bf33e..14ad924079 100644 --- a/entry_types/scrolled/package/spec/support/stories.js +++ b/entry_types/scrolled/package/spec/support/stories.js @@ -136,6 +136,7 @@ function renderCss(rules) { * * "turtle" (image) * * "interview_toni" (video) * * "quicktime_jingle" (audio) + * * "lottieAnimation" (lottie) * * @param {string} collectionName - A name of a files collection like `"imageFiles"`. * @param {string} testReferenceName - Name of a predefined file from the seed JSON file. @@ -170,6 +171,10 @@ export function filePermaId(collectionName, testReferenceName) { return file.permaId; } +const inlineFileRightsCollectionNames = [ + 'audioFiles', 'imageFiles', 'lottieFiles', 'videoFiles' +]; + export function exampleStories(options) { return [ ...variantsExampleStories(options), @@ -198,7 +203,7 @@ function variantsExampleStories({typeName, baseConfiguration, variants}) { themeOptions, sectionConfiguration, viewport, - inlineFileRightsFor: inlineFileRightsWidgetTypeName ? ['audioFiles', 'imageFiles', 'videoFiles'] : [], + inlineFileRightsFor: inlineFileRightsWidgetTypeName ? inlineFileRightsCollectionNames : [], widgets: inlineFileRightsWidgetTypeName ? [{ role: 'inlineFileRights', typeName: inlineFileRightsWidgetTypeName @@ -290,7 +295,7 @@ function inlineFileRightsStories({typeName, inlineFileRights, baseConfiguration} return exampleStoryGroup({ typeName, name: 'Inline File Rights', - inlineFileRightsFor: ['audioFiles', 'imageFiles', 'videoFiles'], + inlineFileRightsFor: inlineFileRightsCollectionNames, examples: [ ['Icon', 'iconInlineFileRights'], ['Text', 'textInlineFileRights'] ].map(([name, typeName]) => ( @@ -426,7 +431,7 @@ export function normalizeAndMergeFixture({inlineFileRightsFor = [], ...options} }; } -function applyInlineFileRights(files) { +function applyInlineFileRights(files = []) { return files.map(file => ({ ...file, rights: 'Jane Doe', diff --git a/entry_types/scrolled/package/src/contentElements/lottieAnimation/stories.js b/entry_types/scrolled/package/src/contentElements/lottieAnimation/stories.js new file mode 100644 index 0000000000..3b81acf90d --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/lottieAnimation/stories.js @@ -0,0 +1,39 @@ +import './frontend'; +import {storiesOfContentElement, filePermaId} from 'pageflow-scrolled/spec/support/stories'; + +storiesOfContentElement(module, { + typeName: 'lottieAnimation', + baseConfiguration: { + id: filePermaId('lottieFiles', 'lottieAnimation') + }, + variants: [ + { + name: 'With Caption', + configuration: {caption: 'Some text here'} + }, + { + name: 'With Rounded Corners', + configuration: { + imageModifiers: [ + {name: 'rounded', value: 'md'} + ] + }, + themeOptions: { + properties: { + root: { + 'contentElementBoxBorderRadius-md': '16px' + } + } + } + }, + { + name: 'With Circle Crop', + configuration: { + imageModifiers: [ + {name: 'crop', value: 'circle'} + ] + } + } + ], + inlineFileRights: true +});