spina 2.3.5 → 2.6.1

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.

Potentially problematic release.


This version of spina might be problematic. Click here for more details.

Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -3
  3. data/app/assets/javascripts/spina/application.js +1 -1
  4. data/app/assets/javascripts/spina/controllers/application.js +11 -0
  5. data/app/assets/javascripts/spina/controllers/embed_controller.js +16 -0
  6. data/app/assets/javascripts/spina/controllers/embed_tag_controller.js +16 -0
  7. data/app/assets/javascripts/spina/controllers/index.js +5 -0
  8. data/app/assets/javascripts/spina/controllers/trix_controller.js +12 -2
  9. data/app/assets/stylesheets/spina/_fonts.css.erb +114 -0
  10. data/app/assets/stylesheets/spina/_tailwind.css +18 -5
  11. data/app/assets/stylesheets/spina/application.css +6 -0
  12. data/app/assets/stylesheets/spina/tailwind/custom.css +13 -5
  13. data/app/components/spina/forms/trix_toolbar_component.html.erb +26 -15
  14. data/app/components/spina/media_picker/image_component.html.erb +1 -1
  15. data/app/components/spina/pages/new_page_button_component.html.erb +32 -0
  16. data/app/components/spina/pages/new_page_button_component.rb +19 -0
  17. data/app/controllers/concerns/spina/frontend.rb +1 -1
  18. data/app/controllers/spina/admin/embeds_controller.rb +36 -0
  19. data/app/controllers/spina/admin/pages_controller.rb +1 -2
  20. data/app/controllers/spina/admin/password_resets_controller.rb +2 -4
  21. data/app/helpers/spina/admin/icons_helper.rb +2 -2
  22. data/app/helpers/spina/images_helper.rb +1 -1
  23. data/app/helpers/spina/spina_helper.rb +3 -2
  24. data/app/mailers/spina/application_mailer.rb +7 -0
  25. data/app/mailers/spina/user_mailer.rb +6 -14
  26. data/app/models/spina/embeds/base.rb +7 -0
  27. data/app/models/spina/embeds/button.rb +13 -0
  28. data/app/models/spina/embeds/vimeo.rb +39 -0
  29. data/app/models/spina/embeds/youtube.rb +39 -0
  30. data/app/models/spina/page.rb +10 -3
  31. data/app/models/spina/user.rb +6 -0
  32. data/app/presenters/spina/content_presenter.rb +1 -1
  33. data/app/presenters/spina/rich_text_presenter.rb +45 -0
  34. data/app/views/layouts/spina/admin/application.html.erb +1 -1
  35. data/app/views/layouts/spina/mail.html.erb +476 -1
  36. data/app/views/spina/admin/embeds/new.html.erb +47 -0
  37. data/app/views/spina/admin/pages/edit_template.html.erb +1 -1
  38. data/app/views/spina/admin/pages/index.html.erb +1 -21
  39. data/app/views/spina/admin/shared/_flash.html.erb +2 -2
  40. data/app/views/spina/embeds/buttons/_button.html.erb +3 -0
  41. data/app/views/spina/embeds/buttons/_button_fields.html.erb +14 -0
  42. data/app/views/spina/embeds/buttons/_trix_button.html.erb +3 -0
  43. data/app/views/spina/embeds/vimeos/_thumbnail.html.erb +6 -0
  44. data/app/views/spina/embeds/vimeos/_vimeo.html.erb +1 -0
  45. data/app/views/spina/embeds/vimeos/_vimeo_fields.html.erb +9 -0
  46. data/app/views/spina/embeds/youtubes/_thumbnail.html.erb +6 -0
  47. data/app/views/spina/embeds/youtubes/_youtube.html.erb +1 -0
  48. data/app/views/spina/embeds/youtubes/_youtube_fields.html.erb +9 -0
  49. data/app/views/spina/user_mailer/forgot_password.html.erb +57 -1
  50. data/app/views/spina/user_mailer/forgot_password.text.erb +9 -0
  51. data/config/initializers/importmap.rb +12 -0
  52. data/config/locales/en.yml +30 -0
  53. data/config/locales/nl.yml +1 -0
  54. data/config/routes.rb +5 -2
  55. data/lib/generators/spina/embed_generator.rb +18 -0
  56. data/lib/generators/spina/install_generator.rb +36 -14
  57. data/lib/generators/spina/templates/app/models/spina/embeds/embed.rb.tt +18 -0
  58. data/lib/generators/spina/templates/app/views/default/pages/homepage.html.erb +1 -1
  59. data/lib/generators/spina/templates/app/views/spina/embeds/_fields.html.erb +13 -0
  60. data/lib/generators/spina/templates/app/views/spina/embeds/_partial.html.erb +2 -0
  61. data/lib/generators/spina/templates/config/initializers/spina.rb +7 -0
  62. data/lib/generators/spina/templates/config/initializers/themes/default.rb +3 -0
  63. data/lib/generators/spina/templates/config/initializers/themes/demo.rb +4 -1
  64. data/lib/spina/embeddable.rb +48 -0
  65. data/lib/spina/embeds/trix_conversion.rb +41 -0
  66. data/lib/spina/embeds.rb +11 -0
  67. data/lib/spina/engine.rb +7 -21
  68. data/lib/spina/part.rb +10 -2
  69. data/lib/spina/railtie.rb +10 -0
  70. data/lib/spina/theme.rb +16 -3
  71. data/lib/spina/theme_reloader.rb +20 -0
  72. data/lib/spina/version.rb +1 -1
  73. data/lib/spina.rb +11 -0
  74. data/lib/tasks/spina_tasks.rake +19 -2
  75. metadata +66 -34
  76. data/app/assets/stylesheets/spina/_fonts.scss +0 -114
  77. data/app/assets/stylesheets/spina/application.sass +0 -3
  78. data/app/views/spina/user_mailer/forgot_password.txt.erb +0 -1
