spina-blocks 0.2.0 → 0.3.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -12
  3. data/Rakefile +20 -2
  4. data/app/assets/javascripts/spina/controllers/block_collection_controller.js +188 -44
  5. data/app/controllers/spina/blocks/admin/blocks_controller.rb +64 -20
  6. data/app/controllers/spina/blocks/admin/categories_controller.rb +6 -6
  7. data/app/controllers/spina/blocks/admin/page_blocks_controller.rb +9 -9
  8. data/app/helpers/spina/blocks/blocks_helper.rb +3 -3
  9. data/app/models/concerns/spina/parts/block_filterable.rb +18 -0
  10. data/app/models/spina/blocks/account_extension.rb +27 -0
  11. data/app/models/spina/blocks/block.rb +5 -5
  12. data/app/models/spina/blocks/category.rb +1 -1
  13. data/app/models/spina/blocks/page_block.rb +2 -2
  14. data/app/models/spina/blocks/page_extension.rb +14 -0
  15. data/app/models/spina/blocks/search_extension.rb +15 -0
  16. data/app/models/spina/parts/block_collection.rb +2 -0
  17. data/app/models/spina/parts/block_reference.rb +2 -0
  18. data/app/views/spina/admin/parts/block_collections/_form.html.erb +41 -22
  19. data/app/views/spina/admin/parts/block_references/_form.html.erb +2 -2
  20. data/app/views/spina/admin/pro/search/results/_block.html.erb +23 -0
  21. data/app/views/spina/blocks/admin/blocks/_form_block_content.html.erb +2 -2
  22. data/app/views/spina/blocks/admin/blocks/_modal_form.html.erb +61 -0
  23. data/app/views/spina/blocks/admin/blocks/_new_block_form.html.erb +1 -1
  24. data/app/views/spina/blocks/admin/blocks/_new_block_modal_form.html.erb +42 -0
  25. data/app/views/spina/blocks/admin/blocks/edit_modal.html.erb +3 -0
  26. data/app/views/spina/blocks/admin/blocks/index.html.erb +1 -1
  27. data/app/views/spina/blocks/admin/blocks/new.html.erb +5 -1
  28. data/app/views/spina/blocks/admin/page_blocks/index.html.erb +3 -3
  29. data/config/locales/en.yml +1 -0
  30. data/config/routes.rb +5 -3
  31. data/db/migrate/1_create_spina_blocks_categories.rb +36 -0
  32. data/db/migrate/2_create_spina_blocks_blocks.rb +39 -0
  33. data/db/migrate/3_create_spina_blocks_page_blocks.rb +36 -0
  34. data/db/migrate/5_rename_title_to_name_in_spina_blocks_blocks.rb +18 -0
  35. data/lib/generators/spina/blocks/install_generator.rb +17 -0
  36. data/lib/spina/blocks/engine.rb +25 -25
  37. data/lib/spina/blocks/version.rb +1 -1
  38. data/lib/spina-blocks.rb +3 -3
  39. metadata +73 -13
  40. data/app/overrides/spina/account_override.rb +0 -19
  41. data/app/overrides/spina/page_override.rb +0 -6
  42. data/db/migrate/20250101000001_create_spina_blocks_categories.rb +0 -31
  43. data/db/migrate/20250101000002_create_spina_blocks_blocks.rb +0 -35
  44. data/db/migrate/20250101000003_create_spina_blocks_page_blocks.rb +0 -31
  45. data/db/migrate/20250101000004_remove_name_from_spina_blocks_blocks.rb +0 -24
@@ -13,7 +13,7 @@ module Spina
13
13
  block = page_block.block
14
14
  next unless block&.active?
15
15
 
16
- concat render_block(block)
16
+ concat(render_block(block))
17
17
  end
18
18
  end
19
19
 
@@ -28,7 +28,7 @@ module Spina
28
28
  partial_path = "#{theme_name}/blocks/#{block.block_template}"
29
29
 
30
30
  if lookup_context.exists?(partial_path, [], true)
31
- render partial: partial_path, locals: { block: block }
31
+ render(partial: partial_path, locals: { block: block })
32
32
  else
