avo 2.0.0 → 2.1.2.pre1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of avo might be problematic. Click here for more details.

Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +3 -3
  4. data/app/assets/builds/avo.css +8810 -0
  5. data/app/assets/builds/avo.js +423 -0
  6. data/app/assets/builds/avo.js.map +7 -0
  7. data/app/assets/stylesheets/avo.css +1 -1
  8. data/app/assets/svgs/save.svg +8 -1
  9. data/app/components/avo/actions_component.html.erb +1 -1
  10. data/app/components/avo/alert_component.html.erb +1 -1
  11. data/app/components/avo/alert_component.rb +6 -6
  12. data/app/components/avo/blank_field_component.html.erb +0 -0
  13. data/app/components/avo/blank_field_component.rb +4 -0
  14. data/app/components/avo/card_component.html.erb +7 -2
  15. data/app/components/avo/fields/badge_field/index_component.html.erb +1 -1
  16. data/app/components/avo/fields/boolean_field/index_component.html.erb +1 -1
  17. data/app/components/avo/fields/external_image_field/index_component.html.erb +1 -2
  18. data/app/components/avo/fields/file_field/index_component.html.erb +3 -3
  19. data/app/components/avo/fields/file_field/index_component.rb +11 -0
  20. data/app/components/avo/fields/gravatar_field/index_component.html.erb +1 -1
  21. data/app/components/avo/fields/progress_bar_field/index_component.html.erb +1 -1
  22. data/app/components/avo/index/field_wrapper_component.html.erb +1 -1
  23. data/app/components/avo/index/field_wrapper_component.rb +12 -1
  24. data/app/components/avo/index/resource_controls_component.html.erb +5 -1
  25. data/app/components/avo/index/resource_table_component.html.erb +1 -1
  26. data/app/components/avo/panel_component.html.erb +15 -5
  27. data/app/components/avo/panel_component.rb +9 -0
  28. data/app/components/avo/referrer_params_component.html.erb +4 -0
  29. data/app/components/avo/referrer_params_component.rb +9 -0
  30. data/app/components/avo/sidebar_component.html.erb +2 -0
  31. data/app/components/avo/sidebar_component.rb +3 -1
  32. data/app/components/avo/views/resource_edit_component.html.erb +14 -1
  33. data/app/components/avo/views/resource_new_component.html.erb +13 -0
  34. data/app/components/avo/views/resource_show_component.html.erb +11 -0
  35. data/app/components/avo/views/resource_show_component.rb +5 -0
  36. data/app/controllers/avo/application_controller.rb +1 -1
  37. data/app/controllers/avo/base_controller.rb +87 -37
  38. data/app/controllers/avo/dashboards_controller.rb +2 -6
  39. data/app/controllers/avo/search_controller.rb +5 -1
  40. data/app/helpers/avo/url_helpers.rb +6 -2
  41. data/app/javascript/avo.js +4 -3
  42. data/app/views/avo/actions/show.html.erb +1 -0
  43. data/app/views/avo/associations/new.html.erb +1 -0
  44. data/app/views/avo/dashboards/show.html.erb +4 -1
  45. data/app/views/avo/home/failed_to_load.html.erb +21 -1
  46. data/app/views/avo/partials/_sidebar_extra.html.erb +0 -0
  47. data/app/views/layouts/avo/application.html.erb +7 -0
  48. data/bin/dev +7 -6
  49. data/lib/avo/app.rb +8 -1
  50. data/lib/avo/base_action.rb +5 -4
  51. data/lib/avo/base_card.rb +175 -0
  52. data/lib/avo/base_resource.rb +34 -8
  53. data/lib/avo/configuration.rb +2 -0
  54. data/lib/avo/dashboards/base_dashboard.rb +37 -2
  55. data/lib/avo/dashboards/base_divider.rb +3 -1
  56. data/lib/avo/dashboards/chartkick_card.rb +1 -1
  57. data/lib/avo/dashboards/dashboard_card.rb +6 -0
  58. data/lib/avo/dashboards/partial_card.rb +1 -1
  59. data/lib/avo/fields/base_field.rb +19 -6
  60. data/lib/avo/fields/belongs_to_field.rb +1 -1
  61. data/lib/avo/fields/date_field.rb +1 -1
  62. data/lib/avo/fields/external_image_field.rb +2 -2
  63. data/lib/avo/fields_collector.rb +7 -2
  64. data/lib/avo/hosts/dashboard_card.rb +1 -0
  65. data/lib/avo/hosts/dashboard_visibility.rb +19 -0
  66. data/lib/avo/licensing/pro_license.rb +1 -0
  67. data/lib/avo/version.rb +1 -1
  68. data/lib/generators/avo/templates/dashboards/dashboard.tt +4 -1
  69. data/public/avo-assets/avo.css +55 -34
  70. data/public/avo-assets/avo.js +166 -165
  71. data/public/avo-assets/avo.js.map +3 -3
  72. metadata +15 -6
  73. data/app/views/avo/partials/_failed_state.html.erb +0 -16
  74. data/lib/avo/dashboards/base_card.rb +0 -151
