pageflow 17.1.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +319 -345
  3. data/README.md +3 -4
  4. data/admins/pageflow/accounts.rb +6 -6
  5. data/admins/pageflow/entry.rb +10 -8
  6. data/admins/pageflow/membership.rb +11 -11
  7. data/admins/pageflow/translations.rb +5 -5
  8. data/app/assets/images/pageflow/admin/icons/comment.svg +1 -0
  9. data/app/assets/javascripts/pageflow/dist/ui.js +1350 -630
  10. data/app/assets/javascripts/pageflow/editor/vendor.js +0 -1
  11. data/app/assets/javascripts/pageflow/ui.js +0 -1
  12. data/app/assets/stylesheets/pageflow/admin/entry_comments_indicator.scss +45 -0
  13. data/app/assets/stylesheets/pageflow/admin/permalink_input.scss +8 -1
  14. data/app/assets/stylesheets/pageflow/admin.scss +1 -0
  15. data/app/assets/stylesheets/pageflow/animations/spin.scss +9 -0
  16. data/app/assets/stylesheets/pageflow/animations.scss +1 -0
  17. data/app/assets/stylesheets/pageflow/editor/background_positioning.scss +21 -5
  18. data/app/assets/stylesheets/pageflow/editor/base.scss +9 -1
  19. data/app/assets/stylesheets/pageflow/editor/drop_down_button.scss +13 -1
  20. data/app/assets/stylesheets/pageflow/editor/edit_configuration_view.scss +5 -0
  21. data/app/assets/stylesheets/pageflow/editor/file_folders.scss +127 -0
  22. data/app/assets/stylesheets/pageflow/editor/file_meta_data.scss +0 -1
  23. data/app/assets/stylesheets/pageflow/editor/file_meta_data_overlay.scss +45 -0
  24. data/app/assets/stylesheets/pageflow/editor/file_preview.scss +138 -0
  25. data/app/assets/stylesheets/pageflow/editor/file_references.scss +68 -0
  26. data/app/assets/stylesheets/pageflow/editor/file_settings_dialog.scss +1 -1
  27. data/app/assets/stylesheets/pageflow/editor/file_stage_icons.scss +17 -0
  28. data/app/assets/stylesheets/pageflow/editor/file_stages.scss +25 -32
  29. data/app/assets/stylesheets/pageflow/editor/file_thumbnails.scss +31 -48
  30. data/app/assets/stylesheets/pageflow/editor/file_type_pills.scss +73 -0
  31. data/app/assets/stylesheets/pageflow/editor/files.scss +230 -50
  32. data/app/assets/stylesheets/pageflow/editor/filtered_files.scss +142 -14
  33. data/app/assets/stylesheets/pageflow/editor/folder_breadcrumb.scss +40 -0
  34. data/app/assets/stylesheets/pageflow/editor/info_box.scss +12 -0
  35. data/app/assets/stylesheets/pageflow/editor/inputs/edit_defaults_button.scss +15 -0
  36. data/app/assets/stylesheets/pageflow/editor/inputs.scss +1 -0
  37. data/app/assets/stylesheets/pageflow/editor/list.scss +88 -38
  38. data/app/assets/stylesheets/pageflow/editor/manage_files.scss +9 -0
  39. data/app/assets/stylesheets/pageflow/editor/menu.scss +14 -0
  40. data/app/assets/stylesheets/pageflow/editor/move_to_folder_dialog.scss +83 -0
  41. data/app/assets/stylesheets/pageflow/mixins/pageflow.scss +9 -5
  42. data/app/assets/stylesheets/pageflow/ui/color_picker.scss +230 -0
  43. data/app/assets/stylesheets/pageflow/ui/forms.scss +34 -4
  44. data/app/assets/stylesheets/pageflow/ui/functions.scss +12 -2
  45. data/app/assets/stylesheets/pageflow/ui/input/color_input.scss +9 -16
  46. data/app/assets/stylesheets/pageflow/ui/input/text_area_input.scss +15 -0
  47. data/app/assets/stylesheets/pageflow/ui/overrides.scss +8 -0
  48. data/app/assets/stylesheets/pageflow/ui/properties.scss +29 -0
  49. data/app/assets/stylesheets/pageflow/ui/tabs_view.scss +8 -3
  50. data/app/assets/stylesheets/pageflow/ui.scss +4 -2
  51. data/app/controllers/pageflow/editor/entries_controller.rb +1 -0
  52. data/app/controllers/pageflow/editor/file_folders_controller.rb +80 -0
  53. data/app/controllers/pageflow/editor/file_import_controller.rb +1 -0
  54. data/app/controllers/pageflow/editor/files_controller.rb +15 -6
  55. data/app/controllers/pageflow/editor/oembeds_controller.rb +21 -0
  56. data/app/controllers/pageflow/entries_controller.rb +17 -13
  57. data/app/controllers/pageflow/review/comment_thread_reads_controller.rb +30 -0
  58. data/app/controllers/pageflow/review/comment_threads_controller.rb +60 -0
  59. data/app/controllers/pageflow/review/comments_controller.rb +53 -0
  60. data/app/controllers/pageflow/revisions_controller.rb +4 -1
  61. data/app/helpers/pageflow/admin/entries_helper.rb +40 -0
  62. data/app/helpers/pageflow/admin/widgets_helper.rb +2 -2
  63. data/app/helpers/pageflow/common_entry_seed_helper.rb +5 -3
  64. data/app/helpers/pageflow/editor/config_helper.rb +3 -1
  65. data/app/helpers/pageflow/entries_helper.rb +24 -42
  66. data/app/helpers/pageflow/entry_json_seed_helper.rb +4 -4
  67. data/app/helpers/pageflow/public_i18n_helper.rb +9 -8
  68. data/app/helpers/pageflow/structured_data_helper.rb +4 -1
  69. data/app/helpers/pageflow/themes_helper.rb +1 -3
  70. data/app/helpers/pageflow/widgets_helper.rb +2 -1
  71. data/app/inputs/pageflow_permalink_input.rb +6 -2
  72. data/app/models/concerns/pageflow/auto_generated_perma_id.rb +11 -4
  73. data/app/models/concerns/pageflow/reusable_file.rb +1 -1
  74. data/app/models/concerns/pageflow/serialization_blacklist.rb +2 -4
  75. data/app/models/concerns/pageflow/translated_attributes.rb +101 -0
  76. data/app/models/concerns/pageflow/uploadable_file.rb +1 -1
  77. data/app/models/pageflow/comment.rb +30 -0
  78. data/app/models/pageflow/comment_thread.rb +65 -0
  79. data/app/models/pageflow/comment_thread_read.rb +22 -0
  80. data/app/models/pageflow/customized_theme.rb +3 -2
  81. data/app/models/pageflow/draft_entry.rb +20 -8
  82. data/app/models/pageflow/entry.rb +2 -0
  83. data/app/models/pageflow/entry_at_revision.rb +9 -1
  84. data/app/models/pageflow/entry_comment_summary.rb +114 -0
  85. data/app/models/pageflow/file_folder.rb +74 -0
  86. data/app/models/pageflow/file_reuse.rb +3 -2
  87. data/app/models/pageflow/file_usage.rb +8 -0
  88. data/app/models/pageflow/image_file.rb +24 -20
  89. data/app/models/pageflow/image_file_url_templates.rb +3 -3
  90. data/app/models/pageflow/legal_info.rb +38 -0
  91. data/app/models/pageflow/nested_revision_component_copy.rb +1 -1
  92. data/app/models/pageflow/permalink.rb +1 -0
  93. data/app/models/pageflow/revision.rb +6 -2
  94. data/app/models/pageflow/site.rb +10 -0
  95. data/app/models/pageflow/used_file.rb +8 -2
  96. data/app/models/pageflow/widget.rb +2 -2
  97. data/app/views/pageflow/audio_files/_audio_file.json.jbuilder +0 -1
  98. data/app/views/pageflow/editor/config/_seeds.json.jbuilder +4 -2
  99. data/app/views/pageflow/editor/entries/_entry.json.jbuilder +2 -1
  100. data/app/views/pageflow/editor/entries/seed.json.erb +1 -0
  101. data/app/views/pageflow/editor/file_folders/_file_folder.json.jbuilder +5 -0
  102. data/app/views/pageflow/editor/file_folders/create.json.jbuilder +1 -0
  103. data/app/views/pageflow/editor/file_folders/index.json.jbuilder +3 -0
  104. data/app/views/pageflow/editor/files/_file.json.jbuilder +2 -1
  105. data/app/views/pageflow/editor/image_files/_image_file.json.jbuilder +3 -0
  106. data/app/views/pageflow/editor/video_files/_video_file.json.jbuilder +3 -0
  107. data/app/views/pageflow/files/_file.json.jbuilder +2 -0
  108. data/app/views/pageflow/image_files/_image_file.json.jbuilder +0 -1
  109. data/app/views/pageflow/review/comment_threads/_comment_thread.json.jbuilder +20 -0
  110. data/app/views/pageflow/review/comment_threads/create.json.jbuilder +2 -0
  111. data/app/views/pageflow/review/comment_threads/index.json.jbuilder +13 -0
  112. data/app/views/pageflow/review/comments/_comment.json.jbuilder +13 -0
  113. data/app/views/pageflow/review/comments/create.json.jbuilder +1 -0
  114. data/app/views/pageflow/structured_data/_entry.json.jbuilder +2 -3
  115. data/app/views/pageflow/video_files/_video_file.json.jbuilder +0 -1
  116. data/config/locales/de.yml +152 -14
  117. data/config/locales/en.yml +150 -14
  118. data/config/routes.rb +14 -0
  119. data/db/migrate/20140418225525_setup_schema.rb +2 -2
  120. data/db/migrate/20251015000000_add_structured_data_type_name_to_revisions.rb +5 -0
  121. data/db/migrate/20251029142849_convert_legacy_social_embeds_to_unified_social_embed.rb +38 -0
  122. data/db/migrate/20251119083347_fix_main_storyline_positions.rb +16 -0
  123. data/db/migrate/20251119083348_ensure_scrolled_entries_have_excursion_storyline.rb +15 -0
  124. data/db/migrate/20260303000000_add_layout_version_to_revisions.rb +6 -0
  125. data/db/migrate/20260323000000_create_comment_threads_and_comments.rb +30 -0
  126. data/db/migrate/20260414000000_add_subject_range_to_comment_threads.rb +5 -0
  127. data/db/migrate/20260415000000_add_section_perma_id_to_comment_threads.rb +5 -0
  128. data/db/migrate/20260727000000_add_quote_to_comments.rb +5 -0
  129. data/db/migrate/20260728000000_create_file_folders.rb +17 -0
  130. data/db/migrate/20260805000000_add_attribute_translations_to_sites.rb +5 -0
  131. data/db/migrate/20260806000000_add_edited_at_to_comments.rb +5 -0
  132. data/db/migrate/20260810000000_create_pageflow_scrolled_lottie_files.rb +24 -0
  133. data/db/migrate/20260817000000_create_comment_thread_reads.rb +16 -0
  134. data/db/migrate/20260819000000_add_unread_comments_since_at_to_users.rb +18 -0
  135. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/editor.js +8665 -3890
  136. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/frontend.js +979 -3571
  137. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/react-client.js +20199 -28120
  138. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/react-server.js +18652 -26584
  139. data/entry_types/scrolled/app/assets/stylesheets/pageflow_scrolled/ui.scss +2 -0
  140. data/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/content_elements_controller.rb +33 -3
  141. data/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/sections_controller.rb +2 -3
  142. data/entry_types/scrolled/app/controllers/pageflow_scrolled/entries_controller.rb +21 -1
  143. data/entry_types/scrolled/app/helpers/pageflow_scrolled/cache_helper.rb +3 -3
  144. data/entry_types/scrolled/app/helpers/pageflow_scrolled/editor/entry_json_seed_helper.rb +7 -5
  145. data/entry_types/scrolled/app/helpers/pageflow_scrolled/editor/seed_html_helper.rb +5 -2
  146. data/entry_types/scrolled/app/helpers/pageflow_scrolled/entry_json_seed_helper.rb +13 -5
  147. data/entry_types/scrolled/app/helpers/pageflow_scrolled/i18n_helper.rb +28 -13
  148. data/entry_types/scrolled/app/helpers/pageflow_scrolled/packs_helper.rb +26 -11
  149. data/entry_types/scrolled/app/helpers/pageflow_scrolled/react_server_side_rendering_helper.rb +2 -2
  150. data/entry_types/scrolled/app/helpers/pageflow_scrolled/sprockets_helper.rb +16 -0
  151. data/entry_types/scrolled/app/helpers/pageflow_scrolled/themes_helper.rb +174 -3
  152. data/entry_types/scrolled/app/models/pageflow_scrolled/content_element.rb +49 -7
  153. data/entry_types/scrolled/app/models/pageflow_scrolled/lottie_file.rb +6 -0
  154. data/entry_types/scrolled/app/models/pageflow_scrolled/lottie_file_url_templates.rb +16 -0
  155. data/entry_types/scrolled/app/models/pageflow_scrolled/storyline.rb +1 -0
  156. data/entry_types/scrolled/app/views/pageflow_scrolled/entries/show.html.erb +13 -12
  157. data/entry_types/scrolled/app/views/pageflow_scrolled/entry_json_seed/_entry.json.jbuilder +30 -10
  158. data/entry_types/scrolled/app/views/pageflow_scrolled/entry_json_seed/_theme.json.jbuilder +9 -0
  159. data/entry_types/scrolled/app/views/pageflow_scrolled/favicons/_entry.html.erb +1 -1
  160. data/entry_types/scrolled/config/configuration_schemas/pageflow-scrolled.json +414 -0
  161. data/entry_types/scrolled/config/locales/de.yml +512 -65
  162. data/entry_types/scrolled/config/locales/en.yml +518 -52
  163. data/entry_types/scrolled/lib/pageflow_scrolled/additional_seed_data.rb +2 -2
  164. data/entry_types/scrolled/lib/pageflow_scrolled/configuration.rb +51 -6
  165. data/entry_types/scrolled/lib/pageflow_scrolled/configuration_schemas.rb +91 -0
  166. data/entry_types/scrolled/lib/pageflow_scrolled/engine.rb +2 -1
  167. data/entry_types/scrolled/lib/pageflow_scrolled/entry_file_reference_locations.rb +71 -0
  168. data/entry_types/scrolled/lib/pageflow_scrolled/entry_structured_data_types/faq_page.rb +58 -0
  169. data/entry_types/scrolled/lib/pageflow_scrolled/file_reference_locations.rb +69 -0
  170. data/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb +205 -22
  171. data/entry_types/scrolled/lib/pageflow_scrolled/react_widget_type.rb +2 -4
  172. data/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb +56 -36
  173. data/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb +35 -0
  174. data/entry_types/scrolled/lib/pageflow_scrolled.rb +13 -0
  175. data/entry_types/scrolled/lib/tasks/pageflow_scrolled/storybook.rake +19 -1
  176. data/entry_types/scrolled/package/config/configurationSchemas.js +70 -0
  177. data/entry_types/scrolled/package/config/rollup/configurationSchemas.js +19 -0
  178. data/entry_types/scrolled/package/config/webpack.js +72 -0
  179. data/entry_types/scrolled/package/contentElements/hotspots-frontend.css +1 -1
  180. data/entry_types/scrolled/package/contentElements/hotspots-frontend.js +1181 -915
  181. data/entry_types/scrolled/package/contentElements/inlineBeforeAfter-frontend.css +1 -0
  182. data/entry_types/scrolled/package/contentElements/inlineBeforeAfter-frontend.js +219 -0
  183. data/entry_types/scrolled/package/contentElements/lottieAnimation-frontend.css +1 -0
  184. data/entry_types/scrolled/package/contentElements/lottieAnimation-frontend.js +244 -0
  185. data/entry_types/scrolled/package/contentElements/socialEmbed-frontend.css +1 -0
  186. data/entry_types/scrolled/package/contentElements/socialEmbed-frontend.js +823 -0
  187. data/entry_types/scrolled/package/contentElements/tikTokEmbed-frontend.js +71 -21
  188. data/entry_types/scrolled/package/contentElements/twitterEmbed-frontend.css +1 -0
  189. data/entry_types/scrolled/package/contentElements/twitterEmbed-frontend.js +277 -0
  190. data/entry_types/scrolled/package/contentElements/videoEmbed-frontend.css +1 -0
  191. data/entry_types/scrolled/package/contentElements/videoEmbed-frontend.js +244 -0
  192. data/entry_types/scrolled/package/contentElements-editor.js +1915 -1097
  193. data/entry_types/scrolled/package/contentElements-frontend.css +1 -1
  194. data/entry_types/scrolled/package/contentElements-frontend.js +3093 -1679
  195. data/entry_types/scrolled/package/editor.css +1 -0
  196. data/entry_types/scrolled/package/editor.js +8667 -4077
  197. data/entry_types/scrolled/package/entryState.js +1600 -0
  198. data/entry_types/scrolled/package/frontend/FloatingPortalRootProvider-20c600de.js +1979 -0
  199. data/entry_types/scrolled/package/frontend/FloatingPortalRootProvider-58189220.js +2538 -0
  200. data/entry_types/scrolled/package/frontend/{PhonePlatformContext-9f76033e.js → PhonePlatformContext-035a99fa.js} +7 -12
  201. data/entry_types/scrolled/package/frontend/PhonePlatformContext-9da43797.js +33 -0
  202. data/entry_types/scrolled/package/frontend/Placeholder.module-bf0966b4.js +5267 -0
  203. data/entry_types/scrolled/package/frontend/Placeholder.module-d22a9335.js +4180 -0
  204. data/entry_types/scrolled/package/frontend/ThemeIcon-ab834849.js +754 -0
  205. data/entry_types/scrolled/package/frontend/ThemeIcon-f6cb3a8f.js +773 -0
  206. data/entry_types/scrolled/package/frontend/{ToggleFullscreenCornerButton-788e9cdb.js → ToggleFullscreenCornerButton-30fd1124.js} +6 -6
  207. data/entry_types/scrolled/package/frontend/ToggleFullscreenCornerButton-61c25224.js +72 -0
  208. data/entry_types/scrolled/package/frontend/Viewer-1074fd59.js +147 -0
  209. data/entry_types/scrolled/package/frontend/Viewer-1482ca34.js +480 -0
  210. data/entry_types/scrolled/package/frontend/Viewer-70504231.js +210 -0
  211. data/entry_types/scrolled/package/frontend/{Viewer-0345ce57.js → Viewer-cc8d4072.js} +84 -92
  212. data/entry_types/scrolled/package/frontend/Wavesurfer-656e3c36.js +302 -0
  213. data/entry_types/scrolled/package/frontend/Wavesurfer-b753b838.js +385 -0
  214. data/entry_types/scrolled/package/frontend/commenting.css +1 -0
  215. data/entry_types/scrolled/package/frontend/extensions-167ae143.js +1455 -0
  216. data/entry_types/scrolled/package/frontend/extensions-2318f92b.js +5918 -0
  217. data/entry_types/scrolled/package/frontend/extensions-a461fd13.js +4890 -0
  218. data/entry_types/scrolled/package/frontend/extensions-c98269fd.js +1982 -0
  219. data/entry_types/scrolled/package/frontend/i18n-493cd2a6.js +93 -0
  220. data/entry_types/scrolled/package/frontend/i18n-e06cac14.js +100 -0
  221. data/entry_types/scrolled/package/frontend/{index-eb670c2f.js → index-2e5546f7.js} +36 -44
  222. data/entry_types/scrolled/package/frontend/index-de2b418d.js +130 -0
  223. data/entry_types/scrolled/package/frontend/index.css +1 -1
  224. data/entry_types/scrolled/package/frontend/index.js +1870 -875
  225. data/entry_types/scrolled/package/frontend/inlineEditing.css +1 -0
  226. data/entry_types/scrolled/package/frontend/useContentElementEditorState-a084912e.js +25 -0
  227. data/entry_types/scrolled/package/frontend/useDelayedBoolean-a387d85b.js +24 -0
  228. data/entry_types/scrolled/package/frontend/{usePhonePlatform-c2ba875d.js → usePhonePlatform-4ecb96d4.js} +1 -1
  229. data/entry_types/scrolled/package/frontend/usePhonePlatform-ce9ff08d.js +8 -0
  230. data/entry_types/scrolled/package/frontend-server.js +20 -209
  231. data/entry_types/scrolled/package/package.json +5 -3
  232. data/entry_types/scrolled/package/review.css +1 -0
  233. data/entry_types/scrolled/package/review.js +3306 -0
  234. data/entry_types/scrolled/package/testHelpers.js +836 -359
  235. data/entry_types/scrolled/package/widgets/consentBar.css +1 -1
  236. data/entry_types/scrolled/package/widgets/consentBar.js +269 -290
  237. data/entry_types/scrolled/package/widgets/defaultNavigation.css +2 -2
  238. data/entry_types/scrolled/package/widgets/defaultNavigation.js +911 -352
  239. data/entry_types/scrolled/package/widgets/excursionSheet.css +1 -1
  240. data/entry_types/scrolled/package/widgets/excursionSheet.js +87 -124
  241. data/entry_types/scrolled/package/widgets/iconInlineFileRights.css +1 -1
  242. data/entry_types/scrolled/package/widgets/iconInlineFileRights.js +62 -29
  243. data/entry_types/scrolled/package/widgets/iconScrollIndicator.js +32 -13
  244. data/entry_types/scrolled/package/widgets/mainStorylineSheet.css +1 -1
  245. data/entry_types/scrolled/package/widgets/mainStorylineSheet.js +56 -100
  246. data/entry_types/scrolled/package/widgets/textInlineFileRights.css +1 -1
  247. data/entry_types/scrolled/package/widgets/textInlineFileRights.js +33 -28
  248. data/entry_types/scrolled/package/widgets-server.js +1 -0
  249. data/entry_types/scrolled/spec/factories/lottie_files.rb +29 -0
  250. data/entry_types/scrolled/spec/fixtures/animation.lottie +0 -0
  251. data/entry_types/scrolled/spec/fixtures/font.woff +0 -0
  252. data/entry_types/scrolled/spec/fixtures/font.woff2 +0 -0
  253. data/lib/generators/pageflow/resque/resque_generator.rb +0 -2
  254. data/lib/pageflow/configuration.rb +49 -7
  255. data/lib/pageflow/deprecation.rb +19 -0
  256. data/lib/pageflow/engine.rb +1 -1
  257. data/lib/pageflow/entries_controller_env_helper.rb +32 -2
  258. data/lib/pageflow/entry_export_import/entry_serialization.rb +4 -4
  259. data/lib/pageflow/entry_export_import/page_type_versions.rb +2 -2
  260. data/lib/pageflow/entry_export_import/revision_serialization/import.rb +10 -0
  261. data/lib/pageflow/entry_export_import/revision_serialization.rb +3 -0
  262. data/lib/pageflow/entry_structured_data_types.rb +43 -0
  263. data/lib/pageflow/entry_type_configuration.rb +1 -0
  264. data/lib/pageflow/file_type.rb +2 -2
  265. data/lib/pageflow/file_types.rb +1 -1
  266. data/lib/pageflow/oembed_providers.rb +42 -0
  267. data/lib/pageflow/primary_domain_entry_redirect.rb +6 -0
  268. data/lib/pageflow/rails_version.rb +13 -2
  269. data/lib/pageflow/themes.rb +41 -0
  270. data/lib/pageflow/user_mixin.rb +12 -0
  271. data/lib/pageflow/version.rb +1 -1
  272. data/lib/pageflow.rb +1 -0
  273. data/package/config/eslint-rules/documented-in-toc.js +125 -0
  274. data/package/config/jest/index.js +0 -1
  275. data/package/config/webpack.js +0 -1
  276. data/package/config/webpack5.js +0 -1
  277. data/package/editor.js +4378 -1897
  278. data/package/frontend.js +647 -886
  279. data/package/package.json +2 -1
  280. data/package/review.js +384 -0
  281. data/package/testHelpers.js +248 -188
  282. data/package/ui.js +1346 -630
  283. data/package/vendor/stubs/wysihtml5.js +2 -0
  284. data/spec/factories/comment_thread_reads.rb +10 -0
  285. data/spec/factories/comment_threads.rb +10 -0
  286. data/spec/factories/comments.rb +9 -0
  287. data/spec/factories/file_folders.rb +8 -0
  288. data/spec/factories/published_entries.rb +2 -0
  289. metadata +188 -67
  290. data/app/assets/stylesheets/pageflow/editor/select_button.scss +0 -51
  291. data/entry_types/scrolled/app/helpers/pageflow_scrolled/generated_media_queries_helper.rb +0 -55
  292. data/entry_types/scrolled/package/frontend/FloatingPortalRootProvider-51914be7.js +0 -5439
  293. data/entry_types/scrolled/package/frontend/ThemeIcon-81f2f066.js +0 -1932
  294. data/entry_types/scrolled/package/frontend/Viewer-cdc549cc.js +0 -147
  295. data/entry_types/scrolled/package/frontend/Wavesurfer-1cdc3925.js +0 -313
  296. data/entry_types/scrolled/package/frontend/components-96660ffd.js +0 -3803
  297. data/entry_types/scrolled/package/frontend/inherits-539844a6.js +0 -99
  298. data/entry_types/scrolled/package/frontend/useContentElementEditorState-77fe6c79.js +0 -51
  299. data/entry_types/scrolled/package/widgets/excursionDialog.css +0 -1
  300. data/entry_types/scrolled/package/widgets/excursionDialog.js +0 -109
  301. data/lib/generators/pageflow/resque/templates/resque_enqueue_after_commit_patch.rb +0 -25
  302. data/package/vendor/jquery.minicolors.js +0 -1108