33
33
  render_block_fallback(block)
34
34
  end
@@ -49,7 +49,7 @@ module Spina
49
49
 
50
50
  def render_block_fallback(block)
51
51
  content_tag(:div, class: "spina-block spina-block--#{block.block_template}") do
52
- content_tag(:p, block.title, class: 'spina-block__title')
52
+ content_tag(:p, block.name, class: "spina-block__title")
53
53
  end
54
54
  end
55
55
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Parts
5
+ module BlockFilterable
6
+ extend ActiveSupport::Concern
7
+
8
+ def available_blocks
9
+ scope = ::Spina::Blocks::Block.active.sorted
10
+ opts = options.is_a?(Hash) ? options.with_indifferent_access : nil
11
+ if opts&.dig(:block_template).present?
12
+ scope = scope.where(block_template: opts[:block_template])
13
+ end
14
+ scope
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module AccountExtension
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ after_save :bootstrap_block_categories
10
+ end
11
+
12
+ private
13
+
14
+ def bootstrap_block_categories
15
+ theme_config = Spina::Theme.find_by_name(theme)
16
+ return unless theme_config
17
+ return unless theme_config.respond_to?(:block_categories) && theme_config.block_categories.present?
18
+
19
+ theme_config.block_categories.each_with_index do |category, index|
20
+ Spina::Blocks::Category.where(name: category[:name])
21
+ .first_or_create(label: category[:label])
22
+ .update(label: category[:label], position: index)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -8,18 +8,18 @@ module Spina
8
8
  include Spina::Partable
9
9
  include Spina::TranslatedContent
10
10
 
11
- belongs_to :category, class_name: 'Spina::Blocks::Category', optional: true
12
- has_many :page_blocks, class_name: 'Spina::Blocks::PageBlock', dependent: :destroy
13
- has_many :pages, through: :page_blocks, class_name: 'Spina::Page'
11
+ belongs_to :category, class_name: "Spina::Blocks::Category", optional: true
12
+ has_many :page_blocks, class_name: "Spina::Blocks::PageBlock", dependent: :destroy
13
+ has_many :pages, through: :page_blocks, class_name: "Spina::Page"
14
14
 
15
- validates :title, presence: true
15
+ validates :name, presence: true
16
16
  validates :block_template, presence: true
17
17
 
18
18
  scope :active, -> { where(active: true) }
19
19
  scope :sorted, -> { order(:position) }
20
20
 
21
21
  def to_s
22
- title
22
+ name
23
23
  end
24
24
  end
25
25
  end
@@ -3,7 +3,7 @@
3
3
  module Spina
4
4
  module Blocks
5
5
  class Category < ApplicationRecord
6
- has_many :blocks, class_name: 'Spina::Blocks::Block', foreign_key: :category_id, dependent: :nullify
6
+ has_many :blocks, class_name: "Spina::Blocks::Block", foreign_key: :category_id, dependent: :nullify
7
7
 
8
8
  validates :name, presence: true, uniqueness: true
9
9
  validates :label, presence: true
@@ -3,8 +3,8 @@
3
3
  module Spina
4
4
  module Blocks
5
5
  class PageBlock < ApplicationRecord
6
- belongs_to :page, class_name: 'Spina::Page'
7
- belongs_to :block, class_name: 'Spina::Blocks::Block'
6
+ belongs_to :page, class_name: "Spina::Page"
7
+ belongs_to :block, class_name: "Spina::Blocks::Block"
8
8
 
9
9
  validates :block_id, uniqueness: { scope: :page_id }
10
10
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module PageExtension
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ has_many :page_blocks, class_name: "Spina::Blocks::PageBlock", foreign_key: :page_id, dependent: :destroy
10
+ has_many :blocks, through: :page_blocks, class_name: "Spina::Blocks::Block"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module SearchExtension
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include Spina::Pro::Search
10
+
11
+ spina_searchable against: [:name]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,8 @@
3
3
  module Spina
4
4
  module Parts
5
5
  class BlockCollection < Base
6
+ include BlockFilterable
7
+
6
8
  attr_json :block_ids, :integer, array: true, default: -> { [] }
7
9
 
8
10
  attr_accessor :options
