super 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@superadministration/super",
3
3
  "description": "The frontend code for Super, a Rails admin framework",
4
- "homepage": "https://github.com/zachahn/super",
4
+ "homepage": "https://github.com/superadministration/super",
5
5
  "license": "LGPL-3.0-only",
6
6
  "main": "application.js",
7
7
  "files": [
@@ -9,5 +9,5 @@
9
9
  "application.css",
10
10
  "package.json"
11
11
  ],
12
- "version": "0.19.0"
12
+ "version": "0.20.0"
13
13
  }
@@ -22,6 +22,9 @@ module Super
22
22
  "read" => %w[index show new edit],
23
23
  "write" => %w[create update destroy],
24
24
  "delete" => %w[destroy],
25
+
26
+ "collection" => %w[index new create],
27
+ "member" => %w[show edit update destroy],
25
28
  }
26
29
  end
27
30
 
data/lib/super/error.rb CHANGED
@@ -31,6 +31,7 @@ module Super
31
31
  class AlreadyRegistered < Error; end
32
32
  class AlreadyTranscribed < Error; end
33
33
  class NotImplementedError < Error; end
34
+ class IncompleteBuilder < Error; end
34
35
 
35
36
  class Enum < Error
36
37
  class ImpossibleValue < Enum; end
@@ -105,42 +105,6 @@ module Super
105
105
  Direct.new(super_builder: super_builder, method_name: method_name, args: args, kwargs: kwargs)
106
106
  end
107
107
 
108
- def select(*args, **kwargs)
109
- Direct.new(super_builder: true, method_name: :select!, args: args, kwargs: kwargs)
110
- end
111
-
112
- def text_field(*args, **kwargs)
113
- Direct.new(super_builder: true, method_name: :text_field!, args: args, kwargs: kwargs)
114
- end
115
-
116
- def rich_text_area(*args, **kwargs)
117
- Direct.new(super_builder: true, method_name: :rich_text_area!, args: args, kwargs: kwargs)
118
- end
119
-
120
- def check_box(*args, **kwargs)
121
- Direct.new(super_builder: true, method_name: :check_box!, args: args, kwargs: kwargs)
122
- end
123
-
124
- def date_flatpickr(*args, **kwargs)
125
- Direct.new(super_builder: true, method_name: :date_flatpickr!, args: args, kwargs: kwargs)
126
- end
127
-
128
- def datetime_flatpickr(*args, **kwargs)
129
- Direct.new(super_builder: true, method_name: :datetime_flatpickr!, args: args, kwargs: kwargs)
130
- end
131
-
132
- def hidden_field(*args, **kwargs)
133
- Direct.new(super_builder: false, method_name: :hidden_field, args: args, kwargs: kwargs)
134
- end
135
-
136
- def password_field(*args, **kwargs)
137
- Direct.new(super_builder: true, method_name: :password_field!, args: args, kwargs: kwargs)
138
- end
139
-
140
- def time_flatpickr(*args, **kwargs)
141
- Direct.new(super_builder: true, method_name: :time_flatpickr!, args: args, kwargs: kwargs)
142
- end
143
-
144
108
  def has_many(reader, **extras)
145
109
  subfields = Schema::Fields.new
146
110
  @fields.nested do
@@ -176,6 +140,14 @@ module Super
176
140
  nested: {}
177
141
  )
178
142
  end