@@ -15,7 +15,9 @@ module Pageflow
15
15
  )
16
16
 
17
17
  if entry
18
- handle_public_entry_request(entry)
18
+ respond_to do |format|
19
+ format.html { handle_public_entry_request(entry) }
20
+ end
19
21
  elsif site.home_url.present?
20
22
  redirect_to(site.home_url, allow_other_host: true)
21
23
  else
@@ -85,11 +87,13 @@ module Pageflow
85
87
  end
86
88
 
87
89
  def handle_public_entry_request(entry)
88
- return if redirect_according_to_entry_redirect(entry)
90
+ config = Pageflow.config_for(entry)
91
+
92
+ return if redirect_according_to_entry_redirect(entry, config)
89
93
  return if redirect_according_to_public_https_mode
90
94
  return unless check_entry_password_protection(entry)
91
95
 
92
- delegate_to_entry_type_frontend_app!(entry)
96
+ delegate_to_entry_type_frontend_app!(entry, config:)
93
97
  end
94
98
 
95
99
  def redirect_according_to_permalink_redirect
@@ -107,23 +111,21 @@ module Pageflow
107
111
  true
108
112
  end
109
113
 
110
- def redirect_according_to_entry_redirect(entry)
111
- return false unless (redirect_location = entry_redirect(entry))
114
+ def redirect_according_to_entry_redirect(entry, config)
115
+ return false unless (redirect_location = config.public_entry_redirect.call(entry, request))
112
116
 
