avo 2.9.2.pre1 → 2.10.0

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -7
  3. data/README.md +4 -0
  4. data/app/assets/stylesheets/css/buttons.css +4 -1
  5. data/app/components/avo/actions_component.rb +6 -2
  6. data/app/components/avo/base_component.rb +2 -0
  7. data/app/components/avo/button_component.rb +3 -1
  8. data/app/components/avo/fields/common/key_value_component.html.erb +2 -2
  9. data/app/components/avo/fields/common/single_file_viewer_component.rb +1 -1
  10. data/app/components/avo/fields/date_field/edit_component.html.erb +1 -0
  11. data/app/components/avo/fields/date_time_field/edit_component.html.erb +10 -25
  12. data/app/components/avo/fields/date_time_field/index_component.html.erb +1 -9
  13. data/app/components/avo/fields/date_time_field/show_component.html.erb +1 -9
  14. data/app/components/avo/fields/edit_component.rb +5 -0
  15. data/app/components/avo/fields/show_component.rb +1 -1
  16. data/app/components/avo/index/ordering/button_component.rb +2 -12
  17. data/app/components/avo/index/resource_controls_component.html.erb +2 -2
  18. data/app/components/avo/index/resource_controls_component.rb +5 -1
  19. data/app/components/avo/index/resource_table_component.html.erb +1 -1
  20. data/app/components/avo/index/table_row_component.html.erb +1 -1
  21. data/app/components/avo/item_switcher_component.html.erb +19 -0
  22. data/app/components/avo/item_switcher_component.rb +45 -0
  23. data/app/components/avo/panel_component.html.erb +23 -24
  24. data/app/components/avo/panel_component.rb +8 -5
  25. data/app/components/avo/tab_group_component.html.erb +53 -0
  26. data/app/components/avo/tab_group_component.rb +51 -0
  27. data/app/components/avo/tab_switcher_component.html.erb +21 -0
  28. data/app/components/avo/tab_switcher_component.rb +86 -0
  29. data/app/components/avo/views/resource_edit_component.html.erb +34 -56
  30. data/app/components/avo/views/resource_edit_component.rb +11 -1
  31. data/app/components/avo/views/resource_index_component.html.erb +1 -1
  32. data/app/components/avo/views/resource_index_component.rb +3 -3
  33. data/app/components/avo/views/resource_show_component.html.erb +58 -89
  34. data/app/components/avo/views/resource_show_component.rb +2 -2
  35. data/app/controllers/avo/actions_controller.rb +1 -1
  36. data/app/controllers/avo/application_controller.rb +20 -3
  37. data/app/helpers/avo/application_helper.rb +0 -6
  38. data/app/helpers/avo/url_helpers.rb +1 -1
  39. data/app/javascript/avo.js +5 -1
  40. data/app/javascript/js/controllers/fields/date_field_controller.js +25 -87
  41. data/app/javascript/js/controllers/loading_button_controller.js +25 -21
  42. data/app/javascript/js/controllers/tabs_controller.js +86 -0
  43. data/app/javascript/js/controllers.js +2 -0
  44. data/app/views/avo/base/index.html.erb +1 -1
  45. data/app/views/avo/base/show.html.erb +1 -1
  46. data/app/views/avo/cards/show.html.erb +1 -1
  47. data/app/views/avo/debug/index.html.erb +1 -1
  48. data/app/views/avo/home/_actions.html.erb +1 -1
  49. data/app/views/avo/home/_dashboards.html.erb +19 -0
  50. data/app/views/avo/home/_filters.html.erb +1 -1
  51. data/app/views/avo/home/_resources.html.erb +1 -1
  52. data/app/views/avo/home/failed_to_load.html.erb +1 -1
  53. data/app/views/avo/home/index.html.erb +14 -2
  54. data/app/views/avo/partials/_javascript.html.erb +1 -1
  55. data/app/views/avo/partials/_tabs_toggle.html.erb +20 -0
  56. data/app/views/avo/private/design.html.erb +1 -1
  57. data/config/routes.rb +1 -1
  58. data/db/migrate/20210421064037_add_color_to_teams.rb +5 -0
  59. data/db/migrate/20210423075924_add_progress_to_projects.rb +5 -0
  60. data/db/migrate/20210525143134_add_slug_to_users.rb +6 -0
  61. data/lib/avo/app.rb +11 -4
  62. data/lib/avo/base_action.rb +2 -19
  63. data/lib/avo/base_card.rb +1 -7
  64. data/lib/avo/base_resource.rb +1 -95
  65. data/lib/avo/base_resource_tool.rb +3 -1
  66. data/lib/avo/concerns/handles_field_args.rb +1 -1
  67. data/lib/avo/concerns/has_fields.rb +247 -50
  68. data/lib/avo/concerns/has_html_attributes.rb +1 -1
  69. data/lib/avo/concerns/is_resource_item.rb +36 -0
  70. data/lib/avo/concerns/model_class_constantized.rb +23 -0
  71. data/lib/avo/dashboards/base_dashboard.rb +1 -1
  72. data/lib/avo/dsl/field_parser.rb +83 -0
  73. data/lib/avo/fields/base_field.rb +19 -2
  74. data/lib/avo/fields/date_field.rb +2 -0
  75. data/lib/avo/fields/date_time_field.rb +9 -21
  76. data/lib/avo/fields/field_extensions/visible_in_different_views.rb +18 -1
  77. data/lib/avo/fields/has_base_field.rb +20 -1
  78. data/lib/avo/fields/has_one_field.rb +4 -1
  79. data/lib/avo/grid_collector.rb +6 -3
  80. data/lib/avo/items_holder.rb +68 -0
  81. data/lib/avo/licensing/h_q.rb +10 -0
  82. data/lib/avo/main_panel.rb +3 -0
  83. data/lib/avo/menu/builder.rb +8 -7
  84. data/lib/avo/panel.rb +25 -0
  85. data/lib/avo/panel_builder.rb +23 -0
  86. data/lib/avo/services/uri_service.rb +71 -0
  87. data/lib/avo/tab.rb +78 -0
  88. data/lib/avo/tab_builder.rb +25 -0
  89. data/lib/avo/tab_group.rb +40 -0
  90. data/lib/avo/tab_group_builder.rb +43 -0
  91. data/lib/avo/version.rb +1 -1
  92. data/lib/avo.rb +1 -0
  93. data/lib/generators/avo/templates/locales/avo.fr.yml +115 -0
  94. data/lib/generators/avo/templates/resource/controller.tt +2 -0
  95. data/lib/generators/avo/templates/resource_tools/partial.tt +1 -1
  96. data/lib/generators/avo/templates/tool/view.tt +1 -1
  97. data/public/avo-assets/avo.css +27 -3
  98. data/public/avo-assets/avo.js +77 -77
  99. data/public/avo-assets/avo.js.map +3 -3
  100. metadata +28 -13
  101. data/app/assets/builds/action_cable.js +0 -2
  102. data/app/assets/builds/action_cable.js.map +0 -7
  103. data/app/assets/builds/application.js +0 -2
  104. data/app/assets/builds/application.js.map +0 -7
  105. data/app/assets/builds/avo.css +0 -9028
  106. data/app/assets/builds/avo.js +0 -512
  107. data/app/assets/builds/avo.js.map +0 -7
  108. data/app/assets/builds/avo_custom.js +0 -6
  109. data/app/assets/builds/avo_custom.js.map +0 -7
  110. data/lib/avo/concerns/has_tools.rb +0 -47
