cm-admin 0.7.8 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +22 -5
  3. data/app/assets/images/image_not_available.png +0 -0
  4. data/app/assets/stylesheets/cm_admin/cm_admin.css.scss +1 -0
  5. data/app/assets/stylesheets/cm_admin/pages/import_page.scss +125 -0
  6. data/app/controllers/cm_admin/resource_controller.rb +19 -0
  7. data/app/helpers/cm_admin/application_helper.rb +41 -0
  8. data/app/jobs/file_import_processor_job.rb +38 -0
  9. data/app/models/concerns/cm_admin/file_import.rb +40 -0
  10. data/app/models/file_import.rb +27 -0
  11. data/app/policies/cm_admin/file_import_policy.rb +11 -0
  12. data/app/views/cm_admin/main/_actions_dropdown.html.slim +3 -15
  13. data/app/views/cm_admin/main/_associated_table.html.slim +2 -0
  14. data/app/views/cm_admin/main/_member_custom_action_modal.html.slim +13 -0
  15. data/app/views/cm_admin/main/_table.html.slim +2 -0
  16. data/app/views/cm_admin/main/_top_navbar.html.slim +2 -0
  17. data/app/views/cm_admin/main/import_form.html.slim +35 -0
  18. data/cm_admin.gemspec +1 -0
  19. data/config/routes.rb +6 -0
  20. data/lib/cm_admin/model.rb +7 -1
  21. data/lib/cm_admin/models/column.rb +3 -1
  22. data/lib/cm_admin/models/field.rb +4 -2
  23. data/lib/cm_admin/models/form_field.rb +2 -1
  24. data/lib/cm_admin/models/importer.rb +17 -0
  25. data/lib/cm_admin/version.rb +1 -1
  26. data/lib/cm_admin/view_helpers/field_display_helper.rb +8 -0
  27. data/lib/cm_admin/view_helpers/form_field_helper.rb +2 -2
  28. data/lib/cm_admin.rb +2 -1
  29. data/lib/generators/cm_admin/install_generator.rb +2 -0
  30. data/lib/generators/cm_admin/templates/cm_admin_initializer.rb +1 -1
  31. data/tmp/cache/webpacker/last-compilation-digest-development +1 -1
  32. metadata +28 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a03ed809475bd54b0365e020bb5d3ff1be6a2c16d2e2a2bf82b175af9ee26b8
4
- data.tar.gz: 454ea27724b9f6e8c6297a81af181ce9e365fba0bbd777c68c9c1fdb6d5f47d0
3
+ metadata.gz: 5fa47a489bd6b44db00d91cb7631c5491646fd00148ba756b87208f1218ee705
4
+ data.tar.gz: 5a61fd4d0b01637fbf16bd0a443b912929d12993863a936bd329682433ce54f6
5
5
  SHA512:
6
- metadata.gz: f6b7f5d92c8e26d67b3587fe3c1e0b674bf3bb49aca01d958495f1093b369ab1993383eda2ae744587dea723a8534a7bc3d4d51ef94cd0f6c9aa35bc2186525a
7
- data.tar.gz: 2ccc66f2c64f05e584a4a33da9b0117868272d039e984e8822a90c6957fa48c2654ca3f9ad8b1a2a631a87f50bcd2c761de5d84e4f50b30915b1ad24f2c291ec
6
+ metadata.gz: 708568975092346a45370f46c3b7119feb5a4c4bc43a33ec61a30a3966da7ed8ec13d1a9d91e3a2120f2d5a462f18cb5827a9deaed6fc84bc451b0740f80187d
7
+ data.tar.gz: fc2fba169f614e1d48a11cec08135a12db4e20eb0dcfb6eada41a810bbd7bbeb40b046512c472232542670da2e2c301db58b3263c37fe05de791c3e2b15d46a8
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cm-admin (0.7.7)
4
+ cm-admin (0.8.0)
5
5
  caxlsx_rails
6
6
  cocoon (~> 1.2.15)
7
+ csv-importer (~> 0.8.2)
7
8
  local_time (~> 2.1.0)
8
9
  pagy (~> 4.11.0)
9
10
  pundit (~> 2.2.0)
@@ -32,6 +33,10 @@ GEM
32
33
  minitest (>= 5.1)
33
34
  tzinfo (~> 2.0)
34
35
  ast (2.4.2)
36
+ axiom-types (0.1.1)
37
+ descendants_tracker (~> 0.0.4)
38
+ ice_nine (~> 0.11.0)
39
+ thread_safe (~> 0.3, >= 0.3.1)
35
40
  builder (3.2.4)
36
41
  caxlsx (3.2.0)
37
42
  htmlentities (~> 4.3, >= 4.3.4)
@@ -42,22 +47,29 @@ GEM
42
47
  actionpack (>= 3.1)
43
48
  caxlsx (>= 3.0)
44
49
  cocoon (1.2.15)
50
+ coercible (1.0.0)
51
+ descendants_tracker (~> 0.0.1)
45
52
  concurrent-ruby (1.1.10)
46
53
  crass (1.0.6)
