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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +29 -0
  3. data/.gitignore +20 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +91 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +716 -0
  9. data/Rakefile +6 -0
  10. data/gestart-admino.gemspec +36 -0
  11. data/lib/admino/action_view_extension.rb +35 -0
  12. data/lib/admino/query/base.rb +124 -0
  13. data/lib/admino/query/base_presenter.rb +27 -0
  14. data/lib/admino/query/builder.rb +24 -0
  15. data/lib/admino/query/configuration.rb +100 -0
  16. data/lib/admino/query/dsl.rb +38 -0
  17. data/lib/admino/query/filter_group.rb +73 -0
  18. data/lib/admino/query/filter_group_presenter.rb +85 -0
  19. data/lib/admino/query/scope_presenter.rb +17 -0
  20. data/lib/admino/query/search_field.rb +43 -0
  21. data/lib/admino/query/sorting.rb +74 -0
  22. data/lib/admino/query/sorting_presenter.rb +60 -0
  23. data/lib/admino/query.rb +15 -0
  24. data/lib/admino/table/head_row.rb +86 -0
  25. data/lib/admino/table/presenter.rb +122 -0
  26. data/lib/admino/table/resource_row.rb +145 -0
  27. data/lib/admino/table/row.rb +56 -0
  28. data/lib/admino/table.rb +7 -0
  29. data/lib/admino/version.rb +3 -0
  30. data/lib/admino.rb +14 -0
  31. data/lib/gestart-admino.rb +1 -0
  32. data/spec/admino/integration_spec.rb +63 -0
  33. data/spec/admino/query/base_presenter_spec.rb +59 -0
  34. data/spec/admino/query/base_spec.rb +192 -0
  35. data/spec/admino/query/dsl_spec.rb +52 -0
  36. data/spec/admino/query/filter_group_presenter_spec.rb +182 -0
  37. data/spec/admino/query/filter_group_spec.rb +133 -0
  38. data/spec/admino/query/search_field_spec.rb +66 -0
  39. data/spec/admino/query/sorting_presenter_spec.rb +206 -0
  40. data/spec/admino/query/sorting_spec.rb +137 -0
  41. data/spec/admino/table/head_row_spec.rb +158 -0
  42. data/spec/admino/table/presenter_spec.rb +180 -0
  43. data/spec/admino/table/resource_row_spec.rb +225 -0
  44. data/spec/admino/table/row_spec.rb +76 -0
  45. data/spec/spec_helper.rb +93 -0
  46. metadata +267 -0
