spina-blocks 0.2.0 → 0.3.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -12
  3. data/Rakefile +20 -2
  4. data/app/assets/javascripts/spina/controllers/block_collection_controller.js +10 -3
  5. data/app/controllers/spina/blocks/admin/blocks_controller.rb +29 -19
  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/spina/blocks/account_extension.rb +27 -0
  10. data/app/models/spina/blocks/block.rb +5 -5
  11. data/app/models/spina/blocks/category.rb +1 -1
  12. data/app/models/spina/blocks/page_block.rb +2 -2
  13. data/app/models/spina/blocks/page_extension.rb +14 -0
  14. data/app/models/spina/blocks/search_extension.rb +15 -0
  15. data/app/views/spina/admin/parts/block_collections/_form.html.erb +8 -1
  16. data/app/views/spina/admin/parts/block_references/_form.html.erb +1 -1
  17. data/app/views/spina/admin/pro/search/results/_block.html.erb +23 -0
  18. data/app/views/spina/blocks/admin/blocks/_form_block_content.html.erb +2 -2
  19. data/app/views/spina/blocks/admin/blocks/_modal_form.html.erb +61 -0
  20. data/app/views/spina/blocks/admin/blocks/_new_block_form.html.erb +1 -1
  21. data/app/views/spina/blocks/admin/blocks/edit_modal.html.erb +3 -0
  22. data/app/views/spina/blocks/admin/blocks/index.html.erb +1 -1
  23. data/app/views/spina/blocks/admin/page_blocks/index.html.erb +3 -3
  24. data/config/locales/en.yml +1 -0
  25. data/config/routes.rb +4 -3
  26. data/db/migrate/1_create_spina_blocks_categories.rb +36 -0
  27. data/db/migrate/2_create_spina_blocks_blocks.rb +39 -0
  28. data/db/migrate/3_create_spina_blocks_page_blocks.rb +36 -0
  29. data/db/migrate/5_rename_title_to_name_in_spina_blocks_blocks.rb +18 -0
  30. data/lib/generators/spina/blocks/install_generator.rb +17 -0
  31. data/lib/spina/blocks/engine.rb +25 -25
  32. data/lib/spina/blocks/version.rb +1 -1
  33. data/lib/spina-blocks.rb +3 -3
  34. metadata +71 -13
  35. data/app/overrides/spina/account_override.rb +0 -19
  36. data/app/overrides/spina/page_override.rb +0 -6
  37. data/db/migrate/20250101000001_create_spina_blocks_categories.rb +0 -31
  38. data/db/migrate/20250101000002_create_spina_blocks_blocks.rb +0 -35
  39. data/db/migrate/20250101000003_create_spina_blocks_page_blocks.rb +0 -31
  40. data/db/migrate/20250101000004_remove_name_from_spina_blocks_blocks.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08e197d3a0201be80942ea3c1807a1c538312e4ca58ddf9cc0c8ce08f39a0c05'
4
- data.tar.gz: 04ae851acfdbfbecf0f30486748f0283b9afa9d18c0bb8f03cde8f83fec21450
3
+ metadata.gz: 76633f69018375e8d330eee4b062677dd4dc10bd380ec2e7b08f3900fd448540
4
+ data.tar.gz: dd386042f84a6554db3eb15200011e4a953f3559054a44f05caa0e052b10fa3b
5
5
  SHA512:
6
- metadata.gz: 4606d45194c8fabe89c79f51a8129af1d1fb8043ec07e6601a65c3c2d5f59997c475904ee04088f6966b3a56652584c20c41c0fb15433d22589b445373276af1
7
- data.tar.gz: cb7906b60e6100513d8e637759074e0e7ed6968a7a18deaa7a6b3806b93c86853efc9df43fdc0461e3c025d78f8070f6db2546bec0a60ab766929909b9440c2b
6
+ metadata.gz: 19b5d1d156f66e85fb34aa6d2240d0f23cd0ffdb973601e2a4a1f3eca8c9058d39332c33bc986b52a69519b7eee22c3e39c6b713c6aadc9ccb5bdb93abaf5e3e
7
+ data.tar.gz: 7442845e4527e7dcfc8e429771b957638b89c053ae6b20767393756429000908af24337853d5899b9af5d06298efd492f53316a1db91712e85d9321f690647f2
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Spina Blocks
2
2
 
