glib-web 4.39.2 → 4.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -410,7 +410,11 @@ class Glib::JsonUi::ViewBuilder
410
410
  singleton_array :styleClass, :styleClasses
411
411
  array :events
412
412
  views :childViews
413
- string :truncateLine
413
+ # Controls which end(s) of the timeline line to truncate.
414
+ # @see https://vuetifyjs.com/en/api/v-timeline/#props-truncate-line Vuetify Timeline truncateLine
415
+ enum :truncateLine, options: [:start, :end, :both]
416
+ enum :side, options: [:start, :end]
417
+ enum :direction, options: [:horizontal, :vertical]
414
418
  end
415
419
 
416
420
  # deprecated
@@ -7,73 +7,103 @@ end
7
7
  page = json_ui_page json
8
8
  render "#{@path_prefix}/nav_menu", json: json, page: page
9
9
 
10
- page.list firstSection: ->(section) do
11
- section.rows builder: ->(template) do
12
- template.thumbnail \
13
- title: 'Click menu (web) or swipe left (Android/iOS)',
14
- subtitle: "Page index: #{page_index}",
15
- leftButtons: ->(menu) do
16
- menu.button \
17
- styleClasses: ['icon', 'text', 'x-small'],
18
- icon: 'check_box',
19
- onClick: ->(subaction) do
20
- subaction.dialogs_alert message: 'Tick/untick'
10
+ page.list(
11
+ firstSection: ->(section) do
12
+ section.rows(
13
+ builder: ->(template) do
14
+ template.thumbnail(
15
+ title: 'Click menu (web) or swipe left (Android/iOS)',
16
+ subtitle: "Page index: #{page_index}",
17
+ leftButtons: ->(menu) do
18
+ menu.button(
19
+ styleClasses: ['icon', 'text', 'x-small'],
20
+ icon: 'check_box',
21
+ onClick: ->(subaction) do
22
+ subaction.dialogs_alert message: 'Tick/untick'
23
+ end
24
+ )
25
+ end,
26
+ rightButtons: ->(menu) do
27
+ menu.button(
28
+ styleClasses: ['icon', 'text', 'x-small'],
29
+ icon: 'share',
30
+ tooltip: { text: 'Share', placement: 'left' },
31
+ onClick: ->(subaction) do
32
+ subaction.dialogs_alert message: 'Share'
33
+ end
34
+ )
35
+ # TODO: Use popoever
36
+ # childButtons: ->(submenu) do
37
+ # submenu.button text: 'Dropdown item 1'
38
+ # submenu.button text: 'Dropdown item 2'
39
+ # submenu.button text: 'Dropdown item 3'
40
+ # end
41
+ menu.button(
42
+ styleClasses: ['icon', 'text', 'x-small'],
43
+ icon: 'open_in_new',
44
+ tooltip: { text: 'Open in new window' },
45
+ onClick: ->(subaction) do
46
+ subaction.dialogs_alert message: 'Open'
47
+ end
48
+ )
49
+ end,
50
+ # editButtons: ->(menu) do
51
+ # menu.button(
52
+ # text: "Edit (ID: #{page_index})",
53
+ # onClick: ->(action) do
54
+ # action.dialogs_alert message: 'Perform Edit action'
55
+ # end
56
+ # )
57
+ # menu.button(
58
+ # text: 'Delete',
59
+ # onClick: ->(action) do
60
+ # action.dialogs_alert message: 'Perform Delete action'
61
+ # end
62
+ # )
63
+ # end,
64
+ onClick: ->(action) do
65
+ action.windows_open url: json_ui_garage_url(path: 'lists/edit_actions', page: page_index + 1)
21
66
  end
22
- end, rightButtons: ->(menu) do
23
- menu.button \
24
- styleClasses: ['icon', 'text', 'x-small'],
25
- icon: 'share',
26
- tooltip: { text: 'Share', placement: 'left' }
27
-
28
- # TODO: Use popoever
29
- # childButtons: ->(submenu) do
30
- # submenu.button text: 'Dropdown item 1'
31
- # submenu.button text: 'Dropdown item 2'
32
- # submenu.button text: 'Dropdown item 3'
33
- # end
34
-
35
- menu.button \
36
- styleClasses: ['icon', 'text', 'x-small'],
37
- icon: 'open_in_new',
38
- tooltip: { text: 'Open in new window' },
39
- onClick: ->(subaction) do
40
- subaction.dialogs_alert message: 'Open'
67
+ )
68
+ template.thumbnail(
69
+ title: 'Long press to get an alert',
70
+ onLongPress: ->(action) do
71
+ action.dialogs_alert message: 'This is an alert'
41
72
  end
42
-
43
- end, editButtons: ->(menu) do
44
- menu.button text: "Edit (ID: #{page_index})", onClick: ->(action) do
45
- action.dialogs_alert message: 'Perform Edit action'
46
- end
47
- menu.button text: 'Delete', onClick: ->(action) do
48
- action.dialogs_alert message: 'Perform Delete action'
49
- end
50
- end, onClick: ->(action) do
51
- action.windows_open url: json_ui_garage_url(path: 'lists/edit_actions', page: page_index + 1)
52
- end
53
-
54
- template.thumbnail title: 'Long press to get an alert', onLongPress: ->(action) do
55
- action.dialogs_alert message: 'This is an alert'
56
- end
57
-
58
- template.thumbnail title: 'Long press to see menu', onLongPress: ->(action) do
59
- action.sheets_select message: 'Context Menu', buttons: ->(menu) do
60
- menu.button icon: 'edit', text: 'Edit', onClick: ->(subaction) do
61
- subaction.dialogs_alert message: 'Perform action'
62
- end
63
- menu.button icon: 'delete', text: 'Delete', onClick: ->(subaction) do
64
- subaction.dialogs_alert message: 'Perform action'
65
- end
73
+ )
74
+ template.thumbnail(
75
+ title: 'Long press to see menu',
76
+ onLongPress: ->(action) do
77
+ action.sheets_select(
78
+ message: 'Context Menu',
79
+ buttons: ->(menu) do
80
+ menu.button(
81
+ icon: 'edit',
82
+ text: 'Edit',
83
+ onClick: ->(subaction) do
84
+ subaction.dialogs_alert message: 'Perform action'
85
+ end
86
+ )
87
+ menu.button(
88
+ icon: 'delete',
89
+ text: 'Delete',
90
+ onClick: ->(subaction) do
91
+ subaction.dialogs_alert message: 'Perform action'
92
+ end
93
+ )
94
+ end
95
+ )
96
+ end
97
+ )
98
+ # template.thumbnail title: 'Long press to see menu', contextButtons: ->(menu) do
99
+ # menu.button text: 'Edit', onClick: ->(action) do
100
+ # action.dialogs_alert message: 'Perform action'
101
+ # end
102
+ # menu.button text: 'Delete', onClick: ->(action) do
103
+ # action.dialogs_alert message: 'Perform action'
104
+ # end
105
+ # end
66
106
  end
67
- end
68
-
69
- # template.thumbnail title: 'Long press to see menu', contextButtons: ->(menu) do
70
- # menu.button text: 'Edit', onClick: ->(action) do
71
- # action.dialogs_alert message: 'Perform action'
72
- # end
73
- # menu.button text: 'Delete', onClick: ->(action) do
74
- # action.dialogs_alert message: 'Perform action'
75
- # end
76
- # end
107
+ )
77
108
  end
78
-
79
- end
109
+ )
@@ -6,61 +6,78 @@ render "#{@path_prefix}/nav_menu", json: json, page: page
6
6
 
