motor-admin 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +20 -0
- data/app/controllers/motor/alerts_controller.rb +74 -0
- data/app/controllers/motor/api_base_controller.rb +25 -0
- data/app/controllers/motor/application_controller.rb +6 -0
- data/app/controllers/motor/assets_controller.rb +28 -0
- data/app/controllers/motor/configs_controller.rb +30 -0
- data/app/controllers/motor/dashboards_controller.rb +54 -0
- data/app/controllers/motor/data_controller.rb +102 -0
- data/app/controllers/motor/forms_controller.rb +54 -0
- data/app/controllers/motor/queries_controller.rb +54 -0
- data/app/controllers/motor/resource_methods_controller.rb +21 -0
- data/app/controllers/motor/resources_controller.rb +23 -0
- data/app/controllers/motor/run_queries_controller.rb +45 -0
- data/app/controllers/motor/schemas_controller.rb +11 -0
- data/app/controllers/motor/send_alerts_controller.rb +24 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +19 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- data/app/mailers/motor/alerts_mailer.rb +50 -0
- data/app/mailers/motor/application_mailer.rb +7 -0
- data/app/models/motor/alert.rb +22 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/application_record.rb +8 -0
- data/app/models/motor/config.rb +7 -0
- data/app/models/motor/dashboard.rb +18 -0
- data/app/models/motor/form.rb +14 -0
- data/app/models/motor/query.rb +14 -0
- data/app/models/motor/resource.rb +7 -0
- data/app/models/motor/tag.rb +7 -0
- data/app/models/motor/taggable_tag.rb +8 -0
- data/app/views/layouts/motor/application.html.erb +14 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +126 -0
- data/app/views/motor/ui/show.html.erb +1 -0
- data/config/routes.rb +60 -0
- data/lib/generators/motor/install_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +135 -0
- data/lib/motor-admin.rb +3 -0
- data/lib/motor.rb +47 -0
- data/lib/motor/active_record_utils.rb +7 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +54 -0
- data/lib/motor/admin.rb +12 -0
- data/lib/motor/alerts.rb +10 -0
- data/lib/motor/alerts/persistance.rb +84 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/api.rb +6 -0
- data/lib/motor/api_query.rb +22 -0
- data/lib/motor/api_query/build_json.rb +109 -0
- data/lib/motor/api_query/build_meta.rb +17 -0
- data/lib/motor/api_query/filter.rb +55 -0
- data/lib/motor/api_query/paginate.rb +18 -0
- data/lib/motor/api_query/search.rb +73 -0
- data/lib/motor/api_query/sort.rb +27 -0
- data/lib/motor/assets.rb +45 -0
- data/lib/motor/build_schema.rb +23 -0
- data/lib/motor/build_schema/find_display_column.rb +60 -0
- data/lib/motor/build_schema/load_from_rails.rb +176 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +77 -0
- data/lib/motor/build_schema/persist_resource_configs.rb +208 -0
- data/lib/motor/build_schema/reorder_schema.rb +52 -0
- data/lib/motor/build_schema/utils.rb +17 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/dashboards/persistance.rb +63 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/forms/persistance.rb +63 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/queries.rb +10 -0
- data/lib/motor/queries/persistance.rb +63 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/run_query.rb +68 -0
- data/lib/motor/tags.rb +31 -0
- data/lib/motor/ui_configs.rb +62 -0
- data/lib/motor/version.rb +5 -0
- data/ui/dist/fonts/ionicons.woff2 +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
- data/ui/dist/manifest.json +13 -0
- metadata +237 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ApiQuery
|
5
|
+
module Paginate
|
6
|
+
MAX_PER_PAGE = 500
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def call(rel, params)
|
11
|
+
params ||= {}
|
12
|
+
|
13
|
+
rel = rel.limit([MAX_PER_PAGE, (params[:limit] || MAX_PER_PAGE).to_i].min)
|
14
|
+
rel.offset(params[:offset].to_i)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ApiQuery
|
5
|
+
module Search
|
6
|
+
SELECT_COLUMNS_AMOUNT = 2
|
7
|
+
COLUMN_TYPES = BuildSchema::SEARCHABLE_COLUMN_TYPES
|
8
|
+
ID_REGEXP = /\A\d+\z/.freeze
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def call(rel, keyword)
|
13
|
+
return rel if keyword.blank?
|
14
|
+
|
15
|
+
filters = fetch_filters(rel, keyword)
|
16
|
+
|
17
|
+
arel_where = build_arel_or_query(filters)
|
18
|
+
|
19
|
+
rel.where(arel_where)
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch_filters(rel, keyword)
|
23
|
+
arel_filters = []
|
24
|
+
|
25
|
+
klass = rel.klass
|
26
|
+
arel_table = klass.arel_table
|
27
|
+
|
28
|
+
arel_filters << arel_table[klass.primary_key].eq(keyword) if keyword.match?(ID_REGEXP)
|
29
|
+
|
30
|
+
string_column_names = find_searchable_columns(klass)
|
31
|
+
selected_columns = select_columns(string_column_names)
|
32
|
+
|
33
|
+
selected_columns.each { |name| arel_filters << arel_table[name].matches("%#{keyword}%") }
|
34
|
+
|
35
|
+
arel_filters
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_arel_or_query(filter_array)
|
39
|
+
filter_array.reduce(nil) do |acc, filter|
|
40
|
+
next acc = filter unless acc
|
41
|
+
|
42
|
+
acc.or(filter)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def select_columns(columns)
|
47
|
+
selected_columns =
|
48
|
+
columns.select do |name|
|
49
|
+
BuildSchema::FindDisplayColumn::DISPLAY_NAME_REGEXP.match?(name)
|
50
|
+
end.presence
|
51
|
+
|
52
|
+
selected_columns ||= columns.first(SELECT_COLUMNS_AMOUNT)
|
53
|
+
|
54
|
+
selected_columns
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_searchable_columns(model)
|
58
|
+
model.columns.map do |column|
|
59
|
+
next unless column.type.in?(COLUMN_TYPES)
|
60
|
+
|
61
|
+
has_inclusion_validator =
|
62
|
+
model.validators_on(column.name).any? do |e|
|
63
|
+
e.is_a?(ActiveModel::Validations::InclusionValidator)
|
64
|
+
end
|
65
|
+
|
66
|
+
next if has_inclusion_validator
|
67
|
+
|
68
|
+
column.name
|
69
|
+
end.compact
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ApiQuery
|
5
|
+
module Sort
|
6
|
+
FIELD_PARSE_REGEXP = /\A(-)?(.*)\z/.freeze
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def call(rel, params)
|
11
|
+
return rel if params.blank?
|
12
|
+
|
13
|
+
normalized_params = build_params(params)
|
14
|
+
|
15
|
+
rel.order(normalized_params)
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_params(param)
|
19
|
+
param.split(',').each_with_object({}) do |field, hash|
|
20
|
+
direction, name = field.match(FIELD_PARSE_REGEXP).captures
|
21
|
+
|
22
|
+
hash[name] = direction.present? ? :desc : :asc
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/motor/assets.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Assets
|
5
|
+
InvalidPathError = Class.new(StandardError)
|
6
|
+
|
7
|
+
ASSETS_PATH = Pathname.new(__dir__).join('../../ui/dist')
|
8
|
+
MANIFEST_PATH = ASSETS_PATH.join('manifest.json')
|
9
|
+
DEV_SERVER_URL = 'http://localhost:9090/'
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def manifest
|
14
|
+
JSON.parse(MANIFEST_PATH.read)
|
15
|
+
end
|
16
|
+
|
17
|
+
def asset_path(path)
|
18
|
+
Motor::Admin.routes.url_helpers.motor_asset_path(manifest[path])
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_asset(filename)
|
22
|
+
if Motor.development?
|
23
|
+
load_from_dev_server(filename)
|
24
|
+
else
|
25
|
+
load_from_disk(filename)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_from_disk(filename)
|
30
|
+
filename += '.gz' if filename.match?(/\.(?:js|css)\z/)
|
31
|
+
|
32
|
+
path = ASSETS_PATH.join(filename)
|
33
|
+
|
34
|
+
raise InvalidPathError unless path.to_s.starts_with?(ASSETS_PATH.to_s)
|
35
|
+
|
36
|
+
path.read
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_from_dev_server(filename)
|
40
|
+
uri = URI(DEV_SERVER_URL + filename)
|
41
|
+
|
42
|
+
Net::HTTP.get_response(uri).body
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './build_schema/load_from_rails'
|
4
|
+
require_relative './build_schema/find_display_column'
|
5
|
+
require_relative './build_schema/persist_resource_configs'
|
6
|
+
require_relative './build_schema/reorder_schema'
|
7
|
+
require_relative './build_schema/merge_schema_configs'
|
8
|
+
require_relative './build_schema/utils'
|
9
|
+
|
10
|
+
module Motor
|
11
|
+
module BuildSchema
|
12
|
+
SEARCHABLE_COLUMN_TYPES = %i[citext text string bitstring].freeze
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
def call
|
17
|
+
schema = LoadFromRails.call
|
18
|
+
schema = MergeSchemaConfigs.call(schema)
|
19
|
+
|
20
|
+
ReorderSchema.call(schema)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module BuildSchema
|
5
|
+
module FindDisplayColumn
|
6
|
+
DISPLAY_NAMES = %w[
|
7
|
+
name
|
8
|
+
full_name
|
9
|
+
fullname
|
10
|
+
last_name
|
11
|
+
lastname
|
12
|
+
first_name
|
13
|
+
firstname
|
14
|
+
fname
|
15
|
+
lname
|
16
|
+
sname
|
17
|
+
phone
|
18
|
+
phone_number
|
19
|
+
email
|
20
|
+
domain
|
21
|
+
phone
|
22
|
+
company
|
23
|
+
filename
|
24
|
+
file_name
|
25
|
+
title
|
26
|
+
url
|
27
|
+
make
|
28
|
+
brand
|
29
|
+
manufacturer
|
30
|
+
model
|
31
|
+
address
|
32
|
+
].freeze
|
33
|
+
|
34
|
+
DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES), Regexp::IGNORECASE)
|
35
|
+
|
36
|
+
module_function
|
37
|
+
|
38
|
+
def call(model)
|
39
|
+
column_names = fetch_column_names(model)
|
40
|
+
|
41
|
+
select_column_name(column_names)
|
42
|
+
end
|
43
|
+
|
44
|
+
def select_column_name(column_names)
|
45
|
+
name = column_names.find { |column_name| column_name.in?(DISPLAY_NAMES) }
|
46
|
+
name ||= column_names.find { |column_name| column_name.match?(DISPLAY_NAME_REGEXP) }
|
47
|
+
|
48
|
+
name
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_column_names(model)
|
52
|
+
model.columns.map do |column|
|
53
|
+
next unless column.type.in?(BuildSchema::SEARCHABLE_COLUMN_TYPES)
|
54
|
+
|
55
|
+
column.name
|
56
|
+
end.compact
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module BuildSchema
|
5
|
+
module LoadFromRails
|
6
|
+
module ColumnAccessTypes
|
7
|
+
ALL = [
|
8
|
+
READ_ONLY = 'read_only',
|
9
|
+
WRITE_ONLY = 'write_only',
|
10
|
+
READ_WRITE = 'read_write',
|
11
|
+
HIDDEN = 'hidden'
|
12
|
+
].freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
COLUMN_NAME_ACCESS_TYPES = {
|
16
|
+
id: ColumnAccessTypes::READ_ONLY,
|
17
|
+
created_at: ColumnAccessTypes::READ_ONLY,
|
18
|
+
updated_at: ColumnAccessTypes::READ_ONLY,
|
19
|
+
deleted_at: ColumnAccessTypes::READ_ONLY
|
20
|
+
}.with_indifferent_access.freeze
|
21
|
+
|
22
|
+
DEFAULT_ACTIONS = [
|
23
|
+
{
|
24
|
+
name: 'create',
|
25
|
+
display_name: 'Create',
|
26
|
+
action_type: 'default',
|
27
|
+
preferences: {},
|
28
|
+
visible: true
|
29
|
+
},
|
30
|
+
{
|
31
|
+
name: 'edit',
|
32
|
+
display_name: 'Edit',
|
33
|
+
action_type: 'default',
|
34
|
+
preferences: {},
|
35
|
+
visible: true
|
36
|
+
},
|
37
|
+
{
|
38
|
+
name: 'remove',
|
39
|
+
display_name: 'Remove',
|
40
|
+
action_type: 'default',
|
41
|
+
preferences: {},
|
42
|
+
visible: true
|
43
|
+
}
|
44
|
+
].freeze
|
45
|
+
|
46
|
+
DEFAULT_TABS = [
|
47
|
+
{
|
48
|
+
name: 'summary',
|
49
|
+
display_name: 'Summary',
|
50
|
+
tab_type: 'default',
|
51
|
+
preferences: {},
|
52
|
+
visible: true
|
53
|
+
}
|
54
|
+
].freeze
|
55
|
+
|
56
|
+
module_function
|
57
|
+
|
58
|
+
def call
|
59
|
+
models.map do |model|
|
60
|
+
build_model_schema(model)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def models
|
65
|
+
Rails.application.eager_load!
|
66
|
+
|
67
|
+
models = load_descendants(ActiveRecord::Base).uniq
|
68
|
+
models = models.reject(&:abstract_class)
|
69
|
+
|
70
|
+
models -= Motor::ApplicationRecord.descendants
|
71
|
+
models -= [ActiveRecord::SchemaMigration] if defined?(ActiveRecord::SchemaMigration)
|
72
|
+
models -= [ActiveStorage::Blob, ActiveStorage::VariantRecord] if defined?(ActiveStorage::Blob)
|
73
|
+
|
74
|
+
models
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_descendants(model)
|
78
|
+
model.descendants + model.descendants.flat_map do |klass|
|
79
|
+
load_descendants(klass)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_model_schema(model)
|
84
|
+
{
|
85
|
+
name: model.name.underscore,
|
86
|
+
slug: Utils.slugify(model),
|
87
|
+
table_name: model.table_name,
|
88
|
+
primary_key: model.primary_key,
|
89
|
+
display_name: model.name.titleize.pluralize,
|
90
|
+
display_column: FindDisplayColumn.call(model),
|
91
|
+
columns: fetch_columns(model),
|
92
|
+
associations: fetch_associations(model),
|
93
|
+
actions: DEFAULT_ACTIONS,
|
94
|
+
tabs: DEFAULT_TABS,
|
95
|
+
visible: true
|
96
|
+
}.with_indifferent_access
|
97
|
+
end
|
98
|
+
|
99
|
+
def fetch_columns(model)
|
100
|
+
default_attrs = model.new.attributes
|
101
|
+
|
102
|
+
model.columns.map do |column|
|
103
|
+
{
|
104
|
+
name: column.name,
|
105
|
+
display_name: column.name.humanize,
|
106
|
+
column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
|
107
|
+
access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
|
108
|
+
default_value: default_attrs[column.name],
|
109
|
+
validators: fetch_validators(model, column.name),
|
110
|
+
virtual: false
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def fetch_associations(model)
|
116
|
+
model.reflections.map do |name, ref|
|
117
|
+
next if ref.polymorphic? && !ref.belongs_to?
|
118
|
+
|
119
|
+
begin
|
120
|
+
ref.klass
|
121
|
+
rescue StandardError
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
next if defined?(ActiveStorage::Blob) && ref.klass == ActiveStorage::Blob
|
126
|
+
|
127
|
+
{
|
128
|
+
name: name,
|
129
|
+
display_name: name.humanize,
|
130
|
+
slug: name.underscore,
|
131
|
+
model_name: ref.klass.name.underscore,
|
132
|
+
model_slug: Utils.slugify(ref.klass),
|
133
|
+
association_type: fetch_association_type(ref),
|
134
|
+
foreign_key: ref.foreign_key,
|
135
|
+
polymorphic: ref.polymorphic?,
|
136
|
+
visible: true
|
137
|
+
}
|
138
|
+
end.compact
|
139
|
+
end
|
140
|
+
|
141
|
+
def fetch_association_type(association)
|
142
|
+
case association.association_class.to_s
|
143
|
+
when 'ActiveRecord::Associations::HasManyAssociation',
|
144
|
+
'ActiveRecord::Associations::HasManyThroughAssociation'
|
145
|
+
'has_many'
|
146
|
+
when 'ActiveRecord::Associations::HasOneAssociation',
|
147
|
+
'ActiveRecord::Associations::HasOneThroughAssociation'
|
148
|
+
'has_one'
|
149
|
+
when 'ActiveRecord::Associations::BelongsToAssociation'
|
150
|
+
'belongs_to'
|
151
|
+
else
|
152
|
+
raise ArgumentError, 'Unknown association type'
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def fetch_validators(model, column_name)
|
157
|
+
model.validators_on(column_name).map do |validator|
|
158
|
+
case validator
|
159
|
+
when ActiveModel::Validations::InclusionValidator
|
160
|
+
{ includes: validator.send(:delimiter) }
|
161
|
+
when ActiveRecord::Validations::PresenceValidator
|
162
|
+
{ required: true }
|
163
|
+
when ActiveModel::Validations::FormatValidator
|
164
|
+
{ format: JsRegex.new(validator.options[:with]).to_h.slice(:source, :options) }
|
165
|
+
when ActiveRecord::Validations::LengthValidator
|
166
|
+
{ length: validator.options }
|
167
|
+
when ActiveModel::Validations::NumericalityValidator
|
168
|
+
{ numeric: validator.options }
|
169
|
+
else
|
170
|
+
next
|
171
|
+
end
|
172
|
+
end.compact
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module BuildSchema
|
5
|
+
module MergeSchemaConfigs
|
6
|
+
RESOURCE_ATTRS = PersistResourceConfigs::RESOURCE_ATTRS
|
7
|
+
COLUMN_DEFAULTS = PersistResourceConfigs::COLUMN_DEFAULTS
|
8
|
+
ACTION_DEFAULTS = PersistResourceConfigs::ACTION_DEFAULTS
|
9
|
+
TAB_DEFAULTS = PersistResourceConfigs::TAB_DEFAULTS
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# @param schema [Array<HashWithIndifferentAccess>]
|
14
|
+
# @return [Array<HashWithIndifferentAccess>]
|
15
|
+
def call(schema)
|
16
|
+
configs = load_configs
|
17
|
+
|
18
|
+
schema.map do |model|
|
19
|
+
merge_model(model, configs.fetch(model[:name], {}))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param model [HashWithIndifferentAccess]
|
24
|
+
# @param configs [HashWithIndifferentAccess]
|
25
|
+
# @return [HashWithIndifferentAccess]
|
26
|
+
def merge_model(model, configs)
|
27
|
+
updated_model = model.merge(configs.slice(*RESOURCE_ATTRS))
|
28
|
+
|
29
|
+
updated_model[:columns] = merge_by_name(
|
30
|
+
model[:columns],
|
31
|
+
configs[:columns],
|
32
|
+
COLUMN_DEFAULTS
|
33
|
+
)
|
34
|
+
|
35
|
+
updated_model[:associations] = merge_by_name(
|
36
|
+
model[:associations],
|
37
|
+
configs[:associations]
|
38
|
+
)
|
39
|
+
|
40
|
+
updated_model[:actions] = merge_by_name(
|
41
|
+
model[:actions],
|
42
|
+
configs[:actions],
|
43
|
+
ACTION_DEFAULTS
|
44
|
+
)
|
45
|
+
|
46
|
+
updated_model[:tabs] = merge_by_name(
|
47
|
+
model[:tabs],
|
48
|
+
configs[:tabs],
|
49
|
+
ACTION_DEFAULTS
|
50
|
+
)
|
51
|
+
|
52
|
+
updated_model
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param defaults [Array<HashWithIndifferentAccess>]
|
56
|
+
# @param configs [Array<HashWithIndifferentAccess>]
|
57
|
+
# @return [Array<HashWithIndifferentAccess>]
|
58
|
+
def merge_by_name(defaults, configs, default_attrs = {})
|
59
|
+
return defaults if configs.blank?
|
60
|
+
|
61
|
+
(defaults.pluck(:name) + configs.pluck(:name)).uniq.map do |name|
|
62
|
+
config_item = configs.find { |e| e[:name] == name } || {}
|
63
|
+
default_item = defaults.find { |e| e[:name] == name } || default_attrs
|
64
|
+
|
65
|
+
default_item.merge(config_item)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [HashWithIndifferentAccess<String, HashWithIndifferentAccess>]
|
70
|
+
def load_configs
|
71
|
+
Motor::Resource.all.each_with_object(HashWithIndifferentAccess.new) do |resource, acc|
|
72
|
+
acc[resource.name] = resource.preferences
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|