113
117
  redirect_to(redirect_location, status: :moved_permanently, allow_other_host: true)
114
118
  true
115
119
  end
116
120
 
117
- def entry_redirect(entry)
118
- Pageflow.config.public_entry_redirect.call(entry, request)
119
- end
120
-
121
- def delegate_to_entry_type_frontend_app!(entry, override_status: nil)
122
- EntriesControllerEnvHelper.add_entry_info_to_env(request.env, entry:, mode: :published)
121
+ def delegate_to_entry_type_frontend_app!(entry, config:, override_status: nil)
122
+ EntriesControllerEnvHelper.add_entry_info_to_env(request.env,
123
+ entry:,
124
+ mode: :published,
125
+ embed: params[:embed].present?)
123
126
 
124
127
  delegate_to_rack_app!(entry.entry_type.frontend_app) do |result|
125
128
  status, headers, body = result
126
- config = Pageflow.config_for(entry)
127
129
 
128
130
  allow_iframe_for_embed(headers)
129
131
  apply_additional_headers(entry, config, headers)
@@ -155,7 +157,9 @@ module Pageflow
155
157
 
156
158
  if site&.custom_404_entry&.published_without_password_protection?
157
159
  entry = PublishedEntry.new(site.custom_404_entry)
158
- delegate_to_entry_type_frontend_app!(entry, override_status: 404)
160
+ config = Pageflow.config_for(entry)
161
+
162
+ delegate_to_entry_type_frontend_app!(entry, config:, override_status: 404)
159
163
  else
