flexi_admin 0.0.5 → 0.0.6

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +254 -18
  3. data/lib/flexi_admin/components/helpers/action_helper.rb +21 -0
  4. data/lib/flexi_admin/components/helpers/frontend_helper.rb +23 -0
  5. data/lib/flexi_admin/components/helpers/link_helper.rb +35 -2
  6. data/lib/flexi_admin/components/helpers/resource_helper.rb +79 -14
  7. data/lib/flexi_admin/components/helpers/url_helper.rb +15 -0
  8. data/lib/flexi_admin/components/nav/breadcrumbs_component.html.slim +13 -0
  9. data/lib/flexi_admin/components/nav/breadcrumbs_component.rb +86 -0
  10. data/lib/flexi_admin/components/resource/autocomplete_component.html.slim +19 -7
  11. data/lib/flexi_admin/components/resource/autocomplete_component.rb +41 -11
  12. data/lib/flexi_admin/components/resource/button_select_component.html.slim +2 -2
  13. data/lib/flexi_admin/components/resource/button_select_component.rb +8 -1
  14. data/lib/flexi_admin/components/resource/filter_component.html.slim +47 -0
  15. data/lib/flexi_admin/components/resource/filter_component.rb +51 -0
  16. data/lib/flexi_admin/components/resource/form_component.rb +16 -1
  17. data/lib/flexi_admin/components/resource/form_element_component.rb +1 -1
  18. data/lib/flexi_admin/components/resource/form_mixin.rb +186 -42
  19. data/lib/flexi_admin/components/resource/link_action_component.html.slim +2 -2
  20. data/lib/flexi_admin/components/resource/link_action_component.rb +3 -2
  21. data/lib/flexi_admin/components/resource/view_component.html.slim +14 -4
  22. data/lib/flexi_admin/components/resource/view_component.rb +11 -2
  23. data/lib/flexi_admin/components/resources/bulk_action/button_component.rb +8 -1
  24. data/lib/flexi_admin/components/resources/bulk_action/modal_component.html.slim +21 -15
  25. data/lib/flexi_admin/components/resources/bulk_action/modal_component.rb +16 -8
  26. data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.html.slim +16 -0
  27. data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.rb +78 -0
  28. data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.html.slim +15 -0
  29. data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.rb +33 -0
  30. data/lib/flexi_admin/components/resources/grid_view/card_component.html.slim +4 -1
  31. data/lib/flexi_admin/components/resources/grid_view/card_component.rb +4 -1
  32. data/lib/flexi_admin/components/resources/grid_view/grid_component.html.slim +3 -1
  33. data/lib/flexi_admin/components/resources/grid_view/grid_component.rb +4 -2
  34. data/lib/flexi_admin/components/resources/grid_view_component.rb +7 -2
  35. data/lib/flexi_admin/components/resources/index_page_component.html.slim +5 -4
  36. data/lib/flexi_admin/components/resources/index_page_component.rb +1 -0
  37. data/lib/flexi_admin/components/resources/list_view/table_component.html.slim +1 -1
  38. data/lib/flexi_admin/components/resources/list_view_component.rb +2 -1
  39. data/lib/flexi_admin/components/resources/pagination_component.html.slim +51 -39
  40. data/lib/flexi_admin/components/resources/pagination_component.rb +52 -47
  41. data/lib/flexi_admin/components/resources/switch_view_component.html.slim +2 -2
  42. data/lib/flexi_admin/components/resources/view_component.html.slim +15 -6
  43. data/lib/flexi_admin/components/shared/medium_component.html.slim +1 -1
  44. data/lib/flexi_admin/components/shared/table/header_item_component.html.slim +1 -1
  45. data/lib/flexi_admin/components/shared/table/header_item_component.rb +59 -61
  46. data/lib/flexi_admin/components/shared/tabs_component.html.slim +26 -0
  47. data/lib/flexi_admin/components/shared/tabs_component.rb +68 -0
  48. data/lib/flexi_admin/components.rb +11 -3
  49. data/lib/flexi_admin/config.rb +21 -3
  50. data/lib/flexi_admin/controllers/modals_controller.rb +4 -2
  51. data/lib/flexi_admin/controllers/resources_controller.rb +246 -51
  52. data/lib/flexi_admin/engine.rb +4 -0
  53. data/lib/flexi_admin/helpers/application_helper.rb +3 -0
  54. data/lib/flexi_admin/javascript/controllers/add_row_controller.js +118 -0
  55. data/lib/flexi_admin/javascript/controllers/autocomplete_controller.js +17 -1
  56. data/lib/flexi_admin/javascript/controllers/bulk_action_controller.js +192 -33
  57. data/lib/flexi_admin/javascript/controllers/copy_to_clipboard_controller.js +16 -0
  58. data/lib/flexi_admin/javascript/controllers/datalist_controller.js +1 -1
  59. data/lib/flexi_admin/javascript/controllers/delete_controller.js +40 -0
  60. data/lib/flexi_admin/javascript/controllers/expandable_field_controller.js +15 -0
  61. data/lib/flexi_admin/javascript/controllers/filter_auto_submit_controller.js +93 -0
  62. data/lib/flexi_admin/javascript/controllers/index.js +18 -0
  63. data/lib/flexi_admin/javascript/controllers/pagination_controller.js +8 -0
  64. data/lib/flexi_admin/javascript/controllers/tabs_controller.js +24 -0
  65. data/lib/flexi_admin/models/concerns/application_resource.rb +6 -0
  66. data/lib/flexi_admin/models/concerns/parentable.rb +3 -0
  67. data/lib/flexi_admin/models/context_params.rb +35 -10
  68. data/lib/flexi_admin/models/resources/context.rb +4 -0
  69. data/lib/flexi_admin/routes.rb +7 -1
  70. data/lib/flexi_admin/services/create_resource.rb +6 -6
  71. data/lib/flexi_admin/version.rb +1 -1
  72. data/lib/flexi_admin/views/shared/_reload_frame.slim +19 -0
  73. data/lib/flexi_admin.rb +0 -3
  74. metadata +171 -10
