cm-admin 4.4.8 → 4.6.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f200f0b81773d02d7dd472b90c0dcdc124cd3797e7a09b06cef05b42b597132
4
- data.tar.gz: 31ebaa25c7016f262697e52f76bc695db07595286d8ac088175390d09eea1ffe
3
+ metadata.gz: 5b2fb69e4b0dd8324fcf648c7fa0309e86208cf5cb580ed74897013c8fe1c1d2
4
+ data.tar.gz: 249eadf19d6edc9bebc6d7798c3ba3203f0429d7f1b18ede7e3e6264a9a85a5c
5
5
  SHA512:
6
- metadata.gz: a655ebbf63906cd67dd83f3e2a68eaff22c1d215a4e6b0eb1ea7e05b471ccb24907a65552236ca893161fa64979cb03c825d4b922637de73124fb601174df8d3
7
- data.tar.gz: 9ebfab98bc9321b20141765412dbb229e6c994195a3f82de0c1420c423bea0fe6790aaaaf57147a92a9c850713e051f471e49f35e9d64f5e1d31a470fab6268a
6
+ metadata.gz: 5070c55b836e1eebe92f4548eb5a79b567506e760d38ea4938dc42e72555dcf9d9d07788b5903c4d15cda3180aee7f9101055e15cd3153af2d1847261e8ebda4
7
+ data.tar.gz: 2d8a1a41e1155f6060546cc1d61d04457c9b1ab182dd03c50a065fe40c48b16cee8bb2535578763303bfa922f364a60ff33573244144e6b73ce812576a16918d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cm-admin (4.4.8)
4
+ cm-admin (4.6.5)
5
5
  caxlsx_rails
6
6
  cocoon (~> 1.2.15)
7
7
  csv (>= 3.3.0)
@@ -34,19 +34,20 @@ $(document).on("click", '[data-behaviour="export-submit"]', function (e) {
34
34
  });
35
35
 
36
36
  document.addEventListener("turbo:load", function () {
37
- document
38
- .querySelector('[data-behaviour="export-modal"]')
39
- .addEventListener("hidden.bs.modal", function () {
40
- const exportFormBody = $("[data-behaviour='export-form-container']");
41
- const exportProcessing = $("[data-behaviour='export-processing']");
42
- const exportTitle = $("[data-behaviour='export-modal-title']");
43
- const exportFooter = $("[data-behaviour='export-modal-footer']");
44
- const exportContent = $("[data-behaviour='export-modal-content']");
45
- exportTitle.html("Export Data");
46
- exportProcessing.addClass("hidden");
47
- exportFormBody.removeClass("hidden");
48
- exportFooter.removeClass("hidden");
49
- exportContent.addClass("export-modal-content");
50
- exportContent.removeClass("modal-content");
51
- });
37
+ const exportModal = document.querySelector('[data-behaviour="export-modal"]');
38
+ if (!exportModal) return;
39
+
40
+ exportModal.addEventListener("hidden.bs.modal", function () {
41
+ const exportFormBody = $("[data-behaviour='export-form-container']");
42
+ const exportProcessing = $("[data-behaviour='export-processing']");
43
+ const exportTitle = $("[data-behaviour='export-modal-title']");
44
+ const exportFooter = $("[data-behaviour='export-modal-footer']");
45
+ const exportContent = $("[data-behaviour='export-modal-content']");
46
+ exportTitle.html("Export Data");
47
+ exportProcessing.addClass("hidden");
48
+ exportFormBody.removeClass("hidden");
49
+ exportFooter.removeClass("hidden");
50
+ exportContent.addClass("export-modal-content");
51
+ exportContent.removeClass("modal-content");
52
+ });
52
53
  });
@@ -1,12 +1,15 @@
1
- import LocalTime from "./local-time"
1
+ import LocalTime from "./local-time";
2
2
  import * as bootstrap from "bootstrap";
3
3
  window.bootstrap = bootstrap;
4
4
 
5
-
6
- export function initializeComponents() {
7
- $(".select-2").select2({
5
+ export function initializeComponents(select2Parent = null) {
6
+ let select2Options = {
8
7
  theme: "bootstrap-5",
9
- });
8
+ };
9
+ if (select2Parent) {
10
+ select2Options.dropdownParent = select2Parent;
11
+ }
12
+ $(".select-2").select2(select2Options);
10
13
  flatpickr("[data-behaviour='date-only']", {
11
14
  dateFormat: "d-m-Y",
12
15
  });
@@ -23,7 +26,12 @@ export function initializeComponents() {
23
26
  animation: 150,
24
27
  });
25
28
  }
26
-
29
+ $("select").each(function () {
30
+ $(this).select2({
31
+ theme: "bootstrap-5",
32
+ dropdownParent: $(this).parent(),
33
+ });
34
+ });
27
35
  var headerElemHeight = $(".page-top-bar").height() + 64;
28
36
  var calculatedHeight = "calc(100vh - " + headerElemHeight + "px" + ")";
29
37
  $(".table-wrapper").css("maxHeight", calculatedHeight);
@@ -18,7 +18,7 @@ import "flatpickr";
18
18
  import "trix";
19
19
  import "@rails/actiontext";