160
164
  # Fallback to ApplicationController's handler method
161
165
  render_static_404_error_page
@@ -0,0 +1,30 @@
1
+ module Pageflow
2
+ module Review
3
+ # @api private
4
+ class CommentThreadReadsController < Pageflow::ApplicationController
5
+ respond_to :json
6
+ before_action :authenticate_user!
7
+
8
+ def create
9
+ entry = DraftEntry.find(params[:entry_id])
10
+ authorize!(:read, entry.to_model)
11
+
12
+ CommentThreadRead.mark(entry: entry.to_model,
13
+ user: current_user,
14
+ comment_thread_perma_ids: known_perma_ids(entry))
15
+
16
+ head :no_content
17
+ end
18
+
19
+ private
20
+
21
+ # Guards against read records piling up for comment threads that
22
+ # do not exist in the entry.
23
+ def known_perma_ids(entry)
24
+ entry.comment_threads
25
+ .where(perma_id: params.fetch(:comment_thread_perma_ids, []))
26
+ .pluck(:perma_id)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ module Pageflow
2
+ module Review
3
+ # @api private
4
+ class CommentThreadsController < Pageflow::ApplicationController
5
+ respond_to :json
6
+ before_action :authenticate_user!
7
+
8
+ def index
9
+ entry = DraftEntry.find(params[:entry_id])
10
+ authorize!(:read, entry.to_model)
11
+
12
+ @comment_threads = entry.comment_threads.includes(:resolver, comments: :creator)
13
+ @read_at_by_perma_id =
14
+ CommentThreadRead.read_at_by_perma_id(entry: entry.to_model, user: current_user)
15
+ end
16
+
17
+ def create
18
+ entry = DraftEntry.find(params[:entry_id])
19
+ authorize!(:read, entry.to_model)
20
+
21
+ @comment_thread = entry.comment_threads.build(thread_params)
22
+ @comment_thread.creator = current_user
23
+
24
+ first_comment = @comment_thread.comments.build(first_comment_params)
25
+ first_comment.creator = current_user
26
+
27
+ @comment_thread.save!
28
+ render :create, status: :created
29
+ end
30
+
31
+ def update
32
+ entry = DraftEntry.find(params[:entry_id])
33
+ authorize!(:read, entry.to_model)
34
+
35
+ @comment_thread = entry.comment_threads.find(params[:id])
36
+
37
+ if ActiveModel::Type::Boolean.new.cast(params[:comment_thread][:resolved])
38
+ @comment_thread.resolve(current_user)
39
+ else
40
+ @comment_thread.unresolve
41
+ end
42
+
43
+ render :create
44
+ end
45
+
46
+ private
47
+
48
+ def thread_params
49
+ permitted = params.require(:comment_thread)
50
+ .permit(:subject_type, :subject_id, :section_perma_id)
51
+ permitted[:subject_range] = params[:comment_thread][:subject_range]&.permit!
52
+ permitted
53
+ end
54
+
55
+ def first_comment_params
56
+ params.require(:comment_thread).require(:comment).permit(:body, :quote)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,53 @@
1
+ module Pageflow
2
+ module Review
3
+ # @api private
4
+ class CommentsController < Pageflow::ApplicationController
5
+ respond_to :json
6
+ before_action :authenticate_user!
7
+
8
+ def create
9
+ entry = DraftEntry.find(params[:entry_id])
10
+ authorize!(:read, entry.to_model)
11
+
12
+ thread = entry.comment_threads.find(params[:comment_thread_id])
13
+ @comment = thread.comments.build(comment_params)
14
+ @comment.creator = current_user
15
+ @comment.save!
16
+
17
+ render :create, status: :created
18
+ end
19
+
20
+ def update
21
+ entry = DraftEntry.find(params[:entry_id])
22
+ authorize!(:read, entry.to_model)
23
+
24
+ @comment = find_comment(entry)
25
+
26
+ return head(:forbidden) unless @comment.creator == current_user
27
+
28
+ @comment.update!(update_params.merge(edited_at: Time.current))
29
+
30
+ render :create
31
+ end
32
+
33
+ private
34
+
35
+ def find_comment(entry)
36
+ entry.comment_threads
37
+ .find(params[:comment_thread_id])
38
+ .comments
39
+ .find(params[:id])
40
+ end
41
+
42
+ def comment_params
43
+ params.require(:comment).permit(:body, :quote)
44
+ end
45
+
46
+ # The quote records the wording the comment referred to when it was
47
+ # written. Editing must not make it describe some later version.
48
+ def update_params
49
+ params.require(:comment).permit(:body)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -39,7 +39,10 @@ module Pageflow
39
39
  end
