motor-admin-cstham8 0.4.35

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +230 -0
  4. data/Rakefile +11 -0
  5. data/app/channels/motor/application_cable/channel.rb +14 -0
  6. data/app/channels/motor/application_cable/connection.rb +27 -0
  7. data/app/channels/motor/notes_channel.rb +9 -0
  8. data/app/channels/motor/notifications_channel.rb +9 -0
  9. data/app/controllers/concerns/motor/current_ability.rb +21 -0
  10. data/app/controllers/concerns/motor/current_user_method.rb +18 -0
  11. data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
  12. data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
  13. data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
  14. data/app/controllers/motor/alerts_controller.rb +82 -0
  15. data/app/controllers/motor/api_base_controller.rb +33 -0
  16. data/app/controllers/motor/api_configs_controller.rb +54 -0
  17. data/app/controllers/motor/application_controller.rb +8 -0
  18. data/app/controllers/motor/assets_controller.rb +43 -0
  19. data/app/controllers/motor/audits_controller.rb +16 -0
  20. data/app/controllers/motor/auth_tokens_controller.rb +36 -0
  21. data/app/controllers/motor/configs_controller.rb +33 -0
  22. data/app/controllers/motor/dashboards_controller.rb +64 -0
  23. data/app/controllers/motor/data_controller.rb +88 -0
  24. data/app/controllers/motor/forms_controller.rb +61 -0
  25. data/app/controllers/motor/icons_controller.rb +22 -0
  26. data/app/controllers/motor/note_tags_controller.rb +13 -0
  27. data/app/controllers/motor/notes_controller.rb +58 -0
  28. data/app/controllers/motor/notifications_controller.rb +33 -0
  29. data/app/controllers/motor/queries_controller.rb +64 -0
  30. data/app/controllers/motor/reminders_controller.rb +38 -0
  31. data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
  32. data/app/controllers/motor/resource_methods_controller.rb +23 -0
  33. data/app/controllers/motor/resources_controller.rb +26 -0
  34. data/app/controllers/motor/run_api_requests_controller.rb +56 -0
  35. data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
  36. data/app/controllers/motor/run_queries_controller.rb +77 -0
  37. data/app/controllers/motor/schema_controller.rb +31 -0
  38. data/app/controllers/motor/send_alerts_controller.rb +26 -0
  39. data/app/controllers/motor/sessions_controller.rb +23 -0
  40. data/app/controllers/motor/slack_conversations_controller.rb +11 -0
  41. data/app/controllers/motor/tags_controller.rb +11 -0
  42. data/app/controllers/motor/ui_controller.rb +51 -0
  43. data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
  44. data/app/jobs/motor/alert_sending_job.rb +13 -0
  45. data/app/jobs/motor/application_job.rb +6 -0
  46. data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
  47. data/app/jobs/motor/notify_reminder_job.rb +9 -0
  48. data/app/mailers/motor/alerts_mailer.rb +39 -0
  49. data/app/mailers/motor/application_mailer.rb +33 -0
  50. data/app/mailers/motor/notifications_mailer.rb +33 -0
  51. data/app/models/motor/alert.rb +30 -0
  52. data/app/models/motor/alert_lock.rb +7 -0
  53. data/app/models/motor/api_config.rb +28 -0
  54. data/app/models/motor/application_record.rb +18 -0
  55. data/app/models/motor/audit.rb +13 -0
  56. data/app/models/motor/config.rb +13 -0
  57. data/app/models/motor/dashboard.rb +26 -0
  58. data/app/models/motor/form.rb +23 -0
  59. data/app/models/motor/note.rb +18 -0
  60. data/app/models/motor/note_tag.rb +7 -0
  61. data/app/models/motor/note_tag_tag.rb +8 -0
  62. data/app/models/motor/notification.rb +14 -0
  63. data/app/models/motor/query.rb +33 -0
  64. data/app/models/motor/reminder.rb +13 -0
  65. data/app/models/motor/resource.rb +15 -0
  66. data/app/models/motor/tag.rb +7 -0
  67. data/app/models/motor/taggable_tag.rb +8 -0
  68. data/app/views/layouts/motor/application.html.erb +17 -0
  69. data/app/views/layouts/motor/mailer.html.erb +72 -0
  70. data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
  71. data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
  72. data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
  73. data/app/views/motor/ui/show.html.erb +1 -0
  74. data/config/locales/el.yml +420 -0
  75. data/config/locales/en.yml +340 -0
  76. data/config/locales/es.yml +420 -0
  77. data/config/locales/ja.yml +340 -0
  78. data/config/locales/pt.yml +416 -0
  79. data/config/routes.rb +65 -0
  80. data/lib/generators/motor/install_generator.rb +24 -0
  81. data/lib/generators/motor/install_notes_generator.rb +22 -0
  82. data/lib/generators/motor/migration.rb +17 -0
  83. data/lib/generators/motor/templates/install.rb +271 -0
  84. data/lib/generators/motor/templates/install_api_configs.rb +86 -0
  85. data/lib/generators/motor/templates/install_notes.rb +83 -0
  86. data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
  87. data/lib/generators/motor/upgrade_generator.rb +43 -0
  88. data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
  89. data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
  90. data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
  91. data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
  92. data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
  93. data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
  94. data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
  95. data/lib/motor/active_record_utils/types.rb +64 -0
  96. data/lib/motor/active_record_utils.rb +45 -0
  97. data/lib/motor/admin.rb +141 -0
  98. data/lib/motor/alerts/persistance.rb +97 -0
  99. data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
  100. data/lib/motor/alerts/scheduler.rb +30 -0
  101. data/lib/motor/alerts/slack_sender.rb +74 -0
  102. data/lib/motor/alerts.rb +52 -0
  103. data/lib/motor/api_configs.rb +41 -0
  104. data/lib/motor/api_query/apply_scope.rb +44 -0
  105. data/lib/motor/api_query/build_json.rb +171 -0
  106. data/lib/motor/api_query/build_meta.rb +20 -0
  107. data/lib/motor/api_query/filter.rb +125 -0
  108. data/lib/motor/api_query/paginate.rb +19 -0
  109. data/lib/motor/api_query/search.rb +60 -0
  110. data/lib/motor/api_query/sort.rb +64 -0
  111. data/lib/motor/api_query.rb +24 -0
  112. data/lib/motor/assets.rb +62 -0
  113. data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
  114. data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
  115. data/lib/motor/build_schema/apply_permissions.rb +64 -0
  116. data/lib/motor/build_schema/defaults.rb +66 -0
  117. data/lib/motor/build_schema/find_display_column.rb +65 -0
  118. data/lib/motor/build_schema/find_icon.rb +135 -0
  119. data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
  120. data/lib/motor/build_schema/load_from_rails.rb +361 -0
  121. data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
  122. data/lib/motor/build_schema/reorder_schema.rb +88 -0
  123. data/lib/motor/build_schema/utils.rb +31 -0
  124. data/lib/motor/build_schema.rb +125 -0
  125. data/lib/motor/cancan_utils/ability_patch.rb +31 -0
  126. data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
  127. data/lib/motor/cancan_utils.rb +9 -0
  128. data/lib/motor/configs/build_configs_hash.rb +90 -0
  129. data/lib/motor/configs/build_ui_app_tag.rb +177 -0
  130. data/lib/motor/configs/load_from_cache.rb +110 -0
  131. data/lib/motor/configs/sync_from_file.rb +35 -0
  132. data/lib/motor/configs/sync_from_hash.rb +159 -0
  133. data/lib/motor/configs/sync_middleware.rb +72 -0
  134. data/lib/motor/configs/sync_with_remote.rb +47 -0
  135. data/lib/motor/configs/write_to_file.rb +36 -0
  136. data/lib/motor/configs.rb +39 -0
  137. data/lib/motor/dashboards/persistance.rb +73 -0
  138. data/lib/motor/dashboards.rb +8 -0
  139. data/lib/motor/forms/persistance.rb +93 -0
  140. data/lib/motor/forms.rb +8 -0
  141. data/lib/motor/hash_serializer.rb +21 -0
  142. data/lib/motor/net_http_utils.rb +50 -0
  143. data/lib/motor/notes/notify_mentions.rb +71 -0
  144. data/lib/motor/notes/notify_reminder.rb +48 -0
  145. data/lib/motor/notes/persist.rb +36 -0
  146. data/lib/motor/notes/reminders_scheduler.rb +39 -0
  147. data/lib/motor/notes/tags.rb +34 -0
  148. data/lib/motor/notes.rb +12 -0
  149. data/lib/motor/queries/persistance.rb +90 -0
  150. data/lib/motor/queries/postgresql_exec_query.rb +28 -0
  151. data/lib/motor/queries/render_sql_template.rb +61 -0
  152. data/lib/motor/queries/run_query.rb +289 -0
  153. data/lib/motor/queries.rb +11 -0
  154. data/lib/motor/railtie.rb +11 -0
  155. data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
  156. data/lib/motor/resources/fetch_configured_model.rb +269 -0
  157. data/lib/motor/resources/persist_configs.rb +232 -0
  158. data/lib/motor/resources.rb +19 -0
  159. data/lib/motor/slack/client.rb +62 -0
  160. data/lib/motor/slack.rb +16 -0
  161. data/lib/motor/tags.rb +32 -0
  162. data/lib/motor/tasks/motor.rake +54 -0
  163. data/lib/motor/version.rb +5 -0
  164. data/lib/motor-admin-cstham8.rb +3 -0
  165. data/lib/motor.rb +87 -0
  166. data/ui/dist/manifest.json +1990 -0
  167. metadata +303 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module FindDisplayColumn
