spina-blocks 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76633f69018375e8d330eee4b062677dd4dc10bd380ec2e7b08f3900fd448540
4
- data.tar.gz: dd386042f84a6554db3eb15200011e4a953f3559054a44f05caa0e052b10fa3b
3
+ metadata.gz: 4bb4bd87c20dc0578090ce0f7e9b26a0ff511f79c52c1e11319527fb9a132362
4
+ data.tar.gz: 768fbfeb07527abc7d752e7f8af4d51ebd0e86f0e7fc643f313e11d11874de3d
5
5
  SHA512:
6
- metadata.gz: 19b5d1d156f66e85fb34aa6d2240d0f23cd0ffdb973601e2a4a1f3eca8c9058d39332c33bc986b52a69519b7eee22c3e39c6b713c6aadc9ccb5bdb93abaf5e3e
7
- data.tar.gz: 7442845e4527e7dcfc8e429771b957638b89c053ae6b20767393756429000908af24337853d5899b9af5d06298efd492f53316a1db91712e85d9321f690647f2
6
+ metadata.gz: 6887a086f16feb25de70d36706ef1f5492c8b3f2831a3e1e7d583b1e62dcf79d547fa9e46edeca27805260a3e68835d9b1234a7144df0a1affd7ef5acf548fbc
7
+ data.tar.gz: 3741e38abf77005b798a73bdfbfc335f10c7a6109c88551ad6c649f287761714f15aa2e4467e5d65feed3e957c233210da0411e3691dc5204b3b479311dc52a2
data/README.md CHANGED
@@ -119,6 +119,24 @@ theme.parts = [
119
119
  ]
120
120
  ```
121
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
+
122
140
  Then in your template:
123
141
 
124
142
  ```erb
@@ -2,17 +2,26 @@ 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
- "addButton",
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
 
@@ -21,6 +30,8 @@ export default class extends Controller {
21
30
  blocks: Array, // [{id, name, templateName, templateTitle}]
22
31
  selectedIds: Array, // [id, id, ...]
23
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
24
35
  };
25
36
  }
26
37
 
@@ -36,10 +47,17 @@ export default class extends Controller {
36
47
  onEnd: this.reorderHiddenFields.bind(this),
37
48
  });
38
49
  this.render();
50
+ this.#observeModal();
39
51
  }
40
52
 
41
53
  disconnect() {
42
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;
43
61
  }
44
62
 
45
63
  // --- Actions ---
@@ -50,6 +68,9 @@ export default class extends Controller {
50
68
  if (this.selectedIdsValue.indexOf(id) !== -1) return;
51
69
 
52
70
  this.selectedIdsValue = this.selectedIdsValue.concat([id]);
71
+ if (this.hasSearchInputTarget) {
72
+ this.searchInputTarget.value = "";
73
+ }
53
74
  this.render();
54
75
  this.closeDropdown();
55
76
  }
@@ -61,19 +82,23 @@ export default class extends Controller {
61
82
  this.render();
62
83
  }
63
84
 
64
- toggleDropdown(event) {
65
- event.preventDefault();
66
- event.stopPropagation();
67
- const dropdown = this.dropdownTarget;
68
- if (dropdown.style.display === "none" || dropdown.style.display === "") {
69
- this.openDropdown();
70
- } else {
71
- this.closeDropdown();
72
- }
85
+ onSearchInput() {
86
+ this.renderDropdown();
87
+ this.openDropdown();
88
+ }
89
+
90
+ onSearchFocus() {
91
+ this.renderDropdown();
92
+ this.openDropdown();
73
93
  }
74
94
 
75
95
  closeOnOutsideClick(event) {
76
- if (!this.element.contains(event.target)) {
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) {
77
102
  this.closeDropdown();
78
103
  }
79
104
  }
@@ -118,43 +143,74 @@ export default class extends Controller {
118
143
  }
119
144
 
120
145
  renderDropdown() {
121
- const availableBlocks = this.blocksValue.filter((b) => {
146
+ let availableBlocks = this.blocksValue.filter((b) => {
122
147
  return this.selectedIdsValue.indexOf(b.id) === -1;
123
148
  });
124
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
+
125
162
  this.dropdownTarget.innerHTML = "";
126
163
 
127
164
  if (availableBlocks.length === 0) {
128
165
  const empty = this.dropdownEmptyTemplateTarget.content.cloneNode(true);
129
166
  this.dropdownTarget.appendChild(empty);
130
- return;
131
- }
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
+ });
132
175
 
133
- // Group by templateTitle
134
- const groups = {};
135
- availableBlocks.forEach((b) => {
136
- const key = b.templateTitle || b.templateName || "Other";
137
- if (!groups[key]) groups[key] = [];
138
- groups[key].push(b);
139
- });
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);
140
183
 
141
- Object.keys(groups)
142
- .sort()
143
- .forEach((groupName) => {
144
- const header = this.groupHeaderTemplateTarget.content.cloneNode(true);
145
- header.querySelector("[data-role='group-name']").textContent =
146
- groupName;
147
- this.dropdownTarget.appendChild(header);
148
-
149
- groups[groupName].forEach((block) => {
150
- const option =
151
- this.dropdownOptionTemplateTarget.content.cloneNode(true);
152
- const button = option.querySelector("button");
153
- button.dataset.blockId = block.id;
154
- option.querySelector("[data-role='title']").textContent = block.name;
155
- 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
+ });
156
193
  });
157
- });
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
+ }
158
214
  }
