yummy-guide-generic-administrate 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +155 -0
- data/app/assets/javascripts/yummy_guide_administrate/filter_controls.js +30 -0
- data/app/helpers/yummy_guide/administrate/filter_controls_helper.rb +183 -0
- data/app/helpers/yummy_guide/administrate/number_input_helper.rb +37 -0
- data/lib/yummy_guide/administrate/engine.rb +11 -0
- data/lib/yummy_guide/administrate/filters.rb +319 -0
- data/lib/yummy_guide/administrate/version.rb +1 -1
- data/lib/yummy_guide/administrate.rb +4 -1
- data/spec/yummy_guide/administrate/filter_controls_helper_spec.rb +76 -0
- data/spec/yummy_guide/administrate/filters_spec.rb +35 -0
- data/spec/yummy_guide/administrate/number_input_helper_spec.rb +154 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8906e8c09072b22eebdaeaa49b0fb57a395aa30693bf6f9986ebc3c0687e254d
|
|
4
|
+
data.tar.gz: 802a739018ffc6e269a688a459464e17e213c6d6ee195d7aacd6e396aa74f97a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b37b6a8dfbe765ab42af40cc6c2db86a7f568c049af2a6c3a47d3ef5871bacb2351896b15b9f1368ad446a97f6d9e1a3eab44cbd1d045b1ab2efd68a2fbfbb31
|
|
7
|
+
data.tar.gz: 84255b48f37a7b0926e74228cd24b1ff0d5080f7c7dedc89b7b7ecedf86eff3a871299bbd002882eefe9ad3f0b4800ea3b706eae7ddd39806ca11d2317fecdf0
|
data/README.md
CHANGED
|
@@ -41,8 +41,12 @@ 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
|
|
48
|
+
- `YummyGuide::Administrate::NumberInputHelper`
|
|
49
|
+
- 管理画面フォーム用の number input text 化 helper
|
|
46
50
|
- 共通 partial / assets
|
|
47
51
|
- collection partial
|
|
48
52
|
- fixed table header partial
|
|
@@ -50,6 +54,7 @@ bundle install
|
|
|
50
54
|
- `clipboards.js`
|
|
51
55
|
- `datetime_input.js`
|
|
52
56
|
- `fixed_submit_actions.js`
|
|
57
|
+
- `filter_controls.js`
|
|
53
58
|
- `filter_form.js`
|
|
54
59
|
- `sticky_left_columns.js`
|
|
55
60
|
- `sticky_table_headers.js`
|
|
@@ -101,10 +106,16 @@ class Admin::ApplicationController < Administrate::ApplicationController
|
|
|
101
106
|
|
|
102
107
|
helper YummyGuide::Administrate::CollectionHelper
|
|
103
108
|
helper YummyGuide::Administrate::DatetimeInputHelper
|
|
109
|
+
helper YummyGuide::Administrate::FilterControlsHelper
|
|
104
110
|
helper YummyGuide::Administrate::FilterFormHelper
|
|
111
|
+
helper YummyGuide::Administrate::NumberInputHelper
|
|
105
112
|
end
|
|
106
113
|
```
|
|
107
114
|
|
|
115
|
+
`NumberInputHelper` は `Administrate::ApplicationController` の view helper として
|
|
116
|
+
自動適用されます。`Administrate::ApplicationController` を継承しない独自 admin
|
|
117
|
+
controller で同じ挙動が必要な場合だけ、上記のように明示的に読み込んでください。
|
|
118
|
+
|
|
108
119
|
### Collection partial
|
|
109
120
|
|
|
110
121
|
ホストアプリ側の `app/views/administrate/application/_collection.html.erb` から、
|
|
@@ -293,6 +304,149 @@ def search_term
|
|
|
293
304
|
end
|
|
294
305
|
```
|
|
295
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 型は `YummyGuide::Administrate::Filters::Base` を継承して作ります。
|
|
413
|
+
単一 input なら `input` を実装し、行全体を制御したい場合は `row` または `input_cell` を上書きします。
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
class CurrencyFilter < YummyGuide::Administrate::Filters::Base
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
def input(view_context, form, current_values, locals)
|
|
420
|
+
form.select(
|
|
421
|
+
name,
|
|
422
|
+
view_context.options_for_select(options.fetch(:collection), current_value(current_values)),
|
|
423
|
+
{ include_blank: true },
|
|
424
|
+
html_options(view_context, locals)
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
FILTER_ATTRIBUTES = {
|
|
430
|
+
payout_currency: CurrencyFilter.with_options(
|
|
431
|
+
label: "Currency",
|
|
432
|
+
collection: [["JPY", "JPY"], ["USD", "USD"]]
|
|
433
|
+
)
|
|
434
|
+
}.freeze
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Number input helper
|
|
438
|
+
|
|
439
|
+
Admin/Administrate 画面では、`number_field` / `number_field_tag` を
|
|
440
|
+
`type="text"` かつ `inputmode="decimal"` の input として描画します。これにより、
|
|
441
|
+
ブラウザ標準の number spinner や mouse wheel による意図しない数値変更を避けます。
|
|
442
|
+
|
|
443
|
+
`class`, `id`, `name`, `value`, `data`, `required`, `disabled`, `readonly`,
|
|
444
|
+
`placeholder` などの通常 option は維持されます。`min`, `max`, `step`, `in`,
|
|
445
|
+
`within` は `type="number"` 前提のブラウザ制御なので出力しません。
|
|
446
|
+
|
|
447
|
+
`range_field` / `range_field_tag` は対象外で、従来どおり `type="range"` として描画
|
|
448
|
+
されます。raw HTML の `<input type="number">` は helper を通らないため対象外です。
|
|
449
|
+
|
|
296
450
|
### Asset の読み込み
|
|
297
451
|
|
|
298
452
|
この engine の asset はホストアプリ側で明示的に読み込んでください。
|
|
@@ -301,6 +455,7 @@ end
|
|
|
301
455
|
//= require yummy_guide_administrate/clipboards
|
|
302
456
|
//= require yummy_guide_administrate/datetime_input
|
|
303
457
|
//= require yummy_guide_administrate/fixed_submit_actions
|
|
458
|
+
//= require yummy_guide_administrate/filter_controls
|
|
304
459
|
//= require yummy_guide_administrate/filter_form
|
|
305
460
|
//= require yummy_guide_administrate/sticky_left_columns
|
|
306
461
|
//= 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
|
+
})();
|
|
@@ -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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YummyGuide
|
|
4
|
+
module Administrate
|
|
5
|
+
module NumberInputHelper
|
|
6
|
+
NUMBER_INPUT_CONTROL_OPTIONS = %i[type min max step in within].freeze
|
|
7
|
+
|
|
8
|
+
def number_field(object_name, method, options = {})
|
|
9
|
+
text_field(object_name, method, yummy_guide_administrate_number_input_options(options))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def number_field_tag(name, value = nil, options = {})
|
|
13
|
+
return super if yummy_guide_administrate_range_input?(options)
|
|
14
|
+
|
|
15
|
+
text_field_tag(name, value, yummy_guide_administrate_number_input_options(options))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def yummy_guide_administrate_range_input?(options)
|
|
21
|
+
options[:type].to_s == "range" || options["type"].to_s == "range"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def yummy_guide_administrate_number_input_options(options)
|
|
25
|
+
options = options.to_h.deep_dup
|
|
26
|
+
|
|
27
|
+
NUMBER_INPUT_CONTROL_OPTIONS.each do |option|
|
|
28
|
+
options.delete(option)
|
|
29
|
+
options.delete(option.to_s)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
options[:inputmode] ||= "decimal"
|
|
33
|
+
options
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -11,11 +11,22 @@ 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
|
|
20
|
+
|
|
21
|
+
initializer "yummy_guide.administrate.helpers" do |app|
|
|
22
|
+
app.config.to_prepare do
|
|
23
|
+
next unless defined?(::Administrate::ApplicationController)
|
|
24
|
+
|
|
25
|
+
::Administrate::ApplicationController.helper YummyGuide::Administrate::NumberInputHelper
|
|
26
|
+
::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterFormHelper
|
|
27
|
+
::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterControlsHelper
|
|
28
|
+
end
|
|
29
|
+
end
|
|
19
30
|
end
|
|
20
31
|
end
|
|
21
32
|
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
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
require "rails"
|
|
4
4
|
require "administrate"
|
|
5
5
|
|
|
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"
|
|
6
9
|
require_relative "administrate/version"
|
|
10
|
+
require_relative "administrate/filters"
|
|
7
11
|
require_relative "administrate/engine"
|
|
8
12
|
|
|
9
13
|
module YummyGuide
|
|
10
14
|
module Administrate
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
|
-
|
|
@@ -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
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
RSpec.describe YummyGuide::Administrate::NumberInputHelper do
|
|
7
|
+
subject(:helper_host) do
|
|
8
|
+
Class.new do
|
|
9
|
+
include ActionView::Context
|
|
10
|
+
include ActionView::Helpers::FormHelper
|
|
11
|
+
include ActionView::Helpers::FormTagHelper
|
|
12
|
+
include ActionView::Helpers::OutputSafetyHelper
|
|
13
|
+
include ActionView::Helpers::TagHelper
|
|
14
|
+
include YummyGuide::Administrate::NumberInputHelper
|
|
15
|
+
end.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
stub_const("SpecNumberInputResource", Class.new do
|
|
20
|
+
extend ActiveModel::Naming
|
|
21
|
+
include ActiveModel::Conversion
|
|
22
|
+
|
|
23
|
+
attr_accessor :base_salary
|
|
24
|
+
|
|
25
|
+
def persisted?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
end)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
let(:resource) do
|
|
32
|
+
SpecNumberInputResource.new.tap do |record|
|
|
33
|
+
record.base_salary = "123.45"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fragment(html)
|
|
38
|
+
Nokogiri::HTML.fragment(html)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "#number_field" do
|
|
42
|
+
it "renders a decimal-friendly text input without number-only control attributes" do
|
|
43
|
+
html = helper_host.number_field(
|
|
44
|
+
:job_match,
|
|
45
|
+
:base_salary,
|
|
46
|
+
class: "payment-input",
|
|
47
|
+
data: { role: "base-salary" },
|
|
48
|
+
required: true,
|
|
49
|
+
disabled: true,
|
|
50
|
+
readonly: true,
|
|
51
|
+
placeholder: "0.0",
|
|
52
|
+
value: "100.5",
|
|
53
|
+
min: 0,
|
|
54
|
+
max: 999,
|
|
55
|
+
step: "any",
|
|
56
|
+
in: 1..10,
|
|
57
|
+
within: 1..10
|
|
58
|
+
)
|
|
59
|
+
input = fragment(html).at_css("input")
|
|
60
|
+
|
|
61
|
+
expect(input["type"]).to eq("text")
|
|
62
|
+
expect(input["inputmode"]).to eq("decimal")
|
|
63
|
+
expect(input["class"]).to eq("payment-input")
|
|
64
|
+
expect(input["id"]).to eq("job_match_base_salary")
|
|
65
|
+
expect(input["name"]).to eq("job_match[base_salary]")
|
|
66
|
+
expect(input["value"]).to eq("100.5")
|
|
67
|
+
expect(input["data-role"]).to eq("base-salary")
|
|
68
|
+
expect(input["required"]).to eq("required")
|
|
69
|
+
expect(input["disabled"]).to eq("disabled")
|
|
70
|
+
expect(input["readonly"]).to eq("readonly")
|
|
71
|
+
expect(input["placeholder"]).to eq("0.0")
|
|
72
|
+
expect(input["min"]).to be_nil
|
|
73
|
+
expect(input["max"]).to be_nil
|
|
74
|
+
expect(input["step"]).to be_nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "keeps range fields as range inputs" do
|
|
78
|
+
input = fragment(helper_host.range_field(:job_match, :base_salary, value: "50")).at_css("input")
|
|
79
|
+
|
|
80
|
+
expect(input["type"]).to eq("range")
|
|
81
|
+
expect(input["value"]).to eq("50")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "#number_field_tag" do
|
|
86
|
+
it "renders a decimal-friendly text input without number-only control attributes" do
|
|
87
|
+
html = helper_host.number_field_tag(
|
|
88
|
+
"job_match[base_salary]",
|
|
89
|
+
"200.5",
|
|
90
|
+
id: "base_salary",
|
|
91
|
+
class: "payment-input",
|
|
92
|
+
data: { role: "base-salary" },
|
|
93
|
+
required: true,
|
|
94
|
+
disabled: true,
|
|
95
|
+
readonly: true,
|
|
96
|
+
placeholder: "0.0",
|
|
97
|
+
min: 0,
|
|
98
|
+
max: 999,
|
|
99
|
+
step: "any",
|
|
100
|
+
in: 1..10,
|
|
101
|
+
within: 1..10
|
|
102
|
+
)
|
|
103
|
+
input = fragment(html).at_css("input")
|
|
104
|
+
|
|
105
|
+
expect(input["type"]).to eq("text")
|
|
106
|
+
expect(input["inputmode"]).to eq("decimal")
|
|
107
|
+
expect(input["id"]).to eq("base_salary")
|
|
108
|
+
expect(input["name"]).to eq("job_match[base_salary]")
|
|
109
|
+
expect(input["value"]).to eq("200.5")
|
|
110
|
+
expect(input["class"]).to eq("payment-input")
|
|
111
|
+
expect(input["data-role"]).to eq("base-salary")
|
|
112
|
+
expect(input["required"]).to eq("required")
|
|
113
|
+
expect(input["disabled"]).to eq("disabled")
|
|
114
|
+
expect(input["readonly"]).to eq("readonly")
|
|
115
|
+
expect(input["placeholder"]).to eq("0.0")
|
|
116
|
+
expect(input["min"]).to be_nil
|
|
117
|
+
expect(input["max"]).to be_nil
|
|
118
|
+
expect(input["step"]).to be_nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "keeps range field tags as range inputs" do
|
|
122
|
+
input = fragment(helper_host.range_field_tag("base_salary", "50")).at_css("input")
|
|
123
|
+
|
|
124
|
+
expect(input["type"]).to eq("range")
|
|
125
|
+
expect(input["value"]).to eq("50")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "keeps explicit range type options as range inputs" do
|
|
129
|
+
input = fragment(helper_host.number_field_tag("base_salary", "50", "type" => "range")).at_css("input")
|
|
130
|
+
|
|
131
|
+
expect(input["type"]).to eq("range")
|
|
132
|
+
expect(input["value"]).to eq("50")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe "form builder integration" do
|
|
137
|
+
it "routes f.number_field through the helper override" do
|
|
138
|
+
form_builder = ActionView::Helpers::FormBuilder.new(:job_match, resource, helper_host, {})
|
|
139
|
+
input = fragment(form_builder.number_field(:base_salary)).at_css("input")
|
|
140
|
+
|
|
141
|
+
expect(input["type"]).to eq("text")
|
|
142
|
+
expect(input["inputmode"]).to eq("decimal")
|
|
143
|
+
expect(input["value"]).to eq("123.45")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "keeps f.range_field as a range input" do
|
|
147
|
+
form_builder = ActionView::Helpers::FormBuilder.new(:job_match, resource, helper_host, {})
|
|
148
|
+
input = fragment(form_builder.range_field(:base_salary, value: "50")).at_css("input")
|
|
149
|
+
|
|
150
|
+
expect(input["type"]).to eq("range")
|
|
151
|
+
expect(input["value"]).to eq("50")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
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.0
|
|
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-25 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,7 +109,9 @@ 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
|
|
114
|
+
- app/helpers/yummy_guide/administrate/number_input_helper.rb
|
|
112
115
|
- app/views/fields/number/_form.html.erb
|
|
113
116
|
- app/views/fields/yummy_guide_administrate/area/picture/_form.html.erb
|
|
114
117
|
- app/views/fields/yummy_guide_administrate/area/picture/_index.html.erb
|
|
@@ -128,6 +131,7 @@ files:
|
|
|
128
131
|
- lib/yummy/guide/generic/administrate.rb
|
|
129
132
|
- lib/yummy_guide/administrate.rb
|
|
130
133
|
- lib/yummy_guide/administrate/engine.rb
|
|
134
|
+
- lib/yummy_guide/administrate/filters.rb
|
|
131
135
|
- lib/yummy_guide/administrate/version.rb
|
|
132
136
|
- spec/spec_helper.rb
|
|
133
137
|
- spec/views/fields/number/form_spec.rb
|
|
@@ -139,7 +143,10 @@ files:
|
|
|
139
143
|
- spec/yummy_guide/administrate/fields/json_pretty_field_spec.rb
|
|
140
144
|
- spec/yummy_guide/administrate/fields/version_item_field_spec.rb
|
|
141
145
|
- spec/yummy_guide/administrate/fields/version_whodunnit_field_spec.rb
|
|
146
|
+
- spec/yummy_guide/administrate/filter_controls_helper_spec.rb
|
|
142
147
|
- spec/yummy_guide/administrate/filter_form_helper_spec.rb
|
|
148
|
+
- spec/yummy_guide/administrate/filters_spec.rb
|
|
149
|
+
- spec/yummy_guide/administrate/number_input_helper_spec.rb
|
|
143
150
|
- yummy-guide-generic-administrate.gemspec
|
|
144
151
|
homepage: https://github.com/yummy-guide/generic-administrate
|
|
145
152
|
licenses:
|