@@ -2,20 +2,32 @@
2
2
  - if disabled && data_list?
3
3
  = value
4
4
  - elsif disabled && !data_list?
5
- a href="#{resource_path(resource)}" data-turbo-frame="_top" = resource&.ac_title
5
+ - if resource.present?
6
+ a href="#{resource_path(resource)}" data-turbo-frame="_top" = resource&.try(:ac_title) || resource&.try(:title)
6
7
  /= helpers.link_to resource&.ac_title, resource_path(resource), data: { turbo_frame: '_top' }
8
+ - else
9
+ span.text-muted = disabled_empty_custom_message
7
10
  - else
8
11
  .input-group style="width: #{width || '100%'}"
9
12
  .input-group-text id="button-addon1"
10
13
  i.bi class="#{icon_class}"
11
14
 
12
- = text_field_tag name, resource&.ac_title,
13
- class: 'form-control',
14
- placeholder: 'hledat',
15
+ - if data_list?
16
+ = text_area_tag name, value,
17
+ class: 'form-control expandable-field',
18
+ placeholder:,
15
19
  'aria-describedby': 'button-addon1',
16
- value: ,
17
20
  required:,
18
- **autocomplete_options
21
+ rows: 1,
22
+ **expandable_autocomplete_options
23
+ - else
24
+ = text_field_tag name, resource&.ac_title,
25
+ class: 'form-control',
26
+ placeholder:,
27
+ 'aria-describedby': 'button-addon1',
28
+ value: ,
29
+ required:,
30
+ **autocomplete_options
19
31
  .invalid-feedback.d-none
20
32
  - if select?
21
33
  input [type="hidden"
@@ -27,4 +39,4 @@
27
39
  .loading-icon.d-none data-autocomplete-target="loadingIcon"
28
40
  .icon
29
41
 
30
- ul.collapse.show data-autocomplete-target="list"
42
+ ul.collapse.show data-autocomplete-target="list"
@@ -5,41 +5,71 @@ module FlexiAdmin::Components::Resource
5
5
  class AutocompleteComponent < FlexiAdmin::Components::BaseComponent
6
6
  include FlexiAdmin::Components::Helpers::ResourceHelper
7
7
 
8
+ ALLOWED_ACTIONS = %i[select show input].freeze
9
+
8
10
  attr_reader :resource, :disabled, :action, :parent, :fields, :required,
9
- :name, :html_options, :path, :width, :value
11
+ :name, :html_options, :path, :width, :value,
12
+ :disabled_empty_custom_message, :placeholder, :priority_value
10
13
 
11
14
  def initialize(resource = nil, scope:, fields: [:title],
12
15
  action: :select, parent: nil, path: nil,
13
- value: nil, **html_options)
16
+ value: nil, disabled_empty_custom_message: nil,
17
+ target_controller: nil, placeholder: nil,
18
+ custom_scope: nil, priority_value: nil, **html_options)
14
19
  @resource = resource
15
20
  @scope = scope
21
+ @target_controller = target_controller
16
22
  @parent = parent
17
23
  @fields = fields
18
24
  @path = path
19
25
  @action = action
20
26
  @value = value