20
20
  import Select2 from "select2";
21
- import LocalTime from "./local-time"
21
+ import LocalTime from "./local-time";
22
22
  Select2();
23
23
 
24
24
  // import '@nathanvda/cocoon'
@@ -37,10 +37,10 @@ document.addEventListener("turbo:load", function () {
37
37
  $(".select-2").select2({
38
38
  theme: "bootstrap-5",
39
39
  });
40
- LocalTime.start()
40
+ LocalTime.start();
41
41
 
42
42
  setup_select_2_ajax();
43
-
43
+
44
44
  const bsToast = $('[data-behaviour="toast"]')[0];
45
45
  if (bsToast) {
46
46
  const toast = new bootstrap.Toast(bsToast);
@@ -79,67 +79,100 @@ $(document).on("click", function (e) {
79
79
  }
80
80
  });
81
81
 
82
- $(document).on("click", '[data-section="destroy-attachment"] button', function (e) {
83
- e.preventDefault();
84
- var ar_id = $(this).parent('[data-section="destroy-attachment"]').data("ar-id");
85
- $(this).parent('[data-section="destroy-attachment"]').addClass("hidden");
86
- $(this).closest('[data-field-type="single_file_upload"]').find('input').removeClass('hidden')
87
- $(this).append(
88
- '<input type="text" name="attachment_destroy_ids[]" value="' + ar_id + '"/>'
89
- );
90
- });
82
+ $(document).on(
83
+ "click",
84
+ '[data-section="destroy-attachment"] button',
85
+ function (e) {
86
+ e.preventDefault();
87
+ var ar_id = $(this)
88
+ .parent('[data-section="destroy-attachment"]')
89
+ .data("ar-id");
90
+ $(this).parent('[data-section="destroy-attachment"]').addClass("hidden");
91
+ $(this)
92
+ .closest('[data-field-type="single_file_upload"]')
93
+ .find("input")
94
+ .removeClass("hidden");
95
+ $(this).append(
96
+ '<input type="text" name="attachment_destroy_ids[]" value="' +
97
+ ar_id +
98
+ '"/>'
99
+ );
100
+ }
101
+ );
91
102
 
92
- $(document).on('change', '[data-field-type="single_file_upload"] input', function(e) {
93
- console.log("File changed")
94
- const [file] = $(this)[0].files
95
- if (file) {
96
- // blah.src = URL.createObjectURL(file)
97
- const upload_div = $(this).closest('[data-field-type="single_file_upload"]')
98
- $(upload_div).find('input').addClass('hidden')
99
- $(upload_div).find('[data-section="destroy-attachment"]').removeClass("hidden")
100
- $(upload_div).find('[data-section="image-preview"]').html(`<div data-section="destroy-attachment" data-attachment-name="listing[media_items_attributes][0]_attachment_attachments">\
103
+ $(document).on(
104
+ "change",
105
+ '[data-field-type="single_file_upload"] input',
106
+ function (e) {
107
+ const [file] = $(this)[0].files;
108
+ if (file) {
109
+ // blah.src = URL.createObjectURL(file)
110
+ const upload_div = $(this).closest(
111
+ '[data-field-type="single_file_upload"]'
112
+ );
113
+ $(upload_div).find("input").addClass("hidden");
114
+ $(upload_div)
115
+ .find('[data-section="destroy-attachment"]')
116
+ .removeClass("hidden");
117
+ $(upload_div).find('[data-section="image-preview"]')
118
+ .html(`<div data-section="destroy-attachment" data-attachment-name="listing[media_items_attributes][0]_attachment_attachments">\
101
119
  <span class="btn-link">${file.name}</span>\
102
120
  <button class="btn-ghost">\
103
121
  <i class="fa-regular fa-trash-can"></i>\
104
122
  </button>\
105
- </div>`
106
- )
107
- var allowExtention = ".jpg,.bmp,.gif,.png";
108
- var extention = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase();
109
-
110
- if (allowExtention.indexOf(extention) > -1) {
111
- var img = $('<img height="50" width="50" class="rounded"/>');
112
- img.attr("src", URL.createObjectURL(file));
113
- $(upload_div).find('[data-section="image-preview"]').find('[data-section="destroy-attachment"]').prepend(img);
114
- }
115
- }
116
- })
117
-
118
- $(document).on('change', '[data-field-type="multi_file_upload"] input', function(e) {
119
- console.log("File changed")
120
- const files = $(this)[0].files
121
- const element_id = $(this).attr('id')
122
- const upload_div = $(this).closest('[data-field-type="multi_file_upload"]')
123
- $(upload_div).find('[data-section="image-preview"]').html('')
124
- if (files) {
125
- $.each(files, function(index, file) {
126
- $(upload_div).find('[data-section="destroy-attachment"]').removeClass("hidden");
127
- $(upload_div).find('[data-section="image-preview"]').append(`<div data-section="destroy-attachment" data-index="${index}" data-attachment-name="${element_id}_attachments">\
128
- <span class="btn-link">${file.name}</span>\
129
- </div>`
130
- )
123
+ </div>`);
131
124
  var allowExtention = ".jpg,.bmp,.gif,.png";
132
- var extention = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase();
133
-
125
+ var extention = file.name
126
+ .substring(file.name.lastIndexOf(".") + 1)
127
+ .toLowerCase();
128
+
134
129
  if (allowExtention.indexOf(extention) > -1) {
135
130
  var img = $('<img height="50" width="50" class="rounded"/>');
136
131
  img.attr("src", URL.createObjectURL(file));
137
- $(upload_div).find('[data-section="image-preview"]').find(`[data-index="${index}"]`).prepend(img);
132
+ $(upload_div)
133
+ .find('[data-section="image-preview"]')
134
+ .find('[data-section="destroy-attachment"]')
135
+ .prepend(img);
138
136
  }
139
- })
140
-
137
+ }
141
138
  }
142
- })
139
+ );
140
+
141
+ $(document).on(
142
+ "change",
143
+ '[data-field-type="multi_file_upload"] input',
144
+ function (e) {
145
+ const files = $(this)[0].files;
146
+ const element_id = $(this).attr("id");
147
+ const upload_div = $(this).closest('[data-field-type="multi_file_upload"]');
148
+ $(upload_div).find('[data-section="image-preview"]').html("");
149
+ if (files) {
150
+ $.each(files, function (index, file) {
151
+ $(upload_div)
152
+ .find('[data-section="destroy-attachment"]')
153
+ .removeClass("hidden");
154
+ $(upload_div).find(
155
+ '[data-section="image-preview"]'
156
+ ).append(`<div data-section="destroy-attachment" data-index="${index}" data-attachment-name="${element_id}_attachments">\
157
+ <span class="btn-link">${file.name}</span>\
158
+ </div>`);
159
+ var allowExtention = ".jpg,.bmp,.gif,.png";
160
+ var extention = file.name
161
+ .substring(file.name.lastIndexOf(".") + 1)
162
+ .toLowerCase();
163
+
164
+ if (allowExtention.indexOf(extention) > -1) {
165
+ var img = $('<img height="50" width="50" class="rounded"/>');
166
+ img.attr("src", URL.createObjectURL(file));
167
+ $(upload_div)
168
+ .find('[data-section="image-preview"]')
169
+ .find(`[data-index="${index}"]`)
170
+ .prepend(img);
171
+ }
172
+ });
173
+ }
174
+ }
175
+ );
143
176
 