159
215
 
160
216
  renderEmptyMessage() {
@@ -197,20 +253,101 @@ export default class extends Controller {
197
253
  }
198
254
 
199
255
  openDropdown() {
256
+ if (this.dropdownTarget.style.display === "block") {
257
+ // Already open, just reposition
258
+ this.#positionDropdown();
259
+ return;
260
+ }
261
+
200
262
  this.dropdownTarget.style.display = "block";
201
- this._outsideClickHandler = this.closeOnOutsideClick.bind(this);
202
- document.addEventListener("click", this._outsideClickHandler, true);
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 });
203
283
  }
204
284
 
205
285
  closeDropdown() {
206
286
  this.dropdownTarget.style.display = "none";
207
- if (this._outsideClickHandler) {
208
- document.removeEventListener("click", this._outsideClickHandler, true);
209
- this._outsideClickHandler = null;
287
+ if (this.hasSearchInputTarget) {
288
+ this.searchInputTarget.value = "";
210
289
  }
290
+ this.#abortController?.abort();
291
+ this.#abortController = null;
211
292
  }
212
293
 
213
294
  findBlock(id) {
214
295
  return this.blocksValue.find((b) => b.id === id);
215
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
+ }
216
353
  }
@@ -29,18 +29,31 @@ 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
- redirect_to(spina.edit_blocks_admin_block_url(@block))
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
- render(turbo_stream: turbo_stream.update(
41
- helpers.dom_id(@block, :new_block_form),
42
- partial: "new_block_form",
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
 
@@ -79,6 +92,19 @@ module Spina
79
92
  end
80
93
  end
81
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
+
82
108
  def sort
83
109
  params[:ids].each.with_index do |id, index|
84
110
  Spina::Blocks::Block.where(id: id).update_all(position: index + 1)
@@ -108,6 +134,14 @@ module Spina
108
134
  @tabs = ["block_content", "block_settings"]
109
135
  end
110
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
143
+ end
144
+
111
145
  def block_params
112
146
  params.require(:block).permit!
113
147
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Parts
5
+ module BlockFilterable
6
+ extend ActiveSupport::Concern
7
+
8
+ def available_blocks
9
+ scope = ::Spina::Blocks::Block.active.sorted
10
+ opts = options.is_a?(Hash) ? options.with_indifferent_access : nil
11
+ if opts&.dig(:block_template).present?
12
+ scope = scope.where(block_template: opts[:block_template])
13
+ end
14
+ scope
15
+ end
16
+ end
17
+ end
18
+ end
@@ -3,6 +3,8 @@
3
3
  module Spina
4
4
  module Parts
5
5
  class BlockCollection < Base
6
+ include BlockFilterable
7
+
6
8
  attr_json :block_ids, :integer, array: true, default: -> { [] }
7
9
 
8
10
  attr_accessor :options
@@ -3,6 +3,8 @@
3
3
  module Spina
4
4
  module Parts
5
5
  class BlockReference < Base
6
+ include BlockFilterable
7
+
6
8
  attr_json :block_id, :integer, default: nil
7
9
 
8
10
  attr_accessor :options
@@ -1,5 +1,5 @@
1
1
  <%
2
- blocks = Spina::Blocks::Block.active.sorted
2
+ blocks = f.object.available_blocks
3
3
  selected_ids = f.object.block_ids || []
4
4
  field_name = "#{f.object_name}[block_ids][]"
5
5
 
@@ -27,6 +27,8 @@
27
27
  data-block-collection-blocks-value="<%= blocks_json.to_json %>"
28
28
  data-block-collection-selected-ids-value="<%= selected_ids.to_json %>"
29
29
  data-block-collection-edit-url-value="<%= spina.edit_modal_blocks_admin_block_path("__ID__") %>"
30
+ data-block-collection-new-url-value="<%= spina.new_blocks_admin_block_path(modal: 1) %>"
31
+ data-block-collection-blocks-data-url-value="<%= spina.blocks_data_blocks_admin_blocks_path(format: :json) %>"
30
32
  data-field-name="<%= field_name %>">
31
33
 
32
34
  <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
@@ -34,11 +36,26 @@
34
36
  <div class="text-gray-400 text-sm mb-2"><%= f.object.hint %></div>
35
37
  <% end %>
36
38
 
39
+ <%# Search input + dropdown %>
40
+ <div class="relative mb-3">
41
+ <input type="text"
42
+ data-block-collection-target="searchInput"
43
+ data-action="input->block-collection#onSearchInput focus->block-collection#onSearchFocus"
44
+ placeholder="Search or add block..."
45
+ autocomplete="off"
46
+ class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-spina focus:ring-spina" />
47
+
48
+ <div data-block-collection-target="dropdown"
49
+ style="display:none"
50
+ class="absolute z-10 w-full bg-white border border-gray-200 rounded-md shadow-lg py-1 max-h-60 overflow-y-auto">
51
+ </div>
52
+ </div>
53
+
37
54
  <%# Empty state message %>
38
55
  <div data-block-collection-target="emptyMessage"
39
56
  class="text-sm text-gray-400 italic py-3"
40
57
  style="<%= selected_ids.any? ? 'display:none' : '' %>">
41
- No blocks selected. Click &ldquo;Add block&rdquo; to get started.
58
+ No blocks selected. Use the search field above to add blocks.
42
59
  </div>
43
60
 
44
61
  <%# Sortable list of selected blocks (rendered by Stimulus) %>
@@ -47,24 +64,6 @@
47
64
  <%# Hidden fields container (rendered by Stimulus) %>
48
65
  <div data-block-collection-target="hiddenFields"></div>
49
66
 
50
- <%# Add block button + dropdown %>
51
- <div class="relative inline-block">
52
- <button type="button"
53
- data-action="block-collection#toggleDropdown"
54
- data-block-collection-target="addButton"
55
- class="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-spina">
56
- <svg class="w-4 h-4 mr-1.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
58
- </svg>
59
- Add block
60
- </button>
61
-
62
- <div data-block-collection-target="dropdown"
63
- style="display:none"
64
- class="absolute z-10 mt-1 w-64 bg-white border border-gray-200 rounded-md shadow-lg py-1 max-h-60 overflow-y-auto">
65
- </div>
66
- </div>
67
-
68
67
  <%# ===== Templates for Stimulus controller ===== %>
69
68
 
70
69
  <%# Selected block list item %>
@@ -110,6 +109,19 @@
110
109
 
111
110
  <%# Dropdown empty state %>
112
111
  <template data-block-collection-target="dropdownEmptyTemplate">
113
- <div class="px-3 py-2 text-sm text-gray-400 italic">All blocks added</div>
112
+ <div class="px-3 py-2 text-sm text-gray-400 italic">No blocks found</div>
113
+ </template>
114
+
115
+ <%# Dropdown "New block" link %>
116
+ <template data-block-collection-target="newBlockTemplate">
117
+ <div class="border-t border-gray-200 mt-1 pt-1">
118
+ <a class="w-full text-left px-3 py-2 text-sm text-spina-primary hover:bg-gray-100 flex items-center font-medium"
119
+ href="" data-turbo-frame="modal" data-role="new-block-link">
120
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
122
+ </svg>
123
+ <%= t('spina.blocks.new') %>
124
+ </a>
125
+ </div>
114
126
  </template>
115
127
  </div>
@@ -4,7 +4,7 @@
4
4
  <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
5
5
  <% end %>
6
6
  <div class="mt-1">
7
- <% blocks = Spina::Blocks::Block.active.sorted %>
7
+ <% blocks = f.object.available_blocks %>
8
8
  <%= f.select :block_id,
9
9
  blocks.map { |b| [b.name, b.id] },
10
10
  {include_blank: t('spina.blocks.select_block')},
@@ -0,0 +1,42 @@
1
+ <%# Modal variant of the new block form. Used when creating a block from BlockCollection.
2
+ After successful creation, the modal transitions to the block edit form.
3
+ Expects: @block, @block_templates %>
4
+ <%= turbo_frame_tag dom_id(@block, :new_block_modal_form) do %>
5
+ <%= form_with model: @block, url: spina.blocks_admin_blocks_path, data: { turbo_frame: "modal" } do |f| %>
6
+ <%= hidden_field_tag :modal, 1 %>
7
+
8
+ <div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
9
+ <div class="sm:flex sm:items-start">
10
+ <div class="w-full">
11
+ <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
12
+ <%= t('spina.blocks.new') %>
13
+ </h3>
14
+
15
+ <div class="mt-4">
16
+ <label for="block_block_template" class="block text-sm font-medium text-gray-700 mb-1">
17
+ <%= t('spina.blocks.block_type') %>
18
+ </label>
19
+ <%= f.select :block_template,
20
+ @block_templates.map { |bt| [bt[:title], bt[:name]] },
21
+ { include_blank: t('spina.blocks.select_block_type') },
22
+ class: "block w-full rounded-md border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 text-sm",
23
+ required: true %>
24
+ </div>
25
+
26
+ <div class="mt-3">
27
+ <%= render Spina::Forms::TextFieldComponent.new(f, :name, size: 'lg', autofocus: true) %>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div class="px-4 pb-5 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
33
+ <button type="submit" class="btn btn-primary w-full sm:w-auto sm:ml-3">
34
+ <%= t('spina.blocks.create') %>
35
+ </button>
36
+
37
+ <button type="button" class="btn btn-default w-full sm:w-auto mt-2 sm:mt-0" data-action="modal#close">
38
+ <%= t('spina.ui.cancel') %>
39
+ </button>
40
+ </div>
41
+ <% end %>
42
+ <% end %>
@@ -1,3 +1,7 @@
1
1
  <%= render(Spina::UserInterface::ModalComponent.new) do %>
2
- <%= render 'new_block_form' %>
2
+ <% if @modal %>
3
+ <%= render 'new_block_modal_form' %>
4
+ <% else %>
5
+ <%= render 'new_block_form' %>
6
+ <% end %>
3
7
  <% end %>
data/config/routes.rb CHANGED
@@ -11,6 +11,7 @@ Spina::Engine.routes.draw do
11
11
 
12
12
  collection do
13
13
  post :sort
14
+ get :blocks_data
14
15
  end
15
16
  end
16
17
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Spina
4
4
  module Blocks
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spina-blocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Kanashchuk
@@ -94,6 +94,7 @@ files:
94
94
  - app/controllers/spina/blocks/admin/categories_controller.rb
95
95
  - app/controllers/spina/blocks/admin/page_blocks_controller.rb
96
96
  - app/helpers/spina/blocks/blocks_helper.rb
97
+ - app/models/concerns/spina/parts/block_filterable.rb
97
98
  - app/models/spina/blocks/account_extension.rb
98
99
  - app/models/spina/blocks/application_record.rb
99
100
  - app/models/spina/blocks/block.rb
@@ -114,6 +115,7 @@ files:
114
115
  - app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb
115
116
  - app/views/spina/blocks/admin/blocks/_modal_form.html.erb
116
117
  - app/views/spina/blocks/admin/blocks/_new_block_form.html.erb
118
+ - app/views/spina/blocks/admin/blocks/_new_block_modal_form.html.erb
117
119
  - app/views/spina/blocks/admin/blocks/edit.html.erb
118
120
  - app/views/spina/blocks/admin/blocks/edit_content.html.erb
119
121
  - app/views/spina/blocks/admin/blocks/edit_modal.html.erb