@@ -1 +1,57 @@
1
- You forgot your password. Here's a link to setup a new one: <%= link_to 'Reset password', spina.edit_admin_password_reset_url(@user.password_reset_token) %>. It expires in 2 hours.
1
+ <span class="preheader" style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;"><%=t "spina.user_mailer.forgot_password.preheader" %></span>
2
+ <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
3
+ <tr>
4
+ <td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
5
+ <table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
6
+ <!-- Email Body -->
7
+ <tr>
8
+ <td class="email-body" width="570" cellpadding="0" cellspacing="0" style="word-break: break-word; margin: 0; padding: 0; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0;">
9
+ <table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation" style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0 auto; padding: 0;">
10
+ <!-- Body content -->
11
+ <tr>
12
+ <td class="content-cell" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;">
13
+ <div class="f-fallback">
14
+ <h1 style="margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;" align="left">
15
+ <%=t "spina.user_mailer.forgot_password.salutation", name: @user.name %>
16
+ </h1>
17
+ <p style="font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
18
+ <%=t "spina.user_mailer.forgot_password.introduction_html" %>
19
+ </p>
20
+ <!-- Action -->
21
+ <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 30px auto; padding: 0;">
22
+ <tr>
23
+ <td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
24
+ <!-- Border based button
25
+ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
26
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
27
+ <tr>
28
+ <td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
29
+ <a href="<%= spina.edit_admin_password_reset_url(@user.password_reset_token) %>" class="f-fallback button button--green" target="_blank" style="color: #FFF; border-color: #6865b4; border-style: solid; border-width: 10px 18px; background-color: #6865b4; display: inline-block; text-decoration: none; border-radius: 7px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16); -webkit-text-size-adjust: none; box-sizing: border-box;"><%=t "spina.user_mailer.forgot_password.button" %></a>
30
+ </td>
31
+ </tr>
32
+ </table>
33
+ </td>
34
+ </tr>
35
+ </table>
36
+ <p style="font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
37
+ <%=t "spina.user_mailer.forgot_password.request_origin", platform: @browser.platform.name, browser: @browser.name %>
38
+ </p>
39
+ <!-- Sub copy -->
40
+ <table class="body-sub" role="presentation" style="margin-top: 25px; padding-top: 25px; border-top-width: 1px; border-top-color: #EAEAEC; border-top-style: solid;">
41
+ <tr>
42
+ <td style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
43
+ <p class="f-fallback sub" style="font-size: 13px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;"><%=t "spina.user_mailer.forgot_password.trouble" %></p>
44
+ <p class="f-fallback sub" style="font-size: 13px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;"><%= spina.edit_admin_password_reset_url(@user.password_reset_token) %></p>
45
+ </td>
46
+ </tr>
47
+ </table>
48
+ </div>
49
+ </td>
50
+ </tr>
51
+ </table>
52
+ </td>
53
+ </tr>
54
+ </table>
55
+ </td>
56
+ </tr>
57
+ </table>
@@ -0,0 +1,9 @@
1
+ <%=t "spina.user_mailer.forgot_password.preheader" %>
2
+
3
+ <%=t "spina.user_mailer.forgot_password.salutation", name: @user.name %>
4
+
5
+ <%=t "spina.user_mailer.forgot_password.introduction" %>
6
+
7
+ <%= spina.edit_admin_password_reset_url(@user.password_reset_token) %>
8
+
9
+ <%=t "spina.user_mailer.forgot_password.request_origin", platform: @browser.platform.name, browser: @browser.name %>
@@ -0,0 +1,12 @@
1
+ Spina.config.importmap.draw do
2
+ # Stimulus & Turbo
3
+ pin "@hotwired/stimulus", to: "stimulus.js"
4
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
5
+ pin "@hotwired/turbo-rails", to: "turbo.js"
6
+
7
+ # Spina entrypoint
8
+ pin "application", to: "spina/application.js"
9
+
10
+ pin_all_from Spina::Engine.root.join("app/assets/javascripts/spina/controllers"), under: "controllers", to: "spina/controllers"
11
+ pin_all_from Spina::Engine.root.join("app/assets/javascripts/spina/libraries"), under: "libraries", to: "spina/libraries"
12
+ end
@@ -1,4 +1,10 @@
1
1
  en:
2
+ activemodel:
3
+ attributes:
4
+ spina/embeds/youtube:
5
+ url: youtube.com/watch?v=...
6
+ spina/embeds/vimeo:
7
+ url: vimeo.com/...
2
8
  activerecord:
3
9
  attributes:
4
10
  spina/account:
@@ -94,6 +100,15 @@ en:
94
100
  spina/user:
95
101
  one: User
96
102
  other: Users
103
+ helpers:
104
+ label:
105
+ spina/embeds/button:
106
+ url: URL
107
+ label: Button label
108
+ spina/embeds/youtube:
109
+ url: Youtube URL
110
+ spina/embeds/vimeo:
111
+ url: Vimeo URL
97
112
  spina:
98
113
  accounts:
99
114
  contact_details: Contact details
@@ -117,6 +132,9 @@ en:
117
132
  delete_confirmation: Are you sure you want to delete <strong>%{subject}</strong>?
118
133
  edit: Edit
119
134
  edit_website: Edit your website
135
+ embeds:
136
+ embed_a_component: Embed a component
137
+ embed_component: Embed component
120
138
  forgot_password:
121
139
  expired: Your password reset token has expired
122
140
  mail_subject: Reset your password
@@ -140,6 +158,7 @@ en:
140
158
  insert_photo: Insert photo
141
159
  insert_photos: Insert photos
142
160
  link: Link
161
+ missing_image: Missing image
143
162
  new_folder: New folder
144
163
  organize: Organize
145
164
  remove_image: Remove image
@@ -321,6 +340,16 @@ en:
321
340
  rename: Rename
322
341
  save_changes: Save changes
323
342
  saving: Saving...
