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.
- checksums.yaml +4 -4
- data/app/controllers/glib/api_docs_controller.rb +145 -0
- data/app/helpers/glib/json_ui/abstract_builder.rb +16 -0
- data/app/helpers/glib/json_ui/action_builder/dialogs.rb +4 -0
- data/app/helpers/glib/json_ui/list_builders.rb +2 -0
- data/app/helpers/glib/json_ui/page_helper.rb +6 -0
- data/app/helpers/glib/json_ui/view_builder/fields.rb +554 -34
- data/app/helpers/glib/json_ui/view_builder/panels.rb +455 -12
- data/app/helpers/glib/json_ui/view_builder.rb +1 -1
- data/app/views/glib/api_docs/component.json.jbuilder +215 -0
- data/app/views/glib/api_docs/index.json.jbuilder +103 -0
- data/app/views/glib/api_docs/show.json.jbuilder +111 -0
- 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/lists/templating.json.jbuilder +68 -44
- 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/app/views/json_ui/garage/views/markdowns.json.jbuilder +2 -0
- data/config/routes.rb +4 -0
- data/lib/glib/rubocop/cops/test_name_parentheses.rb +33 -0
- data/lib/glib/rubocop.rb +1 -0
- data/lib/glib/snapshot.rb +75 -17
- data/lib/tasks/docs.rake +59 -0
- metadata +12 -6
|
@@ -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,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
|
|
7
|
-
|
|
8
|
-
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
|
|
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/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
|
-
|
|
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
|