40
40
 
41
41
  def delegate_to_entry_type_frontend_app!(entry)
42
- EntriesControllerEnvHelper.add_entry_info_to_env(request.env, entry:, mode: :preview)
42
+ EntriesControllerEnvHelper.add_entry_info_to_env(request.env,
43
+ entry:,
44
+ mode: :preview,
45
+ ui_locale: I18n.locale)
43
46
  delegate_to_rack_app!(entry.entry_type.frontend_app)
44
47
  end
45
48
  end
@@ -19,6 +19,46 @@ module Pageflow
19
19
  end
20
20
  end
21
21
 
22
+ def entry_comments_indicator(entry, summaries: entry_comment_summaries)
23
+ summary = summaries[entry.id]
24
+ return unless summary&.any?
25
+
26
+ content_tag(:span,
27
+ class: 'entry_comments_indicator',
28
+ data: {tooltip: entry_comments_tooltip(summary)}) do
29
+ safe_join([summary.topic_count.to_s,
30
+ (content_tag(:span, '', class: 'unread_dot') if summary.unread?)].compact)
31
+ end
32
+ end
33
+
34
+ # Built for the whole page at once, so that rendering a row does
35
+ # not query. Views without an index table collection pass their own
36
+ # summaries instead.
37
+ def entry_comment_summaries
38
+ @entry_comment_summaries ||=
39
+ EntryCommentSummary.for_entries(collection, user: current_user)
40
+ end
41
+
42
+ def entry_comments_tooltip(summary)
43
+ scope = 'pageflow.admin.entries.comments'
44
+
45
+ parts = [t("#{scope}.topic_count", count: summary.topic_count)]
46
+
47
+ if summary.unread_topic_count.positive?
48
+ parts << t("#{scope}.unread_topic_count", count: summary.unread_topic_count)
49
+ end
50
+
51
+ if summary.unread_reply_count.positive?
52
+ parts << t("#{scope}.unread_reply_count", count: summary.unread_reply_count)
53
+ end
54
+
55
+ if summary.unread_resolution_count.positive?
56
+ parts << t("#{scope}.unread_resolution_count", count: summary.unread_resolution_count)
57
+ end
58
+
59
+ t("#{scope}.tooltip", summary: parts.join(', '))
60
+ end
61
+
22
62
  def entry_type_collection(entry_types = Pageflow.config.entry_types)