343
+ user_mailer:
344
+ forgot_password:
345
+ button: Reset your password
346
+ introduction: You recently requested to reset your password for your Spina CMS account. This password reset is only valid for the next 2 hours. Copy and paste the URL below into your web browser.
347
+ introduction_html: "You recently requested to reset your password for your Spina CMS account. Use the button below to reset it. <strong>This password reset is only valid for the next 2 hours.</strong>"
348
+ preheader: Use this link to reset your password. The link is only valid for 2 hours.
349
+ request_origin: For security, this request was received from a %{platform} device using %{browser}. If you did not request a password reset, please ignore this email.
350
+ salutation: Hi, %{name}
351
+ subject: Reset your password
352
+ trouble: If you’re having trouble with the button above, copy and paste the URL below into your web browser.
324
353
  users:
325
354
  authorization: Authorization
326
355
  authorization_description: Administrators can manage users. Regular users cannot.
@@ -341,6 +370,7 @@ en:
341
370
  content: Content
342
371
  documents: Documents
343
372
  images: Images
373
+ main: Main
344
374
  media_library: Media library
345
375
  media_library_description: Here's where you'll find all of your images and documents
346
376
  pages: Pages
@@ -337,6 +337,7 @@ nl:
337
337
  all_pages: Alle pagina's
338
338
  documents: Documenten
339
339
  images: Afbeeldingen
340
+ main: Pagina's
340
341
  media_library: Mediabibliotheek
341
342
  media_library_description: Hier staan al je afbeeldingen, video's en documenten
342
343
  pages: Pagina's
data/config/routes.rb CHANGED
@@ -80,6 +80,8 @@ Spina::Engine.routes.draw do
80
80
  resources :images
81
81
 
82
82
  resource :media_picker, controller: "media_picker", only: [:show]
83
+
84
+ resources :embeds, only: [:new, :create]
83
85
  end
84
86
 
85
87
  # Sitemap
@@ -92,8 +94,9 @@ Spina::Engine.routes.draw do
92
94
  # Pages