54
+ csv-importer (0.8.2)
55
+ virtus
56
+ descendants_tracker (0.0.4)
57
+ thread_safe (~> 0.3, >= 0.3.1)
47
58
  diff-lcs (1.4.4)
48
- erubi (1.10.0)
59
+ erubi (1.11.0)
49
60
  htmlentities (4.3.4)
50
61
  i18n (1.11.0)
51
62
  concurrent-ruby (~> 1.0)
63
+ ice_nine (0.11.2)
52
64
  local_time (2.1.0)
53
- loofah (2.18.0)
65
+ loofah (2.19.0)
54
66
  crass (~> 1.0.2)
55
67
  nokogiri (>= 1.5.9)
56
68
  marcel (1.0.2)
57
69
  method_source (1.0.0)
58
70
  mini_portile2 (2.8.0)
59
71
  minitest (5.16.2)
60
- nokogiri (1.13.7)
72
+ nokogiri (1.13.8)
61
73
  mini_portile2 (~> 2.8.0)
62
74
  racc (~> 1.4)
63
75
  pagy (4.11.0)
@@ -68,7 +80,7 @@ GEM
68
80
  activesupport (>= 3.0.0)
69
81
  racc (1.6.0)
70
82
  rack (2.2.4)
71
- rack-proxy (0.7.2)
83
+ rack-proxy (0.7.4)
72
84
  rack
73
85
  rack-test (2.0.2)
74
86
  rack (>= 1.3)
@@ -120,10 +132,15 @@ GEM
120
132
  tilt (>= 2.0.6, < 2.1)
121
133
  temple (0.8.2)
122
134
  thor (1.2.1)
135
+ thread_safe (0.3.6)
123
136
  tilt (2.0.10)
124
137
  tzinfo (2.0.4)
125
138
  concurrent-ruby (~> 1.0)
126
139
  unicode-display_width (2.1.0)
140
+ virtus (2.0.0)
141
+ axiom-types (~> 0.1)
142
+ coercible (~> 1.0)
143
+ descendants_tracker (~> 0.0, >= 0.0.3)
127
144
  webpacker (5.4.3)
128
145
  activesupport (>= 5.2)
129
146
  rack-proxy (>= 0.6.1)
@@ -10,6 +10,7 @@
10
10
  * files in this directory. Styles in this file should be added after the last require_* statement.
11
11
  * It is generally better to create a new file per style scope.
12
12
  *
13
+ *= require 'cm_admin/pages/import_page'
13
14
  *= require 'cm_admin/base/table'
14
15
  *= require 'cm_admin/base/navbar'
15
16
  *= require 'cm_admin/base/sidebar'
