spina-blocks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +154 -0
- data/Rakefile +4 -0
- data/app/controllers/spina/blocks/admin/blocks_controller.rb +105 -0
- data/app/controllers/spina/blocks/admin/categories_controller.rb +38 -0
- data/app/controllers/spina/blocks/admin/page_blocks_controller.rb +61 -0
- data/app/helpers/spina/blocks/blocks_helper.rb +57 -0
- data/app/models/spina/blocks/application_record.rb +9 -0
- data/app/models/spina/blocks/block.rb +26 -0
- data/app/models/spina/blocks/category.rb +18 -0
- data/app/models/spina/blocks/page_block.rb +14 -0
- data/app/models/spina/parts/block_collection.rb +18 -0
- data/app/models/spina/parts/block_reference.rb +15 -0
- data/app/overrides/spina/account_override.rb +19 -0
- data/app/overrides/spina/page_override.rb +6 -0
- data/app/views/spina/admin/hooks/blocks/_website_secondary_navigation.html.erb +5 -0
- data/app/views/spina/admin/parts/block_collections/_form.html.erb +24 -0
- data/app/views/spina/admin/parts/block_references/_form.html.erb +13 -0
- data/app/views/spina/blocks/admin/blocks/_button_block_content.html.erb +4 -0
- data/app/views/spina/blocks/admin/blocks/_button_block_settings.html.erb +4 -0
- data/app/views/spina/blocks/admin/blocks/_form.html.erb +53 -0
- data/app/views/spina/blocks/admin/blocks/_form_block_content.html.erb +8 -0
- data/app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb +24 -0
- data/app/views/spina/blocks/admin/blocks/_new_block_form.html.erb +29 -0
- data/app/views/spina/blocks/admin/blocks/edit.html.erb +1 -0
- data/app/views/spina/blocks/admin/blocks/edit_content.html.erb +11 -0
- data/app/views/spina/blocks/admin/blocks/index.html.erb +81 -0
- data/app/views/spina/blocks/admin/blocks/new.html.erb +3 -0
- data/app/views/spina/blocks/admin/page_blocks/index.html.erb +90 -0
- data/config/locales/en.yml +37 -0
- data/config/routes.rb +27 -0
- data/db/migrate/1_create_spina_blocks_categories.rb +15 -0
- data/db/migrate/2_create_spina_blocks_blocks.rb +19 -0
- data/db/migrate/3_create_spina_blocks_page_blocks.rb +15 -0
- data/db/migrate/4_remove_name_from_spina_blocks_blocks.rb +8 -0
- data/lib/spina/blocks/engine.rb +70 -0
- data/lib/spina/blocks/version.rb +7 -0
- data/lib/spina-blocks.rb +5 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f004335deec2f8669d819540c01d8a58786f016b2a13de4d037da05f18301442
|
|
4
|
+
data.tar.gz: 623b17f09b53f84b957c9b112675951df21bc73d9de1a4719a3f8ed91bba7519
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e6d2c8a6d51c3c3393a2fb9511472272f0af777e150a6fab6c7fb6409a8a1450a87c6f01a98323272850a2896958a2ead13c20d1d9831cc4f6edfeb0daf901d4
|
|
7
|
+
data.tar.gz: c1a70f6285ba5888a6507fecdd0a432c8d23a74d174465cc6569a6a3a18a5f9b8efb44c1aaa375cea3cfb38fb95a068553be037676b68efa633e43850f0f2b40
|
data/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Spina Blocks
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "spina-blocks"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
rails db:migrate
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
### Theme setup
|
|
23
|
+
|
|
24
|
+
In your theme initializer, add block templates and categories:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# config/initializers/themes/my_theme.rb
|
|
28
|
+
Spina::Theme.register do |theme|
|
|
29
|
+
theme.name = "my_theme"
|
|
30
|
+
theme.title = "My Theme"
|
|
31
|
+
|
|
32
|
+
# Enable the blocks plugin
|
|
33
|
+
theme.plugins = ["blocks"]
|
|
34
|
+
|
|
35
|
+
# Define all available parts (shared between pages and blocks)
|
|
36
|
+
theme.parts = [
|
|
37
|
+
{ name: "headline", title: "Headline", part_type: "Spina::Parts::Line" },
|
|
38
|
+
{ name: "body", title: "Body", part_type: "Spina::Parts::Text" },
|
|
39
|
+
{ name: "image", title: "Image", part_type: "Spina::Parts::Image" },
|
|
40
|
+
{ name: "button_text", title: "Button Text", part_type: "Spina::Parts::Line" },
|
|
41
|
+
{ name: "button_url", title: "Button URL", part_type: "Spina::Parts::Line" }
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Block categories (for organizing blocks in the library)
|
|
45
|
+
theme.block_categories = [
|
|
46
|
+
{ name: "heroes", label: "Heroes" },
|
|
47
|
+
{ name: "features", label: "Features" },
|
|
48
|
+
{ name: "cta", label: "Call to Action" }
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Block templates (which parts each block type uses)
|
|
52
|
+
theme.block_templates = [
|
|
53
|
+
{
|
|
54
|
+
name: "hero",
|
|
55
|
+
title: "Hero Section",
|
|
56
|
+
description: "Full-width hero with headline and image",
|
|
57
|
+
parts: ["headline", "body", "image", "button_text", "button_url"]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "cta_banner",
|
|
61
|
+
title: "CTA Banner",
|
|
62
|
+
description: "Call to action banner",
|
|
63
|
+
parts: ["headline", "body", "button_text", "button_url"]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# Page templates (can use BlockCollection or BlockReference parts)
|
|
68
|
+
theme.view_templates = [
|
|
69
|
+
{
|
|
70
|
+
name: "homepage",
|
|
71
|
+
title: "Homepage",
|
|
72
|
+
parts: ["headline", "page_blocks"]
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Block view templates
|
|
79
|
+
|
|
80
|
+
Create partials for each block template:
|
|
81
|
+
|
|
82
|
+
```erb
|
|
83
|
+
<%# app/views/my_theme/blocks/_hero.html.erb %>
|
|
84
|
+
<section class="hero">
|
|
85
|
+
<h1><%= block_content(block, :headline) %></h1>
|
|
86
|
+
<div><%= block.content(:body) %></div>
|
|
87
|
+
</section>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Using blocks on pages
|
|
91
|
+
|
|
92
|
+
#### Option 1: Page assembled from blocks (via PageBlocks)
|
|
93
|
+
|
|
94
|
+
In your page template, render all attached blocks:
|
|
95
|
+
|
|
96
|
+
```erb
|
|
97
|
+
<%# app/views/my_theme/pages/homepage.html.erb %>
|
|
98
|
+
<%= render_blocks %>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Manage which blocks appear on a page via Admin > Blocks > (select page).
|
|
102
|
+
|
|
103
|
+
#### Option 2: Block as a part type
|
|
104
|
+
|
|
105
|
+
Use `Spina::Parts::BlockReference` for a single block or `Spina::Parts::BlockCollection` for multiple blocks in your theme parts:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
theme.parts = [
|
|
109
|
+
{ name: "hero_block", title: "Hero Block", part_type: "Spina::Parts::BlockReference" },
|
|
110
|
+
{ name: "page_blocks", title: "Page Blocks", part_type: "Spina::Parts::BlockCollection" }
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then in your template:
|
|
115
|
+
|
|
116
|
+
```erb
|
|
117
|
+
<%# Single block reference %>
|
|
118
|
+
<%= render_block(content(:hero_block)) %>
|
|
119
|
+
|
|
120
|
+
<%# Block collection %>
|
|
121
|
+
<% content(:page_blocks)&.each do |block| %>
|
|
122
|
+
<%= render_block(block) %>
|
|
123
|
+
<% end %>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Models
|
|
127
|
+
|
|
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 |
|
|
132
|
+
| `Spina::Blocks::PageBlock` | Join model linking blocks to pages (with position) |
|
|
133
|
+
|
|
134
|
+
## Admin interface
|
|
135
|
+
|
|
136
|
+
The plugin adds:
|
|
137
|
+
|
|
138
|
+
- **Blocks** link in the Content section of the admin sidebar
|
|
139
|
+
- Block library with category tabs for filtering
|
|
140
|
+
- Block editor with content fields (same as page editor)
|
|
141
|
+
- Page Blocks management page (per-page block assignment and ordering)
|
|
142
|
+
|
|
143
|
+
## Helper methods
|
|
144
|
+
|
|
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 |
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
module Admin
|
|
6
|
+
class BlocksController < ::Spina::Admin::AdminController
|
|
7
|
+
admin_section :content
|
|
8
|
+
|
|
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]
|
|
12
|
+
|
|
13
|
+
helper ::Spina::Admin::PagesHelper
|
|
14
|
+
|
|
15
|
+
def index
|
|
16
|
+
add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path
|
|
17
|
+
|
|
18
|
+
@block_templates = current_theme.try(:block_templates) || []
|
|
19
|
+
|
|
20
|
+
if params[:block_template].present?
|
|
21
|
+
@current_block_template = params[:block_template]
|
|
22
|
+
@blocks = Spina::Blocks::Block.where(block_template: @current_block_template).sorted
|
|
23
|
+
else
|
|
24
|
+
@current_block_template = nil
|
|
25
|
+
@blocks = Spina::Blocks::Block.sorted
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def new
|
|
30
|
+
@block = Spina::Blocks::Block.new(block_template: params[:block_template])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create
|
|
34
|
+
@block = Spina::Blocks::Block.new(block_params)
|
|
35
|
+
if @block.save
|
|
36
|
+
redirect_to spina.edit_blocks_admin_block_url(@block)
|
|
37
|
+
else
|
|
38
|
+
render turbo_stream: turbo_stream.update(
|
|
39
|
+
helpers.dom_id(@block, :new_block_form),
|
|
40
|
+
partial: 'new_block_form'
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def edit
|
|
46
|
+
add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
|
|
47
|
+
add_breadcrumb @block.title
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def edit_content
|
|
51
|
+
@parts = current_theme.block_templates&.find do |bt|
|
|
52
|
+
bt[:name].to_s == @block.block_template.to_s
|
|
53
|
+
end&.dig(:parts) || []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def update
|
|
57
|
+
Mobility.locale = @locale
|
|
58
|
+
if @block.update(block_params)
|
|
59
|
+
flash[:success] = I18n.t('spina.blocks.saved')
|
|
60
|
+
redirect_to spina.edit_blocks_admin_block_url(@block, params: { locale: @locale })
|
|
61
|
+
else
|
|
62
|
+
add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
|
|
63
|
+
Mobility.locale = I18n.locale
|
|
64
|
+
add_breadcrumb @block.title
|
|
65
|
+
flash.now[:error] = I18n.t('spina.blocks.couldnt_be_saved')
|
|
66
|
+
render :edit, status: :unprocessable_entity
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def sort
|
|
71
|
+
params[:ids].each.with_index do |id, index|
|
|
72
|
+
Spina::Blocks::Block.where(id: id).update_all(position: index + 1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
flash.now[:info] = I18n.t('spina.blocks.sorting_saved')
|
|
76
|
+
render_flash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def destroy
|
|
80
|
+
flash[:info] = I18n.t('spina.blocks.deleted')
|
|
81
|
+
@block.destroy
|
|
82
|
+
redirect_to spina.blocks_admin_blocks_url
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def set_locale
|
|
88
|
+
@locale = params[:locale] || I18n.default_locale
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def set_block
|
|
92
|
+
@block = Spina::Blocks::Block.find(params[:id])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_tabs
|
|
96
|
+
@tabs = %w[block_content block_settings]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def block_params
|
|
100
|
+
params.require(:block).permit!
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
module Admin
|
|
6
|
+
class CategoriesController < ::Spina::Admin::AdminController
|
|
7
|
+
admin_section :content
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@categories = Spina::Blocks::Category.sorted
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def edit
|
|
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
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update
|
|
20
|
+
@category = Spina::Blocks::Category.find(params[:id])
|
|
21
|
+
if @category.update(category_params)
|
|
22
|
+
flash[:success] = I18n.t('spina.block_categories.saved')
|
|
23
|
+
redirect_to spina.blocks_admin_blocks_url
|
|
24
|
+
else
|
|
25
|
+
flash.now[:error] = I18n.t('spina.block_categories.couldnt_be_saved')
|
|
26
|
+
render :edit, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def category_params
|
|
33
|
+
params.require(:category).permit(:name, :label, :position)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
module Admin
|
|
6
|
+
class PageBlocksController < ::Spina::Admin::AdminController
|
|
7
|
+
admin_section :content
|
|
8
|
+
|
|
9
|
+
before_action :set_page
|
|
10
|
+
|
|
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')
|
|
15
|
+
|
|
16
|
+
@page_blocks = @page.page_blocks.sorted.includes(:block)
|
|
17
|
+
@available_blocks = Spina::Blocks::Block.active.sorted.where.not(id: @page.block_ids)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@page_block = @page.page_blocks.build(page_block_params)
|
|
22
|
+
@page_block.position = @page.page_blocks.maximum(:position).to_i + 1
|
|
23
|
+
|
|
24
|
+
if @page_block.save
|
|
25
|
+
flash[:success] = I18n.t('spina.page_blocks.added')
|
|
26
|
+
else
|
|
27
|
+
flash[:error] = I18n.t('spina.page_blocks.couldnt_be_added')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
redirect_to spina.blocks_admin_page_page_blocks_url(page_id: @page.id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy
|
|
34
|
+
@page_block = @page.page_blocks.find(params[:id])
|
|
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)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def sort
|
|
41
|
+
params[:ids].each.with_index do |id, index|
|
|
42
|
+
@page.page_blocks.where(id: id).update_all(position: index + 1)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
flash.now[:info] = I18n.t('spina.page_blocks.sorting_saved')
|
|
46
|
+
render_flash
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def set_page
|
|
52
|
+
@page = ::Spina::Page.find(params[:page_id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def page_block_params
|
|
56
|
+
params.require(:page_block).permit(:block_id)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
module BlocksHelper
|
|
6
|
+
# Render all blocks attached to the current page via PageBlocks
|
|
7
|
+
# Usage in a page template: <%= render_blocks %>
|
|
8
|
+
def render_blocks(page = nil)
|
|
9
|
+
page ||= current_page
|
|
10
|
+
return unless page.respond_to?(:page_blocks)
|
|
11
|
+
|
|
12
|
+
page.page_blocks.sorted.includes(:block).each do |page_block|
|
|
13
|
+
block = page_block.block
|
|
14
|
+
next unless block&.active?
|
|
15
|
+
|
|
16
|
+
concat render_block(block)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Render a single block using its block_template partial
|
|
21
|
+
# Usage: <%= render_block(some_block) %>
|
|
22
|
+
def render_block(block)
|
|
23
|
+
return unless block&.active?
|
|
24
|
+
|
|
25
|
+
current_spina_theme = Spina::Current.theme || current_theme
|
|
26
|
+
theme_name = current_spina_theme.name.parameterize.underscore
|
|
27
|
+
|
|
28
|
+
partial_path = "#{theme_name}/blocks/#{block.block_template}"
|
|
29
|
+
|
|
30
|
+
if lookup_context.exists?(partial_path, [], true)
|
|
31
|
+
render partial: partial_path, locals: { block: block }
|
|
32
|
+
else
|
|
33
|
+
render_block_fallback(block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Access a block's content like page content
|
|
38
|
+
# Usage in block partial: block_content(block, :headline)
|
|
39
|
+
def block_content(block, part_name = nil)
|
|
40
|
+
block.content(part_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if a block has content for a given part
|
|
44
|
+
def block_has_content?(block, part_name)
|
|
45
|
+
block.has_content?(part_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def render_block_fallback(block)
|
|
51
|
+
content_tag(:div, class: "spina-block spina-block--#{block.block_template}") do
|
|
52
|
+
content_tag(:p, block.title, class: 'spina-block__title')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
class Block < ApplicationRecord
|
|
6
|
+
include AttrJson::Record
|
|
7
|
+
include AttrJson::NestedAttributes
|
|
8
|
+
include Spina::Partable
|
|
9
|
+
include Spina::TranslatedContent
|
|
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'
|
|
14
|
+
|
|
15
|
+
validates :title, presence: true
|
|
16
|
+
validates :block_template, presence: true
|
|
17
|
+
|
|
18
|
+
scope :active, -> { where(active: true) }
|
|
19
|
+
scope :sorted, -> { order(:position) }
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
title
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
class Category < ApplicationRecord
|
|
6
|
+
has_many :blocks, class_name: 'Spina::Blocks::Block', foreign_key: :category_id, dependent: :nullify
|
|
7
|
+
|
|
8
|
+
validates :name, presence: true, uniqueness: true
|
|
9
|
+
validates :label, presence: true
|
|
10
|
+
|
|
11
|
+
scope :sorted, -> { order(:position) }
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
label
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
class PageBlock < ApplicationRecord
|
|
6
|
+
belongs_to :page, class_name: 'Spina::Page'
|
|
7
|
+
belongs_to :block, class_name: 'Spina::Blocks::Block'
|
|
8
|
+
|
|
9
|
+
validates :block_id, uniqueness: { scope: :page_id }
|
|
10
|
+
|
|
11
|
+
scope :sorted, -> { order(:position) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Parts
|
|
5
|
+
class BlockCollection < Base
|
|
6
|
+
attr_json :block_ids, :integer, array: true, default: -> { [] }
|
|
7
|
+
|
|
8
|
+
attr_accessor :options
|
|
9
|
+
|
|
10
|
+
def content
|
|
11
|
+
return [] if block_ids.blank?
|
|
12
|
+
|
|
13
|
+
blocks_by_id = ::Spina::Blocks::Block.active.where(id: block_ids).index_by(&:id)
|
|
14
|
+
block_ids.filter_map { |id| blocks_by_id[id] }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Parts
|
|
5
|
+
class BlockReference < Base
|
|
6
|
+
attr_json :block_id, :integer, default: nil
|
|
7
|
+
|
|
8
|
+
attr_accessor :options
|
|
9
|
+
|
|
10
|
+
def content
|
|
11
|
+
::Spina::Blocks::Block.active.find_by(id: block_id)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div class="mt-6">
|
|
2
|
+
<label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
|
|
3
|
+
<% if f.object.hint.present? %>
|
|
4
|
+
<div class="text-gray-400 text-sm"><%= f.object.hint %></div>
|
|
5
|
+
<% end %>
|
|
6
|
+
<div class="mt-1">
|
|
7
|
+
<% blocks = Spina::Blocks::Block.active.sorted %>
|
|
8
|
+
<% selected_ids = f.object.block_ids || [] %>
|
|
9
|
+
<div class="space-y-2">
|
|
10
|
+
<% blocks.each do |block| %>
|
|
11
|
+
<label class="flex items-center space-x-2 cursor-pointer">
|
|
12
|
+
<input type="checkbox"
|
|
13
|
+
name="<%= f.object_name %>[block_ids][]"
|
|
14
|
+
value="<%= block.id %>"
|
|
15
|
+
<%= 'checked' if selected_ids.include?(block.id) %>
|
|
16
|
+
class="form-checkbox rounded text-spina border-gray-300">
|
|
17
|
+
<span class="text-sm text-gray-700"><%= block.title %></span>
|
|
18
|
+
<span class="text-xs text-gray-400">(<%= block.block_template %>)</span>
|
|
19
|
+
</label>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<input type="hidden" name="<%= f.object_name %>[block_ids][]" value="">
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="mt-6">
|
|
2
|
+
<label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
|
|
3
|
+
<% if f.object.hint.present? %>
|
|
4
|
+
<div class="text-gray-400 text-sm"><%= f.object.hint %></div>
|
|
5
|
+
<% end %>
|
|
6
|
+
<div class="mt-1">
|
|
7
|
+
<% blocks = Spina::Blocks::Block.active.sorted %>
|
|
8
|
+
<%= f.select :block_id,
|
|
9
|
+
blocks.map { |b| [b.title, b.id] },
|
|
10
|
+
{include_blank: t('spina.blocks.select_block')},
|
|
11
|
+
class: "form-select text-sm w-full shadow-xs" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div data-controller="tabs" data-tabs-active="cursor-default text-gray-900 bg-spina-dark/10" data-tabs-inactive="cursor-pointer bg-transparent text-gray-400 border-transparent">
|
|
2
|
+
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
|
3
|
+
<% header.with_actions do %>
|
|
4
|
+
<%= render Spina::UserInterface::TranslationsComponent.new(@block, label: @locale.to_s.upcase) %>
|
|
5
|
+
|
|
6
|
+
<div class="relative" data-controller="reveal" data-reveal-away-value>
|
|
7
|
+
<button type="button" class="btn btn-default px-3" data-action="reveal#toggle">
|
|
8
|
+
<svg class="w-5 h-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
9
|
+
<path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
|
|
10
|
+
</svg>
|
|
11
|
+
</button>
|
|
12
|
+
|
|
13
|
+
<div hidden data-reveal data-transition class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg border border-gray-200 z-30">
|
|
14
|
+
<div class="rounded-md bg-white shadow-xs py-1">
|
|
15
|
+
<%= button_to t('spina.blocks.delete'),
|
|
16
|
+
spina.blocks_admin_block_path(@block),
|
|
17
|
+
method: :delete,
|
|
18
|
+
class: "block w-full text-left px-4 py-2 text-sm leading-5 font-medium text-red-500 cursor-pointer bg-white hover:bg-red-100/50 hover:text-red-500",
|
|
19
|
+
form: {data: {controller: "confirm", confirm_message: t('spina.blocks.delete_confirmation')}} %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<button type="submit" form="<%= dom_id(@block) %>" class="btn btn-primary" data-controller="button hotkeys" data-hotkeys="command+s, ctrl+s" data-hotkeys-target="button" data-action="button#loading" data-loading-message="Saving...">
|
|
25
|
+
<%= heroicon('check', style: :mini, class: 'w-5 h-5 -ml-1 mr-1') %>
|
|
26
|
+
<%= t('spina.blocks.save') %>
|
|
27
|
+
</button>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<% header.with_navigation do %>
|
|
31
|
+
<nav class="-mb-3 mt-4">
|
|
32
|
+
<ul class="inline-flex w-auto rounded-md bg-white">
|
|
33
|
+
<% @tabs.each do |tab_name| %>
|
|
34
|
+
<%= render partial: "button_#{tab_name}" %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</ul>
|
|
37
|
+
</nav>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<div class="p-4 md:p-8">
|
|
42
|
+
<%= form_with model: @block, url: spina.blocks_admin_block_path(@block), id: dom_id(@block) do |f| %>
|
|
43
|
+
<%= hidden_field_tag :locale, @locale %>
|
|
44
|
+
<% Mobility.with_locale(@locale) do %>
|
|
45
|
+
<% @tabs.each_with_index do |tab_name, idx| %>
|
|
46
|
+
<div data-tabs-target="pane" id="<%= tab_name %>" <%= 'hidden' unless idx.zero? %>>
|
|
47
|
+
<%= render partial: "form_#{tab_name}", locals: {f: f} %>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="max-w-5xl">
|
|
2
|
+
<label class="font-medium text-gray-700">
|
|
3
|
+
<%= Spina::Blocks::Block.human_attribute_name(:title) %>
|
|
4
|
+
</label>
|
|
5
|
+
<%= render Spina::Forms::TextFieldComponent.new(f, :title, size: "lg") %>
|
|
6
|
+
<turbo-frame id="block_content" src="<%= spina.edit_content_blocks_admin_block_path(f.object, locale: @locale) %>">
|
|
7
|
+
</turbo-frame>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div class="max-w-5xl grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6">
|
|
2
|
+
<div class="col-span-1 mt-6 md:mt-0">
|
|
3
|
+
<label class="block text-sm font-medium leading-5 text-gray-700">
|
|
4
|
+
<%= Spina::Blocks::Block.human_attribute_name(:block_template) %>
|
|
5
|
+
</label>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="col-span-2 mt-1">
|
|
9
|
+
<span class="text-sm text-gray-700"><%= @block.block_template %></span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="col-span-1 mt-6 md:mt-0">
|
|
13
|
+
<label class="block text-sm font-medium leading-5 text-gray-700">
|
|
14
|
+
<%= Spina::Blocks::Block.human_attribute_name(:active) %>
|
|
15
|
+
</label>
|
|
16
|
+
<div class="text-sm leading-5">
|
|
17
|
+
<p class="text-gray-500"><%= t('spina.blocks.active_description') %></p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="col-span-2 mt-1">
|
|
22
|
+
<%= render Spina::Forms::SwitchComponent.new(f, :active) %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%= turbo_frame_tag dom_id(@block, :new_block_form) do %>
|
|
2
|
+
<%= form_with model: @block, url: spina.blocks_admin_blocks_path, data: {turbo_frame: "_top"} do |f| %>
|
|
3
|
+
<%= f.hidden_field :block_template %>
|
|
4
|
+
|
|
5
|
+
<div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
6
|
+
<div class="sm:flex sm:items-start">
|
|
7
|
+
<div class="w-full">
|
|
8
|
+
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
|
9
|
+
<%= t('spina.blocks.new') %>
|
|
10
|
+
<span class="text-gray-400 text-sm">(<%= f.object.block_template %>)</span>
|
|
11
|
+
</h3>
|
|
12
|
+
|
|
13
|
+
<div class="mt-3">
|
|
14
|
+
<%= render Spina::Forms::TextFieldComponent.new(f, :title, size: 'lg', autofocus: true) %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="px-4 pb-5 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
20
|
+
<button type="submit" class="btn btn-primary w-full sm:w-auto sm:ml-2">
|
|
21
|
+
<%= t('spina.blocks.create') %>
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
<button type="button" class="btn btn-default w-full sm:w-auto mt-2 sm:mt-0" data-action="modal#close">
|
|
25
|
+
<%= t('spina.ui.cancel') %>
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'form' %>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<turbo-frame id="block_content">
|
|
2
|
+
<%= fields_for(@block) do |f| %>
|
|
3
|
+
<% f.fields_for "#{@locale}_content".to_sym, build_parts(f.object, @parts) do |ff| %>
|
|
4
|
+
<%= ff.hidden_field :type, value: ff.object.class %>
|
|
5
|
+
<%= ff.hidden_field :name %>
|
|
6
|
+
<%= ff.hidden_field :title %>
|
|
7
|
+
|
|
8
|
+
<%= render "spina/admin/parts/#{parts_partial_namespace(ff.object.class.to_s)}/form", f: ff %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</turbo-frame>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
|
2
|
+
<% header.with_actions do %>
|
|
3
|
+
<% block_templates = current_theme.try(:block_templates) || [] %>
|
|
4
|
+
<% if block_templates.any? %>
|
|
5
|
+
<div class="relative" data-controller="reveal" data-reveal-away-value>
|
|
6
|
+
<button type="button" class="btn btn-primary" data-action="reveal#toggle">
|
|
7
|
+
<%= heroicon('plus', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
|
|
8
|
+
<%= t('spina.blocks.new') %>
|
|
9
|
+
</button>
|
|
10
|
+
|
|
11
|
+
<div hidden data-reveal data-transition class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg border border-gray-200 z-30">
|
|
12
|
+
<div class="rounded-md bg-white shadow-xs py-1">
|
|
13
|
+
<% block_templates.each do |bt| %>
|
|
14
|
+
<%= link_to spina.new_blocks_admin_block_path(block_template: bt[:name]),
|
|
15
|
+
class: "block px-4 py-2 text-sm font-medium leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
|
16
|
+
data: {turbo_frame: "modal", action: "reveal#hide"} do %>
|
|
17
|
+
<%= bt[:title] %>
|
|
18
|
+
<% if bt[:description].present? %>
|
|
19
|
+
<span class="text-gray-400 text-xs block"><%= bt[:description] %></span>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<% header.with_navigation do %>
|
|
30
|
+
<nav class="-mb-1 md:-mb-3 mt-4">
|
|
31
|
+
<ul class="inline-flex flex-wrap w-auto rounded-md bg-white">
|
|
32
|
+
<%= render Spina::UserInterface::TabLinkComponent.new(spina.blocks_admin_blocks_path, active: @current_block_template.nil?) do %>
|
|
33
|
+
<%= heroicon('squares-2x2', class: 'h-4 w-4 mr-1 -ml-1 opacity-75') %>
|
|
34
|
+
<%= t('spina.blocks.all') %>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<% @block_templates.each do |bt| %>
|
|
38
|
+
<%= render Spina::UserInterface::TabLinkComponent.new(
|
|
39
|
+
spina.blocks_admin_blocks_path(block_template: bt[:name]),
|
|
40
|
+
active: @current_block_template == bt[:name].to_s) do %>
|
|
41
|
+
<%= bt[:title] %>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% end %>
|
|
44
|
+
</ul>
|
|
45
|
+
</nav>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% end %>
|
|
48
|
+
|
|
49
|
+
<% if @blocks.any? %>
|
|
50
|
+
<div class="my-6 md:m-8 bg-white md:rounded-lg border-l-0 border-r-0 border md:border-r md:border-l border-gray-200 border-b-0 shadow-xs">
|
|
51
|
+
<div data-controller="sortable" data-sortable-url="<%= spina.sort_blocks_admin_blocks_path %>">
|
|
52
|
+
<form data-sortable-target="form" method="post" action="<%= spina.sort_blocks_admin_blocks_path %>">
|
|
53
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
54
|
+
</form>
|
|
55
|
+
<ul data-sortable-target="list">
|
|
56
|
+
<% @blocks.each do |block| %>
|
|
57
|
+
<li class="border-b border-gray-200 flex items-center" data-id="<%= block.id %>">
|
|
58
|
+
<div class="px-4 py-1 cursor-move text-gray-300 hover:text-gray-500" data-sortable-handle>
|
|
59
|
+
<%= heroicon('bars-3', style: :mini, class: 'w-5 h-5') %>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex-1 py-3 pr-4">
|
|
62
|
+
<%= link_to spina.edit_blocks_admin_block_path(block), class: "block" do %>
|
|
63
|
+
<div class="text-gray-900 font-medium text-sm"><%= block.title %></div>
|
|
64
|
+
<div class="text-gray-400 text-xs">
|
|
65
|
+
<%= block.block_template %>
|
|
66
|
+
<% unless block.active? %>
|
|
67
|
+
· <span class="text-red-400"><%= t('spina.blocks.inactive') %></span>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
</li>
|
|
73
|
+
<% end %>
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<% else %>
|
|
78
|
+
<div class="m-8 my-6 italic text-gray-400">
|
|
79
|
+
<%= t('spina.blocks.no_blocks_yet') %>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
|
2
|
+
<% header.with_actions do %>
|
|
3
|
+
<% if @available_blocks.any? %>
|
|
4
|
+
<div class="relative" data-controller="reveal" data-reveal-away-value>
|
|
5
|
+
<button type="button" class="btn btn-primary" data-action="reveal#toggle">
|
|
6
|
+
<%= heroicon('plus', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
|
|
7
|
+
<%= t('spina.page_blocks.add_block') %>
|
|
8
|
+
</button>
|
|
9
|
+
|
|
10
|
+
<div hidden data-reveal data-transition class="origin-top-right absolute right-0 mt-2 w-72 max-h-96 overflow-y-auto rounded-md shadow-lg border border-gray-200 z-30">
|
|
11
|
+
<div class="rounded-md bg-white shadow-xs py-1">
|
|
12
|
+
<% Spina::Blocks::Category.sorted.each do |category| %>
|
|
13
|
+
<% category_blocks = @available_blocks.where(category: category) %>
|
|
14
|
+
<% if category_blocks.any? %>
|
|
15
|
+
<div class="px-4 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider mt-1"><%= category.label %></div>
|
|
16
|
+
<% category_blocks.each do |block| %>
|
|
17
|
+
<%= button_to spina.blocks_admin_page_page_blocks_path(page_id: @page.id),
|
|
18
|
+
params: {page_block: {block_id: block.id}},
|
|
19
|
+
method: :post,
|
|
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>
|
|
22
|
+
<div class="text-xs text-gray-400"><%= block.block_template %></div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<% uncategorized = @available_blocks.where(category_id: nil) %>
|
|
29
|
+
<% if uncategorized.any? %>
|
|
30
|
+
<div class="px-4 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider mt-1"><%= t('spina.blocks.uncategorized') %></div>
|
|
31
|
+
<% uncategorized.each do |block| %>
|
|
32
|
+
<%= button_to spina.blocks_admin_page_page_blocks_path(page_id: @page.id),
|
|
33
|
+
params: {page_block: {block_id: block.id}},
|
|
34
|
+
method: :post,
|
|
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>
|
|
37
|
+
<div class="text-xs text-gray-400"><%= block.block_template %></div>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<%= link_to spina.edit_admin_page_path(@page), class: "btn btn-default" do %>
|
|
47
|
+
<%= heroicon('arrow-left', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
|
|
48
|
+
<%= t('spina.page_blocks.back_to_page') %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
|
|
53
|
+
<% if @page_blocks.any? %>
|
|
54
|
+
<div class="my-6 md:m-8 bg-white md:rounded-lg border-l-0 border-r-0 border md:border-r md:border-l border-gray-200 border-b-0 shadow-xs">
|
|
55
|
+
<div data-controller="sortable" data-sortable-url="<%= spina.sort_blocks_admin_page_page_blocks_path(page_id: @page.id) %>">
|
|
56
|
+
<form data-sortable-target="form" method="post" action="<%= spina.sort_blocks_admin_page_page_blocks_path(page_id: @page.id) %>">
|
|
57
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
58
|
+
</form>
|
|
59
|
+
<ul data-sortable-target="list">
|
|
60
|
+
<% @page_blocks.each do |page_block| %>
|
|
61
|
+
<li class="border-b border-gray-200 flex items-center" data-id="<%= page_block.id %>">
|
|
62
|
+
<div class="px-4 py-1 cursor-move text-gray-300 hover:text-gray-500" data-sortable-handle>
|
|
63
|
+
<%= heroicon('bars-3', style: :mini, class: 'w-5 h-5') %>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="flex-1 py-3">
|
|
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>
|
|
68
|
+
<div class="text-gray-400 text-xs">
|
|
69
|
+
<%= page_block.block.block_template %>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="pr-4">
|
|
74
|
+
<%= button_to spina.blocks_admin_page_page_block_path(page_id: @page.id, id: page_block.id),
|
|
75
|
+
method: :delete,
|
|
76
|
+
class: "text-gray-300 hover:text-red-500 transition-colors",
|
|
77
|
+
form: {data: {controller: "confirm", confirm_message: t('spina.page_blocks.remove_confirmation')}} do %>
|
|
78
|
+
<%= heroicon('x-mark', style: :mini, class: 'w-5 h-5') %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
|
81
|
+
</li>
|
|
82
|
+
<% end %>
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<% else %>
|
|
87
|
+
<div class="m-8 my-6 italic text-gray-400">
|
|
88
|
+
<%= t('spina.page_blocks.no_blocks_yet') %>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
en:
|
|
2
|
+
spina:
|
|
3
|
+
blocks:
|
|
4
|
+
title: "Blocks"
|
|
5
|
+
new: "New block"
|
|
6
|
+
create: "Create block"
|
|
7
|
+
save: "Save block"
|
|
8
|
+
saved: "Block saved"
|
|
9
|
+
deleted: "Block deleted"
|
|
10
|
+
delete: "Delete block"
|
|
11
|
+
delete_confirmation: "Are you sure you want to delete this block?"
|
|
12
|
+
couldnt_be_saved: "Block couldn't be saved"
|
|
13
|
+
sorting_saved: "Sorting saved"
|
|
14
|
+
content: "Content"
|
|
15
|
+
settings: "Settings"
|
|
16
|
+
all: "All"
|
|
17
|
+
category: "Category"
|
|
18
|
+
no_category: "— No category —"
|
|
19
|
+
select_block: "— Select block —"
|
|
20
|
+
no_blocks_yet: "No blocks yet. Create your first block to get started."
|
|
21
|
+
inactive: "Inactive"
|
|
22
|
+
uncategorized: "Uncategorized"
|
|
23
|
+
active_description: "Inactive blocks are hidden from pages."
|
|
24
|
+
block_categories:
|
|
25
|
+
saved: "Category saved"
|
|
26
|
+
couldnt_be_saved: "Category couldn't be saved"
|
|
27
|
+
page_blocks:
|
|
28
|
+
title: "Page Blocks"
|
|
29
|
+
add_block: "Add block"
|
|
30
|
+
back_to_page: "Back to page"
|
|
31
|
+
added: "Block added to page"
|
|
32
|
+
couldnt_be_added: "Block couldn't be added"
|
|
33
|
+
removed: "Block removed from page"
|
|
34
|
+
sorting_saved: "Sorting saved"
|
|
35
|
+
remove_confirmation: "Remove this block from the page?"
|
|
36
|
+
no_blocks_yet: "No blocks on this page yet. Add blocks from the library to build your page."
|
|
37
|
+
manage_blocks: "Manage blocks"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Spina::Engine.routes.draw do
|
|
4
|
+
namespace :blocks, path: '', module: 'blocks' do
|
|
5
|
+
namespace :admin, path: Spina.config.backend_path do
|
|
6
|
+
resources :blocks do
|
|
7
|
+
member do
|
|
8
|
+
get :edit_content
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
collection do
|
|
12
|
+
post :sort
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
resources :categories, only: %i[index edit update]
|
|
17
|
+
|
|
18
|
+
resources :pages, only: [] do
|
|
19
|
+
resources :page_blocks, only: %i[index create destroy] do
|
|
20
|
+
collection do
|
|
21
|
+
post :sort
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpinaBlocksCategories < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :spina_blocks_categories do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :label, null: false
|
|
8
|
+
t.integer :position, default: 0
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :spina_blocks_categories, :name, unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :spina_blocks_blocks do |t|
|
|
6
|
+
t.string :title, null: false
|
|
7
|
+
t.string :name, null: false
|
|
8
|
+
t.string :block_template, null: false
|
|
9
|
+
t.references :category, foreign_key: { to_table: :spina_blocks_categories }, null: true
|
|
10
|
+
t.integer :position, default: 0
|
|
11
|
+
t.boolean :active, default: true
|
|
12
|
+
t.json :json_attributes
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :spina_blocks_blocks, :name, unique: true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSpinaBlocksPageBlocks < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :spina_blocks_page_blocks do |t|
|
|
6
|
+
t.references :page, null: false, foreign_key: { to_table: :spina_pages }
|
|
7
|
+
t.references :block, null: false, foreign_key: { to_table: :spina_blocks_blocks }
|
|
8
|
+
t.integer :position, default: 0
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :spina_blocks_page_blocks, %i[page_id block_id], unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Blocks
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace Spina::Blocks
|
|
7
|
+
|
|
8
|
+
config.before_initialize do
|
|
9
|
+
::Spina::Plugin.register do |plugin|
|
|
10
|
+
plugin.name = 'blocks'
|
|
11
|
+
plugin.namespace = 'blocks'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
::Spina::Theme.class_eval do
|
|
15
|
+
attr_accessor :block_templates, :block_categories
|
|
16
|
+
|
|
17
|
+
unless method_defined?(:initialize_without_blocks)
|
|
18
|
+
alias_method :initialize_without_blocks, :initialize
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
initialize_without_blocks
|
|
22
|
+
@block_templates = []
|
|
23
|
+
@block_categories = []
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
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.register_parts' do
|
|
38
|
+
config.to_prepare do
|
|
39
|
+
::Spina::Part.register(
|
|
40
|
+
Spina::Parts::BlockReference,
|
|
41
|
+
Spina::Parts::BlockCollection
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
initializer 'spina.blocks.extend_page' do
|
|
47
|
+
config.to_prepare do
|
|
48
|
+
Dir.glob(Spina::Blocks::Engine.root.join('app/overrides/**/*.rb')).sort.each do |override|
|
|
49
|
+
load override
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
initializer 'spina.blocks.tailwind_content' do
|
|
55
|
+
::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/views/**/*.*"
|
|
56
|
+
::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/helpers/**/*.*"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
initializer 'spina.blocks.i18n' do
|
|
60
|
+
config.i18n.load_path += Dir[Spina::Blocks::Engine.root.join('config', 'locales', '*.{rb,yml}')]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
initializer 'spina.blocks.helpers' do
|
|
64
|
+
config.to_prepare do
|
|
65
|
+
ActionView::Base.include Spina::Blocks::BlocksHelper
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/spina-blocks.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spina-blocks
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Konstantin Kanashchuk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: spina
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
description: A plugin for Spina CMS that adds reusable block components that can be
|
|
27
|
+
assembled into pages.
|
|
28
|
+
email:
|
|
29
|
+
- ksnitsky@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- README.md
|
|
35
|
+
- Rakefile
|
|
36
|
+
- app/controllers/spina/blocks/admin/blocks_controller.rb
|
|
37
|
+
- app/controllers/spina/blocks/admin/categories_controller.rb
|
|
38
|
+
- app/controllers/spina/blocks/admin/page_blocks_controller.rb
|
|
39
|
+
- app/helpers/spina/blocks/blocks_helper.rb
|
|
40
|
+
- app/models/spina/blocks/application_record.rb
|
|
41
|
+
- app/models/spina/blocks/block.rb
|
|
42
|
+
- app/models/spina/blocks/category.rb
|
|
43
|
+
- app/models/spina/blocks/page_block.rb
|
|
44
|
+
- app/models/spina/parts/block_collection.rb
|
|
45
|
+
- app/models/spina/parts/block_reference.rb
|
|
46
|
+
- app/overrides/spina/account_override.rb
|
|
47
|
+
- app/overrides/spina/page_override.rb
|
|
48
|
+
- app/views/spina/admin/hooks/blocks/_website_secondary_navigation.html.erb
|
|
49
|
+
- app/views/spina/admin/parts/block_collections/_form.html.erb
|
|
50
|
+
- app/views/spina/admin/parts/block_references/_form.html.erb
|
|
51
|
+
- app/views/spina/blocks/admin/blocks/_button_block_content.html.erb
|
|
52
|
+
- app/views/spina/blocks/admin/blocks/_button_block_settings.html.erb
|
|
53
|
+
- app/views/spina/blocks/admin/blocks/_form.html.erb
|
|
54
|
+
- app/views/spina/blocks/admin/blocks/_form_block_content.html.erb
|
|
55
|
+
- app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb
|
|
56
|
+
- app/views/spina/blocks/admin/blocks/_new_block_form.html.erb
|
|
57
|
+
- app/views/spina/blocks/admin/blocks/edit.html.erb
|
|
58
|
+
- app/views/spina/blocks/admin/blocks/edit_content.html.erb
|
|
59
|
+
- app/views/spina/blocks/admin/blocks/index.html.erb
|
|
60
|
+
- app/views/spina/blocks/admin/blocks/new.html.erb
|
|
61
|
+
- app/views/spina/blocks/admin/page_blocks/index.html.erb
|
|
62
|
+
- config/locales/en.yml
|
|
63
|
+
- config/routes.rb
|
|
64
|
+
- db/migrate/1_create_spina_blocks_categories.rb
|
|
65
|
+
- db/migrate/2_create_spina_blocks_blocks.rb
|
|
66
|
+
- db/migrate/3_create_spina_blocks_page_blocks.rb
|
|
67
|
+
- db/migrate/4_remove_name_from_spina_blocks_blocks.rb
|
|
68
|
+
- lib/spina-blocks.rb
|
|
69
|
+
- lib/spina/blocks/engine.rb
|
|
70
|
+
- lib/spina/blocks/version.rb
|
|
71
|
+
homepage: https://github.com/ksnitsky/spina-blocks
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
rubygems_mfa_required: 'true'
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 2.7.0
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 4.0.3
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Block component library for Spina CMS
|
|
93
|
+
test_files: []
|