cm-admin 2.3.3 → 2.4.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 +1 -1
  3. data/app/assets/javascripts/cm_admin/scaffolds.js +43 -28
  4. data/app/assets/javascripts/cm_admin/shared_scaffolds.js +109 -38
  5. data/app/assets/stylesheets/cm_admin/base/scaffold.scss +6 -0
  6. data/app/assets/stylesheets/cm_admin/cm_admin.css.scss +0 -1
  7. data/app/assets/stylesheets/cm_admin/components/_drawer.scss +17 -100
  8. data/app/assets/stylesheets/cm_admin/scaffold.scss +0 -14
  9. data/app/controllers/cm_admin/resource_controller.rb +45 -9
  10. data/app/javascript/stylesheets/cm_admin/application.scss +0 -1
  11. data/app/views/cm_admin/main/_associated_table.html.slim +7 -6
  12. data/app/views/cm_admin/main/_card.html.slim +0 -11
  13. data/app/views/cm_admin/main/_kanban.html.slim +0 -7
  14. data/app/views/cm_admin/main/_table.html.slim +6 -7
  15. data/app/views/cm_admin/main/edit.html.slim +1 -0
  16. data/app/views/cm_admin/main/new.html.slim +4 -3
  17. data/app/views/layouts/_cm_flash_message.html.slim +3 -6
  18. data/app/views/layouts/_drawer.html.slim +23 -0
  19. data/app/views/layouts/cm_admin.html.slim +1 -1
  20. data/config/importmap.rb +0 -1
  21. data/config/routes.rb +4 -0
  22. data/lib/cm_admin/model.rb +1 -1
  23. data/lib/cm_admin/models/action.rb +11 -7
  24. data/lib/cm_admin/models/form_field.rb +2 -1
  25. data/lib/cm_admin/version.rb +1 -1
  26. data/lib/cm_admin/view_helpers/filter_helper.rb +2 -2
  27. data/lib/cm_admin/view_helpers/form_helper.rb +37 -8
  28. data/lib/cm_admin/view_helpers/page_info_helper.rb +21 -16
  29. data/lib/cm_admin/view_helpers.rb +11 -0
  30. metadata +3 -4
  31. data/app/assets/stylesheets/cm_admin/dependency/jquery-jgrowl.min.css +0 -1
  32. data/app/views/cm_admin/main/_drawer.html.slim +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f85a062f8d926712d7817aeb392c0233208690eacbefeccdbcadb90ac6e85cf
4
- data.tar.gz: eb59e86b5c40e9e592d8c1c488cbb5bf9ab2d7de5e66fbad97c0abb401449688
3
+ metadata.gz: 78211818c7078c8cf9723ca166d19c9eaea3155544754a60fca317a8eaf86276
4
+ data.tar.gz: 7a53125a99db46cc8a7686076baf674eb971f20430bf680c5c0e105b06c89dc1
5
5
  SHA512:
6
- metadata.gz: e4bf5998df379c42994928003eac0f18770c1650983520aae1fd2940f062d239f567066ea16469b1c7fa5135d9d77a0307be7df13c2a34342625b02da0ed2445
7
- data.tar.gz: 0a7d326057c1a49aff2ef3db674ab7e21f1634634f1b527d4cb85bc535c49c0ef441b102bcb16b6d8077f33f5866d333b49c9ab6d096c9833bd7da019c43e961
6
+ metadata.gz: a2da45fd35ae854471c2c0255359fc96df661ebcb482c452a65734daf3364465bfe7cbb5250488a13be7381a7c129af90288b31d5c364c65bd878523ed8c75b8
7
+ data.tar.gz: a7ed71594d873f6c525577f2f19a48efe68bb873f331ab9b49bec2b18b980ea03bdd86fcbd8d0d83d2eb78bb3a3805e97626806c83ac43b138b88dc376638335
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cm-admin (2.3.3)
4
+ cm-admin (2.4.0)
5
5
  caxlsx_rails
6
6
  cocoon (~> 1.2.15)
7
7
  csv-importer (~> 0.8.2)
@@ -15,7 +15,6 @@ import "moment";
15
15
  import "bootstrap";
16
16
  import "@popperjs/core";
17
17
  import "flatpickr";
18
- import "jgrowl";
19
18
  import "trix";
20
19
  import "@rails/actiontext";
21
20
  import Select2 from "select2";
@@ -23,7 +22,6 @@ Select2();
23
22
 
24
23
  // import '@nathanvda/cocoon'
25
24
  import "daterangepicker";
26
- import jqueryJgrowl from "jgrowl";
27
25
 
