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 +4 -4
- data/Gemfile.lock +1 -1
- data/app/assets/javascripts/cm_admin/exports.js +16 -15
- data/app/assets/javascripts/cm_admin/initialize_components.js +14 -6
- data/app/assets/javascripts/cm_admin/scaffolds.js +87 -53
- data/app/assets/javascripts/cm_admin/shared_scaffolds.js +16 -7
- data/app/helpers/cm_admin/application_helper.rb +2 -2
- data/app/jobs/generate_export_file_job.rb +22 -1
- data/app/jobs/send_cm_admin_email_job.rb +7 -0
- data/app/mailers/cm_admin_mailer.rb +8 -0
- data/app/models/concerns/exportable.rb +24 -7
- data/app/views/cm_admin/main/_actions_associated.html.slim +21 -0
- data/app/views/cm_admin/main/_associated_card.html.slim +40 -0
- data/app/views/cm_admin/main/_associated_table.html.slim +1 -22
- data/app/views/cm_admin/main/_nested_fields.html.slim +2 -2
- data/app/views/cm_admin/main/associated_index.html.slim +4 -1
- data/app/views/cm_admin_mailer/cm_admin_mail.html.slim +24 -0
- data/app/views/layouts/cm_admin.html.slim +2 -1
- data/docs/CmAdminEmail.md +50 -0
- data/lib/cm_admin/models/column.rb +3 -2
- data/lib/cm_admin/models/dsl_method.rb +4 -2
- data/lib/cm_admin/version.rb +1 -1
- data/lib/cm_admin/view_helpers/field_display_helper.rb +1 -1
- data/lib/cm_admin/view_helpers/page_info_helper.rb +2 -0
- data/lib/cm_admin.rb +6 -1
- metadata +8 -4
- data/app/mailers/export_mailer.rb +0 -22
- data/app/views/export_mailer/export_email.html.slim +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b2fb69e4b0dd8324fcf648c7fa0309e86208cf5cb580ed74897013c8fe1c1d2
|
4
|
+
data.tar.gz: 249eadf19d6edc9bebc6d7798c3ba3203f0429d7f1b18ede7e3e6264a9a85a5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5070c55b836e1eebe92f4548eb5a79b567506e760d38ea4938dc42e72555dcf9d9d07788b5903c4d15cda3180aee7f9101055e15cd3153af2d1847261e8ebda4
|
7
|
+
data.tar.gz: 2d8a1a41e1155f6060546cc1d61d04457c9b1ab182dd03c50a065fe40c48b16cee8bb2535578763303bfa922f364a60ff33573244144e6b73ce812576a16918d
|
data/Gemfile.lock
CHANGED
@@ -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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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(
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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(
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
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)
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
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] =
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
10
|
-
|
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
|
data/lib/cm_admin/version.rb
CHANGED
@@ -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.
|
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
|
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
|
+
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-
|
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/
|
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/
|
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}"
|