@@ -3,6 +3,8 @@
3
3
  module Spina
4
4
  module Parts
5
5
  class BlockReference < Base
6
+ include BlockFilterable
7
+
6
8
  attr_json :block_id, :integer, default: nil
7
9
 
8
10
  attr_accessor :options
@@ -1,5 +1,5 @@
1
1
  <%
2
- blocks = Spina::Blocks::Block.active.sorted
2
+ blocks = f.object.available_blocks
3
3
  selected_ids = f.object.block_ids || []
4
4
  field_name = "#{f.object_name}[block_ids][]"
5
5
 
@@ -15,7 +15,7 @@
15
15
  blocks_json = blocks.map { |b|
16
16
  {
17
17
  id: b.id,
18
- title: b.title,
18
+ name: b.name,
19
19
  templateName: b.block_template.to_s,
20
20
  templateTitle: template_titles[b.block_template.to_s] || b.block_template.to_s.titleize
21
21
  }
@@ -26,6 +26,9 @@
26
26
  data-controller="block-collection"
27
27
  data-block-collection-blocks-value="<%= blocks_json.to_json %>"
28
28
  data-block-collection-selected-ids-value="<%= selected_ids.to_json %>"
29
+ data-block-collection-edit-url-value="<%= spina.edit_modal_blocks_admin_block_path("__ID__") %>"
30
+ data-block-collection-new-url-value="<%= spina.new_blocks_admin_block_path(modal: 1) %>"
31
+ data-block-collection-blocks-data-url-value="<%= spina.blocks_data_blocks_admin_blocks_path(format: :json) %>"
29
32
  data-field-name="<%= field_name %>">
30
33
 
31
34
  <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
@@ -33,11 +36,26 @@
33
36
  <div class="text-gray-400 text-sm mb-2"><%= f.object.hint %></div>
34
37
  <% end %>
35
38
 
39
+ <%# Search input + dropdown %>
40
+ <div class="relative mb-3">
41
+ <input type="text"
42
+ data-block-collection-target="searchInput"
43
+ data-action="input->block-collection#onSearchInput focus->block-collection#onSearchFocus"
44
+ placeholder="Search or add block..."
45
+ autocomplete="off"
46
+ class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-spina focus:ring-spina" />
47
+
48
+ <div data-block-collection-target="dropdown"
49
+ style="display:none"
50
+ class="absolute z-10 w-full bg-white border border-gray-200 rounded-md shadow-lg py-1 max-h-60 overflow-y-auto">
51
+ </div>
52
+ </div>
53
+
36
54
  <%# Empty state message %>
37
55
  <div data-block-collection-target="emptyMessage"
38
56
  class="text-sm text-gray-400 italic py-3"
39
57
  style="<%= selected_ids.any? ? 'display:none' : '' %>">
40
- No blocks selected. Click &ldquo;Add block&rdquo; to get started.
58
+ No blocks selected. Use the search field above to add blocks.
41
59
  </div>
42
60
 
43
61
  <%# Sortable list of selected blocks (rendered by Stimulus) %>
@@ -46,24 +64,6 @@
46
64
  <%# Hidden fields container (rendered by Stimulus) %>
47
65
  <div data-block-collection-target="hiddenFields"></div>
48
66
 
49
- <%# Add block button + dropdown %>
50
- <div class="relative inline-block">
51
- <button type="button"
52
- data-action="block-collection#toggleDropdown"
53
- data-block-collection-target="addButton"
54
- class="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-spina">
55
- <svg class="w-4 h-4 mr-1.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
57
- </svg>
58
- Add block
59
- </button>
60
-
61
- <div data-block-collection-target="dropdown"
62
- style="display:none"
63
- class="absolute z-10 mt-1 w-64 bg-white border border-gray-200 rounded-md shadow-lg py-1 max-h-60 overflow-y-auto">
64
- </div>
65
- </div>
66
-
67
67
  <%# ===== Templates for Stimulus controller ===== %>
68
68
 
69
69
  <%# Selected block list item %>
@@ -78,6 +78,12 @@
78
78
  <span class="text-sm font-medium text-gray-800 truncate" data-role="title"></span>
79
79
  <span class="ml-2 text-xs text-gray-400" data-role="template-label"></span>
80
80
  </div>
81
+ <a class="ml-2 text-gray-300 hover:text-gray-600 transition-colors"
82
+ data-role="edit-link" href="" data-turbo-frame="modal" title="Edit">
83
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
85
+ </svg>
86
+ </a>
81
87
  <button type="button" class="ml-2 text-gray-300 hover:text-red-500 transition-colors"
82
88
  data-action="block-collection#remove" data-block-id="" title="Remove">
83
89
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -103,6 +109,19 @@
103
109
 
104
110
  <%# Dropdown empty state %>
105
111
  <template data-block-collection-target="dropdownEmptyTemplate">
106
- <div class="px-3 py-2 text-sm text-gray-400 italic">All blocks added</div>
112
+ <div class="px-3 py-2 text-sm text-gray-400 italic">No blocks found</div>
113
+ </template>
114
+
115
+ <%# Dropdown "New block" link %>
116
+ <template data-block-collection-target="newBlockTemplate">
117
+ <div class="border-t border-gray-200 mt-1 pt-1">
118
+ <a class="w-full text-left px-3 py-2 text-sm text-spina-primary hover:bg-gray-100 flex items-center font-medium"
119
+ href="" data-turbo-frame="modal" data-role="new-block-link">
120
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
122
+ </svg>
123
+ <%= t('spina.blocks.new') %>
124
+ </a>
125
+ </div>
107
126
  </template>
108
127
  </div>
@@ -4,9 +4,9 @@
4
4
  <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
5
5
  <% end %>
6
6
  <div class="mt-1">
7
- <% blocks = Spina::Blocks::Block.active.sorted %>
7
+ <% blocks = f.object.available_blocks %>
8
8
  <%= f.select :block_id,
9
- blocks.map { |b| [b.title, b.id] },
9
+ blocks.map { |b| [b.name, b.id] },
10
10
  {include_blank: t('spina.blocks.select_block')},
11
11
  class: "form-select text-sm w-full shadow-xs" %>
12
12
  </div>
@@ -0,0 +1,23 @@
1
+ <%= link_to spina.edit_blocks_admin_block_path(block), class: "block px-2 leading-tight", data: {selected: index.zero?, search_target: "result", action: "mouseenter->search#select", turbo_frame: "_top"} do %>
2
+ <div class="flex items-center justify-between w-full px-4 py-3 rounded-md bg-gray-400 bg-opacity-0 <%= 'bg-opacity-25' if index.zero? %>" data-selected-class="bg-opacity-25">
3
+ <div>
4
+ <div class="font-medium flex items-center">
5
+ <%= block.name %>
6
+ <% unless block.active? %>
7
+ <div class="opacity-50 text-xs ml-1">(inactive)</div>
8
+ <% end %>
9
+ <div class="flex items-center ml-2 text-xs px-1.5 py-0.5 space-x-1 bg-gray-900 bg-opacity-5 rounded">
10
+ <%= heroicon('cube', style: :outline, class: 'w-3 h-3 opacity-50') %>
11
+ <div class="opacity-75"><%= block.block_template %></div>
12
+ </div>
13
+ </div>
14
+ <% if block.category %>
15
+ <div class="opacity-50 text-xs"><%= block.category.label %></div>
16
+ <% end %>
17
+ </div>
18
+
19
+ <div class="opacity-0 <%= 'opacity-100' if index.zero? %>" data-selected-class="opacity-100">
20
+ <%= heroicon('arrow-narrow-right', style: :solid, class: 'w-5 h-5 text-gray-600') %>
21
+ </div>
22
+ </div>
23
+ <% end %>
@@ -1,8 +1,8 @@
1
1
  <div class="max-w-5xl">
2
2
  <label class="font-medium text-gray-700">
3
- <%= Spina::Blocks::Block.human_attribute_name(:title) %>
3
+ <%= Spina::Blocks::Block.human_attribute_name(:name) %>
4
4
  </label>
5
- <%= render Spina::Forms::TextFieldComponent.new(f, :title, size: "lg") %>
5
+ <%= render Spina::Forms::TextFieldComponent.new(f, :name, size: "lg") %>
6
6
  <turbo-frame id="block_content" src="<%= spina.edit_content_blocks_admin_block_path(f.object, locale: @locale) %>">
7
7
  </turbo-frame>
8
8
  </div>
@@ -0,0 +1,61 @@
1
+ <%# Reusable modal form for editing a block. Used by edit_modal action.
2
+ Expects: @block, @tabs, @locale %>
3
+ <%= turbo_frame_tag dom_id(@block, :modal_form) do %>
4
+ <div data-controller="tabs"
5
+ data-tabs-active="cursor-default text-gray-900 bg-spina-dark/10"
6
+ data-tabs-inactive="cursor-pointer bg-transparent text-gray-400 border-transparent">
7
+
8
+ <%# Modal header %>
9
+ <div class="px-6 pt-5 pb-4 border-b border-gray-200">
10
+ <div class="flex items-center justify-between">
11
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
12
+ <%= @block.name %>
13
+ </h3>
14
+
15
+ <%# Tab navigation %>
16
+ <nav>
17
+ <ul class="inline-flex w-auto rounded-md bg-gray-100">
18
+ <% @tabs.each do |tab_name| %>
19
+ <%= render partial: "button_#{tab_name}" %>
20
+ <% end %>
21
+ </ul>
22
+ </nav>
23
+ </div>
24
+ </div>
25
+
26
+ <%# Form %>
27
+ <%= form_with model: @block,
28
+ url: spina.blocks_admin_block_path(@block),
29
+ id: dom_id(@block, :modal),
30
+ data: { turbo_frame: dom_id(@block, :modal_form) } do |f| %>
31
+ <%= hidden_field_tag :locale, @locale %>
32
+ <%= hidden_field_tag :modal, 1 %>
33
+
34
+ <div class="px-6 py-4 max-h-[70vh] overflow-y-auto">
35
+ <% Mobility.with_locale(@locale) do %>
36
+ <% @tabs.each_with_index do |tab_name, idx| %>
37
+ <div data-tabs-target="pane" id="<%= tab_name %>" <%= 'hidden' unless idx.zero? %>>
38
+ <%= render partial: "form_#{tab_name}", locals: { f: f } %>
39
+ </div>
40
+ <% end %>
41
+ <% end %>
42
+ </div>
43
+
44
+ <%# Action buttons %>
45
+ <div class="px-6 py-4 border-t border-gray-200 flex flex-row-reverse">
46
+ <button type="submit"
47
+ form="<%= dom_id(@block, :modal) %>"
48
+ class="btn btn-primary ml-3"
49
+ data-controller="button"
50
+ data-action="button#loading"
51
+ data-loading-message="<%= t('spina.blocks.save') %>...">
52
+ <%= t('spina.blocks.save') %>
53
+ </button>
54
+
55
+ <button type="button" class="btn btn-default" data-action="modal#close">
56
+ <%= t('spina.ui.cancel') %>
57
+ </button>
58
+ </div>
59
+ <% end %>
60
+ </div>
61
+ <% end %>
@@ -19,7 +19,7 @@
19
19
  </div>
20
20
 
21
21
  <div class="mt-3">
22
- <%= render Spina::Forms::TextFieldComponent.new(f, :title, size: 'lg', autofocus: true) %>
22
+ <%= render Spina::Forms::TextFieldComponent.new(f, :name, size: 'lg', autofocus: true) %>
23
23
  </div>
24
24
  </div>
25
25
  </div>
@@ -0,0 +1,42 @@
1
+ <%# Modal variant of the new block form. Used when creating a block from BlockCollection.
2
+ After successful creation, the modal transitions to the block edit form.
3
+ Expects: @block, @block_templates %>
4
+ <%= turbo_frame_tag dom_id(@block, :new_block_modal_form) do %>
5
+ <%= form_with model: @block, url: spina.blocks_admin_blocks_path, data: { turbo_frame: "modal" } do |f| %>
6
+ <%= hidden_field_tag :modal, 1 %>
7
+
8
+ <div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
9
+ <div class="sm:flex sm:items-start">
10
+ <div class="w-full">
11
+ <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
12
+ <%= t('spina.blocks.new') %>
13
+ </h3>
14
+
15
+ <div class="mt-4">
16
+ <label for="block_block_template" class="block text-sm font-medium text-gray-700 mb-1">
17
+ <%= t('spina.blocks.block_type') %>
18
+ </label>
19
+ <%= f.select :block_template,
20
+ @block_templates.map { |bt| [bt[:title], bt[:name]] },
21
+ { include_blank: t('spina.blocks.select_block_type') },
22
+ class: "block w-full rounded-md border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 text-sm",
23
+ required: true %>
24
+ </div>
25
+
26
+ <div class="mt-3">
27
+ <%= render Spina::Forms::TextFieldComponent.new(f, :name, size: 'lg', autofocus: true) %>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div class="px-4 pb-5 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
33
+ <button type="submit" class="btn btn-primary w-full sm:w-auto sm:ml-3">
34
+ <%= t('spina.blocks.create') %>
35
+ </button>
36
+
37
+ <button type="button" class="btn btn-default w-full sm:w-auto mt-2 sm:mt-0" data-action="modal#close">
38
+ <%= t('spina.ui.cancel') %>
39
+ </button>
40
+ </div>
41
+ <% end %>
42
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= render(Spina::UserInterface::ModalComponent.new(size: "max-w-5xl")) do %>
2
+ <%= render 'modal_form' %>
3
+ <% end %>
@@ -75,7 +75,7 @@
75
75
  </div>
76
76
  <div class="flex-1 py-3 pr-4">
77
77
  <%= link_to spina.edit_blocks_admin_block_path(block), class: "block" do %>
78
- <div class="text-gray-900 font-medium text-sm"><%= block.title %></div>
78
+ <div class="text-gray-900 font-medium text-sm"><%= block.name %></div>
79
79
  <div class="text-gray-400 text-xs">
80
80
  <%= block.block_template %>
81
81
  <% unless block.active? %>
@@ -1,3 +1,7 @@
1
1
  <%= render(Spina::UserInterface::ModalComponent.new) do %>
2
- <%= render 'new_block_form' %>
2
+ <% if @modal %>
3
+ <%= render 'new_block_modal_form' %>
4
+ <% else %>
5
+ <%= render 'new_block_form' %>
6
+ <% end %>
3
7
  <% end %>
@@ -18,7 +18,7 @@
18
18
  params: {page_block: {block_id: block.id}},
19
19
  method: :post,
20
20
  class: "block w-full text-left px-4 py-2 text-sm font-medium leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900" do %>
21
- <div><%= block.title %></div>
21
+ <div><%= block.name %></div>
22
22
  <div class="text-xs text-gray-400"><%= block.block_template %></div>
23
23
  <% end %>
24
24
  <% end %>
@@ -33,7 +33,7 @@
33
33
  params: {page_block: {block_id: block.id}},
34
34
  method: :post,
35
35
  class: "block w-full text-left px-4 py-2 text-sm font-medium leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900" do %>
36
- <div><%= block.title %></div>
36
+ <div><%= block.name %></div>
37
37
  <div class="text-xs text-gray-400"><%= block.block_template %></div>
38
38
  <% end %>
39
39
  <% end %>
@@ -64,7 +64,7 @@
64
64
  </div>
65
65
  <div class="flex-1 py-3">
66
66
  <%= link_to spina.edit_blocks_admin_block_path(page_block.block), class: "block" do %>
67
- <div class="text-gray-900 font-medium text-sm"><%= page_block.block.title %></div>
67
+ <div class="text-gray-900 font-medium text-sm"><%= page_block.block.name %></div>
68
68
  <div class="text-gray-400 text-xs">
69
69
  <%= page_block.block.block_template %>
70
70
  </div>
@@ -5,6 +5,7 @@ en:
5
5
  new: "New block"
6
6
  create: "Create block"
7
7
  save: "Save block"
8
+ edit: "Edit block"
8
9
  saved: "Block saved"
9
10
  deleted: "Block deleted"
10
11
  delete: "Delete block"
data/config/routes.rb CHANGED
@@ -1,22 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Spina::Engine.routes.draw do
4
- namespace :blocks, path: '', module: 'blocks' do
4
+ namespace :blocks, path: "", module: "blocks" do
5
5
  namespace :admin, path: Spina.config.backend_path do
6
6
  resources :blocks do
7
7
  member do
8
8
  get :edit_content
9
+ get :edit_modal
9
10
  end
10
11
 
11
12
  collection do
12
13
  post :sort
14
+ get :blocks_data
13
15
  end
14
16
  end
15
17
 
16
- resources :categories, only: %i[index edit update]
18
+ resources :categories, only: [:index, :edit, :update]
17
19
 
18
20
  resources :pages, only: [] do
19
- resources :page_blocks, only: %i[index create destroy] do
21
+ resources :page_blocks, only: [:index, :create, :destroy] do
20
22
  collection do
21
23
  post :sort
22
24
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpinaBlocksCategories < ActiveRecord::Migration[7.0]
4
+ def change
5
+ if previous_version_applied?("20250101000001")
6
+ cleanup_version!("20250101000001")
7
+ return
8
+ end
9
+
10
+ return if connection.table_exists?(:spina_blocks_categories)
11
+
12
+ create_table(:spina_blocks_categories) do |t|
13
+ t.string(:name, null: false)
14
+ t.string(:label, null: false)
15
+ t.integer(:position, default: 0)
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index(:spina_blocks_categories, :name, unique: true)
21
+ end
22
+
23
+ private
24
+
25
+ def previous_version_applied?(version)
26
+ connection.select_value(
27
+ "SELECT 1 FROM schema_migrations WHERE version = #{connection.quote(version)}",
28
+ )
29
+ end
30
+
31
+ def cleanup_version!(version)
32
+ connection.execute(
33
+ "DELETE FROM schema_migrations WHERE version = #{connection.quote(version)}",
34
+ )
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ if previous_version_applied?("20250101000002")
6
+ cleanup_version!("20250101000002")
7
+ return
8
+ end
9
+
10
+ return if connection.table_exists?(:spina_blocks_blocks)
11
+
12
+ create_table(:spina_blocks_blocks) do |t|
13
+ t.string(:name, null: false)
14
+ t.string(:block_template, null: false)
15
+ t.references(:category, foreign_key: { to_table: :spina_blocks_categories }, null: true)
16
+ t.integer(:position, default: 0)
17
+ t.boolean(:active, default: true)
18
+ t.json(:json_attributes)
19
+
20
+ t.timestamps
21
+ end
22
+
23
+ add_index(:spina_blocks_blocks, :name, unique: true)
24
+ end
25
+
26
+ private
27
+
28
+ def previous_version_applied?(version)
29
+ connection.select_value(
30
+ "SELECT 1 FROM schema_migrations WHERE version = #{connection.quote(version)}",
31
+ )
32
+ end
33
+
34
+ def cleanup_version!(version)
35
+ connection.execute(
36
+ "DELETE FROM schema_migrations WHERE version = #{connection.quote(version)}",
37
+ )
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpinaBlocksPageBlocks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ if previous_version_applied?("20250101000003")
6
+ cleanup_version!("20250101000003")
7
+ return
8
+ end
9
+
10
+ return if connection.table_exists?(:spina_blocks_page_blocks)
11
+
12
+ create_table(:spina_blocks_page_blocks) do |t|
13
+ t.references(:page, null: false, foreign_key: { to_table: :spina_pages })
14
+ t.references(:block, null: false, foreign_key: { to_table: :spina_blocks_blocks })
15
+ t.integer(:position, default: 0)
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index(:spina_blocks_page_blocks, [:page_id, :block_id], unique: true)
21
+ end
22
+
23
+ private
24
+
25
+ def previous_version_applied?(version)
26
+ connection.select_value(
27
+ "SELECT 1 FROM schema_migrations WHERE version = #{connection.quote(version)}",
28
+ )
29
+ end
30
+
31
+ def cleanup_version!(version)
32
+ connection.execute(
33
+ "DELETE FROM schema_migrations WHERE version = #{connection.quote(version)}",
34
+ )
35
+ end
36
+ end