27
+ @custom_scope = custom_scope&.to_s
28
+ @priority_value = priority_value
21
29
 
22
30
  @html_options = html_options
23
31
  @width = html_options.delete(:width)
24
32
  @required = html_options[:required]
25
33
  @style = html_options.delete(:style)
26
34
  @disabled = html_options.key?(:disabled) ? html_options[:disabled] : false
35
+
27
36
  @name = html_options[:name] || resource_input_name
28
37
 
38
+ @disabled_empty_custom_message = disabled_empty_custom_message || 'žádný zdroj'
39
+ @placeholder = placeholder || 'hledat'
40
+
29
41
  validate_action!
30
42
  end
31
43
 
32
44
  def autocomplete_options
45
+ base_data = { autocomplete_target: 'input',
46
+ action: 'focus->autocomplete#focus keyup->autocomplete#keyup ' \
47
+ 'focusout->autocomplete#onFocusOut',
48
+ autocomplete_search_path: get_path,
49
+ autocomplete_is_disabled: disabled,
50
+ field_type: kind }.merge(html_options)
51
+
52
+ # An app can mark one record as "the one the user is working on" so the
53
+ # backend can float it to the top; the value is set client-side because
54
+ # only the app knows what "current" means (see `ac_priority`).
55
+ base_data[:autocomplete_priority_value] = priority_value if priority_value.present?
56
+
33
57
  {
34
58
  style: 'border-top-right-radius: 0.4rem; border-bottom-right-radius: 0.4rem;',
35
- data: { autocomplete_target: 'input',
36
- action: 'keyup->autocomplete#keyup focusout->autocomplete#onFocusOut',
37
- autocomplete_search_path: get_path,
38
- autocomplete_is_disabled: disabled,
39
- field_type: kind }.merge(html_options)
59
+ autocomplete: 'off',
60
+ data: base_data
40
61
  }
41
62
  end
42
63
 
64
+ def expandable_autocomplete_options
65
+ opts = autocomplete_options
66
+ data = opts[:data].merge(
67
+ controller: 'expandable-field',
68
+ action: "input->expandable-field#resize #{opts[:data][:action]}"
69
+ )
70
+ { data: data, **opts.except(:data) }
71
+ end
72
+
43
73
  private
44
74
 
45
75
  def icon_class
@@ -68,16 +98,16 @@ module FlexiAdmin::Components::Resource
68
98
 
69
99
  case action
70
100
  when :input
71
- datalist_path(action: :input, parent:, fields:)
101
+ datalist_path(action: :input, parent: parent, fields: fields)
72
102
  else
73
- autocomplete_path(action:, parent:, fields:)
103
+ autocomplete_path(action: action, parent: parent, fields: fields, custom_scope: @custom_scope)
74
104
  end
75
105
  end
76
106
 
77
107
  def validate_action!
78
- return if %i[select show input].include?(@action)
108
+ return if ALLOWED_ACTIONS.include?(@action)
79
109
 
80
110
  raise "Invalid action: #{@action}"
81
111
  end
82
112
  end
83
- end
113
+ end
@@ -2,11 +2,11 @@ div.button-select data-controller="button-select" data-button-select-disabled-va
2
2
  .btn-group
3
3
  - if disabled
4
4
  button.btn.btn-outline-secondary.text-nowrap data-value=value data-button-select-target="button" disabled=true
5
- = value || '-'
5
+ = value ? label_for(value) : '-'
6
6
  - else
7
7
  - options.each do |option|
8
8
  button.btn.btn-outline-secondary.text-nowrap data-action="click->button-select#select" data-value=option data-button-select-target="button"
9
- = option
9
+ = label_for(option)
10
10
  = hidden_field_tag html_options[:name] || attr_name, value, data: { 'button-select-target': 'input' }
11
11
  - if form
12
12
  .invalid-feedback data-button-select-target="invalid-feedback"
@@ -5,7 +5,7 @@ module FlexiAdmin::Components::Resource
5
5
  class ButtonSelectComponent < FlexiAdmin::Components::BaseComponent
6
6
  attr_reader :resource, :attr_name, :options, :form, :label, :value, :html_options, :disabled
7
7
 
8
- def initialize(resource, attr_name, options, form:, label: nil, value: nil, disabled: false, **html_options)
8
+ def initialize(resource, attr_name, options, form:, label: nil, value: nil, disabled: false, labels: nil, **html_options)
9
9
  @resource = resource
10
10
  @attr_name = attr_name
11
11
  @options = options
@@ -14,6 +14,13 @@ module FlexiAdmin::Components::Resource
14
14
  @value = value
15
15
  @html_options = html_options
16
16
  @disabled = disabled
