cms42 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +299 -0
- data/Rakefile +10 -0
- data/app/assets/stylesheets/cms/application.css +3 -0
- data/app/controllers/cms/admin/api_keys_controller.rb +52 -0
- data/app/controllers/cms/admin/base_controller.rb +54 -0
- data/app/controllers/cms/admin/documents_controller.rb +56 -0
- data/app/controllers/cms/admin/form_fields_controller.rb +70 -0
- data/app/controllers/cms/admin/form_submissions_controller.rb +36 -0
- data/app/controllers/cms/admin/images_controller.rb +67 -0
- data/app/controllers/cms/admin/pages_controller.rb +188 -0
- data/app/controllers/cms/admin/sections_controller.rb +177 -0
- data/app/controllers/cms/admin/sites_controller.rb +60 -0
- data/app/controllers/cms/admin/webhook_deliveries_controller.rb +19 -0
- data/app/controllers/cms/admin/webhooks_controller.rb +51 -0
- data/app/controllers/cms/api/base_controller.rb +45 -0
- data/app/controllers/cms/api/v1/base_controller.rb +29 -0
- data/app/controllers/cms/api/v1/pages_controller.rb +21 -0
- data/app/controllers/cms/api/v1/sites_controller.rb +18 -0
- data/app/controllers/cms/application_controller.rb +11 -0
- data/app/controllers/cms/public/base_controller.rb +23 -0
- data/app/controllers/cms/public/form_submissions_controller.rb +63 -0
- data/app/controllers/cms/public/previews_controller.rb +21 -0
- data/app/controllers/cms/public/sites_controller.rb +37 -0
- data/app/controllers/concerns/cms/admin/page_scoped_sections.rb +42 -0
- data/app/controllers/concerns/cms/current_site_resolver.rb +17 -0
- data/app/controllers/concerns/cms/public/page_paths.rb +31 -0
- data/app/controllers/concerns/cms/public/page_rendering.rb +23 -0
- data/app/controllers/concerns/cms/site_resolvable.rb +27 -0
- data/app/helpers/cms/admin/pages_helper.rb +29 -0
- data/app/helpers/cms/admin/sections_helper.rb +86 -0
- data/app/helpers/cms/admin/sites_helper.rb +8 -0
- data/app/helpers/cms/application_helper.rb +51 -0
- data/app/helpers/cms/media_helper.rb +28 -0
- data/app/helpers/cms/pages_helper.rb +16 -0
- data/app/helpers/cms/sections_helper.rb +25 -0
- data/app/helpers/cms/sites_helper.rb +11 -0
- data/app/javascript/cms/controllers/sortable_controller.js +38 -0
- data/app/jobs/cms/application_job.rb +6 -0
- data/app/jobs/cms/deliver_webhook_job.rb +66 -0
- data/app/mailers/cms/application_mailer.rb +9 -0
- data/app/mailers/cms/form_submission_mailer.rb +16 -0
- data/app/models/cms/api_key.rb +27 -0
- data/app/models/cms/application_record.rb +7 -0
- data/app/models/cms/document.rb +48 -0
- data/app/models/cms/form_field.rb +27 -0
- data/app/models/cms/form_submission.rb +42 -0
- data/app/models/cms/image.rb +61 -0
- data/app/models/cms/image_translation.rb +22 -0
- data/app/models/cms/page.rb +228 -0
- data/app/models/cms/page_section.rb +43 -0
- data/app/models/cms/page_translation.rb +22 -0
- data/app/models/cms/section/block_base.rb +32 -0
- data/app/models/cms/section/blocks/call_to_action_block.rb +16 -0
- data/app/models/cms/section/blocks/hero_block.rb +14 -0
- data/app/models/cms/section/blocks/image_block.rb +13 -0
- data/app/models/cms/section/blocks/rich_text_block.rb +12 -0
- data/app/models/cms/section/kind_registry.rb +66 -0
- data/app/models/cms/section.rb +94 -0
- data/app/models/cms/section_image.rb +10 -0
- data/app/models/cms/section_translation.rb +25 -0
- data/app/models/cms/site.rb +87 -0
- data/app/models/cms/webhook.rb +41 -0
- data/app/models/cms/webhook_delivery.rb +12 -0
- data/app/serializers/cms/api/base_serializer.rb +51 -0
- data/app/serializers/cms/api/page_serializer.rb +145 -0
- data/app/serializers/cms/api/site_serializer.rb +45 -0
- data/app/services/cms/locale_resolver.rb +30 -0
- data/app/services/cms/page_resolver.rb +73 -0
- data/app/services/cms/public_page_context.rb +49 -0
- data/app/views/cms/admin/api_keys/_form.html.erb +23 -0
- data/app/views/cms/admin/api_keys/create.html.erb +9 -0
- data/app/views/cms/admin/api_keys/edit.html.erb +5 -0
- data/app/views/cms/admin/api_keys/index.html.erb +36 -0
- data/app/views/cms/admin/api_keys/new.html.erb +5 -0
- data/app/views/cms/admin/documents/_form.html.erb +24 -0
- data/app/views/cms/admin/documents/edit.html.erb +2 -0
- data/app/views/cms/admin/documents/index.html.erb +37 -0
- data/app/views/cms/admin/documents/new.html.erb +2 -0
- data/app/views/cms/admin/form_fields/_form.html.erb +46 -0
- data/app/views/cms/admin/form_fields/edit.html.erb +2 -0
- data/app/views/cms/admin/form_fields/index.html.erb +41 -0
- data/app/views/cms/admin/form_fields/new.html.erb +2 -0
- data/app/views/cms/admin/form_submissions/index.html.erb +38 -0
- data/app/views/cms/admin/images/_form.html.erb +36 -0
- data/app/views/cms/admin/images/edit.html.erb +2 -0
- data/app/views/cms/admin/images/index.html.erb +25 -0
- data/app/views/cms/admin/images/new.html.erb +2 -0
- data/app/views/cms/admin/pages/_attach_section_panel.html.erb +20 -0
- data/app/views/cms/admin/pages/_form.html.erb +116 -0
- data/app/views/cms/admin/pages/_section_editor_frame.html.erb +3 -0
- data/app/views/cms/admin/pages/_sections_list.html.erb +9 -0
- data/app/views/cms/admin/pages/edit.html.erb +2 -0
- data/app/views/cms/admin/pages/index.html.erb +62 -0
- data/app/views/cms/admin/pages/new.html.erb +2 -0
- data/app/views/cms/admin/pages/show.html.erb +111 -0
- data/app/views/cms/admin/sections/_form.html.erb +128 -0
- data/app/views/cms/admin/sections/_section.html.erb +22 -0
- data/app/views/cms/admin/sections/edit.html.erb +9 -0
- data/app/views/cms/admin/sections/index.html.erb +47 -0
- data/app/views/cms/admin/sections/new.html.erb +9 -0
- data/app/views/cms/admin/sections/page_update.turbo_stream.erb +17 -0
- data/app/views/cms/admin/sections/show.html.erb +97 -0
- data/app/views/cms/admin/sites/_form.html.erb +44 -0
- data/app/views/cms/admin/sites/edit.html.erb +3 -0
- data/app/views/cms/admin/sites/new.html.erb +5 -0
- data/app/views/cms/admin/sites/show.html.erb +22 -0
- data/app/views/cms/admin/webhook_deliveries/index.html.erb +29 -0
- data/app/views/cms/admin/webhooks/_form.html.erb +38 -0
- data/app/views/cms/admin/webhooks/edit.html.erb +5 -0
- data/app/views/cms/admin/webhooks/index.html.erb +34 -0
- data/app/views/cms/admin/webhooks/new.html.erb +5 -0
- data/app/views/cms/form_submission_mailer/notify.html.erb +14 -0
- data/app/views/cms/form_submission_mailer/notify.text.erb +7 -0
- data/app/views/cms/public/pages/_content.html.erb +48 -0
- data/app/views/cms/public/pages/show.html.erb +44 -0
- data/app/views/cms/public/pages/templates/_custom.html.erb +3 -0
- data/app/views/cms/public/pages/templates/_form.html.erb +3 -0
- data/app/views/cms/public/pages/templates/_landing.html.erb +3 -0
- data/app/views/cms/public/pages/templates/_standard.html.erb +3 -0
- data/app/views/cms/sections/kinds/_cta.html.erb +13 -0
- data/app/views/cms/sections/kinds/_hero.html.erb +14 -0
- data/app/views/cms/sections/kinds/_image.html.erb +19 -0
- data/app/views/cms/sections/kinds/_rich_text.html.erb +6 -0
- data/app/views/layouts/cms/application.html.erb +13 -0
- data/app/views/layouts/cms/public.html.erb +14 -0
- data/bin/rails +19 -0
- data/bin/rubocop +9 -0
- data/cms.gemspec +29 -0
- data/config/importmap.rb +4 -0
- data/config/locales/activerecord.cms.en.yml +65 -0
- data/config/locales/en.yml +390 -0
- data/config/routes.rb +56 -0
- data/lib/cms/engine.rb +45 -0
- data/lib/cms/version.rb +5 -0
- data/lib/cms.rb +75 -0
- data/lib/cms42.rb +3 -0
- data/lib/generators/cms/install/install_generator.rb +26 -0
- data/lib/generators/cms/install/templates/create_cms_tables.rb +194 -0
- data/lib/generators/cms/install/templates/initializer.rb +21 -0
- data/lib/generators/cms/views/views_generator.rb +79 -0
- data/lib/tasks/cms_tasks.rake +6 -0
- data/lib/tasks/version.rake +8 -0
- metadata +281 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Public
|
|
5
|
+
class FormSubmissionsController < BaseController
|
|
6
|
+
include Cms::SiteResolvable
|
|
7
|
+
include Cms::Public::PageRendering
|
|
8
|
+
include Cms::Public::PagePaths
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
@site = find_site!
|
|
12
|
+
@page = @site.published_pages.includes(:form_fields, :page_translations, :localised,
|
|
13
|
+
{ page_sections: :section },
|
|
14
|
+
{ hero_image_attachment: :blob }).find(params[:page_id])
|
|
15
|
+
@fields = @page.form_fields.ordered
|
|
16
|
+
data = build_submission_data
|
|
17
|
+
|
|
18
|
+
@submission = @page.form_submissions.build(
|
|
19
|
+
data: data,
|
|
20
|
+
ip_address: request.remote_ip
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if @submission.save
|
|
24
|
+
notify_by_email(@submission)
|
|
25
|
+
redirect_back(
|
|
26
|
+
fallback_location: public_site_page_path_for(@site, @page),
|
|
27
|
+
notice: t("cms.notices.form_submission_sent")
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
I18n.with_locale(@site.default_locale) do
|
|
31
|
+
assign_public_page(site: @site, page: @page)
|
|
32
|
+
render template: "cms/public/pages/show", status: :unprocessable_content
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
rescue ActiveRecord::RecordNotFound
|
|
36
|
+
render plain: t("cms.errors.page_not_found"), status: :not_found
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_submission_data
|
|
42
|
+
@fields.to_h do |field|
|
|
43
|
+
[field.field_name, normalized_submission_value(field)]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalized_submission_value(field)
|
|
48
|
+
params.dig(:submission, field.field_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def notify_by_email(submission)
|
|
52
|
+
return unless Cms.config.form_submission_email
|
|
53
|
+
|
|
54
|
+
email = Cms.config.form_submission_email.call(@page)
|
|
55
|
+
return if email.blank?
|
|
56
|
+
|
|
57
|
+
Cms::FormSubmissionMailer.notify(submission, email).deliver_later
|
|
58
|
+
rescue StandardError
|
|
59
|
+
# Mailer failures must never break form submission
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Public
|
|
5
|
+
class PreviewsController < BaseController
|
|
6
|
+
include Cms::SiteResolvable
|
|
7
|
+
include Cms::Public::PageRendering
|
|
8
|
+
include Cms::Public::PagePaths
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
page = Cms::Page.kept.includes(:site).find_by!(preview_token: params[:preview_token])
|
|
12
|
+
site = page.site
|
|
13
|
+
|
|
14
|
+
I18n.with_locale(site.default_locale) do
|
|
15
|
+
assign_public_page(site: site, page: page)
|
|
16
|
+
render template: "cms/public/pages/show"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Public
|
|
5
|
+
class SitesController < BaseController
|
|
6
|
+
include Cms::SiteResolvable
|
|
7
|
+
include Cms::Public::PageRendering
|
|
8
|
+
include Cms::Public::PagePaths
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
site = find_site!
|
|
12
|
+
result = find_page_for_show!(site)
|
|
13
|
+
|
|
14
|
+
return render_not_found unless result
|
|
15
|
+
|
|
16
|
+
I18n.with_locale(result.locale) do
|
|
17
|
+
assign_public_page(site: site, page: result.page)
|
|
18
|
+
render template: "cms/public/pages/show"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def find_page_for_show!(site)
|
|
25
|
+
page_resolver_class.resolve(
|
|
26
|
+
site: site,
|
|
27
|
+
slug: params[:slug],
|
|
28
|
+
locale: I18n.locale.to_s
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def render_not_found
|
|
33
|
+
render plain: t("cms.errors.page_not_found"), status: :not_found
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Admin
|
|
5
|
+
module PageScopedSections
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def set_page
|
|
11
|
+
@page = current_site.pages.find(params[:page_id])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def page_scoped_request?
|
|
15
|
+
params[:page_id].present?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def turbo_page_scoped_request?
|
|
19
|
+
page_scoped_request? && (turbo_frame_request? || request.format.turbo_stream?)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attach_to_page!
|
|
23
|
+
@page.page_sections.create!(section: @section, position: next_position)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def next_position
|
|
27
|
+
@page.page_sections.maximum(:position).to_i + 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_page_show_context
|
|
31
|
+
@translation_locale = requested_locale
|
|
32
|
+
@subpages = @page.subpages.sort_by { |page| [page.position || 0, page.id] }
|
|
33
|
+
@page_sections = @page.page_sections.ordered.includes(:section)
|
|
34
|
+
@available_sections = current_site.sections
|
|
35
|
+
.kept
|
|
36
|
+
.global
|
|
37
|
+
.where.not(id: @page.section_ids)
|
|
38
|
+
.ordered
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module CurrentSiteResolver
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def configured_current_site
|
|
10
|
+
resolver = Cms.config.current_site_resolver
|
|
11
|
+
site = resolver.call(self) if resolver
|
|
12
|
+
return site if site.is_a?(Cms::Site)
|
|
13
|
+
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Public
|
|
5
|
+
module PagePaths
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
helper_method :public_site_path_for, :public_site_page_path_for
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def routed_without_site_slug?
|
|
15
|
+
params[:site_slug].blank? && (request.headers["X-CMS-SITE-SLUG"].present? || request.subdomains.first.present?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def public_site_path_for(site)
|
|
19
|
+
routed_without_site_slug? ? current_site_path : site_path(site.slug)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def public_site_page_path_for(site, page)
|
|
23
|
+
return public_site_path_for(site) if page.home?
|
|
24
|
+
|
|
25
|
+
page_path = page.public_path
|
|
26
|
+
|
|
27
|
+
routed_without_site_slug? ? current_site_page_path(page_path) : site_page_path(site.slug, page_path)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Public
|
|
5
|
+
module PageRendering
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def assign_public_page(site:, page:)
|
|
11
|
+
ctx = Cms::PublicPageContext.build(site: site, page: page)
|
|
12
|
+
|
|
13
|
+
@site = ctx.site
|
|
14
|
+
@page = ctx.page
|
|
15
|
+
@header_nav_items = ctx.header_nav_items
|
|
16
|
+
@footer_pages = ctx.footer_pages
|
|
17
|
+
@sections = ctx.sections
|
|
18
|
+
@form_fields = ctx.form_fields
|
|
19
|
+
@submission = ctx.submission if @submission.nil?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module SiteResolvable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include Cms::CurrentSiteResolver
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def find_site!
|
|
11
|
+
return @resolved_site if defined?(@resolved_site) && @resolved_site
|
|
12
|
+
return configured_current_site if configured_current_site
|
|
13
|
+
|
|
14
|
+
Cms::Site.live.find_by!(slug: resolved_site_slug!)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def resolved_site_slug!
|
|
18
|
+
slug = params[:site_slug].presence ||
|
|
19
|
+
request.headers["X-CMS-SITE-SLUG"].presence ||
|
|
20
|
+
request.subdomains.first.presence
|
|
21
|
+
normalized = slug.to_s.parameterize
|
|
22
|
+
raise ActiveRecord::RecordNotFound, I18n.t("cms.errors.site_slug_not_provided") if normalized.blank?
|
|
23
|
+
|
|
24
|
+
normalized
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Admin
|
|
5
|
+
module PagesHelper
|
|
6
|
+
# Flatten a tree of pages (already loaded with subpages) into [page, depth] pairs.
|
|
7
|
+
def flat_page_tree(pages, depth: 0, result: [])
|
|
8
|
+
pages.each do |page|
|
|
9
|
+
result << [page, depth]
|
|
10
|
+
flat_page_tree(Array(page.subpages), depth: depth + 1, result: result) if page.subpages.loaded?
|
|
11
|
+
end
|
|
12
|
+
result
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns breadcrumb array for a page: [ancestor, ..., page]
|
|
16
|
+
def breadcrumbs_for(page)
|
|
17
|
+
page.ancestors + [page]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Translation completeness: { "en" => :complete, "fr" => :missing }
|
|
21
|
+
def translation_completeness(page)
|
|
22
|
+
I18n.available_locales.each_with_object({}) do |locale, hash|
|
|
23
|
+
translation = page.page_translations.find { |t| t.locale == locale.to_s }
|
|
24
|
+
hash[locale.to_s] = translation&.title.present? ? :complete : :missing
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module Admin
|
|
5
|
+
module SectionsHelper
|
|
6
|
+
# Renders a form field for a block settings field definition.
|
|
7
|
+
#
|
|
8
|
+
# @param form [ActionView::Helpers::FormBuilder]
|
|
9
|
+
# @param field [Hash] field definition from BlockBase.settings_schema
|
|
10
|
+
def render_settings_field(form, field, site: nil)
|
|
11
|
+
name = field[:name]
|
|
12
|
+
|
|
13
|
+
content_tag(:div, class: "cms-form__field") do
|
|
14
|
+
concat form.label("settings_#{name}", cms_section_setting_label(name), class: "cms-form__label")
|
|
15
|
+
concat settings_input(form, field, site: site)
|
|
16
|
+
if field[:type] == :image
|
|
17
|
+
concat image_library_actions
|
|
18
|
+
concat image_library_hint(site) if site&.images&.none?
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def cms_section_image_options(site)
|
|
24
|
+
return [] unless site
|
|
25
|
+
|
|
26
|
+
site.images.ordered.map { |image| [image.display_title, image.id] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def settings_input(form, field, site:)
|
|
32
|
+
name = field[:name]
|
|
33
|
+
|
|
34
|
+
case field[:type]
|
|
35
|
+
when :url
|
|
36
|
+
form.url_field "settings[#{name}]", input_options(form, name)
|
|
37
|
+
when :color
|
|
38
|
+
form.color_field "settings[#{name}]", input_options(form, name, default: field[:default])
|
|
39
|
+
when :boolean
|
|
40
|
+
form.check_box "settings[#{name}]", { class: "cms-checkbox" }, "true", "false"
|
|
41
|
+
when :select
|
|
42
|
+
form.select "settings[#{name}]",
|
|
43
|
+
select_options(field),
|
|
44
|
+
{ selected: form.object.settings[name] || field[:default] },
|
|
45
|
+
class: "cms-input"
|
|
46
|
+
when :image
|
|
47
|
+
form.select "settings[#{name}]",
|
|
48
|
+
cms_section_image_options(site),
|
|
49
|
+
{
|
|
50
|
+
include_blank: t("cms.admin.sections.form.select_image"),
|
|
51
|
+
selected: form.object.settings[name]
|
|
52
|
+
},
|
|
53
|
+
class: "cms-input"
|
|
54
|
+
else
|
|
55
|
+
form.text_field "settings[#{name}]", input_options(form, name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def input_options(form, name, default: nil)
|
|
60
|
+
{ class: "cms-input", value: form.object.settings[name] || default }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def select_options(field)
|
|
64
|
+
field[:options].map { |option| [cms_section_setting_option_label(field[:name], option), option] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def image_library_hint(site)
|
|
68
|
+
return "".html_safe unless site&.images&.none?
|
|
69
|
+
|
|
70
|
+
content_tag(:p, t("cms.admin.sections.form.image_library_empty"), class: "cms-form__hint")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def image_library_actions
|
|
74
|
+
content_tag(:p, class: "cms-form__hint") do
|
|
75
|
+
safe_join(
|
|
76
|
+
[
|
|
77
|
+
link_to(t("cms.admin.sections.form.manage_images"), admin_images_path),
|
|
78
|
+
" ",
|
|
79
|
+
link_to(t("cms.admin.sections.form.upload_image"), new_admin_image_path)
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module ApplicationHelper
|
|
5
|
+
def cms_attachment_path(attachment)
|
|
6
|
+
return unless attachment.attached?
|
|
7
|
+
|
|
8
|
+
main_app.rails_blob_path(attachment, only_path: true)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def cms_yes_no(value)
|
|
12
|
+
t(value ? "cms.shared.yes" : "cms.shared.no")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cms_date(value)
|
|
16
|
+
l(value.to_date, format: :cms_date)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cms_datetime(value)
|
|
20
|
+
l(value, format: :cms_datetime)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def cms_page_template_label(key)
|
|
24
|
+
t("cms.page_templates.#{key}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def cms_page_status_label(key)
|
|
28
|
+
t("cms.page_statuses.#{key}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cms_form_field_kind_label(key)
|
|
32
|
+
t("cms.form_field_kinds.#{key}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cms_section_kind_label(key)
|
|
36
|
+
t("cms.section_kinds.#{key}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cms_webhook_event_label(key)
|
|
40
|
+
t("cms.webhook_events.#{key.tr('.', '_')}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def cms_section_setting_label(name)
|
|
44
|
+
t("cms.section_settings.labels.#{name}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cms_section_setting_option_label(name, option)
|
|
48
|
+
t("cms.section_settings.options.#{name}.#{option}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module MediaHelper
|
|
5
|
+
def cms_image_tag(image, rendition: nil, **)
|
|
6
|
+
return "" unless image&.file&.attached?
|
|
7
|
+
|
|
8
|
+
if rendition && Cms.config.image_renditions&.key?(rendition)
|
|
9
|
+
dimensions = Cms.config.image_renditions[rendition]
|
|
10
|
+
width, height = dimensions.split("x").map(&:to_i)
|
|
11
|
+
variant = image.file.variant(resize_to_limit: [width, height]).processed
|
|
12
|
+
image_tag(main_app.rails_representation_path(variant, only_path: true),
|
|
13
|
+
alt: image.alt_text.presence || image.display_title,
|
|
14
|
+
**)
|
|
15
|
+
else
|
|
16
|
+
image_tag(main_app.rails_blob_path(image.file, only_path: true),
|
|
17
|
+
alt: image.alt_text.presence || image.display_title,
|
|
18
|
+
**)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cms_document_url(document)
|
|
23
|
+
return nil unless document&.file&.attached?
|
|
24
|
+
|
|
25
|
+
url_for(document.file)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module PagesHelper
|
|
5
|
+
def render_page_template(page)
|
|
6
|
+
render partial: cms_page_template_partial(page)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def cms_page_template_partial(page)
|
|
10
|
+
template = "cms/public/pages/templates/#{page.template_key}"
|
|
11
|
+
return template if lookup_context.exists?(template, [], true)
|
|
12
|
+
|
|
13
|
+
"cms/public/pages/templates/standard"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
module SectionsHelper
|
|
5
|
+
def cms_sections(kind:, site: nil)
|
|
6
|
+
target_site = site || (defined?(@site) ? @site : nil)
|
|
7
|
+
return Cms::Section.none unless target_site
|
|
8
|
+
|
|
9
|
+
target_site.sections.enabled.by_kind(kind).includes(:translations,
|
|
10
|
+
section_images: { image: { file_attachment: :blob } }).ordered
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Renders the partial registered for the section's kind.
|
|
14
|
+
# In non-production, renders an error notice for unknown kinds.
|
|
15
|
+
# In production, raises so the error is surfaced properly.
|
|
16
|
+
def render_section(section)
|
|
17
|
+
partial = Cms::Section::KindRegistry.partial_for(section.kind)
|
|
18
|
+
render partial, section: section
|
|
19
|
+
rescue Cms::Section::KindRegistry::UnknownKindError => e
|
|
20
|
+
raise e if Rails.env.production?
|
|
21
|
+
|
|
22
|
+
content_tag(:div, e.message, class: "cms-section--unknown", style: "color:red;padding:1em;border:1px solid red;")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import Sortable from "sortablejs"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
url: String,
|
|
7
|
+
handle: { type: String, default: ".cms-section-item__handle" }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.sortable = Sortable.create(this.element, {
|
|
12
|
+
handle: this.handleValue,
|
|
13
|
+
animation: 150,
|
|
14
|
+
onEnd: this.persistOrder.bind(this)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
this.sortable?.destroy()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
persistOrder() {
|
|
23
|
+
const ids = Array.from(this.element.querySelectorAll("[data-page-section-id]"))
|
|
24
|
+
.map(el => el.dataset.pageSectionId)
|
|
25
|
+
|
|
26
|
+
const body = new URLSearchParams()
|
|
27
|
+
ids.forEach(id => body.append("page_section_ids[]", id))
|
|
28
|
+
|
|
29
|
+
fetch(this.urlValue, {
|
|
30
|
+
method: "PATCH",
|
|
31
|
+
headers: {
|
|
32
|
+
"X-CSRF-Token": document.querySelector("meta[name=csrf-token]")?.content,
|
|
33
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
34
|
+
},
|
|
35
|
+
body: body.toString()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Cms
|
|
7
|
+
class DeliverWebhookJob < ApplicationJob
|
|
8
|
+
queue_as :default
|
|
9
|
+
|
|
10
|
+
def perform(webhook_id, event, payload)
|
|
11
|
+
webhook = Cms::Webhook.find_by(id: webhook_id)
|
|
12
|
+
return unless deliverable?(webhook, event)
|
|
13
|
+
|
|
14
|
+
response = build_http_client(webhook.url).request(build_request(webhook, event, payload))
|
|
15
|
+
record_delivery(webhook, event, response: response)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
record_delivery(webhook, event, error: e) if webhook
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def deliverable?(webhook, event)
|
|
23
|
+
webhook&.active? && webhook.events.include?(event)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_http_client(url)
|
|
27
|
+
uri = URI.parse(url)
|
|
28
|
+
|
|
29
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
|
30
|
+
http.use_ssl = uri.scheme == "https"
|
|
31
|
+
http.open_timeout = 5
|
|
32
|
+
http.read_timeout = 10
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_request(webhook, event, payload)
|
|
37
|
+
body = request_body(payload, event)
|
|
38
|
+
uri = URI.parse(webhook.url)
|
|
39
|
+
|
|
40
|
+
Net::HTTP::Post.new(uri.request_uri).tap do |request|
|
|
41
|
+
request["Content-Type"] = "application/json"
|
|
42
|
+
request["X-CMS-Event"] = event
|
|
43
|
+
request["X-CMS-Signature"] = sign(body, webhook.secret) if webhook.secret.present?
|
|
44
|
+
request.body = body
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def request_body(payload, event)
|
|
49
|
+
JSON.generate(payload.merge("event" => event, "timestamp" => Time.current.iso8601))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def record_delivery(webhook, event, response: nil, error: nil)
|
|
53
|
+
webhook.deliveries.create!(
|
|
54
|
+
event: event,
|
|
55
|
+
response_code: response&.code&.to_i,
|
|
56
|
+
response_body: response&.body.to_s.truncate(2000),
|
|
57
|
+
success: error.nil? && response&.code.to_i.between?(200, 299),
|
|
58
|
+
error_message: error&.message&.truncate(500)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sign(body, secret)
|
|
63
|
+
"sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, body)}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cms
|
|
4
|
+
class FormSubmissionMailer < ApplicationMailer
|
|
5
|
+
def notify(submission, recipient_email)
|
|
6
|
+
@submission = submission
|
|
7
|
+
@page = submission.page
|
|
8
|
+
@fields = @page.form_fields.ordered
|
|
9
|
+
|
|
10
|
+
mail(
|
|
11
|
+
to: recipient_email,
|
|
12
|
+
subject: I18n.t("cms.mailers.form_submission.subject", page_title: @page.display_title)
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|