@@ -2,125 +2,63 @@ import { Controller } from '@hotwired/stimulus'
2
2
  import { DateTime } from 'luxon'
3
3
  import flatpickr from 'flatpickr'
4
4
 
5
+ import { castBoolean } from '../../helpers/cast_boolean'
6
+
5
7
  // Get the DateTime with the TZ offset applied.
6
8
  function universalTimestamp(timestampStr) {
7
9
  return new Date(new Date(timestampStr).getTime() + (new Date(timestampStr).getTimezoneOffset() * 60 * 1000))
8
10
  }
9
11
 
10
12
  export default class extends Controller {
11
- static targets = ['input', 'fakeInput']
12
-
13
- static values = {
14
- view: String,
15
- timezone: String,
16
- format: String,
17
- enableTime: Boolean,
18
- pickerFormat: String,
19
- firstDayOfWeek: Number,
20
- time24Hr: Boolean,
21
- }
22
-
23
- get browserZone() {
24
- const time = DateTime.local()
25
-
26
- return time.zoneName
27
- }
28
-
29
- get initialValue() {
30
- if (this.isOnShow || this.isOnIndex) {
31
- return this.context.element.innerText
32
- } if (this.isOnEdit) {
33
- return this.inputTarget.value
34
- }
35
-
36
- return null
37
- }
38
-
39
- get isOnIndex() {
40
- return this.viewValue === 'index'
41
- }
42
-
43
- get isOnEdit() {
44
- return this.viewValue === 'edit'
45
- }
46
-
47
- get isOnShow() {
48
- return this.viewValue === 'show'
49
- }
50
-
51
- // Parse the time as if it were UTC
52
- get parsedValue() {
53
- return DateTime.fromISO(this.initialValue, { zone: 'UTC' })
54
- }
55
-
56
- get displayTimezone() {
57
- return this.timezoneValue || this.browserZone
58
- }
13
+ static targets = ['input']
59
14
 
60
15
  connect() {
61
- if (this.isOnShow || this.isOnIndex) {
62
- this.initShow()
63
- } else if (this.isOnEdit) {
64
- this.initEdit()
65
- }
66
- }
67
-
68
- // Turns the value in the controller wrapper into the timezone of the browser
69
- initShow() {
70
- this.context.element.innerText = this.parsedValue.setZone(this.displayTimezone).toFormat(this.formatValue)
71
- }
72
-
73
- initEdit() {
74
16
  const options = {
75
17
  enableTime: false,
76
18
  enableSeconds: false,
77
19
  // eslint-disable-next-line camelcase
78
- time_24hr: this.time24HrValue,
20
+ time_24hr: false,
79
21
  locale: {
80
22
  firstDayOfWeek: 0,
81
23
  },
82
24
  altInput: true,
83
- onChange: this.onChange.bind(this),
84
25
  }
26
+ const enableTime = castBoolean(this.inputTarget.dataset.enableTime)
85
27
 
86
28
  // Set the format of the displayed input field.
87
- options.altFormat = this.pickerFormatValue
29
+ options.altFormat = this.inputTarget.dataset.pickerFormat
30
+
31
+ // Disable native input in mobile browsers
32
+ options.disableMobile = this.inputTarget.dataset.disableMobile
88
33
 
89
34
  // Set first day of the week.
90
- options.locale.firstDayOfWeek = this.firstDayOfWeekValue
35
+ options.locale.firstDayOfWeek = this.inputTarget.dataset.firstDayOfWeek
91
36
 
92
37
  // Enable time if needed.
93
- options.enableTime = this.enableTimeValue
94
- options.enableSeconds = this.enableTimeValue
38
+ options.enableTime = enableTime
39
+ options.enableSeconds = enableTime
40
+
41
+ let currentValue
95
42
 
96
43
  // enable timezone display
97
- if (this.enableTimeValue) {
98
- options.defaultDate = this.parsedValue.setZone(this.displayTimezone).toISO()
44
+ if (enableTime) {
45
+ currentValue = DateTime.fromISO(this.inputTarget.value, { zone: window.Avo.configuration.timezone })
46
+ currentValue = currentValue.setZone(this.inputTarget.dataset.timezone)
47
+ currentValue = currentValue.toISO()
99
48
 
100
49
  options.dateFormat = 'Y-m-d H:i:S'
50
+ // eslint-disable-next-line camelcase
51
+ options.time_24hr = castBoolean(this.inputTarget.dataset.time24hr)
52
+ // this.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
53
+ options.appTimezone = this.inputTarget.dataset.timezone
101
54
  } else {
102
55
  // Because the browser treats the date like a timestamp and updates it ot 00:00 hour, when on a western timezone the date will be converted with one day offset.
103
56
  // Ex: 2022-01-30 will render as 2022-01-29 on an American timezone
104
- options.defaultDate = universalTimestamp(this.initialValue)
57
+ currentValue = universalTimestamp(this.inputTarget.value)
105
58
  }
106
59
 
107
- flatpickr(this.fakeInputTarget, options)
108
-
109
- this.updateRealInput(this.parsedValue.setZone(this.displayTimezone).toISO())
110
- }
111
-
112
- onChange(selectedDates) {
113
- let time
114
-
115
- if (this.timezoneValue) {
116
- time = DateTime.fromISO(selectedDates[0].toISOString()).setZone('UTC', { keepLocalTime: true })
117
- } else {
118
- time = DateTime.fromISO(selectedDates[0].toISOString()).setZone('UTC', { keepLocalTime: false })
119
- }
120
- this.updateRealInput(time)
121
- }
60
+ options.defaultDate = currentValue
122
61
 
123
- updateRealInput(value) {
124
- this.inputTarget.value = value
62
+ flatpickr(this.inputTarget, options)
125
63
  }
126
64
  }
@@ -7,33 +7,37 @@ export default class extends Controller {
7
7
  <div class="double-bounce2"></div>
8
8
  </div>`;
9
9
 
10
- confirmed = false
11
-
12
- connect() {
13
- this.context.scope.element.addEventListener('click', (e) => {
14
- // If the user has to confirm the action
15
- if (this.confirmationMessage) {
16
- // Intervene only if not confirmed
17
- if (!this.confirmed) {
18
- e.preventDefault()
19
- if (window.confirm(this.confirmationMessage)) {
20
- this.applyLoader()
21
- }
22
- }
23
- } else {
10
+ static values = {
11
+ confirmationMessage: String,
12
+ confirmed: Boolean,
13
+ }
14
+
15
+ attemptSubmit(e) {
16
+ // If the user has to confirm the action
17
+ if (this.confirmationMessageValue) {
18
+ this.confirmAndApply(e)
19
+ } else {
20
+ this.applyLoader()
21
+ }
22
+
23
+ return null
24
+ }
25
+
26
+ confirmAndApply(e) {
27
+ // Intervene only if not confirmed
28
+ if (!this.confirmedValue) {
29
+ e.preventDefault()
30
+
31
+ if (window.confirm(this.confirmationMessageValue)) {
24
32
  this.applyLoader()
25
33
  }
26
- })
34
+ }
27
35
  }
28
36
 
29
37
  get button() {
30
38
  return this.context.scope.element
31
39
  }
32
40
 
33
- get confirmationMessage() {
34
- return this.context.scope.element.getAttribute('data-avo-confirm')
35
- }
36
-
37
41
  applyLoader() {
38
42
  const { button } = this
39
43
 
@@ -50,10 +54,10 @@ export default class extends Controller {
50
54
  }
51
55
 
52
56
  markConfirmed() {
53
- this.confirmed = true
57
+ this.confirmedValue = true
54
58
  }
55
59
 
56
60
  markUnconfirmed() {
57
- this.confirmed = false
61
+ this.confirmedValue = false
58
62
  }
59
63
  }
@@ -0,0 +1,86 @@
1
+ import { AttributeObserver } from '@stimulus/mutation-observers'
2
+ import { Controller } from '@hotwired/stimulus'
3
+ import { castBoolean } from '../helpers/cast_boolean'
4
+
5
+ export default class extends Controller {
6
+ static targets = ['tab'];
7
+
8
+ static values = {
9
+ view: String,
10
+ activeTab: String,
11
+ };
12
+
13
+ get currentTab() {
14
+ return this.tabTargets.find(
15
+ (element) => element.dataset.tabId === this.activeTabValue,
16
+ )
17
+ }
18
+
19
+ targetTab(id) {
20
+ return this.tabTargets.find((element) => element.dataset.tabId === id)
21
+ }
22
+
23
+ changeTab(e) {
24
+ e.preventDefault()
25
+
26
+ const { params } = e
27
+ const { id } = params
28
+
29
+ this.setTheTargetPanelHeight(id)
30
+
31
+ this.hideTabs()
32
+ this.showTab(id)
33
+ this.markTabLoaded(id)
34
+
35
+ this.activeTabValue = id
36
+ }
37
+
38
+ /**
39
+ * Sets the target container height to the previous panel height so we don't get jerky tab changes.
40
+ */
41
+ setTheTargetPanelHeight(id) {
42
+ // Ignore this on edit.
43
+ // All tabs are loaded beforehand, they have their own height, and the page won't jiggle when the user toggles between them.
44
+ if (this.viewValue === 'edit' || this.viewValue === 'new') {
45
+ return
46
+ }
47
+
48
+ // We don't need to add a height to this panel because it was loaded before
49
+ if (castBoolean(this.targetTab(id).dataset.loaded)) {
50
+ return
51
+ }
52
+
53
+ // Get the height of the active panel
54
+ const { height } = this.currentTab.getBoundingClientRect()
55
+ // Set it to the target panel
56
+ this.targetTab(id).style.height = `${height}px`
57
+
58
+ // Wait until the panel loaded it's content and then remove the forced height
59
+ const observer = new AttributeObserver(this.targetTab(id), 'busy', {
60
+ elementUnmatchedAttribute: () => {
61
+ // The content is not available in an instant so delay the height reset a bit.
62
+ setTimeout(() => {
63
+ this.targetTab(id).style.height = ''
64
+ }, 300)
65
+ if (observer) observer.stop()
66
+ },
67
+ })
68
+ observer.start()
69
+ }
70
+
71
+ markTabLoaded(id) {
72
+ this.targetTab(id).dataset.loaded = true
73
+ }
74
+
75
+ showTab(id) {
76
+ this.tabTargets.forEach((element) => {
77
+ if (element.dataset.tabId === id) {
78
+ element.classList.remove('hidden')
79
+ }
80
+ })
81
+ }
82
+
83
+ hideTabs() {
84
+ this.tabTargets.map((element) => element.classList.add('hidden'))
85
+ }
86
+ }
@@ -27,6 +27,7 @@ import SearchController from './controllers/search_controller'
27
27
  import SelectController from './controllers/select_controller'
28
28
  import SelectFilterController from './controllers/select_filter_controller'
29
29
  import SimpleMdeController from './controllers/fields/simple_mde_controller'
30
+ import TabsController from './controllers/tabs_controller'
30
31
  import TagsFieldController from './controllers/fields/tags_field_controller'
31
32
  import TextFilterController from './controllers/text_filter_controller'
32
33
  import TippyController from './controllers/tippy_controller'
@@ -55,6 +56,7 @@ application.register('resource-show', ResourceShowController)
55
56
  application.register('search', SearchController)
56
57
  application.register('select', SelectController)
57
58
  application.register('select-filter', SelectFilterController)
59
+ application.register('tabs', TabsController)
58
60
  application.register('tags-field', TagsFieldController)
59
61
  application.register('text-filter', TextFilterController)
60
62
  application.register('tippy', TippyController)
@@ -1,4 +1,4 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%= render Avo::Views::ResourceIndexComponent.new(
3
3
  resource: @resource,
4
4
  resources: @resources,
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%= render Avo::Views::ResourceShowComponent.new(resource: @resource, reflection: @reflection, actions: @actions) %>
3
3
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%= render Avo::CardComponent.new card: @card %>
3
3
  <% end %>
@@ -14,7 +14,7 @@
14
14
  hq_payload = Avo::Licensing::HQ.new(request).payload
15
15
  %>
16
16
  <div class="flex flex-col">
17
- <%= render Avo::PanelComponent.new title: 'Debug Avo', description: 'Use this page to debug the Avo license.' do |c| %>
17
+ <%= render Avo::PanelComponent.new(title: 'Debug Avo', description: 'Use this page to debug the Avo license.') do |c| %>
18
18
  <% c.tools do %>
19
19
  <% end %>
20
20
  <% c.bare_content do %>
@@ -11,6 +11,6 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <a href="https://docs.avohq.io/1.0/actions.html" target="_blank" title="Avo Actions documentation" class="text-bold cursor-pointer block mt-2">Actions in the docs 👉</a>
14
+ <a href="https://docs.avohq.io/2.0/actions.html" target="_blank" title="Avo Actions documentation" class="text-bold cursor-pointer block mt-2">Actions in the docs 👉</a>
15
15
  </div>
16
16
  </div>
@@ -0,0 +1,19 @@
1
+ <div>
2
+ <h3>Dashboards</h3>
3
+
4
+ <div>There comes the point in your app's life when you need to display the data in an aggregated form like a metric or chart. That's what Avo's Dashboards are all about.</div>
5
+
6
+ <div>Generate a dashboard using the command below 👇👇</div>
7
+
8
+
9
+ <div class="mt-2">
10
+ <div class="mt-1">
11
+ <code class="p-1 rounded bg-sky-500 text-white">bin/rails generate avo:dashboard dashy</code>
12
+ </div>
13
+ </div>
14
+
15
+ <p>
16
+ <a href="https://docs.avohq.io/2.0/dashboards.html" target="_blank" title="Avo Dashboards documentation">Docs</a>
17
+ </p>
18
+ </div>
19
+
@@ -11,6 +11,6 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <a href="https://docs.avohq.io/1.0/filters.html" target="_blank" title="Avo Filters documentation" class="text-bold cursor-pointer block mt-2">Filters in the docs 👉</a>
14
+ <a href="https://docs.avohq.io/2.0/filters.html" target="_blank" title="Avo Filters documentation" class="text-bold cursor-pointer block mt-2">Filters in the docs 👉</a>
15
15
  </div>
16
16
  </div>
@@ -35,7 +35,7 @@
35
35
  <% end %>
36
36
 
37
37
  <p>
38
- <a href="https://docs.avohq.io/1.0/resources.html" target="_blank" title="Avo Resources documentation">Docs</a>
38
+ <a href="https://docs.avohq.io/2.0/resources.html" target="_blank" title="Avo Resources documentation">Docs</a>
39
39
  </p>
40
40
  </div>
41
41
 
@@ -1,4 +1,4 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%
3
3
  classes = 'absolute inset-auto left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2'
4
4
  label = t 'avo.failed_to_load'
@@ -1,5 +1,5 @@
1
1
  <div class="flex flex-col">
2
- <%= render Avo::PanelComponent.new title: 'Welcome to Avo', description: 'This page is visible only in development. It will be hidden in other environments.' do |c| %>
2
+ <%= render Avo::PanelComponent.new(title: 'Welcome to Avo', description: 'This page is visible only in development. It will be hidden in other environments.') do |c| %>
3
3
  <% c.body do %>
4
4
  <div class="flex flex-col justify-between py-6 min-h-24">
5
5
  <div class="px-6 space-y-4">
@@ -23,10 +23,20 @@
23
23
  <li><span class="mr-2">👍</span> Grid view</li>
24
24
  <li><span class="mr-2">👍</span> Authorization</li>
25
25
  <li><span class="mr-2">👍</span> Localization</li>
26
+ <li><span class="mr-2">👍</span> Resource tools</li>
26
27
  <li><span class="mr-2">👍</span> Custom tools</li>
27
28
  <li><span class="mr-2">👍</span> Custom fields</li>
29
+ <li><span class="mr-2">👍</span> Menu editor</li>
28
30
  <li><span class="mr-2">👍</span> Search</li>
29
- <li><span class="mr-2">🕐</span> Dashboards</li>
31
+ <li><span class="mr-2">👍</span> Dashboards</li>
32
+ <li><span class="mr-2">👍</span> Responsive design</li>
33
+ <li><span class="mr-2">👍</span> Menu editor</li>
34
+ <li><span class="mr-2">👍</span> Stimulus JS integration</li>
35
+ <li><span class="mr-2">👍</span> Tabs and panels</li>
36
+ <li><span class="mr-2">🕐</span> Resource cards</li>
37
+ <li><span class="mr-2">🕐</span> Track resource changes</li>
38
+ <li><span class="mr-2">🕐</span> Smart resource generation</li>
39
+ <li><span class="mr-2">🕐</span> Records preview</li>
30
40
  <li><span class="mr-2">🕐</span> Themes</li>
31
41
  </ul>
32
42
  </p>
@@ -37,6 +47,8 @@
37
47
  <div class="space">
38
48
  <%= render partial: 'resources' %>
39
49
  <hr class="my-6">
50
+ <%= render partial: 'dashboards' %>
51
+ <hr class="my-6">
40
52
  <%= render partial: 'filters' %>
41
53
  <hr class="my-6">
42
54
  <%= render partial: 'actions' %>
@@ -1,6 +1,6 @@
1
1
  <%= javascript_tag nonce: true do %>
2
2
  window.Avo = window.Avo || { configuration: {} }
3
3
  Avo.configuration.timezone = '<%= Avo.configuration.timezone %>'
4
- Avo.configuration.root_path = '<%= Avo::App.root_path %>'
4
+ Avo.configuration.root_path = '<%= Avo.configuration.root_path %>'
5
5
  Avo.configuration.search_debounce = '<%= Avo.configuration.search_debounce %>'
6
6
  <% end %>
@@ -0,0 +1,20 @@
1
+ <div class="flex">
2
+ <div class="button-group">
3
+ <% tabs.each do |tab| %>
4
+ <% is_active_view = tab.name.to_s == active_tab_name.to_s %>
5
+ <%= a_link resource_path(resource: @resource, model: @resource.model, keep_query_params: true, active_tab_name: tab.name, tab_turbo_frame: group.turbo_frame_id),
6
+ color: is_active_view ? :gray : :primary,
7
+ rounded: false,
8
+ size: :sm,
9
+ class: is_active_view ? ' bg-gray-100 border-gray-300' : ' z-20',
10
+ title: tab.description || tab.name,
11
+ data: {
12
+ tippy: 'tooltip',
13
+ 'turbo-frame': group.turbo_frame_id,
14
+ control: "view-type-toggle-#{tab}"
15
+ } do %>
16
+ <%= tab.name %>
17
+ <% end %>
18
+ <% end %>
19
+ </div>
20
+ </div>
@@ -1,5 +1,5 @@
1
1
  <div class="flex flex-col">
2
- <%= render Avo::PanelComponent.new title: 'Welcome to Avo', description: 'This page is visible only in development. It will be hidden in other environments.' do |c| %>
2
+ <%= render Avo::PanelComponent.new(title: 'Welcome to Avo', description: 'This page is visible only in development. It will be hidden in other environments.') do |c| %>
3
3
  <% c.tools do %>
4
4
  <%= a_link('/admin', icon: 'arrow-left', style: :primary, is_link: true) do %>
5
5
  Primary
data/config/routes.rb CHANGED
@@ -23,7 +23,7 @@ Avo::Engine.routes.draw do
23
23
  delete "/:resource_name/:id/active_storage_attachments/:attachment_name/:attachment_id", to: "attachments#destroy"
24
24
 
25
25
  # Ordering
26
- patch "/:resource_name/:id/order", to: "resources#order"
26
+ patch "/:resource_name/:id/order", to: "resources#order", as: "order"
27
27
  patch "/:resource_name/:id/:related_name/:related_id/order", to: "associations#order", as: "associations_order"
28
28
 
29
29
  # Actions
@@ -0,0 +1,5 @@
1
+ class AddColorToTeams < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :teams, :color, :string
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddProgressToProjects < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :projects, :progress, :integer
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddSlugToUsers < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :users, :slug, :string
4
+ add_index :users, :slug, unique: true
5
+ end
6
+ end
data/lib/avo/app.rb CHANGED
@@ -29,14 +29,21 @@ module Avo
29
29
  end
30
30
  end
31
31
 
32
- def init(request:, context:, current_user:, root_path:, view_context:, params:)
32
+ # Renerate a dynamic root path using the URIService
33
+ def root_path(paths: [], query: {}, **args)
34
+ Avo::Services::URIService.parse(view_context.avo.root_url.to_s)
35
+ .append_paths(paths)
36
+ .append_query(query)
37
+ .to_s
38
+ end
39
+
40
+ def init(request:, context:, current_user:, view_context:, params:)
33
41
  self.error_messages = []
34
- self.request = request
35
42
  self.context = context
36
43
  self.current_user = current_user
37
- self.root_path = root_path
38
- self.view_context = view_context
39
44
  self.params = params
45
+ self.request = request
46
+ self.view_context = view_context
40
47
 
41
48
  self.license = Licensing::LicenseManager.new(Licensing::HQ.new(request).response).license
42
49
  self.translation_enabled = license.has(:localization)
@@ -13,7 +13,6 @@ module Avo
13
13
  class_attribute :view
14
14
  class_attribute :user
15
15
  class_attribute :resource
16
- class_attribute :invalid_fields
17
16
  class_attribute :standalone, default: false
18
17
  class_attribute :visible
19
18
  class_attribute :may_download_file, default: false
@@ -22,7 +21,8 @@ module Avo
22
21
  attr_accessor :model
23
22
  attr_accessor :resource
24
23
  attr_accessor :user
25
- attr_accessor :fields_loader
24
+
25
+ delegate :view, to: :class
26
26
 
27
27
  class << self
28
28
  def form_data_attributes
@@ -68,23 +68,6 @@ module Avo
68
68
  self.class.context
69
69
  end
70
70
 
71
- def get_field_definitions
72
- return [] if self.class.fields.blank?
73
-
74
- self.class.fields.map do |field|
75
- field.hydrate(action: self)
76
- end
77
- end
78
-
79
- def get_fields
80
- get_field_definitions.map do |field|
81
- field.hydrate(action: self, model: @model)
82
- end
83
- .select do |field|
84
- field.visible?
85
- end
86
- end
87
-
88
71
  def get_attributes_for_action
89
72
  get_fields.map do |field|
90
73
  [field.id, field.value]
data/lib/avo/base_card.rb CHANGED
@@ -59,13 +59,7 @@ module Avo
59
59
  def frame_url(enforced_range: nil, params: {})
60
60
  enforced_range ||= initial_range || ranges.first
61
61
 
62
- # append the parent params to the card request
63
- begin
64
- other_params = "&#{params.permit!.to_h.map { |k, v| "#{k}=#{v}" }.join("&")}"
65
- rescue
66
- end
67
-
68
- "#{Avo::App.root_path}/dashboards/#{dashboard.id}/cards/#{id}?turbo_frame=#{turbo_frame}&index=#{index}&range=#{enforced_range}#{other_params}"
62
+ Avo::App.view_context.avo.dashboard_card_path(dashboard.id, id, turbo_frame: turbo_frame, index: index, range: enforced_range, **params.permit!)
69
63
  end
70
64
 
71
65
  def card_classes