17
+ @labels = labels || {}
18
+ end
19
+
20
+ def label_for(option)
21
+ return option if @labels.empty?
22
+
23
+ @labels[option] || @labels[option.to_s] || @labels[option.to_s.to_sym] || option
17
24
  end
18
25
  end
19
26
  end
@@ -0,0 +1,47 @@
1
+ .filters.w-100
2
+ - active_filters = @params.to_h.except('controller', 'action', 'subdomain').select { |k, v| v.present? }
3
+ - filter_label_lookup = {}
4
+ - filter_configs.each do |filter|
5
+ - filter_label_lookup[filter[:name].to_s] = filter[:options].index_by { |o| o[:value].to_s }
6
+ - if active_filters.any?
7
+ .mb-2.w-100
8
+ | Aktivní filtry:
9
+ - active_filters.each do |k, v|
10
+ - values = v.is_a?(Array) ? v : [v]
11
+ - values.each do |val|
12
+ - label = filter_label_lookup[k]&.dig(val.to_s, :label) || val
13
+ - new_params = @params.deep_dup
14
+ - if new_params[k].is_a?(Array)
15
+ - new_params[k] = new_params[k] - [val]
16
+ - new_params.delete(k) if new_params[k].empty?
17
+ - else
18
+ - new_params.delete(k)
19
+ span.badge.bg-primary.text-white.mx-1
20
+ = "#{field_labels[k] || k.humanize}: #{label}"
21
+ = link_to helpers.url_for(params: new_params), class: 'text-white ms-1', title: "Odebrat filtr" do
22
+ i.bi.bi-x
23
+ .form-bar.w-100
24
+ = form_with url: '', method: :get, local: true, html: { class: 'd-flex align-items-center flex-row flex-wrap gap-2 w-100 filter-bar', style: '', data: { controller: 'filter-auto-submit' } } do
25
+ - if search_field
26
+ div(style="flex: 1 1 auto; min-width: 250px; max-width: 500px;" data-action="focusout->filter-auto-submit#submitOnBlur")
27
+ = search_field
28
+ - filter_configs.each do |filter|
29
+ - custom_label = field_labels[filter[:name].to_s] || filter[:label]
30
+ - if filter[:multiple]
31
+ .flex-shrink-0(style="min-width: 180px;")
32
+ .dropdown
33
+ button.btn.btn-outline-secondary.form-control.dropdown-toggle type="button" id="#{filter[:dropdown_id]}" data-bs-toggle="dropdown" aria-expanded="false"
34
+ = custom_label
35
+ .dropdown-menu.p-2 aria-labelledby="#{filter[:dropdown_id]}" style="max-height: 300px; overflow-y: auto; min-width: 220px;" data-action="click->filter-auto-submit#handleDropdownChange:stop"
36
+ - filter[:options].each do |opt|
37
+ label.form-check-label.dropdown-item
38
+ = check_box_tag "#{filter[:name]}[]", opt[:value], Array(filter[:current_value]).include?(opt[:value].to_s), class: 'form-check-input me-2'
39
+ = opt[:label]
40
+ - else
41
+ .flex-shrink-0(style="min-width: 180px;")
42
+ - select_classes = ['form-select']
43
+ - select_classes << 'is-active' if filter[:current_value].present?
44
+ label.form-label.mb-0 = custom_label
45
+ = select_tag filter[:name], options_for_select(filter[:options].map { |o| [o[:label], o[:value]] }, filter[:current_value]), class: select_classes.join(' '), include_blank: true, onchange: 'this.form.submit();'
46
+ .d-flex.gap-2
47
+ = button_tag t('flexi_admin.filters.cancel', default: 'Zrušit'), type: 'button', class: 'btn btn-outline-secondary', onclick: "window.location = window.location.pathname;"
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlexiAdmin::Components::Resource
4
+ class FilterComponent < FlexiAdmin::Components::BaseComponent
5
+ include FlexiAdmin::Components::Helpers::ResourceHelper
6
+
7
+ attr_reader :filter_options, :filters, :params, :search_field, :field_labels
8
+
9
+ def initialize(filter_options:, filters: true, params: {}, search_field: nil, field_labels: {})
10
+ @filter_options = filter_options
11
+ @filters = filters
12
+ @search_field = search_field
13
+ @field_labels = field_labels.with_indifferent_access
14
+ # tech-debt: handle params conversion
15
+ if params.respond_to?(:to_unsafe_h)
16
+ @params = params.to_unsafe_h.with_indifferent_access
17
+ elsif params.respond_to?(:to_h)
18
+ @params = params.to_h.with_indifferent_access
19
+ else
20
+ @params = params.with_indifferent_access
21
+ end
22
+ end
23
+
24
+ def filter_configs
25
+ return [] unless filters && filter_options.present?
26
+
27
+ filter_options.map do |filter_name, options|
28
+ opts, multiple = if options.is_a?(Hash)
29
+ [options[:options], options[:multiple]]
30
+ else
31
+ [options, false]
32
+ end
33
+ opts = opts.map do |opt|
34
+ if opt.is_a?(Array)
35
+ { label: opt[1], value: opt[0] }
36
+ else
37
+ { label: opt.to_s.humanize, value: opt }
38
+ end
39
+ end
40
+ {
41
+ name: filter_name,
42
+ label: filter_name.to_s.humanize,
43
+ options: opts,
44
+ multiple: multiple,
45
+ current_value: params[filter_name],
46
+ dropdown_id: "dropdown-#{filter_name}"
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -4,6 +4,8 @@ module FlexiAdmin::Components::Resource
4
4
  class FormComponent < FlexiAdmin::Components::BaseComponent
5
5
  include FlexiAdmin::Components::Resource::FormMixin
6
6
 
7
+ ResourceOrScopeNotDefinedError = Class.new(StandardError)
8
+
7
9
  attr_reader :resource
8
10
 
9
11
  def initialize(resource, disabled: true)
@@ -12,7 +14,20 @@ module FlexiAdmin::Components::Resource
12
14
  end
13
15
 
14
16
  def disabled
15
- !helpers.current_ability&.can?(:update, resource) || @disabled
17
+ if defined?(CanCan)
18
+ raise ResourceOrScopeNotDefinedError, "Resource or scope is not defined" if resource.blank? && scope.blank?
19
+
20
+ resource_to_check = resource.presence || self.class.class_name || scope.classify.constantize
21
+
22
+ !helpers.current_ability&.can?(:update, resource_to_check) || @disabled
23
+ else
24
+ @disabled
25
+ end
26
+ rescue ResourceOrScopeNotDefinedError => e
27
+ BugTracker.notify(e, scope: scope)
28
+
29
+ # Let it pass, we need to fix missing scope or resource
30
+ false
16
31
  end
17
32
  end
18
33
  end
@@ -24,7 +24,7 @@ module FlexiAdmin::Components::Resource
24
24
 
25
25
  def merge_data_keys(data)
26
26
  controller = data[:controller]
27
- controllers = controller.present? ? [controller, "form-validation"] : ["form-validation"]
27
+ controllers = controller.present? ? [controller, "form-validation", "add-row"] : ["form-validation", "add-row"]
28
28
  data.merge(controller: controllers.join(" "))
29
29
  end
30
30
  end
@@ -28,48 +28,64 @@ module FlexiAdmin::Components::Resource
28
28
  attr_reader :resource, :disabled, :inline, :parent_resource
29
29
 
30
30
  # Form does not render; fields do.
31
- def form(url: resource_path(resource), css_class: 'myForm section', method: :patch, **html_options, &block)
31
+ # TODO: allow setting label_col/field_col for non-modal forms (e.g. via FormComponent initialize)
32
+ def form(url: resource_path(resource), css_class: 'myForm section', method: :patch, label_col: 'col-md-3', field_col: 'col-md-9', **html_options, &block)
33
+ previous_label_col = @label_col
34
+ previous_field_col = @field_col
35
+ @label_col = label_col
36
+ @field_col = field_col
37
+
32
38
  render FlexiAdmin::Components::Resource::FormElementComponent.new(resource, url:, css_class:, method:, **html_options) do |component|
33
39
  component.with_fields do
34
40
  capture(&block)
35
41
  end
36
42
  end
43
+ ensure
44
+ @label_col = previous_label_col
45
+ @field_col = previous_field_col
37
46
  end
38
47
 
39
48
  def select_field(attr_name, options:, label: nil, value: nil, **html_options)
40
49
  field = render_select(attr_name, value:, options:, **html_options)
41
50
  field_wrapper = render_field_wrapper(field, attr_name)
42
51
 
43
- inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:)
52
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
44
53
  end