@@ -0,0 +1,60 @@
1
+ require 'showcase'
2
+ require 'active_support/core_ext/object/deep_dup'
3
+
4
+ module Admino
5
+ module Query
6
+ class SortingPresenter < Showcase::Presenter
7
+ def scope_link(scope, *args)
8
+ options = args.extract_options!
9
+
10
+ label = args.first || scope_name(scope)
11
+
12
+ desc_class = options.delete(:desc_class) { 'is-desc' }
13
+ asc_class = options.delete(:asc_class) { 'is-asc' }
14
+
15
+ options = Showcase::Helpers::HtmlOptions.new(options)
16
+
17
+ if is_scope_active?(scope)
18
+ options.add_class!(ascending? ? asc_class : desc_class)
19
+ end
20
+
21
+ h.link_to label, scope_path(scope), options.to_h
22
+ end
23
+
24
+ def scope_path(scope)
25
+ h.request.path + "?" + scope_params(scope).to_query
26
+ end
27
+
28
+ def scopes
29
+ @scopes ||= super.map do |scope|
30
+ ScopePresenter.new(scope, self, view_context)
31
+ end
32
+ end
33
+
34
+ def scope_params(scope)
35
+ params = ActiveSupport::HashWithIndifferentAccess.new(
36
+ h.request.query_parameters.deep_dup
37
+ )
38
+
39
+ if is_scope_active?(scope)
40
+ params.merge!(sorting: scope.to_s, sort_order: ascending? ? 'desc' : 'asc')
41
+ elsif default_scope == scope
42
+ params.merge!(sorting: scope.to_s, sort_order: default_direction.to_s)
43
+ else
44
+ params.merge!(sorting: scope.to_s, sort_order: 'asc')
45
+ end
46
+
47
+ params
48
+ end
49
+
50
+ def scope_name(scope)
51
+ I18n.t(
52
+ :"#{query_i18n_key}.#{scope}",
53
+ scope: 'query.sorting_scopes',
54
+ default: scope.to_s.titleize.capitalize
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,15 @@
1
+ require 'admino/query/base'
2
+ require 'admino/query/base_presenter'
3
+ require 'admino/query/configuration'
4
+ require 'admino/query/search_field'
5
+ require 'admino/query/filter_group'
6
+ require 'admino/query/filter_group_presenter'
7
+ require 'admino/query/sorting'
8
+ require 'admino/query/sorting_presenter'
9
+ require 'admino/query/scope_presenter'
10
+
11
+ module Admino
12
+ module Query
13
+ end
14
+ end
15
+
@@ -0,0 +1,86 @@
1
+ require 'admino/table/row'
2
+ require 'showcase/helpers/html_options'
3
+
4
+ module Admino
5
+ module Table
6
+ class HeadRow < Row
7
+ attr_reader :resource_klass
8
+ attr_reader :query
9
+
10
+ def initialize(resource_klass, query, view_context)
11
+ @resource_klass = resource_klass
12
+ @query = query
13
+ @columns = +""
14
+
15
+ super(view_context)
16
+ end
17
+
18
+ def actions(*args, &block)
19
+ default_options = column_html_options(:actions)
20
+ label = I18n.t(
21
+ :"#{resource_klass.model_name.i18n_key}.title",
22
+ scope: 'table.actions',
23
+ default: [
24
+ :title,
25
+ 'Actions'
26
+ ]
27
+ )
28
+
29
+ @columns << h.content_tag(:th, label.to_s, default_options)
30
+ end
31
+
32
+ def column(*args, &block)
33
+ attribute_name, label, html_options = parse_column_args(args)
34
+
35
+ if label.nil?
36
+ label = column_label(attribute_name)
37
+ elsif label.is_a? Symbol
38
+ label = column_label(label)
39
+ end
40
+
41
+ html_options = complete_column_html_options(
42
+ attribute_name,
43
+ html_options
44
+ )
45
+
46
+ sorting_scope = html_options.delete(:sorting)
47
+ sorting_html_options = html_options.delete(:sorting_html_options) { {} }
48
+
49
+ if sorting_scope
50
+ raise ArgumentError, 'query object is required' unless query
51
+ label = query.sorting.scope_link(sorting_scope, label, sorting_html_options)
52
+ end
53
+
54
+ @columns << h.content_tag(:th, label.to_s, html_options)
55
+ end
56
+
57
+ def to_html
58
+ @columns.html_safe
59
+ end
60
+
61
+ private
62
+
63
+ def column_label(attribute_name)
64
+ if attribute_name
65
+ resource_klass.human_attribute_name(attribute_name.to_s)
66
+ end
67
+ end
68
+
69
+ def complete_column_html_options(attribute_name, final_html_options)
70
+ if attribute_name.nil?
71
+ return final_html_options
72
+ end
73
+
74
+ default_options = column_html_options(attribute_name)
75
+ html_options = Showcase::Helpers::HtmlOptions.new(default_options)
76
+ html_options.merge_attrs!(final_html_options)
77
+ html_options = html_options.to_h
78
+ end
79
+
80
+ def column_html_options(attribute_name)
81
+ { role: attribute_name.to_s.gsub(/_/, '-') }
82
+ end
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,122 @@
1
+ require 'showcase'
2
+ require 'admino/table/head_row'
3
+ require 'admino/table/resource_row'
4
+
5
+ module Admino
6
+ module Table
7
+ class Presenter < Showcase::Presenter
8
+ attr_reader :collection_klass
9
+ attr_reader :query
10
+
11
+ def self.tag_helper(name, tag, options = {})
12
+ options_method = :"#{name}_html_options"
13
+
14
+ define_method :"#{name}_tag" do |*args, &block|
15
+ options = args.extract_options!
16
+ if respond_to?(options_method, true)
17
+ default_options = send(options_method, *args)
18
+ html_options = Showcase::Helpers::HtmlOptions.new(default_options)
19
+ html_options.merge_attrs!(options)
20
+ options = html_options.to_h
21
+ end
22
+ h.content_tag(tag, options, &block)
23
+ end
24
+ end
25
+
26
+ tag_helper :table, :table
27
+ tag_helper :thead, :thead
28
+ tag_helper :thead_tr, :tr
29
+ tag_helper :tbody, :tbody
30
+ tag_helper :tbody_tr, :tr, params: %w(resource index)
31
+
32
+ def initialize(*args)
33
+ context = args.pop
34
+ collection = args.shift
35
+
36
+ @collection_klass = args.shift
37
+
38
+ if !@collection_klass && !collection.empty?
39
+ @collection_klass = collection.first.class
40
+ end
41
+
42
+ @query = args.shift
43
+
44
+ super(collection, context)
45
+ end
46
+
47
+ def to_html(options = {}, &block)
48
+
49
+ if @collection_klass.nil?
50
+ raise ArgumentError, 'collection is empty and no explicit class is specified'
51
+ end
52
+
53
+ table_tag(options) do
54
+ thead_tag do
55
+ thead_tr_tag do
56
+ th_tags(&block)
57
+ end
58
+ end <<
59
+ tbody_tag do
60
+ tbody_tr_tags(&block)
61
+ end
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def tbody_tr_tags(&block)
68
+ collection.each_with_index.map do |resource, index|
69
+ html_options = base_tbody_tr_html_options(resource, index)
70
+ tbody_tr_tag(resource, index, html_options) do
71
+ td_tags(resource, &block)
72
+ end
73
+ end.join.html_safe
74
+ end
75
+
76
+ def th_tags(&block)
77
+ row = head_row(collection_klass, query, view_context)
78
+ if block_given?
79
+ h.capture(row, nil, &block)
80
+ end
81
+ row.to_html
82
+ end
83
+
84
+ def td_tags(resource, &block)
85
+ row = resource_row(resource, view_context)
86
+ if block_given?
87
+ h.capture(row, resource, &block)
88
+ end
89
+ row.to_html
90
+ end
91
+
92
+ def collection
93
+ object
94
+ end
95
+
96
+ def head_row(collection_klass, query, view_context)
97
+ HeadRow.new(collection_klass, query, view_context)
98
+ end
99
+
100
+ def resource_row(resource, view_context)
101
+ ResourceRow.new(resource, view_context)
102
+ end
103
+
104
+ def base_tbody_tr_html_options(resource, index)
105
+ options = {
106
+ class: zebra_css_classes[index % zebra_css_classes.size]
107
+ }
108
+
109
+ if resource.respond_to?(:dom_id)
110
+ options[:id] = resource.dom_id
111
+ end
112
+
113
+ options
114
+ end
115
+
116
+ def zebra_css_classes
117
+ %w(is-even is-odd)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
@@ -0,0 +1,145 @@
1
+ require 'admino/table/row'
2
+ require 'showcase/helpers/html_options'
3
+
4
+ module Admino
5
+ module Table
6
+ class ResourceRow < Row
7
+ attr_reader :resource
8
+
9
+ def initialize(resource, view_context)
10
+ @resource = resource
11
+ @columns = +""
12
+ @actions = []
13
+
14
+ super(view_context)
15
+ end
16
+
17
+ def column(*args, &block)
18
+ attribute_name, label, html_options = parse_column_args(args)
19
+
20
+ html_options = complete_column_html_options(
21
+ attribute_name,
22
+ html_options
23
+ )
24
+
25
+ if block_given?
26
+ content = h.capture(resource, &block)
27
+ elsif attribute_name.present?
28
+ content = resource.send(attribute_name)
29
+ else
30
+ raise ArgumentError, 'attribute name or block required'
31
+ end
32
+
33
+ @columns << h.content_tag(:td, content, html_options)
34
+ end
35
+
36
+ def actions(*actions, &block)
37
+ if block_given?
38
+ h.capture(&block)
39
+ else
40
+ actions.each do |action|
41
+ action(action)
42
+ end
43
+ end
44
+ end
45
+
46
+ def action(*args, &block)
47
+ if block_given?
48
+ @actions << h.capture(resource, &block)
49
+ else
50
+ action_name, url, label, html_options = parse_action_args(args)
51
+
52
+ label ||= action_label(action_name)
53
+ url ||= action_url(action_name)
54
+ html_options = complete_action_html_options(
55
+ action_name,
56
+ html_options
57
+ )
58
+
59
+ @actions << h.link_to(label, url, html_options)
60
+ end
61
+ end
62
+
63
+ def to_html
64
+ buffer = @columns
65
+
66
+ if @actions.any?
67
+ html_options = column_html_options(:actions)
68
+ buffer << h.content_tag(:td, html_options) do
69
+ @actions.join(" ").html_safe
70
+ end
71
+ end
72
+
73
+ buffer.html_safe
74
+ end
75
+
76
+ private
77
+
78
+ def action_url(action_name)
79
+ if action_name.nil?
80
+ raise ArgumentError,
81
+ 'no URL provided, action name required'
82
+ end
83
+
84
+ action_url_method = "#{action_name}_action_url"
85
+
86
+ if !respond_to?(action_url_method, true)
87
+ raise ArgumentError,
88
+ "no URL provided, ##{action_url_method} method required"
89
+ end
90
+
91
+ url = send(action_url_method)
92
+ end
93
+
94
+ def complete_action_html_options(action_name, final_html_options)
95
+ if action_name
96
+ default_options = action_html_options(action_name)
97
+ html_options = Showcase::Helpers::HtmlOptions.new(default_options)
98
+
99
+ action_html_options_method = "#{action_name}_action_html_options"
100
+ if respond_to?(action_html_options_method, true)
101
+ html_options.merge_attrs!(send(action_html_options_method))
102
+ end
103
+
104
+ html_options.merge_attrs!(final_html_options)
105
+ html_options.to_h
106
+ else
107
+ final_html_options
108
+ end
109
+ end
110
+
111
+ def complete_column_html_options(attribute_name, final_html_options)
112
+ if attribute_name
113
+ default_options = column_html_options(attribute_name)
114
+ html_options = Showcase::Helpers::HtmlOptions.new(default_options)
115
+ html_options.merge_attrs!(final_html_options)
116
+ html_options.to_h
117
+ else
118
+ final_html_options
119
+ end
120
+ end
121
+
122
+ def action_label(action_name)
123
+ return nil unless action_name
124
+
125
+ I18n.t(
126
+ :"#{resource.class.model_name.i18n_key}.#{action_name}",
127
+ scope: 'table.actions',
128
+ default: [
129
+ :"#{action_name}",
130
+ action_name.to_s.titleize
131
+ ]
132
+ )
133
+ end
134
+
135
+ def action_html_options(action_name)
136
+ { role: action_name.to_s.gsub(/_/, '-') }
137
+ end
138
+
139
+ def column_html_options(attribute_name)
140
+ { role: attribute_name.to_s.gsub(/_/, '-') }
141
+ end
142
+ end
143
+ end
144
+ end
145
+
@@ -0,0 +1,56 @@
1
+ module Admino
2
+ module Table
3
+ class Row
4
+ attr_reader :view_context
5
+
6
+ alias_method :h, :view_context
7
+
8
+ def initialize(view_context)
9
+ @view_context = view_context
10
+ end
11
+
12
+ def parse_column_args(args)
13
+ html_options = args.extract_options!
14
+
15
+ attribute_name = if args.first.is_a?(Symbol)
16
+ args.shift
17
+ else
18
+ nil
19
+ end
20
+
21
+ label = if args.first.is_a?(String) || args.first.is_a?(Symbol)
22
+ args.shift
23
+ else
24
+ nil
25
+ end
26
+
27
+ [attribute_name, label, html_options]
28
+ end
29
+
30
+ def parse_action_args(args)
31
+ html_options = args.extract_options!
32
+
33
+ action_name = if args.first.is_a?(Symbol)
34
+ args.shift
35
+ else
36
+ nil
37
+ end
38
+
39
+ url = if args.first.is_a?(String)
40
+ args.shift
41
+ else
42
+ nil
43
+ end
44
+
45
+ label = args.shift
46
+
47
+ [action_name, url, label, html_options]
48
+ end
49
+
50
+ def to_html
51
+ nil
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,7 @@
1
+ require 'admino/table/presenter'
2
+
3
+ module Admino
4
+ module Table
5
+ end
6
+ end
7
+
@@ -0,0 +1,3 @@
1
+ module Admino
2
+ VERSION = "1.0.0"
3
+ end
data/lib/admino.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ require "admino/version"
4
+ require "admino/query"
5
+ require "admino/table"
6
+ require "admino/action_view_extension"
7
+
8
+ module Admino
9
+ end
10
+
11
+ ActiveSupport.on_load(:action_view) do
12
+ include Admino::ActionViewExtension
13
+ end
14
+
@@ -0,0 +1 @@
1
+ require 'admino'
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Action View integration' do
4
+ let(:context) { RailsViewContext.new }
5
+
6
+ describe '#simple_table_for' do
7
+ let(:collection) { [ first_post, second_post ] }
8
+ let(:first_post) { Post.new('1') }
9
+ let(:second_post) { Post.new('2') }
10
+ let(:params) {
11
+ {
12
+ sorting: 'by_title',
13
+ sort_order: 'desc'
14
+ }
15
+ }
16
+ let(:query) { TestQuery.new(params) }
17
+
18
+ it 'produces HTML' do
19
+ result = context.table_for(collection, query: query) do |table, record|
20
+ table.column :title, sorting: :by_title
21
+ table.actions do
22
+ table.action :show, '/foo', 'Show'
23
+ end
24
+ end
25
+
26
+ expect(result).to have_tag(:table) do
27
+ with_tag 'thead th:first-child', with: { role: 'title' }
28
+ with_tag 'thead th:last-child', with: { role: 'actions' }
29
+
30
+ with_tag 'th:first-child a', with: { class: 'is-desc', href: '/?sort_order=asc&sorting=by_title' }, text: 'Title'
31
+ with_tag 'th:last-child', text: 'Actions'
32
+
33
+ with_tag 'tbody tr:first-child', with: { class: 'is-even' }
34
+ with_tag 'tbody tr:last-child', with: { class: 'is-odd' }
35
+
36
+ with_tag 'tbody tr:first-child td:first-child', with: { role: 'title' }, text: 'Post 1'
37
+ with_tag 'tbody tr:first-child td:last-child', with: { role: 'actions' }
38
+
39
+ with_tag 'tbody tr:first-child td:last-child a', with: { role: 'show', href: '/foo' }
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '#simple_search_form_for' do
45
+ let(:params) {
46
+ {
47
+ query: { foo: 'test' }
48
+ }
49
+ }
50
+ let(:query) { TestQuery.new(params) }
51
+
52
+ it 'produces HTML' do
53
+ result = context.simple_search_form_for(query) do |f|
54
+ f.input :foo
55
+ end
56
+
57
+ expect(result).to have_tag(:form, with: { action: '/?p=1', method: 'get' }) do
58
+ with_tag 'input', with: { type: 'text', value: 'test', name: 'query[foo]' }
59
+ end
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ module Admino
4
+ module Query
5
+ describe BasePresenter do
6
+ subject(:presenter) { BasePresenter.new(query, view) }
7
+ let(:view) { RailsViewContext.new }
8
+ let(:query) do
9
+ TestQuery.new(query: { foo: 'value' })
10
+ end
11
+ let(:request_object) do
12
+ double(
13
+ 'ActionDispatch::Request',
14
+ fullpath: '/?foo=bar'
15
+ )
16
+ end
17
+
18
+ before do
19
+ allow(view).to receive(:request).and_return(request_object)
20
+ end
21
+
22
+ describe '#simple_form' do
23
+ let(:result) do
24
+ presenter.simple_form do |form|
25
+ form.input(:foo)
26
+ end
27
+ end
28
+
29
+ before do
30
+ I18n.backend.store_translations(
31
+ :en,
32
+ query: { attributes: { test_query: { foo: 'NAME' } } }
33
+ )
34
+ end
35
+
36
+ it 'renders a form pointing to the current URL' do
37
+ expect(result).to have_tag(:form, with: { action: '/?foo=bar' })
38
+ end
39
+
40
+ it 'renders a form with method GET' do
41
+ expect(result).to have_tag(:form, with: { method: 'get' })
42
+ end
43
+
44
+ it 'renders inputs with a query[] name prefix' do
45
+ expect(result).to have_tag(:input, with: { type: 'text', name: 'query[foo]' })
46
+ end
47
+
48
+ it 'prefills inputs with query value' do
49
+ expect(result).to have_tag(:input, with: { type: 'text', value: 'value' })
50
+ end
51
+
52
+ it 'it generates labels in the :query I18n space' do
53
+ expect(result).to have_tag(:label, content: 'NAME')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+