7
7
  tab_index = params[:tab].to_i
8
8
 
9
- page.header childViews: ->(header) do
10
- # Allow navigating to another "edit mode" page to test reuse issues.
11
- header.tabBar buttons: ->(menu) do
12
- ['FIRST', 'SECOND'].each_with_index do |text, index|
13
- menu.button \
14
- text: text,
15
- disabled: tab_index == index,
16
- onClick: ->(action) do
17
- action.windows_reload url: json_ui_garage_url(path: 'lists/edit_mode', tab: index)
9
+ page.header(
10
+ childViews: ->(header) do
11
+ # Allow navigating to another "edit mode" page to test reuse issues.
12
+ header.tabBar(
13
+ buttons: ->(menu) do
14
+ ['FIRST', 'SECOND'].each_with_index do |text, index|
15
+ menu.button(
16
+ text: text,
17
+ disabled: tab_index == index,
18
+ onClick: ->(action) do
19
+ action.windows_reload url: json_ui_garage_url(path: 'lists/edit_mode', tab: index)
20
+ end
21
+ )
18
22
  end
19
- end
23
+ end
24
+ )
20
25
  end
21
- end
26
+ )
22
27
 
23
- page.form \
28
+ page.form(
24
29
  width: 'matchParent',
25
30
  url: json_ui_garage_url(path: 'forms/generic_post'),
26
31
  method: 'post',
27
32
  padding: glib_json_padding_body,
28
33
  childViews: ->(form) do
29
- form.panels_list fieldPrefix: 'user[items]', fieldTitleName: 'name', width: 'matchParent', sections: [
30
- ->(section) do
31
- section.header padding: glib_json_padding_list, childViews: ->(header) do
32
-
33
- header.panels_horizontal childViews: ->(horizontal) do
34
- horizontal.fields_check name: 'user[check_all]', label: 'All', checkValue: true
35
-
36
- horizontal.spacer width: 20
37
- # header.fields_text width: 'matchParent', styleClass: 'outlined', name: 'user[new_name]', label: 'Item name'
38
- statuses = [:pending, :active]
39
- horizontal.fields_select \
40
- styleClass: 'outlined',
41
- name: 'user[status]',
42
- width: 'matchParent',
43
- label: 'Status',
44
- options: statuses.map { |status| { value: status, text: status.to_s.humanize } }
45
-
46
- horizontal.spacer width: 20
47
- horizontal.fields_submit text: 'Update'
48
- end
34
+ form.panels_list(
35
+ fieldPrefix: 'user[items]',
36
+ fieldTitleName: 'name',
37
+ width: 'matchParent',
38
+ sections: [
39
+ ->(section) do
40
+ section.header(
41
+ padding: glib_json_padding_list,
42
+ childViews: ->(header) do
43
+ header.panels_horizontal(
44
+ childViews: ->(horizontal) do
45
+ horizontal.fields_check name: 'user[check_all]', label: 'All', checkValue: true
46
+ horizontal.spacer width: 20
47
+ # header.fields_text width: 'matchParent', styleClass: 'outlined', name: 'user[new_name]', label: 'Item name'
48
+ statuses = [:pending, :active]
49
+ horizontal.fields_select(
50
+ styleClass: 'outlined',
51
+ name: 'user[status]',
52
+ width: 'matchParent',
53
+ label: 'Status',
54
+ options: statuses.map { |status| { value: status, text: status.to_s.humanize } }
55
+ )
56
+ horizontal.spacer width: 20
57
+ horizontal.fields_submit text: 'Update'
58
+ end
59
+ )
49
60
  end
50
- section.rows builder: ->(row) do
61
+ )
62
+ section.rows(
63
+ builder: ->(row) do
51
64
  batch_count = 20
52
65
  batch_count.times do |index|
53
66
  id = (batch_count * tab_index) + index
54
67
  row.editable title: "Item #{id}", recordId: "PK_#{id}"
55
68
  end
56
69
  end
57
- end
58
- ], fieldCheckValueIf: {
59
- "==": [
60
- {
61
- "var": 'user[check_all]'
62
- },
63
- true
64
- ]
70
+ )
71
+ end
72
+ ],
73
+ fieldCheckValueIf: {
74
+ "==": [
75
+ {
76
+ "var": 'user[check_all]'
77
+ },
78
+ true
79
+ ]
65
80
  }
81
+ )
66
82
  end
83
+ )
@@ -3,85 +3,94 @@ json.title 'Timeline Panels'
3
3
  page = json_ui_page json
4
4
  render "#{@path_prefix}/nav_menu", json: json, page: page
5
5
 
6
- page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
7
- timeline_items = [
8
- { icon: 'place', color: '#4BB543' },
9
- { icon: 'check_circle', color: 'blue' },
10
- { icon: 'hourglass_empty', color: 'blue', text: 'Pending' },
11
- { icon: 'radio_button_unchecked' },
12
- { icon: 'radio_button_unchecked' },
6
+ page.scroll(
7
+ padding: glib_json_padding_body,
8
+ childViews: ->(scroll) do
9
+ timeline_items = [
10
+ { icon: 'place', color: '#4BB543' },
11
+ { icon: 'check_circle', color: 'blue' },
12
+ { icon: 'hourglass_empty', color: 'blue', text: 'Pending' },
13
+ { icon: 'radio_button_unchecked' },
14
+ { icon: 'radio_button_unchecked' },
13
15
  ]
14
-
15
- scroll.h2 text: 'Basic timeline'
16
- scroll.panels_timeline \
17
- events: timeline_items,
18
- childViews: ->(timeline) do
19
- timeline.label styleClass: 'mt-2', text: 'Order submitted'
20
-
21
- timeline.label styleClass: 'mt-2', text: 'Finding you a driver'
22
-
23
- timeline.panels_vertical styleClass: 'mt-2', childViews: ->(vertical) do
24
- vertical.h4 text: 'Driver found, picking you up..'
25
- vertical.spacer height: 16
26
- render 'json_ui/garage/panels/timeline_content', tview: vertical
16
+ scroll.h2 text: 'Basic timeline'
17
+ scroll.panels_timeline(
18
+ events: timeline_items,
19
+ childViews: ->(timeline) do
20
+ timeline.label styleClass: 'mt-2', text: 'Order submitted'
21
+ timeline.label styleClass: 'mt-2', text: 'Finding you a driver'
22
+ timeline.panels_vertical(
23
+ styleClass: 'mt-2',
24
+ childViews: ->(vertical) do
25
+ vertical.h4 text: 'Driver found, picking you up..'
26
+ vertical.spacer height: 16
27
+ render 'json_ui/garage/panels/timeline_content', tview: vertical
28
+ end
29
+ )
30
+ timeline.label styleClass: 'mt-2', text: 'On the way'
31
+ timeline.label styleClass: 'mt-2', text: 'Arrived'
27
32
  end
28
-
29
- timeline.label styleClass: 'mt-2', text: 'On the way'
30
-
31
- timeline.label styleClass: 'mt-2', text: 'Arrived'
32
- end
33
-
34
- timeline_items = [
35
- { icon: 'place', color: '#4BB543', styleClasses: ['outlined'] },
36
- { icon: 'check_circle', color: 'blue', styleClasses: ['outlined'] },
37
- { icon: 'check_circle', color: 'blue', styleClasses: ['outlined'] },
38
- { icon: 'check_circle', color: 'blue', styleClasses: ['outlined'] },
39
- { icon: 'flag', color: '#FFA500', styleClasses: ['outlined'] },
33
+ )
34
+ timeline_items = [
35
+ { icon: 'place', color: '#4BB543', styleClasses: ['outlined'] },
36
+ { icon: 'check_circle', color: 'blue', styleClasses: ['outlined'] },
37
+ { icon: 'check_circle', color: 'blue', styleClasses: ['outlined'] },
38
+ { icon: 'check_circle', color: 'blue', styleClasses: ['outlined'] },
39
+ { icon: 'flag', color: '#FFA500', styleClasses: ['outlined'] },
40
40
  ]
41
-
42
- scroll.h2 text: 'Timeline with outlined dots'
43
- scroll.panels_timeline \
44
- events: timeline_items,
45
- childViews: ->(timeline) do
46
- timeline.panels_vertical childViews: ->(vertical) do
47
- vertical.h4 text: 'Order submitted'
48
- vertical.spacer height: 2
49
- vertical.label text: '15 minutes ago'
50
- end
51
-
52
- timeline.panels_vertical childViews: ->(vertical) do
53
- vertical.h4 text: 'Finding you a driver'
54
- vertical.spacer height: 2
55
- vertical.label text: '15 minutes ago'
56
- end
57
-
58
- timeline.panels_vertical childViews: ->(vertical) do
59
- vertical.h4 text: 'Driver found'
60
- vertical.spacer height: 2
61
- vertical.label text: '12 minutes ago'
62
- end
63
-
64
- timeline.panels_vertical childViews: ->(vertical) do
65
- vertical.h4 text: 'On the way'
66
- vertical.spacer height: 2
67
- vertical.label text: 'Duration: 11 minutes'
41
+ timeline_events = [
42
+ { title: 'Order submitted', time: '15 minutes ago' },
43
+ { title: 'Finding you a driver', time: '15 minutes ago' },
44
+ { title: 'Driver found', time: '12 minutes ago' },
45
+ { title: 'On the way', time: 'Duration: 11 minutes' },
46
+ { title: 'Arrived', time: '1 minute ago' }
47
+ ]
48
+ scroll.spacer height: 32
49
+ scroll.h2 text: 'Timeline with outlined dots'
50
+ scroll.panels_timeline(
51
+ events: timeline_items,
52
+ childViews: ->(timeline) do
53
+ timeline_events.each do |event|
54
+ timeline.panels_vertical(
55
+ childViews: ->(vertical) do
56
+ vertical.h4 text: event[:title]
57
+ vertical.spacer height: 2
58
+ vertical.label text: event[:time]
59
+ end
60
+ )
61
+ end
68
62
  end
63
+ )
69
64
 
70
- timeline.panels_vertical childViews: ->(vertical) do
71
- vertical.h4 text: 'Arrived'
72
- vertical.spacer height: 2
73
- vertical.label text: '1 minute ago'
65
+ scroll.spacer height: 32
66
+ scroll.h2 text: 'Timeline horizontal'
67
+ scroll.panels_timeline(
68
+ events: timeline_items,
69
+ direction: 'horizontal',
70
+ side: 'end',
71
+ childViews: ->(timeline) do
72
+ timeline_events.each do |event|
73
+ timeline.panels_vertical(
74
+ childViews: ->(vertical) do
75
+ vertical.h4 text: event[:title]
76
+ vertical.spacer height: 2
77
+ vertical.label text: event[:time]
78
+ end
79
+ )
80
+ end
74
81
  end
75
- end
82
+ )
76
83
 
77
- timeline_items = [
78
- { backgroundColor: 'blue', styleClasses: ['small'] },
79
- { backgroundColor: 'blue', styleClasses: ['x-small'] },
80
- { backgroundColor: 'blue', color: 'white', text: '3' },
81
- { backgroundColor: 'white', icon: 'radio_button_unchecked', styleClasses: ['small'] },
82
- { backgroundColor: 'white', icon: 'radio_button_unchecked', styleClasses: ['small'] },
84
+ scroll.spacer height: 32
85
+ timeline_items = [
86
+ { backgroundColor: 'blue', styleClasses: ['small'] },
87
+ { backgroundColor: 'blue', styleClasses: ['x-small'] },
88
+ { backgroundColor: 'blue', color: 'white', text: '3' },
89
+ { backgroundColor: 'white', icon: 'radio_button_unchecked', styleClasses: ['small'] },
90
+ { backgroundColor: 'white', icon: 'radio_button_unchecked', styleClasses: ['small'] },
83
91
  ]
92
+ scroll.h2 text: 'Timeline without content'
93
+ scroll.panels_timeline truncateLine: 'both', events: timeline_items
84
94
 
85
- scroll.h2 text: 'Timeline without content'
86
- scroll.panels_timeline truncateLine: 'both', events: timeline_items
87
- end
95
+ end
96
+ )
@@ -9,6 +9,9 @@ page.on load: ->(action) do
9
9
  end,
10
10
  reRender: ->(action) do
11
11
  action.snackbars_alert message: 'page.onRerender'
12
+ end,
13
+ foreground: ->(action) do
14
+ action.snackbars_alert message: 'page.onForeground'
12
15
  end
13
16
 
14
17
  page.body childViews: ->(body) do
data/lib/glib/snapshot.rb CHANGED
@@ -23,17 +23,25 @@ module Glib
23
23
  return true if changed?
24
24
 
25
25
  associations_for_snapshot.each do |association_name|
26
- if (association = self.class.reflect_on_association(association_name))
27
- case association.macro
28
- when :has_many
29
- public_send(association_name).each do |record|
30
- return true if record.changed?
31
- end
32
- when :has_one, :belongs_to
33
- return true if public_send(association_name).changed?
26
+ association = self.class.reflect_on_association(association_name)
27
+ association_value = public_send(association_name)
28
+
29
+ if active_storage_association?(association, association_value)
30
+ active_storage_records(association_value).each do |record|
31
+ return true if record.changed?
34
32
  end
35
- else
36
- raise "Invalid association: #{association_name}"
33
+ next
34
+ end
35
+
36
+ raise "Invalid association: #{association_name}" if association.nil?
37
+
38
+ case association.macro
39
+ when :has_many
40
+ association_value.each do |record|
41
+ return true if record.changed?
42
+ end
43
+ when :has_one, :belongs_to
44
+ return true if association_value&.changed?
37
45
  end
38
46
  end
39
47
 
@@ -134,7 +142,7 @@ module Glib
134
142
  end
135
143
 
136
144
  # always watch column with name *id
137
- ignore_keys.filter! { |key| !key.ends_with?('id') || key != 'id' }
145
+ ignore_keys.reject! { |key| key == 'id' || key.ends_with?('_id') }
138
146
 
139
147
  if snapshot.present?
140
148
  item, associations = snapshot.fetch_reified_items
@@ -147,15 +155,18 @@ module Glib
147
155
  'item' => ::Hashdiff.diff(item.attributes.except(*ignore_keys), attributes.except(*ignore_keys))
148
156
  }
149
157
 
150
- obj['associations'] = associations_for_snapshot.reduce({}) do |prev, curr|
151
- first_record_off_same_collection = send(curr).first
158
+ obj['associations'] = associations_for_snapshot.reduce({}) do |prev, curr| # rubocop:disable Glib/MultilineMethodCallStyle
159
+ association_records = records_for_snapshot(curr)
160
+ first_record_off_same_collection = association_records.first
152
161
 
153
- if !first_record_off_same_collection.respond_to?(:watched_keys_for_snapshot) && !first_record_off_same_collection.nil?
162
+ if !first_record_off_same_collection.nil? && !active_storage_record?(first_record_off_same_collection) && !first_record_off_same_collection.respond_to?(:watched_keys_for_snapshot)
154
163
  raise NotImplementedError, "please add method 'watched_keys_for_snapshot' to #{first_record_off_same_collection.class}"
155
164
  end
156
165
 
157
166
  association_ignored_keys =
158
- if !first_record_off_same_collection.try(:watched_keys_for_snapshot).nil?
167
+ if active_storage_record?(first_record_off_same_collection)
168
+ default_ignored_keys
169
+ elsif !first_record_off_same_collection.try(:watched_keys_for_snapshot).nil?
159
170
  assoc_ignore_keys = first_record_off_same_collection.attributes.except(*first_record_off_same_collection.watched_keys_for_snapshot.map(&:to_s)).keys.map(&:to_s)
160
171
  (default_ignored_keys + assoc_ignore_keys).uniq
161
172
  else
@@ -163,13 +174,13 @@ module Glib
163
174
  end
164
175
 
165
176
  # always watch column with name *id
166
- association_ignored_keys.filter! { |key| !key.ends_with?('id') || key != 'id' }
177
+ association_ignored_keys.reject! { |key| key == 'id' || key.ends_with?('_id') }
167
178
 
168
179
  # attrs_before = (associations[curr] || []).map { |record| record.attributes.except(*association_ignored_keys) }
169
180
  # attrs_now = send(curr).order(id: :asc).map { |record| record.attributes.except(*association_ignored_keys) }
170
181
 
171
182
  before = (associations[curr] || [])
172
- now = send(curr).order(id: :asc)
183
+ now = association_records
173
184
 
174
185
  if before.blank?
175
186
  prev.merge(curr.to_s => ::Hashdiff.diff([], now.map { |record| record.attributes.except(*association_ignored_keys) }))
@@ -238,5 +249,52 @@ module Glib
238
249
  def max_snapshots
239
250
  10
240
251
  end
252
+
253
+ private
254
+ def active_storage_association?(association, association_value)
255
+ return true if association&.macro == :has_many_attached || association&.macro == :has_one_attached
256
+ return false if association_value.nil?
257
+
258
+ if defined?(ActiveStorage::Attached)
259
+ return true if association_value.is_a?(ActiveStorage::Attached::Many) || association_value.is_a?(ActiveStorage::Attached::One)
260
+ end
261
+
262
+ association_value.respond_to?(:attachments) || association_value.respond_to?(:attachment)
263
+ end
264
+
265
+ def active_storage_records(value)
266
+ if value.respond_to?(:attachments)
267
+ value.attachments
268
+ elsif value.respond_to?(:attachment)
269
+ Array(value.attachment).compact
270
+ else
271
+ []
272
+ end
273
+ end
274
+
275
+ def active_storage_record?(record)
276
+ return false if record.nil?
277
+
278
+ (defined?(ActiveStorage::Attachment) && record.is_a?(ActiveStorage::Attachment)) ||
279
+ record.class.name.start_with?('ActiveStorage::Attachment')
280
+ end
281
+
282
+ def records_for_snapshot(association_name)
283
+ association_value = public_send(association_name)
284
+
285
+ records = if active_storage_association?(self.class.reflect_on_association(association_name), association_value)
286
+ active_storage_records(association_value)
287
+ else
288
+ association_value
289
+ end
290
+
291
+ if records.respond_to?(:order)
292
+ records.order(id: :asc)
293
+ elsif records.respond_to?(:sort_by)
294
+ records.sort_by { |record| record.try(:id) || 0 }
295
+ else
296
+ Array(records)
297
+ end
298
+ end
241
299
  end
242
300
  end
data/lib/tasks/db.rake CHANGED
@@ -63,7 +63,7 @@ namespace :db do
63
63
 
64
64
  entries.each do |a|
65
65
  attrs = Hash[a.attributes.sort]
66
- # attrs.delete_if { |k, v| v.nil? }
66
+ attrs.delete_if { |k, v| v.nil? }
67
67
 
68
68
  id = if attrs.include?('id')
69
69
  [a.id]