45
54
 
46
55
  def button_select_field(attr_name, options:, label: nil, value: nil, **html_options)
47
56
  field = render_button_select(attr_name, options:, value:, disabled:, **html_options)
48
57
  field_wrapper = render_field_wrapper(field, attr_name)
49
58
 
50
- inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:)
59
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
51
60
  end
52
61
 
53
- def text_field(attr_name, label: nil, value: nil, **html_options)
54
- field = text_field_tag(attr_name, value:, **html_options)
62
+ def text_field(attr_name, label: nil, value: nil, expandable: false, **html_options)
63
+ field = if expandable
64
+ render_expandable_field(attr_name, value, html_options)
65
+ else
66
+ text_field_tag(attr_name, value:, **html_options)
67
+ end
55
68
  field_wrapper = render_field_wrapper(field, attr_name)
56
69
 
57
- inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:)
70
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
58
71
  end
59
72
 
60
73
  def text_field_tag(attr_name, value: nil, **html_options)
61
74
  render_standard_field(:text, attr_name, value, html_options)
62
75
  end
63
76
 
64
- def number_field(attr_name, label: nil, value: nil, **html_options)
65
- render_standard_field(:number, attr_name, value, html_options)
77
+ def number_field(attr_name, label: nil, value: nil, step: 0.01, **html_options)
78
+ field = render_standard_field(:number, attr_name, value, html_options.merge(step: step.to_s))
79
+ field_wrapper = render_field_wrapper(field, attr_name)
80
+
81
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
66
82
  end