23
63
  entry_types.map(&:name).index_by do |type|
24
64
  I18n.t(type, scope: 'activerecord.values.pageflow/entry.type_names')
@@ -9,8 +9,8 @@ module Pageflow
9
9
  end
10
10
 
11
11
  def admin_widget_types_collection_for_role(config, role)
12
- config.widget_types.find_all_by_role(role).each_with_object({}) do |widget_type, result|
13
- result[I18n.t(widget_type.translation_key)] = widget_type.name
12
+ config.widget_types.find_all_by_role(role).to_h do |widget_type|
13
+ [I18n.t(widget_type.translation_key), widget_type.name]
14
14
  end
15
15
  end
16
16
  end
@@ -10,7 +10,9 @@ module Pageflow
10
10
  config = Pageflow.config_for(entry)
11
11
 
12
12
  json.locale entry.locale
13
- json.site entry.site.as_json(only: [:privacy_link_url])
13
+ json.site do
14
+ json.privacy_link_url entry.legal_info.privacy_url
15
+ end
14
16
  json.enabled_feature_names entry.enabled_feature_names
15
17
  json.page_types PageTypesSeed.new(config).as_json
16
18
 
@@ -26,8 +28,8 @@ module Pageflow
26
28
  end
27
29
 
28
30
  def as_json
29
- config.page_types.each_with_object({}) do |page_type, result|
30
- result[page_type.name.to_sym] = page_type_seed(page_type)
31
+ config.page_types.to_h do |page_type|
32
+ [page_type.name.to_sym, page_type_seed(page_type)]
31
33
  end
32
34
  end
33
35
 
@@ -5,7 +5,9 @@ module Pageflow
5
5
  include RenderJsonHelper
6
6
 
7
7
  def editor_config_seeds(entry)
8
- render_json_partial('pageflow/editor/config/seeds', entry:)
8
+ render_json_partial('pageflow/editor/config/seeds',
9
+ entry:,
10
+ entry_config: Pageflow.config_for(entry))
9
11
  end
10
12
  end
11
13
  end
@@ -72,10 +72,12 @@ module Pageflow
72
72
  end
73
73
 
74
74
  def entry_privacy_link_url(entry)
75
- return unless entry.site.privacy_link_url.present?
76
- return entry.site.privacy_link_url if entry.site.privacy_link_url.start_with?('javascript:')
75
+ url = entry.legal_info.privacy_url
77
76
 
78
- "#{entry.site.privacy_link_url}?lang=#{entry.locale}"
77
+ return unless url.present?
78
+ return url if url.start_with?('javascript:')
79
+
80
+ "#{url}?lang=#{entry.locale}"
79
81
  end
80
82
 