3
+ > ⚠️ **Warning**: This plugin is not production-ready and may cause issues. Use at your own risk.
4
+
3
5
  A plugin for [Spina CMS](https://www.spinacms.com) that adds reusable block components. Blocks are independent content units with their own templates and fields that can be assembled into pages.
4
6
 
5
7
  ## Installation
@@ -14,9 +16,15 @@ Run:
14
16
 
15
17
  ```bash
16
18
  bundle install
17
- rails db:migrate
19
+ rails generate spina:blocks:install
18
20
  ```
19
21
 
22
+ This copies the plugin's migrations into your app and runs `db:migrate`.
23
+
24
+ ## Upgrading
25
+
26
+ If you are upgrading from a version that used timestamp migrations (`20250101000001`–`20250101000004`), the new migrations will detect the old version numbers in your `schema_migrations` table, clean them up automatically, and skip any tables that already exist. No manual intervention is needed.
27
+
20
28
  ## Configuration
21
29
 
22
30
  ### Theme setup
@@ -125,10 +133,10 @@ Then in your template:
125
133
 
126
134
  ## Models
127
135
 
128
- | Model | Description |
129
- |-------|-------------|
130
- | `Spina::Blocks::Block` | Reusable content block with template and parts |
131
- | `Spina::Blocks::Category` | Block category for organizing the library |
136
+ | Model | Description |
137
+ | -------------------------- | -------------------------------------------------- |
138
+ | `Spina::Blocks::Block` | Reusable content block with template and parts |
139
+ | `Spina::Blocks::Category` | Block category for organizing the library |
132
140
  | `Spina::Blocks::PageBlock` | Join model linking blocks to pages (with position) |
133
141
 
134
142
  ## Admin interface
@@ -142,13 +150,13 @@ The plugin adds:
142
150
 
143
151
  ## Helper methods
144
152
 
145
- | Helper | Description |
146
- |--------|-------------|
147
- | `render_blocks(page)` | Render all blocks attached to a page via PageBlocks |
148
- | `render_block(block)` | Render a single block using its template partial |
149
- | `block_content(block, :part_name)` | Access a block's content |
150
- | `block_has_content?(block, :part_name)` | Check if a block has content |
153
+ | Helper | Description |
154
+ | --------------------------------------- | --------------------------------------------------- |
155
+ | `render_blocks(page)` | Render all blocks attached to a page via PageBlocks |
156
+ | `render_block(block)` | Render a single block using its template partial |
157
+ | `block_content(block, :part_name)` | Access a block's content |
158
+ | `block_has_content?(block, :part_name)` | Check if a block has content |
151
159
 
152
160
  ## License
153
161
 
154
- MIT
162
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,4 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/setup'
4
- require 'bundler/gem_tasks'
3
+ require "bundler/gem_tasks"
4
+
5
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+
8
+ # The engine.rake mechanism auto-appends db/migrate/ from ENGINE_ROOT to the
9
+ # migration paths. This causes ordering issues because engine migrations use
10
+ # sequential versions (1, 2, 3, 4) that sort before the timestamped copies in
11
+ # spec/dummy/db/migrate/. Since we copy engine migrations to the dummy app via
12
+ # railties:install:migrations, we remove the engine source path to avoid
13
+ # duplicates and ordering problems.
14
+ Rake::Task["app:db:load_config"].enhance do
15
+ engine_migrate = File.expand_path("db/migrate", ENGINE_ROOT)
16
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths.delete(engine_migrate)
17
+ end
18
+
19
+ require "rspec/core/rake_task"
20
+ RSpec::Core::RakeTask.new(spec: "app:db:test:prepare")
21
+
22
+ task default: :spec
@@ -18,8 +18,9 @@ export default class extends Controller {
18
18
 
19
19
  static get values() {
20
20
  return {
21
- blocks: Array, // [{id, title, templateName, templateTitle}]
21
+ blocks: Array, // [{id, name, templateName, templateTitle}]
22
22
  selectedIds: Array, // [id, id, ...]
23
+ editUrl: String, // base edit_modal URL with __ID__ placeholder
23
24
  };
24
25
  }
25
26
 
@@ -150,7 +151,7 @@ export default class extends Controller {
150
151
  this.dropdownOptionTemplateTarget.content.cloneNode(true);
151
152
  const button = option.querySelector("button");
152
153
  button.dataset.blockId = block.id;
153
- option.querySelector("[data-role='title']").textContent = block.title;
154
+ option.querySelector("[data-role='title']").textContent = block.name;
154
155
  this.dropdownTarget.appendChild(option);
155
156
  });
156
157
  });