67
83
 
68
84
  def checkbox_field(attr_name, label: nil, checked: nil, **html_options)
69
85
  checkbox = checkbox_field_tag(attr_name, checked:, **html_options)
70
86
  field_wrapper = render_field_wrapper(checkbox, attr_name)
71
87
 
72
- inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:)
88
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
73
89
  end
74
90
 
75
91
  def checkbox_field_tag(attr_name, checked: nil, **html_options)
@@ -99,14 +115,25 @@ module FlexiAdmin::Components::Resource
99
115
  field = render_standard_field(:date, attr_name, value, html_options.merge(style: 'max-width: 180px;'))
100
116
  field_wrapper = render_field_wrapper(field, attr_name)
101
117
 
102
- inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:)
118
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
103
119
  end
104
120
 
105
121
  def datetime_field(attr_name, label: nil, value: nil, **html_options)
106
122
  field = render_standard_field(:datetime, attr_name, value, html_options.merge(style: 'max-width: 180px;'))
107
123
  field_wrapper = render_field_wrapper(field, attr_name)
108
124
 
109
- inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:)
125
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
126
+ end
127
+
128
+ def text_area_field(attr_name, label: nil, value: nil, **html_options)
129
+ field = text_area_field_tag(attr_name, value:, **html_options)
130
+ field_wrapper = render_field_wrapper(field, attr_name)
131
+
132
+ inline ? field_wrapper : render_form_row(attr_name, field_wrapper, label:, required: html_options[:required])
133
+ end
134
+
135
+ def text_area_field_tag(attr_name, value: nil, **html_options)
136
+ content_tag(:textarea, value, html_options.merge(name: attr_name, class: 'form-control'))
110
137
  end
111
138
 
112
139
  # Trix field
@@ -158,6 +185,73 @@ module FlexiAdmin::Components::Resource
158
185
  end
159
186
  end
160
187
 
188
+ def add_row_button(container:, template_selector: '.dynamic-rows > .form-row:has(input, select, textarea)', label: I18n.t('flexi_admin.form.add_row_button'), icon: 'plus', classes: '')
189
+ button = if icon.present?
190
+ content_tag(:button, class: 'btn btn-outline-secondary ' + classes, type: 'button',
191
+ data: { action: 'click->add-row#add',
192
+ 'add-row-container-value': container,
193
+ 'add-row-template-selector-value': template_selector }) do
194
+ content = []
195
+ content << content_tag(:i, '', class: "bi bi-#{icon} me-2")
196
+ content << label
197
+ content.join.html_safe
198
+ end
199
+ else
200
+ content_tag(:button, label, class: 'btn btn-outline-secondary ' + classes, type: 'button',
201
+ data: { action: 'click->add-row#add',
202
+ 'add-row-container-value': container,
203
+ 'add-row-template-selector-value': template_selector })
204
+ end
205
+
206
+ content_tag(:div, class: 'form-row') do
207
+ content_tag(:div, class: 'col-12') { button }
208
+ end
209
+ end
210
+
211
+ def remove_row_button(icon: 'trash', classes: '')
212
+ content_tag(:button, class: 'btn btn-outline-danger btn-sm ' + classes, type: 'button',
213
+ data: { action: 'click->add-row#remove',
214
+ 'add-row-template-selector-value': '.form-row' }) do
215
+ content = []
216
+ content << content_tag(:i, '', class: "bi bi-#{icon}")
217
+ content.join.html_safe
218
+ end
219
+ end
220
+
221
+ def columns(**html_options, &block)
222
+ previous_column_row_counts = @column_row_counts
223
+ @column_row_counts = []
224
+ content = capture(&block)
225
+ max_rows = @column_row_counts.max || 1
226
+ col_count = @column_row_counts.size
227
+
228
+ css_classes = ['form-columns']
229
+ css_classes << html_options.delete(:class) if html_options[:class]
230
+
231
+ style = "grid-template-rows: repeat(#{max_rows}, auto); grid-template-columns: repeat(#{col_count}, 1fr);"
232
+ existing_style = html_options[:style]
233
+ style = "#{existing_style} #{style}" if existing_style
234
+
235
+ content_tag(:div, content, html_options.merge(class: css_classes.join(' '), style:))
236
+ ensure
237
+ @column_row_counts = previous_column_row_counts
238
+ end
239
+
240
+ def column(**html_options, &block)
241
+ previous_inside_column = @inside_column
242
+ @inside_column = true
243
+ @current_column_row_count = 0
244
+ content = capture(&block)
245
+ @column_row_counts << @current_column_row_count
246
+
247
+ css_classes = ['form-column']
248
+ css_classes << html_options.delete(:class) if html_options[:class]
249
+
250
+ content_tag(:div, content, html_options.merge(class: css_classes.join(' ')))
251
+ ensure
252
+ @inside_column = previous_inside_column
253
+ end
254
+
161
255
  def with_resource(resource)