81
83
  def entry_file_rights(entry)
@@ -94,50 +96,30 @@ module Pageflow
94
96
  end
95
97
  end
96
98
 
97
- # rubocop:todo Metrics/PerceivedComplexity
98
- # rubocop:todo Metrics/CyclomaticComplexity
99
99
  def entry_global_links(entry) # rubocop:todo Metrics/AbcSize
100
- links = []
101
-
102
- if entry.site.imprint_link_label.present? && entry.site.imprint_link_url.present?
103
- links << link_to(
104
- raw(entry.site.imprint_link_label),
105
- entry.site.imprint_link_url,
106
- target: entry.site.imprint_link_url.start_with?('javascript:') ? nil : '_blank',
107
- tabindex: 2,
108
- class: 'legal'
109
- )
110
- end
111
-
112
- if entry.site.copyright_link_label.present? && entry.site.copyright_link_url.present?
113
- links << link_to(
114
- raw(entry.site.copyright_link_label),
115
- entry.site.copyright_link_url,
116
- target: entry.site.copyright_link_url.start_with?('javascript:') ? nil : '_blank',
117
- tabindex: 2,
118
- class: 'copy'
119
- )
100
+ legal_info = entry.legal_info
101
+
102
+ candidates = [
103
+ [legal_info.imprint.label, legal_info.imprint.url, 'legal'],
104
+ [legal_info.copyright.label, legal_info.copyright.url, 'copy'],
105
+ [I18n.t('pageflow.public.privacy_notice'), entry_privacy_link_url(entry), 'privacy']
106
+ ]
107
+
108
+ links = candidates.filter_map do |label, url, css_class|
109
+ next if label.blank? || url.blank?
110
+
111
+ link_to(raw(label),
112
+ url,
113
+ target: url.start_with?('javascript:') ? nil : '_blank',
114
+ tabindex: 2,
115
+ class: css_class)
120
116
  end
121
117
 
122
- if entry.site.privacy_link_url.present?
123
- links << link_to(
124
- I18n.t('pageflow.public.privacy_notice'),
125
- entry_privacy_link_url(entry),
126
- target: entry.site.privacy_link_url.start_with?('javascript:') ? nil : '_blank',
127
- tabindex: 2,
128
- class: 'privacy'
129
- )
130
- end
118
+ return '' if links.empty?
131
119
 
132
- if links.any?
133
- content_tag(:span, I18n.t('pageflow.helpers.entries.global_links'),
134
- class: 'hidden') + safe_join(links, ''.html_safe)
135
- else
136
- ''
137
- end
120
+ content_tag(:span, I18n.t('pageflow.helpers.entries.global_links'),
121
+ class: 'hidden') + safe_join(links, ''.html_safe)
138
122
  end
139
- # rubocop:enable Metrics/CyclomaticComplexity
140
- # rubocop:enable Metrics/PerceivedComplexity
141
123
 
142
124
  def entry_theme_stylesheet_link_tag(entry)
143
125
  stylesheet_link_tag(entry.theme.stylesheet_path,
@@ -51,14 +51,14 @@ module Pageflow
51
51
  end
52
52
 
53
53
  def entry_file_ids_seed(entry)
54
- Pageflow.config.file_types.with_thumbnail_support.each_with_object({}) do |file_type, result|
55
- result[file_type.collection_name] = entry.find_files(file_type.model).map(&:perma_id)
54
+ Pageflow.config.file_types.with_thumbnail_support.to_h do |file_type|
55
+ [file_type.collection_name, entry.find_files(file_type.model).map(&:perma_id)]
56
56
  end
57
57
  end
58
58
 
59
59
  def entry_audio_files_json_seed(entry)
60
- seed = entry.find_files(AudioFile).each_with_object({}) do |audio_file, result|
61
- result[audio_file.perma_id] = audio_file_sources(audio_file)
60
+ seed = entry.find_files(AudioFile).to_h do |audio_file|
61
+ [audio_file.perma_id, audio_file_sources(audio_file)]
62
62
  end
63
63
 
64
64
  sanitize_json(seed.to_json).html_safe
@@ -11,14 +11,15 @@ module Pageflow
11
11
  value.presence || fallback
12
12
  end
13
13
 
14
- {
15
- pageflow: {
16
- public: I18n.t('pageflow.public', locale: I18n.default_locale)
17
- .dup
18
- .deep_merge(I18n.t('pageflow.public', locale: entry.locale),
19
- &merge_ignoring_nil)
20
- }
21
- }
14
+ locales = [I18n.default_locale, *I18n.fallbacks[entry.locale].reverse].uniq &
15
+ I18n.available_locales
16
+
17
+ translations = locales.reduce({}) do |result, locale|
18
+ result.deep_merge(I18n.t('pageflow.public', locale:, default: {}),
19
+ &merge_ignoring_nil)
20
+ end
21
+
22
+ {pageflow: {public: translations}}
22
23
  end
23
24
  end
24
25
  end
@@ -6,10 +6,13 @@ module Pageflow
6
6
  include MetaTagsHelper
7
7
 
8
8
  def structured_data_for_entry(entry)
9
+ custom_properties = Pageflow.config_for(entry).entry_structured_data_types.for(entry)
10
+
9
11
  content_tag(:script, type: 'application/ld+json') do
10
12
  render_json_partial('pageflow/structured_data/entry',
11
13
  entry:,
12
- meta_data: meta_tags_data_for_entry(entry))
14
+ meta_data: meta_tags_data_for_entry(entry),
15
+ custom_properties:)
13
16
  end
14
17
  end
15
18
 
@@ -3,9 +3,7 @@ module Pageflow
3
3
  include RenderJsonHelper
4
4
 
5
5
  def themes_options_json_seed(config = Pageflow.config)
6
- config.themes.each_with_object({}) { |theme, options|
7
- options[theme.name] = theme.options
8
- }.to_json.html_safe
6
+ config.themes.to_h { |theme| [theme.name, theme.options] }.to_json.html_safe
9
7
  end
10
8
 
11
9
  def theme_json_seeds(config)
@@ -33,7 +33,8 @@ module Pageflow
33
33
  result[role] << {
34
34
  name: widget_type.name,
35
35
  translationKey: widget_type.translation_key,
36
- insertPoint: widget_type.insert_point
36
+ insertPoint: widget_type.insert_point,
37
+ enabledInEditor: widget_type.enabled_in_editor?
37
38
  }
38
39
  end
39
40
  }.to_json.html_safe