@@ -169,12 +170,18 @@ export default class extends Controller {
169
170
  const fragment = this.listItemTemplateTarget.content.cloneNode(true);
170
171
  const root = fragment.querySelector("[data-block-id]");
171
172
  root.dataset.blockId = block.id;
172
- fragment.querySelector("[data-role='title']").textContent = block.title;
173
+ fragment.querySelector("[data-role='title']").textContent = block.name;
173
174
  fragment.querySelector("[data-role='template-label']").textContent =
174
175
  "(" + (block.templateTitle || block.templateName) + ")";
175
176
  fragment.querySelector(
176
177
  "[data-action='block-collection#remove']",
177
178
  ).dataset.blockId = block.id;
179
+
180
+ const editLink = fragment.querySelector("[data-role='edit-link']");
181
+ if (editLink && this.hasEditUrlValue && this.editUrlValue) {
182
+ editLink.href = this.editUrlValue.replace("__ID__", block.id);
183
+ }
184
+
178
185
  return fragment;
179
186
  }
180
187
 
@@ -7,13 +7,13 @@ module Spina
7
7
  admin_section :content
8
8
 
9
9
  before_action :set_locale
10
- before_action :set_block, only: %i[edit edit_content update destroy]
11
- before_action :set_tabs, only: %i[edit update]
10
+ before_action :set_block, only: [:edit, :edit_content, :edit_modal, :update, :destroy]
11
+ before_action :set_tabs, only: [:edit, :edit_modal, :update]
12
12
 
13
13
  helper ::Spina::Admin::PagesHelper
14
14
 
15
15
  def index
16
- add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path
16
+ add_breadcrumb(I18n.t("spina.blocks.title"), spina.blocks_admin_blocks_path)
17
17
 
18
18
  @block_templates = current_theme.try(:block_templates) || []
19
19
 
@@ -34,19 +34,19 @@ module Spina
34
34
  def create
35
35
  @block = Spina::Blocks::Block.new(block_params)
36
36
  if @block.save
37
- redirect_to spina.edit_blocks_admin_block_url(@block)
37
+ redirect_to(spina.edit_blocks_admin_block_url(@block))
38
38
  else
39
39
  @block_templates = current_theme.try(:block_templates) || []
40
- render turbo_stream: turbo_stream.update(
40
+ render(turbo_stream: turbo_stream.update(
41
41
  helpers.dom_id(@block, :new_block_form),
42
- partial: 'new_block_form'
43
- )
42
+ partial: "new_block_form",
43
+ ))
44
44
  end
45
45
  end
46
46
 
47
47
  def edit
48
- add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
49
- add_breadcrumb @block.title
48
+ add_breadcrumb(I18n.t("spina.blocks.title"), spina.blocks_admin_blocks_path, class: "text-gray-400")
49
+ add_breadcrumb(@block.name)
50
50
  end
51
51
 
52
52
  def edit_content
@@ -55,17 +55,27 @@ module Spina
55
55
  end&.dig(:parts) || []
56
56
  end
57
57
 
58
+ def edit_modal
59
+ end
60
+
58
61
  def update
59
62
  Mobility.locale = @locale
60
63
  if @block.update(block_params)
61
- flash[:success] = I18n.t('spina.blocks.saved')
62
- redirect_to spina.edit_blocks_admin_block_url(@block, params: { locale: @locale })
64
+ if params[:modal]
65
+ render(turbo_stream: turbo_stream.update("modal", ""))
66
+ else
67
+ flash[:success] = I18n.t("spina.blocks.saved")
68
+ redirect_to(spina.edit_blocks_admin_block_url(@block, params: { locale: @locale }))
69
+ end
70
+ elsif params[:modal]
71
+ flash.now[:error] = I18n.t("spina.blocks.couldnt_be_saved")
72
+ render(:edit_modal, status: :unprocessable_entity)
63
73
  else
64
- add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
74
+ add_breadcrumb(I18n.t("spina.blocks.title"), spina.blocks_admin_blocks_path, class: "text-gray-400")
65
75
  Mobility.locale = I18n.locale