6
+ DISPLAY_NAMES = Set.new(
7
+ %w[
8
+ name
9
+ full_name
10
+ fullname
11
+ last_name
12
+ lastname
13
+ first_name
14
+ firstname
15
+ fname
16
+ lname
17
+ sname
18
+ company
19
+ domain
20
+ website
21
+ title
22
+ phone
23
+ phone_number
24
+ email
25
+ phone
26
+ filename
27
+ file_name
28
+ url
29
+ make
30
+ brand
31
+ manufacturer
32
+ model
33
+ address
34
+ code
35
+ ]
36
+ ).freeze
37
+
38
+ DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES.to_a).source, Regexp::IGNORECASE)
39
+
40
+ module_function
41
+
42
+ def call(model)
43
+ column_names = fetch_column_names(model)
44
+
45
+ select_column_name(column_names)
46
+ end
47
+
48
+ def select_column_name(column_names)
49
+ name = column_names.find { |column_name| column_name.in?(DISPLAY_NAMES) }
50
+ name ||= column_names.find { |column_name| column_name.match?(DISPLAY_NAME_REGEXP) }
51
+
52
+ name
53
+ end
54
+
55
+ def fetch_column_names(model)
56
+ model.columns.map do |column|
57
+ next unless column.type.in?(BuildSchema::SEARCHABLE_COLUMN_TYPES)
58
+ next if column.try(:array?)
59
+
60
+ column.name
61
+ end.compact
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module FindIcon
5
+ ICONS_MAP = {
6
+ 'audit' => 'history',
7
+ 'block' => 'ban',
8
+ 'blocked' => 'ban',
9
+ 'blacklisted' => 'ban',
10
+ 'blocklisted' => 'ban',
11
+ 'banned' => 'ban',
12
+ 'website' => 'world',
13
+ 'location' => 'gps',
14
+ 'photo' => 'photo',
15
+ 'image' => 'photo',
16
+ 'screenshot' => 'photo',
17
+ 'picture' => 'photo',
18
+ 'video' => 'video',
19
+ 'file' => 'file',
20
+ 'pdf' => 'file-text',
21
+ 'ad' => 'ad',
22
+ 'shop' => 'building-store',
23
+ 'store' => 'building-store',
24
+ 'seller' => 'building-store',
25
+ 'app' => 'apps',
26
+ 'book' => 'book',
27
+ 'movie' => 'movie',
28
+ 'country' => 'map',
29
+ 'api' => 'tool',
30
+ 'config' => 'tool',
31
+ 'key' => 'key',
32
+ 'phone' => 'phone',
33
+ 'call' => 'phone',
34
+ 'cell' => 'phone',
35
+ 'token' => 'key',
36
+ 'secret' => 'lock',
37
+ 'automation' => 'manual-gearbox',
38
+ 'workflow' => 'manual-gearbox',
39
+ 'relationship' => 'hierarchy',
40
+ 'person' => 'user',
41
+ 'people' => 'users',
42
+ 'mail' => 'mail',
43
+ 'user' => 'user',
44
+ 'rating' => 'star',
45
+ 'article' => 'blockquote',
46
+ 'podcast' => 'headphones',
47
+ 'payment' => 'cash',
48
+ 'log' => 'history',
49
+ 'history' => 'history',
50
+ 'rule' => 'manual-gearbox',
51
+ 'tracking' => 'zoom-question',
52
+ 'github' => 'brand-github',
53
+ 'tag' => 'hash',
54
+ 'category' => 'hash',
55
+ 'label' => 'hash',
56
+ 'twitter' => 'brand-twitter',
57
+ 'facebook' => 'brand-facebook',
58
+ 'google' => 'brand-google',
59
+ 'renewal' => 'repeat',
60
+ 'sales' => 'report-money',
61
+ 'telegram' => 'brand-telegram',
62
+ 'whatsapp' => 'brand-whatsapp',
63
+ 'survey' => 'checkbox',
64
+ 'subcategory' => 'hash',
65
+ 'type' => 'hash',
66
+ 'model' => 'hash',
67
+ 'taxon' => 'hash',
68
+ 'affiliate' => 'affiliate',
69
+ 'chat' => 'message-circle',
70
+ 'message' => 'messages',
71
+ 'poll' => 'messages',
72
+ 'feedpack' => 'messages',
73
+ 'attachment' => 'paperclip',
74
+ 'certificate' => 'certificate',
75
+ 'approval' => 'certificate',
76
+ 'bank' => 'building-bank',
77
+ 'bridge' => 'building-bridge',
78
+ 'car' => 'car',
79
+ 'vehicle' => 'car',
80
+ 'analytic' => 'device-analytics',
81
+ 'stat' => 'device-analytics',
82
+ 'statistic' => 'device-analytics',
83
+ 'link' => 'link',
84
+ 'slug' => 'link',
85
+ 'city' => 'building-skyscraper',
86
+ 'region' => 'map',
87
+ 'district' => 'building-community',
88
+ 'community' => 'building-community',
89
+ 'activity' => 'activity',
90
+ 'invoice' => 'file-invoice',
91
+ 'settlement' => 'file-invoice',
92
+ 'state' => 'map',
93
+ 'note' => 'note',
94
+ 'order' => 'truck-delivery',
95
+ 'lead' => 'building-lighthouse',
96
+ 'error' => 'file-alert',
97
+ 'page' => 'brand-pagekit',
98
+ 'date' => 'calendar-event',
99
+ 'customer' => 'users',
100
+ 'client' => 'users',
101
+ 'ticket' => 'ticket',
102
+ 'contact' => 'users',
103
+ 'member' => 'users',
104
+ 'admin' => 'user-check',
105
+ 'notification' => 'bell',
106
+ 'alert' => 'bell',
107
+ 'team' => 'users',
108
+ 'folder' => 'folder',
109
+ 'subscriber' => 'user-plus',
110
+ 'product' => 'building-store',
111
+ 'html' => 'code',
112
+ 'stripe' => 'brand-stripe',
113
+ 'email' => 'mail',
114
+ 'status' => 'hash'
115
+ }.freeze
116
+
117
+ DEFAULT_ICON = BuildSchema::DEFAULT_ICON
118
+
119
+ module_function
120
+
121
+ def call(text)
122
+ text = text.underscore
123
+ icon = ICONS_MAP[text] || ICONS_MAP[text.singularize]
124
+
125
+ icon ||=
126
+ text.split(%r{[\s_/]}).reverse.reduce(nil) do |_acc, word|
127
+ found_icon = ICONS_MAP[word] || ICONS_MAP[word.singularize]
128
+
129
+ break found_icon if found_icon
130
+ end
131
+
132
+ icon || DEFAULT_ICON
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module FindSearchableColumns
6
+ SELECT_COLUMNS_AMOUNT = 2
7
+ COLUMN_TYPES = BuildSchema::SEARCHABLE_COLUMN_TYPES
8
+
9
+ module_function
10
+
11
+ def call(model)
12
+ columns = find_searchable_columns(model)
13
+
14
+ selected_columns =
15
+ columns.grep(BuildSchema::FindDisplayColumn::DISPLAY_NAME_REGEXP).presence
16
+
17
+ selected_columns ||= columns.first(SELECT_COLUMNS_AMOUNT)
18
+
19
+ ([model.primary_key] + selected_columns).compact.sort
20
+ end
21
+
22
+ def find_searchable_columns(model)
23
+ model.columns.map do |column|
24
+ next unless column.type.in?(COLUMN_TYPES)
25
+ next if column.try(:array?)
26
+ next if model.validators_on(column.name).any?(ActiveModel::Validations::InclusionValidator)
27
+
28
+ column.name
29
+ end.compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module LoadFromRails
6
+ MUTEX = Mutex.new
7
+ UNIFIED_TYPES = ActiveRecordUtils::Types::UNIFIED_TYPES
8
+
9
+ I18N_SCOPES_KEY = 'activerecord.scopes'
10
+
11
+ ACTION_TEXT_REFLECTION_PREFIX = 'rich_text_'
12
+ ACTION_TEXT_COLUMN_SUFFIX = '_body'
13
+ ACTION_TEXT_SCOPE_PREFIX = 'with_rich_text_'
14
+ ACTIVE_STORAGE_SCOPE_PREFIX = 'with_attached_'
15
+
16
+ DEFAULT_CURRENCY_FORMAT_HASH = {
17
+ currency: 'USD',
18
+ currency_base: 'units'
19
+ }.freeze
20
+
21
+ module_function
22
+
23
+ def call
24
+ models.map do |model|
25
+ model = Object.const_get(model.name)
26
+
27
+ next unless model.table_exists?
28
+
29
+ schema = build_model_schema(model)
30
+
31
+ if model.respond_to?(:devise_modules)
32
+ Motor::BuildSchema::AdjustDeviseModelSchema.call(schema, model.devise_modules)
33
+ end
34
+
35
+ schema
36
+ rescue StandardError, NotImplementedError => e
37
+ Rails.logger.error(e)
38
+
39
+ next
40
+ end.compact.uniq
41
+ end
42
+
43
+ def models
44
+ eager_load_models!
45
+
46
+ models = ActiveRecord::Base.descendants.reject { |k| k.abstract_class || k.anonymous? }
47
+
48
+ models -= Motor::ApplicationRecord.descendants
49
+ models -= [Motor::Audit]
50
+ models -= [ActiveRecord::SchemaMigration] if defined?(ActiveRecord::SchemaMigration)
51
+ models -= [ActiveRecord::InternalMetadata] if defined?(ActiveRecord::InternalMetadata)
52
+ models -= [ActiveStorage::Blob] if defined?(ActiveStorage::Blob)
53
+ models -= [ActionText::RichText] if defined?(ActionText::RichText)
54
+ models -= [ActiveStorage::VariantRecord] if defined?(ActiveStorage::VariantRecord)
55
+
56
+ models
57
+ end
58
+
59
+ # rubocop:disable Metrics/MethodLength
60
+ def build_model_schema(model)
61
+ model_name = model.name
62
+
63
+ return Motor::BuildSchema::ActiveStorageAttachmentSchema.call if model_name == 'ActiveStorage::Attachment'
64
+
65
+ {
66
+ name: model_name.underscore,
67
+ slug: Utils.slugify(model),
68
+ table_name: model.table_name,
69
+ class_name: model_name,
70
+ primary_key: model.primary_key,
71
+ display_name: model.model_name.human(count: :many, default: model_name.titleize.pluralize),
72
+ display_column: FindDisplayColumn.call(model),
73
+ columns: fetch_columns(model),
74
+ associations: fetch_associations(model),
75
+ icon: Motor::FindIcon.call(model_name),
76
+ scopes: fetch_scopes(model),
77
+ actions: BuildSchema::Defaults.actions,
78
+ tabs: BuildSchema::Defaults.tabs,
79
+ searchable_columns: FindSearchableColumns.call(model),
80
+ custom_sql: nil,
81
+ visible: true,
82
+ display_primary_key: true,
83
+ preferences: {
84
+ display_as: 'table'
85
+ }
86
+ }.with_indifferent_access
87
+ end
88
+ # rubocop:enable Metrics/MethodLength
89
+
90
+ def fetch_scopes(model)
91
+ model.defined_scopes.map do |scope_name|
92
+ scope_name = scope_name.to_s
93
+
94
+ next if scope_name.starts_with?(ACTIVE_STORAGE_SCOPE_PREFIX)
95
+ next if scope_name.starts_with?(ACTION_TEXT_SCOPE_PREFIX)
96
+
97
+ {
98
+ name: scope_name,
99
+ display_name: I18n.t(scope_name,
100
+ scope: [I18N_SCOPES_KEY, model.name.underscore].join('.'),
101
+ default: scope_name.humanize),
102
+ scope_type: DEFAULT_TYPE,
103
+ visible: true,
104
+ preferences: {}
105
+ }
106
+ end.compact
107
+ end
108
+
109
+ def fetch_columns(model)
110
+ default_attrs = model.new.attributes
111
+
112
+ reference_columns = fetch_reference_columns(model)
113
+
114
+ table_columns =
115
+ model.columns.map do |column|
116
+ next if reference_columns.find { |c| c[:name] == column.name }
117
+
118
+ build_table_column(column, model, default_attrs)
119
+ end.compact
120
+
121
+ reference_columns + table_columns
122
+ end
123
+
124
+ def build_table_column(column, model, default_attrs)
125
+ access_type =
126
+ if model.primary_key == column.name
127
+ ColumnAccessTypes::READ_ONLY
128
+ else
129
+ COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE)
130
+ end
131
+
132
+ {
133
+ name: column.name,
134
+ display_name: Utils.humanize_column_name(model.human_attribute_name(column.name)),
135
+ column_type: fetch_column_type(column, model),
136
+ column_source: ColumnSources::TABLE,
137
+ is_array: column.try(:array?),
138
+ access_type: access_type,
139
+ default_value: default_attrs[column.name],
140
+ validators: fetch_validators(model, column.name),
141
+ reference: nil,
142
+ format: fetch_format_hash(column, model),
143
+ description: '',
144
+ virtual: false
145
+ }
146
+ end
147
+
148
+ def fetch_format_hash(column, model)
149
+ return DEFAULT_CURRENCY_FORMAT_HASH if column.name == 'price'
150
+
151
+ inclusion_validator, = model.validators_on(column.name).grep(ActiveModel::Validations::InclusionValidator)
152
+
153
+ return { select_options: inclusion_validator.send(:delimiter) } if inclusion_validator
154
+
155
+ enum = model.defined_enums[column.name]
156
+
157
+ return { select_options: enum.keys } if enum
158
+
159
+ return {} if column.name == 'year'
160
+
161
+ return { number_format: true } if !column.name == 'id' &&
162
+ !column.name.match?(/_(?:id|year)\z/) &&
163
+ %i[integer float].include?(column.type)
164
+
165
+ {}
166
+ end
167
+
168
+ def fetch_column_type(column, model)
169
+ return ColumnTypes::CURRENCY if column.name == 'price'
170
+ return ColumnTypes::COLOR if %w[hex color].include?(column.name)
171
+ return ColumnTypes::TAG if model.defined_enums[column.name]
172
+ return ColumnTypes::TAG if model.validators_on(column.name).any?(ActiveModel::Validations::InclusionValidator)
173
+ return ColumnTypes::RICHTEXT if column.name.ends_with?('_html')
174
+ return ColumnTypes::COLOR if column.name.match?(/_(color|hex)\z/)
175
+
176
+ UNIFIED_TYPES[column.type.to_s] || column.type.to_s
177
+ end
178
+
179
+ def fetch_reference_columns(model)
180
+ default_attrs = model.new.attributes
181
+
182
+ model.reflections.map do |name, ref|
183
+ next if !ref.has_one? && !ref.belongs_to?
184
+
185
+ unless ref.polymorphic?
186
+ begin
187
+ next if ref.klass.name == 'ActiveStorage::Blob'
188
+ rescue StandardError => e
189
+ Rails.logger.error(e)
190
+
191
+ next
192
+ end
193
+ end
194
+
195
+ build_reflection_column(name, model, ref, default_attrs)
196
+ end.compact
197
+ end
198
+
199
+ # rubocop:disable Metrics/AbcSize
200
+ def build_reflection_column(name, model, ref, default_attrs)
201
+ if !ref.polymorphic? && ref.klass.name == 'ActionText::RichText'
202
+ return build_action_text_column(name, model, ref)
203
+ end
204
+
205
+ column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
206
+ is_attachment = !ref.polymorphic? && ref.klass.name == 'ActiveStorage::Attachment'
207
+ access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
208
+ column_type = is_attachment ? ColumnTypes::FILE : ColumnTypes::REFERENCE
209
+ column_source = model.columns_hash[column_name] ? ColumnSources::TABLE : ColumnSources::REFLECTION
210
+
211
+ {
212
+ name: column_name,
213
+ display_name: model.human_attribute_name(name),
214
+ column_type: column_type,
215
+ column_source: column_source,
216
+ access_type: access_type,
217
+ default_value: default_attrs[column_name],
218
+ validators: fetch_validators(model, column_name, ref),
219
+ format: {},
220
+ reference: build_reference(model, name, ref),
221
+ virtual: false
222
+ }
223
+ end
224
+ # rubocop:enable Metrics/AbcSize
225
+
226
+ def build_action_text_column(name, model, ref)
227
+ name = name.delete_prefix(ACTION_TEXT_REFLECTION_PREFIX)
228
+
229
+ {
230
+ name: name + ACTION_TEXT_COLUMN_SUFFIX,
231
+ display_name: model.human_attribute_name(name),
232
+ column_type: ColumnTypes::RICHTEXT,
233
+ column_source: ColumnSources::REFLECTION,
234
+ access_type: ColumnAccessTypes::READ_WRITE,
235
+ default_value: '',
236
+ validators: fetch_validators(model, name, ref),
237
+ format: {},
238
+ reference: nil,
239
+ virtual: true
240
+ }
241
+ end
242
+
243
+ def build_reference(model, name, reflection)
244
+ primary_key = reflection.polymorphic? ? 'id' : reflection.join_primary_key
245
+ foreign_key = reflection.join_foreign_key
246
+
247
+ primary_key, foreign_key = foreign_key, primary_key if reflection.has_one?
248
+
249
+ {
250
+ name: name,
251
+ display_name: model.human_attribute_name(name),
252
+ model_name: reflection.polymorphic? ? nil : reflection.klass.name.underscore,
253
+ reference_type: reflection.belongs_to? ? 'belongs_to' : 'has_one',
254
+ foreign_key: foreign_key,
255
+ primary_key: primary_key,
256
+ options: reflection.options.slice(:through, :source),
257
+ polymorphic: reflection.polymorphic?,
258
+ virtual: false
259
+ }
260
+ end
261
+
262
+ def fetch_associations(model)
263
+ model.reflections.map do |name, ref|
264
+ next if ref.has_one? || ref.belongs_to?
265
+ next unless valid_reflection?(ref)
266
+
267
+ model_class = ref.klass
268
+
269
+ next if model_class.name == 'ActiveStorage::Blob'
270
+
271
+ {
272
+ name: name,
273
+ display_name: model.human_attribute_name(name),
274
+ slug: name.underscore,
275
+ model_name: model_class.name.underscore,
276
+ foreign_key: ref.join_primary_key,
277
+ primary_key: ref.join_foreign_key,
278
+ polymorphic: ref.options[:as].present?,
279
+ icon: Motor::FindIcon.call(name),
280
+ options: ref.options.slice(:through, :source),
281
+ virtual: false,
282
+ visible: true
283
+ }
284
+ end.compact
285
+ end
286
+
287
+ def fetch_validators(model, column_name, reflection = nil)
288
+ validators =
289
+ if reflection&.belongs_to? && !reflection.options[:optional]
290
+ [{ required: true }]
291
+ else
292
+ []
293
+ end
294
+
295
+ enum = model.defined_enums[column_name]
296
+
297
+ validators += [{ includes: enum.keys }] if enum
298
+
299
+ validators += model.validators_on(column_name).map do |validator|
300
+ next if validator.options[:if] || validator.options[:unless]
301
+
302
+ build_validator_hash(validator)
303
+ end.compact
304
+
305
+ validators.uniq
306
+ end
307
+
308
+ def build_validator_hash(validator)
309
+ options = validator.options.reject { |_, v| v.is_a?(Proc) || v.is_a?(Symbol) }
310
+
311
+ case validator
312
+ when ActiveModel::Validations::InclusionValidator
313
+ { includes: validator.send(:delimiter) }
314
+ when ActiveRecord::Validations::PresenceValidator
315
+ { required: true }
316
+ when ActiveRecord::Validations::LengthValidator
317
+ { length: normalize_length_validation_options(options) }
318
+ when ActiveModel::Validations::NumericalityValidator
319
+ { numeric: options }
320
+ end
321
+ end
322
+
323
+ def normalize_length_validation_options(options)
324
+ return options if options[:in].blank?
325
+
326
+ in_range = options[:in]
327
+
328
+ options.merge(in: in_range.minmax)
329
+ end
330
+
331
+ def valid_reflection?(reflection)
332
+ reflection.klass
333
+ reflection.foreign_key
334
+
335
+ true
336
+ rescue StandardError => e
337
+ Rails.logger.error(e)
338
+
339
+ false
340
+ end
341
+
342
+ def eager_load_models!
343
+ MUTEX.synchronize do
344
+ if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
345
+ Zeitwerk::Loader.eager_load_all
346
+ else
347
+ Rails.application.eager_load!
348
+ end
349
+
350
+ ActiveRecord::Base.descendants.each do |model|
351
+ model.reflections.each_value do |ref|
352
+ ref.klass
353
+ rescue StandardError
354
+ next
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end