@@ -30,8 +30,12 @@ class PageflowPermalinkInput
30
30
  def permalink_inputs_html
31
31
  return '' if options[:site].permalink_directories.empty? || options[:only_root]
32
32
 
33
- builder.select(:directory_id, directory_select_options) <<
34
- builder.text_field(:slug, data: {placeholder: options[:slug_placeholder]})
33
+ template.content_tag(
34
+ :div,
35
+ builder.select(:directory_id, directory_select_options) <<
36
+ builder.text_field(:slug, data: {placeholder: options[:slug_placeholder]}),
37
+ class: 'permalink_inputs'
38
+ )
35
39
  end
36
40
 
37
41
  def error_keys
@@ -14,10 +14,17 @@ module Pageflow
14
14
 
15
15
  entry = entry_for_auto_generated_perma_id
16
16
 
17
- entry.with_lock do
18
- entry.increment!(:perma_id_counter)
19
- self.perma_id = entry.perma_id_counter
20
- end
17
+ self.perma_id =
18
+ if entry.new_record?
19
+ # No other process can see the entry yet, so there is nothing
20
+ # to lock.
21
+ entry.increment(:perma_id_counter).perma_id_counter
22
+ else
23
+ entry.with_lock do
24
+ entry.increment!(:perma_id_counter)
25
+ entry.perma_id_counter
26
+ end
27
+ end
21
28
  end
22
29
 
23
30
  def entry_for_auto_generated_perma_id
@@ -4,7 +4,7 @@ module Pageflow
4
4
 
5
5
  included do
6
6
  belongs_to :uploader, class_name: 'User', optional: true
7
- belongs_to :entry, optional: true
7
+ belongs_to :entry, class_name: 'Pageflow::Entry', optional: true
8
8
  belongs_to :parent_file, polymorphic: true, foreign_type: :parent_file_model_type,
9
9
  optional: true
10
10
 
@@ -2,10 +2,8 @@ module Pageflow
2
2
  # @api private
3
3
  module SerializationBlacklist
4
4
  def serializable_hash(options = nil)
5
- options ||= {}
6
-
7
- options[:except] = Array(options[:except])
8
- options[:except].concat(blacklist_for_serialization)
5
+ options = (options || {}).dup
6
+ options[:except] = Array(options[:except]) + blacklist_for_serialization
9
7
 
10
8
  super
11
9
  end
@@ -0,0 +1,101 @@
1
+ module Pageflow
2
+ # Store per locale overrides for a set of attributes in an
3
+ # `attribute_translations` column. The attribute itself keeps holding
4
+ # the value used for locales without translation. Since that value is
5
+ # read via the attribute's reader, overriding the reader also changes
6
+ # the value locales without translation fall back to.
7
+ #
8
+ # Not to be confused with {Translatable}, which groups entries that
9
+ # are translations of each other.
10
+ #
11
+ # @api private
12
+ module TranslatedAttributes
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ class_attribute :translated_attribute_names, default: []
17
+
18
+ serialize :attribute_translations, coder: JSON
19
+
20
+ validate :attribute_translations_are_known_locales_and_attributes
21
+ end
22
+
23
+ module ClassMethods # rubocop:todo Style/Documentation
24
+ # Declare which attributes can be overridden per locale. Defines
25
+ # a `<name>_translations` reader and writer for each of them,
26
+ # mapping locales to values.
27
+ def translated_attributes(*names)
28
+ self.translated_attribute_names = names.map(&:to_s)
29
+
30
+ names.each do |name|
31
+ define_method(:"#{name}_translations") { translations_for(name) }
32
+
33
+ define_method(:"#{name}_translations=") do |translations|
34
+ assign_translations(name, translations)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def attribute_translations
41
+ self[:attribute_translations] || {}
42
+ end
43
+
44
+ # Returns the translation for the given locale, or the value of the
45
+ # attribute itself if there is none. The fallback goes through the
46
+ # attribute's reader, so models can override it to compute a
47
+ # default. Such an override must not call `translated` itself, since
48
+ # that would recurse.
49
+ def translated(attribute, locale:)
50
+ locale_chain(locale)
51
+ .lazy
52
+ .map { |candidate| attribute_translations.dig(candidate, attribute.to_s) }
53
+ .find(&:present?) || public_send(attribute)
54
+ end
55
+
56
+ private
57
+
58
+ def translations_for(attribute)
59
+ attribute_translations.each_with_object({}) do |(locale, values), result|
60
+ value = values[attribute.to_s]
61
+ result[locale] = value if value.present?
62
+ end
63
+ end
64
+
65
+ # Translations of other attributes are preserved, while the given
66
+ # translations replace all translations previously stored for this
67
+ # attribute. Removing a translation in a form therefore does not
68
+ # require sending a separate delete instruction.
69
+ def assign_translations(attribute, translations)
70
+ result = attribute_translations.deep_dup
71
+ result.each_value { |values| values.delete(attribute.to_s) }
72
+
73
+ translations.each do |locale, value|
74
+ next if value.blank?
75
+
76
+ (result[locale.to_s] ||= {})[attribute.to_s] = value
77
+ end
78
+
79
+ self[:attribute_translations] = result.reject { |_locale, values| values.empty? }
80
+ end
81
+
82
+ # Regional locales fall back to their base locale, matching how
83
+ # translations are looked up for the entry's locale elsewhere.
84
+ def locale_chain(locale)
85
+ [locale.to_s, locale.to_s.split('-').first].uniq
86
+ end
87
+
88
+ def attribute_translations_are_known_locales_and_attributes
89
+ available_locales = Pageflow.config.available_public_locales.map(&:to_s)
90
+
91
+ attribute_translations.each do |locale, values|
92
+ unless available_locales.include?(locale)
93
+ errors.add(:attribute_translations, :unknown_locale)
94
+ end
95
+
96
+ unknown = values.keys - translated_attribute_names
97
+ errors.add(:attribute_translations, :not_translatable) if unknown.any?
98
+ end
99
+ end
100
+ end
101
+ end
@@ -20,7 +20,7 @@ module Pageflow
20
20
  ))
21
21
 
22
22
  validates_attachment_presence :attachment_on_s3
23
- validates_attachment_file_name :attachment_on_s3, matches: %r{^[^/\\]+\.\w{3,4}$}
23
+ validates_attachment_file_name :attachment_on_s3, matches: %r{^[^/\\]+\.\w{3,6}$}
24
24
  do_not_validate_attachment_file_type :attachment_on_s3
25
25
 
26
26
  state_machine initial: 'uploading' do