66
- add_breadcrumb @block.title
67
- flash.now[:error] = I18n.t('spina.blocks.couldnt_be_saved')
68
- render :edit, status: :unprocessable_entity
76
+ add_breadcrumb(@block.name)
77
+ flash.now[:error] = I18n.t("spina.blocks.couldnt_be_saved")
78
+ render(:edit, status: :unprocessable_entity)
69
79
  end
70
80
  end
71
81
 
@@ -74,14 +84,14 @@ module Spina
74
84
  Spina::Blocks::Block.where(id: id).update_all(position: index + 1)
75
85
  end
76
86
 
77
- flash.now[:info] = I18n.t('spina.blocks.sorting_saved')
87
+ flash.now[:info] = I18n.t("spina.blocks.sorting_saved")
78
88
  render_flash
79
89
  end
80
90
 
81
91
  def destroy
82
- flash[:info] = I18n.t('spina.blocks.deleted')
92
+ flash[:info] = I18n.t("spina.blocks.deleted")
83
93
  @block.destroy
84
- redirect_to spina.blocks_admin_blocks_url
94
+ redirect_to(spina.blocks_admin_blocks_url)
85
95
  end
86
96
 
87
97
  private
@@ -95,7 +105,7 @@ module Spina
95
105
  end
96
106
 
97
107
  def set_tabs
98
- @tabs = %w[block_content block_settings]
108
+ @tabs = ["block_content", "block_settings"]
99
109
  end
100
110
 
101
111
  def block_params
@@ -12,18 +12,18 @@ module Spina
12
12
 
13
13
  def edit
14
14
  @category = Spina::Blocks::Category.find(params[:id])
15
- add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
16
- add_breadcrumb @category.label
15
+ add_breadcrumb(I18n.t("spina.blocks.title"), spina.blocks_admin_blocks_path, class: "text-gray-400")
16
+ add_breadcrumb(@category.label)
17
17
  end
18
18
 
19
19
  def update
20
20
  @category = Spina::Blocks::Category.find(params[:id])
21
21
  if @category.update(category_params)
22
- flash[:success] = I18n.t('spina.block_categories.saved')
23
- redirect_to spina.blocks_admin_blocks_url
22
+ flash[:success] = I18n.t("spina.block_categories.saved")
23
+ redirect_to(spina.blocks_admin_blocks_url)
24
24
  else
25
- flash.now[:error] = I18n.t('spina.block_categories.couldnt_be_saved')
26
- render :edit, status: :unprocessable_entity
25
+ flash.now[:error] = I18n.t("spina.block_categories.couldnt_be_saved")
26
+ render(:edit, status: :unprocessable_entity)
27
27
  end
28
28
  end
29
29
 
@@ -9,9 +9,9 @@ module Spina
9
9
  before_action :set_page
10
10
 
11
11
  def index
12
- add_breadcrumb I18n.t('spina.website.pages'), spina.admin_pages_path, class: 'text-gray-400'
13
- add_breadcrumb @page.title, spina.edit_admin_page_path(@page), class: 'text-gray-400'
14
- add_breadcrumb I18n.t('spina.page_blocks.title')
12
+ add_breadcrumb(I18n.t("spina.website.pages"), spina.admin_pages_path, class: "text-gray-400")
13
+ add_breadcrumb(@page.title, spina.edit_admin_page_path(@page), class: "text-gray-400")
14
+ add_breadcrumb(I18n.t("spina.page_blocks.title"))
15
15
 
16
16
  @page_blocks = @page.page_blocks.sorted.includes(:block)
17
17
  @available_blocks = Spina::Blocks::Block.active.sorted.where.not(id: @page.block_ids)
@@ -22,19 +22,19 @@ module Spina
22
22
  @page_block.position = @page.page_blocks.maximum(:position).to_i + 1
23
23
 
24
24
  if @page_block.save
25
- flash[:success] = I18n.t('spina.page_blocks.added')
25
+ flash[:success] = I18n.t("spina.page_blocks.added")
26
26
  else
27
- flash[:error] = I18n.t('spina.page_blocks.couldnt_be_added')
27
+ flash[:error] = I18n.t("spina.page_blocks.couldnt_be_added")
28
28
  end
29
29
 
