spina-blocks 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +38 -12
- data/Rakefile +20 -2
- data/app/assets/javascripts/spina/controllers/block_collection_controller.js +188 -44
- data/app/controllers/spina/blocks/admin/blocks_controller.rb +64 -20
- data/app/controllers/spina/blocks/admin/categories_controller.rb +6 -6
- data/app/controllers/spina/blocks/admin/page_blocks_controller.rb +9 -9
- data/app/helpers/spina/blocks/blocks_helper.rb +3 -3
- data/app/models/concerns/spina/parts/block_filterable.rb +18 -0
- data/app/models/spina/blocks/account_extension.rb +27 -0
- data/app/models/spina/blocks/block.rb +5 -5
- data/app/models/spina/blocks/category.rb +1 -1
- data/app/models/spina/blocks/page_block.rb +2 -2
- data/app/models/spina/blocks/page_extension.rb +14 -0
- data/app/models/spina/blocks/search_extension.rb +15 -0
- data/app/models/spina/parts/block_collection.rb +2 -0
- data/app/models/spina/parts/block_reference.rb +2 -0
- data/app/views/spina/admin/parts/block_collections/_form.html.erb +41 -22
- data/app/views/spina/admin/parts/block_references/_form.html.erb +2 -2
- data/app/views/spina/admin/pro/search/results/_block.html.erb +23 -0
- data/app/views/spina/blocks/admin/blocks/_form_block_content.html.erb +2 -2
- data/app/views/spina/blocks/admin/blocks/_modal_form.html.erb +61 -0
- data/app/views/spina/blocks/admin/blocks/_new_block_form.html.erb +1 -1
- data/app/views/spina/blocks/admin/blocks/_new_block_modal_form.html.erb +42 -0
- data/app/views/spina/blocks/admin/blocks/edit_modal.html.erb +3 -0
- data/app/views/spina/blocks/admin/blocks/index.html.erb +1 -1
- data/app/views/spina/blocks/admin/blocks/new.html.erb +5 -1
- data/app/views/spina/blocks/admin/page_blocks/index.html.erb +3 -3
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +5 -3
- data/db/migrate/1_create_spina_blocks_categories.rb +36 -0
- data/db/migrate/2_create_spina_blocks_blocks.rb +39 -0
- data/db/migrate/3_create_spina_blocks_page_blocks.rb +36 -0
- data/db/migrate/5_rename_title_to_name_in_spina_blocks_blocks.rb +18 -0
- data/lib/generators/spina/blocks/install_generator.rb +17 -0
- data/lib/spina/blocks/engine.rb +25 -25
- data/lib/spina/blocks/version.rb +1 -1
- data/lib/spina-blocks.rb +3 -3
- metadata +73 -13
- data/app/overrides/spina/account_override.rb +0 -19
- data/app/overrides/spina/page_override.rb +0 -6
- data/db/migrate/20250101000001_create_spina_blocks_categories.rb +0 -31
- data/db/migrate/20250101000002_create_spina_blocks_blocks.rb +0 -35
- data/db/migrate/20250101000003_create_spina_blocks_page_blocks.rb +0 -31
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bb4bd87c20dc0578090ce0f7e9b26a0ff511f79c52c1e11319527fb9a132362
|
|
4
|
+
data.tar.gz: 768fbfeb07527abc7d752e7f8af4d51ebd0e86f0e7fc643f313e11d11874de3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6887a086f16feb25de70d36706ef1f5492c8b3f2831a3e1e7d583b1e62dcf79d547fa9e46edeca27805260a3e68835d9b1234a7144df0a1affd7ef5acf548fbc
|
|
7
|
+
data.tar.gz: 3741e38abf77005b798a73bdfbfc335f10c7a6109c88551ad6c649f287761714f15aa2e4467e5d65feed3e957c233210da0411e3691dc5204b3b479311dc52a2
|
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
|
|
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
|
|
@@ -111,6 +119,24 @@ theme.parts = [
|
|
|
111
119
|
]
|
|
112
120
|
```
|
|
113
121
|
|
|
122
|
+
#### Filtering blocks by template
|
|
123
|
+
|
|
124
|
+
By default, `BlockReference` and `BlockCollection` show all active blocks in their selects. To limit the list to blocks of a specific template, pass `block_template` in the part's `options`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
theme.parts = [
|
|
128
|
+
{ name: "hero_block", title: "Hero Block",
|
|
129
|
+
part_type: "Spina::Parts::BlockReference",
|
|
130
|
+
options: { block_template: "hero" } },
|
|
131
|
+
|
|
132
|
+
{ name: "sidebar_blocks", title: "Sidebar Blocks",
|
|
133
|
+
part_type: "Spina::Parts::BlockCollection",
|
|
134
|
+
options: { block_template: "sidebar_widget" } }
|
|
135
|
+
]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With this configuration, the "Hero Block" select will only show blocks created with the `hero` template, and "Sidebar Blocks" will only show `sidebar_widget` blocks. If `options` is omitted or does not contain `block_template`, all active blocks are shown (the default behavior).
|
|
139
|
+
|
|
114
140
|
Then in your template:
|
|
115
141
|
|
|
116
142
|
```erb
|
|
@@ -125,10 +151,10 @@ Then in your template:
|
|
|
125
151
|
|
|
126
152
|
## Models
|
|
127
153
|
|
|
128
|
-
| Model
|
|
129
|
-
|
|
130
|
-
| `Spina::Blocks::Block`
|
|
131
|
-
| `Spina::Blocks::Category`
|
|
154
|
+
| Model | Description |
|
|
155
|
+
| -------------------------- | -------------------------------------------------- |
|
|
156
|
+
| `Spina::Blocks::Block` | Reusable content block with template and parts |
|
|
157
|
+
| `Spina::Blocks::Category` | Block category for organizing the library |
|
|
132
158
|
| `Spina::Blocks::PageBlock` | Join model linking blocks to pages (with position) |
|
|
133
159
|
|
|
134
160
|
## Admin interface
|
|
@@ -142,13 +168,13 @@ The plugin adds:
|
|
|
142
168
|
|
|
143
169
|
## Helper methods
|
|
144
170
|
|
|
145
|
-
| Helper
|
|
146
|
-
|
|
147
|
-
| `render_blocks(page)`
|
|
148
|
-
| `render_block(block)`
|
|
149
|
-
| `block_content(block, :part_name)`
|
|
150
|
-
| `block_has_content?(block, :part_name)` | Check if a block has content
|
|
171
|
+
| Helper | Description |
|
|
172
|
+
| --------------------------------------- | --------------------------------------------------- |
|
|
173
|
+
| `render_blocks(page)` | Render all blocks attached to a page via PageBlocks |
|
|
174
|
+
| `render_block(block)` | Render a single block using its template partial |
|
|
175
|
+
| `block_content(block, :part_name)` | Access a block's content |
|
|
176
|
+
| `block_has_content?(block, :part_name)` | Check if a block has content |
|
|
151
177
|
|
|
152
178
|
## License
|
|
153
179
|
|
|
154
|
-
MIT
|
|
180
|
+
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
|
|
4
|
-
|
|
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
|
|
@@ -2,24 +2,36 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
2
2
|
import Sortable from "libraries/sortablejs";
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
|
+
#abortController = null;
|
|
6
|
+
#modalWasOpen = false;
|
|
7
|
+
#modalObserver = null;
|
|
8
|
+
|
|
9
|
+
get #searchQuery() {
|
|
10
|
+
return this.hasSearchInputTarget ? this.searchInputTarget.value : "";
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
static get targets() {
|
|
6
14
|
return [
|
|
7
15
|
"list",
|
|
8
16
|
"hiddenFields",
|
|
9
17
|
"dropdown",
|
|
10
|
-
"
|
|
18
|
+
"searchInput",
|
|
11
19
|
"emptyMessage",
|
|
12
20
|
"listItemTemplate",
|
|
13
21
|
"groupHeaderTemplate",
|
|
14
22
|
"dropdownOptionTemplate",
|
|
15
23
|
"dropdownEmptyTemplate",
|
|
24
|
+
"newBlockTemplate",
|
|
16
25
|
];
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
static get values() {
|
|
20
29
|
return {
|
|
21
|
-
blocks: Array, // [{id,
|
|
30
|
+
blocks: Array, // [{id, name, templateName, templateTitle}]
|
|
22
31
|
selectedIds: Array, // [id, id, ...]
|
|
32
|
+
editUrl: String, // base edit_modal URL with __ID__ placeholder
|
|
33
|
+
newUrl: String, // URL for new block modal
|
|
34
|
+
blocksDataUrl: String, // JSON endpoint to refresh blocks list
|
|
23
35
|
};
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -35,10 +47,17 @@ export default class extends Controller {
|
|
|
35
47
|
onEnd: this.reorderHiddenFields.bind(this),
|
|
36
48
|
});
|
|
37
49
|
this.render();
|
|
50
|
+
this.#observeModal();
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
disconnect() {
|
|
41
54
|
if (this.sortable) this.sortable.destroy();
|
|
55
|
+
if (this.#modalObserver) {
|
|
56
|
+
this.#modalObserver.disconnect();
|
|
57
|
+
this.#modalObserver = null;
|
|
58
|
+
}
|
|
59
|
+
this.#abortController?.abort();
|
|
60
|
+
this.#abortController = null;
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
// --- Actions ---
|
|
@@ -49,6 +68,9 @@ export default class extends Controller {
|
|
|
49
68
|
if (this.selectedIdsValue.indexOf(id) !== -1) return;
|
|
50
69
|
|
|
51
70
|
this.selectedIdsValue = this.selectedIdsValue.concat([id]);
|
|
71
|
+
if (this.hasSearchInputTarget) {
|
|
72
|
+
this.searchInputTarget.value = "";
|
|
73
|
+
}
|
|
52
74
|
this.render();
|
|
53
75
|
this.closeDropdown();
|
|
54
76
|
}
|
|
@@ -60,19 +82,23 @@ export default class extends Controller {
|
|
|
60
82
|
this.render();
|
|
61
83
|
}
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
85
|
+
onSearchInput() {
|
|
86
|
+
this.renderDropdown();
|
|
87
|
+
this.openDropdown();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onSearchFocus() {
|
|
91
|
+
this.renderDropdown();
|
|
92
|
+
this.openDropdown();
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
closeOnOutsideClick(event) {
|
|
75
|
-
|
|
96
|
+
const clickedInsideSearch =
|
|
97
|
+
this.hasSearchInputTarget &&
|
|
98
|
+
this.searchInputTarget.contains(event.target);
|
|
99
|
+
const clickedInsideDropdown = this.dropdownTarget.contains(event.target);
|
|
100
|
+
|
|
101
|
+
if (!clickedInsideSearch && !clickedInsideDropdown) {
|
|
76
102
|
this.closeDropdown();
|
|
77
103
|
}
|
|
78
104
|
}
|
|
@@ -117,43 +143,74 @@ export default class extends Controller {
|
|
|
117
143
|
}
|
|
118
144
|
|
|
119
145
|
renderDropdown() {
|
|
120
|
-
|
|
146
|
+
let availableBlocks = this.blocksValue.filter((b) => {
|
|
121
147
|
return this.selectedIdsValue.indexOf(b.id) === -1;
|
|
122
148
|
});
|
|
123
149
|
|
|
150
|
+
// Filter by search query
|
|
151
|
+
const query = this.#searchQuery.toLowerCase().trim();
|
|
152
|
+
if (query) {
|
|
153
|
+
availableBlocks = availableBlocks.filter((b) => {
|
|
154
|
+
return (
|
|
155
|
+
b.name.toLowerCase().includes(query) ||
|
|
156
|
+
(b.templateTitle && b.templateTitle.toLowerCase().includes(query)) ||
|
|
157
|
+
(b.templateName && b.templateName.toLowerCase().includes(query))
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
124
162
|
this.dropdownTarget.innerHTML = "";
|
|
125
163
|
|
|
126
164
|
if (availableBlocks.length === 0) {
|
|
127
165
|
const empty = this.dropdownEmptyTemplateTarget.content.cloneNode(true);
|
|
128
166
|
this.dropdownTarget.appendChild(empty);
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
} else {
|
|
168
|
+
// Group by templateTitle
|
|
169
|
+
const groups = {};
|
|
170
|
+
availableBlocks.forEach((b) => {
|
|
171
|
+
const key = b.templateTitle || b.templateName || "Other";
|
|
172
|
+
if (!groups[key]) groups[key] = [];
|
|
173
|
+
groups[key].push(b);
|
|
174
|
+
});
|
|
131
175
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
176
|
+
Object.keys(groups)
|
|
177
|
+
.sort()
|
|
178
|
+
.forEach((groupName) => {
|
|
179
|
+
const header = this.groupHeaderTemplateTarget.content.cloneNode(true);
|
|
180
|
+
header.querySelector("[data-role='group-name']").textContent =
|
|
181
|
+
groupName;
|
|
182
|
+
this.dropdownTarget.appendChild(header);
|
|
139
183
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const option =
|
|
150
|
-
this.dropdownOptionTemplateTarget.content.cloneNode(true);
|
|
151
|
-
const button = option.querySelector("button");
|
|
152
|
-
button.dataset.blockId = block.id;
|
|
153
|
-
option.querySelector("[data-role='title']").textContent = block.title;
|
|
154
|
-
this.dropdownTarget.appendChild(option);
|
|
184
|
+
groups[groupName].forEach((block) => {
|
|
185
|
+
const option =
|
|
186
|
+
this.dropdownOptionTemplateTarget.content.cloneNode(true);
|
|
187
|
+
const button = option.querySelector("button");
|
|
188
|
+
button.dataset.blockId = block.id;
|
|
189
|
+
option.querySelector("[data-role='title']").textContent =
|
|
190
|
+
block.name;
|
|
191
|
+
this.dropdownTarget.appendChild(option);
|
|
192
|
+
});
|
|
155
193
|
});
|
|
156
|
-
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// "New block" link at the bottom of the dropdown
|
|
197
|
+
if (
|
|
198
|
+
this.hasNewBlockTemplateTarget &&
|
|
199
|
+
this.hasNewUrlValue &&
|
|
200
|
+
this.newUrlValue
|
|
201
|
+
) {
|
|
202
|
+
const newBlock = this.newBlockTemplateTarget.content.cloneNode(true);
|
|
203
|
+
const link = newBlock.querySelector("[data-role='new-block-link']");
|
|
204
|
+
if (link) {
|
|
205
|
+
link.href = this.newUrlValue;
|
|
206
|
+
}
|
|
207
|
+
this.dropdownTarget.appendChild(newBlock);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reposition if dropdown is currently visible
|
|
211
|
+
if (this.dropdownTarget.style.display !== "none") {
|
|
212
|
+
this.#positionDropdown();
|
|
213
|
+
}
|
|
157
214
|
}
|
|
158
215
|
|
|
159
216
|
renderEmptyMessage() {
|
|
@@ -169,12 +226,18 @@ export default class extends Controller {
|
|
|
169
226
|
const fragment = this.listItemTemplateTarget.content.cloneNode(true);
|
|
170
227
|
const root = fragment.querySelector("[data-block-id]");
|
|
171
228
|
root.dataset.blockId = block.id;
|
|
172
|
-
fragment.querySelector("[data-role='title']").textContent = block.
|
|
229
|
+
fragment.querySelector("[data-role='title']").textContent = block.name;
|
|
173
230
|
fragment.querySelector("[data-role='template-label']").textContent =
|
|
174
231
|
"(" + (block.templateTitle || block.templateName) + ")";
|
|
175
232
|
fragment.querySelector(
|
|
176
233
|
"[data-action='block-collection#remove']",
|
|
177
234
|
).dataset.blockId = block.id;
|
|
235
|
+
|
|
236
|
+
const editLink = fragment.querySelector("[data-role='edit-link']");
|
|
237
|
+
if (editLink && this.hasEditUrlValue && this.editUrlValue) {
|
|
238
|
+
editLink.href = this.editUrlValue.replace("__ID__", block.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
178
241
|
return fragment;
|
|
179
242
|
}
|
|
180
243
|
|
|
@@ -190,20 +253,101 @@ export default class extends Controller {
|
|
|
190
253
|
}
|
|
191
254
|
|
|
192
255
|
openDropdown() {
|
|
256
|
+
if (this.dropdownTarget.style.display === "block") {
|
|
257
|
+
// Already open, just reposition
|
|
258
|
+
this.#positionDropdown();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
193
262
|
this.dropdownTarget.style.display = "block";
|
|
194
|
-
this
|
|
195
|
-
|
|
263
|
+
this.#positionDropdown();
|
|
264
|
+
|
|
265
|
+
this.#abortController = new AbortController();
|
|
266
|
+
const { signal } = this.#abortController;
|
|
267
|
+
|
|
268
|
+
document.addEventListener("click", (e) => this.closeOnOutsideClick(e), {
|
|
269
|
+
capture: true,
|
|
270
|
+
signal,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let scrollTimeout;
|
|
274
|
+
const debouncedPosition = () => {
|
|
275
|
+
clearTimeout(scrollTimeout);
|
|
276
|
+
scrollTimeout = setTimeout(() => this.#positionDropdown(), 100);
|
|
277
|
+
};
|
|
278
|
+
window.addEventListener("scroll", debouncedPosition, {
|
|
279
|
+
capture: true,
|
|
280
|
+
signal,
|
|
281
|
+
});
|
|
282
|
+
window.addEventListener("resize", debouncedPosition, { signal });
|
|
196
283
|
}
|
|
197
284
|
|
|
198
285
|
closeDropdown() {
|
|
199
286
|
this.dropdownTarget.style.display = "none";
|
|
200
|
-
if (this.
|
|
201
|
-
|
|
202
|
-
this._outsideClickHandler = null;
|
|
287
|
+
if (this.hasSearchInputTarget) {
|
|
288
|
+
this.searchInputTarget.value = "";
|
|
203
289
|
}
|
|
290
|
+
this.#abortController?.abort();
|
|
291
|
+
this.#abortController = null;
|
|
204
292
|
}
|
|
205
293
|
|
|
206
294
|
findBlock(id) {
|
|
207
295
|
return this.blocksValue.find((b) => b.id === id);
|
|
208
296
|
}
|
|
297
|
+
|
|
298
|
+
// --- Private ---
|
|
299
|
+
|
|
300
|
+
#positionDropdown() {
|
|
301
|
+
const dropdown = this.dropdownTarget;
|
|
302
|
+
const container = dropdown.parentElement;
|
|
303
|
+
const containerRect = container.getBoundingClientRect();
|
|
304
|
+
const dropdownHeight = dropdown.offsetHeight;
|
|
305
|
+
const viewportHeight = window.innerHeight;
|
|
306
|
+
|
|
307
|
+
const spaceBelow = viewportHeight - containerRect.bottom;
|
|
308
|
+
const spaceAbove = containerRect.top;
|
|
309
|
+
|
|
310
|
+
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
|
311
|
+
// Not enough space below and more space above, show above
|
|
312
|
+
dropdown.style.bottom = "100%";
|
|
313
|
+
dropdown.style.top = "auto";
|
|
314
|
+
dropdown.style.marginBottom = "4px";
|
|
315
|
+
dropdown.style.marginTop = "0";
|
|
316
|
+
} else {
|
|
317
|
+
// Default: show below
|
|
318
|
+
dropdown.style.bottom = "auto";
|
|
319
|
+
dropdown.style.top = "100%";
|
|
320
|
+
dropdown.style.marginTop = "4px";
|
|
321
|
+
dropdown.style.marginBottom = "0";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#observeModal() {
|
|
326
|
+
const modalFrame = document.querySelector("turbo-frame[id='modal']");
|
|
327
|
+
if (!modalFrame || !this.hasBlocksDataUrlValue || !this.blocksDataUrlValue)
|
|
328
|
+
return;
|
|
329
|
+
|
|
330
|
+
this.#modalWasOpen = false;
|
|
331
|
+
this.#modalObserver = new MutationObserver(() => {
|
|
332
|
+
const hasModal = modalFrame.querySelector(".modal");
|
|
333
|
+
if (hasModal) {
|
|
334
|
+
this.#modalWasOpen = true;
|
|
335
|
+
} else if (this.#modalWasOpen) {
|
|
336
|
+
this.#modalWasOpen = false;
|
|
337
|
+
this.refreshBlocks();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
this.#modalObserver.observe(modalFrame, { childList: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
refreshBlocks() {
|
|
344
|
+
fetch(this.blocksDataUrlValue, {
|
|
345
|
+
headers: { Accept: "application/json" },
|
|
346
|
+
})
|
|
347
|
+
.then((response) => response.json())
|
|
348
|
+
.then((blocks) => {
|
|
349
|
+
this.blocksValue = blocks;
|
|
350
|
+
this.render();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
209
353
|
}
|
|
@@ -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:
|
|
11
|
-
before_action :set_tabs, only:
|
|
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
|
|
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
|
|
|
@@ -29,24 +29,37 @@ module Spina
|
|
|
29
29
|
def new
|
|
30
30
|
@block_templates = current_theme.try(:block_templates) || []
|
|
31
31
|
@block = Spina::Blocks::Block.new(block_template: params[:block_template])
|
|
32
|
+
@modal = params[:modal]
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def create
|
|
35
36
|
@block = Spina::Blocks::Block.new(block_params)
|
|
36
37
|
if @block.save
|
|
37
|
-
|
|
38
|
+
if params[:modal]
|
|
39
|
+
redirect_to(spina.edit_modal_blocks_admin_block_url(@block))
|
|
40
|
+
else
|
|
41
|
+
redirect_to(spina.edit_blocks_admin_block_url(@block))
|
|
42
|
+
end
|
|
38
43
|
else
|
|
39
44
|
@block_templates = current_theme.try(:block_templates) || []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
if params[:modal]
|
|
46
|
+
@modal = true
|
|
47
|
+
render(turbo_stream: turbo_stream.update(
|
|
48
|
+
helpers.dom_id(@block, :new_block_modal_form),
|
|
49
|
+
partial: "new_block_modal_form",
|
|
50
|
+
))
|
|
51
|
+
else
|
|
52
|
+
render(turbo_stream: turbo_stream.update(
|
|
53
|
+
helpers.dom_id(@block, :new_block_form),
|
|
54
|
+
partial: "new_block_form",
|
|
55
|
+
))
|
|
56
|
+
end
|
|
44
57
|
end
|
|
45
58
|
end
|
|
46
59
|
|
|
47
60
|
def edit
|
|
48
|
-
add_breadcrumb
|
|
49
|
-
add_breadcrumb
|
|
61
|
+
add_breadcrumb(I18n.t("spina.blocks.title"), spina.blocks_admin_blocks_path, class: "text-gray-400")
|
|
62
|
+
add_breadcrumb(@block.name)
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
def edit_content
|
|
@@ -55,33 +68,56 @@ module Spina
|
|
|
55
68
|
end&.dig(:parts) || []
|
|
56
69
|
end
|
|
57
70
|
|
|
71
|
+
def edit_modal
|
|
72
|
+
end
|
|
73
|
+
|
|
58
74
|
def update
|
|
59
75
|
Mobility.locale = @locale
|
|
60
76
|
if @block.update(block_params)
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
if params[:modal]
|
|
78
|
+
render(turbo_stream: turbo_stream.update("modal", ""))
|
|
79
|
+
else
|
|
80
|
+
flash[:success] = I18n.t("spina.blocks.saved")
|
|
81
|
+
redirect_to(spina.edit_blocks_admin_block_url(@block, params: { locale: @locale }))
|
|
82
|
+
end
|
|
83
|
+
elsif params[:modal]
|
|
84
|
+
flash.now[:error] = I18n.t("spina.blocks.couldnt_be_saved")
|
|
85
|
+
render(:edit_modal, status: :unprocessable_entity)
|
|
63
86
|
else
|
|
64
|
-
add_breadcrumb
|
|
87
|
+
add_breadcrumb(I18n.t("spina.blocks.title"), spina.blocks_admin_blocks_path, class: "text-gray-400")
|
|
65
88
|
Mobility.locale = I18n.locale
|
|
66
|
-
add_breadcrumb
|
|
67
|
-
flash.now[:error] = I18n.t(
|
|
68
|
-
render
|
|
89
|
+
add_breadcrumb(@block.name)
|
|
90
|
+
flash.now[:error] = I18n.t("spina.blocks.couldnt_be_saved")
|
|
91
|
+
render(:edit, status: :unprocessable_entity)
|
|
69
92
|
end
|
|
70
93
|
end
|
|
71
94
|
|
|
95
|
+
def blocks_data
|
|
96
|
+
blocks = Spina::Blocks::Block.active.sorted
|
|
97
|
+
template_titles = build_template_titles
|
|
98
|
+
render(json: blocks.map do |b|
|
|
99
|
+
{
|
|
100
|
+
id: b.id,
|
|
101
|
+
name: b.name,
|
|
102
|
+
templateName: b.block_template.to_s,
|
|
103
|
+
templateTitle: template_titles[b.block_template.to_s] || b.block_template.to_s.titleize,
|
|
104
|
+
}
|
|
105
|
+
end)
|
|
106
|
+
end
|
|
107
|
+
|
|
72
108
|
def sort
|
|
73
109
|
params[:ids].each.with_index do |id, index|
|
|
74
110
|
Spina::Blocks::Block.where(id: id).update_all(position: index + 1)
|
|
75
111
|
end
|
|
76
112
|
|
|
77
|
-
flash.now[:info] = I18n.t(
|
|
113
|
+
flash.now[:info] = I18n.t("spina.blocks.sorting_saved")
|
|
78
114
|
render_flash
|
|
79
115
|
end
|
|
80
116
|
|
|
81
117
|
def destroy
|
|
82
|
-
flash[:info] = I18n.t(
|
|
118
|
+
flash[:info] = I18n.t("spina.blocks.deleted")
|
|
83
119
|
@block.destroy
|
|
84
|
-
redirect_to
|
|
120
|
+
redirect_to(spina.blocks_admin_blocks_url)
|
|
85
121
|
end
|
|
86
122
|
|
|
87
123
|
private
|
|
@@ -95,7 +131,15 @@ module Spina
|
|
|
95
131
|
end
|
|
96
132
|
|
|
97
133
|
def set_tabs
|
|
98
|
-
@tabs =
|
|
134
|
+
@tabs = ["block_content", "block_settings"]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def build_template_titles
|
|
138
|
+
titles = {}
|
|
139
|
+
(current_theme.try(:block_templates) || []).each do |bt|
|
|
140
|
+
titles[bt[:name].to_s] = bt[:title].to_s
|
|
141
|
+
end
|
|
142
|
+
titles
|
|
99
143
|
end
|
|
100
144
|
|
|
101
145
|
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
|
|
16
|
-
add_breadcrumb
|
|
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(
|
|
23
|
-
redirect_to
|
|
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(
|
|
26
|
-
render
|
|
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
|
|
13
|
-
add_breadcrumb
|
|
14
|
-
add_breadcrumb
|
|
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(
|
|
25
|
+
flash[:success] = I18n.t("spina.page_blocks.added")
|
|
26
26
|
else
|
|
27
|
-
flash[:error] = I18n.t(
|
|
27
|
+
flash[:error] = I18n.t("spina.page_blocks.couldnt_be_added")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
redirect_to
|
|
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(
|
|
37
|
-
redirect_to
|
|
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(
|
|
45
|
+
flash.now[:info] = I18n.t("spina.page_blocks.sorting_saved")
|
|
46
46
|
render_flash
|
|
47
47
|
end
|
|
48
48
|
|