yummy-guide-generic-administrate 0.5.4 → 0.6.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 +139 -0
- data/app/assets/javascripts/yummy_guide_administrate/filter_controls.js +30 -0
- data/app/assets/javascripts/yummy_guide_administrate/filter_form.js +70 -1
- data/app/helpers/yummy_guide/administrate/filter_controls_helper.rb +183 -0
- data/lib/yummy_guide/administrate/engine.rb +4 -1
- data/lib/yummy_guide/administrate/filters.rb +319 -0
- data/lib/yummy_guide/administrate/version.rb +1 -1
- data/lib/yummy_guide/administrate.rb +3 -0
- data/spec/yummy_guide/administrate/filter_controls_helper_spec.rb +76 -0
- data/spec/yummy_guide/administrate/filters_spec.rb +35 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3caffae44b6ea10fc063a2cc8fb8a3938e3cc42078c4af3bc2a08d4188cf01a
|
|
4
|
+
data.tar.gz: cea155304d262445a29199f9c5e57c574600508dca66206f49b81b3d3db66fb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a2e5a4b98e6cea96bfdc9e0cb026212c6ed37f39518800368329cc8ba1fcff202859acb8b3340754b493544b92f33e191d92606de032575c889e436ae39b789
|
|
7
|
+
data.tar.gz: 669b6a383ff6d184527e7324def8875626dc46dd0b5d9c5d55d3b5f18fd8f4e97188d6a943ffa65e4076a70eefd0389e41abbd7b77d97f9e98466a3c580ecc43
|
data/README.md
CHANGED
|
@@ -41,6 +41,8 @@ bundle install
|
|
|
41
41
|
- 一覧テーブルの固定列数、リンク生成、action partial 解決を補助する helper
|
|
42
42
|
- `YummyGuide::Administrate::FilterFormHelper`
|
|
43
43
|
- datetime フィルターや checkbox group の組み立てを補助する helper
|
|
44
|
+
- `YummyGuide::Administrate::FilterControlsHelper`
|
|
45
|
+
- dashboard の Field 型フィルター定義から Filter ボタンとモーダルフォームを描画する helper
|
|
44
46
|
- `YummyGuide::Administrate::DatetimeInputHelper`
|
|
45
47
|
- 管理画面フォーム用の date + time 入力 helper
|
|
46
48
|
- `YummyGuide::Administrate::NumberInputHelper`
|
|
@@ -52,6 +54,7 @@ bundle install
|
|
|
52
54
|
- `clipboards.js`
|
|
53
55
|
- `datetime_input.js`
|
|
54
56
|
- `fixed_submit_actions.js`
|
|
57
|
+
- `filter_controls.js`
|
|
55
58
|
- `filter_form.js`
|
|
56
59
|
- `sticky_left_columns.js`
|
|
57
60
|
- `sticky_table_headers.js`
|
|
@@ -103,6 +106,7 @@ class Admin::ApplicationController < Administrate::ApplicationController
|
|
|
103
106
|
|
|
104
107
|
helper YummyGuide::Administrate::CollectionHelper
|
|
105
108
|
helper YummyGuide::Administrate::DatetimeInputHelper
|
|
109
|
+
helper YummyGuide::Administrate::FilterControlsHelper
|
|
106
110
|
helper YummyGuide::Administrate::FilterFormHelper
|
|
107
111
|
helper YummyGuide::Administrate::NumberInputHelper
|
|
108
112
|
end
|
|
@@ -300,6 +304,140 @@ def search_term
|
|
|
300
304
|
end
|
|
301
305
|
```
|
|
302
306
|
|
|
307
|
+
### Dashboard driven filter controls
|
|
308
|
+
|
|
309
|
+
dashboard に `FILTER_ATTRIBUTES` を定義すると、`admin_filter_controls` で既存の
|
|
310
|
+
Administrate index header に Filter ボタンとモーダルフォームを描画できます。
|
|
311
|
+
|
|
312
|
+
#### Filter の設定方法
|
|
313
|
+
|
|
314
|
+
1. dashboard に送信先と filter 対象項目を定義します。
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
class AdspendDashboard < ApplicationDashboard
|
|
318
|
+
FILTER_PATH = ->(view, _locals) { view.admin_adspends_path }
|
|
319
|
+
FILTER_CLEAR_PATH = ->(view, _locals) { view.admin_adspends_path }
|
|
320
|
+
|
|
321
|
+
FILTER_ATTRIBUTES = {
|
|
322
|
+
campaign_name: YummyGuide::Administrate::Filters::Text.with_options(label: "Campaign name"),
|
|
323
|
+
yearmonth: YummyGuide::Administrate::Filters::Text.with_options(
|
|
324
|
+
label: "Year month",
|
|
325
|
+
inputmode: "numeric",
|
|
326
|
+
pattern: "\\d{6}",
|
|
327
|
+
placeholder: "YYYYMM"
|
|
328
|
+
)
|
|
329
|
+
}.freeze
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
2. index header などで `admin_filter_controls` を呼び出します。`page` から dashboard を
|
|
334
|
+
解決し、`FILTER_ATTRIBUTES` が存在する場合だけ Filter ボタンとフォームを描画します。
|
|
335
|
+
|
|
336
|
+
```erb
|
|
337
|
+
<%= admin_filter_controls(
|
|
338
|
+
page: page,
|
|
339
|
+
search_options: search_options
|
|
340
|
+
) %>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
送信先は helper の `path:` / `clear_path:` で明示することもできます。helper の指定は
|
|
344
|
+
dashboard の `FILTER_PATH` / `FILTER_CLEAR_PATH` より優先されます。
|
|
345
|
+
|
|
346
|
+
```erb
|
|
347
|
+
<%= admin_filter_controls(
|
|
348
|
+
page: page,
|
|
349
|
+
path: admin_adspends_path,
|
|
350
|
+
clear_path: admin_adspends_path,
|
|
351
|
+
search_options: search_options,
|
|
352
|
+
filter_locals: { show_discarded_filter: false },
|
|
353
|
+
root_hidden_fields: { "reservation[order]" => current_order[:order] },
|
|
354
|
+
extra_actions: [button_tag("Download PDF", type: "button", class: "button")]
|
|
355
|
+
) %>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
標準 Field は `Text`, `Select`, `Checkbox`, `RadioGroup`, `CheckboxGroup`,
|
|
359
|
+
`DateRange`, `DatetimeRange`, `DatetimeLocalRange`, `Custom` です。
|
|
360
|
+
主な option は次のとおりです。
|
|
361
|
+
|
|
362
|
+
- 共通: `label`, `default`, `if`, `class`, `id`, `placeholder`, `inputmode`, `pattern`
|
|
363
|
+
- 選択系: `collection` または `options`, `select_options`
|
|
364
|
+
- checkbox: `checked_value`, `unchecked_value`
|
|
365
|
+
- checkbox group: `group`
|
|
366
|
+
- range: `from`, `to`, `from_default`, `to_default`, `css_class`
|
|
367
|
+
|
|
368
|
+
`label`, `collection`, `default`, `if` などの option には Proc も指定できます。Proc は
|
|
369
|
+
arity に応じて `call`, `call(view_context)`, `call(view_context, filter_locals)` のいずれかで評価されます。
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
FILTER_ATTRIBUTES = {
|
|
373
|
+
owner_id: YummyGuide::Administrate::Filters::Text.with_options(
|
|
374
|
+
label: "Owner ID",
|
|
375
|
+
inputmode: "numeric",
|
|
376
|
+
if: ->(view, _locals) { !view.current_user&.owner? }
|
|
377
|
+
),
|
|
378
|
+
status: YummyGuide::Administrate::Filters::Select.with_options(
|
|
379
|
+
label: "Status",
|
|
380
|
+
collection: ->(_view, locals) { locals[:status_collection] },
|
|
381
|
+
select_options: { include_blank: true }
|
|
382
|
+
)
|
|
383
|
+
}.freeze
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### カスタム Filter の作成方法
|
|
387
|
+
|
|
388
|
+
既存の Field 型で表現できない場合は、partial を使う方法と独自クラスを定義する方法があります。
|
|
389
|
+
|
|
390
|
+
partial だけで足りる場合は `Custom` を使います。partial には `form`, `form_scope`,
|
|
391
|
+
`field`, `current_values`, `filter_locals` が渡されます。
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
FILTER_ATTRIBUTES = {
|
|
395
|
+
price_range: YummyGuide::Administrate::Filters::Custom.with_options(
|
|
396
|
+
partial: "admin/filters/price_range"
|
|
397
|
+
)
|
|
398
|
+
}.freeze
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```erb
|
|
402
|
+
<tr>
|
|
403
|
+
<td><%= form.label field.name, field.label_text(self, filter_locals) %></td>
|
|
404
|
+
<td>
|
|
405
|
+
<%= form.number_field :min_price, value: current_values["min_price"] %>
|
|
406
|
+
<%= form.number_field :max_price, value: current_values["max_price"] %>
|
|
407
|
+
</td>
|
|
408
|
+
<%= filter_field_clear_cell if respond_to?(:filter_field_clear_cell) %>
|
|
409
|
+
</tr>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`filter_form.js` を読み込んでいる場合、`data-behavior="filter-field-clear"` を持つ
|
|
413
|
+
button は同じ `tr` 内の input / select / textarea / checkbox / radio / datetime
|
|
414
|
+
filter だけをクリアします。
|
|
415
|
+
|
|
416
|
+
複数画面で再利用する filter 型は `YummyGuide::Administrate::Filters::Base` を継承して作ります。
|
|
417
|
+
単一 input なら `input` を実装し、行全体を制御したい場合は `row` または `input_cell` を上書きします。
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
class CurrencyFilter < YummyGuide::Administrate::Filters::Base
|
|
421
|
+
private
|
|
422
|
+
|
|
423
|
+
def input(view_context, form, current_values, locals)
|
|
424
|
+
form.select(
|
|
425
|
+
name,
|
|
426
|
+
view_context.options_for_select(options.fetch(:collection), current_value(current_values)),
|
|
427
|
+
{ include_blank: true },
|
|
428
|
+
html_options(view_context, locals)
|
|
429
|
+
)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
FILTER_ATTRIBUTES = {
|
|
434
|
+
payout_currency: CurrencyFilter.with_options(
|
|
435
|
+
label: "Currency",
|
|
436
|
+
collection: [["JPY", "JPY"], ["USD", "USD"]]
|
|
437
|
+
)
|
|
438
|
+
}.freeze
|
|
439
|
+
```
|
|
440
|
+
|
|
303
441
|
### Number input helper
|
|
304
442
|
|
|
305
443
|
Admin/Administrate 画面では、`number_field` / `number_field_tag` を
|
|
@@ -321,6 +459,7 @@ Admin/Administrate 画面では、`number_field` / `number_field_tag` を
|
|
|
321
459
|
//= require yummy_guide_administrate/clipboards
|
|
322
460
|
//= require yummy_guide_administrate/datetime_input
|
|
323
461
|
//= require yummy_guide_administrate/fixed_submit_actions
|
|
462
|
+
//= require yummy_guide_administrate/filter_controls
|
|
324
463
|
//= require yummy_guide_administrate/filter_form
|
|
325
464
|
//= require yummy_guide_administrate/sticky_left_columns
|
|
326
465
|
//= require yummy_guide_administrate/sticky_table_headers
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
function initialize(root) {
|
|
3
|
+
var scope = root || document;
|
|
4
|
+
scope.querySelectorAll('[data-admin-filter-form="true"]').forEach(function(formEl) {
|
|
5
|
+
if (formEl.dataset.adminFilterControlsInitialized === 'true') return;
|
|
6
|
+
|
|
7
|
+
formEl.dataset.adminFilterControlsInitialized = 'true';
|
|
8
|
+
|
|
9
|
+
formEl.addEventListener('submit', function(event) {
|
|
10
|
+
if (formEl.dataset.submitMode !== 'event') return;
|
|
11
|
+
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
document.dispatchEvent(new CustomEvent('yummy-guide:administrate-filter:submit', {
|
|
14
|
+
detail: {
|
|
15
|
+
form: formEl,
|
|
16
|
+
formData: new FormData(formEl)
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (document.readyState === 'loading') {
|
|
24
|
+
document.addEventListener('DOMContentLoaded', function() { initialize(document); }, { once: true });
|
|
25
|
+
} else {
|
|
26
|
+
initialize(document);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
document.addEventListener('turbo:load', function() { initialize(document); });
|
|
30
|
+
})();
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
formEl.querySelectorAll('[data-behavior="filter-field-clear"]').forEach(function(buttonEl) {
|
|
47
|
+
buttonEl.addEventListener("click", function(event) {
|
|
48
|
+
clearFilterField(formEl, event);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
46
52
|
formEl.querySelectorAll('[data-behavior="checkbox-group-select-all"]').forEach(function(buttonEl) {
|
|
47
53
|
buttonEl.addEventListener("click", function() {
|
|
48
54
|
setCheckboxGroupState(formEl, buttonEl.dataset.target, true);
|
|
@@ -58,6 +64,19 @@
|
|
|
58
64
|
syncDatetimeFilterFields(formEl);
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
function clearFilterField(formEl, event) {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
|
|
70
|
+
var rowEl = event.currentTarget.closest("tr");
|
|
71
|
+
if (!rowEl) return;
|
|
72
|
+
|
|
73
|
+
clearTextControls(rowEl);
|
|
74
|
+
clearChoiceControls(rowEl);
|
|
75
|
+
clearSelectControls(rowEl);
|
|
76
|
+
clearDatetimeFilters(rowEl);
|
|
77
|
+
syncDatetimeFilterFields(formEl);
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
function clearFormFields(formEl) {
|
|
62
81
|
formEl.querySelectorAll("input, select, textarea").forEach(function(fieldEl) {
|
|
63
82
|
if (fieldEl.disabled || fieldEl.type === "hidden" || fieldEl.type === "submit") {
|
|
@@ -86,6 +105,57 @@
|
|
|
86
105
|
syncDatetimeFilterFields(formEl);
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
function clearTextControls(rowEl) {
|
|
109
|
+
rowEl.querySelectorAll("input, textarea").forEach(function(inputEl) {
|
|
110
|
+
var inputType = (inputEl.getAttribute("type") || "").toLowerCase();
|
|
111
|
+
|
|
112
|
+
if (inputType === "checkbox" || inputType === "radio") return;
|
|
113
|
+
if (inputType === "hidden" && inputEl.dataset.datetimePart !== "combined") return;
|
|
114
|
+
|
|
115
|
+
inputEl.value = "";
|
|
116
|
+
inputEl.setAttribute("value", "");
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clearChoiceControls(rowEl) {
|
|
121
|
+
rowEl.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(function(inputEl) {
|
|
122
|
+
inputEl.checked = false;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function clearSelectControls(rowEl) {
|
|
127
|
+
rowEl.querySelectorAll("select").forEach(function(selectEl) {
|
|
128
|
+
if (selectEl.querySelector('option[value=""]')) {
|
|
129
|
+
selectEl.value = "";
|
|
130
|
+
} else if (selectEl.querySelector('option[value="all"]')) {
|
|
131
|
+
selectEl.value = "all";
|
|
132
|
+
} else {
|
|
133
|
+
selectEl.selectedIndex = -1;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clearDatetimeFilters(rowEl) {
|
|
139
|
+
rowEl.querySelectorAll("[data-datetime-filter]").forEach(function(groupEl) {
|
|
140
|
+
var combinedEl = groupEl.querySelector('[data-datetime-part="combined"]');
|
|
141
|
+
var dateEl = groupEl.querySelector('[data-datetime-part="date"]');
|
|
142
|
+
|
|
143
|
+
if (combinedEl) {
|
|
144
|
+
combinedEl.value = "";
|
|
145
|
+
combinedEl.setAttribute("value", "");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dateEl) {
|
|
149
|
+
dateEl.value = "";
|
|
150
|
+
dateEl.setAttribute("value", "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
clearDatetimeTimeParts(groupEl);
|
|
154
|
+
syncDatetimeTimeDisabledState(groupEl);
|
|
155
|
+
syncBlankMinuteOptionState(groupEl);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
89
159
|
function setCheckboxGroupState(formEl, groupName, checked) {
|
|
90
160
|
if (!groupName) return;
|
|
91
161
|
|
|
@@ -216,4 +286,3 @@
|
|
|
216
286
|
|
|
217
287
|
document.addEventListener("turbo:load", initializeFromDocument);
|
|
218
288
|
})();
|
|
219
|
-
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YummyGuide
|
|
4
|
+
module Administrate
|
|
5
|
+
module FilterControlsHelper
|
|
6
|
+
def admin_filter_controls(
|
|
7
|
+
dashboard: nil,
|
|
8
|
+
page: nil,
|
|
9
|
+
path: nil,
|
|
10
|
+
clear_path: nil,
|
|
11
|
+
search_options: {},
|
|
12
|
+
form: :search_options,
|
|
13
|
+
method: :get,
|
|
14
|
+
hidden_fields: {},
|
|
15
|
+
root_hidden_fields: {},
|
|
16
|
+
filter_locals: {},
|
|
17
|
+
extra_actions: [],
|
|
18
|
+
button_label: "Filter",
|
|
19
|
+
title: "Filter Options",
|
|
20
|
+
submit_label: "Filter"
|
|
21
|
+
)
|
|
22
|
+
dashboard ||= admin_filter_dashboard_from_page(page)
|
|
23
|
+
scope = form.to_s
|
|
24
|
+
current_values = admin_filter_current_values(search_options)
|
|
25
|
+
base_locals = filter_locals.merge(
|
|
26
|
+
dashboard: dashboard,
|
|
27
|
+
page: page,
|
|
28
|
+
search_options: search_options,
|
|
29
|
+
current_values: current_values,
|
|
30
|
+
form_scope: scope
|
|
31
|
+
)
|
|
32
|
+
fields = admin_filter_visible_fields(dashboard, base_locals)
|
|
33
|
+
return if fields.blank?
|
|
34
|
+
|
|
35
|
+
path ||= admin_filter_dashboard_setting(dashboard, :filter_path, :FILTER_PATH, base_locals)
|
|
36
|
+
raise ArgumentError, "admin_filter_controls requires path or dashboard FILTER_PATH" if path.blank?
|
|
37
|
+
|
|
38
|
+
clear_path ||= admin_filter_dashboard_setting(dashboard, :filter_clear_path, :FILTER_CLEAR_PATH, base_locals) || path
|
|
39
|
+
locals = filter_locals.merge(
|
|
40
|
+
dashboard: dashboard,
|
|
41
|
+
page: page,
|
|
42
|
+
path: path,
|
|
43
|
+
clear_path: clear_path,
|
|
44
|
+
search_options: search_options,
|
|
45
|
+
current_values: current_values,
|
|
46
|
+
form_scope: scope
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
safe_join([
|
|
50
|
+
content_tag(:div, "", class: "modal_overlay"),
|
|
51
|
+
content_tag(:div, id: "reserv-filter-options") do
|
|
52
|
+
safe_join([
|
|
53
|
+
link_to(button_label, "javascript:void(0)", class: "button"),
|
|
54
|
+
admin_filter_form(
|
|
55
|
+
fields: fields,
|
|
56
|
+
scope: scope,
|
|
57
|
+
path: path,
|
|
58
|
+
clear_path: clear_path,
|
|
59
|
+
method: method,
|
|
60
|
+
current_values: current_values,
|
|
61
|
+
hidden_fields: hidden_fields,
|
|
62
|
+
root_hidden_fields: root_hidden_fields,
|
|
63
|
+
locals: locals,
|
|
64
|
+
extra_actions: extra_actions,
|
|
65
|
+
title: title,
|
|
66
|
+
submit_label: submit_label
|
|
67
|
+
)
|
|
68
|
+
])
|
|
69
|
+
end
|
|
70
|
+
])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def admin_filter_dashboard_from_page(page)
|
|
74
|
+
return page.dashboard if page.respond_to?(:dashboard)
|
|
75
|
+
return unless page.respond_to?(:instance_variable_defined?) && page.instance_variable_defined?(:@dashboard)
|
|
76
|
+
|
|
77
|
+
page.instance_variable_get(:@dashboard)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def admin_filter_dashboard_setting(dashboard, method_name, constant_name, locals)
|
|
81
|
+
return if dashboard.blank?
|
|
82
|
+
|
|
83
|
+
value =
|
|
84
|
+
if dashboard.respond_to?(method_name)
|
|
85
|
+
dashboard.public_send(method_name)
|
|
86
|
+
else
|
|
87
|
+
admin_filter_dashboard_constant(dashboard, constant_name)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
admin_filter_evaluate_value(value, locals)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def admin_filter_dashboard_constant(dashboard, constant_name)
|
|
94
|
+
target = dashboard.is_a?(Class) ? dashboard : dashboard.class
|
|
95
|
+
target.const_get(constant_name, false) if target.const_defined?(constant_name, false)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def admin_filter_visible_fields(dashboard, locals)
|
|
99
|
+
return [] if dashboard.blank?
|
|
100
|
+
|
|
101
|
+
YummyGuide::Administrate::Filters::Resolver
|
|
102
|
+
.attributes_for(dashboard)
|
|
103
|
+
.values
|
|
104
|
+
.select { |field| field.visible?(self, locals) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def admin_filter_form(fields:, scope:, path:, clear_path:, method:, current_values:, hidden_fields:, root_hidden_fields:, locals:, extra_actions:, title:, submit_label:)
|
|
108
|
+
form_with(
|
|
109
|
+
url: path,
|
|
110
|
+
scope: scope,
|
|
111
|
+
method: method,
|
|
112
|
+
html: {
|
|
113
|
+
class: "filter-form",
|
|
114
|
+
data: {
|
|
115
|
+
yummy_guide_administrate_filter_form: true,
|
|
116
|
+
admin_filter_form: true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
) do |f|
|
|
120
|
+
safe_join([
|
|
121
|
+
admin_filter_hidden_fields(scope, hidden_fields, root_hidden_fields),
|
|
122
|
+
content_tag(:h2, title, class: "filter-form__title"),
|
|
123
|
+
content_tag(:div, class: "filter-form__body") do
|
|
124
|
+
content_tag(:table, class: "filter_table") do
|
|
125
|
+
safe_join(fields.map { |field| field.row(self, f, scope, current_values, locals) })
|
|
126
|
+
end
|
|
127
|
+
end,
|
|
128
|
+
content_tag(:div, class: "filter-form__actions") do
|
|
129
|
+
safe_join([
|
|
130
|
+
link_to("Clear", clear_path, class: "button button--outline-primary"),
|
|
131
|
+
Array(extra_actions),
|
|
132
|
+
f.submit(submit_label, class: "submit_filter")
|
|
133
|
+
].flatten)
|
|
134
|
+
end
|
|
135
|
+
])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def admin_filter_hidden_fields(scope, hidden_fields, root_hidden_fields)
|
|
140
|
+
root_tags = admin_filter_evaluated_hash(root_hidden_fields).map do |key, value|
|
|
141
|
+
hidden_field_tag(key, value)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
scoped_tags = admin_filter_evaluated_hash(hidden_fields).map do |key, value|
|
|
145
|
+
hidden_field_tag("#{scope}[#{key}]", value)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
safe_join(root_tags + scoped_tags)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def admin_filter_evaluated_hash(values)
|
|
152
|
+
values = admin_filter_evaluate_value(values, {})
|
|
153
|
+
(values || {}).to_h
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def admin_filter_evaluate_value(value, locals)
|
|
157
|
+
return value unless value.respond_to?(:call)
|
|
158
|
+
|
|
159
|
+
case value.arity
|
|
160
|
+
when 0
|
|
161
|
+
value.call
|
|
162
|
+
when 1
|
|
163
|
+
value.call(self)
|
|
164
|
+
else
|
|
165
|
+
value.call(self, locals)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def admin_filter_current_values(raw_values)
|
|
170
|
+
values =
|
|
171
|
+
if raw_values.respond_to?(:to_unsafe_h)
|
|
172
|
+
raw_values.to_unsafe_h
|
|
173
|
+
elsif raw_values.respond_to?(:to_h)
|
|
174
|
+
raw_values.to_h
|
|
175
|
+
else
|
|
176
|
+
raw_values || {}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
values.deep_stringify_keys
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -11,17 +11,20 @@ module YummyGuide
|
|
|
11
11
|
yummy_guide_administrate/clipboards.js
|
|
12
12
|
yummy_guide_administrate/datetime_input.js
|
|
13
13
|
yummy_guide_administrate/fixed_submit_actions.js
|
|
14
|
+
yummy_guide_administrate/filter_controls.js
|
|
14
15
|
yummy_guide_administrate/filter_form.js
|
|
15
16
|
yummy_guide_administrate/sticky_left_columns.js
|
|
16
17
|
yummy_guide_administrate/sticky_table_headers.js
|
|
17
18
|
]
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
initializer "yummy_guide.administrate.
|
|
21
|
+
initializer "yummy_guide.administrate.helpers" do |app|
|
|
21
22
|
app.config.to_prepare do
|
|
22
23
|
next unless defined?(::Administrate::ApplicationController)
|
|
23
24
|
|
|
24
25
|
::Administrate::ApplicationController.helper YummyGuide::Administrate::NumberInputHelper
|
|
26
|
+
::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterFormHelper
|
|
27
|
+
::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterControlsHelper
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
end
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YummyGuide
|
|
4
|
+
module Administrate
|
|
5
|
+
module Filters
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :name, :options
|
|
8
|
+
|
|
9
|
+
def self.with_options(options = {})
|
|
10
|
+
new(options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(options = {})
|
|
14
|
+
@options = options.symbolize_keys
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def with_name(name)
|
|
18
|
+
copy = dup
|
|
19
|
+
copy.instance_variable_set(:@name, name.to_sym)
|
|
20
|
+
copy
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visible?(view_context, locals)
|
|
24
|
+
condition = options.fetch(:if, true)
|
|
25
|
+
evaluate_option(condition, view_context, locals) != false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def row(view_context, form, scope, current_values, locals)
|
|
29
|
+
view_context.content_tag(:tr) do
|
|
30
|
+
view_context.safe_join([
|
|
31
|
+
label_cell(view_context, form, locals),
|
|
32
|
+
input_cell(view_context, form, scope, current_values, locals),
|
|
33
|
+
clear_cell(view_context)
|
|
34
|
+
])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def label_text(view_context, locals)
|
|
39
|
+
evaluated_label = evaluate_option(options[:label], view_context, locals)
|
|
40
|
+
return evaluated_label if evaluated_label.present?
|
|
41
|
+
|
|
42
|
+
name.to_s.humanize
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
def label_cell(view_context, form, locals)
|
|
48
|
+
view_context.content_tag(:td, form.label(name, label_text(view_context, locals)))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def input_cell(view_context, form, _scope, current_values, locals)
|
|
52
|
+
view_context.content_tag(:td, input(view_context, form, current_values, locals))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear_cell(view_context)
|
|
56
|
+
return view_context.filter_field_clear_cell if view_context.respond_to?(:filter_field_clear_cell)
|
|
57
|
+
|
|
58
|
+
view_context.content_tag(:td, "", class: "filter-table__clear")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def current_value(current_values)
|
|
62
|
+
value = current_values[name.to_s]
|
|
63
|
+
value.presence || options[:default]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def html_options(view_context, locals)
|
|
67
|
+
options.slice(:placeholder, :inputmode, :pattern, :class, :id, :autocomplete).transform_values do |value|
|
|
68
|
+
evaluate_option(value, view_context, locals)
|
|
69
|
+
end.compact
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def evaluate_option(value, view_context, locals)
|
|
73
|
+
return value unless value.respond_to?(:call)
|
|
74
|
+
|
|
75
|
+
case value.arity
|
|
76
|
+
when 0
|
|
77
|
+
value.call
|
|
78
|
+
when 1
|
|
79
|
+
value.call(view_context)
|
|
80
|
+
else
|
|
81
|
+
value.call(view_context, locals)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def normalize_options(view_context, locals)
|
|
86
|
+
raw_options = evaluate_option(options[:collection] || options[:options], view_context, locals)
|
|
87
|
+
|
|
88
|
+
Array(raw_options).map do |option|
|
|
89
|
+
if option.is_a?(Array)
|
|
90
|
+
option
|
|
91
|
+
else
|
|
92
|
+
[option, option]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def input(_view_context, _form, _current_values, _locals)
|
|
98
|
+
raise NotImplementedError
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class Text < Base
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def input(view_context, form, current_values, locals)
|
|
106
|
+
form.text_field(name, html_options(view_context, locals).merge(value: current_value(current_values)))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class Select < Base
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def input(view_context, form, current_values, locals)
|
|
114
|
+
selected = current_value(current_values)
|
|
115
|
+
form.select(
|
|
116
|
+
name,
|
|
117
|
+
view_context.options_for_select(normalize_options(view_context, locals), selected),
|
|
118
|
+
evaluate_option(options[:select_options] || {}, view_context, locals),
|
|
119
|
+
html_options(view_context, locals)
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class Checkbox < Base
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def input(view_context, form, current_values, locals)
|
|
128
|
+
checked_value = options.fetch(:checked_value, "true")
|
|
129
|
+
unchecked_value = options.fetch(:unchecked_value, "false")
|
|
130
|
+
checked = ActiveModel::Type::Boolean.new.cast(current_value(current_values))
|
|
131
|
+
form.check_box(name, html_options(view_context, locals).merge(checked: checked), checked_value, unchecked_value)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class RadioGroup < Base
|
|
136
|
+
protected
|
|
137
|
+
|
|
138
|
+
def input_cell(view_context, _form, scope, current_values, locals)
|
|
139
|
+
selected = current_value(current_values).to_s
|
|
140
|
+
controls = normalize_options(view_context, locals).map do |label, value|
|
|
141
|
+
value_string = value.to_s
|
|
142
|
+
id = "#{scope}_#{name}_#{value_string.parameterize(separator: "_")}"
|
|
143
|
+
view_context.content_tag(:label, style: "display: flex; align-items: center; gap: 6px;") do
|
|
144
|
+
view_context.safe_join([
|
|
145
|
+
view_context.radio_button_tag("#{scope}[#{name}]", value, selected == value_string, id: id),
|
|
146
|
+
view_context.content_tag(:span, label)
|
|
147
|
+
])
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
view_context.content_tag(:td, view_context.safe_join(controls))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class CheckboxGroup < Base
|
|
156
|
+
protected
|
|
157
|
+
|
|
158
|
+
def input_cell(view_context, _form, scope, current_values, locals)
|
|
159
|
+
selected_values = Array(current_values[name.to_s]).map(&:to_s).reject(&:blank?)
|
|
160
|
+
group_name = (options[:group] || name).to_s.dasherize
|
|
161
|
+
controls = normalize_options(view_context, locals).map do |label, value|
|
|
162
|
+
value_string = value.to_s
|
|
163
|
+
id = "#{scope}_#{name}_#{value_string.parameterize(separator: "_")}"
|
|
164
|
+
view_context.content_tag(:label, style: "display: flex; align-items: center; gap: 6px;") do
|
|
165
|
+
view_context.safe_join([
|
|
166
|
+
view_context.check_box_tag(
|
|
167
|
+
"#{scope}[#{name}][]",
|
|
168
|
+
value,
|
|
169
|
+
selected_values.include?(value_string),
|
|
170
|
+
id: id,
|
|
171
|
+
data: { checkbox_group_item: group_name }
|
|
172
|
+
),
|
|
173
|
+
view_context.content_tag(:span, label)
|
|
174
|
+
])
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
view_context.content_tag(:td) do
|
|
179
|
+
view_context.content_tag(:div, data: { checkbox_group: group_name }) do
|
|
180
|
+
view_context.content_tag(:div, view_context.safe_join(controls), class: "filter-checkbox-group__options")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def clear_cell(view_context)
|
|
186
|
+
group_name = (options[:group] || name).to_s.dasherize
|
|
187
|
+
return view_context.checkbox_group_action_cell(target: group_name) if view_context.respond_to?(:checkbox_group_action_cell)
|
|
188
|
+
|
|
189
|
+
super
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class DatetimeRange < Base
|
|
194
|
+
def row(view_context, form, scope, current_values, locals)
|
|
195
|
+
view_context.content_tag(:tr) do
|
|
196
|
+
view_context.safe_join([
|
|
197
|
+
label_cell(view_context, form, locals),
|
|
198
|
+
view_context.content_tag(:td, range_inputs(view_context, scope, current_values, locals)),
|
|
199
|
+
clear_cell(view_context)
|
|
200
|
+
])
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def range_inputs(view_context, scope, current_values, locals)
|
|
207
|
+
from_name = (options[:from] || :"start_#{name}").to_sym
|
|
208
|
+
to_name = (options[:to] || :"end_#{name}").to_sym
|
|
209
|
+
css_class = evaluate_option(options[:css_class] || "#{scope}_#{name}", view_context, locals)
|
|
210
|
+
|
|
211
|
+
view_context.safe_join([
|
|
212
|
+
view_context.render(
|
|
213
|
+
"yummy_guide/administrate/filter_forms/datetime_field",
|
|
214
|
+
form_scope: scope,
|
|
215
|
+
field_name: from_name,
|
|
216
|
+
current_value: current_values[from_name.to_s],
|
|
217
|
+
css_class: css_class,
|
|
218
|
+
end_target: to_name
|
|
219
|
+
),
|
|
220
|
+
view_context.content_tag(:p, "〜", class: "filter-datetime-range-separator"),
|
|
221
|
+
view_context.render(
|
|
222
|
+
"yummy_guide/administrate/filter_forms/datetime_field",
|
|
223
|
+
form_scope: scope,
|
|
224
|
+
field_name: to_name,
|
|
225
|
+
current_value: current_values[to_name.to_s],
|
|
226
|
+
css_class: css_class,
|
|
227
|
+
end_of_day: true
|
|
228
|
+
)
|
|
229
|
+
])
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
class DateRange < Base
|
|
234
|
+
def row(view_context, form, _scope, current_values, locals)
|
|
235
|
+
from_name = (options[:from] || :"start_#{name}").to_sym
|
|
236
|
+
to_name = (options[:to] || :"end_#{name}").to_sym
|
|
237
|
+
|
|
238
|
+
view_context.content_tag(:tr) do
|
|
239
|
+
view_context.safe_join([
|
|
240
|
+
label_cell(view_context, form, locals),
|
|
241
|
+
view_context.content_tag(:td) do
|
|
242
|
+
view_context.safe_join([
|
|
243
|
+
form.date_field(from_name, value: current_values[from_name.to_s].presence || evaluate_option(options[:from_default], view_context, locals)),
|
|
244
|
+
view_context.content_tag(:span, "〜"),
|
|
245
|
+
form.date_field(to_name, value: current_values[to_name.to_s].presence || evaluate_option(options[:to_default], view_context, locals))
|
|
246
|
+
])
|
|
247
|
+
end,
|
|
248
|
+
clear_cell(view_context)
|
|
249
|
+
])
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class DatetimeLocalRange < Base
|
|
255
|
+
def row(view_context, form, _scope, current_values, locals)
|
|
256
|
+
from_name = (options[:from] || :"start_#{name}").to_sym
|
|
257
|
+
to_name = (options[:to] || :"end_#{name}").to_sym
|
|
258
|
+
|
|
259
|
+
view_context.content_tag(:tr) do
|
|
260
|
+
view_context.safe_join([
|
|
261
|
+
label_cell(view_context, form, locals),
|
|
262
|
+
view_context.content_tag(:td) do
|
|
263
|
+
view_context.safe_join([
|
|
264
|
+
form.datetime_local_field(from_name, value: current_values[from_name.to_s].presence || evaluate_option(options[:from_default], view_context, locals)),
|
|
265
|
+
view_context.content_tag(:p, "〜", style: "text-align: center; margin: 0;"),
|
|
266
|
+
form.datetime_local_field(to_name, value: current_values[to_name.to_s].presence || evaluate_option(options[:to_default], view_context, locals))
|
|
267
|
+
])
|
|
268
|
+
end,
|
|
269
|
+
clear_cell(view_context)
|
|
270
|
+
])
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
class Custom < Base
|
|
276
|
+
def row(view_context, form, scope, current_values, locals)
|
|
277
|
+
view_context.render(
|
|
278
|
+
options.fetch(:partial),
|
|
279
|
+
form: form,
|
|
280
|
+
form_scope: scope,
|
|
281
|
+
field: self,
|
|
282
|
+
current_values: current_values,
|
|
283
|
+
filter_locals: locals
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
module Resolver
|
|
289
|
+
module_function
|
|
290
|
+
|
|
291
|
+
def attributes_for(dashboard)
|
|
292
|
+
source = dashboard.respond_to?(:filter_attributes) ? dashboard.filter_attributes : constant_value(dashboard, :FILTER_ATTRIBUTES)
|
|
293
|
+
normalize_attributes(source || {})
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def normalize_attributes(attributes)
|
|
297
|
+
attributes.to_h.map do |name, field|
|
|
298
|
+
[name.to_sym, normalize_field(name, field)]
|
|
299
|
+
end.to_h
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def normalize_field(name, field)
|
|
303
|
+
if field.respond_to?(:with_name)
|
|
304
|
+
field.with_name(name)
|
|
305
|
+
elsif field.is_a?(Class) && field < Base
|
|
306
|
+
field.with_options.with_name(name)
|
|
307
|
+
else
|
|
308
|
+
raise ArgumentError, "Unsupported filter field for #{name}: #{field.inspect}"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def constant_value(dashboard, name)
|
|
313
|
+
target = dashboard.is_a?(Class) ? dashboard : dashboard.class
|
|
314
|
+
target.const_get(name, false) if target.const_defined?(name, false)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -4,7 +4,10 @@ require "rails"
|
|
|
4
4
|
require "administrate"
|
|
5
5
|
|
|
6
6
|
require_relative "../../app/helpers/yummy_guide/administrate/number_input_helper"
|
|
7
|
+
require_relative "../../app/helpers/yummy_guide/administrate/filter_form_helper"
|
|
8
|
+
require_relative "../../app/helpers/yummy_guide/administrate/filter_controls_helper"
|
|
7
9
|
require_relative "administrate/version"
|
|
10
|
+
require_relative "administrate/filters"
|
|
8
11
|
require_relative "administrate/engine"
|
|
9
12
|
|
|
10
13
|
module YummyGuide
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
RSpec.describe YummyGuide::Administrate::FilterControlsHelper do
|
|
7
|
+
subject(:helper_host) do
|
|
8
|
+
Class.new do
|
|
9
|
+
include ActionView::Context
|
|
10
|
+
include ActionView::Helpers::FormHelper
|
|
11
|
+
include ActionView::Helpers::FormOptionsHelper
|
|
12
|
+
include ActionView::Helpers::FormTagHelper
|
|
13
|
+
include ActionView::Helpers::OutputSafetyHelper
|
|
14
|
+
include ActionView::Helpers::TagHelper
|
|
15
|
+
include ActionView::Helpers::UrlHelper
|
|
16
|
+
include YummyGuide::Administrate::FilterControlsHelper
|
|
17
|
+
end.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fragment(html)
|
|
21
|
+
Nokogiri::HTML.fragment(html)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#admin_filter_controls" do
|
|
25
|
+
# dashboard の FILTER_ATTRIBUTES から Filter ボタンとフォーム項目を描画することを確認する
|
|
26
|
+
it "renders controls from dashboard filter attributes" do
|
|
27
|
+
dashboard = Class.new
|
|
28
|
+
dashboard.const_set(
|
|
29
|
+
:FILTER_ATTRIBUTES,
|
|
30
|
+
{
|
|
31
|
+
keyword: YummyGuide::Administrate::Filters::Text.with_options(label: "Keyword"),
|
|
32
|
+
status: YummyGuide::Administrate::Filters::Select.with_options(
|
|
33
|
+
label: "Status",
|
|
34
|
+
collection: [["Open", "open"], ["Closed", "closed"]]
|
|
35
|
+
)
|
|
36
|
+
}.freeze
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
html = helper_host.admin_filter_controls(
|
|
40
|
+
dashboard: dashboard,
|
|
41
|
+
path: "/admin/resources",
|
|
42
|
+
search_options: { keyword: "tokyo", status: "closed" }
|
|
43
|
+
)
|
|
44
|
+
document = fragment(html)
|
|
45
|
+
|
|
46
|
+
expect(document.at_css("#reserv-filter-options > a.button").text).to eq("Filter")
|
|
47
|
+
expect(document.at_css("form.filter-form")["action"]).to eq("/admin/resources")
|
|
48
|
+
expect(document.at_css('input[name="search_options[keyword]"]')["value"]).to eq("tokyo")
|
|
49
|
+
expect(document.at_css('select[name="search_options[status]"] option[selected]')["value"]).to eq("closed")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# dashboard に定義した FILTER_PATH をフィルター送信先として利用できることを確認する
|
|
53
|
+
it "uses dashboard filter path when no explicit path is passed" do
|
|
54
|
+
dashboard = Class.new
|
|
55
|
+
dashboard.const_set(
|
|
56
|
+
:FILTER_ATTRIBUTES,
|
|
57
|
+
{
|
|
58
|
+
keyword: YummyGuide::Administrate::Filters::Text.with_options(label: "Keyword")
|
|
59
|
+
}.freeze
|
|
60
|
+
)
|
|
61
|
+
dashboard.const_set(:FILTER_PATH, ->(view, _locals) { view.filter_path })
|
|
62
|
+
|
|
63
|
+
helper_host.define_singleton_method(:filter_path) { "/admin/dashboard-filters" }
|
|
64
|
+
|
|
65
|
+
html = helper_host.admin_filter_controls(dashboard: dashboard)
|
|
66
|
+
document = fragment(html)
|
|
67
|
+
|
|
68
|
+
expect(document.at_css("form.filter-form")["action"]).to eq("/admin/dashboard-filters")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# dashboard にフィルター定義が存在しない場合は Filter ボタンを描画しないことを確認する
|
|
72
|
+
it "does not render controls when dashboard has no filter attributes" do
|
|
73
|
+
expect(helper_host.admin_filter_controls(dashboard: Class.new, path: "/admin/resources")).to be_nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe YummyGuide::Administrate::Filters do
|
|
6
|
+
describe YummyGuide::Administrate::Filters::Resolver do
|
|
7
|
+
# dashboard に定義した Field クラス型フィルターへ属性名が付与されることを確認する
|
|
8
|
+
it "normalizes dashboard filter attributes with field names" do
|
|
9
|
+
dashboard = Class.new
|
|
10
|
+
dashboard.const_set(
|
|
11
|
+
:FILTER_ATTRIBUTES,
|
|
12
|
+
{
|
|
13
|
+
keyword: YummyGuide::Administrate::Filters::Text.with_options(label: "Keyword")
|
|
14
|
+
}.freeze
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
fields = described_class.attributes_for(dashboard)
|
|
18
|
+
|
|
19
|
+
expect(fields[:keyword]).to be_a(YummyGuide::Administrate::Filters::Text)
|
|
20
|
+
expect(fields[:keyword].name).to eq(:keyword)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#visible?" do
|
|
25
|
+
# view context に依存する条件でフィルター表示を切り替えられることを確認する
|
|
26
|
+
it "evaluates visibility conditions with the view context" do
|
|
27
|
+
view_context = double("view_context", owner?: true)
|
|
28
|
+
field = YummyGuide::Administrate::Filters::Text
|
|
29
|
+
.with_options(if: ->(view, _locals) { !view.owner? })
|
|
30
|
+
.with_name(:owner_name)
|
|
31
|
+
|
|
32
|
+
expect(field.visible?(view_context, {})).to be(false)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yummy-guide-generic-administrate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- akatsuki-kk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: administrate
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- app/assets/images/yummy_guide_administrate/icon-copy.svg
|
|
93
93
|
- app/assets/javascripts/yummy_guide_administrate/clipboards.js
|
|
94
94
|
- app/assets/javascripts/yummy_guide_administrate/datetime_input.js
|
|
95
|
+
- app/assets/javascripts/yummy_guide_administrate/filter_controls.js
|
|
95
96
|
- app/assets/javascripts/yummy_guide_administrate/filter_form.js
|
|
96
97
|
- app/assets/javascripts/yummy_guide_administrate/fixed_submit_actions.js
|
|
97
98
|
- app/assets/javascripts/yummy_guide_administrate/sticky_left_columns.js
|
|
@@ -108,6 +109,7 @@ files:
|
|
|
108
109
|
- app/fields/yummy_guide/administrate/fields/version_whodunnit_field.rb
|
|
109
110
|
- app/helpers/yummy_guide/administrate/collection_helper.rb
|
|
110
111
|
- app/helpers/yummy_guide/administrate/datetime_input_helper.rb
|
|
112
|
+
- app/helpers/yummy_guide/administrate/filter_controls_helper.rb
|
|
111
113
|
- app/helpers/yummy_guide/administrate/filter_form_helper.rb
|
|
112
114
|
- app/helpers/yummy_guide/administrate/number_input_helper.rb
|
|
113
115
|
- app/views/fields/number/_form.html.erb
|
|
@@ -129,6 +131,7 @@ files:
|
|
|
129
131
|
- lib/yummy/guide/generic/administrate.rb
|
|
130
132
|
- lib/yummy_guide/administrate.rb
|
|
131
133
|
- lib/yummy_guide/administrate/engine.rb
|
|
134
|
+
- lib/yummy_guide/administrate/filters.rb
|
|
132
135
|
- lib/yummy_guide/administrate/version.rb
|
|
133
136
|
- spec/spec_helper.rb
|
|
134
137
|
- spec/views/fields/number/form_spec.rb
|
|
@@ -140,7 +143,9 @@ files:
|
|
|
140
143
|
- spec/yummy_guide/administrate/fields/json_pretty_field_spec.rb
|
|
141
144
|
- spec/yummy_guide/administrate/fields/version_item_field_spec.rb
|
|
142
145
|
- spec/yummy_guide/administrate/fields/version_whodunnit_field_spec.rb
|
|
146
|
+
- spec/yummy_guide/administrate/filter_controls_helper_spec.rb
|
|
143
147
|
- spec/yummy_guide/administrate/filter_form_helper_spec.rb
|
|
148
|
+
- spec/yummy_guide/administrate/filters_spec.rb
|
|
144
149
|
- spec/yummy_guide/administrate/number_input_helper_spec.rb
|
|
145
150
|
- yummy-guide-generic-administrate.gemspec
|
|
146
151
|
homepage: https://github.com/yummy-guide/generic-administrate
|