30
- redirect_to spina.blocks_admin_page_page_blocks_url(page_id: @page.id)
30
+ redirect_to(spina.blocks_admin_page_page_blocks_url(page_id: @page.id))
31
31
  end
32
32
 
33
33
  def destroy
34
34
  @page_block = @page.page_blocks.find(params[:id])
35
35
  @page_block.destroy
36
- flash[:info] = I18n.t('spina.page_blocks.removed')
37
- redirect_to spina.blocks_admin_page_page_blocks_url(page_id: @page.id)
36
+ flash[:info] = I18n.t("spina.page_blocks.removed")
37
+ redirect_to(spina.blocks_admin_page_page_blocks_url(page_id: @page.id))
38
38
  end
39
39
 
40
40
  def sort
@@ -42,7 +42,7 @@ module Spina
42
42
  @page.page_blocks.where(id: id).update_all(position: index + 1)
43
43
  end
44
44
 
45
- flash.now[:info] = I18n.t('spina.page_blocks.sorting_saved')
45
+ flash.now[:info] = I18n.t("spina.page_blocks.sorting_saved")
46
46
  render_flash
47
47
  end
48
48
 
@@ -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,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
@@ -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,7 @@
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__") %>"
29
30
  data-field-name="<%= field_name %>">
30
31
 
31
32
  <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
@@ -78,6 +79,12 @@
78
79
  <span class="text-sm font-medium text-gray-800 truncate" data-role="title"></span>
79
80
  <span class="ml-2 text-xs text-gray-400" data-role="template-label"></span>
80
81
  </div>
82
+ <a class="ml-2 text-gray-300 hover:text-gray-600 transition-colors"
83
+ data-role="edit-link" href="" data-turbo-frame="modal" title="Edit">
84
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <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"/>
86
+ </svg>
87
+ </a>
81
88
  <button type="button" class="ml-2 text-gray-300 hover:text-red-500 transition-colors"
82
89
  data-action="block-collection#remove" data-block-id="" title="Remove">
83
90
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -6,7 +6,7 @@
6
6
  <div class="mt-1">
7
7
  <% blocks = Spina::Blocks::Block.active.sorted %>
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,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? %>
@@ -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,11 +1,12 @@
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
@@ -13,10 +14,10 @@ Spina::Engine.routes.draw do
13
14
  end
14
15
  end
15
16
 
16
- resources :categories, only: %i[index edit update]
17
+ resources :categories, only: [:index, :edit, :update]
17
18
 
18
19
  resources :pages, only: [] do
19
- resources :page_blocks, only: %i[index create destroy] do
20
+ resources :page_blocks, only: [:index, :create, :destroy] do
20
21
  collection do
21
22
  post :sort
22
23
  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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenameTitleToNameInSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ # For fresh installs: migration 2 already creates only `name`, no `title`.
6
+ return unless connection.column_exists?(:spina_blocks_blocks, :title)
7
+
8
+ # Edge case: both `title` and `name` exist (migration 2 ran with both
9
+ # columns but migration 4, which removed `name`, was never executed).
10
+ if connection.column_exists?(:spina_blocks_blocks, :name)
11
+ remove_index(:spina_blocks_blocks, :name) if connection.index_exists?(:spina_blocks_blocks, :name)
12
+ remove_column(:spina_blocks_blocks, :name, :string)
13
+ end
14
+
15
+ rename_column(:spina_blocks_blocks, :title, :name)
16
+ add_index(:spina_blocks_blocks, :name, unique: true)
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ class InstallGenerator < Rails::Generators::Base
6
+ desc "Install Spina Blocks plugin: copy migrations and run them"
7
+
8
+ def copy_migrations
9
+ rake("spina_blocks:install:migrations")
10
+ end
11
+
12
+ def run_migrations
13
+ rake("db:migrate")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -7,8 +7,8 @@ module Spina
7
7
 
8
8
  config.before_initialize do
9
9
  ::Spina::Plugin.register do |plugin|
10
- plugin.name = 'blocks'
11
- plugin.namespace = 'blocks'
10
+ plugin.name = "blocks"
11
+ plugin.namespace = "blocks"
12
12
  end
13
13
 
14
14
  ::Spina::Theme.class_eval do
@@ -26,55 +26,55 @@ module Spina
26
26
  end
27
27
  end