@@ -0,0 +1,125 @@
1
+ @import "../helpers/index.scss";
2
+
3
+ @function size($size) {
4
+ @return map-get($font-size, $size);
5
+ }
6
+
7
+ @function weight($weight) {
8
+ @return map-get($font-weight, $weight);
9
+ }
10
+
11
+ .import-page {
12
+ &__header {
13
+ min-height: 110px;
14
+ padding: 24px;
15
+ background: $white;
16
+ .header-title {
17
+ @include font($size: size(24), $color: $primary-text-clr, $weight: bold);
18
+ font-family: $primary-font;
19
+ line-height: 32px;
20
+ margin-bottom: 8px;
21
+ }
22
+ .header-description {
23
+ @include font($size: size(14), $color: $primary-text-clr);
24
+ font-family: $primary-font;
25
+ line-height: 22px;
26
+ margin-bottom: 0;
27
+ }
28
+ }
29
+ &__body {
30
+ padding: 16px 24px;
31
+ .body-title {
32
+ @include font($size: size(16), $color: $primary-text-clr, $weight: bold);
33
+ font-family: $primary-font;
34
+ line-height: 24px;
35
+ margin-bottom: 16px;
36
+ }
37
+
38
+ .success-card {
39
+ max-width: 752px;
40
+ margin-top: 24px;
41
+ padding: 24px;
42
+ background: $green-lightest-clr;
43
+ border-radius: $radius-4;
44
+ .success-title {
45
+ @include font($size: size(12), $color: $green-regular-clr, $weight: bold);
46
+ font-family: $primary-font;
47
+ margin-bottom: 16px;
48
+ text-transform: uppercase;
49
+ }
50
+ .success-msg {
51
+ @include font($size: size(16), $weight: bold);
52
+ font-family: $primary-font;
53
+ line-height: 24px;
54
+ margin-bottom: 8px;
55
+ }
56
+ .success-msg-info {
57
+ @include font($size: size(14));
58
+ font-family: $primary-font;
59
+ line-height: 22px;
60
+ margin-bottom: 0;
61
+ }
62
+ }
63
+
64
+ .actions-wrapper {
65
+ margin-top: 32px;
66
+ button {
67
+ padding: 5px 10px;
68
+ }
69
+ }
70
+
71
+ //form UI
72
+ .csv-import-form {
73
+ .form-card {
74
+ max-width: 752px;
75
+ margin-bottom: 32px;
76
+ padding: 24px;
77
+ background: $white;
78
+ border: 1px solid $grey-light-clr;
79
+ border-radius: $radius-4;
80
+ }
81
+ .note-card {
82
+ width: 100%;
83
+ margin-top: 24px;
84
+ padding: 24px;
85
+ background: $blue-lightest-clr;
86
+ border-radius: $radius-4;
87
+ .note-title {
88
+ @include font($size: size(12), $color: $blue-regular-clr, $weight: bold);
89
+ font-family: $primary-font;
90
+ margin-bottom: 16px;
91
+ text-transform: uppercase;
92
+ }
93
+ .steps-wrapper {
94
+ .steps-title {
95
+ @include font($size: size(16), $weight: bold);
96
+ font-family: $primary-font;
97
+ line-height: 24px;
98
+ margin-bottom: 8px;
99
+ }
100
+ .steps-list {
101
+ margin-bottom: 0;
102
+ padding-inline-start: 16px;
103
+ li {
104
+ @include font($size: size(14));
105
+ font-family: $primary-font;
106
+ line-height: 22px;
107
+ margin-bottom: 8px;
108
+ &:nth-last-child(1) {
109
+ margin-bottom: 0;
110
+ }
111
+ a {
112
+ color: $brand-color;
113
+ text-decoration: underline !important;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ .import-btn {
120
+ padding: 5px 10px;
121
+ }
122
+ }
123
+
124
+ }
125
+ }
@@ -71,6 +71,25 @@ module CmAdmin
71
71
  end
72
72
  end
73
73
 
74
+ def import
75
+ @model = Model.find_by({name: controller_name.titleize})
76
+ allowed_params = params.permit(file_import: [:associated_model_name, :import_file]).to_h
77
+ file_import = ::FileImport.new(allowed_params[:file_import])
78
+ file_import.added_by = Current.user
79
+ respond_to do |format|
80
+ if file_import.save!
81
+ format.html { redirect_back fallback_location: cm_admin.send("#{@model.name.underscore}_index_path"), notice: "Your import is successfully queued." }
82
+ end
83
+ end
84
+ end
85
+
86
+ def import_form
87
+ @model = Model.find_by({name: controller_name.titleize})
88
+ respond_to do |format|
89
+ format.html { render '/cm_admin/main/import_form' }
90
+ end
91
+ end
92
+
74
93
  def cm_custom_method(params)
75
94
  scoped_model = "CmAdmin::#{@model.name}Policy::Scope".constantize.new(Current.user, @model.name.constantize).resolve
76
95
  resource_identifier
@@ -24,5 +24,46 @@ module CmAdmin
24
24
  return action_name.to_sym
25
25
  end
26
26
  end
27
+
28
+ def formatted_error_message(model_name, field_name)
29
+ invalid_rows = model_name.send(field_name)
30
+ if invalid_rows.present?
31
+ content_tag(:div) do
32
+ concat error_header
33
+ concat error_items(invalid_rows)
34
+ end
35
+ end
36
+ end
37
+
38
+ def error_header
39
+ content_tag :div, class: 'info-split' do
40
+ concat content_tag(:div, "Row number", class: 'info-split__lhs')
41
+ concat content_tag(:div, "Error")
42
+ end
43
+ end
44
+
45
+ def error_items(invalid_rows)
46
+ content_tag :div do
47
+ invalid_rows.each do |row_item|
48
+ concat format_error_item(row_item)
49
+ end
50
+ end
51
+ end
52
+
53
+ def format_error_item(row_item)
54
+ content_tag :div, class: 'info-split' do
55
+ concat content_tag(:div, row_item[0], class: 'info-split__lhs')
56
+ concat format_error(row_item[2])
57
+ end
58
+ end
59
+
60
+ def format_error(errors)
61
+ content_tag :div do
62
+ errors.each do |error|
63
+ message = error[1].instance_of?(Array) ? error[1].join(', ') : error[1]
64
+ concat content_tag(:div, error[0].titleize + '-' + message)
65
+ end
66
+ end
67
+ end
27
68
  end
28
69
  end
@@ -0,0 +1,38 @@
1
+ class FileImportProcessorJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(file_import)
5
+ model = CmAdmin::Model.find_by({name: file_import.associated_model_name})
6
+ path = ActiveStorage::Blob.service.path_for(file_import.import_file.key)
7
+ importer = model.importer.class_name.classify.constantize.new(path: path)
8
+ case model.importer.importer_type.to_s
9
+ when 'csv_importer'
10
+ run_csv_importer(importer, file_import)
11
+ when 'custom_importer'
12
+ run_custom_importer(importer, file_import)
13
+ end
14
+ end
15
+
16
+ # All logic are from csv_importer gem
17
+ def run_csv_importer(importer, file_import)
18
+ if importer.valid_header?
19
+ importer.run!
20
+ if importer.report.success?
21
+ file_import.update(status: :success, completed_at: DateTime.now)
22
+ else
23
+ identifier = importer.config.identifiers.first
24
+ invalid_items_array = importer.report.invalid_rows.map { |row| [row.line_number, row.model.send(identifier), row.errors] }
25
+ file_import.update(status: :failed, completed_at: DateTime.now, invalid_row_items: invalid_items_array)
26
+ end
27
+ else
28
+ file_import.update(status: :failed, completed_at: DateTime.now, invalid_row_items: [[1, 'invalid_header', {invalid_header: importer.report.message}]])
29
+ end
30
+ end
31
+
32
+ def run_custom_importer(importer, file_import)
33
+ importer.run!
34
+ invalid_items_array = importer.invalid_rows.map { |row| [row.line_number, row.identifier, row.errors] }
35
+ status = importer.invalid_rows.empty? ? :success : :failed
36
+ file_import.update(status: status, completed_at: DateTime.now, invalid_row_items: invalid_items_array)
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ module CmAdmin::FileImport
2
+ extend ActiveSupport::Concern
3
+ included do
4
+ cm_admin do
5
+ STATUS_TAG_COLOR = { in_progress: 'yellow-tag', success: 'success'}
6
+ actions only: [:index, :show]
7
+ set_icon 'fa fa-user'
8
+ cm_index do
9
+ page_title 'File Import'
10
+ page_description 'Manage all file import progress here'
11
+
12
+ column :id
13
+ column :imported_file_name, header: 'File'
14
+ column :created_at, header: 'Started At', field_type: :datetime, format: '%B %d, %Y, %H:%M %p'
15
+ column :completed_at, header: 'Completed At', field_type: :datetime, format: '%B %d, %Y, %H:%M %p'
16
+ column :associated_model_name, header: 'Model name'
17
+ column :added_by_name, header: 'Uploaded By'
18
+ column :status, field_type: :tag, tag_class: STATUS_TAG_COLOR
19
+ end
20
+
21
+ cm_show page_title: :id do
22
+ tab :profile, '' do
23
+ cm_show_section 'Import details' do
24
+ field :id, label: 'Import ID'
25
+ field :imported_file_name, label: 'File'
26
+ field :created_at, header: 'Started At', field_type: :datetime, format: '%B %d, %Y, %H:%M %p'
27
+ field :completed_at, header: 'Completed At', field_type: :datetime, format: '%B %d, %Y, %H:%M %p'
28
+ field :associated_model_name, header: 'Model name'
29
+ field :added_by_name, header: 'Uploaded By'
30
+ field :status, field_type: :tag, tag_class: STATUS_TAG_COLOR
31
+ end
32
+ cm_show_section 'Errors' do
33
+ field :invalid_row_items, field_type: :custom, helper_method: :formatted_error_message
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ class FileImport < ApplicationRecord
2
+ include CmAdmin::FileImport
3
+
4
+ belongs_to :added_by, polymorphic: true
5
+
6
+ enum status: { in_progress: 0, success: 1, failed: 2 }
7
+
8
+ has_one_attached :import_file
9
+
10
+ after_create_commit :process_uploaded_file
11
+
12
+ store_accessor :error_report, :invalid_row_items
13
+
14
+
15
+ def process_uploaded_file
16
+ FileImportProcessorJob.perform_later(self)
17
+ end
18
+
19
+ def imported_file_name
20
+ import_file.filename.to_s
21
+ end
22
+
23
+ def added_by_name
24
+ added_by.first_name
25
+ end
26
+
27
+ end
@@ -0,0 +1,11 @@
1
+ class CmAdmin::FileImportPolicy < ApplicationPolicy
2
+
3
+ def index?
4
+ true
5
+ end
6
+
7
+ def show?
8
+ true
9
+ end
10
+
11
+ end
@@ -1,8 +1,7 @@
1
1
  - edit_action = available_actions(cm_model, 'edit')
2
2
  - destroy_action = available_actions(cm_model, 'destroy')
3
3
  - custom_actions = available_actions(cm_model, 'custom_actions')
4
- - custom_actions_modals = custom_actions.select{ |act| act if act.display_type.eql?(:modal) }
5
-
4
+ - current_model = @associated_model || @model
6
5
  - if custom_actions.any? || edit_action.present? || destroy_action.present?
7
6
  td.row-action-cell
8
7
  .row-action-tool
@@ -13,13 +12,13 @@
13
12
  i.fa.fa-angle-down
14
13
  .popup-card.table-export-popup.hidden
15
14
  - if edit_action.present?
16
- = link_to "#{page_url('edit', ar_object)}" do
15
+ = link_to cm_admin.send("#{current_model.name.underscore}_edit_path", ar_object.id) do
17
16
  .popup-option
18
17
  span
19
18
  i.fa.fa-edit
20
19
  | Edit
21
20
  - if destroy_action.present?
22
- = link_to "#{page_url('destroy', ar_object)}", method: :delete do
21
+ = link_to cm_admin.send("#{current_model.name.underscore}_destroy_path", ar_object.id), method: :delete do
23
22
  .popup-option
24
23
  span
25
24
  i.fa.fa-trash
@@ -39,14 +38,3 @@
39
38
  span
40
39
  i class="#{custom_action.icon_name}"
41
40
  = custom_action.name.humanize
42
-
43
- - if custom_actions_modals.any?
44
- - custom_actions_modals.each do |custom_action|
45
- .modal.fade id="#{custom_action.name.classify}Modal-#{ar_object.id.to_s}" aria-hidden='true' aria-labelledby="#{custom_action.name.classify}ModalLabel" tabindex='1'
46
- .modal-dialog
47
- .modal-content
48
- .modal-header
49
- h5.modal-title id="#{custom_action.name.classify}ModalLabel" = custom_action.name.classify
50
- button.btn-close aria-label='Close' data-bs-dismiss='modal'
51
- .modal-body
52
- = render partial: custom_action.partial, locals: { ar_object: ar_object }
@@ -46,3 +46,5 @@
46
46
  .cm-pagination__lhs Showing #{@associated_ar_object.pagy.from} to #{@associated_ar_object.pagy.to} out of #{@associated_ar_object.pagy.count}
47
47
  .cm-pagination__rhs
48
48
  == render partial: 'cm_admin/main/cm_pagy_nav', locals: { pagy: @associated_ar_object.pagy }
49
+
50
+ = render partial: 'cm_admin/main/member_custom_action_modal', locals: { cm_model: @associated_model, ar_collection: @associated_ar_object }
@@ -0,0 +1,13 @@
1
+ - custom_actions = available_actions(cm_model, 'custom_actions')
2
+ - custom_actions_modals = custom_actions.select{ |act| act if act.display_type.eql?(:modal) }
3
+ - ar_collection.data.each do |ar_object|
4
+ - if custom_actions_modals.any?
5
+ - custom_actions_modals.each do |custom_action|
6
+ .modal.fade id="#{custom_action.name.classify}Modal-#{ar_object.id.to_s}" aria-hidden='true' aria-labelledby="#{custom_action.name.classify}ModalLabel" tabindex='1'
7
+ .modal-dialog
8
+ .modal-content
9
+ .modal-header
10
+ h5.modal-title id="#{custom_action.name.classify}ModalLabel" = custom_action.name.classify
11
+ button.btn-close aria-label='Close' data-bs-dismiss='modal'
12
+ .modal-body
13
+ = render partial: custom_action.partial, locals: { ar_object: ar_object }
@@ -39,3 +39,5 @@
39
39
  .cm-pagination__lhs Showing #{@ar_object.pagy.from} to #{@ar_object.pagy.to} out of #{@ar_object.pagy.count}
40
40
  .cm-pagination__rhs
41
41
  == render partial: 'cm_admin/main/cm_pagy_nav', locals: { pagy: @ar_object.pagy }
42
+
43
+ = render partial: 'cm_admin/main/member_custom_action_modal', locals: { cm_model: @model, ar_collection: @ar_object }
@@ -22,6 +22,8 @@
22
22
  li
23
23
  .popup-option.pointer data-bs-toggle='modal' data-bs-target='#exportmodal'
24
24
  span Export
25
+ - if @model.importer
26
+ = link_to 'Import', cm_admin.send(:"#{@model.name.underscore}_import_path"), class: 'primary-btn ml-2'
25
27
  - new_action = @model.available_actions.select{|act| act if act.action_type.eql?(:default) && act.name.eql?('new')}
26
28
  - if new_action.any? && policy([:cm_admin, @model.name.classify.constantize]).new?
27
29
  = link_to 'Add', cm_admin.send(:"#{@model.name.underscore}_new_path"), class: 'primary-btn ml-2'
@@ -0,0 +1,35 @@
1
+ .import-page
2
+ .import-page__header
3
+ h1.header-title #{@model.name} Import
4
+ p.header-description To import #{@model.name.underscore} data using the form below.
5
+ .import-page__body
6
+ h6.body-title Import data
7
+ - if flash[:notice]
8
+ .success-card
9
+ h6.success-title
10
+ i.fa-solid.fa-circle-check
11
+ | SUCCESS
12
+ p.success-msg Your file has been uploaded and it will be processed soon.
13
+ / p.success-msg-info An email will be sent once the process is completed. If there are any problems with the import, we'll let you know through an email.
14
+ .actions-wrapper
15
+ a.secondary-btn href="#{cm_admin.send(:"#{@model.name.underscore}_import_path")}" Import new data
16
+ / button.cta-btn.ml-2 Back to Page_Name
17
+ - else
18
+ = simple_form_for(FileImport.new, url: "/admin/#{@model.ar_model.table_name}/import", method: :post, html: { class: "csv-import-form" }) do |f|
19
+ .form-card
20
+ = f.input :associated_model_name, as: :hidden, placeholder: 'Enter product title', label: false, input_html: { value: @model.name }
21
+ = f.input :import_file, as: :file, placeholder: 'Enter product title', label: false
22
+ .note-card
23
+ h6.note-title
24
+ i.fa.fa-info-circle
25
+ | Note
26
+ .steps-wrapper
27
+ h6.steps-title Follow these steps to import your data
28
+ ul.steps-list
29
+ li
30
+ | Download this
31
+ a href="#" template file
32
+ li Add your data on the file without changing the format
33
+ li Save the file as a csv, xls or xlsx
34
+ li Upload the file in the field above
35
+ = f.button :submit, class: "cta-btn import-btn", value: 'Import data'
data/cm_admin.gemspec CHANGED
@@ -33,4 +33,5 @@ Gem::Specification.new do |spec|
33
33
  spec.add_runtime_dependency 'pundit', '~> 2.2.0'
34
34
  spec.add_runtime_dependency 'slim', '~> 4.1.0'
35
35
  spec.add_runtime_dependency 'webpacker', '~> 5.4.3'
36
+ spec.add_runtime_dependency 'csv-importer', '~> 0.8.2'
36
37
  end
data/config/routes.rb CHANGED
@@ -10,6 +10,12 @@ CmAdmin::Engine.routes.draw do
10
10
 
11
11
  # Defining action routes for each model
12
12
  CmAdmin.config.cm_admin_models.each do |model|
13
+ if model.importer
14
+ scope model.name.tableize do
15
+ send(:get, 'import', to: "#{model.name.underscore}#import_form", as: "#{model.name.underscore}_import_form")
16
+ send(:post, 'import', to: "#{model.name.underscore}#import", as: "#{model.name.underscore}_import")
17
+ end
18
+ end
13
19
  model.available_actions.sort_by {|act| act.name}.each do |act|
14
20
  scope model.name.tableize do
15
21
  send(act.verb, act.path.present? ? act.path : act.name, to: "#{model.name.underscore}##{act.name}", as: "#{model.name.underscore}_#{act.name}")
@@ -1,5 +1,6 @@
1
1
  require_relative 'constants'
2
2
  require_relative 'models/action'
3
+ require_relative 'models/importer'
3
4
  require_relative 'models/custom_action'
4
5
  require_relative 'models/field'
5
6
  require_relative 'models/form_field'
@@ -15,6 +16,7 @@ require 'axlsx'
15
16
  require 'cocoon'
16
17
  require 'pundit'
17
18
  require 'local_time'
19
+ require 'csv_importer'
18
20
 
19
21
  module CmAdmin
20
22
  class Model
@@ -23,7 +25,7 @@ module CmAdmin
23
25
  include Models::DslMethod
24
26
  attr_accessor :available_actions, :actions_set, :available_fields, :permitted_fields,
25
27
  :current_action, :params, :filters, :available_tabs, :icon_name
26
- attr_reader :name, :ar_model, :is_visible_on_sidebar
28
+ attr_reader :name, :ar_model, :is_visible_on_sidebar, :importer
27
29
 
28
30
  def initialize(entity, &block)
29
31
  @name = entity.name
@@ -80,6 +82,10 @@ module CmAdmin
80
82
  @actions_set = true
81
83
  end
82
84
 
85
+ def importable(class_name:, importer_type:)
86
+ @importer = CmAdmin::Models::Importer.new(class_name, importer_type)
87
+ end
88
+
83
89
  def visible_on_sidebar(visible_option)
84
90
  @is_visible_on_sidebar = visible_option
85
91
  end
@@ -1,7 +1,7 @@
1
1
  module CmAdmin
2
2
  module Models
3
3
  class Column
4
- attr_accessor :field_name, :field_type, :header, :format, :prefix, :suffix, :exportable, :round,
4
+ attr_accessor :field_name, :field_type, :header, :format, :prefix, :suffix, :exportable, :round, :height, :width,
5
5
  :cm_css_class, :link, :url, :custom_method, :helper_method, :managable, :lockable, :drawer_partial, :tag_class
6
6
 
7
7
  def initialize(field_name, attributes = {})
@@ -13,6 +13,8 @@ module CmAdmin
13
13
 
14
14
  #formatting header (either field_name or value present in header attribute)
15
15
  self.send("header=", format_header)
16
+ self.height = 50 if self.field_type == :image && self.height.nil?
17
+ self.width = 50 if self.field_type == :image && self.width.nil?
16
18
  end
17
19
 
18
20
  #returns a string value as a header (either field_name or value present in header attribute)
@@ -2,8 +2,8 @@ module CmAdmin
2
2
  module Models
3
3
  class Field
4
4
 
5
- attr_accessor :field_name, :label, :header, :field_type, :format, :precision,
6
- :helper_method, :preview, :custom_link, :precision, :prefix, :suffix, :tag_class
5
+ attr_accessor :field_name, :label, :header, :field_type, :format, :precision, :height,
6
+ :width, :helper_method, :preview, :custom_link, :precision, :prefix, :suffix, :tag_class
7
7
 
8
8
  def initialize(field_name, attributes = {})
9
9
  @field_name = field_name
@@ -11,6 +11,8 @@ module CmAdmin
11
11
  attributes.each do |key, value|
12
12
  self.send("#{key.to_s}=", value)
13
13
  end
14
+ self.height = 50 if self.field_type == :image && self.height.nil?
15
+ self.width = 50 if self.field_type == :image && self.width.nil?
14
16
  end
15
17
 
16
18
  def set_default_values
@@ -5,17 +5,18 @@ module CmAdmin
5
5
  VALID_INPUT_TYPES = [:integer, :decimal, :string, :single_select, :multi_select, :date, :date_time, :text, :single_file_upload, :multi_file_upload, :hidden, :rich_text].freeze
6
6
 
7
7
  def initialize(field_name, input_type, attributes = {})
8
- raise ArgumentError, "Kindly select a valid filter type like #{VALID_INPUT_TYPES.sort.to_sentence(last_word_connector: ', or ')} instead of #{input_type} for column #{field_name}" unless VALID_INPUT_TYPES.include?(input_type.to_sym)
9
8
  @field_name = field_name
10
9
  set_default_values
11
10
  attributes.each do |key, value|
12
11
  self.send("#{key.to_s}=", value)
13
12
  end
13
+ raise ArgumentError, "Kindly select a valid input type like #{VALID_INPUT_TYPES.sort.to_sentence(last_word_connector: ', or ')} instead of #{self.input_type} for form field #{field_name}" unless VALID_INPUT_TYPES.include?(self.input_type.to_sym)
14
14
  end
15
15
 
16
16
  def set_default_values
17
17
  self.disabled = false
18
18
  self.label = self.field_name.to_s.titleize
19
+ self.input_type = :string
19
20
  end
20
21
  end
21
22
  end
@@ -0,0 +1,17 @@
1
+ module CmAdmin
2
+ module Models
3
+ class Importer
4
+
5
+ attr_accessor :class_name, :importer_type
6
+
7
+ VALID_IMPORTER_TYPES = [:csv_importer, :custom_importer]
8
+
9
+ def initialize(class_name, importer_type=:csv_importer)
10
+ raise ArgumentError, "Kindly select a valid importer type like #{VALID_IMPORTER_TYPES.sort.to_sentence(last_word_connector: ', or ')} instead of #{importer_type}" unless VALID_IMPORTER_TYPES.include?(importer_type.to_sym)
11
+ @class_name = class_name
12
+ @importer_type = importer_type
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module CmAdmin
2
- VERSION = "0.7.8"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -61,6 +61,14 @@ module CmAdmin
61
61
  concat content_tag(:div, ar_object.send(field.field_name).to_s, class: 'text-ellipsis')
62
62
  concat content_tag(:div, 'View', class: 'drawer-btn')
63
63
  end
64
+ when :image
65
+ content_tag(:div, class: 'd-flex') do
66
+ if ar_object.send(field.field_name).attached?
67
+ image_tag(ar_object.send(field.field_name).url, height: field.height, width: field.height)
68
+ else
69
+ image_tag('/assets/image_not_available', height: 50, width: 50)
70
+ end
71
+ end
64
72
  end
65
73
  end
66
74
 
@@ -13,9 +13,9 @@ module CmAdmin
13
13
  when :string
14
14
  return f.text_field field.field_name, class: "normal-input #{required_class}", disabled: field.disabled, value: value, placeholder: "Enter #{field.field_name.to_s.downcase.gsub('_', ' ')}"
15
15
  when :single_select
16
- return f.select field.field_name, options_for_select(select_collection_value(field), value), {include_blank: "Select #{field.field_name.to_s.downcase.gsub('_', ' ')}"}, class: "normal-input #{required_class} select-2", disabled: field.disabled
16
+ return f.select field.field_name, options_for_select(select_collection_value(field), f.object.send(field.field_name)), {include_blank: "Select #{field.field_name.to_s.downcase.gsub('_', ' ')}"}, class: "normal-input #{required_class} select-2", disabled: field.disabled
17
17
  when :multi_select
18
- return f.select field.field_name, options_for_select(select_collection_value(field), value), {include_blank: "Select #{field.field_name.to_s.downcase.gsub('_', ' ')}"}, class: "normal-input #{required_class} select-2", disabled: field.disabled, multiple: true
18
+ return f.select field.field_name, options_for_select(select_collection_value(field), f.object.send(field.field_name)), {include_blank: "Select #{field.field_name.to_s.downcase.gsub('_', ' ')}"}, class: "normal-input #{required_class} select-2", disabled: field.disabled, multiple: true
19
19
  when :date
20
20
  return f.text_field field.field_name, class: "normal-input #{required_class}", disabled: field.disabled, value: value&.strftime('%d-%m-%Y'), placeholder: "Enter #{field.field_name.to_s.downcase.gsub('_', ' ')}", data: { behaviour: 'date-only' }
21
21
  when :date_time
data/lib/cm_admin.rb CHANGED
@@ -35,7 +35,8 @@ module CmAdmin
35
35
  def initialize_model(entity, &block)
36
36
  if entity.is_a?(Class)
37
37
  return if CmAdmin::Model.find_by({name: entity.name})
38
- config.cm_admin_models << CmAdmin::Model.new(entity, &block)
38
+ cm_model = CmAdmin::Model.new(entity, &block)
39
+ config.cm_admin_models << cm_model
39
40
  end
40
41
  end
41
42
  end
@@ -14,6 +14,8 @@ module CmAdmin
14
14
  remove_file 'app/assets/stylesheets/actiontext.scss'
15
15
  copy_file 'application_policy.rb', 'app/policies/application_policy.rb'
16
16
  route 'mount CmAdmin::Engine => "/admin"'
17
+ generate 'migration', 'CreateFileImport associated_model_name:string added_by:references{polymorphic} error_report:jsonb completed_at:datetime status:integer'
18
+ rake 'db:migrate'
17
19
  end
18
20
  end
19
21
  end
@@ -2,5 +2,5 @@ CmAdmin.configure do |config|
2
2
  # Sets the default layout to be used for admin
3
3
  config.layout = 'admin'
4
4
  # config.authorized_roles = [:super_admin?]
5
- # config.included_models = [Admin]
5
+ config.included_models = [FileImport]
6
6
  end
@@ -1 +1 @@
1
- 00ac6841e4fa3607e5f705b375dada0dcaf2c84d
1
+ 31533361827c4faa12bf892403c629479a81246a
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cm-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.8
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sajinmp
8
8
  - anbublacky
9
9
  - AdityaTiwari2102
10
- autorequire:
10
+ autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2022-09-15 00:00:00.000000000 Z
13
+ date: 2022-10-12 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: caxlsx_rails
@@ -110,6 +110,20 @@ dependencies:
110
110
  - - "~>"
111
111
  - !ruby/object:Gem::Version
112
112
  version: 5.4.3
113
+ - !ruby/object:Gem::Dependency
114
+ name: csv-importer
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: 0.8.2
120
+ type: :runtime
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: 0.8.2
113
127
  description: This is an admin panel gem
114
128
  email:
115
129
  - sajinprasadkm@gmail.com
@@ -128,6 +142,7 @@ files:
128
142
  - LICENSE.txt
129
143
  - README.md
130
144
  - Rakefile
145
+ - app/assets/images/image_not_available.png
131
146
  - app/assets/images/logo.png
132
147
  - app/assets/stylesheets/cm_admin/base/auth.scss
133
148
  - app/assets/stylesheets/cm_admin/base/common.scss
@@ -155,6 +170,7 @@ files:
155
170
  - app/assets/stylesheets/cm_admin/helpers/_mixins.scss
156
171
  - app/assets/stylesheets/cm_admin/helpers/_variable.scss
157
172
  - app/assets/stylesheets/cm_admin/helpers/index.scss
173
+ - app/assets/stylesheets/cm_admin/pages/import_page.scss
158
174
  - app/assets/stylesheets/cm_admin/scaffold.scss
159
175
  - app/controllers/cm_admin/application_controller.rb
160
176
  - app/controllers/cm_admin/exports_controller.rb
@@ -169,11 +185,16 @@ files:
169
185
  - app/javascript/packs/cm_admin/quick_search.js
170
186
  - app/javascript/packs/cm_admin/scaffolds.js
171
187
  - app/javascript/stylesheets/cm_admin/application.scss
188
+ - app/jobs/file_import_processor_job.rb
189
+ - app/models/concerns/cm_admin/file_import.rb
190
+ - app/models/file_import.rb
191
+ - app/policies/cm_admin/file_import_policy.rb
172
192
  - app/views/cm_admin/main/_actions_dropdown.html.slim
173
193
  - app/views/cm_admin/main/_associated_table.html.slim
174
194
  - app/views/cm_admin/main/_cm_pagy_nav.html.slim
175
195
  - app/views/cm_admin/main/_drawer.html.slim
176
196
  - app/views/cm_admin/main/_filters.html.slim
197
+ - app/views/cm_admin/main/_member_custom_action_modal.html.slim
177
198
  - app/views/cm_admin/main/_nested_fields.html.slim
178
199
  - app/views/cm_admin/main/_nested_table_form.html.slim
179
200
  - app/views/cm_admin/main/_table.html.slim
@@ -183,6 +204,7 @@ files:
183
204
  - app/views/cm_admin/main/associated_show.html.slim
184
205
  - app/views/cm_admin/main/dashboard.html.slim
185
206
  - app/views/cm_admin/main/edit.html.slim
207
+ - app/views/cm_admin/main/import_form.html.slim
186
208
  - app/views/cm_admin/main/index.html.slim
187
209
  - app/views/cm_admin/main/new.html.slim
188
210
  - app/views/cm_admin/main/show.html.slim
@@ -225,6 +247,7 @@ files:
225
247
  - lib/cm_admin/models/field.rb
226
248
  - lib/cm_admin/models/filter.rb
227
249
  - lib/cm_admin/models/form_field.rb
250
+ - lib/cm_admin/models/importer.rb
228
251
  - lib/cm_admin/models/tab.rb
229
252
  - lib/cm_admin/utils.rb
230
253
  - lib/cm_admin/version.rb
@@ -278,7 +301,7 @@ licenses:
278
301
  metadata:
279
302
  homepage_uri: https://github.com/commutatus/cm-admin
280
303
  source_code_uri: https://github.com/commutatus/cm-admin
281
- post_install_message:
304
+ post_install_message:
282
305
  rdoc_options: []
283
306
  require_paths:
284
307
  - lib
@@ -294,7 +317,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
294
317
  version: '0'
295
318
  requirements: []
296
319
  rubygems_version: 3.2.3
297
- signing_key:
320
+ signing_key:
298
321
  specification_version: 4
299
322
  summary: This is an admin panel gem
300
323
  test_files: []