admino 0.0.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 +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/admino.gemspec +34 -0
- data/lib/admino.rb +9 -0
- data/lib/admino/query.rb +12 -0
- data/lib/admino/query/base.rb +94 -0
- data/lib/admino/query/base_presenter.rb +34 -0
- data/lib/admino/query/configuration.rb +54 -0
- data/lib/admino/query/dsl.rb +30 -0
- data/lib/admino/query/field.rb +52 -0
- data/lib/admino/query/group.rb +55 -0
- data/lib/admino/query/group_presenter.rb +67 -0
- data/lib/admino/table.rb +7 -0
- data/lib/admino/table/head_row.rb +59 -0
- data/lib/admino/table/presenter.rb +81 -0
- data/lib/admino/table/resource_row.rb +135 -0
- data/lib/admino/table/row.rb +65 -0
- data/lib/admino/version.rb +3 -0
- data/spec/admino/query/base_presenter_spec.rb +60 -0
- data/spec/admino/query/base_spec.rb +133 -0
- data/spec/admino/query/dsl_spec.rb +45 -0
- data/spec/admino/query/field_spec.rb +69 -0
- data/spec/admino/query/group_presenter_spec.rb +140 -0
- data/spec/admino/query/group_spec.rb +65 -0
- data/spec/admino/table/head_row_spec.rb +116 -0
- data/spec/admino/table/presenter_spec.rb +168 -0
- data/spec/admino/table/resource_row_spec.rb +206 -0
- data/spec/admino/table/row_spec.rb +71 -0
- data/spec/spec_helper.rb +82 -0
- metadata +244 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Admino
|
4
|
+
module Query
|
5
|
+
describe Group do
|
6
|
+
subject(:group) { Group.new(config, params) }
|
7
|
+
let(:config) { Configuration::Group.new(:foo, [:bar]) }
|
8
|
+
let(:params) { {} }
|
9
|
+
|
10
|
+
describe '#current_scope' do
|
11
|
+
context 'with no param' do
|
12
|
+
let(:params) { {} }
|
13
|
+
|
14
|
+
it 'returns the param value for the field' do
|
15
|
+
expect(group.current_scope).to be_nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with an invalid value' do
|
20
|
+
let(:params) { { 'foo' => 'qux' } }
|
21
|
+
|
22
|
+
it 'returns the param value for the field' do
|
23
|
+
expect(group.current_scope).to be_nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with a valid value' do
|
28
|
+
let(:params) { { 'foo' => 'bar' } }
|
29
|
+
|
30
|
+
it 'returns nil' do
|
31
|
+
expect(group.current_scope).to eq :bar
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#augment_scope' do
|
37
|
+
let(:result) { group.augment_scope(scope) }
|
38
|
+
let(:scope) { ScopeMock.new('original') }
|
39
|
+
|
40
|
+
context 'if the field has a value' do
|
41
|
+
let(:params) { { 'foo' => 'bar' } }
|
42
|
+
|
43
|
+
it 'returns the original scope chained with the group scope' do
|
44
|
+
expect(result.chain).to eq [:bar, []]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'else' do
|
49
|
+
it 'returns the original scope' do
|
50
|
+
expect(result).to eq scope
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#is_scope_active?' do
|
56
|
+
let(:params) { { 'foo' => 'bar' } }
|
57
|
+
|
58
|
+
it 'returns true if the provided scope is the one currently active' do
|
59
|
+
expect(group.is_scope_active?(:bar)).to be_true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Admino
|
4
|
+
module Table
|
5
|
+
describe HeadRow do
|
6
|
+
subject(:row) { HeadRow.new(klass, view) }
|
7
|
+
let(:view) { RailsViewContext.new }
|
8
|
+
let(:klass) { Post }
|
9
|
+
|
10
|
+
it 'takes a class and a view context' do
|
11
|
+
expect(row.resource_klass).to eq klass
|
12
|
+
expect(row.view_context).to eq view
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#column' do
|
16
|
+
subject { row.to_html }
|
17
|
+
|
18
|
+
context 'text' do
|
19
|
+
context 'with label' do
|
20
|
+
before do
|
21
|
+
row.column(:title, 'This is a title')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'generates a label with it' do
|
25
|
+
should have_tag(:th, text: 'This is a title')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with no label' do
|
30
|
+
before { row.column(:title) }
|
31
|
+
|
32
|
+
it 'generates a label with the titleized attribute name' do
|
33
|
+
should have_tag(:th, text: 'Title')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with I18n set up' do
|
38
|
+
before do
|
39
|
+
I18n.backend.store_translations(
|
40
|
+
:en,
|
41
|
+
activemodel: { attributes: { post: { title: 'Post title' } } }
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
before { row.column(:title) }
|
46
|
+
|
47
|
+
it 'generates a label with the human attribute name' do
|
48
|
+
should have_tag(:th, text: 'Post title')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'role' do
|
54
|
+
before { row.column(:author_name) }
|
55
|
+
|
56
|
+
it 'generates a role attribute with the snake-cased name of the attribute' do
|
57
|
+
should have_tag(:th, with: { role: 'author-name' })
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with html options param' do
|
62
|
+
before { row.column(:title, class: 'foo') }
|
63
|
+
|
64
|
+
it 'uses it to build attributes' do
|
65
|
+
should have_tag(:th, with: { class: 'foo' })
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#actions' do
|
71
|
+
subject { row.to_html }
|
72
|
+
|
73
|
+
context do
|
74
|
+
before { row.actions }
|
75
|
+
|
76
|
+
it 'renders a th cell with role "actions"' do
|
77
|
+
should have_tag(:th, with: { role: 'actions' })
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'renders a th cell with text "Actions"' do
|
81
|
+
should have_tag(:th, text: 'Actions')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with generic I18n set up' do
|
86
|
+
before do
|
87
|
+
I18n.backend.store_translations(
|
88
|
+
:en,
|
89
|
+
table: { actions: { title: 'Available actions' } }
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'renders a th cell with I18n text' do
|
94
|
+
row.actions
|
95
|
+
should have_tag(:th, text: 'Available actions')
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'and specific I18n set up' do
|
99
|
+
before do
|
100
|
+
I18n.backend.store_translations(
|
101
|
+
:en,
|
102
|
+
table: { actions: { post: { title: 'Post actions' } } }
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'uses the specific I18n text' do
|
107
|
+
row.actions
|
108
|
+
should have_tag(:th, text: 'Post actions')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,168 @@
|
|
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, view) }
|
8
|
+
let(:presenter_klass) { Presenter }
|
9
|
+
let(:view) { RailsViewContext.new }
|
10
|
+
|
11
|
+
let(:collection) { [ first_post, second_post ] }
|
12
|
+
let(:first_post) { Post.new('1') }
|
13
|
+
let(:first_post_presenter) { double('PresentedPost', dom_id: 'post_1') }
|
14
|
+
let(:second_post) { Post.new('2') }
|
15
|
+
let(:second_post_presenter) { double('PresentedPost', dom_id: 'post_2') }
|
16
|
+
|
17
|
+
let(:head_row) { double('HeadRow', to_html: '<td id="thead_td"></td>'.html_safe) }
|
18
|
+
let(:resource_row) { double('ResourceRow', to_html: '<td id="tbody_td"></td>'.html_safe) }
|
19
|
+
|
20
|
+
before do
|
21
|
+
PostPresenter.stub(:new).with(first_post, view).and_return(first_post_presenter)
|
22
|
+
PostPresenter.stub(:new).with(second_post, view).and_return(second_post_presenter)
|
23
|
+
|
24
|
+
HeadRow.stub(:new).with(Post, view).and_return(head_row)
|
25
|
+
ResourceRow.stub(:new).with(first_post_presenter, view).and_return(resource_row)
|
26
|
+
ResourceRow.stub(:new).with(second_post_presenter, view).and_return(resource_row)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#.to_html' do
|
30
|
+
let(:result) { presenter.to_html }
|
31
|
+
|
32
|
+
it 'outputs a table' do
|
33
|
+
expect(result).to have_tag(:table)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'outputs a thead with a single row' do
|
37
|
+
expect(result).to have_tag('table thead tr')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'outputs a tbody with a row for each collection member' do
|
41
|
+
expect(result).to have_tag('table tbody tr', count: 2)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'adds a record idenfier to each collection row' do
|
45
|
+
expect(result).to have_tag('tbody tr#post_1')
|
46
|
+
expect(result).to have_tag('tbody tr#post_2')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'adds zebra classes to each collection row' do
|
50
|
+
expect(result).to have_tag('tbody #post_1.is-even')
|
51
|
+
expect(result).to have_tag('tbody #post_2.is-odd')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'delegates thead columns creation to .to_html HeadRow' do
|
55
|
+
expect(result).to have_tag('thead tr td#thead_td')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'delegates tbody columns creation to .to_html ResourceRow' do
|
59
|
+
expect(result).to have_tag('tbody tr td#tbody_td')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'allows passing table HTML options' do
|
63
|
+
expect(presenter.to_html(id: 'table')).to have_tag(:table, with: { id: 'table' })
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'with a block' do
|
67
|
+
let(:block_call_args) do
|
68
|
+
block_call_args = []
|
69
|
+
presenter.to_html do |*args|
|
70
|
+
block_call_args << args
|
71
|
+
end
|
72
|
+
block_call_args
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'calls it once passing the HeadRow instance' do
|
76
|
+
expect(block_call_args[0]).to eq [head_row, nil]
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'calls it once for each collection member passing the ResourceRow instance and the member itself' do
|
80
|
+
expect(block_call_args[1]).to eq [resource_row, first_post_presenter]
|
81
|
+
expect(block_call_args[2]).to eq [resource_row, second_post_presenter]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'custom table HTML options' do
|
86
|
+
let(:presenter_klass) do
|
87
|
+
Class.new(Presenter) do
|
88
|
+
private
|
89
|
+
|
90
|
+
def table_html_options
|
91
|
+
{ id: 'table' }
|
92
|
+
end
|
93
|
+
|
94
|
+
def thead_html_options
|
95
|
+
{ id: 'thead' }
|
96
|
+
end
|
97
|
+
|
98
|
+
def thead_tr_html_options
|
99
|
+
{ id: 'thead_tr' }
|
100
|
+
end
|
101
|
+
|
102
|
+
def tbody_html_options
|
103
|
+
{ id: 'tbody' }
|
104
|
+
end
|
105
|
+
|
106
|
+
def tbody_tr_html_options(resource, index)
|
107
|
+
{ class: "index-#{index}" }
|
108
|
+
end
|
109
|
+
|
110
|
+
def zebra_css_classes
|
111
|
+
%w(one two)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it "allows customizing the default table html attributes" do
|
117
|
+
expect(presenter.to_html).to have_tag(:table, with: { id: 'table' })
|
118
|
+
end
|
119
|
+
|
120
|
+
it "allows customizing the the default thead html attributes" do
|
121
|
+
expect(presenter.to_html).to have_tag(:thead, with: { id: 'thead' })
|
122
|
+
end
|
123
|
+
|
124
|
+
it "allows customizing the the default thead_tr html attributes" do
|
125
|
+
expect(presenter.to_html).to have_tag('thead tr#thead_tr')
|
126
|
+
end
|
127
|
+
|
128
|
+
it "allows customizing the the default tbody html attributes" do
|
129
|
+
expect(presenter.to_html).to have_tag(:tbody, with: { id: 'tbody' })
|
130
|
+
end
|
131
|
+
|
132
|
+
it "allows customizing the tbody_tr html attributes" do
|
133
|
+
expect(presenter.to_html).to have_tag(:tr, with: { id: 'post_1', class: 'index-0' })
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'allows customizing zebra classes' do
|
137
|
+
expect(presenter.to_html).to have_tag(:tr, with: { id: 'post_1', class: 'one' })
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'custom row builders' do
|
142
|
+
let(:presenter_klass) do
|
143
|
+
Class.new(Presenter) do
|
144
|
+
private
|
145
|
+
|
146
|
+
def head_row(*args)
|
147
|
+
OpenStruct.new(to_html: '<td id="custom_thead_td"></td>'.html_safe)
|
148
|
+
end
|
149
|
+
|
150
|
+
def resource_row(*args)
|
151
|
+
OpenStruct.new(to_html: '<td id="custom_tbody_td"></td>'.html_safe)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'allows customizing head row renderers' do
|
157
|
+
expect(presenter.to_html).to have_tag('thead tr td#custom_thead_td')
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'allows customizing resource row renderers' do
|
161
|
+
expect(presenter.to_html).to have_tag('tbody tr td#custom_tbody_td')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Admino
|
4
|
+
module Table
|
5
|
+
describe ResourceRow do
|
6
|
+
subject(:row) { ResourceRow.new(resource, view) }
|
7
|
+
let(:view) { RailsViewContext.new }
|
8
|
+
let(:resource) { Post.new('1') }
|
9
|
+
|
10
|
+
it 'takes a resource and a view context' do
|
11
|
+
expect(row.resource).to eq resource
|
12
|
+
expect(row.view_context).to eq view
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#column' do
|
16
|
+
subject { row.to_html }
|
17
|
+
|
18
|
+
context 'if block is present' do
|
19
|
+
before do
|
20
|
+
row.column { 'foo' }
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'fills the cell with the block content' do
|
24
|
+
should have_tag(:td, text: 'foo')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'if attribute is present' do
|
29
|
+
before { row.column(:title) }
|
30
|
+
|
31
|
+
it 'fills the cell with the attribute value' do
|
32
|
+
should have_tag(:td, text: 'Post 1')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'if both attribute and block are missing' do
|
37
|
+
it 'raises an ArgumentError' do
|
38
|
+
expect { row.column('Title') }.to raise_error(ArgumentError)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'role attribute' do
|
43
|
+
before { row.column(:author_name) }
|
44
|
+
|
45
|
+
it 'generates a role attribute with the snake-cased name of the attribute' do
|
46
|
+
should have_tag(:td, with: { role: 'author-name' })
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with HTML options param' do
|
51
|
+
before { row.column(:title, class: 'title') }
|
52
|
+
|
53
|
+
it 'uses it to build attributes' do
|
54
|
+
should have_tag(:td, with: { class: 'title' })
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#actions' do
|
60
|
+
context 'block given' do
|
61
|
+
it 'yields the block' do
|
62
|
+
called = false
|
63
|
+
result = row.actions do
|
64
|
+
called = true
|
65
|
+
end
|
66
|
+
|
67
|
+
expect(called).to be_true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'no block' do
|
72
|
+
before do
|
73
|
+
row.stub(:action)
|
74
|
+
end
|
75
|
+
|
76
|
+
before do
|
77
|
+
row.actions(:show, :destroy)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'calls #action for each passed param' do
|
81
|
+
expect(row).to have_received(:action).with(:show)
|
82
|
+
expect(row).to have_received(:action).with(:destroy)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#action' do
|
88
|
+
subject { row.to_html }
|
89
|
+
|
90
|
+
context 'URL' do
|
91
|
+
context 'with an explicit URL' do
|
92
|
+
before { row.action(:show, '/') }
|
93
|
+
|
94
|
+
it 'generates a link with the specified URL' do
|
95
|
+
should have_tag(:a, with: { href: '/' })
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'with no explicit URL' do
|
100
|
+
let(:row) { row_subclass.new(resource, view) }
|
101
|
+
let(:row_subclass) do
|
102
|
+
Class.new(ResourceRow) do
|
103
|
+
def show_action_url
|
104
|
+
"/posts/#{resource.to_param}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
before { row.action(:show) }
|
110
|
+
|
111
|
+
it 'uses a method to build the URL (ie. show_url)' do
|
112
|
+
should have_tag(:a, with: { href: '/posts/1' })
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with no explicit URL and no action name' do
|
117
|
+
it 'raises an ArgumentError' do
|
118
|
+
expect { row.action(:show) }.to raise_error(ArgumentError)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'with no arguments' do
|
124
|
+
it 'raises an ArgumentError' do
|
125
|
+
expect { row.action }.to raise_error(ArgumentError)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'td cell' do
|
130
|
+
before { row.action(:show, '/') }
|
131
|
+
|
132
|
+
it 'generates a td cell with actions role' do
|
133
|
+
should have_tag(:td, with: { role: 'actions' })
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'link role' do
|
138
|
+
before { row.action(:show, '/') }
|
139
|
+
|
140
|
+
it 'generates a link with role' do
|
141
|
+
should have_tag(:a, with: { role: 'show' })
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'link text' do
|
146
|
+
context do
|
147
|
+
before { row.action(:show, '/') }
|
148
|
+
|
149
|
+
it 'generates a link with a titleized attribute' do
|
150
|
+
should have_tag(:a, text: 'Show')
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'if I18n is set up' do
|
155
|
+
before do
|
156
|
+
I18n.backend.store_translations(
|
157
|
+
:en,
|
158
|
+
table: { actions: { post: { show: 'Show post' } } }
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
before { row.action(:show, '/') }
|
163
|
+
|
164
|
+
it 'generates a I18n text' do
|
165
|
+
should have_tag(:a, text: 'Show post')
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'with html options' do
|
171
|
+
before { row.action(:show, '/', class: 'foo') }
|
172
|
+
|
173
|
+
it 'renders them as attributes' do
|
174
|
+
should have_tag(:a, with: { class: 'foo' })
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'with a class that implements a <action_name>_html_options' do
|
178
|
+
let(:row) { row_subclass.new(resource, view) }
|
179
|
+
let(:row_subclass) do
|
180
|
+
Class.new(ResourceRow) do
|
181
|
+
def show_action_html_options
|
182
|
+
{ class: 'button' }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'renders them as attributes' do
|
188
|
+
should have_tag(:a, with: { class: 'foo button' })
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'with block' do
|
194
|
+
before do
|
195
|
+
row.action { 'Foo' }
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'renders it' do
|
199
|
+
should have_tag(:td, text: 'Foo')
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|