162
256
  previous_resource = @resource
163
257
  @resource = resource
@@ -168,35 +262,57 @@ module FlexiAdmin::Components::Resource
168
262
 
169
263
  private
170
264
 
265
+ def render_expandable_field(attr_name, value, html_options)
266
+ val, input_classes, resolved_disabled = resolve_field_attrs(attr_name, value, html_options, extra_class: 'expandable-field')
267
+ data = {
268
+ controller: 'expandable-field',
269
+ action: 'input->expandable-field#resize blur->form-validation#handleInvalid keyup->form-validation#handleInvalid'
270
+ }
271
+ merge_model_error!(data, attr_name)
272
+
273
+ content_tag(:textarea, val, {
274
+ name: attr_name, class: input_classes, rows: 1, data:, disabled: resolved_disabled
275
+ }.merge(html_options.except(:disabled)))
276
+ end
277
+
171
278
  def render_standard_field(type, attr_name, value, html_options)
172
- val = value.is_a?(Proc) ? value.call : resource.try(attr_name) || value
279
+ val, input_classes, resolved_disabled = resolve_field_attrs(attr_name, value, html_options)
173
280
  data = {
174
281
  action: 'blur->form-validation#handleInvalid keyup->form-validation#handleInvalid'
175
282
  }
176
283
  merge_model_error!(data, attr_name) unless type == :submit
177
284
 
178
- content = []
179
- input_classes = 'form-control'
180
- input_classes += ' is-invalid' if resource.present? && attr_name.present? && resource.errors[attr_name].present?
181
- if type != :submit
182
- content << content_tag(:input, nil,
183
- {
184
- type:,
185
- name: attr_name,
186
- value: val,
187
- class: input_classes,
188
- data:
189
- }.merge(html_options)
190
- .merge(disabled:))
191
- end
285
+ return ''.html_safe if type == :submit
192
286
 
193
- content.join.html_safe
287
+ content_tag(:input, nil, {
288
+ type:, name: attr_name, value: val, class: input_classes, data:, disabled: resolved_disabled
289
+ }.merge(html_options.except(:disabled)))
290
+ end
291
+
292
+ def resolve_field_attrs(attr_name, value, html_options, extra_class: nil)
293
+ val = value.is_a?(Proc) ? value.call : resource.try(attr_name) || value
294
+ input_classes = ['form-control', extra_class].compact.join(' ')
295
+ input_classes += ' is-invalid' if resource.present? && attr_name.present? && resource.errors[attr_name].present?
296
+ resolved_disabled = html_options[:disabled] || disabled
297
+ [val, input_classes, resolved_disabled]
194
298
  end
195
299
 
196
300
  def render_field_wrapper(field, attr_name)
197
301
  content_tag(:div, class: inline ? 'inline-field' : 'field-wrapper') do
198
302
  content = []
199
- content << field
303
+ if disabled && attr_name.present?
304
+ content << content_tag(:div, class: 'copy-field-wrapper', data: { controller: 'copy-to-clipboard' }) do
305
+ field_content = []
306
+ field_content << field
307
+ field_content << content_tag(:button, type: 'button', class: 'copy-field-btn', tabindex: -1,
308
+ data: { action: 'click->copy-to-clipboard#copy' }) do
309
+ content_tag(:i, '', class: 'bi bi-clipboard')
310
+ end
311
+ field_content.join.html_safe
312
+ end
313
+ else
314
+ content << field
315
+ end
200
316
  if resource.present? && attr_name.present?
