custom_table 0.1.0

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +210 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/custom_table_manifest.js +1 -0
  5. data/app/assets/stylesheets/custom_table/application.scss +1 -0
  6. data/app/assets/stylesheets/custom_table/table.scss +89 -0
  7. data/app/controllers/concerns/custom_table_concern.rb +118 -0
  8. data/app/controllers/custom_table/application_controller.rb +4 -0
  9. data/app/controllers/custom_table/settings_controller.rb +86 -0
  10. data/app/helpers/custom_table/application_helper.rb +483 -0
  11. data/app/helpers/custom_table/fieldset_helper.rb +90 -0
  12. data/app/helpers/custom_table/icons_helper.rb +42 -0
  13. data/app/inputs/date_picker_input.rb +52 -0
  14. data/app/javascript/controllers/batch_actions_controller.js +53 -0
  15. data/app/javascript/controllers/table_controller.js +109 -0
  16. data/app/jobs/custom_table/application_job.rb +4 -0
  17. data/app/mailers/custom_table/application_mailer.rb +6 -0
  18. data/app/models/concerns/custom_table_settings.rb +42 -0
  19. data/app/models/custom_table/application_record.rb +5 -0
  20. data/app/views/custom_table/_download.haml +19 -0
  21. data/app/views/custom_table/_field.haml +8 -0
  22. data/app/views/custom_table/_field_plain.haml +1 -0
  23. data/app/views/custom_table/_fieldset.haml +2 -0
  24. data/app/views/custom_table/_filter.html.haml +104 -0
  25. data/app/views/custom_table/_settings.html.haml +57 -0
  26. data/app/views/custom_table/_table.html.haml +261 -0
  27. data/app/views/custom_table/_table.xlsx.axlsx +76 -0
  28. data/app/views/custom_table/_table_fe.xlsx.fast_excel +141 -0
  29. data/app/views/custom_table/_table_row.html.haml +72 -0
  30. data/app/views/custom_table/_table_row_data.html.haml +26 -0
  31. data/app/views/custom_table/settings/destroy.html.haml +4 -0
  32. data/app/views/custom_table/settings/edit.html.haml +2 -0
  33. data/app/views/custom_table/settings/update.html.haml +4 -0
  34. data/app/views/layouts/custom_table/application.html.erb +15 -0
  35. data/config/initializers/simple_form_bootstrap.rb +468 -0
  36. data/config/locales/en.yml +18 -0
  37. data/config/locales/ru.yml +18 -0
  38. data/config/routes.rb +5 -0
  39. data/lib/custom_table/configuration.rb +10 -0
  40. data/lib/custom_table/engine.rb +12 -0
  41. data/lib/custom_table/version.rb +3 -0
  42. data/lib/custom_table.rb +14 -0
  43. data/lib/generators/custom_table/USAGE +8 -0
  44. data/lib/generators/custom_table/custom_table_generator.rb +16 -0
  45. data/lib/generators/custom_table/templates/initializer.rb +3 -0
  46. data/lib/generators/custom_table/templates/migration.rb +5 -0
  47. data/lib/tasks/custom_table_tasks.rake +4 -0
  48. metadata +300 -0