93
95
  get '/:locale/*id' => 'pages#show', constraints: {locale: /#{Spina.config.locales.join('|')}/ }
94
96
  get '/:locale/' => 'pages#homepage', constraints: {locale: /#{Spina.config.locales.join('|')}/ }
95
- get '/*id' => 'pages#show', as: "page", controller: 'pages', constraints: lambda { |request|
96
- (!(Rails.env.development? && request.env['PATH_INFO'].starts_with?('/rails/') || request.env['PATH_INFO'].starts_with?("/#{Spina.config.backend_path}") || request.env['PATH_INFO'].starts_with?('/attachments/'))) && request.path.exclude?("rails/active_storage")
97
+ get '/*id' => 'pages#show', as: "page", controller: 'pages', constraints: -> (request) {
98
+ request.path.exclude?(ActiveStorage.routes_prefix) &&
99
+ !(Rails.env.development? && request.path.starts_with?('/rails/'))
97
100
  }
98
101
  end
99
102
 
@@ -0,0 +1,18 @@
1
+ module Spina
2
+ class EmbedGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path("../templates", __FILE__)
4
+
5
+ argument :attributes, type: :array, default: [], banner: "field field"
6
+
7
+ def create_embed_object
8
+ template "app/models/spina/embeds/embed.rb.tt", "app/models/spina/embeds/#{file_name}.rb"
9
+ end
10
+
11
+ def create_views
12
+ template "app/views/spina/embeds/_fields.html.erb", "app/views/spina/embeds/#{plural_file_name}/_#{file_name}_fields.html.erb"
13
+
14
+ template "app/views/spina/embeds/_partial.html.erb", "app/views/spina/embeds/#{plural_file_name}/_#{file_name}.html.erb"
15
+ end
16
+
17
+ end
18
+ end
@@ -1,52 +1,61 @@
1
1
  module Spina
2
2
  class InstallGenerator < Rails::Generators::Base
3
3
  source_root File.expand_path("../templates", __FILE__)
4
+
5
+ class_option :first_deploy, type: :boolean, default: false
6
+ class_option :silent, type: :boolean, default: false
4
7
 
5
8
  def create_initializer_file
6
- return if Rails.env.production?
9
+ return if first_deploy?
7
10
  template 'config/initializers/spina.rb'
8
11
  end
9
12
 
10
13
  def create_mobility_initializer_file
11
- return if Rails.env.production?
14
+ return if first_deploy?
12
15
  template 'config/initializers/mobility.rb'
13
16
  end
14
17
 
15
18
  def add_route
16
- return if Rails.env.production?
19
+ return if first_deploy?
17
20
  return if Rails.application.routes.routes.detect { |route| route.app.app == Spina::Engine }
18
21
  route "mount Spina::Engine => '/'"
19
22
  end
20
23
 
21
24
  def copy_migrations
22
- return if Rails.env.production?
25
+ return if first_deploy?
23
26
  rake 'spina:install:migrations'
24
27
  end
25
28
 
26
29
  def run_migrations
30
+ return if first_deploy?
27
31
  rake 'db:migrate'
28
32
  end
29
33
 
30
34
  def create_account
31
35
  return if ::Spina::Account.exists? && !no?('An account already exists. Skip? [Yn]')
32
36
  name = ::Spina::Account.first.try(:name) || 'MySite'
33
- name = ask("What would you like to name your website? [#{name}]").presence || name
34
- account = ::Spina::Account.first_or_create.update_attribute(:name, name)
37
+ if talkative_install?
38
+ name = ask("What would you like to name your website? [#{name}]").presence || name
39
+ end
40
+ account = ::Spina::Account.first_or_create.update(name: name)
35
41
  end
36
42
 
37
43
  def set_theme
38
44
  account = ::Spina::Account.first
39
- return if account.theme.present? && !no?("Theme '#{account.theme} is set. Skip? [Yn]")
45
+ return if account.theme.present? && !no?("Theme '#{account.theme}' is set. Skip? [Yn]")
40
46
 
41
- theme = begin
42
- theme = account.theme || themes.first
43
- theme = ask("What theme do you want to use? (#{themes.join('/')}) [#{theme}]").presence || theme
44
- end until theme.in? themes
47
+ theme = account.theme || themes.first
48
+ if talkative_install?
49
+ theme = begin
50
+ theme = ask("What theme do you want to use? (#{themes.join('/')}) [#{theme}]").presence || theme
51
+ end until theme.in? themes
52
+ end
45
53
 
46
- account.update_attribute(:theme, theme)
54
+ account.update(theme: theme)
47
55
  end
48
56
 
49
57
  def copy_template_files
58
+ return if options['first_deploy']
50
59
  theme = ::Spina::Account.first.theme
51
60
  if theme.in? ['default', 'demo']
52
61
  template "config/initializers/themes/#{theme}.rb"
@@ -59,10 +68,15 @@ module Spina
59
68
 
60
69
  def create_user
61
70
  return if ::Spina::User.exists? && !no?('A user already exists. Skip? [Yn]')
71
+
62
72
  email = 'admin@domain.com'
63
- email = ask("Please enter an email address for your first user: [#{email}]").presence || email
73
+ if talkative_install?
74
+ email = ask("Please enter an email address for your first user: [#{email}]").presence || email
75
+ end
64
76
  password = 'password'
65
- password = ask("Create a temporary password: [#{password}]").presence || password
77
+ if talkative_install?
78
+ password = ask("Create a temporary password: [#{password}]").presence || password
79
+ end
66
80
  @temporary_password = password
67
81
  ::Spina::User.create name: 'admin', email: email, password: password, admin: true
68
82
  end
@@ -91,6 +105,14 @@ module Spina
91
105
  themes = Spina::Theme.all.map(&:name)
92
106
  themes | ['default', 'demo']
93
107
  end
108
+
109
+ def first_deploy?
110
+ options['first_deploy']
111
+ end
112
+
113
+ def talkative_install?
114
+ !options['silent']
115
+ end
94
116
 
95
117
  end
96
118
  end
@@ -0,0 +1,18 @@
1
+ class <%= class_name %> < Spina::Embeds::Base
2
+ attributes <%= attributes.map{|attr| ":#{attr.name}"}.join(", ") %>
3
+
4
+ # You can use Rails validations on your attributes
5
+ validates <%= attributes.map{|attr| ":#{attr.name}"}.join(", ") %>, presence: true
6
+
7
+ # Pick an icon at https://heroicons.com
8
+ # and it'll show up in the list of embeddable components
9
+ heroicon "chip"
10
+
11
+ # If you want to render your embeddable component differently in Trix,
12
+ # you can choose to render a different partial
13
+ # Default: _<%= file_name %>.html.erb
14
+ #
15
+ # def to_trix_partial_path
16
+ # "spina/embeds/<%= plural_file_name %>/trix_<%= file_name %>"
17
+ # end
18
+ end
@@ -1,2 +1,2 @@
1
- <h1><%= current_page.title %>
1
+ <h1><%= current_page.title %></h1>
2
2
  <%= content.html :text %>
@@ -0,0 +1,13 @@
1
+ <div class="text-xl font-bold mb-3 text-gray-800">
2
+ Your embed
3
+ <div class="text-sm text-gray-500 font-normal">
4
+ Descriptive text
5
+ </div>
6
+ </div>
7
+
8
+ <!--
9
+ Put your fields here using Spina's built-in ViewComponents:
10
+
11
+ render Spina::Forms::LabelComponent.new(f, :url)
12
+ render Spina::Forms::TextFieldComponent.new(f, :url, autofocus: true)
13
+ -->
@@ -0,0 +1,2 @@
1
+ <!-- This is what visitors will see -->
2
+ Your embed
@@ -39,6 +39,13 @@ Spina.configure do |config|
39
39
  # The default is Spina::Authentication::Sessions and includes basic user management
40
40
  # config.authentication = "Spina::Authentication::Sessions"
41
41
 
42
+ # Mailers
43
+ # ===============
44
+ # In order to send emails, you need to set a default from address.
45
+ # You can set an optional reply_to address as well.
46
+ # config.mailer_defaults.from = "no-reply@example.com"
47
+ # config.mailer_defaults.reply_to = "support@example.com"
48
+
42
49
  # API
43
50
  # ===============
44
51
  # Set an API key to activate Spina's API.
@@ -59,4 +59,7 @@ Spina::Theme.register do |theme|
59
59
 
60
60
  # Plugins (optional)
61
61
  theme.plugins = []
62
+
63
+ # Embeds (optional)
64
+ theme.embeds = []
62
65
  end
@@ -37,7 +37,7 @@ Spina::Theme.register do |theme|
37
37
  theme.view_templates = [
38
38
  {name: 'homepage', title: 'Homepage', parts: %w(headline body image_collection)},
39
39
  {name: 'show', title: 'Default', parts: %w(body image repeater)},
40
- {name: 'demo', title: 'Demo', parts: %w(body image_collection image repeater)}
40
+ {name: 'demo', title: 'Demo', parts: %w(body image_collection image repeater), exclude_from: %w(articles)}
41
41
  ]
42
42
 
43
43
  # Custom pages
@@ -69,4 +69,7 @@ Spina::Theme.register do |theme|
69
69
 
70
70
  # Plugins (optional)
71
71
  theme.plugins = ['reviews']
72
+
73
+ # Embeds (optional)
74
+ theme.embeds = []
72
75
  end
@@ -0,0 +1,48 @@
1
+ module Spina
2
+ module Embeddable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :embed_attributes, default: []
7
+ class_attribute :icon, default: nil
8
+ end
9
+
10
+ class_methods do
11
+ def from_json(json)
12
+ begin
13
+ attributes = JSON.parse(json)
14
+ attributes.transform_keys!(&:to_sym)
15
+ new(attributes)
16
+ rescue
17
+ Rails.logger.error "[#{self.class.name}] Couldn't parse JSON"
18
+ nil
19
+ end
20
+ end
21
+
22
+ def attributes(*names)
23
+ attr_accessor(*names.map(&:to_sym))
24
+ self.embed_attributes += names.map(&:to_sym)
25
+ end
26
+
27
+ # Give it an icon
28
+ def heroicon(name)
29
+ self.icon = name
30
+ end
31
+ end
32
+
33
+ def initialize(attributes = {})
34
+ attributes.slice(*self.class.embed_attributes).each do |key, value|
35
+ instance_variable_set("@#{key}", value)
36
+ end
37
+ end
38
+
39
+ def to_fields_path
40
+ "#{to_partial_path}_fields"
41
+ end
42
+
43
+ def to_trix_partial_path
44
+ to_partial_path
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ module Spina
2
+ module Embeds
3
+ module TrixConversion
4
+ extend ActiveSupport::Concern
5
+
6
+ # Wrap rendered partial in an <embed> tag for Trix
7
+ def to_trix_attachment(content = trix_attachment_content)
8
+ wrap_with_embed_tag(content)
9
+ end
10
+
11
+ def wrap_with_embed_tag(html)
12
+ element = html_document.create_element("spina-embed", embed_tag_attributes)
13
+ element.inner_html = html
14
+ element.to_html
15
+ end
16
+
17
+ private
18
+
19
+ def embed_tag_attributes
20
+ spina_attributes.merge({"data-embed-attributes": embed_attributes_to_json})
21
+ end
22
+
23
+ def embed_attributes_to_json
24
+ JSON.generate Hash[self.class.embed_attributes.map{|a| [a.to_s, send(a)]}]
25
+ end
26
+
27
+ def spina_attributes
28
+ {"data-embed-type": self.class.name, "data-controller": "embed-tag"}
29
+ end
30
+
31
+ def trix_attachment_content
32
+ ApplicationController.render(partial: to_trix_partial_path, formats: :html, object: self, as: model_name.element)
33
+ end
34
+
35
+ def html_document
36
+ Nokogiri::HTML::Document.new.tap{|doc| doc.encoding = "UTF-8"}
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module Spina
2
+ module Embeds
3
+
4
+ def self.constantize(embeddable_type)
5
+ return nil if embeddable_type.blank?
6
+ name = embeddable_type.camelize.demodulize.to_s
7
+ "Spina::Embeds::#{name}".safe_constantize
8
+ end
9
+
10
+ end
11
+ end
data/lib/spina/engine.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'sass-rails'
2
1
  require 'importmap-rails'
3
2
  require 'turbo-rails'
4
3
  require 'stimulus-rails'
@@ -9,13 +8,14 @@ require 'mobility'
9
8
  require 'rack-rewrite'
10
9
  require 'babosa'
11
10
  require 'attr_json'
12
- require 'view_component/engine'
11
+ require 'view_component'
13
12
  require 'jsonapi/serializer'
13
+ require 'browser'
14
14
 
15
15
  module Spina
16
16
  class Engine < ::Rails::Engine
17
17
  isolate_namespace Spina
18
-
18
+
19
19
  config.autoload_paths += %W( #{config.root}/lib )
20
20
 
21
21
  config.to_prepare do
@@ -25,8 +25,9 @@ module Spina
25
25
  require_dependency(decorator)
26
26
  end
27
27
  end
28
-
29
- # Register JSON part types for editing content
28
+ end
29
+
30
+ config.to_prepare do
30
31
  Spina::Part.register(
31
32
  Spina::Parts::Line,
32
33
  Spina::Parts::MultiLine,
@@ -38,21 +39,6 @@ module Spina
38
39
  Spina::Parts::Attachment
39
40
  )
40
41
  end
41
-
42
- initializer "spina.importmap" do
43
- Spina.config.importmap.draw do
44
- # Stimulus & Turbo
45
- pin "@hotwired/stimulus", to: "stimulus.js"
46
- pin "@hotwired/stimulus-autoloader", to: "stimulus-autoloader.js"
47
- pin "@hotwired/turbo-rails", to: "turbo.js"
48
-
49
- # Spina entrypoint
50
- pin "application", to: "spina/application.js"
51
-
52
- pin_all_from Spina::Engine.root.join("app/assets/javascripts/spina/controllers"), under: "controllers", to: "spina/controllers"
53
- pin_all_from Spina::Engine.root.join("app/assets/javascripts/spina/libraries"), under: "libraries", to: "spina/libraries"
54
- end
55
- end
56
-
42
+
57
43
  end
58
44
  end
data/lib/spina/part.rb CHANGED
@@ -8,12 +8,20 @@ module Spina
8
8
  end
9
9
 
10
10
  def register(*parts)
11
- parts.each{|part| all << part}
11
+ parts.each do |part|
12
+ unregister(part)
13
+ all << part
14
+ end
15
+ end
16
+
17
+ def unregister(part)
18
+ all.delete_if do |registered_part|
19
+ registered_part.name == part.name
20
+ end
12
21
  end
13
22
 
14
23
  end
15
24
 
16
-
17
25
  end
18
26
  end
19
27
 
data/lib/spina/railtie.rb CHANGED
@@ -5,6 +5,16 @@ module Spina
5
5
  app.config.assets.precompile += %w(spina/manifest)
6
6
  end
7
7
 
8
+ initializer "spina.theme_reloader" do |app|
9
+ reloader = ThemeReloader.new
10
+ reloader.execute
11
+
12
+ app.reloaders << reloader
13
+ app.reloader.to_run do
14
+ reloader.execute
15
+ end
16
+ end
17
+
8
18
  ActiveSupport.on_load(:action_controller) do
9
19
  ::ActionController::Base.send(:include, Spina::AdminSectionable)
10
20
  end
data/lib/spina/theme.rb CHANGED
@@ -1,13 +1,18 @@
1
1
  module Spina
2
2
  class Theme
3
3
 
4
- attr_accessor :name, :title, :parts, :page_parts, :structures, :view_templates, :layout_parts, :custom_pages, :plugins, :public_theme, :config, :navigations, :resources
4
+ attr_accessor :name, :title, :parts, :page_parts, :structures, :view_templates, :layout_parts, :custom_pages, :plugins, :public_theme, :config, :navigations, :resources, :embeds
5
5
 
6
6
  class << self
7
7
 
8
8
  def all
9
9
  ::Spina::THEMES
10
10
  end
11
+
12
+ def unregister(name)
13
+ theme = find_by_name(name)
14
+ all.delete(theme) if theme
15
+ end
11
16
 
12
17
  def find_by_name(name)
13
18
  all.find { |theme| theme.name == name }
@@ -17,6 +22,7 @@ module Spina
17
22
  theme = ::Spina::Theme.new
18
23
  yield theme
19
24
  raise 'Missing theme name' if theme.name.nil?
25
+ unregister(theme.name)
20
26
  if theme.plugins.nil?
21
27
  theme.plugins = ::Spina::Plugin.all.map { |plugin| plugin.name }
22
28
  end
@@ -33,18 +39,25 @@ module Spina
33
39
  @custom_pages = []
34
40
  @navigations = []
35
41
  @resources = []
42
+ @embeds = []
36
43
  @public_theme = false
37
44
  end
45
+
46
+ def embeddables
47
+ embeds.map{|embed| Embeds.constantize(embed)}
48
+ end
38
49
 
39
- def new_page_templates(recommended: "")
50
+ def new_page_templates(resource: nil)
51
+ page_collection = resource&.name || "main"
40
52
  @view_templates.map do |view_template|
41
53
  next if is_custom_undeletable_page?(view_template[:name])
54
+ next if view_template[:exclude_from]&.include?(page_collection)
42
55
 
43
56
  OpenStruct.new({
44
57
  name: view_template[:name],
45
58
  title: view_template[:title],
46
59
  description: view_template[:description],
47
- recommended: view_template[:name] == recommended
60
+ recommended: view_template[:name] == resource&.view_template
48
61
  })
49
62
  end.compact.sort_by do |page_template|
50
63
  [page_template.recommended ? 0 : 1]