143
+
144
+ def self.define_schema_type_for(method_name)
145
+ class_eval(<<~RUBY)
146
+ def #{method_name}(*args, **kwargs)
147
+ Direct.new(super_builder: true, method_name: :#{method_name}!, args: args, kwargs: kwargs)
148
+ end
149
+ RUBY
150
+ end
179
151
  end
180
152
  end
181
153
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super
4
+ class FormBuilder
5
+ class Wrappers
6
+ def rich_text_area(attribute, options = {})
7
+ options, defaults = split_defaults(options, class: "trix-content super-input w-full")
8
+ options[:class] = join_classes(defaults[:class], options[:class])
9
+
10
+ @builder.rich_text_area(attribute, options)
11
+ end
12
+
13
+ define_convenience :rich_text_area
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super
4
+ class FormBuilder
5
+ class Wrappers
6
+ skipped_field_helpers = [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]
7
+ (ActionView::Helpers::FormBuilder.field_helpers - skipped_field_helpers).each do |builder_method_name|
8
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
9
+ def #{builder_method_name}(attribute, options = {})
10
+ options, defaults = split_defaults(options, class: "super-input w-full")
11
+ options[:class] = join_classes(defaults[:class], options[:class])
12
+
13
+ @builder.#{builder_method_name}(attribute, options)
14
+ end
15
+
16
+ define_convenience :#{builder_method_name}
17
+ RUBY
18
+ end
19
+
20
+ def label(attribute, text = nil, options = {}, &block)
21
+ options, defaults = split_defaults(options, class: "block")
22
+ options[:class] = join_classes(defaults[:class], options[:class])
23
+
24
+ @builder.label(attribute, text, options, &block)
25
+ end
26
+
27
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
28
+ @builder.check_box(attribute, options, checked_value, unchecked_value)
29
+ end
30
+
31
+ def check_box!(attribute, checked_value: "1", unchecked_value: "0", label_text: nil, label: {}, field: {}, show_errors: true)
32
+ label[:super] ||= {}
33
+ label[:super] = { class: "select-none ml-1" }.merge(label[:super])
34
+ container do
35
+ compact_join([
36
+ "<div>".html_safe,
37
+ public_send(:check_box, attribute, field, checked_value, unchecked_value),
38
+ public_send(:label, attribute, label_text, label),
39
+ "</div>".html_safe,
40
+ show_errors && inline_errors(attribute),
41
+ ])
42
+ end
43
+ end
44
+
45
+ ::Super::Form::SchemaTypes.define_schema_type_for(:check_box)
46
+
47
+ # def file_field(attribute, options = {})
48
+ # end
49
+
50
+ # def file_field!(attribute, label_text: nil, label: {}, field: {}, show_errors: true)
51
+ # end
52
+
53
+ def hidden_field(attribute, options = {})
54
+ @builder.hidden_field(attribute, options)
55
+ end
56
+
57
+ # def radio_button(attribute, tag_value, options = {})
58
+ # @builder.radio_button(attribute, tag_value, options)
59
+ # end
60
+
61
+ # def radio_button(attribute, tag_value, label_text: nil, label: {}, field: {}, show_errors: true)
62
+ # end
63
+
64
+ def submit(value = nil, options = {})
65
+ value, options = nil, value if value.is_a?(Hash)
66
+ options, defaults = split_defaults(options, class: "super-button")
67
+ options[:class] = join_classes(defaults[:class], options[:class])
68
+
69
+ @builder.submit(value, options)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super
4
+ class FormBuilder
5
+ class Wrappers
6
+ def date_flatpickr(attribute, options = {})
7
+ options, defaults = split_defaults(
8
+ options,
9
+ class: "super-input w-full",
10
+ data: {
11
+ controller: "flatpickr",
12
+ flatpickr_options_value: {
13
+ dateFormat: "Y-m-d",
14
+ }
15
+ }
16
+ )
17
+ options[:class] = join_classes(defaults[:class], options[:class])
18
+ options[:data] = defaults[:data].deep_merge(options[:data] || {})
19
+ options[:value] = @builder.object.public_send(attribute).presence
20
+ options[:value] = options[:value].iso8601 if options[:value].respond_to?(:iso8601)
21
+
22
+ @builder.text_field(attribute, options)
23
+ end
24
+
25
+ define_convenience :date_flatpickr
26
+
27
+ def datetime_flatpickr(attribute, options = {})
28
+ options, defaults = split_defaults(
29
+ options,
30
+ class: "super-input w-full",
31
+ data: {
32
+ controller: "flatpickr",
33
+ flatpickr_options_value: {
34
+ enableSeconds: true,
35
+ enableTime: true,
36
+ dateFormat: "Z",
37
+ }
38
+ }
39
+ )
40
+ options[:class] = join_classes(defaults[:class], options[:class])
41
+ options[:data] = defaults[:data].deep_merge(options[:data] || {})
42
+ options[:value] = @builder.object.public_send(attribute).presence
43
+ options[:value] = options[:value].iso8601 if options[:value].respond_to?(:iso8601)
44
+
45
+ @builder.text_field(attribute, options)
46
+ end
47
+
48
+ define_convenience :datetime_flatpickr
49
+
50
+ def time_flatpickr(attribute, options = {})
51
+ options, defaults = split_defaults(
52
+ options,
53
+ class: "super-input w-full",
54
+ data: {
55
+ controller: "flatpickr",
56
+ flatpickr_options_value: {
57
+ enableSeconds: true,
58
+ enableTime: true,
59
+ noCalendar: true,
60
+ dateFormat: "H:i:S",
61
+ }
62
+ }
63
+ )
64
+ options[:class] = join_classes(defaults[:class], options[:class])
65
+ options[:data] = defaults[:data].deep_merge(options[:data] || {})
66
+ options[:value] = @builder.object.public_send(attribute).presence
67
+ options[:value] = options[:value].strftime("%H:%M:%S") if options[:value].respond_to?(:strftime)
68
+
69
+ @builder.text_field(attribute, options)
70
+ end
71
+
72
+ define_convenience :time_flatpickr
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super
4
+ class FormBuilder
5
+ class Wrappers
6
+ def select(attribute, choices, options = {}, html_options = {}, &block)
7
+ options, defaults = split_defaults(options, include_blank: true)
8
+ options = defaults.merge(options)
9
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select")
10
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
11
+
12
+ @builder.select(attribute, choices, options, html_options, &block)
13
+ end
14
+
15
+ define_convenience :select
16
+
17
+ def collection_select(attribute, collection, value_method, text_method, options = {}, html_options = {})
18
+ options, defaults = split_defaults(options, include_blank: true)
19
+ options = defaults.merge(options)
20
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select")
21
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
22
+
23
+ @builder.collection_select(attribute, collection, value_method, text_method, options, html_options)
24
+ end
25
+
26
+ define_convenience :collection_select
27
+
28
+ def grouped_collection_select(attribute, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
29
+ options, defaults = split_defaults(options, include_blank: true)
30
+ options = defaults.merge(options)
31
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select")
32
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
33
+
34
+ @builder.grouped_collection_select(attribute, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
35
+ end
36
+
37
+ define_convenience :grouped_collection_select
38
+
39
+ def time_zone_select(attribute, priority_zones = nil, options = {}, html_options = {})
40
+ options, defaults = split_defaults(options, include_blank: true)
41
+ options = defaults.merge(options)
42
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select")
43
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
44
+
45
+ @builder.time_zone_select(attribute, priority_zones, options, html_options)
46
+ end
47
+
48
+ define_convenience :time_zone_select, priority_zones: "nil"
49
+
50
+ def collection_check_boxes(attribute, collection, value_method, text_method, options = {}, html_options = {}, &block)
51
+ options, defaults = split_defaults(options, include_blank: true)
52
+ options = defaults.merge(options)
53
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select")
54
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
55
+
56
+ @builder.collection_check_boxes(attribute, collection, value_method, text_method, options, html_options, &block)
57
+ end
58
+
59
+ define_convenience :collection_check_boxes
60
+
61
+ def collection_radio_buttons(attribute, collection, value_method, text_method, options = {}, html_options = {}, &block)
62
+ options, defaults = split_defaults(options, include_blank: true)
63
+ options = defaults.merge(options)
64
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select")
65
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
66
+
67
+ @builder.collection_radio_buttons(attribute, collection, value_method, text_method, options, html_options, &block)
68
+ end
69
+
70
+ define_convenience :collection_radio_buttons
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super
4
+ # Example
5
+ #
6
+ # ```ruby
7
+ # super_form_for([:admin, @member]) do |f|
8
+ # # the long way
9
+ # f.super.label :name
10
+ # f.super.text_field :name
11
+ # f.super.inline_errors :name
12
+ #
13
+ # # the short way (slightly different from the long way, for alignment)
14
+ # f.super.text_field! :position
15
+ # end
16
+ # ```
17
+ #
18
+ # Refer to the Rails docs:
19
+ # https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html
20
+ class FormBuilder < ActionView::Helpers::FormBuilder
21
+ FIELD_ERROR_PROC = proc { |html_tag, instance| html_tag }
22
+ FORM_BUILDER_DEFAULTS = { builder: self }.freeze
23
+
24
+ def super(**options)
25
+ @super_wrappers ||= Wrappers.new(self, @template)
26
+ end
27
+
28
+ class Wrappers
29
+ def initialize(builder, template)
30
+ @builder = builder
31
+ @template = template
32
+ end
33
+
34
+ def inline_errors(attribute)
35
+ if @builder.object
36
+ messages = Form::InlineErrors.error_messages(@builder.object, attribute).map do |msg|
37
+ error_content_tag(msg)
38
+ end
39
+
40
+ @template.safe_join(messages)
41
+ else
42
+ error_content_tag(<<~MSG.html_safe)
43
+ This form doesn't have an object, so something is probably wrong.
44
+ Maybe <code>accepts_nested_attributes_for</code> isn't set up?
45
+ MSG
46
+ end
47
+ end
48
+
49
+ def container(&block)
50
+ @template.content_tag(:div, class: "super-field-group", &block)
51
+ end
52
+
53
+ private
54
+
55
+ private_class_method def self.define_with_label_tag(method_name, **optionals)
56
+ parameters = instance_method(method_name).parameters
57
+ definition = []
58
+ call = []
59
+ definition_last = ["label_text: nil", "label: {}", "show_errors: true"]
60
+ call_last = []
61
+ parameters.each do |type, name|
62
+ if name == :options && type == :opt
63
+ definition.push("field: {}")
64
+ call.push("field")
65
+ elsif name == :html_options && type == :opt
66
+ definition.push("field_html: {}")
67
+ call.push("field_html")
68
+ elsif type == :block
69
+ definition_last.push("&#{name}")
70
+ call_last.push("&#{name}")
71
+ else
72
+ if type == :req
73
+ definition.push(name.to_s)
74
+ call.push(name.to_s)
75
+ elsif type == :opt || type == :key
76
+ if !optionals.key?(name)
77
+ raise Super::Error::ArgumentError, "Form bang method has optional argument, but doesn't know the default value: #{name}"
78
+ end
79
+
80
+ default_value = optionals[name]
81
+
82
+ if type == :opt
83
+ definition.push("#{name} = #{default_value}")
84
+ call.push(name.to_s)
85
+ elsif type == :key
86
+ definition.push("#{name}: #{default_value}")
87
+ call.push("#{name}: #{name}")
88
+ else
89
+ raise Super::Error::ArgumentError, "Form bang method has a unprocessable argument with name #{name}"
90
+ end
91
+ else
92
+ raise Super::Error::ArgumentError, "Form bang method has keyword argument type #{type} and name #{name}"
93
+ end
94
+ end
95
+ end
96
+
97
+ definition += definition_last
98
+ call += call_last
99
+
100
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
101
+ def #{method_name}!(#{definition.join(", ")})
102
+ container do
103
+ compact_join([
104
+ public_send(:label, attribute, label_text, label),
105
+ %(<div class="mt-1">).html_safe,
106
+ #{method_name}(#{call.join(", ")}),
107
+ show_errors && inline_errors(attribute),
108
+ %(</div>).html_safe,
109
+ ])
110
+ end
111
+ end
112
+ RUBY
113
+ end
114
+
115
+ private_class_method def self.define_convenience(method_name, *args, **kwargs)
116
+ define_with_label_tag(method_name, *args, **kwargs)
117
+ ::Super::Form::SchemaTypes.define_schema_type_for(method_name)
118
+ end
119
+
120
+ def split_defaults(options, **internal_defaults)
121
+ defaults = options.delete(:super) || {}
122
+ # prefer options set in `defaults`, since they are user overrides
123
+ defaults = internal_defaults.merge(defaults)
124
+
125
+ [options, defaults]
126
+ end
127
+
128
+ def join_classes(*class_lists)
129
+ class_lists.flatten.map(&:presence).compact
130
+ end
131
+
132
+ def error_content_tag(content)
133
+ @template.content_tag(:p, content, class: "text-red-400 text-xs italic pt-1")
134
+ end
135
+
136
+ def compact_join(*parts)
137
+ @template.safe_join(
138
+ parts.flatten.map(&:presence).compact
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
data/lib/super/link.rb CHANGED
@@ -9,75 +9,43 @@ module Super
9
9
  end
10
10
 
11
11
  def self.find(link)
12
- if link.kind_of?(self)
13
- return link
14
- end
15
-
16
12
  if registry.key?(link)
17
- return registry[link]
13
+ found = registry[link]
14
+
15
+ if found.is_a?(LinkBuilder)
16
+ return found.dup
17
+ else
18
+ return found
19
+ end
18
20
  end
19
21
 
20
22
  raise Error::LinkNotRegistered, "Unknown link `#{link}`"
21
23
  end
22
24
 
23
25
  def self.registry
24
- @registry ||= {
25
- new: LinkBuilder.new(
26
- "New",
27
- -> (params:) {
28
- {
29
- controller: params[:controller],
30
- action: :new,
31
- only_path: true
32
- }
33
- }
34
- ),
35
- index: LinkBuilder.new(
36
- "Index",
37
- -> (params:) {
38
- {
39
- controller: params[:controller],
40
- action: :index,
41
- only_path: true
42
- }
43
- }
44
- ),
45
- show: LinkBuilder.new(
46
- "View",
47
- -> (record:, params:) {
48
- {
49
- controller: params[:controller],
50
- action: :show,
51
- id: record,
52
- only_path: true
53
- }
54
- }
55
- ),
56
- edit: LinkBuilder.new(
57
- "Edit",
58
- -> (record:, params:) {
59
- {
60
- controller: params[:controller],
61
- action: :edit,
62
- id: record,
63
- only_path: true
64
- }
65
- }
66
- ),
67
- destroy: LinkBuilder.new(
68
- "Delete",
69
- -> (record:, params:) {
70
- {
71
- controller: params[:controller],
72
- action: :destroy,
73
- id: record,
74
- only_path: true
75
- }
76
- },
77
- method: :delete,
78
- data: { confirm: "Really delete?" }
79
- ),
80
- }
26
+ @registry ||= {}.tap do |reg|
27
+ reg[:new] = LinkBuilder.new
28
+ .text { |params:| Super::Useful::I19.i18n_with_fallback("super", params[:controller].split("/"), "actions.new") }
29
+ .href { |params:| { controller: params[:controller], action: :new, only_path: true } }
30
+ .freeze
31
+ reg[:index] = LinkBuilder.new
32
+ .text { |params:| Super::Useful::I19.i18n_with_fallback("super", params[:controller].split("/"), "actions.index") }
33
+ .href { |params:| { controller: params[:controller], action: :index, only_path: true } }
34
+ .freeze
35
+ reg[:show] = LinkBuilder.new
36
+ .text { |params:, **| Super::Useful::I19.i18n_with_fallback("super", params[:controller].split("/"), "actions.show") }
37
+ .href { |params:, record:| { controller: params[:controller], action: :show, id: record, only_path: true } }
38
+ .freeze
39
+ reg[:edit] = LinkBuilder.new
40
+ .text { |params:, **| Super::Useful::I19.i18n_with_fallback("super", params[:controller].split("/"), "actions.edit") }
41
+ .href { |params:, record:| { controller: params[:controller], action: :edit, id: record, only_path: true } }
42
+ .freeze
43
+ reg[:destroy] = LinkBuilder.new
44
+ .text { |params:, **| Super::Useful::I19.i18n_with_fallback("super", params[:controller].split("/"), "actions.destroy") }
45
+ .href { |params:, record:| { controller: params[:controller], action: :destroy, id: record, only_path: true } }
46
+ .options { |**| { method: :delete, data: { confirm: "Really delete?" } } }
47
+ .freeze
48
+ end
81
49
  end
82
50
 
83
51
  def self.polymorphic_parts(*parts_tail)
@@ -85,15 +53,31 @@ module Super
85
53
  parts_head.map { |part| part.is_a?(String) ? part.to_sym : part } + parts_tail
86
54
  end
87
55
 
56
+ # The first argument should be the text of the link. If it's an array,
57
+ # it'll send it directly into `I18n.t`
88
58
  def initialize(text, href, **options)
89
59
  @text = text
90
60
  @href = href
91
61
  @options = options
92
62
  end
93
63
 
94
- attr_reader :text
95
64
  attr_reader :options
96
65
 
66
+ def text
67
+ if @text.is_a?(Array)
68
+ *head, tail = @text
69
+ if !tail.is_a?(Hash)
70
+ head.push(tail)
71
+ tail = {}
72
+ end
73
+
74
+ @text = I18n.t(*head, **tail)
75
+ return @text
76
+ end
77
+
78
+ @text
79
+ end
80
+
97
81
  def href
98
82
  if @href.is_a?(String)
99
83
  return @href