@@ -0,0 +1,53 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ static targets = [ "checkbox", "form" ]
6
+
7
+ connect() {
8
+ if (this.hasFormTarget && this.hasCheckboxTarget) {
9
+ console.log("Batch actions form", this.formTarget)
10
+ this.refresh();
11
+ }
12
+ }
13
+
14
+ submit(event) {
15
+
16
+ if (!this.hasCheckboxTarget) {
17
+ console.log("No checkbox targets at all")
18
+ return
19
+ }
20
+
21
+ // Clearing any matching hidden fields from form
22
+ this.formTarget.querySelectorAll('input[name="'+this.checkboxTargets[0].getAttribute("name")+'"]').forEach((cb) => {
23
+ cb.parentNode.removeChild(cb)
24
+ });
25
+
26
+ // Adding selected fields to form as hiddens
27
+ this.checkboxTargets.forEach((cb) => {
28
+
29
+ if (!cb.checked) return;
30
+
31
+ let input = document.createElement('input');
32
+ input.setAttribute('name', cb.getAttribute("name"));
33
+ input.setAttribute('value', cb.getAttribute("value"));
34
+ input.setAttribute('type', "hidden")
35
+
36
+ this.formTarget.appendChild(input);//append the input to the form
37
+
38
+ })
39
+
40
+ // event.preventDefault()
41
+ console.log("Batch actions submit form")
42
+
43
+ }
44
+
45
+ refresh(){
46
+ let v = false
47
+ this.checkboxTargets.forEach((cb) => {
48
+ if (cb.checked) v = true;
49
+ });
50
+ this.formTarget.querySelector('input[type="submit"]').disabled = !v
51
+ }
52
+
53
+ }
@@ -0,0 +1,109 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ static targets = [ "move", "hide", "sum", "sumval" ]
6
+
7
+ connect() {
8
+ if (this.hasMoveTarget) this.moveup();
9
+ if (this.hasHideTarget) this.hideEmpty();
10
+
11
+ if (!this.hasSumTarget) {
12
+ let d = document.createElement("caption")
13
+ d.classList.add("d-none", "table-sum")
14
+ d.dataset.tableTarget = "sum"
15
+ d.innerHTML = '<i class="fa fa-times-circle mt-1 float-start" data-action="click->table#clearSum"></i><span data-table-target="sumval"></span>'
16
+ this.element.appendChild(d)
17
+ }
18
+
19
+ this.element.querySelector("tbody").addEventListener('click', (e) => {
20
+ const cell = e.target.closest('td.amount');
21
+ if (!cell) {return;} // Quit, not clicked on a cell
22
+ if (!isNaN(this.cellValue(cell))) {
23
+ const row = cell.parentElement;
24
+ cell.classList.toggle("selected")
25
+ this.showSum();
26
+ }
27
+ });
28
+
29
+ }
30
+
31
+ moveup() {
32
+ console.log("Moving row")
33
+ let elm = this.moveTarget
34
+ let tbl = elm.parentElement
35
+ this.moveTarget.remove()
36
+ tbl.prepend(elm)
37
+ }
38
+
39
+ showSum(){
40
+ let elements = this.element.querySelectorAll("td.selected");
41
+ if (elements.length == 0) {
42
+ this.sumTarget.classList.add("d-none")
43
+ return
44
+ }
45
+ this.sumTarget.classList.remove("d-none")
46
+ let sum = 0
47
+ elements.forEach((e) => {
48
+ sum += this.cellValue(e)
49
+ })
50
+ if (this.hasSumTarget) {
51
+ this.sumvalTarget.innerHTML = "SUM="+Math.round(sum * 100) / 100
52
+ }
53
+ }
54
+
55
+ cellValue(e){
56
+ if (e.querySelector("span[data-raw]")) {
57
+ return Number(e.querySelector("span[data-raw]").dataset.raw);
58
+ }
59
+ else {
60
+ return Number(e.innerText.replace(/\s,/g, ''));
61
+ }
62
+ }
63
+
64
+ clearSum(){
65
+ this.element.querySelectorAll("td.selected").forEach((e) => {
66
+ e.classList.remove("selected")
67
+ })
68
+ this.showSum();
69
+ }
70
+
71
+ hideEmpty(){
72
+
73
+ console.log("Hiding empty!!!")
74
+
75
+ this.element.querySelectorAll("tr th").forEach((e, i) => {
76
+
77
+ if (!e.classList.contains("hide-empty")) return;
78
+
79
+ let tds = this.element.querySelectorAll("tr td:nth-child(".concat(i+1, ")"));
80
+ if (Array.prototype.slice.call(tds).every(td => { return (td.parentElement.classList.contains("totals") || td.textContent == "0" || !td.textContent || Number(td.textContent) == 0); })) {
81
+ e.hidden = true;
82
+ tds.forEach(e => {e.hidden = true})
83
+ }
84
+
85
+ });
86
+
87
+ }
88
+
89
+ toggle(event) {
90
+ let params = event.params
91
+ console.log("Table Toggle", event.currentTarget.innerHTML)
92
+ this.element.querySelectorAll(params.css).forEach((e, i) => {
93
+ e.classList.toggle("d-none")
94
+ });
95
+ event.currentTarget.classList.toggle("opened")
96
+ }
97
+
98
+ search(event) {
99
+ console.log("Table Search", event.currentTarget.value)
100
+ this.element.querySelectorAll("tbody tr").forEach((tr) => {
101
+ if (tr.innerText.toLowerCase().includes(event.currentTarget.value.toLowerCase())) {
102
+ tr.classList.remove("d-none")
103
+ } else {
104
+ tr.classList.add("d-none")
105
+ }
106
+ })
107
+ }
108
+
109
+ }
@@ -0,0 +1,4 @@
1
+ module CustomTable
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module CustomTable
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,42 @@
1
+ module CustomTableSettings
2
+
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ serialize :custom_table
7
+ end
8
+
9
+ def save_custom_table_settings model_class, variant = nil, fields: nil, sorts: nil, per_page: nil
10
+
11
+ model = model_class.model_name.to_s
12
+ key = model
13
+ key = "#{model}-#{variant}" if !variant.nil?
14
+ self.custom_table = {} if self.custom_table.nil?
15
+ self.custom_table[key] = {} if self.custom_table[key].nil?
16
+ self.custom_table[key][:model] = model
17
+ self.custom_table[key][:fields] = fields.symbolize_keys if !fields.nil?
18
+ self.custom_table[key][:sorts] = sorts if !sorts.nil?
19
+ self.custom_table[key][:per_page] = per_page.to_i if !per_page.nil? && [25, 50, 100].include?(per_page.to_i)
20
+
21
+ return save!
22
+ # write_attribute :custom_table, (custom_table||{}).merge(ss)
23
+ end
24
+
25
+
26
+ def destroy_custom_table_settings model_class, variant = nil
27
+
28
+ return true if self.custom_table.nil?
29
+
30
+ model = model_class.model_name.to_s
31
+ key = model
32
+ key = "#{model}-#{variant}" if !variant.nil?
33
+
34
+ return true if self.custom_table[key].nil?
35
+
36
+ self.custom_table.delete(key)
37
+
38
+ return save
39
+ # write_attribute :custom_table, (custom_table||{}).merge(ss)
40
+ end
41
+
42
+ end
@@ -0,0 +1,5 @@
1
+ module CustomTable
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ - model_class = local_assigns[:collection].nil? ? local_assigns[:class] : local_assigns[:collection].model
2
+ - local_assigns[:with_selected_fields] = true if local_assigns[:with_selected_fields].nil?
3
+ - if (model_class.present? && can?(:download, model_class))
4
+ .btn-group
5
+ = link_to params.permit!.merge({:format => :xlsx, all_fields: true}), :class => "btn btn-outline-primary" do
6
+ = t("download")
7
+ = custom_table_download_icon
8
+
9
+ - if (local_assigns[:downloads] && downloads.length > 0) || (local_assigns[:with_selected_fields] == true)
10
+
11
+ %button.btn.btn-outline-primary.dropdown-toggle{type: "button", data: {"bs-toggle": "dropdown"}}
12
+ %ul.dropdown-menu
13
+ - if local_assigns[:with_selected_fields] == true
14
+ %li
15
+ = link_to params.permit!.merge({:format => :xlsx}), :class => "dropdown-item" do
16
+ = t("custom_table.download_only_selected_fields")
17
+ - downloads.each do |d|
18
+ %li
19
+ = link_to d[:title], d[:href], class: "dropdown-item"
@@ -0,0 +1,8 @@
1
+ %div.d-flex.justify-content-between.custom-table-field
2
+ %b
3
+ - if local_assigns[:hint]
4
+ %abbr{title: hint}= label
5
+ - else
6
+ = label
7
+ %span.text-end.d-inline{id: "#{column}_#{object.model_name.singular}_#{object.id}"}= yield
8
+ -# %dt{class: adaptive ? "col-lg-2 col-md-5" : "col-5"}= label
@@ -0,0 +1 @@
1
+ %span{id: "#{column}_#{object.model_name.singular}_#{object.id}"}= yield
@@ -0,0 +1,2 @@
1
+ %div
2
+ = yield
@@ -0,0 +1,104 @@
1
+ .card
2
+
3
+ .card-body.bg-light
4
+
5
+ - url = local_assigns[:url].presence
6
+ - variant = local_assigns[:variant].presence
7
+
8
+ - a = {}
9
+
10
+ = custom_table_form_for @q, url: url do |f|
11
+
12
+ .d-none
13
+ = f.input :s, as: :hidden, input_html: {value: params[:q].try(:[], :s)}
14
+
15
+ - fields = custom_table_fields_for(search_model, variant: variant, current_search: params[:q], predefined_fields: local_assigns[:fields]) if local_assigns[:fields].nil?
16
+
17
+ - fields.each do |key, defs|
18
+
19
+ - next if defs[:search].nil?
20
+
21
+ - a[key] = []
22
+
23
+ - defs[:search] = [defs[:search]] if !defs[:search].kind_of?(Array)
24
+ - defs[:label] = defs[:search][0][:label] if defs[:label].nil?
25
+ - defs[:label] = search_model.human_attribute_name(key) if defs[:label].nil?
26
+
27
+ - defs[:search].each do |field|
28
+
29
+ - a[key].push field[:q]
30
+ - label = field[:label] || defs[:label]
31
+
32
+ - if field[:type] == :text
33
+
34
+ - ih = {class: "form-control-sm"}
35
+ - ih = ih.merge(defs[:input_html]) if !defs[:input_html].nil?
36
+
37
+ = f.input field[:q], input_html: ih, required: false, label: false, placeholder: label
38
+
39
+ - if field[:type] == :boolean
40
+
41
+ = f.input field[:q], as: :boolean, input_html: {}, required: false, label: label, unchecked_value: ""
42
+
43
+ - if field[:type] == :switch
44
+
45
+ = f.input field[:q], as: :select, input_html: {:class => "form-select form-select-sm"}, required: false, label: false, :collection => [[t("yes")+" | #{label}", "true"], [t("no")+" | #{label}", "false"]], include_blank: label
46
+
47
+ - if field[:type] == :select
48
+
49
+ - collection = field[:collection]
50
+ - label_method = nil
51
+
52
+ - if collection.class.to_s =~ /ActiveRecord_Relation/i
53
+ - collection = collection.accessible_by(current_ability)
54
+ - label_method = :to_s
55
+
56
+ = f.input field[:q], :as => :select, :input_html => {:class => "form-select form-select-sm"}, :required => false, :label => false, :collection => collection, label_method: label_method, include_blank: label, value_method: field[:value_method]
57
+
58
+ - if field[:type] == :grouped_select
59
+
60
+ - collection = field[:collection]
61
+
62
+ - collection = collection.accessible_by(current_ability) if collection.class.to_s =~ /ActiveRecord_Relation/i
63
+
64
+ = f.input field[:q], :as => :grouped_select, :input_html => {:class => "form-select form-select-sm"}, :required => false, :label => false, group_method: field[:group_method], :collection => collection, include_blank: label
65
+
66
+ - if field[:type] == :autocomplete
67
+
68
+ - collection = field[:collection]
69
+
70
+ - collection = collection.accessible_by(current_ability) if collection.class.to_s =~ /ActiveRecord_Relation/i
71
+
72
+ = f.input field[:q], :as => :autocomplete, :input_html => {:class => "input-sm"}, :required => false, :label => false, :collection => collection, prompt: label
73
+
74
+ - if field[:type] == :enum
75
+
76
+ - collection = field[:collection]
77
+ - collection = [search_model, key.to_s.pluralize] if collection.nil?
78
+ - coll = collection[0].send(collection[1]).keys.map { |w| [(collection[0].method_defined?(:human_enum_name) ? collection[0].human_enum_name(collection[1].to_s.singularize, w) : w), w] }
79
+
80
+ = f.input field[:q], :as => :select, :input_html => {:class => "form-select form-select-sm"}, :required => false, :label => false, :collection => coll, include_blank: label
81
+
82
+ - if field[:type] == :dates
83
+ .col-12
84
+ .input-group.input-group-sm{"data-controller": "dates"}
85
+ = f.input_field field[:q][0], :as => :date_picker, :label => false, :class => "form-control", data: {"dates-target": "dateFrom"}, :placeholder => label+" ("+t("custom_table.date_from")+")"
86
+ = f.input_field field[:q][1], :as => :date_picker, :label => false, :class => "form-control", data: {"dates-target": "dateTo"}, :placeholder => label+" ("+t("custom_table.date_to")+")"
87
+ %button.btn.btn-outline-secondary.dropdown-toggle(type="button" data-bs-toggle="dropdown" aria-expanded="false")
88
+ %ul.dropdown-menu.dropdown-menu-end
89
+ %button.dropdown-item{type: "button", "data-action": "dates#prevWeek"}= t("analytics.previos_week")
90
+ %button.dropdown-item{type: "button", "data-action": "dates#prevMonth"}= t("analytics.previos_month")
91
+ %button.dropdown-item{type: "button", "data-action": "dates#prevYear"}= t("analytics.previos_year")
92
+ %hr.dropdown-divider
93
+ %button.dropdown-item{type: "button", "data-action": "dates#clear"}= t("clear")
94
+
95
+ .btn-group
96
+ = f.button :submit, t("search"), name: "", :class => "btn-sm btn-primary"
97
+ - if user_signed_in? && current_user_has_customizable_fields_for?(search_model, variant) && !local_assigns[:hide_customization] && !local_assigns[:fields]# && !(profile_path rescue nil).nil?
98
+ = custom_table_settings_button search_model, variant
99
+ - if params[:q].present? && !params[:q].except(:s).empty?
100
+ = link_to url_for(params.permit!.except(:q)), class: "btn btn-sm btn-outline-danger", title: t("custom_table.clear_search") do
101
+ = custom_table_cancel_icon
102
+
103
+ = yield f
104
+
@@ -0,0 +1,57 @@
1
+ - variant = nil if !defined?("variant")
2
+ - field_key = search_model.model_name.to_s
3
+ - field_key += "-#{variant}" if !variant.nil?
4
+ - cls = local_assigns[:large].nil? ? "btn-sm" : ""
5
+ - items = custom_table_fields_settings_for(search_model, variant: variant)
6
+
7
+ - columns = 1; size = "modal-md"
8
+ - (columns = 2; size = "modal-lg") if items.length > 10
9
+ - (columns = 3; size = "modal-xl") if items.length > 30
10
+
11
+ .modal.fade.show{id: "customize-items-#{field_key}", tabindex: "-1", role: "dialog", "aria-labelledby": "loginLabel", "aria-hidden": "true", data: {controller: "remote-modal",
12
+ action: "turbo:before-render@document->remote-modal#hideBeforeRender"}}
13
+ .modal-dialog{class: size}
14
+ .modal-content
15
+ .modal-header
16
+ %h4#loginLabel.modal-title
17
+ = t("custom_table.customize_table")+":"
18
+ = search_model.model_name.human
19
+ - if !variant.nil?
20
+ %small= "(#{variant})"
21
+
22
+ %button.btn-close(type="button" data-bs-dismiss="modal" aria-label="close")
23
+ .modal-body
24
+
25
+ = turbo_frame_tag "custom-table-settings" do
26
+
27
+ %p= t("custom_table.customize_table_description")
28
+
29
+ = simple_form_for current_user, url: custom_table.setting_path(id: search_model.model_name) do |f|
30
+
31
+ = f.hidden_field "model", value: search_model.model_name
32
+ = f.hidden_field "variant", value: variant
33
+
34
+ = f.simple_fields_for :fields do |sfi|
35
+ %div{data: {controller: "sortable", "data-sortable-handle-value": ".handle"}, style: "columns: #{columns}"}
36
+
37
+ - items.each do |field, item|
38
+ -# = abort item.inspect
39
+ %div
40
+ %i.handle{style: "cursor: move", class: custom_table_move_icon_class}
41
+ .form-check.form-switch.d-inline-block
42
+ -# - if item[:appear] == :always
43
+ = sfi.hidden_field field, value: 1 if item[:appear] == :always
44
+ = sfi.check_box field, {checked: item[:selected], class: "form-check-input", disabled: (item[:appear] == :always)}, 1, 0#, as: :boolean, wrapper: :custom_boolean_switch, label: item[0], checked: true
45
+ %label.form-check-label= item[:label]
46
+ -# - if items.length < 20
47
+ -# = sf.input "fields", :as => :check_boxes, wrapper: :custom_boolean_switch, wrapper_html: {data: "kek"}, :collection => items, :label_text => false, label: false, checked: checkeds
48
+ -# - else
49
+ -# .row
50
+ -# .col-md-6
51
+ -# = sf.input "fields", :as => :check_boxes, wrapper: :custom_boolean_switch, :collection => items[0..(items.length/2)], :label_text => false, label: false, checked: checkeds
52
+ -# .col-md-6
53
+ -# = sf.input "fields", :as => :check_boxes, wrapper: :custom_boolean_switch, :collection => items[(items.length/2+1)..], :label_text => false, label: false, checked: checkeds
54
+
55
+ .mt-2
56
+ = f.button :submit, class: "btn-success", value: t("custom_table.save_settings")
57
+ = link_to t("custom_table.reset_settings"), custom_table.setting_path(id: search_model.model_name, variant: variant), data: {"turbo-method": "delete", "turbo-confirm": t("custom_table.are_you_sure")}, class: "btn btn-outline-secondary"