144
177
  $(document).on(
145
178
  "click",
@@ -192,6 +225,7 @@ window.addEventListener("popstate", (e) => window.location.reload());
192
225
  function setup_select_2_ajax() {
193
226
  $(".select-2-ajax").each(function (index, element) {
194
227
  $(element).select2({
228
+ theme: "bootstrap-5",
195
229
  ajax: {
196
230
  url: $(element)[0]["dataset"].ajaxUrl,
197
231
  dataType: "json",
@@ -7,7 +7,7 @@ $(document).on(
7
7
  "[data-behaviour='decimal-only'], [data-behaviour='filter'][data-filter-type='range']",
8
8
  function (e) {
9
9
  var charCode = e.which ? e.which : e.keyCode;
10
- if (charCode > 31 && charCode != 46 && (charCode < 48 || charCode > 57))
10
+ if (charCode > 31 && charCode != 46 && charCode != 45 && (charCode < 48 || charCode > 57))
11
11
  return false;
12
12
  return true;
13
13
  }
@@ -20,9 +20,9 @@ $(document).on(
20
20
  $(this).val(
21
21
  $(this)
22
22
  .val()
23
- .replace(/[^\d].+/, "")
23
+ .replace(/[^\d].-+/, "")
24
24
  );
25
- if (event.which < 48 || event.which > 57) {
25
+ if ((event.which < 48 || event.which > 57) && (event.which != 45)) {
26
26
  event.preventDefault();
27
27
  }
28
28
  }
@@ -72,9 +72,17 @@ $(document).on("click", '[data-behaviour="offcanvas"]', function (e) {
72
72
  });
73
73
  });
74
74
 
75
- // Select2 focus on open
76
- $(document).on("select2:open", () => {
77
- document.querySelector(".select2-search__field").focus();
75
+ // Select2 search input focus on open
76
+ $(document).on("select2:open", function () {
77
+ // Find the search input inside the Select2 dropdown and focus it
78
+ setTimeout(() => {
79
+ const searchInput = document.querySelector(
80
+ ".select2-container--open .select2-search__field"
81
+ );
82
+ if (searchInput) {
83
+ searchInput.focus();
84
+ }
85
+ }, 100);
78
86
  });
79
87
 
80
88
  $(document).on("click", '[data-bs-dismiss="offcanvas"]', function (e) {
@@ -232,7 +240,8 @@ var replaceAccordionTitle = function (element) {
232
240
  .attr("data-bs-target", "#" + accordion_id);
233
241
  $(this).find(".accordion-collapse").attr("id", accordion_id);
234
242
  });
235
- initializeComponents();
243
+ const dropdownParent = $("#cm-drawer").length > 0 ? "#cm-drawer" : null;
244
+ initializeComponents(dropdownParent);
236
245
  };
237
246
 
238
247
 
@@ -36,13 +36,13 @@ module CmAdmin
36
36
 
37
37
  def validate_cm_path_policy(path_helper)
38
38
  recognized_path= cm_recognize_path(path_helper)
39
-
39
+
40
40
  model_name = recognized_path[:controller].split('/').last.singularize.classify
41
41
  action_name = action(recognized_path[:action])
42
42
  @policy_model = CmAdmin::Model.find_by(name: model_name)
43
43
  policy([:cm_admin, @policy_model&.name&.constantize]).send("#{action_name}?")
44
44
  end
45
-
45
+
46
46
 
47
47
  def action(action_name)
48
48
  case action_name.to_sym
@@ -4,12 +4,33 @@ class GenerateExportFileJob < ApplicationJob
4
4
  def perform(file_export)
5
5
  Current.user = file_export.exported_by
6
6
  CmCurrent.user_permissions = Current.user.cm_role.cm_permissions if Current.user.cm_role.present?
7
+ set_current_for_development
7
8
 
8
9
  file_export.attach_export_file
9
- ExportMailer.export_email(file_export).deliver_now
10
+
11
+ expires_in = (file_export.expires_at - Time.now).to_i
12
+ model = file_export.associated_model_name&.pluralize
13
+ link = file_export.export_file.url(expires_in:)
14
+ expires_at = file_export.expires_at.utc
15
+ html_body = ::FileExport.email_html_body(model, expires_at)
16
+ subject = "#{model} Export is ready for download"
17
+ SendCmAdminEmailJob.perform_later(file_export.exported_by.email, subject, html_body, 'Download Excel', link)
18
+
10
19
  file_export.success!
11
20
  rescue StandardError => e
12
21
  file_export&.failed!
13
22
  raise e
14
23
  end
24
+
25
+ private
26
+
27
+ def set_current_for_development
28
+ return unless Rails.env.development?
29
+
30
+ url = Rails.application.credentials[:be_url]
31
+ host = URI.parse(url).host
32
+ protocol = URI.parse(url).scheme
33
+ port = URI.parse(url).port
34
+ ActiveStorage::Current.url_options = { host:, protocol:, port: }
35
+ end
15
36
  end
@@ -0,0 +1,7 @@
1
+ class SendCmAdminEmailJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(to, subject, body, button_text = nil, button_link = nil)
5
+ CmAdminMailer.cm_admin_mail(to, subject, body, button_text, button_link).deliver_now
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ class CmAdminMailer < ApplicationMailer
2
+ def cm_admin_mail(to, subject, body, button_text = nil, button_link = nil)
3
+ @body = body
4
+ @button_text = button_text
5
+ @button_link = button_link
6
+ mail(to:, subject:)
7
+ end
8
+ end
@@ -46,6 +46,24 @@ module Exportable
46
46
  klass.available_fields[action_name].select(&:exportable)
47
47
  end
48
48
 
49
+ class_methods do
50
+ def email_html_body(model, expires_at)
51
+ <<~HTML
52
+ <p style="color: #333333; font-weight: 700; font-size: 16px; line-height: 36px; margin: 0; margin-bottom: 16px;">
53
+ #{model} Export is ready
54
+ </p>
55
+ <div style="margin-bottom: 40px;">
56
+ <p>
57
+ The download link will expire at <strong>#{expires_at.strftime("%H:%M:%S")} UTC</strong> on <strong>#{expires_at.strftime("%d %B %Y")}.</strong>
58
+ </p>
59
+ <p>
60
+ Make sure to save the file before the link expires.
61
+ </p>
62
+ </div>
63
+ HTML
64
+ end
65
+ end
66
+
49
67
  private
50
68
 
51
69
  def fetch_ar_object(model_object, id)
@@ -58,7 +76,7 @@ module Exportable
58
76
  deserialized_columns = CmAdmin::Utils.deserialize_csv_columns(selected_column_names, :as_json_params)
59
77
  main_model = parent_model || model
60
78
  available_fields = main_model.available_fields[action_name.to_sym]
61
- # This includes isn't recursve, a full solution should be recursive
79
+ # This includes isn't recursive, a full solution should be recursive
62
80
  records_arr = []
63
81
  records.includes(deserialized_columns[:include].keys).find_each do |record|
64
82
  record_hash = record.as_json({ only: selected_column_names.map(&:to_sym) })
@@ -66,12 +84,11 @@ module Exportable
66
84
  break unless available_fields.map(&:field_name).include?(column_name.to_sym)
67
85
 
68
86
  column = CmAdmin::Models::Column.find_by(model, :index, { name: column_name.to_sym })
69
- record_hash[column.field_name] = if column.field_type == :custom
70
- send(column.helper_method, record, column.field_name).to_s
71
- elsif column.field_type == :enum
72
- record.send(column.field_name).to_s.titleize
73
- else
74
- record.send(column.field_name).to_s
87
+ record_hash[column.field_name] = case column.field_type
88
+ when :enum
89
+ record.send(column.export_method).to_s.titleize
90
+ else
91
+ record.send(column.export_method).to_s
75
92
  end
76
93
  end
77
94
  records_arr << record_hash
@@ -0,0 +1,21 @@
1
+ .table-top
2
+ - if @associated_model.filters.present? && @action.partial.nil?
3
+ .cm-index-page__filters
4
+ == render partial: 'cm_admin/main/filters', locals: { filters: @associated_model.filters }
5
+ p.table-top__total-count = "#{humanized_ar_collection_count(@associated_ar_object.pagy.count, @action.child_records.to_s)}"
6
+ div.d-flex.gap-3
7
+ .table-top__column-action
8
+ - if @associated_model && @associated_model.available_actions.map(&:name).include?('new') && has_valid_policy(@associated_ar_object, 'new')
9
+ - association = @ar_object.class.reflect_on_all_associations.select{|x| x.name == @associated_model.name.tableize.to_sym }.first
10
+ - polymorphic_name = (association && association.inverse_of && association.inverse_of.options[:polymorphic]) ? association.inverse_of.name : ''
11
+ a href="#{CmAdmin::Engine.mount_path}/#{@associated_model.name.tableize}/new?associated_id=#{@ar_object.id}&associated_class=#{@ar_object.class.name.underscore}&polymorphic_name=#{polymorphic_name}&referrer=#{request.path}"
12
+ button.btn-secondary Add
13
+ - if @associated_model.sort_columns.present?
14
+ = render 'cm_admin/main/sort', model: @associated_model, ar_object: @associated_ar_object
15
+
16
+ - if flash[:bulk_action_error].present?
17
+ .alert.alert-danger.me-4 role="alert"
18
+ = flash[:bulk_action_error].html_safe
19
+ - if flash[:bulk_action_success].present?
20
+ .alert.alert-success.me-4 role="alert"
21
+ = flash[:bulk_action_success].html_safe
@@ -0,0 +1,40 @@
1
+ .card-list-container
2
+ == render partial: 'cm_admin/main/actions_associated', locals: { cm_model: @model }
3
+ - bulk_actions = actions_filter(@associated_model, @associated_ar_object, :bulk_action)
4
+ - if bulk_actions.present?
5
+ .table-top.hidden data-section="bulk-action"
6
+ - bulk_actions.each do |action|
7
+ = custom_action_items(action, 'index')
8
+ .card-grid
9
+ - @associated_ar_object.data.each do |ar_object|
10
+ .col
11
+ .item-card
12
+ .card-menu
13
+ - if bulk_actions.present?
14
+ .check-box-space
15
+ span
16
+ input.cm-checkbox type="checkbox" data-behaviour="bulk-action-checkbox" data-ar-object-id="#{ar_object.id}"
17
+ - if @associated_model
18
+ .row-action-cell
19
+ == render partial: 'cm_admin/main/actions_dropdown', locals: { cm_model: @associated_model, ar_object: ar_object }
20
+ - attachment = ar_object.profile_picture
21
+ - if attachment
22
+ = image_tag(attachment.url, class: "card-img-top")
23
+ - else
24
+ = image_tag("https://www.hawaiilife.com/images/tile/missing.png", class: "card-img-top")
25
+ .card-body
26
+ - @model.available_fields[@action.name.to_sym].each_with_index do |column, index|
27
+ - if column.display_if.call(Current.user) && column.viewable
28
+ .card-text
29
+ th = "#{column.header.titleize}:"
30
+ - if index == 0 && is_show_action_available(@associated_model, ar_object)
31
+ a href="#{CmAdmin::Engine.mount_path}/#{@associated_model.name.tableize}/#{ar_object.id}" = show_field_value(ar_object, column)
32
+ - else
33
+ = show_field_value(ar_object, column)
34
+ - if column.field_type == :drawer
35
+ = render partial: column.drawer_partial, locals: { ar_object: ar_object }
36
+ .pagination-bar
37
+ p.count-text.m-0 Showing #{number_with_delimiter(@associated_ar_object.pagy.from.to_i)} to #{number_with_delimiter(@associated_ar_object.pagy.to.to_i)} out of #{number_with_delimiter(@associated_ar_object.pagy.count.to_i)}
38
+ == render partial: 'cm_admin/main/cm_pagy_nav', locals: { pagy: @associated_ar_object.pagy }
39
+
40
+ = export_modal(@model, action_name: @action.name.to_sym, associated_klass: @associated_model)
@@ -1,25 +1,4 @@
1
- .table-top
2
- - if @associated_model.filters.present? && @action.partial.nil?
3
- .cm-index-page__filters
4
- == render partial: 'cm_admin/main/filters', locals: { filters: @associated_model.filters }
5
- p.table-top__total-count = "#{humanized_ar_collection_count(@associated_ar_object.pagy.count, @action.child_records.to_s)}"
6
- div.d-flex.gap-3
7
- .table-top__column-action
8
- - if @associated_model && @associated_model.available_actions.map(&:name).include?('new') && has_valid_policy(@associated_ar_object, 'new')
9
- - association = @ar_object.class.reflect_on_all_associations.select{|x| x.name == @associated_model.name.tableize.to_sym }.first
10
- - polymorphic_name = (association && association.inverse_of && association.inverse_of.options[:polymorphic]) ? association.inverse_of.name : ''
11
- a href="#{CmAdmin::Engine.mount_path}/#{@associated_model.name.tableize}/new?associated_id=#{@ar_object.id}&associated_class=#{@ar_object.class.name.underscore}&polymorphic_name=#{polymorphic_name}&referrer=#{request.path}"
12
- button.btn-secondary Add
13
- - if @associated_model.sort_columns.present?
14
- = render 'cm_admin/main/sort', model: @associated_model, ar_object: @associated_ar_object
15
-
16
- - if flash[:bulk_action_error].present?
17
- .alert.alert-danger.me-4 role="alert"
18
- = flash[:bulk_action_error].html_safe
19
- - if flash[:bulk_action_success].present?
20
- .alert.alert-success.me-4 role="alert"
21
- = flash[:bulk_action_success].html_safe
22
-
1
+ == render partial: 'cm_admin/main/actions_associated'
23
2
  - bulk_actions = actions_filter(@associated_model, @associated_ar_object, :bulk_action)
24
3
  - if bulk_actions.present?
25
4
  .table-top.hidden data-section="bulk-action"
@@ -1,8 +1,8 @@
1
1
  - if nested_table_field.display_type == :table
2
2
  tr.nested-fields
3
3
  - if nested_table_field.is_positionable.call(f.object)
4
- td data-behaviour='action-icons'
5
- i class='fa-solid fa-grip-dots-vertical drag-handle pointer'
4
+ td data-behaviour='action-icons' class='drag-handle pointer'
5
+ i class='fa-solid fa-grip-dots-vertical'
6
6
  = f.hidden_field :position, class: 'hidden-position'
7
7
  - if nested_table_field.is_deletable.call(f.object)
8
8
  td.item-delete-cell data-behaviour='action-icons'
@@ -1,3 +1,6 @@
1
1
  .cm-index-page.associated-index
2
2
  .cm-index-page__table-container
3
- == render partial: 'cm_admin/main/associated_table'
3
+ - if params[:view_type] == 'card' || @current_action.view_type == :card
4
+ == render partial: "cm_admin/main/associated_card"
5
+ - else
6
+ == render partial: 'cm_admin/main/associated_table'
@@ -0,0 +1,24 @@
1
+ table style="background: #F2F4F6; padding: 56px 0px; width: 100%; border-spacing: 0 24px; border-collapse: separate;"
2
+ tr
3
+ td colspan="3" style="text-align: center;"
4
+ img src="#{Rails.configuration.x.project_settings.logo_url}" alt="logo" style="height: auto; width: 50px;"
5
+ tr style="width: fit-content;"
6
+ td
7
+ div style="padding: 40px; background: #fff; border-radius: 5px; margin: 0px 24px;"
8
+ h4 style="margin: 0; font-size: 20px; color: #000;" Hello,
9
+ = raw(@body)
10
+ - if @button_link.present? && @button_link.present?
11
+ table width="100%" border="0" cellspacing="0" cellpadding="0"
12
+ tr
13
+ td align='center'
14
+ div style="border: none; background: #6D0094; padding: 16px 32px; width: fit-content; border-radius: 5px;"
15
+ a style="text-decoration: none; color: #fff;" href="#{@button_link}"
16
+ = @button_text
17
+ div
18
+ p style="margin: 0; color: #000; font-size: 14px;"
19
+ strong Best Regards,
20
+ p style="margin: 0; color: #000; font-size: 14px;"
21
+ strong = Rails.configuration.x.project_settings.name
22
+ tr
23
+ td colspan="3" style="text-align: center; color: #5F6166; font-size: 12px;"
24
+ = "© #{Time.current.year} #{Rails.configuration.x.project_settings.name}"
@@ -13,7 +13,8 @@ html
13
13
  script
14
14
  | window.dataLayer = window.dataLayer || [];
15
15
  | dataLayer.push({
16
- | 'projectName': "#{project_name_with_env}"
16
+ | 'projectName': "#{project_name_with_env}",
17
+ | 'userId': "#{current_user&.id}"
17
18
  | });
18
19
  title
19
20
  = project_name_with_env
@@ -0,0 +1,50 @@
1
+ # Sending Email from CM Admin 📧
2
+
3
+ ## Overview
4
+
5
+ The CM Admin email feature allows to send emails directly from the CM Admin. This documentation outlines how to use the CM Admin emails, including its configuration and examples.
6
+
7
+ ## Usage
8
+
9
+ To user CM Admin email use the `CmAdmin.send_email` method.
10
+
11
+ ### Attributes
12
+
13
+ - `to`
14
+
15
+ - Specify to whom mail should be delivered. Takes array or single emails.
16
+
17
+ - `subject`
18
+
19
+ - subject of email. Takes only string
20
+
21
+ - `body`
22
+
23
+ - body of email. takes html string or normal string.
24
+
25
+ - `button_text` (optional) and `button_link` (optional)
26
+ - If email template requires a link with button, use these attributes.
27
+
28
+ ### Examples
29
+
30
+ Here are some examples of how to use the `CmAdmin.send_email` method to send emails:
31
+
32
+ ```ruby
33
+ CmAdmin.send_email(
34
+ "example@example.com",
35
+ "Test Email",
36
+ "<p>This is a test email from CM Admin.</p>"
37
+ )
38
+ ```
39
+
40
+ - With Link Button:
41
+
42
+ ```ruby
43
+ CmAdmin.send_email(
44
+ "example@example.com",
45
+ "Test Email",
46
+ "<p>This is a test email from CM Admin.</p>",
47
+ "This is link",
48
+ "https://google.com"
49
+ )
50
+ ```
@@ -6,8 +6,8 @@ module CmAdmin
6
6
  include Utils::Associations
7
7
 
8
8
  attr_accessor :field_name, :field_type, :header, :format, :prefix, :suffix, :exportable, :round, :height, :width,
9
- :cm_css_class, :link, :url, :custom_method, :helper_method, :managable, :lockable, :drawer_partial, :tag_class,
10
- :display_if, :association_name, :association_type, :viewable, :custom_link
9
+ :cm_css_class, :link, :url, :custom_method, :helper_method, :managable, :lockable, :drawer_partial, :tag_class,
10
+ :display_if, :association_name, :association_type, :viewable, :custom_link, :export_method
11
11
 
12
12
  def initialize(field_name, attributes = {})
13
13
  @field_name = field_name
@@ -21,6 +21,7 @@ module CmAdmin
21
21
  self.height = 50 if self.field_type == :image && self.height.nil?
22
22
  self.width = 50 if self.field_type == :image && self.width.nil?
23
23
  self.display_if = lambda { |arg| return true } if self.display_if.nil?
24
+ self.export_method = self.field_name if export_method.nil?
24
25
 
25
26
  validation_for_association
26
27
  end
@@ -131,19 +131,20 @@ module CmAdmin
131
131
  # @param layout [String] the layout of tab
132
132
  # @param partial [String] the partial path of tab
133
133
  # @param display_if [Proc] A lambda that takes the current object and return true or false
134
+ # @param view_type [Symbol] view type of page +:table+ or +:card+
134
135
  #
135
136
  # @example Creating a tab
136
137
  # tab :comments, 'comment', associated_model: 'comments', layout_type: 'cm_association_index' do
137
138
  # column :message
138
139
  # end
139
- def tab(tab_name, custom_action, associated_model: nil, layout_type: nil, layout: nil, partial: nil, display_if: nil, &block)
140
+ def tab(tab_name, custom_action, associated_model: nil, layout_type: nil, layout: nil, partial: nil, display_if: nil, view_type: nil, &block)
140
141
  if custom_action.to_s == ''
141
142
  @current_action = CmAdmin::Models::Action.find_by(self, name: 'show')
142
143
  @available_tabs << CmAdmin::Models::Tab.new(tab_name, '', display_if, &block)
143
144
  else
144
145
  action = CmAdmin::Models::Action.new(name: custom_action.to_s, verb: :get, path: ':id/' + custom_action,
145
146
  layout_type:, layout:, partial:, child_records: associated_model,
146
- action_type: :custom, display_type: :page, model_name: name)
147
+ action_type: :custom, display_type: :page, model_name: name, view_type:)
147
148
  @available_actions << action
148
149
  @current_action = action
149
150
  @available_tabs << CmAdmin::Models::Tab.new(tab_name, custom_action, display_if, &block)
@@ -191,6 +192,7 @@ module CmAdmin
191
192
  # @param header [String] the header of field
192
193
  # @param format [String] the format of field for date field
193
194
  # @param helper_method [Symbol] the helper method for field, should be defined in custom_helper.rb file, will take two arguments, +record+ and +field_name+
195
+ # @param export_method [Symbol] the export method for field, should be defined in model, will take no arguments
194
196
  # @param height [Integer] the height of field for image field
195
197
  # @param width [Integer] the width of field for image field
196
198
  # @params custom_link [String] the custom link for field
@@ -1,3 +1,3 @@
1
1
  module CmAdmin
2
- VERSION = '4.4.8'
2
+ VERSION = '4.6.5'
3
3
  end
@@ -152,7 +152,7 @@ module CmAdmin
152
152
  def attachment_with_preview(attachment, field, custom_class = nil)
153
153
  content_tag :a, href: attachment.url, target: '_blank' do
154
154
  if attachment.content_type.include?('image')
155
- image_tag(attachment.url, height: field.height, width: field.width, class: "rounded #{custom_class}")
155
+ image_tag(attachment.variant(resize_to_fill: [field.width, field.height]), height: field.height, width: field.width, class: "rounded #{custom_class}")
156
156
  else
157
157
  image_tag('https://cm-admin.s3.ap-south-1.amazonaws.com/gem_static_assets/image_not_available.png', height: 50, width: 50, class: "rounded #{custom_class}")
158
158
  end
@@ -167,7 +167,9 @@ module CmAdmin
167
167
 
168
168
  def user_full_name
169
169
  return false unless current_user
170
+
170
171
  return current_user.full_name if defined?(current_user.full_name)
172
+
171
173
  current_user.email.split('@').first
172
174
  end
173
175
 
data/lib/cm_admin.rb CHANGED
@@ -44,9 +44,14 @@ module CmAdmin
44
44
 
45
45
  def initialize_model(entity, &block)
46
46
  return unless entity.is_a?(Class)
47
+
47
48
  CmAdmin::Model.delete_entity(entity.name) if CmAdmin::Model.find_by({ name: entity.name })
48
49
 
49
50
  config.cm_admin_models << CmAdmin::Model.new(entity, &block)
50
51
  end
52
+
53
+ def send_email(to, subject, body, button_text = nil, button_link = nil)
54
+ SendCmAdminEmailJob.perform_later(to, subject, body, button_text, button_link)
55
+ end
51
56
  end
52
- end
57
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cm-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.8
4
+ version: 4.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael
@@ -13,7 +13,7 @@ authors:
13
13
  - Austin
14
14
  bindir: exe
15
15
  cert_chain: []
16
- date: 2025-01-17 00:00:00.000000000 Z
16
+ date: 2025-01-31 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: caxlsx_rails
@@ -362,7 +362,8 @@ files:
362
362
  - app/helpers/cm_admin/permission_helper.rb
363
363
  - app/jobs/file_import_processor_job.rb
364
364
  - app/jobs/generate_export_file_job.rb
365
- - app/mailers/export_mailer.rb
365
+ - app/jobs/send_cm_admin_email_job.rb
366
+ - app/mailers/cm_admin_mailer.rb
366
367
  - app/models/cm_current.rb
367
368
  - app/models/cm_permission.rb
368
369
  - app/models/cm_role.rb
@@ -373,8 +374,10 @@ files:
373
374
  - app/models/file_export.rb
374
375
  - app/models/file_import.rb
375
376
  - app/policies/cm_admin/file_import_policy.rb
377
+ - app/views/cm_admin/main/_actions_associated.html.slim
376
378
  - app/views/cm_admin/main/_actions_dropdown.html.slim
377
379
  - app/views/cm_admin/main/_alert_banner.html.slim
380
+ - app/views/cm_admin/main/_associated_card.html.slim
378
381
  - app/views/cm_admin/main/_associated_table.html.slim
379
382
  - app/views/cm_admin/main/_card.html.slim
380
383
  - app/views/cm_admin/main/_cm_pagy_nav.html.slim
@@ -405,7 +408,7 @@ files:
405
408
  - app/views/cm_admin/static/dashboard.html.slim
406
409
  - app/views/cm_admin/static/error_401.html.slim
407
410
  - app/views/cm_admin/static/error_403.html.slim
408
- - app/views/export_mailer/export_email.html.slim
411
+ - app/views/cm_admin_mailer/cm_admin_mail.html.slim
409
412
  - app/views/layouts/_cm_flash_message.html.slim
410
413
  - app/views/layouts/_custom_action_modal.html.slim
411
414
  - app/views/layouts/_custom_action_modals.html.slim
@@ -424,6 +427,7 @@ files:
424
427
  - config/importmap.rb
425
428
  - config/routes.rb
426
429
  - docs/AddingAlert.md
430
+ - docs/CmAdminEmail.md
427
431
  - docs/CustomFilterMethod.md
428
432
  - docs/ListingSelectTwoAjax.md
429
433
  - docs/ListingSelectTwoItems.md
@@ -1,22 +0,0 @@
1
- class ExportMailer < ApplicationMailer
2
- def export_email(file_export)
3
- set_current_for_development
4
- expires_in = (file_export.expires_at - Time.now).to_i
5
- @model = file_export.associated_model_name&.pluralize
6
- @link = file_export.export_file.url(expires_in:)
7
- @expires_at = file_export.expires_at.utc
8
- mail(to: file_export&.exported_by&.email, subject: "#{@model} Export is ready for download")
9
- end
10
-
11
- private
12
-
13
- def set_current_for_development
14
- return unless Rails.env.development?
15
-
16
- url = Rails.application.credentials[:be_url]
17
- host = URI.parse(url).host
18
- protocol = URI.parse(url).scheme
19
- port = URI.parse(url).port
20
- ActiveStorage::Current.url_options = { host:, protocol:, port: }
21
- end
22
- end
@@ -1,19 +0,0 @@
1
- table style="background: #F2F4F6; padding: 56px 0px; width: 100%; border-spacing: 24px;"
2
- tr
3
- td colspan="3" style="text-align: center; margin-bottom: 24px;"
4
- img src="#{Rails.configuration.x.project_settings.logo_url}" alt="logo" style="height: auto; width: 50px;"
5
- tr style="width: fit-content; style=width: 100px;"
6
- td
7
- td style="background: #fff; padding: 40px; padding-bottom: 56px;"
8
- p style="color: #333333; font-weight: 700; font-size: 24px; line-height: 36px; margin: 0; margin-bottom: 24px;"
9
- = "#{@model} Export is ready"
10
- div style="margin-bottom: 40px;"
11
- p
12
- | The download link will expire at <strong>#{@expires_at.strftime("%H:%M:%S")} UTC</strong> on <strong>#{@expires_at.strftime("%d %B %Y")}.</strong>
13
- p Make sure to save the file before the link expires.
14
- a style="border: none; background: #6D0094; color: #fff; padding: 16px 32px; width: fit-content; border-radius: 5px; text-decoration: none;" href="#{@link}"
15
- | Download Excel
16
- td
17
- tr
18
- td colspan="3" style="text-align: center; color: #5F6166; font-size: 16px; margin-top: 24px;"
19
- = "© #{Time.current.year} #{Rails.configuration.x.project_settings.name}"