28
28
 
29
- initializer 'spina.blocks.append_migrations' do |app|
30
- unless app.root.to_s.match?(root.to_s)
31
- config.paths['db/migrate'].expanded.each do |expanded_path|
32
- app.config.paths['db/migrate'] << expanded_path
33
- end
34
- end
35
- end
36
-
37
- initializer 'spina.blocks.importmap', before: 'spina.blocks.register_parts' do
29
+ initializer "spina.blocks.importmap", before: "spina.blocks.register_parts" do
38
30
  Spina.config.importmap.draw do
39
- pin_all_from Spina::Blocks::Engine.root.join('app/assets/javascripts/spina/controllers'),
40
- under: 'controllers', to: 'spina/controllers'
31
+ pin_all_from Spina::Blocks::Engine.root.join("app/assets/javascripts/spina/controllers"),
32
+ under: "controllers",
33
+ to: "spina/controllers"
41
34
  end
42
35
  end
43
36
 
44
- initializer 'spina.blocks.assets.precompile' do |app|
45
- app.config.assets.precompile += %w[spina/controllers/block_collection_controller.js] if defined?(Sprockets)
37
+ initializer "spina.blocks.assets.precompile" do |app|
38
+ app.config.assets.precompile += ["spina/controllers/block_collection_controller.js"] if defined?(Sprockets)
46
39
  end
47
40
 
48
- initializer 'spina.blocks.register_parts' do
41
+ initializer "spina.blocks.register_parts" do
49
42
  config.to_prepare do
50
43
  ::Spina::Part.register(
51
44
  Spina::Parts::BlockReference,
52
- Spina::Parts::BlockCollection
45
+ Spina::Parts::BlockCollection,
53
46
  )
54
47
  end
55
48
  end
56
49
 
57
- initializer 'spina.blocks.extend_page' do
50
+ initializer "spina.blocks.extend_models" do
51
+ config.to_prepare do
52
+ Spina::Page.include(Spina::Blocks::PageExtension) unless Spina::Page < Spina::Blocks::PageExtension
53
+ Spina::Account.include(Spina::Blocks::AccountExtension) unless Spina::Account < Spina::Blocks::AccountExtension
54
+ end
55
+ end
56
+
57
+ initializer "spina.blocks.search" do
58
58
  config.to_prepare do
59
- Dir.glob(Spina::Blocks::Engine.root.join('app/overrides/**/*.rb')).sort.each do |override|
60
- load override
59
+ if defined?(Spina::Pro::Search)
60
+ Spina::Blocks::Block.include(Spina::Blocks::SearchExtension) unless Spina::Blocks::Block < Spina::Blocks::SearchExtension
61
61
  end
62
62
  end
63
63
  end
64
64
 
65
- initializer 'spina.blocks.tailwind_content' do
65
+ initializer "spina.blocks.tailwind_content" do
66
66
  ::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/views/**/*.*"
67
67
  ::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/helpers/**/*.*"
68
68
  ::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/assets/javascripts/**/*.js"
69
69
  end
70
70
 
71
- initializer 'spina.blocks.i18n' do
72
- config.i18n.load_path += Dir[Spina::Blocks::Engine.root.join('config', 'locales', '*.{rb,yml}')]
71
+ initializer "spina.blocks.i18n" do
72
+ config.i18n.load_path += Dir[Spina::Blocks::Engine.root.join("config", "locales", "*.{rb,yml}")]
73
73
  end
74
74
 
75
- initializer 'spina.blocks.helpers' do
75
+ initializer "spina.blocks.helpers" do
76
76
  config.to_prepare do
77
- ActionView::Base.include Spina::Blocks::BlocksHelper
77
+ ActionView::Base.include(Spina::Blocks::BlocksHelper)
78
78
  end
79
79
  end
80
80
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Spina
4
4
  module Blocks
5
- VERSION = '0.2.0'
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/spina-blocks.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spina'
4
- require 'spina/blocks/version'
5
- require 'spina/blocks/engine'
3
+ require "spina"
4
+ require "spina/blocks/version"
5
+ require "spina/blocks/engine"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spina-blocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Kanashchuk
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-03-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: spina
@@ -24,6 +23,62 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: database_cleaner-active_record
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: factory_bot_rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: pg
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec-rails
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
27
82
  description: A plugin for Spina CMS that adds reusable block components that can be
