glib-web 4.39.1 → 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.
@@ -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,49 +3,73 @@ json.title 'Lists'
3
3
  json_ui_page json do |page|
4
4
  render "#{@path_prefix}/nav_menu", json: json, page: page
5
5
 
6
- page.list firstSection: ->(section) do
7
- section.header padding: { top: 12, left: 16, right: 16, bottom: 12 }, childViews: ->(header) do
8
- header.h3 text: 'Section Header'
6
+ page.list(
7
+ firstSection: ->(section) do
8
+ section.header(
9
+ padding: { top: 12, left: 16, right: 16, bottom: 12 },
10
+ childViews: ->(header) do
11
+ header.h3 text: 'Section Header'
12
+ end
13
+ )
14
+ section.rows(
15
+ builder: ->(template) do
16
+ template.thumbnail(
17
+ title: 'Click me',
18
+ onClick: ->(action) do
19
+ action.windows_open url: json_ui_garage_url(path: 'home/blank')
20
+ end,
21
+ accessory: ->(accessory) do
22
+ accessory.header(
23
+ width: 'matchParent',
24
+ backgroundColor: '#b3bac2',
25
+ padding: { top: 10, bottom: 10, left: 10, right: 10 },
26
+ childViews: ->(bottom) do
27
+ bottom.label text: 'Custom row header'
28
+ end
29
+ )
30
+ accessory.footer(
31
+ width: 'matchParent',
32
+ backgroundColor: '#b3bac2',
33
+ padding: { top: 10, bottom: 10, left: 10, right: 10 },
34
+ childViews: ->(bottom) do
35
+ bottom.label text: 'Custom row footer'
36
+ end
37
+ )
38
+ end
39
+ )
40
+ template.thumbnail title: 'Item with icon and subtitle', subtitle: 'Item subtitle', icon: 'facebook'
41
+ template.thumbnail(
42
+ title: 'Item with chips',
43
+ chips: ->(menu) do
44
+ menu.button text: 'Finished', styleClass: 'info'
45
+ menu.button props: { text: 'Succeeded', styleClass: 'success' }
46
+ end
47
+ )
48
+ template.thumbnail(
49
+ title: 'Item with thumbnail image',
50
+ subtitle: 'Item subtitle',
51
+ imageUrl: glib_json_image_standard_url,
52
+ )
53
+ template.featured(
54
+ title: 'Featured with featured image',
55
+ subtitle: 'Item subtitle',
56
+ imageUrl: glib_json_image_standard_url
57
+ )
58
+ template.thumbnail(
59
+ title: 'Item with **formatted** text',
60
+ subtitle: 'Item *subtitle*',
61
+ textFormat: :markdown
62
+ )
63
+ # TODO
64
+ # template.thumbnail title: 'Item with accessories (Experimental)', subtitle: 'Item subtitle', accessoryViews: ->(thumbnail) do
65
+ # thumbnail.panels_horizontal childViews: ->(horizontal) do
66
+ # horizontal.chip text: 'finished'
67
+ # horizontal.spacer width: 10
68
+ # horizontal.chip text: 'succeeded'
69
+ # end
70
+ # end
71
+ end
72
+ )
9
73
  end
10
-
11
- section.rows builder: ->(template) do
12
- template.thumbnail title: 'Click me', onClick: ->(action) do
13
- action.windows_open url: json_ui_garage_url(path: 'home/blank')
14
- end,
15
- accessory: ->(accessory) do
16
- accessory.header \
17
- width: 'matchParent',
18
- backgroundColor: '#b3bac2',
19
- padding: { top: 10, bottom: 10, left: 10, right: 10 },
20
- childViews: ->(bottom) do
21
- bottom.label text: 'Custom row header'
22
- end
23
- accessory.footer \
24
- width: 'matchParent',
25
- backgroundColor: '#b3bac2',
26
- padding: { top: 10, bottom: 10, left: 10, right: 10 },
27
- childViews: ->(bottom) do
28
- bottom.label text: 'Custom row footer'
29
- end
30
- end
31
- template.thumbnail title: 'Item with icon and subtitle', subtitle: 'Item subtitle', icon: 'facebook'
32
- template.thumbnail title: 'Item with chips', chips: ->(menu) do
33
- menu.button text: 'Finished', styleClass: 'info'
34
- menu.button props: { text: 'Succeeded', styleClass: 'success' }
35
- end
36
-
37
- template.thumbnail title: 'Item with thumbnail image', subtitle: 'Item subtitle', imageUrl: glib_json_image_standard_url
38
- template.featured title: 'Featured with featured image', subtitle: 'Item subtitle', imageUrl: glib_json_image_standard_url
39
-
40
- # TODO
41
- # template.thumbnail title: 'Item with accessories (Experimental)', subtitle: 'Item subtitle', accessoryViews: ->(thumbnail) do
42
- # thumbnail.panels_horizontal childViews: ->(horizontal) do
43
- # horizontal.chip text: 'finished'
44
- # horizontal.spacer width: 10
45
- # horizontal.chip text: 'succeeded'
46
- # end
47
- # end
48
-
49
- end
50
- end
74
+ )
51
75
  end
@@ -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
@@ -27,6 +27,8 @@ json_ui_page json do |page|
27
27
  "\n" +
28
28
  '~~Strikethrough~~' + "\n" +
29
29
  "\n" +
30
+ '<u>Underlined</u>' + "\n" +
31
+ "\n" +
30
32
  'https://www.google.com' + "\n"
31
33
 
32
34
  scroll.spacer height: 20
data/config/routes.rb CHANGED
@@ -6,6 +6,10 @@ Glib::Web::Engine.routes.draw do
6
6
  delete 'json_ui_garage', to: 'home#json_ui_garage'
7
7
  post 'chat', to: 'home#chat'
8
8
 
9
+ get 'json_ui_api', to: 'api_docs#index'
10
+ get 'json_ui_api/:category', to: 'api_docs#show', as: :json_ui_api_category
11
+ get 'json_ui_api/:category/:component', to: 'api_docs#component', as: :json_ui_api_component
12
+
9
13
  resources :glib_direct_uploads, only: [:create]
10
14
  resources :blob_url_generators, only: [:create]
11
15
  resources :errors, only: [:create]
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Glib
6
+ # Prevents test method names from containing parentheses which can cause issues with test runners
7
+ #
8
+ # @example
9
+ # # bad
10
+ # test 'user creation (with email)' do
11
+ # end
12
+ #
13
+ # # good
14
+ # test 'user creation with email' do
15
+ # end
16
+ class TestNameParentheses < Base
17
+ MSG = "Test name should not contain parentheses because for some reason, this will produce: 'Syntax error: \"(\" unexpected'."
18
+
19
+ def_node_matcher :test_method?, <<~PATTERN
20
+ (send nil? :test (str $_) ...)
21
+ PATTERN
22
+
23
+ def on_send(node)
24
+ test_method?(node) do |test_name|
25
+ if test_name.include?('(') || test_name.include?(')')
26
+ add_offense(node)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/glib/rubocop.rb CHANGED
@@ -3,3 +3,4 @@ require_relative 'rubocop/cops/multiline_method_call_style'
3
3
  require_relative 'rubocop/cops/json_ui/base_nested_parameter'
4
4
  require_relative 'rubocop/cops/json_ui/nested_block_parameter'
5
5
  require_relative 'rubocop/cops/json_ui/nested_action_parameter'
6
+ require_relative 'rubocop/cops/test_name_parentheses'
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