@@ -74,7 +74,7 @@ body {
74
74
  }
75
75
 
76
76
  .turbo-progress-bar {
77
- @apply bg-green-500;
77
+ @apply bg-blue-400;
78
78
  }
79
79
 
80
80
  body.os-mac .mac\:hidden {
@@ -1 +1,8 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z"/></svg>
1
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 515 450" style="enable-background:new 0 0 515 450;" xml:space="preserve">
2
+ <path d="M438.4,116.4l-72.3-72.3c-7.8-7.8-18.3-12.1-29.2-12.1h-231C83,32,64.5,50.5,64.5,73.4v303.3c0,22.8,18.5,41.4,41.4,41.4
3
+ h303.3c22.8,0,41.4-18.5,41.4-41.4v-231C450.5,134.7,446.1,124.1,438.4,116.4L438.4,116.4z M298.9,73.4v68.9H188.6V73.4H298.9z
4
+ M404,376.6H111c-2.9,0-5.2-2.3-5.2-5.2V78.5c0-2.9,2.3-5.2,5.2-5.2h36.2V163c0,11.4,9.3,20.7,20.7,20.7h151.6
5
+ c11.4,0,20.7-9.3,20.7-20.7V76.7l67.4,67.4c1,1,1.5,2.3,1.5,3.7v223.7C409.1,374.3,406.8,376.6,404,376.6L404,376.6z M257.5,204.3
6
+ c-41.8,0-75.8,34-75.8,75.8s34,75.8,75.8,75.8s75.8-34,75.8-75.8S299.3,204.3,257.5,204.3z M257.5,314.6c-19,0-34.5-15.5-34.5-34.5
7
+ s15.5-34.5,34.5-34.5s34.5,15.5,34.5,34.5S276.5,314.6,257.5,314.6z"/>
8
+ </svg>
@@ -12,7 +12,7 @@
12
12
  <%= t 'avo.actions' %>
13
13
  <% end %>
14
14
  <div
15
- class="absolute flex inset-auto right-0 top-full bg-white w-full sm:w-auto sm:min-w-[300px] mt-2 z-20 shadow-modal rounded overflow-hidden hidden"
15
+ class="absolute flex inset-auto xl:right-0 top-full bg-white w-full sm:w-auto sm:min-w-[300px] mt-2 z-20 shadow-modal rounded overflow-hidden hidden"
16
16
  data-toggle-panel-target="panel"
17
17
  >
18
18
  <div class="w-full space divide-y">
@@ -10,7 +10,7 @@
10
10
  </div>
11
11
  <div class="ml-3 w-0 flex-1 pt-0.5">
12
12
  <p class="text-sm leading-5 font-semibold">
13
- <%= message %>
13
+ <%== message %>
14
14
  </p>
15
15
  </div>
16
16
  <div class="ml-4 flex-shrink-0 flex">
@@ -12,18 +12,18 @@ class Avo::AlertComponent < ViewComponent::Base
12
12
  end
13
13
 
14
14
  def icon
15
- return 'x-circle' if is_error?
15
+ return "x-circle" if is_error?
16
16
 
17
- 'check-circle'
17
+ "check-circle"
18
18
  end
19
19
 
20
20
  def classes
21
- result = "max-w-sm w-full shadow-lg rounded px-4 py-3 rounded relative border text-white pointer-events-auto"
21
+ result = "max-w-lg w-full shadow-lg rounded px-4 py-3 rounded relative border text-white pointer-events-auto"
22
22
 
23
- if is_error?
24
- result += " bg-red-400 border-red-700"
23
+ result += if is_error?
24
+ " bg-red-400 border-red-700"
25
25
  else
26
- result += " bg-green-400 border-green-700"
26
+ " bg-green-400 border-green-700"
27
27
  end
28
28
 
29
29
  result
File without changes
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::BlankFieldComponent < ViewComponent::Base
4
+ end
@@ -1,4 +1,9 @@
1
- <div class="relative flex-1 flex flex-col justify-between h-full" data-controller="dashboard-card" data-dashboard-card-target="card" data-refresh-every="<%= @card.refresh_every %>" data-card-id="<%= @card.id %>">
1
+ <div class="relative flex-1 flex flex-col justify-between h-full"
2
+ data-controller="dashboard-card"
3
+ data-dashboard-card-target="card"
4
+ data-refresh-every="<%= @card.refresh_every %>"
5
+ data-card-id="<%= @card.id %>"
6
+ data-card-index="<%= @card.index %>">
2
7
  <% if @card.class.display_header %>
3
8
  <div class="px-4 pt-4">
4
9
  <div class="flex justify-between items-center min-h-6">
@@ -7,7 +12,7 @@
7
12
  </div>
8
13
  <div data-controller="select">
9
14
  <% if @card.type == :metric && @card.parsed_ranges.present? %>
10
- <%= select_tag "#{@card.id}_range", options_for_select(@card.parsed_ranges, @card.range),
15
+ <%= select_tag "#{@card.id}_#{@card.index}_range", options_for_select(@card.parsed_ranges, @card.range),
11
16
  class: 'appearance-none inline-flex bg-blue-gray-100 disabled:bg-blue-gray-300 disabled:cursor-not-allowed focus:bg-white text-sm text-blue-gray-700 disabled:text-blue-gray-700 leading-none rounded-md py-px px-2 leading-tight border outline-none outline w-24',
12
17
  data: {
13
18
  target: 'select',
@@ -1,3 +1,3 @@
1
- <%= index_field_wrapper field: @field do %>
1
+ <%= index_field_wrapper field: @field, flush: true do %>
2
2
  <%= render Avo::Fields::Common::BadgeViewerComponent.new value: @field.value, options: @field.options %>
3
3
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= index_field_wrapper field: @field, dash_if_blank: false, center_content: true do %>
1
+ <%= index_field_wrapper field: @field, dash_if_blank: false, center_content: true, flush: true do %>
2
2
  <%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
3
3
  <% end %>
@@ -1,8 +1,7 @@
1
- <%= index_field_wrapper field: @field do %>
1
+ <%= index_field_wrapper field: @field, flush: true do %>
2
2
  <% if @field.value.present? %>
3
3
  <%= image_tag @field.value,
4
4
  height: @field.height,
5
- width: @field.width,
6
5
  style: "border-radius: #{@field.radius}px; max-height: #{@field.height}#{@field.height.to_s&.ends_with?('px') ? '' : 'px'};"
7
6
  %>
8
7
  <% end %>
@@ -1,9 +1,9 @@
1
- <%= index_field_wrapper field: @field do %>
1
+ <%= index_field_wrapper field: @field, flush: flush? do %>
2
2
  <% if @field.value.present? %>
3
3
  <% if @field.value.attached? && @field.value.representable? && @field.is_image %>
4
- <%= link_to_if @field.link_to_resource, image_tag(helpers.main_app.url_for(@field.value), class: 'max-h-full'), resource_path, class: 'block' %>
4
+ <%= link_to_if @field.link_to_resource, image_tag(helpers.main_app.url_for(@field.value), class: 'h-10'), resource_path, class: 'block' %>
5
5
  <% elsif @field.value.attached? && @field.is_audio %>
6
- <%= link_to_if @field.link_to_resource, audio_tag(helpers.main_app.url_for(@field.value), controls: true, preload: false, class: 'max-h-full'), resource_path, class: 'block h-8' %>
6
+ <%= link_to_if @field.link_to_resource, audio_tag(helpers.main_app.url_for(@field.value), controls: true, preload: false, class: 'max-h-full h-10'), resource_path, class: 'block h-8' %>
7
7
  <% else %>
8
8
  <%= @field.value.filename %>
9
9
  <% end %>
@@ -1,4 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::Fields::FileField::IndexComponent < Avo::Fields::IndexComponent
4
+ def flush?
5
+ has_image_tag? || has_audio_tag?
6
+ end
7
+
8
+ def has_image_tag?
9
+ @field.value.attached? && @field.value.representable? && @field.is_image
10
+ end
11
+
12
+ def has_audio_tag?
13
+ @field.value.attached? && @field.is_audio
14
+ end
4
15
  end
@@ -1,4 +1,4 @@
1
- <%= index_field_wrapper field: @field do %>
1
+ <%= index_field_wrapper field: @field, flush: true do %>
2
2
  <%= render Avo::Fields::Common::GravatarViewerComponent.new(
3
3
  md5: @field.md5,
4
4
  default: @field.default,
@@ -1,4 +1,4 @@
1
- <%= index_field_wrapper field: @field do %>
1
+ <%= index_field_wrapper field: @field, flush: true do %>
2
2
  <% if @field.display_value %>
3
3
  <div class="text-center text-sm font-semibold w-full leading-none mb-1">
4
4
  <%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
@@ -1,4 +1,4 @@
1
- <td class="min-h-[3rem] px-4 py-3 leading-tight whitespace-nowrap h-full text-slate-800 <%= @classes %>" data-field-id="<%= @field.id %>" data-field-type="<%= @field.type %>">
1
+ <td class="min-h-[3rem] px-3 leading-tight whitespace-nowrap h-full text-slate-800 <%= classes %>" data-field-id="<%= @field.id %>" data-field-type="<%= @field.type %>">
2
2
  <% if @field.value.blank? && @dash_if_blank %>
3
3
 
4
4
  <% else %>
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::Index::FieldWrapperComponent < ViewComponent::Base
4
- def initialize(field: nil, dash_if_blank: true, center_content: false, **args)
4
+ def initialize(field: nil, dash_if_blank: true, center_content: false, flush: false, **args)
5
5
  @field = field
6
6
  @dash_if_blank = dash_if_blank
7
7
  @center_content = center_content
8
8
  @classes = args[:class].present? ? args[:class] : ""
9
9
  @args = args
10
+ @flush = flush
11
+ end
12
+
13
+ def classes
14
+ result = @classes
15
+
16
+ unless @flush
17
+ result += " py-3"
18
+ end
19
+
20
+ result
10
21
  end
11
22
  end
@@ -28,7 +28,10 @@
28
28
  <% end %>
29
29
 
30
30
  <% if can_detach? %>
31
- <%= form_with url: helpers.resource_detach_path(params[:resource_name], params[:id], params[:related_name], @resource.model.id), method: :delete, html: {
31
+ <%= form_with url: helpers.resource_detach_path(params[:resource_name], params[:id], params[:related_name], @resource.model.id),
32
+ method: :delete,
33
+ local: true,
34
+ html: {
32
35
  'data-turbo-frame': params[:turbo_frame]
33
36
  } do |form| %>
34
37
  <%= form.button helpers.svg('detach', class: button_classes),
@@ -51,6 +54,7 @@
51
54
  <%= form_with url: helpers.resource_path(model: @resource.model, resource: @resource),
52
55
  method: :delete,
53
56
  class: 'flex items-center',
57
+ local: true,
54
58
  html: {
55
59
  'data-turbo-frame': params[:turbo_frame]
56
60
  } do |form| %>
@@ -3,7 +3,7 @@
3
3
  <%= render partial: 'avo/partials/table_header', locals: {fields: @resource.get_fields(reflection: @reflection)} %>
4
4
  <tbody class="divide-y">
5
5
  <% @resources.each_with_index do |resource, index| %>
6
- <% cache_if Avo.configuration.cache_resources_on_index_view, resource.cache_hash(@parent_model) do %>
6
+ <% cache_if Avo.configuration.cache_resources_on_index_view, resource.cache_hash(@parent_model), expires_in: 1.day do %>
7
7
  <%= render Avo::Index::TableRowComponent.new(resource: resource, reflection: @reflection, parent_model: @parent_model) %>
8
8
  <% end %>
9
9
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <div <%== data_attributes %>>
2
2
  <% if render_header? %>
3
- <div class="bg-white rounded shadow p-4 flex-1 flex flex-col xl:flex-row justify-between mb-6">
3
+ <div class="<%= white_panel_classes %> p-4 flex-1 flex flex-col xl:flex-row justify-between mb-6">
4
4
  <div class="overflow-hidden mr-4">
5
5
  <% if display_breadcrumbs? %>
6
6
  <div class="breadcrumbs truncate mb-2">
@@ -19,13 +19,15 @@
19
19
  <% end %>
20
20
  </div>
21
21
 
22
- <div class="flex-1 w-full flex flex-col sm:flex-row xl:justify-end sm:items-end space-y-2 sm:space-y-0 sm:space-x-2 mt-4 xl:mt-0">
23
- <%= tools %>
24
- </div>
22
+ <% if tools.present? %>
23
+ <div class="flex-1 w-full flex flex-col sm:flex-row xl:justify-end sm:items-end space-y-2 sm:space-y-0 sm:space-x-2 mt-4 xl:mt-0">
24
+ <%= tools %>
25
+ </div>
26
+ <% end %>
25
27
  </div>
26
28
  <% end %>
27
29
 
28
- <div class="relative bg-white rounded shadow <%= @body_classes %>">
30
+ <div class="relative <%= white_panel_classes %> <%= @body_classes %>">
29
31
  <%= body %>
30
32
  </div>
31
33
 
@@ -33,6 +35,14 @@
33
35
  <%= bare_content %>
34
36
  </div>
35
37
 
38
+ <% if footer_tools.present? %>
39
+ <div class="<%= white_panel_classes %> p-4 flex-1 flex flex-col xl:flex-row justify-between mt-6">
40
+ <div class="flex-1 w-full flex flex-col sm:flex-row xl:justify-end sm:items-end space-y-2 sm:space-y-0 sm:space-x-2 mt-4 xl:mt-0">
41
+ <%= footer_tools %>
42
+ </div>
43
+ </div>
44
+ <% end %>
45
+
36
46
  <div class="flex justify-end w-full">
37
47
  <div>
38
48
  <%= footer %>
@@ -6,6 +6,7 @@ class Avo::PanelComponent < ViewComponent::Base
6
6
  renders_one :tools
7
7
  renders_one :body
8
8
  renders_one :bare_content
9
+ renders_one :footer_tools
9
10
  renders_one :footer
10
11
 
11
12
  def initialize(title: nil, description: nil, body_classes: nil, data: {}, display_breadcrumbs: false, index: nil)
@@ -19,6 +20,10 @@ class Avo::PanelComponent < ViewComponent::Base
19
20
 
20
21
  private
21
22
 
23
+ def white_panel_classes
24
+ 'bg-white rounded shadow'
25
+ end
26
+
22
27
  def data_attributes
23
28
  @data.merge({'panel-index': @index}).map do |key, value|
24
29
  " data-#{key}=\"#{value}\""
@@ -38,4 +43,8 @@ class Avo::PanelComponent < ViewComponent::Base
38
43
  def render_header?
39
44
  @title.present? || description.present? || tools.present? || display_breadcrumbs?
40
45
  end
46
+
47
+ def render_footer_tools?
48
+ footer_tools.present?
49
+ end
41
50
  end
@@ -0,0 +1,4 @@
1
+ <%= hidden_field_tag :via_resource_class, params[:via_resource_class] if params[:via_resource_class] %>
2
+ <%= hidden_field_tag :via_resource_id, params[:via_resource_id] if params[:via_resource_id] %>
3
+ <%= hidden_field_tag :via_relation, params[:via_relation] if params[:via_relation] %>
4
+ <%= hidden_field_tag :referrer, back_path if params[:via_resource_class] %>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::ReferrerParamsComponent < ViewComponent::Base
4
+ attr_reader :back_path
5
+
6
+ def initialize(back_path: nil)
7
+ @back_path = back_path
8
+ end
9
+ end
@@ -43,6 +43,8 @@
43
43
  </div>
44
44
  </div>
45
45
  <% end %>
46
+
47
+ <%= render partial: "/avo/partials/sidebar_extra" %>
46
48
  </div>
47
49
  </div>
48
50
  <%= helpers.render_license_warnings %>
@@ -4,7 +4,9 @@ class Avo::SidebarComponent < ViewComponent::Base
4
4
  def dashboards
5
5
  return [] if Avo::App.license.lacks_with_trial(:dashboards)
6
6
 
7
- Avo::App.get_dashboards(helpers._current_user)
7
+ Avo::App.get_dashboards(helpers._current_user).select do |dashboard|
8
+ dashboard.is_visible?
9
+ end
8
10
  end
9
11
 
10
12
  def resources
@@ -4,8 +4,9 @@
4
4
  scope: @resource.form_scope,
5
5
  url: helpers.resource_path(model: @resource.model, resource: @resource),
6
6
  method: :put,
7
+ local: true,
7
8
  multipart: true do |form| %>
8
- <%= hidden_field_tag :referrer, back_path if params[:via_resource_class] %>
9
+ <%= render Avo::ReferrerParamsComponent.new back_path: back_path %>
9
10
  <%= render Avo::PanelComponent.new(title: resource_panel[:name], description: @resource.resource_description, display_breadcrumbs: true) do |c| %>
10
11
  <% c.tools do %>
11
12
  <%= a_link back_path, icon: 'arrow-left' do %>
@@ -17,6 +18,18 @@
17
18
  <% end %>
18
19
  <% end %>
19
20
  <% end %>
21
+ <% if Avo.configuration.buttons_on_form_footers %>
22
+ <% c.footer_tools do %>
23
+ <%= a_link back_path, icon: 'arrow-left' do %>
24
+ <%= t('avo.cancel').capitalize %>
25
+ <% end %>
26
+ <% if can_see_the_save_button? %>
27
+ <%= a_button color: :green, spinner: true, type: :submit, icon: 'save' do %>
28
+ <%= t('avo.save').capitalize %>
29
+ <% end %>
30
+ <% end %>
31
+ <% end %>
32
+ <% end %>
20
33
  <% c.body do %>
21
34
  <div class="divide-y">
22
35
  <% @resource.get_fields.each_with_index do |field, index| %>
@@ -10,6 +10,7 @@
10
10
  ),
11
11
  local: true,
12
12
  multipart: true do |form| %>
13
+ <%= render Avo::ReferrerParamsComponent.new back_path: back_path %>
13
14
  <%= render Avo::PanelComponent.new(title: resource_panel[:name], description: @resource.resource_description, display_breadcrumbs: true) do |c| %>
14
15
  <% c.tools do %>
15
16
  <div class="flex justify-end space-x-2">
@@ -23,6 +24,18 @@
23
24
  <% end %>
24
25
  </div>
25
26
  <% end %>
27
+ <% if Avo.configuration.buttons_on_form_footers %>
28
+ <% c.footer_tools do %>
29
+ <%= a_link back_path, icon: 'arrow-left' do %>
30
+ <%= t('avo.cancel').capitalize %>
31
+ <% end %>
32
+ <% if can_see_the_save_button? %>
33
+ <%= a_button color: :green, spinner: true, type: :submit, icon: 'save' do %>
34
+ <%= t('avo.save').capitalize %>
35
+ <% end %>
36
+ <% end %>
37
+ <% end %>
38
+ <% end %>
26
39
  <% c.body do %>
27
40
  <div class="divide-y">
28
41
  <% @resource.get_fields.each_with_index do |field, index| %>
@@ -33,6 +33,7 @@
33
33
  <% if can_see_the_destroy_button? %>
34
34
  <%= a_button url: helpers.resource_path(model: @resource.model, resource: @resource),
35
35
  method: :delete,
36
+ local: true,
36
37
  title: t('avo.delete_item', item: @resource.model.model_name.name.downcase).capitalize,
37
38
  spinner: true,
38
39
  color: :red,
@@ -83,4 +84,14 @@
83
84
  <% end %>
84
85
  <% end %>
85
86
  <% end %>
87
+
88
+ <% if should_display_invalid_fields_errors? %>
89
+ <turbo-stream action="append" target="alerts">
90
+ <template>
91
+ <% @resource.invalid_fields.each do |error| %>
92
+ <%= render Avo::AlertComponent.new :error, error[:message] %>
93
+ <% end %>
94
+ </template>
95
+ </turbo-stream>
96
+ <% end %>
86
97
  </div>
@@ -90,4 +90,9 @@ class Avo::Views::ResourceShowComponent < Avo::ResourceComponent
90
90
  end
91
91
  end
92
92
  end
93
+
94
+ # In development and test environments we shoudl show the invalid field errors
95
+ def should_display_invalid_fields_errors?
96
+ (Rails.env.development? || Rails.env.test?) && @resource.invalid_fields.present?
97
+ end
93
98
  end
@@ -28,7 +28,7 @@ module Avo
28
28
  add_flash_types :info, :warning, :success, :error
29
29
 
30
30
  def init_app
31
- Avo::App.init request: request, context: context, root_path: avo.root_path.delete_suffix("/"), current_user: _current_user, view_context: view_context
31
+ Avo::App.init request: request, context: context, root_path: avo.root_path.delete_suffix("/"), current_user: _current_user, view_context: view_context, params: params
32
32
 
33
33
  @license = Avo::App.license
34
34
  end
@@ -7,6 +7,7 @@ module Avo
7
7
  before_action :hydrate_resource
8
8
  before_action :set_model, only: [:show, :edit, :destroy, :update, :order]
9
9
  before_action :set_model_to_fill
10
+ before_action :set_edit_title_and_breadcrumbs, only: [:edit, :update]
10
11
  before_action :fill_model, only: [:create, :update]
11
12
  before_action :authorize_action
12
13
  before_action :reset_pagination_if_filters_changed, only: :index
@@ -43,7 +44,7 @@ module Avo
43
44
  unless @index_params[:sort_by].eql? :created_at
44
45
  @query = @query.unscope(:order)
45
46
  end
46
- @query = @query.order("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}")
47
+ @query = @query.order(Arel.sql("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}"))
47
48
  end
48
49
 
49
50
  # Apply filters
@@ -91,30 +92,9 @@ module Avo
91
92
  add_breadcrumb t("avo.new").humanize
92
93
  end
93
94
 
94
- def edit
95
- @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
96
-
97
- @page_title = @resource.default_panel_name.to_s
98
-
99
- # If we're accessing this resource via another resource add the parent to the breadcrumbs.
100
- if params[:via_resource_class].present? && params[:via_resource_id].present?
101
- via_resource = Avo::App.get_resource_by_model_name params[:via_resource_class]
102
- via_model = via_resource.class.find_scope.find params[:via_resource_id]
103
- via_resource.hydrate model: via_model
104
-
105
- add_breadcrumb via_resource.plural_name, resources_path(resource: @resource)
106
- add_breadcrumb via_resource.model_title, resource_path(model: via_model, resource: via_resource)
107
- else
108
- add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
109
- end
110
-
111
- add_breadcrumb @resource.model_title, resource_path(model: @resource.model, resource: @resource)
112
- add_breadcrumb t("avo.edit").humanize
113
- end
114
-
115
95
  def create
116
96
  # model gets instantiated and filled in the fill_model method
117
- saved = @model.save
97
+ saved = save_model
118
98
  @resource.hydrate(model: @model, view: :new, user: _current_user)
119
99
 
120
100
  # This means that the record has been created through another parent record and we need to attach it somehow.
@@ -148,14 +128,7 @@ module Avo
148
128
 
149
129
  respond_to do |format|
150
130
  if saved
151
- redirect_path = resource_path(model: @model, resource: @resource)
152
-
153
- if params[:via_relation_class].present? && params[:via_resource_id].present?
154
- parent_resource = ::Avo::App.get_resource_by_model_name params[:via_relation_class].safe_constantize
155
- redirect_path = resource_path(model: params[:via_relation_class].safe_constantize, resource: parent_resource, resource_id: params[:via_resource_id])
156
- end
157
-
158
- format.html { redirect_to redirect_path, notice: "#{@model.class.name} #{t("avo.was_successfully_created")}." }
131
+ format.html { redirect_to after_create_path, notice: "#{@model.class.name} #{t("avo.was_successfully_created")}." }
159
132
  else
160
133
  flash.now[:error] = t "avo.you_missed_something_check_form"
161
134
  format.html { render :new, status: :unprocessable_entity }
@@ -163,14 +136,17 @@ module Avo
163
136
  end
164
137
  end
165
138
 
139
+ def edit
140
+ end
141
+
166
142
  def update
167
143
  # model gets instantiated and filled in the fill_model method
168
- saved = @model.save
144
+ saved = save_model
169
145
  @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
170
146
 
171
147
  respond_to do |format|
172
148
  if saved
173
- format.html { redirect_to params[:referrer] || resource_path(model: @model, resource: @resource), notice: "#{@model.class.name} #{t("avo.was_successfully_updated")}." }
149
+ format.html { redirect_to after_update_path, notice: "#{@model.class.name} #{t("avo.was_successfully_updated")}." }
174
150
  else
175
151
  flash.now[:error] = t "avo.you_missed_something_check_form"
176
152
  format.html { render :edit, status: :unprocessable_entity }
@@ -179,11 +155,14 @@ module Avo
179
155
  end
180
156
 
181
157
  def destroy
182
- @model.destroy!
183
-
184
158
  respond_to do |format|
185
- format.html { redirect_to params[:referrer] || resources_path(resource: @resource, turbo_frame: params[:turbo_frame], view_type: params[:view_type]), notice: t("avo.resource_destroyed", attachment_class: @attachment_class) }
186
- format.json { head :no_content }
159
+ if destroy_model
160
+ format.html { redirect_to params[:referrer] || resources_path(resource: @resource, turbo_frame: params[:turbo_frame], view_type: params[:view_type]), notice: t("avo.resource_destroyed", attachment_class: @attachment_class) }
161
+ else
162
+ error_message = @errors.present? ? @errors.first : t("avo.failed")
163
+
164
+ format.html { redirect_back fallback_location: params[:referrer] || resources_path(resource: @resource, turbo_frame: params[:turbo_frame], view_type: params[:view_type]), error: error_message }
165
+ end
187
166
  end
188
167
  end
189
168
 
@@ -204,6 +183,31 @@ module Avo
204
183
 
205
184
  private
206
185
 
186
+ def save_model
187
+ perform_action_and_record_errors do
188
+ @model.save!
189
+ end
190
+ end
191
+
192
+ def destroy_model
193
+ perform_action_and_record_errors do
194
+ @model.destroy!
195
+ end
196
+ end
197
+
198
+ def perform_action_and_record_errors(&block)
199
+ begin
200
+ succeeded = block.call
201
+ rescue => exception
202
+ # In case there's an error somewhere else than the model
203
+ # Example: When you save a license that should create a user for it and creating that user throws and error.
204
+ # Example: When you Try to delete a record and has a foreign key constraint.
205
+ @errors = Array.wrap(exception.message)
206
+ end
207
+
208
+ succeeded
209
+ end
210
+
207
211
  def model_params
208
212
  request_params = params.require(model_param_key).permit(permitted_params)
209
213
 
@@ -327,5 +331,51 @@ module Avo
327
331
  def applied_filters_cache_key
328
332
  "avo.base_controller.#{@resource.model_key}.applied_filters"
329
333
  end
334
+
335
+ def set_edit_title_and_breadcrumbs
336
+ @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
337
+ @page_title = @resource.default_panel_name.to_s
338
+ # If we're accessing this resource via another resource add the parent to the breadcrumbs.
339
+ if params[:via_resource_class].present? && params[:via_resource_id].present?
340
+ via_resource = Avo::App.get_resource_by_model_name params[:via_resource_class]
341
+ via_model = via_resource.class.find_scope.find params[:via_resource_id]
342
+ via_resource.hydrate model: via_model
343
+
344
+ add_breadcrumb via_resource.plural_name, resources_path(resource: @resource)
345
+ add_breadcrumb via_resource.model_title, resource_path(model: via_model, resource: via_resource)
346
+ else
347
+ add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
348
+ end
349
+
350
+ add_breadcrumb @resource.model_title, resource_path(model: @resource.model, resource: @resource)
351
+ add_breadcrumb t("avo.edit").humanize
352
+ end
353
+
354
+ def after_create_path
355
+ # If this is an associated record return to the association show page
356
+ if params[:via_relation_class].present? && params[:via_resource_id].present?
357
+ parent_resource = ::Avo::App.get_resource_by_model_name params[:via_relation_class].safe_constantize
358
+
359
+ return resource_path(model: params[:via_relation_class].safe_constantize, resource: parent_resource, resource_id: params[:via_resource_id])
360
+ end
361
+
362
+ redirect_path_from_resource_option || resource_path(model: @model, resource: @resource)
363
+ end
364
+
365
+ def after_update_path
366
+ return params[:referrer] if params[:referrer].present?
367
+
368
+ redirect_path_from_resource_option || resource_path(model: @model, resource: @resource)
369
+ end
370
+
371
+ def redirect_path_from_resource_option
372
+ return nil if @resource.class.after_update_path.blank?
373
+
374
+ if @resource.class.after_create_path == :index
375
+ resources_path(resource: @resource)
376
+ else
377
+ resource_path(model: @model, resource: @resource)
378
+ end
379
+ end
330
380
  end
331
381
  end
@@ -8,11 +8,7 @@ module Avo
8
8
  end
9
9
 
10
10
  def card
11
- @card = @dashboard.items.find do |item|
12
- next unless item.is_card?
13
-
14
- item.id.to_s == params[:card_id]
15
- end.tap do |card|
11
+ @card = @dashboard.item_at_index(params[:index].to_i).tap do |card|
16
12
  card.hydrate(dashboard: @dashboard, params: params)
17
13
  end
18
14
  end
@@ -22,7 +18,7 @@ module Avo
22
18
  def set_dashboard
23
19
  @dashboard = Avo::App.get_dashboard_by_id params[:dashboard_id]
24
20
 
25
- raise ActionController::RoutingError.new("Not Found") if @dashboard.nil?
21
+ raise ActionController::RoutingError.new("Not Found") if @dashboard.nil? || @dashboard.is_hidden?
26
22
  end
27
23
  end
28
24
  end