28
83
  assembled into pages.
29
84
  email:
@@ -39,34 +94,39 @@ files:
39
94
  - app/controllers/spina/blocks/admin/categories_controller.rb
40
95
  - app/controllers/spina/blocks/admin/page_blocks_controller.rb
41
96
  - app/helpers/spina/blocks/blocks_helper.rb
97
+ - app/models/spina/blocks/account_extension.rb
42
98
  - app/models/spina/blocks/application_record.rb
43
99
  - app/models/spina/blocks/block.rb
44
100
  - app/models/spina/blocks/category.rb
45
101
  - app/models/spina/blocks/page_block.rb
102
+ - app/models/spina/blocks/page_extension.rb
103
+ - app/models/spina/blocks/search_extension.rb
46
104
  - app/models/spina/parts/block_collection.rb
47
105
  - app/models/spina/parts/block_reference.rb
48
- - app/overrides/spina/account_override.rb
49
- - app/overrides/spina/page_override.rb
50
106
  - app/views/spina/admin/hooks/blocks/_website_secondary_navigation.html.erb
51
107
  - app/views/spina/admin/parts/block_collections/_form.html.erb
52
108
  - app/views/spina/admin/parts/block_references/_form.html.erb
109
+ - app/views/spina/admin/pro/search/results/_block.html.erb
53
110
  - app/views/spina/blocks/admin/blocks/_button_block_content.html.erb
54
111
  - app/views/spina/blocks/admin/blocks/_button_block_settings.html.erb
55
112
  - app/views/spina/blocks/admin/blocks/_form.html.erb
56
113
  - app/views/spina/blocks/admin/blocks/_form_block_content.html.erb
57
114
  - app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb
115
+ - app/views/spina/blocks/admin/blocks/_modal_form.html.erb
58
116
  - app/views/spina/blocks/admin/blocks/_new_block_form.html.erb
59
117
  - app/views/spina/blocks/admin/blocks/edit.html.erb
60
118
  - app/views/spina/blocks/admin/blocks/edit_content.html.erb
119
+ - app/views/spina/blocks/admin/blocks/edit_modal.html.erb
61
120
  - app/views/spina/blocks/admin/blocks/index.html.erb
62
121
  - app/views/spina/blocks/admin/blocks/new.html.erb
63
122
  - app/views/spina/blocks/admin/page_blocks/index.html.erb
64
123
  - config/locales/en.yml
65
124
  - config/routes.rb
66
- - db/migrate/20250101000001_create_spina_blocks_categories.rb
67
- - db/migrate/20250101000002_create_spina_blocks_blocks.rb
68
- - db/migrate/20250101000003_create_spina_blocks_page_blocks.rb
69
- - db/migrate/20250101000004_remove_name_from_spina_blocks_blocks.rb
125
+ - db/migrate/1_create_spina_blocks_categories.rb
126
+ - db/migrate/2_create_spina_blocks_blocks.rb
127
+ - db/migrate/3_create_spina_blocks_page_blocks.rb
128
+ - db/migrate/5_rename_title_to_name_in_spina_blocks_blocks.rb
129
+ - lib/generators/spina/blocks/install_generator.rb
70
130
  - lib/spina-blocks.rb
71
131
  - lib/spina/blocks/engine.rb
72
132
  - lib/spina/blocks/version.rb
@@ -75,7 +135,6 @@ licenses:
75
135
  - MIT
76
136
  metadata:
77
137
  rubygems_mfa_required: 'true'
78
- post_install_message:
79
138
  rdoc_options: []
80
139
  require_paths:
81
140
  - lib
@@ -83,15 +142,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
142
  requirements:
84
143
  - - ">="
85
144
  - !ruby/object:Gem::Version
86
- version: 2.7.0
145
+ version: 3.2.0
87
146
  required_rubygems_version: !ruby/object:Gem::Requirement
88
147
  requirements:
89
148
  - - ">="
90
149
  - !ruby/object:Gem::Version
91
150
  version: '0'
92
151
  requirements: []
93
- rubygems_version: 3.5.22
94
- signing_key:
152
+ rubygems_version: 3.6.9
95
153
  specification_version: 4
96
154
  summary: Block component library for Spina CMS
