gestart-admino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +20 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +91 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +716 -0
- data/Rakefile +6 -0
- data/gestart-admino.gemspec +36 -0
- data/lib/admino/action_view_extension.rb +35 -0
- data/lib/admino/query/base.rb +124 -0
- data/lib/admino/query/base_presenter.rb +27 -0
- data/lib/admino/query/builder.rb +24 -0
- data/lib/admino/query/configuration.rb +100 -0
- data/lib/admino/query/dsl.rb +38 -0
- data/lib/admino/query/filter_group.rb +73 -0
- data/lib/admino/query/filter_group_presenter.rb +85 -0
- data/lib/admino/query/scope_presenter.rb +17 -0
- data/lib/admino/query/search_field.rb +43 -0
- data/lib/admino/query/sorting.rb +74 -0
- data/lib/admino/query/sorting_presenter.rb +60 -0
- data/lib/admino/query.rb +15 -0
- data/lib/admino/table/head_row.rb +86 -0
- data/lib/admino/table/presenter.rb +122 -0
- data/lib/admino/table/resource_row.rb +145 -0
- data/lib/admino/table/row.rb +56 -0
- data/lib/admino/table.rb +7 -0
- data/lib/admino/version.rb +3 -0
- data/lib/admino.rb +14 -0
- data/lib/gestart-admino.rb +1 -0
- data/spec/admino/integration_spec.rb +63 -0
- data/spec/admino/query/base_presenter_spec.rb +59 -0
- data/spec/admino/query/base_spec.rb +192 -0
- data/spec/admino/query/dsl_spec.rb +52 -0
- data/spec/admino/query/filter_group_presenter_spec.rb +182 -0
- data/spec/admino/query/filter_group_spec.rb +133 -0
- data/spec/admino/query/search_field_spec.rb +66 -0
- data/spec/admino/query/sorting_presenter_spec.rb +206 -0
- data/spec/admino/query/sorting_spec.rb +137 -0
- data/spec/admino/table/head_row_spec.rb +158 -0
- data/spec/admino/table/presenter_spec.rb +180 -0
- data/spec/admino/table/resource_row_spec.rb +225 -0
- data/spec/admino/table/row_spec.rb +76 -0
- data/spec/spec_helper.rb +93 -0
- metadata +267 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'active_support/hash_with_indifferent_access'
|
|
3
|
+
|
|
4
|
+
module Admino
|
|
5
|
+
module Query
|
|
6
|
+
describe SortingPresenter do
|
|
7
|
+
subject(:presenter) { SortingPresenter.new(sorting, view) }
|
|
8
|
+
let(:view) { RailsViewContext.new }
|
|
9
|
+
let(:sorting) do
|
|
10
|
+
double(
|
|
11
|
+
'Sorting',
|
|
12
|
+
default_scope: 'by_name',
|
|
13
|
+
query_i18n_key: 'query_name'
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
let(:request_object) do
|
|
17
|
+
double(
|
|
18
|
+
'ActionDispatch::Request',
|
|
19
|
+
query_parameters: ActiveSupport::HashWithIndifferentAccess.new(
|
|
20
|
+
'sorting' => 'by_date', 'other' => 'value'
|
|
21
|
+
),
|
|
22
|
+
path: '/'
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
before do
|
|
27
|
+
allow(view).to receive(:request).and_return(request_object)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#scope_link' do
|
|
31
|
+
subject { presenter.scope_link(:by_title, 'Titolo') }
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
allow(sorting).to receive(:is_scope_active?).with(:by_title).and_return(false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'scope is active' do
|
|
38
|
+
before do
|
|
39
|
+
allow(sorting).to receive(:is_scope_active?).with(:by_title).and_return(true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context 'ascending' do
|
|
43
|
+
before do
|
|
44
|
+
allow(sorting).to receive(:ascending?).and_return(true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'adds an is-asc class' do
|
|
48
|
+
is_expected.to have_tag(:a, with: { class: 'is-asc' })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'if an :asc_class option is specified' do
|
|
52
|
+
subject { presenter.scope_link(:by_title, 'Titolo', asc_class: 'asc') }
|
|
53
|
+
|
|
54
|
+
it 'adds it' do
|
|
55
|
+
is_expected.to have_tag(:a, with: { class: 'asc' })
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'descendent' do
|
|
61
|
+
before do
|
|
62
|
+
allow(sorting).to receive(:ascending?).and_return(false)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'adds an is-desc class' do
|
|
66
|
+
is_expected.to have_tag(:a, with: { class: 'is-desc' })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'if a :desc_class option is specified' do
|
|
70
|
+
subject { presenter.scope_link(:by_title, 'Titolo', desc_class: 'desc') }
|
|
71
|
+
|
|
72
|
+
it 'adds it' do
|
|
73
|
+
is_expected.to have_tag(:a, with: { class: 'desc' })
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'else' do
|
|
80
|
+
it 'does not add it' do
|
|
81
|
+
is_expected.not_to have_tag(:a, with: { class: 'is-asc' })
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context 'label' do
|
|
86
|
+
it 'uses the provided argument' do
|
|
87
|
+
is_expected.to have_tag(:a, text: 'Titolo')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context 'URL' do
|
|
92
|
+
before do
|
|
93
|
+
allow(presenter).to receive(:scope_path).with(:by_title).and_return('URL')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'uses #scope_path method' do
|
|
97
|
+
is_expected.to have_tag(:a, href: 'URL')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#scope_params' do
|
|
103
|
+
subject { presenter.scope_params(:by_title) }
|
|
104
|
+
|
|
105
|
+
before do
|
|
106
|
+
allow(sorting).to receive(:is_scope_active?).with(:by_title).and_return(false)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'preserves other params' do
|
|
110
|
+
expect(subject[:other]).to eq 'value'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'keeps the request parameters intact' do
|
|
114
|
+
subject
|
|
115
|
+
expect(request_object.query_parameters[:sorting]).to eq 'by_date'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'sets the sorting param as the scope' do
|
|
119
|
+
expect(subject[:sorting]).to eq 'by_title'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context 'scope is active' do
|
|
123
|
+
before do
|
|
124
|
+
allow(sorting).to receive(:is_scope_active?).with(:by_title).and_return(true)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
context 'is currently ascending' do
|
|
128
|
+
before do
|
|
129
|
+
allow(sorting).to receive(:ascending?).and_return(true)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'sets the sorting order to descending' do
|
|
133
|
+
expect(subject[:sort_order]).to eq 'desc'
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context 'is currently descending' do
|
|
138
|
+
before do
|
|
139
|
+
allow(sorting).to receive(:ascending?).and_return(false)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'sets the sorting order to ascending' do
|
|
143
|
+
expect(subject[:sort_order]).to eq 'asc'
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
context 'scope is the default one' do
|
|
149
|
+
let(:sorting) { double('Sorting', default_scope: :by_title) }
|
|
150
|
+
|
|
151
|
+
context 'default scope is ascending' do
|
|
152
|
+
before do
|
|
153
|
+
allow(sorting).to receive(:default_direction).and_return(:asc)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'sets the sorting order to ascending' do
|
|
157
|
+
expect(subject[:sort_order]).to eq 'asc'
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
context 'default scope is descending' do
|
|
162
|
+
before do
|
|
163
|
+
allow(sorting).to receive(:default_direction).and_return(:desc)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'sets the sorting order to descending' do
|
|
167
|
+
expect(subject[:sort_order]).to eq 'desc'
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
context 'else' do
|
|
173
|
+
before do
|
|
174
|
+
allow(sorting).to receive(:is_scope_active?).with(:by_title).and_return(false)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'sets the sorting order to ascending' do
|
|
178
|
+
expect(subject[:sort_order]).to eq 'asc'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
describe '#scope_name' do
|
|
184
|
+
context do
|
|
185
|
+
before do
|
|
186
|
+
I18n.backend.store_translations(
|
|
187
|
+
:en,
|
|
188
|
+
query: { sorting_scopes: { query_name: { by_name: 'Sort by name' } } }
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'returns a I18n translatable name for the scope' do
|
|
193
|
+
expect(presenter.scope_name(:by_name)).to eq 'Sort by name'
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
context 'if no translation is available' do
|
|
198
|
+
it 'falls back to a titleized version of the scope name' do
|
|
199
|
+
expect(presenter.scope_name(:by_name)).to eq 'By name'
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Admino
|
|
4
|
+
module Query
|
|
5
|
+
describe Sorting do
|
|
6
|
+
subject(:sorting) { Sorting.new(config, params) }
|
|
7
|
+
let(:config) do
|
|
8
|
+
Configuration::Sorting.new(sorting_scopes, options)
|
|
9
|
+
end
|
|
10
|
+
let(:sorting_scopes) { [:by_title, :by_date] }
|
|
11
|
+
let(:options) { {} }
|
|
12
|
+
let(:params) { {} }
|
|
13
|
+
|
|
14
|
+
describe '#active_scope' do
|
|
15
|
+
context 'with no param' do
|
|
16
|
+
let(:params) { {} }
|
|
17
|
+
|
|
18
|
+
it 'returns false' do
|
|
19
|
+
expect(sorting.active_scope).to be_nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context 'if a default scope is configured' do
|
|
23
|
+
let(:options) { { default_scope: :by_date } }
|
|
24
|
+
|
|
25
|
+
it 'returns it' do
|
|
26
|
+
expect(sorting.active_scope).to eq :by_date
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'with an invalid value' do
|
|
32
|
+
let(:params) { { 'sorting' => 'foo' } }
|
|
33
|
+
|
|
34
|
+
it 'returns false' do
|
|
35
|
+
expect(sorting.active_scope).to be_nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'with a valid value' do
|
|
40
|
+
let(:params) { { 'sorting' => 'by_title' } }
|
|
41
|
+
|
|
42
|
+
it 'returns the scope name' do
|
|
43
|
+
expect(sorting.active_scope).to eq :by_title
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#ascending?' do
|
|
49
|
+
context 'with no param' do
|
|
50
|
+
let(:params) { {} }
|
|
51
|
+
|
|
52
|
+
it 'returns true' do
|
|
53
|
+
expect(sorting).to be_ascending
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context 'with invalid value' do
|
|
58
|
+
let(:params) { { 'sort_order' => 'foo' } }
|
|
59
|
+
|
|
60
|
+
it 'returns true' do
|
|
61
|
+
expect(sorting).to be_ascending
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context 'with "asc" value' do
|
|
66
|
+
let(:params) { { 'sort_order' => 'asc' } }
|
|
67
|
+
|
|
68
|
+
it 'returns nil' do
|
|
69
|
+
expect(sorting).to be_ascending
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context 'with "desc" value' do
|
|
74
|
+
let(:params) { { 'sort_order' => 'desc' } }
|
|
75
|
+
|
|
76
|
+
it 'returns the param value for the search_field' do
|
|
77
|
+
expect(sorting).not_to be_ascending
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'if a default scope and direction are set and default scope is current' do
|
|
82
|
+
let(:options) { { default_scope: :by_date, default_direction: :desc } }
|
|
83
|
+
let(:params) { { 'sorting' => 'by_date', 'sort_order' => 'desc' } }
|
|
84
|
+
|
|
85
|
+
it 'returns it' do
|
|
86
|
+
expect(sorting).not_to be_ascending
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe '#augment_scope' do
|
|
92
|
+
let(:result) { sorting.augment_scope(scope) }
|
|
93
|
+
let(:scope) { ScopeMock.new('original') }
|
|
94
|
+
|
|
95
|
+
context 'if the search_field has a value' do
|
|
96
|
+
let(:params) { { 'sorting' => 'by_title', 'sort_order' => 'desc' } }
|
|
97
|
+
|
|
98
|
+
it 'returns the original scope chained with the current scope' do
|
|
99
|
+
expect(result.chain).to eq [:by_title, [:desc]]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
context 'else' do
|
|
104
|
+
it 'returns the original scope' do
|
|
105
|
+
expect(result).to eq scope
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context 'if a default scope is configured' do
|
|
110
|
+
let(:options) { { default_scope: :by_date } }
|
|
111
|
+
let(:params) { {} }
|
|
112
|
+
|
|
113
|
+
it 'returns the original scope chained with the default scope' do
|
|
114
|
+
expect(result.chain).to eq [:by_date, [:asc]]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
context 'if a default direction is configured' do
|
|
118
|
+
let(:options) { { default_scope: :by_date, default_direction: :desc } }
|
|
119
|
+
|
|
120
|
+
it 'returns the original scope chained with the default scope and default direction' do
|
|
121
|
+
expect(result.chain).to eq [:by_date, [:desc]]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe '#is_scope_active?' do
|
|
128
|
+
let(:params) { { 'sorting' => 'by_date' } }
|
|
129
|
+
|
|
130
|
+
it 'returns true if the provided scope is the one currently active' do
|
|
131
|
+
expect(sorting.is_scope_active?(:by_date)).to be_truthy
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Admino
|
|
4
|
+
module Table
|
|
5
|
+
describe HeadRow do
|
|
6
|
+
subject(:row) { HeadRow.new(klass, query, view) }
|
|
7
|
+
let(:klass) { Post }
|
|
8
|
+
let(:query) { nil }
|
|
9
|
+
let(:view) { RailsViewContext.new }
|
|
10
|
+
|
|
11
|
+
it 'takes a class and a view context' do
|
|
12
|
+
expect(row.resource_klass).to eq klass
|
|
13
|
+
expect(row.view_context).to eq view
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#column' do
|
|
17
|
+
subject { row.to_html }
|
|
18
|
+
|
|
19
|
+
context 'text' do
|
|
20
|
+
context 'with string label' do
|
|
21
|
+
before do
|
|
22
|
+
row.column(:title, 'This is a title')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'generates a label with it' do
|
|
26
|
+
is_expected.to have_tag(:th, text: 'This is a title')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'with symbol label and I18n set up' do
|
|
31
|
+
before do
|
|
32
|
+
I18n.backend.store_translations(
|
|
33
|
+
:en,
|
|
34
|
+
activemodel: { attributes: { post: { foo: 'This is foo' } } }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
before do
|
|
39
|
+
row.column(:title, :foo)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'generates a label with the human attribute name' do
|
|
43
|
+
is_expected.to have_tag(:th, text: 'This is foo')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context 'with no label' do
|
|
48
|
+
before { row.column(:title) }
|
|
49
|
+
|
|
50
|
+
it 'generates a label with the titleized attribute name' do
|
|
51
|
+
is_expected.to have_tag(:th, text: 'Title')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'with I18n set up' do
|
|
55
|
+
before do
|
|
56
|
+
I18n.backend.store_translations(
|
|
57
|
+
:en,
|
|
58
|
+
activemodel: { attributes: { post: { title: 'Post title' } } }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
before { row.column(:title) }
|
|
63
|
+
|
|
64
|
+
it 'generates a label with the human attribute name' do
|
|
65
|
+
is_expected.to have_tag(:th, text: 'Post title')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'sorting' do
|
|
72
|
+
context 'if no query object is present' do
|
|
73
|
+
it 'raises an ArgumentError' do
|
|
74
|
+
expect { row.column(:title, sorting: :by_title) }.to raise_error ArgumentError
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'else' do
|
|
79
|
+
let(:query) { double('Query', sorting: sorting) }
|
|
80
|
+
let(:sorting) { double('Sorting') }
|
|
81
|
+
|
|
82
|
+
before do
|
|
83
|
+
allow(sorting).to receive(:scope_link).with(:by_title, 'Title', {}).
|
|
84
|
+
and_return('Link')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
before { row.column(:title, sorting: :by_title) }
|
|
88
|
+
|
|
89
|
+
it 'generates a sorting scope link' do
|
|
90
|
+
is_expected.to have_tag(:th, text: 'Link')
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'role' do
|
|
96
|
+
before { row.column(:author_name) }
|
|
97
|
+
|
|
98
|
+
it 'generates a role attribute with the snake-cased name of the attribute' do
|
|
99
|
+
is_expected.to have_tag(:th, with: { role: 'author-name' })
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
context 'with html options param' do
|
|
104
|
+
before { row.column(:title, class: 'foo') }
|
|
105
|
+
|
|
106
|
+
it 'uses it to build attributes' do
|
|
107
|
+
is_expected.to have_tag(:th, with: { class: 'foo' })
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe '#actions' do
|
|
113
|
+
subject { row.to_html }
|
|
114
|
+
|
|
115
|
+
context do
|
|
116
|
+
before { row.actions }
|
|
117
|
+
|
|
118
|
+
it 'renders a th cell with role "actions"' do
|
|
119
|
+
is_expected.to have_tag(:th, with: { role: 'actions' })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'renders a th cell with text "Actions"' do
|
|
123
|
+
is_expected.to have_tag(:th, text: 'Actions')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
context 'with generic I18n set up' do
|
|
128
|
+
before do
|
|
129
|
+
I18n.backend.store_translations(
|
|
130
|
+
:en,
|
|
131
|
+
table: { actions: { title: 'Available actions' } }
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'renders a th cell with I18n text' do
|
|
136
|
+
row.actions
|
|
137
|
+
is_expected.to have_tag(:th, text: 'Available actions')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context 'and specific I18n set up' do
|
|
141
|
+
before do
|
|
142
|
+
I18n.backend.store_translations(
|
|
143
|
+
:en,
|
|
144
|
+
table: { actions: { post: { title: 'Post actions' } } }
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'uses the specific I18n text' do
|
|
149
|
+
row.actions
|
|
150
|
+
is_expected.to have_tag(:th, text: 'Post actions')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'ostruct'
|
|
3
|
+
|
|
4
|
+
module Admino
|
|
5
|
+
module Table
|
|
6
|
+
describe Presenter do
|
|
7
|
+
subject(:presenter) { presenter_klass.new(collection, Post, query, view) }
|
|
8
|
+
let(:presenter_klass) { Presenter }
|
|
9
|
+
let(:query) { double('Query') }
|
|
10
|
+
let(:view) { RailsViewContext.new }
|
|
11
|
+
|
|
12
|
+
let(:collection) { [ first_post, second_post ] }
|
|
13
|
+
let(:first_post) { Post.new('1') }
|
|
14
|
+
let(:second_post) { Post.new('2') }
|
|
15
|
+
|
|
16
|
+
let(:head_row) { double('HeadRow', to_html: '<td id="thead_td"></td>'.html_safe) }
|
|
17
|
+
let(:resource_row) { double('ResourceRow', to_html: '<td id="tbody_td"></td>'.html_safe) }
|
|
18
|
+
|
|
19
|
+
before do
|
|
20
|
+
allow(HeadRow).to receive(:new).with(Post, query, view).and_return(head_row)
|
|
21
|
+
allow(ResourceRow).to receive(:new).with(first_post, view).and_return(resource_row)
|
|
22
|
+
allow(ResourceRow).to receive(:new).with(second_post, view).and_return(resource_row)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '#to_html' do
|
|
26
|
+
let(:result) { presenter.to_html }
|
|
27
|
+
|
|
28
|
+
it 'outputs a table' do
|
|
29
|
+
expect(result).to have_tag(:table)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'outputs a thead with a single row' do
|
|
33
|
+
expect(result).to have_tag('table thead tr')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'outputs a tbody with a row for each collection member' do
|
|
37
|
+
expect(result).to have_tag('table tbody tr', count: 2)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'if the record responds to #dom_id' do
|
|
41
|
+
before do
|
|
42
|
+
allow(first_post).to receive(:dom_id).and_return('post_1')
|
|
43
|
+
allow(second_post).to receive(:dom_id).and_return('post_2')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'adds a record identifier to each collection row' do
|
|
47
|
+
expect(result).to have_tag('tbody tr#post_1:first-child')
|
|
48
|
+
expect(result).to have_tag('tbody tr#post_2:last-child')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'adds zebra classes to each collection row' do
|
|
53
|
+
expect(result).to have_tag('tbody tr.is-even:first-child')
|
|
54
|
+
expect(result).to have_tag('tbody tr.is-odd:last-child')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'delegates thead columns creation to .to_html HeadRow' do
|
|
58
|
+
expect(result).to have_tag('thead tr td#thead_td')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'delegates tbody columns creation to .to_html ResourceRow' do
|
|
62
|
+
expect(result).to have_tag('tbody tr td#tbody_td')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'allows passing table HTML options' do
|
|
66
|
+
expect(presenter.to_html(id: 'table')).to have_tag(:table, with: { id: 'table' })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'with a block' do
|
|
70
|
+
let(:block_call_args) do
|
|
71
|
+
block_call_args = []
|
|
72
|
+
presenter.to_html do |*args|
|
|
73
|
+
block_call_args << args
|
|
74
|
+
end
|
|
75
|
+
block_call_args
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'calls it once passing the HeadRow instance' do
|
|
79
|
+
expect(block_call_args[0]).to eq [head_row, nil]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'calls it once for each collection member passing the ResourceRow instance and the member itself' do
|
|
83
|
+
expect(block_call_args[1]).to eq [resource_row, first_post]
|
|
84
|
+
expect(block_call_args[2]).to eq [resource_row, second_post]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
context 'custom table HTML options' do
|
|
89
|
+
let(:presenter_klass) do
|
|
90
|
+
Class.new(Presenter) do
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def table_html_options
|
|
94
|
+
{ id: 'table' }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def thead_html_options
|
|
98
|
+
{ id: 'thead' }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def thead_tr_html_options
|
|
102
|
+
{ id: 'thead_tr' }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def tbody_html_options
|
|
106
|
+
{ id: 'tbody' }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def tbody_tr_html_options(resource, index)
|
|
110
|
+
{ class: "index-#{index}" }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def zebra_css_classes
|
|
114
|
+
%w(one two)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "allows customizing the default table html attributes" do
|
|
120
|
+
expect(presenter.to_html).to have_tag(:table, with: { id: 'table' })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "allows customizing the the default thead html attributes" do
|
|
124
|
+
expect(presenter.to_html).to have_tag(:thead, with: { id: 'thead' })
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "allows customizing the the default thead_tr html attributes" do
|
|
128
|
+
expect(presenter.to_html).to have_tag('thead tr#thead_tr')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "allows customizing the the default tbody html attributes" do
|
|
132
|
+
expect(presenter.to_html).to have_tag(:tbody, with: { id: 'tbody' })
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "allows customizing the tbody_tr html attributes" do
|
|
136
|
+
expect(presenter.to_html).to have_tag("tbody tr.index-0:first-child")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'allows customizing zebra classes' do
|
|
140
|
+
expect(presenter.to_html).to have_tag("tbody tr.one:first-child")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
context 'custom row builders' do
|
|
145
|
+
let(:presenter_klass) do
|
|
146
|
+
Class.new(Presenter) do
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def head_row(*args)
|
|
150
|
+
OpenStruct.new(to_html: '<td id="custom_thead_td"></td>'.html_safe)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def resource_row(*args)
|
|
154
|
+
OpenStruct.new(to_html: '<td id="custom_tbody_td"></td>'.html_safe)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'allows customizing head row renderers' do
|
|
160
|
+
expect(presenter.to_html).to have_tag('thead tr td#custom_thead_td')
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'allows customizing resource row renderers' do
|
|
164
|
+
expect(presenter.to_html).to have_tag('tbody tr td#custom_tbody_td')
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context 'with empty collection and no collection_klass' do
|
|
169
|
+
let(:collection) { [] }
|
|
170
|
+
subject(:presenter) { presenter_klass.new(collection, nil, query, view) }
|
|
171
|
+
|
|
172
|
+
it 'raises an Exception' do
|
|
173
|
+
expect { result }.to raise_error(ArgumentError)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|