28
26
  document.addEventListener("turbo:load", function () {
29
27
  flatpickr("[data-behaviour='date-only']", {
@@ -38,19 +36,25 @@ document.addEventListener("turbo:load", function () {
38
36
  $(".select-2").select2({
39
37
  theme: "bootstrap-5",
40
38
  });
41
- jqueryJgrowl();
42
39
  setup_select_2_ajax();
43
- var el = $("[data-section='nested-form-body']")
44
- if(el[0]) {
45
- Sortable.create(el[0],{
46
- handle: '.drag-handle',
40
+ const bsToast = $('[data-behaviour="toast"]')[0];
41
+ if (bsToast) {
42
+ const toast = new bootstrap.Toast(bsToast);
43
+ toast.show();
44
+ }
45
+ var el = $("[data-section='nested-form-body']");
46
+ if (el[0]) {
47
+ Sortable.create(el[0], {
48
+ handle: ".drag-handle",
47
49
  animation: 150,
48
50
  onUpdate: function (evt) {
49
- var itemEl = evt.item
50
- $("[data-section='nested-form-body'] tr").each(function(index, el){
51
- $(el).find('.hidden-position').val(index+1)
52
- })
53
- }
51
+ var itemEl = evt.item;
52
+ $("[data-section='nested-form-body'] tr").each(function (index, el) {
53
+ $(el)
54
+ .find(".hidden-position")
55
+ .val(index + 1);
56
+ });
57
+ },
54
58
  });
55
59
  }
56
60
  });
@@ -84,18 +88,25 @@ $(document).on(
84
88
  "click",
85
89
  '[data-behaviour="permission-check-box"]',
86
90
  function (e) {
87
- var form_check = $(this).closest('.form-check')
88
- var form_field = $(this).closest('.form-field')
89
- if($(this).is(':checked')){
90
- form_check.find('.cm-select-tag').removeClass('hidden')
91
+ var form_check = $(this).closest(".form-check");
92
+ var form_field = $(this).closest(".form-field");
93
+ if ($(this).is(":checked")) {
94
+ form_check.find(".cm-select-tag").removeClass("hidden");
91
95
  } else {
92
- form_check.find('.cm-select-tag').addClass('hidden')
96
+ form_check.find(".cm-select-tag").addClass("hidden");
93
97
  }
94
- if (form_field.find('[data-behaviour="permission-check-box"]:checked').length == form_field.find('[data-behaviour="permission-check-box"]').length) {
95
- form_field.find('[data-behaviour="permission-check-all"]').prop('checked', true)
96
- }
97
- else {
98
- form_field.find('[data-behaviour="permission-check-all"]').prop('checked', false)
98
+ if (
99
+ form_field.find('[data-behaviour="permission-check-box"]:checked')
100
+ .length ==
101
+ form_field.find('[data-behaviour="permission-check-box"]').length
102
+ ) {
103
+ form_field
104
+ .find('[data-behaviour="permission-check-all"]')
105
+ .prop("checked", true);
106
+ } else {
107
+ form_field
108
+ .find('[data-behaviour="permission-check-all"]')
109
+ .prop("checked", false);
99
110
  }
100
111
  }
101
112
  );
@@ -104,13 +115,17 @@ $(document).on(
104
115
  "click",
105
116
  '[data-behaviour="permission-check-all"]',
106
117
  function (e) {
107
- var form_field = $(this).closest('.form-field')
108
- if($(this).is(':checked')){
109
- form_field.find('[data-behaviour="permission-check-box"]').prop('checked', true)
110
- form_field.find('.cm-select-tag').removeClass('hidden')
118
+ var form_field = $(this).closest(".form-field");
119
+ if ($(this).is(":checked")) {
120
+ form_field
121
+ .find('[data-behaviour="permission-check-box"]')
122
+ .prop("checked", true);
123
+ form_field.find(".cm-select-tag").removeClass("hidden");
111
124
  } else {
112
- form_field.find('[data-behaviour="permission-check-box"]').prop('checked', false)
113
- form_field.find('.cm-select-tag').addClass('hidden')
125
+ form_field
126
+ .find('[data-behaviour="permission-check-box"]')
127
+ .prop("checked", false);
128
+ form_field.find(".cm-select-tag").addClass("hidden");
114
129
  }
115
130
  }
116
131
  );
@@ -39,36 +39,107 @@ $(document).on("mouseleave", ".row-action-cell", function () {
39
39
  $(this).find(".table-export-popup").addClass("hidden");
40
40
  });
41
41
 
42
- $(document).on("click", ".drawer-btn", function (e) {
43
- e.stopPropagation();
44
- var drawer_el = $(this).parent().closest(".drawer").find(".cm-drawer");
45
- if (drawer_el.hasClass("hidden")) {
46
- drawer_el.removeClass("hidden");
47
- drawer_container = drawer_el.find(".drawer-container");
48
- if (drawer_container.hasClass("drawer-slide-out")) {
49
- drawer_container.removeClass("drawer-slide-out");
50
- }
51
- drawer_container.addClass("drawer-slide-in");
52
- } else {
53
- return drawer_el.addClass("hidden");
54
- }
42
+ $(document).on("click", '[data-behaviour="offcanvas"]', function (e) {
43
+ const drawerFetchUrl = $(this).attr("data-drawer-fetch-url");
44
+ const drawerContainer = $("[data-behaviour='cm-drawer-container']");
45
+
46
+ if (!drawerFetchUrl || !drawerContainer) return;
47
+
48
+ $.ajax({
49
+ url: drawerFetchUrl,
50
+ method: "GET",
51
+ success: function (response) {
52
+ drawerContainer.html(response);
53
+ const drawerForm = new bootstrap.Offcanvas(
54
+ drawerContainer.children().first()
55
+ );
56
+ drawerForm.show();
57
+ initializeComponents();
58
+ handleDrawerFormSubmission(drawerForm);
59
+ },
60
+ error: function (error) {
61
+ console.error("Error:", error);
62
+ },
63
+ });
55
64
  });
56
65
 
57
- $(document).on("click", ".drawer-close", function (e) {
58
- e.stopPropagation();
59
- $(".drawer-container").removeClass("drawer-slide-in");
60
- $(".drawer-container").addClass("drawer-slide-out");
61
- setTimeout(() => {
62
- $(".cm-drawer").addClass("hidden");
63
- }, 300);
66
+ $(document).on("click", '[data-bs-dismiss="offcanvas"]', function (e) {
67
+ $(document).off("submit", "[data-is-drawer-form='true']");
64
68
  });
65
69
 
70
+ function handleDrawerFormSubmission(drawerForm) {
71
+ $(document).on("submit", "[data-is-drawer-form='true']", function (e) {
72
+ e.preventDefault();
73
+ let url = $(this).attr("action");
74
+ if (url.charAt(url.length - 1) === "/") {
75
+ url = url.slice(0, -1);
76
+ }
77
+ url = `${url}.json`;
78
+ $.ajaxSetup({
79
+ headers: {
80
+ "X-CSRF-Token": $('meta[name="csrf-token"]').attr("content"),
81
+ },
82
+ });
83
+ const isMultipart = $(this).attr("enctype") === "multipart/form-data";
84
+ const ajaxOptions = {
85
+ url: url,
86
+ method: $(this).attr("method"),
87
+ data: null,
88
+ success: function (response) {
89
+ $('[data-behavior="flag-alert"]').addClass("hidden");
90
+ drawerForm.hide();
91
+ $(document).off("submit", "[data-is-drawer-form='true']");
92
+ const fromFieldId = $('[data-behavior="cm-drawer"]').attr(
93
+ "data-from-field-id"
94
+ );
95
+ const fromField = $(`#${fromFieldId}`);
96
+ toastMessage(response?.message);
97
+ const bsToast = $('[data-behaviour="drawer-toast"]')[0];
98
+ if (bsToast) {
99
+ const toast = new bootstrap.Toast(bsToast);
100
+ toast.show();
101
+ }
102
+ fromField.append(
103
+ `<option value="${response?.data?.id}">${response?.data?.name}</option>`
104
+ );
105
+ fromField.val(response?.data?.id).change();
106
+ },
107
+ error: function (error) {
108
+ $('[data-behavior="flag-alert"]').removeClass("hidden");
109
+ $('[data-behavior="error-list"]').html(error?.responseJSON?.message);
110
+ },
111
+ };
112
+ if (isMultipart) {
113
+ ajaxOptions["data"] = new FormData($(this)[0]);
114
+ ajaxOptions["contentType"] = false;
115
+ ajaxOptions["processData"] = false;
116
+ $.ajax(ajaxOptions);
117
+ } else {
118
+ ajaxOptions["data"] = $(this).serialize();
119
+ $.ajax(ajaxOptions);
120
+ }
121
+ });
122
+ }
123
+
124
+ function toastMessage(message) {
125
+ const toastHtml = `
126
+ <div class="cm-toast-container">
127
+ <div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-behaviour="drawer-toast">
128
+ <div class="d-flex">
129
+ <div class="toast-body">${message}</div>
130
+ <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ `;
135
+ $('[data-behaviour="flash-container"]').html(toastHtml);
136
+ }
137
+
66
138
  $(document).on("change", '[data-field-type="linked-field"]', function (e) {
67
139
  e.stopPropagation();
68
140
  var params = {};
69
141
  params[$(this).data("field-name")] = $(this).val();
70
142
  var request_url = $(this).data("target-url") + "?" + $.param(params);
71
- console.log(request_url);
72
143
  $.ajax(request_url, {
73
144
  type: "GET",
74
145
  success: function (data) {
@@ -147,31 +218,31 @@ var replaceAccordionTitle = function (element) {
147
218
  .attr("data-bs-target", "#" + accordion_id);
148
219
  $(this).find(".accordion-collapse").attr("id", accordion_id);
149
220
  });
150
- initializeComponents();
221
+ initializeComponents();
151
222
  };
152
223
 
153
224
  export function initializeComponents() {
154
- $('.select-2').select2({
225
+ $(".select-2").select2({
155
226
  theme: "bootstrap-5",
156
227
  });
157
228
  flatpickr("[data-behaviour='date-only']", {
158
- dateFormat: "d-m-Y"
159
- })
229
+ dateFormat: "d-m-Y",
230
+ });
160
231
  flatpickr("[data-behaviour='date-time']", {
161
- enableTime: true
162
- })
232
+ enableTime: true,
233
+ });
163
234
  flatpickr("[data-behaviour='filter'][data-filter-type='date']", {
164
- mode: 'range'
165
- })
166
- var el = document.getElementsByClassName('columns-list')
167
- if(el[0]) {
168
- Sortable.create(el[0],{
169
- handle: '.dragger',
170
- animation: 150
235
+ mode: "range",
236
+ });
237
+ var el = document.getElementsByClassName("columns-list");
238
+ if (el[0]) {
239
+ Sortable.create(el[0], {
240
+ handle: ".dragger",
241
+ animation: 150,
171
242
  });
172
243
  }
173
244
 
174
- var headerElemHeight = $('.page-top-bar').height() + 64
175
- var calculatedHeight = "calc(100vh - " + headerElemHeight+"px"+")"
176
- $('.table-wrapper').css("maxHeight", calculatedHeight);
177
- }
245
+ var headerElemHeight = $(".page-top-bar").height() + 64;
246
+ var calculatedHeight = "calc(100vh - " + headerElemHeight + "px" + ")";
247
+ $(".table-wrapper").css("maxHeight", calculatedHeight);
248
+ }
@@ -126,3 +126,9 @@ a {
126
126
  padding: 0.375rem 0.75rem 0.375rem 0.75rem !important;
127
127
  background-position: right 0.75rem center !important;
128
128
  }
129
+
130
+ .cm-toast-container {
131
+ @extend .position-fixed, .bottom-0, .end-0, .p-3;
132
+
133
+ z-index: 11;
134
+ }
@@ -21,5 +21,4 @@
21
21
  @import "./base/alertbanner";
22
22
  @import "./components/index";
23
23
  @import "./dependency/flatpickr.min";
24
- @import "./dependency/jquery-jgrowl.min";
25
24
  @import "./scaffold";
@@ -1,110 +1,27 @@
1
- @import "../helpers/index.scss";
2
-
3
1
  .cm-drawer {
4
- position: fixed;
5
- top: 0;
6
- right: 0;
7
- bottom: 0;
8
- left: 0;
9
- width: 100%;
10
- height: 100%;
11
- z-index: 5;
12
- background-color: rgba(0, 0, 0, 0.7);
13
- &__container {
14
- position: relative;
15
- width: 360px;
16
- height: 100%;
17
- background: $white;
18
- box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.16);
19
- .header {
20
- display: flex;
21
- align-items: center;
22
- justify-content: space-between;
23
- padding: 24px;
24
- background: $grey-lightest-clr;
25
- &__lhs {
26
- .title {
27
- @include font($size: $t1-text, $color: $primary-text-clr, $weight: 600);
28
- font-family: $primary-font;
29
- line-height: 28px;
30
- margin-bottom: 8px;
31
- }
32
- .description {
33
- @include font($size: $t4-text, $color: $primary-text-clr);
34
- font-family: $primary-font;
35
- line-height: 22px;
36
- margin-bottom: 0;
37
- }
38
- }
39
- &__rhs {
40
- color: $ink-lighter-clr;
41
- }
42
- }
43
- .body {
44
- padding: 24px;
45
- .info-text {
46
- @include font($size: $t4-text, $color: $primary-text-clr);
47
- font-family: $primary-font;
48
- line-height: 22px;
49
- }
50
- }
51
- }
52
- }
53
-
54
- .drawer-slide-in {
55
- animation: slideInLeft 0.3s ease-in forwards;
2
+ @extend .offcanvas-end;
3
+ width: 640px !important;
56
4
  }
57
5
 
58
- .drawer-slide-out {
59
- animation: slideOutLeft 0.3s ease-in forwards;
6
+ .offcanvas-body {
7
+ padding: 0;
60
8
  }
61
9
 
62
- @keyframes slideInLeft {
63
- 0% {
64
- left: -400px;
65
- opacity: 0;
66
- }
67
- 100% {
68
- left: 0;
69
- opacity: 1;
70
- }
71
- }
72
-
73
- @keyframes slideOutLeft {
74
- 0% {
75
- left: 0;
76
- opacity: 1;
77
- }
78
- 100% {
79
- left: -400px;
80
- opacity: 0;
81
- }
10
+ .drawer-btn {
11
+ @extend .btn-link;
12
+ margin-left: auto;
13
+ margin-bottom: 0;
14
+ font-size: $t3-text !important;
82
15
  }
83
16
 
84
- .drawer-btn {
85
- cursor: pointer;
86
- margin-left: 8px;
87
- @include font($size: $t4-text, $color: $brand-color);
17
+ .drawer-btn-group {
18
+ @extend .d-flex, .flex-column, .gap-2;
19
+ position: absolute;
20
+ margin: 16px 0 0 -47px;
88
21
  }
89
22
 
90
- //Kanban drawer styles
91
- .kanban-drawer {
92
- .cm-drawer {
93
- display: flex;
94
- justify-content: flex-end;
95
- &__actions {
96
- display: flex;
97
- flex-direction: column;
98
- margin: 16px;
99
- }
100
- &__container {
101
- width: 624px;
102
- [class^="col-"] {
103
- width: 100% !important;
104
- }
105
- .show-page__inner {
106
- height: calc(100vh - 126px);
107
- }
108
- }
109
- }
23
+ .drawer-nav-btn {
24
+ @extend .btn-ghost;
25
+ width: 31px;
26
+ height: 31px;
110
27
  }
@@ -1,19 +1,5 @@
1
1
  @import "helpers/index.scss";
2
2
 
3
- .jGrowl {
4
- color: $ink-regular-clr !important;
5
- font-size: $t4-text !important;
6
- .success {
7
- background-color: $green-light-clr !important;
8
- }
9
- .notice {
10
- background-color: $blue-light-clr !important;
11
- }
12
- .error {
13
- background-color: $red-light-clr !important;
14
- }
15
- }
16
-
17
3
  .nested-field-wrapper {
18
4
  label {
19
5
  font-size: $t4-text;
@@ -85,9 +85,9 @@ module CmAdmin
85
85
  redirect_url = request.referrer || cm_admin.send("#{@model.name.underscore}_index_path")
86
86
  respond_to do |format|
87
87
  if @ar_object.destroy
88
- format.html { redirect_back fallback_location: redirect_url, notice: "#{action_name.titleize} #{@ar_object.class.name.downcase} is successful" }
88
+ format.html { redirect_back fallback_location: redirect_url, notice: "#{@model.formatted_name} was deleted" }
89
89
  else
90
- format.html { redirect_back fallback_location: redirect_url, notice: "#{action_name.titleize} #{@ar_object.class.name.downcase} is unsuccessful" }
90
+ format.html { redirect_back fallback_location: redirect_url, alert: "#{@model.formatted_name} could not be deleted" }
91
91
  end
92
92
  end
93
93
  end
@@ -114,12 +114,19 @@ module CmAdmin
114
114
  @bulk_action_processor = CmAdmin::BulkActionProcessor.new(@action, @model, params).perform_bulk_action
115
115
  respond_to do |format|
116
116
  if @bulk_action_processor.invalid_records.empty?
117
- format.html { redirect_to request.referrer, notice: "#{@action.name.humanize} is successful" }
117
+ flash[:notice] = "#{@action.formatted_name} was successful"
118
+ flash[:bulk_action_success] = "<b>#{@action.formatted_name} was successful</b>"
119
+ format.html { redirect_to request.referrer }
118
120
  else
119
121
  error_messages = @bulk_action_processor.invalid_records.map do |invalid_record|
120
122
  "<li>#{invalid_record.error_message}</li>"
121
123
  end.join
122
- format.html { redirect_to request.referrer, alert: "<b>#{@action.name.humanize} is unsuccessful</b><br /><ul>#{error_messages}</ul>" }
124
+ flash[:alert] = "#{@action.formatted_name} was unsuccessful"
125
+ flash[:bulk_action_error] = "<div>
126
+ <div><b>#{@action.formatted_name} encountered the following errors:</b></div>
127
+ <ul>#{error_messages}</ul>
128
+ </div>"
129
+ format.html { redirect_to request.referrer }
123
130
  end
124
131
  end
125
132
  end
@@ -161,13 +168,13 @@ module CmAdmin
161
168
  format.json { render json: response_object }
162
169
  elsif response_object.errors.empty?
163
170
  redirect_url = @model.current_action.redirection_url || @action.redirection_url || request.referrer || "/cm_admin/#{@model.ar_model.table_name}/#{@response_object.id}"
164
- format.html { redirect_to redirect_url, notice: "#{@action.name.titleize} is successful" }
171
+ format.html { redirect_to redirect_url, notice: "#{@action.formatted_name} was successful" }
165
172
  else
166
173
  error_messages = response_object.errors.full_messages.map { |error_message| "<li>#{error_message}</li>" }.join
167
- format.html { redirect_to request.referrer, alert: "<b>#{@action.name.titleize} is unsuccessful</b><br /><ul>#{error_messages}</ul>" }
174
+ format.html { redirect_to request.referrer, alert: "#{@action.formatted_name} was unsuccessful<br /><ul>#{error_messages}</ul>" }
168
175
  end
169
176
  rescue StandardError => e
170
- format.html { redirect_to request.referrer, alert: "<div><b>#{@action.name.titleize} is unsuccessful</b><br /><ul><li>#{e.message}</li></ul></div>" }
177
+ format.html { redirect_to request.referrer, alert: "<div>#{@action.formatted_name} was unsuccessful<br /><ul><li>#{e.message}</li></ul></div>" }
171
178
  end
172
179
  end
173
180
  end
@@ -191,6 +198,16 @@ module CmAdmin
191
198
  @model.default_sort_direction = nil
192
199
  end
193
200
 
201
+ def fetch_drawer
202
+ @model = Model.find_by({ name: controller_name.classify })
203
+ return if @model.blank?
204
+
205
+ action_page_title = CmAdmin::Models::Action.find_by(@model, name: 'new').page_title
206
+ drawer_title = action_page_title.presence || "New #{@model&.formatted_name}"
207
+ @ar_object = @model.ar_model.new
208
+ render partial: 'layouts/drawer', locals: { drawer_title:, from_field_id: params[:from_field_id] }
209
+ end
210
+
194
211
  def get_nested_table_fields(fields)
195
212
  nested_table_fields = []
196
213
  fields.each do |field|
@@ -233,9 +250,28 @@ module CmAdmin
233
250
  cm_admin.send("#{@model.name.underscore}_show_path", @ar_object)
234
251
  end
235
252
  ActiveStorage::Attachment.where(id: params['attachment_destroy_ids']).destroy_all if params['attachment_destroy_ids'].present?
236
- format.html { redirect_to redirect_url, allow_other_host: true, notice: "#{action_name.titleize} #{@ar_object.class.name.downcase} is successful" }
253
+ notice = if @current_action&.name == 'new'
254
+ "#{@model&.formatted_name} was created"
255
+ elsif @current_action&.name == 'edit'
256
+ "#{@model&.formatted_name} was updated"
257
+ else
258
+ "#{@action&.formatted_name} #{@model&.formatted_name} was successful"
259
+ end
260
+ format.html { redirect_to redirect_url, allow_other_host: true, notice: }
261
+ name = if @ar_object.respond_to?(:formatted_name)
262
+ @ar_object.formatted_name
263
+ elsif @ar_object.respond_to?(:name)
264
+ @ar_object.name
265
+ else
266
+ @ar_object&.id
267
+ end
268
+ format.json { render json: { message: notice, data: { id: @ar_object&.id, name: } }, status: :ok }
237
269
  else
238
- format.html { render '/cm_admin/main/new', notice: "#{action_name.titleize} #{@ar_object.class.name.downcase} is unsuccessful" }
270
+ format.html { render '/cm_admin/main/new', notice: "#{@action&.formatted_name} #{@model&.formatted_name} was unsuccessful" }
271
+ format.json do
272
+ formatted_error_response = @ar_object.errors.full_messages.map { |error_message| "<li>#{error_message}</li>" }.join
273
+ render json: { message: formatted_error_response }, status: :unprocessable_entity
274
+ end
239
275
  end
240
276
  end
241
277
  end
@@ -1,4 +1,3 @@
1
1
  @import "bootstrap/scss/bootstrap";
2
2
  @import "flatpickr/dist/flatpickr";
3
3
  @import "../../../../node_modules/daterangepicker/daterangepicker.css";
4
- @import "jgrowl/jquery.jgrowl";
@@ -13,12 +13,13 @@
13
13
  - if @associated_model.sort_columns.present?
14
14
  = render 'cm_admin/main/sort', model: @associated_model, ar_object: @associated_ar_object
15
15
 
16
- - if flash[:alert].present?
17
- .alert.alert-danger role="alert"
18
- = flash[:alert].html_safe
19
- - elsif flash[:notice].present?
20
- .alert.alert-success
21
- = flash[:notice].html_safe
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
+
22
23
  - bulk_actions = actions_filter(@associated_model, @associated_ar_object, :bulk_action)
23
24
  - if bulk_actions.present?
24
25
  .table-top.hidden data-section="bulk-action"
@@ -8,17 +8,6 @@
8
8
  i.fa.fa-table
9
9
  a.btn.btn-ghost href="#{cm_admin.send("#{@model.name.underscore}_index_path")}?page=#{params[:page] || 1}&view_type=card"
10
10
  i.fa.fa-table-cells
11
- / button.secondary-btn.column-btn data-bs-target="#columnActionModal" data-bs-toggle="modal"
12
- / span
13
- / i.fa.fa-columns.bolder
14
- / span
15
- / i.fa.fa-angle-down
16
- - if flash[:alert].present?
17
- .alert.alert-danger role="alert"
18
- = flash[:alert].html_safe
19
- - elsif flash[:notice].present?
20
- .alert.alert-success
21
- = flash[:notice].html_safe
22
11
 
23
12
  - bulk_actions = actions_filter(@model, @ar_object, :bulk_action)
24
13
  - if bulk_actions.present?
@@ -1,10 +1,3 @@
1
- - if flash[:alert].present?
2
- .alert.alert-danger role="alert"
3
- = flash[:alert].html_safe
4
- - elsif flash[:notice].present?
5
- .alert.alert-success
6
- = flash[:notice].html_safe
7
-
8
1
  .kanban-container
9
2
  - column_names = @ar_object.data.keys
10
3
  = hidden_field_tag :kanban_column_names, column_names
@@ -7,16 +7,15 @@
7
7
  i.fa.fa-table
8
8
  a.btn.btn-ghost href="#{cm_admin.send("#{@model.name.underscore}_index_path")}?page=#{params[:page] || 1}&view_type=card"
9
9
  i.fa.fa-table-cells
10
-
11
10
  - if @model.sort_columns.present?
12
- = render 'cm_admin/main/sort', model: @model, ar_object: @ar_object
11
+ = render 'cm_admin/main/sort', model: @model, ar_object: @ar_object
13
12
 
14
- - if flash[:alert].present?
13
+ - if flash[:bulk_action_success].present?
14
+ .alert.alert-success.me-4 role="success"
15
+ = flash[:bulk_action_success].html_safe
16
+ - if flash[:bulk_action_error].present?
15
17
  .alert.alert-danger.me-4 role="alert"
16
- = flash[:alert].html_safe
17
- - elsif flash[:notice].present?
18
- .alert.alert-success.me-4
19
- = flash[:notice].html_safe
18
+ = flash[:bulk_action_error].html_safe
20
19
 
21
20
  - bulk_actions = actions_filter(@model, @ar_object, :bulk_action)
22
21
  - if bulk_actions.present?
@@ -1,3 +1,4 @@
1
+ .drawer-container data-behaviour="cm-drawer-container"
1
2
  .form-page
2
3
  .form-page__header
3
4
  == render 'cm_admin/main/top_navbar'
@@ -1,3 +1,4 @@
1
+ .drawer-container data-behaviour="cm-drawer-container"
1
2
  .form-page
2
3
  .form-page__header
3
4
  == render 'cm_admin/main/top_navbar'
@@ -5,9 +6,9 @@
5
6
  - if @ar_object.errors.present?
6
7
  .flag-alert.mb-4
7
8
  p.alert-header
8
- span
9
- i.fa.fa-exclamation-triangle
10
- | Attention
9
+ span
10
+ i.fa.fa-exclamation-triangle
11
+ | Attention
11
12
  .alert-body
12
13
  p.body-title
13
14
  | Error: unable to save #{@ar_object.class.name.downcase}
@@ -1,9 +1,6 @@
1
1
  - if flash[:notice].present?
2
- javascript:
3
- $.jGrowl("#{flash[:notice]}", { theme: 'notice' })
2
+ = toast_message(flash[:notice], 'notice')
4
3
  - elsif flash[:success].present?
5
- javascript:
6
- $.jGrowl("#{flash[:success]}", { theme: 'success' })
4
+ = toast_message(flash[:success], 'success')
7
5
  - elsif flash[:alert].present?
8
- javascript:
9
- $.jGrowl("#{flash[:alert].html_safe}", { theme: 'error' })
6
+ = toast_message(flash[:alert], 'alert')
@@ -0,0 +1,23 @@
1
+ .offcanvas.cm-drawer tabindex="-1" id="cm-drawer" aria-labelledby="offcanvasExampleLabel" data-behavior="cm-drawer" data-from-field-id="#{params[:from_field_id]}"
2
+ .drawer-btn-group
3
+ button.drawer-nav-btn type="button" data-bs-dismiss="offcanvas" aria-label="Close"
4
+ i.fa-solid.fa-x
5
+ .offcanvas-header
6
+ h4 = drawer_title
7
+ .offcanvas-body
8
+ .form-page
9
+ .form-page__body
10
+ .flag-alert.mb-4.hidden data-behavior="flag-alert"
11
+ p.alert-header
12
+ span
13
+ i.fa.fa-exclamation-triangle
14
+ | Attention
15
+ .alert-body
16
+ p.body-title
17
+ | Error: unable to save #{@ar_object.class.name.downcase}
18
+ p.body-info
19
+ ul data-behavior="error-list"
20
+ - @ar_object.errors.full_messages.each do |error_message|
21
+ li = error_message
22
+ - if @model.present?
23
+ = generate_form(@ar_object, @model, is_drawer_form: true)
@@ -66,7 +66,7 @@ html
66
66
  = yield
67
67
  - else
68
68
  = yield
69
+ div data-behaviour="flash-container"
69
70
  = render 'layouts/cm_flash_message'
70
71
  - unless (@current_action&.view_type == :kanban || params[:view_type] == 'kanban')
71
72
  = render 'layouts/custom_action_modals'
72
-
data/config/importmap.rb CHANGED
@@ -3,7 +3,6 @@ pin 'jquery', to: 'https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js', preload:
3
3
  pin 'bootstrap', to: 'https://ga.jspm.io/npm:bootstrap@5.1.3/dist/js/bootstrap.esm.js', preload: true
4
4
  pin '@popperjs/core', to: 'https://ga.jspm.io/npm:@popperjs/core@2.11.5/lib/index.js', preload: true
5
5
  pin 'flatpickr', to: 'https://ga.jspm.io/npm:flatpickr@4.6.13/dist/esm/index.js'
6
- pin 'jgrowl', to: 'https://ga.jspm.io/npm:jgrowl@1.4.8/jquery.jgrowl.js'
7
6
  pin 'moment', to: 'https://ga.jspm.io/npm:moment@2.29.4/moment.js'
8
7
  pin 'trix', to: 'https://ga.jspm.io/npm:trix@2.1.3/dist/trix.esm.min.js'
9
8
  pin '@rails/actiontext', to: 'https://ga.jspm.io/npm:@rails/actiontext@7.1.3-4/app/assets/javascripts/actiontext.esm.js'
data/config/routes.rb CHANGED
@@ -23,6 +23,10 @@ CmAdmin::Engine.routes.draw do
23
23
  end
24
24
  end
25
25
 
26
+ scope model.name.tableize do
27
+ send(:get, 'fetch_drawer', to: "#{model.name.underscore}#fetch_drawer", as: "cm_fetch_drawer_#{model.name.underscore}")
28
+ end
29
+
26
30
  model.available_actions.sort_by { |act| act.name }.each do |act|
27
31
  scope model.name.tableize do
28
32
  # Define route only when action trail related field is present
@@ -122,7 +122,7 @@ module CmAdmin
122
122
  end
123
123
 
124
124
  def formatted_name
125
- @display_name != @name ? @display_name : @ar_model.table_name
125
+ @display_name != @name ? @display_name : @name.titleize
126
126
  end
127
127
 
128
128
  def alert_box(options = {})
@@ -5,9 +5,9 @@ module CmAdmin
5
5
  class Action
6
6
  include Actions::Blocks
7
7
  attr_accessor :name, :display_name, :verb, :layout_type, :layout, :partial, :path, :page_title, :page_description,
8
- :child_records, :is_nested_field, :nested_table_name, :parent, :display_if, :route_type, :code_block,
9
- :display_type, :action_type, :redirection_url, :sort_direction, :sort_column, :icon_name, :scopes, :view_type,
10
- :kanban_attr, :model_name, :redirect_to
8
+ :child_records, :is_nested_field, :nested_table_name, :parent, :display_if, :route_type, :code_block,
9
+ :display_type, :action_type, :redirection_url, :sort_direction, :sort_column, :icon_name, :scopes, :view_type,
10
+ :kanban_attr, :model_name, :redirect_to
11
11
 
12
12
  VALID_SORT_DIRECTION = Set[:asc, :desc].freeze
13
13
 
@@ -23,14 +23,14 @@ module CmAdmin
23
23
  end
24
24
  set_default_values
25
25
  attributes.each do |key, value|
26
- self.send("#{key.to_s}=", value)
26
+ send("#{key}=", value)
27
27
  end
28
- self.send("code_block=", block) if block_given?
28
+ send('code_block=', block) if block_given?
29
29
  end
30
30
 
31
31
  def set_default_values
32
32
  self.is_nested_field = false
33
- self.display_if = lambda { |arg| return true }
33
+ self.display_if = ->(_arg) { true }
34
34
  self.display_type = :button
35
35
  self.action_type = :default
36
36
  self.sort_column = :created_at
@@ -44,7 +44,7 @@ module CmAdmin
44
44
  self.kanban_attr = {}
45
45
  end
46
46
 
47
- def set_values(page_title, page_description, partial, redirect_to = nil, view_type=:table)
47
+ def set_values(page_title, page_description, partial, redirect_to = nil, view_type = :table)
48
48
  self.page_title = page_title
49
49
  self.page_description = page_description
50
50
  self.partial = partial
@@ -64,6 +64,10 @@ module CmAdmin
64
64
  end
65
65
  end
66
66
 
67
+ def formatted_name
68
+ display_name || name.titleize
69
+ end
70
+
67
71
  class << self
68
72
  def find_by(model, search_hash)
69
73
  model.available_actions.find { |i| i.name == search_hash[:name] }
@@ -7,7 +7,7 @@ module CmAdmin
7
7
 
8
8
  attr_accessor :field_name, :label, :header, :input_type, :collection, :disabled, :helper_method,
9
9
  :placeholder, :display_if, :html_attrs, :target, :col_size, :ajax_url, :helper_text,
10
- :image_preview
10
+ :image_preview, :can_create_new_entity
11
11
 
12
12
  VALID_INPUT_TYPES = %i[
13
13
  integer decimal string single_select multi_select date date_time text switch custom_single_select checkbox_group
@@ -34,6 +34,7 @@ module CmAdmin
34
34
  self.target = {}
35
35
  self.col_size = nil
36
36
  self.image_preview = false
37
+ self.can_create_new_entity = false
37
38
  end
38
39
 
39
40
  def set_default_placeholder
@@ -1,3 +1,3 @@
1
1
  module CmAdmin
2
- VERSION = '2.3.3'
2
+ VERSION = '2.4.0'
3
3
  end
@@ -20,7 +20,7 @@ module CmAdmin
20
20
  def add_filters_dropdown(filters)
21
21
  concat(content_tag(:button, class: 'dropdown btn-ghost', data: { bs_toggle: 'dropdown' }) do
22
22
  concat tag.i(class: 'fas fa-filter')
23
- concat tag.span 'Filter'
23
+ concat tag.span 'Filters'
24
24
  end)
25
25
 
26
26
  concat(content_tag(:div, class: 'dropdown-menu dropdown-popup') do
@@ -32,7 +32,7 @@ module CmAdmin
32
32
  concat(content_tag(:div, class: 'list-area') do
33
33
  filters.each do |filter|
34
34
  concat(content_tag(:div, class: 'pointer list-item', data: { behaviour: 'filter-option', filter_type: "#{filter.filter_type}", db_column: "#{filter.db_column_name}" }) do
35
- tag.span filter.placeholder.to_s.titleize
35
+ tag.span filter.display_name.to_s
36
36
  end)
37
37
  end
38
38
  end)
@@ -7,8 +7,9 @@ module CmAdmin
7
7
  module FormHelper
8
8
  include FormFieldHelper
9
9
  REJECTABLE = %w[id created_at updated_at]
10
+ attr_accessor :is_drawer_form
10
11
 
11
- def generate_form(resource, cm_model)
12
+ def generate_form(resource, cm_model, is_drawer_form: false)
12
13
  if resource.new_record?
13
14
  action = :new
14
15
  method = :post
@@ -16,6 +17,7 @@ module CmAdmin
16
17
  action = :edit
17
18
  method = :patch
18
19
  end
20
+ @is_drawer_form = is_drawer_form
19
21
  return form_with_all_fields(resource, method) if cm_model.available_fields[action].empty?
20
22
 
21
23
  form_with_mentioned_fields(resource, cm_model.available_fields[action], method)
@@ -66,6 +68,7 @@ module CmAdmin
66
68
  def create_sections(resource, form_obj, section)
67
69
  content_tag :div, class: 'col form-container' do
68
70
  return render partial: section.partial, locals: { form_obj: } if section.partial
71
+
69
72
  concat content_tag(:p, section.section_name, class: 'form-title') unless section.parent_section.present?
70
73
  concat set_form_for_fields(resource, form_obj, section)
71
74
  end
@@ -116,10 +119,7 @@ module CmAdmin
116
119
  concat input_field_for_column(form_obj, field, is_required:)
117
120
  else
118
121
  concat(content_tag(:div, class: "form-field #{field.disabled ? 'disabled' : ''}") do
119
- if field.label && %i[check_box switch].exclude?(field.input_type)
120
- concat form_obj.label field.label, field.label, class: "field-label #{is_required ? 'required-label' : ''}"
121
- concat tag.br
122
- end
122
+ concat set_form_label(field, form_obj, is_required)
123
123
  concat input_field_for_column(form_obj, field, is_required:)
124
124
  concat tag.small field.helper_text, class: 'form-text text-muted' if field.helper_text.present?
125
125
  concat tag.p resource.errors[field.field_name].first if resource.errors[field.field_name].present?
@@ -128,6 +128,25 @@ module CmAdmin
128
128
  end
129
129
  end
130
130
 
131
+ def set_form_label(field, form_obj, is_required)
132
+ return unless field.label && %i[check_box switch].exclude?(field.input_type)
133
+
134
+ content_tag(:div, class: 'd-flex') do
135
+ concat form_obj.label field.label, field.label, class: "field-label #{is_required ? 'required-label' : ''}"
136
+ concat set_drawer_form_button(field, form_obj)
137
+ end
138
+ end
139
+
140
+ def set_drawer_form_button(field, form_obj)
141
+ model_name = find_model_name(form_obj, field)
142
+ ar_object = model_name.constantize.new if model_name.present?
143
+ return unless field.input_type.to_s == 'single_select' && model_name.present? && has_valid_policy(ar_object, 'new')
144
+
145
+ field_id = form_obj.field_id(field.field_name.to_sym)
146
+ drawer_fetch_url = "#{CmAdmin::Engine.mount_path}/#{model_name.tableize}/fetch_drawer?from_field_id=#{field_id}"
147
+ tag.div 'New Entity', class: 'drawer-btn', data: { behaviour: 'offcanvas', drawer_fetch_url: }
148
+ end
149
+
131
150
  def set_nested_section_form_fields(resource, form_obj, nested_sections)
132
151
  return if nested_sections.blank?
133
152
 
@@ -142,7 +161,7 @@ module CmAdmin
142
161
  def set_nested_form_fields(form_obj, section)
143
162
  content_tag(:div) do
144
163
  section.nested_table_fields.each do |nested_table_field|
145
- concat(render(partial: '/cm_admin/main/nested_table_form', locals: { f: form_obj, nested_table_field: nested_table_field }))
164
+ concat(render(partial: '/cm_admin/main/nested_table_form', locals: { f: form_obj, nested_table_field: }))
146
165
  end
147
166
  end
148
167
  end
@@ -150,7 +169,7 @@ module CmAdmin
150
169
  def set_form_with_sections(resource, entities, url, method)
151
170
  url_with_query_params = extract_query_params(url)
152
171
 
153
- form_for(resource, url: url_with_query_params || url, method: method, html: { class: "cm_#{resource.class.name.downcase}_form" }) do |form_obj|
172
+ form_for(resource, url: url_with_query_params || url, method:, html: { class: "cm_#{resource.class.name.downcase}_form" }, data: { is_drawer_form: @is_drawer_form }) do |form_obj|
154
173
  concat form_obj.text_field 'referrer', class: 'normal-input', hidden: true, value: params[:referrer], name: 'referrer' if params[:referrer]
155
174
  if params[:polymorphic_name].present?
156
175
  concat form_obj.text_field params[:polymorphic_name] + '_type', class: 'normal-input', hidden: true, value: params[:associated_class].classify
@@ -162,7 +181,7 @@ module CmAdmin
162
181
  concat split_form_into_section(resource, form_obj, entities)
163
182
  concat tag.br
164
183
  concat form_obj.submit 'Save', class: 'btn-cta', data: { behaviour: 'form_submit', form_class: "cm_#{form_obj.object.class.name.downcase}_form" }
165
- concat button_tag 'Discard', class: 'btn-secondary discard-form', data: { behaviour: 'discard_form' }
184
+ concat button_tag 'Discard', class: 'btn-secondary discard-form', data: { behaviour: 'discard_form' } unless @is_drawer_form
166
185
  end
167
186
  end
168
187
 
@@ -184,6 +203,16 @@ module CmAdmin
184
203
  end
185
204
  form_obj.object._validators[field.field_name].map(&:kind).include?(:presence) || associated_field.present?
186
205
  end
206
+
207
+ def find_model_name(form_obj, field)
208
+ return unless field.can_create_new_entity.presence
209
+ return field.can_create_new_entity[:model] if field.can_create_new_entity.is_a?(Hash) && field.can_create_new_entity[:model].present?
210
+
211
+ associated_field = form_obj.object.class.reflect_on_all_associations(:belongs_to).select do |association|
212
+ "#{association.name}_id" == field.field_name.to_s
213
+ end
214
+ associated_field&.first&.klass&.name
215
+ end
187
216
  end
188
217
  end
189
218
  end
@@ -38,21 +38,26 @@ module CmAdmin
38
38
  end
39
39
 
40
40
  def custom_action_items(custom_action, current_action_name)
41
- if custom_action.name.present? && policy([:cm_admin, @model.name.classify.constantize]).send(:"#{custom_action.name}?")
42
- scoped_model = "CmAdmin::#{@model.name}Policy::#{custom_action.name.classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve
43
- if custom_action.display_if.call(@ar_object) && scoped_model.find_by(id: params[:id])
44
- case custom_action.display_type
45
- when :icon_only
46
- custom_action_icon(custom_action, current_action_name)
47
- when :button
48
- custom_action_button(custom_action, current_action_name)
49
- when :modal
50
- custom_modal_button(custom_action)
51
- when :page
52
- path = cm_admin.send("#{@model.name.underscore}_#{custom_action.name}_path", @ar_object.id, custom_action.url_params)
53
- link_to custom_action_title(custom_action), path, class: 'btn-secondary ms-2', method: custom_action.verb
54
- end
55
- end
41
+ return unless custom_action.name.present? && policy([:cm_admin, @model.name.classify.constantize]).send(:"#{custom_action.name}?")
42
+
43
+ scoped_model = "CmAdmin::#{@model.name}Policy::#{custom_action.name.classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve
44
+ has_scoped_record = if current_action_name == 'index'
45
+ scoped_model.present?
46
+ else
47
+ scoped_model.find_by(id: params[:id]).present?
48
+ end
49
+ return unless custom_action.display_if.call(@ar_object) && has_scoped_record
50
+
51
+ case custom_action.display_type
52
+ when :icon_only
53
+ custom_action_icon(custom_action, current_action_name)
54
+ when :button
55
+ custom_action_button(custom_action, current_action_name)
56
+ when :modal
57
+ custom_modal_button(custom_action)
58
+ when :page
59
+ path = cm_admin.send("#{@model.name.underscore}_#{custom_action.name}_path", @ar_object.id, custom_action.url_params)
60
+ link_to custom_action_title(custom_action), path, class: 'btn-secondary ms-2', method: custom_action.verb
56
61
  end
57
62
  end
58
63
 
@@ -66,7 +71,7 @@ module CmAdmin
66
71
 
67
72
  def custom_action_button(custom_action, current_action_name)
68
73
  if current_action_name == "index"
69
- button_to custom_action_title(custom_action), @model.ar_model.table_name + '/' + custom_action.path, class: 'btn-secondary ms-2', method: custom_action.verb
74
+ button_to custom_action_title(custom_action), @model.ar_model.table_name + '/' + (custom_action.path || custom_action.name), class: 'btn-secondary ms-2', method: custom_action.verb
70
75
  elsif current_action_name == "show"
71
76
  button_to custom_action_title(custom_action), custom_action.path.gsub(':id', params[:id]), class: 'btn-secondary ms-2', method: custom_action.verb
72
77
  end
@@ -83,6 +83,17 @@ module CmAdmin
83
83
  "#{count} #{table_name.humanize.downcase} found"
84
84
  end
85
85
 
86
+ def toast_message(message, toast_type)
87
+ tag.div class: 'position-fixed bottom-0 end-0 p-3', style: 'z-index: 11' do
88
+ tag.div class: "toast #{toast_type == 'alert' ? 'text-white bg-danger' : ''}", role: 'alert', 'aria-live': 'assertive', 'aria-atomic': 'true', data: { behaviour: 'toast' } do
89
+ tag.div class: 'd-flex' do
90
+ concat tag.div message.html_safe, class: 'toast-body'
91
+ concat tag.button '', type: 'button', class: "btn-close me-2 m-auto #{toast_type == 'alert' ? 'btn-close-white' : ''}", data: { 'bs-dismiss': 'toast' }, 'aria-label': 'Close'
92
+ end
93
+ end
94
+ end
95
+ end
96
+
86
97
  def project_name_with_env
87
98
  return CmAdmin.config.project_name if Rails.env.production?
88
99
 
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: 2.3.3
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: exe
16
16
  cert_chain: []
17
- date: 2024-10-08 00:00:00.000000000 Z
17
+ date: 2024-10-14 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: caxlsx_rails
@@ -344,7 +344,6 @@ files:
344
344
  - app/assets/stylesheets/cm_admin/dependency/bootstrap/scss/utilities/_api.scss
345
345
  - app/assets/stylesheets/cm_admin/dependency/bootstrap/scss/vendor/_rfs.scss
346
346
  - app/assets/stylesheets/cm_admin/dependency/flatpickr.min.css
347
- - app/assets/stylesheets/cm_admin/dependency/jquery-jgrowl.min.css
348
347
  - app/assets/stylesheets/cm_admin/helpers/_mixins.scss
349
348
  - app/assets/stylesheets/cm_admin/helpers/_variable.scss
350
349
  - app/assets/stylesheets/cm_admin/helpers/index.scss
@@ -375,7 +374,6 @@ files:
375
374
  - app/views/cm_admin/main/_card.html.slim
376
375
  - app/views/cm_admin/main/_cm_pagy_nav.html.slim
377
376
  - app/views/cm_admin/main/_custom_action_modal_form.html.slim
378
- - app/views/cm_admin/main/_drawer.html.slim
379
377
  - app/views/cm_admin/main/_filters.html.slim
380
378
  - app/views/cm_admin/main/_kanban.html.slim
381
379
  - app/views/cm_admin/main/_member_custom_action_modal.html.slim
@@ -406,6 +404,7 @@ files:
406
404
  - app/views/layouts/_custom_action_modal.html.slim
407
405
  - app/views/layouts/_custom_action_modals.html.slim
408
406
  - app/views/layouts/_destroy_action_modal.html.slim
407
+ - app/views/layouts/_drawer.html.slim
409
408
  - app/views/layouts/_left_sidebar_nav.html.slim
410
409
  - app/views/layouts/_quick_links.html.slim
411
410
  - app/views/layouts/cm_admin.html.slim
@@ -1 +0,0 @@
1
- .jGrowl{z-index:9999;color:#fff;font-size:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;position:fixed}.jGrowl.top-left{left:0;top:0}.jGrowl.top-right{right:0;top:0}.jGrowl.bottom-left{left:0;bottom:0}.jGrowl.bottom-right{right:0;bottom:0}.jGrowl.center{top:0;width:50%;left:25%}.jGrowl.center .jGrowl-closer,.jGrowl.center .jGrowl-notification{margin-left:auto;margin-right:auto}.jGrowl-notification{background-color:#000;opacity:.9;zoom:1;width:250px;padding:10px;margin:10px;text-align:left;display:none;border-radius:5px;min-height:40px}.jGrowl-notification .ui-state-highlight,.jGrowl-notification .ui-widget-content .ui-state-highlight,.jGrowl-notification .ui-widget-header .ui-state-highlight{border:1px solid #000;background:#000;color:#fff}.jGrowl-notification .jGrowl-header{font-weight:700;font-size:.85em}.jGrowl-notification .jGrowl-close{background-color:transparent;color:inherit;border:none;z-index:99;float:right;font-weight:700;font-size:1em;cursor:pointer}.jGrowl-closer{background-color:#000;opacity:.9;zoom:1;width:250px;padding:10px;margin:10px;text-align:left;display:none;border-radius:5px;padding-top:4px;padding-bottom:4px;cursor:pointer;font-size:.9em;font-weight:700;text-align:center}.jGrowl-closer .ui-state-highlight,.jGrowl-closer .ui-widget-content .ui-state-highlight,.jGrowl-closer .ui-widget-header .ui-state-highlight{border:1px solid #000;background:#000;color:#fff}@media print{.jGrowl{display:none}}
@@ -1,13 +0,0 @@
1
- .cm-drawer.hidden
2
- .cm-drawer__container#drawer-container
3
- .header
4
- .header__lhs
5
- h2.title Review
6
- p.description Theresa Webb
7
- .header__rhs
8
- i.fa.fa-times.drawer-close.pointer
9
- .body
10
- p.info-text
11
- | It was really an amazing experience being an Unschooler. Its really different from other e-learning websites in every aspects like certification, providing internship opportunities and many more. I am going to recommend Unschool for sure!
12
-
13
-