97
155
  test_files: []
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Spina::Account.class_eval do
4
- after_save :bootstrap_block_categories
5
-
6
- private
7
-
8
- def bootstrap_block_categories
9
- theme_config = Spina::Theme.find_by_name(theme)
10
- return unless theme_config
11
- return unless theme_config.respond_to?(:block_categories) && theme_config.block_categories.present?
12
-
13
- theme_config.block_categories.each_with_index do |category, index|
14
- Spina::Blocks::Category.where(name: category[:name])
15
- .first_or_create(label: category[:label])
16
- .update(label: category[:label], position: index)
17
- end
18
- end
19
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Spina::Page.class_eval do
4
- has_many :page_blocks, class_name: 'Spina::Blocks::PageBlock', foreign_key: :page_id, dependent: :destroy
5
- has_many :blocks, through: :page_blocks, class_name: 'Spina::Blocks::Block'
6
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateSpinaBlocksCategories < ActiveRecord::Migration[7.0]
4
- def change
5
- if legacy_migration_applied?
6
- cleanup_legacy_version!
7
- return
8
- end
9
-
10
- create_table :spina_blocks_categories do |t|
11
- t.string :name, null: false
12
- t.string :label, null: false
13
- t.integer :position, default: 0
14
-
15
- t.timestamps
16
- end
17
-
18
- add_index :spina_blocks_categories, :name, unique: true
19
- end
20
-
21
- private
22
-
23
- def legacy_migration_applied?
24
- ActiveRecord::Base.connection
25
- .select_value("SELECT 1 FROM schema_migrations WHERE version = '1'")
26
- end
27
-
28
- def cleanup_legacy_version!
29
- execute "DELETE FROM schema_migrations WHERE version = '1'"
30
- end
31
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
4
- def change
5
- if legacy_migration_applied?
6
- cleanup_legacy_version!
7
- return
8
- end
9
-
10
- create_table :spina_blocks_blocks do |t|
11
- t.string :title, null: false
12
- t.string :name, null: false
13
- t.string :block_template, null: false
14
- t.references :category, foreign_key: { to_table: :spina_blocks_categories }, null: true
15
- t.integer :position, default: 0
16
- t.boolean :active, default: true
17
- t.json :json_attributes
18
-
19
- t.timestamps
20
- end
21
-
22
- add_index :spina_blocks_blocks, :name, unique: true
23
- end
24
-
25
- private
26
-
27
- def legacy_migration_applied?
28
- ActiveRecord::Base.connection
29
- .select_value("SELECT 1 FROM schema_migrations WHERE version = '2'")
30
- end
31
-
32
- def cleanup_legacy_version!
33
- execute "DELETE FROM schema_migrations WHERE version = '2'"
34
- end
35
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateSpinaBlocksPageBlocks < ActiveRecord::Migration[7.0]
4
- def change
5
- if legacy_migration_applied?
6
- cleanup_legacy_version!
7
- return
8
- end
9
-
10
- create_table :spina_blocks_page_blocks do |t|
11
- t.references :page, null: false, foreign_key: { to_table: :spina_pages }
12
- t.references :block, null: false, foreign_key: { to_table: :spina_blocks_blocks }
13
- t.integer :position, default: 0
14
-
15
- t.timestamps
16
- end
17
-
18
- add_index :spina_blocks_page_blocks, %i[page_id block_id], unique: true
19
- end
20
-
21
- private
22
-
23
- def legacy_migration_applied?
24
- ActiveRecord::Base.connection
25
- .select_value("SELECT 1 FROM schema_migrations WHERE version = '3'")
26
- end
27
-
28
- def cleanup_legacy_version!
29
- execute "DELETE FROM schema_migrations WHERE version = '3'"
30
- end
31
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RemoveNameFromSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
4
- def change
5
- if legacy_migration_applied?
6
- cleanup_legacy_version!
7
- return
8
- end
9
-
10
- remove_index :spina_blocks_blocks, :name
11
- remove_column :spina_blocks_blocks, :name, :string
12
- end
13
-
14
- private
15
-
16
- def legacy_migration_applied?
17
- ActiveRecord::Base.connection
18
- .select_value("SELECT 1 FROM schema_migrations WHERE version = '4'")
19
- end
20
-
21
- def cleanup_legacy_version!
22
- execute "DELETE FROM schema_migrations WHERE version = '4'"
23
- end
24
- end