201
317
  content << content_tag(:div, class: 'invalid-feedback') do
202
318
  resource.errors[attr_name].join(' ')
@@ -255,25 +371,53 @@ module FlexiAdmin::Components::Resource
255
371
  **html_options))
256
372
  end
257
373
 
258
- def render_form_row(attr_name, field_html, label:, **html_options)
374
+ def render_form_row(attr_name, field_html, label:, required: false, **html_options)
259
375
  label ||= attr_name.to_s.humanize unless label == false
260
-
261
- content_tag(:div, html_options.merge(class: 'form-row')) do
262
- concat content_tag(:div, class: 'col-12 col-md-3') { label_tag(attr_name, label) } unless label == false
263
- concat content_tag(:div, class: inline ? 'col-12 col-md-9 inline-field-wrapper' : 'col-12 col-md-9') {
264
- field_html
265
- }
376
+ @current_column_row_count += 1 if @inside_column
377
+
378
+ row_classes = ['form-row']
379
+ label_col = @label_col || 'col-md-3'
380
+ field_col = @field_col || 'col-md-9'
381
+
382
+ content_tag(:div, html_options.merge(class: row_classes.join(' '))) do
383
+ lbl = label
384
+ lbl += ' *' if required && !disabled
385
+
386
+ if @inside_column
387
+ concat content_tag(:div, class: 'col-12') { label_tag(attr_name, lbl) } unless label == false
388
+ concat content_tag(:div, class: inline ? 'col-12 inline-field-wrapper' : 'col-12') { field_html }
389
+ else
390
+ resolved_field_col = label == false ? 'col-md-12' : field_col
391
+ concat content_tag(:div, class: "col-12 #{label_col}") { label_tag(attr_name, lbl) } unless label == false
392
+ concat content_tag(:div, class: inline ? "col-12 #{resolved_field_col} inline-field-wrapper" : "col-12 #{resolved_field_col}") {
393
+ field_html
394
+ }
395
+ end
266
396
  end
267
397
  end
268
398
 
269
399
  def render_custom_field(view_component_instance, label, html_options, _value)
270
- content_tag(:div, html_options.merge(class: 'form-row')) do
271
- unless label == false
272
- concat content_tag(:div, class: 'col-12 col-md-3') {
273
- label_tag('autocomplete', label)
274
- }
400
+ @current_column_row_count += 1 if @inside_column
401
+
402
+ row_classes = ['form-row']
403
+ label_col = @label_col || 'col-md-3'
404
+ field_col = @field_col || 'col-md-9'
405
+
406
+ content_tag(:div, html_options.merge(class: row_classes.join(' '))) do
407
+ if @inside_column
408
+ unless label == false
409
+ concat content_tag(:div, class: 'col-12') { label_tag('autocomplete', label) }
410
+ end
411
+ concat content_tag(:div, class: 'col-12') { render view_component_instance }
412
+ else
413
+ unless label == false
414
+ concat content_tag(:div, class: "col-12 #{label_col}") {
415
+ label_tag('autocomplete', label)
416
+ }
417
+ end
418
+ resolved_field_col = label == false ? 'col-md-12' : field_col
419
+ concat content_tag(:div, class: "col-12 #{resolved_field_col}") { render view_component_instance }
275
420
  end
276
- concat content_tag(:div, class: 'col-12 col-md-9') { render view_component_instance }
277
421
  end
278
422
  end
279
423
 
@@ -284,4 +428,4 @@ module FlexiAdmin::Components::Resource
284
428
  data.merge!(validation_error: resource.errors[attr_name].join(' ')) if resource.errors.present?
285
429
  end
286
430
  end
287
- end
431
+ end
@@ -1,8 +1,8 @@
1
1
  - if icon.present?
2
- = link_to path, **options.merge(data: { turbo_frame: '_top' }, class: 'dropdown-item') do
2
+ = link_to path, **options.merge(data: { turbo_frame: '_top' }, class: "dropdown-item #{disabled ? 'disabled' : ''}") do
3
3
  i.bi.me-2 class=icon_class
4
4
  = label
5
5
  /a href="#{path}" data-turbo-frame="_top" class="dropdown-item"
6
6
  - else
7
- = link_to label, path, **options.merge(data: { turbo_frame: '_top' }, class: 'dropdown-item')
7
+ = link_to label, path, **options.merge(data: { turbo_frame: '_top' }, class: "dropdown-item #{disabled ? 'disabled' : ''}")
8
8
  /a href="#{path}" data-turbo-frame="_top" class="dropdown-item" = label