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.
- checksums.yaml +4 -4
- data/app/helpers/glib/json_ui/page_helper.rb +6 -0
- data/app/helpers/glib/json_ui/view_builder/fields.rb +540 -35
- data/app/helpers/glib/json_ui/view_builder/panels.rb +5 -1
- data/app/views/json_ui/garage/lists/edit_actions.json.jbuilder +96 -66
- data/app/views/json_ui/garage/lists/edit_mode.json.jbuilder +58 -41
- data/app/views/json_ui/garage/panels/timeline.json.jbuilder +82 -73
- data/app/views/json_ui/garage/test_page/lifecycle.json.jbuilder +3 -0
- data/lib/glib/snapshot.rb +75 -17
- data/lib/tasks/db.rake +1 -1
- metadata +6 -7
- data/lib/glib/doc_generator.rb +0 -386
|
@@ -410,7 +410,11 @@ class Glib::JsonUi::ViewBuilder
|
|
|
410
410
|
singleton_array :styleClass, :styleClasses
|
|
411
411
|
array :events
|
|
412
412
|
views :childViews
|
|
413
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
menu
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
+
end
|
|
24
|
+
)
|
|
20
25
|
end
|
|
21
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
horizontal
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
width: 'matchParent',
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
82
|
+
)
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
end
|
|
95
|
+
end
|
|
96
|
+
)
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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 =
|
|
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
|