maquina 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 (134) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/images/maquina/maquina.svg +18 -0
  6. data/app/assets/javascripts/maquina/application.js +4 -0
  7. data/app/assets/javascripts/maquina/controllers/alert_controller.js +29 -0
  8. data/app/assets/javascripts/maquina/controllers/application.js +9 -0
  9. data/app/assets/javascripts/maquina/controllers/file_controller.js +60 -0
  10. data/app/assets/javascripts/maquina/controllers/index.js +11 -0
  11. data/app/assets/javascripts/maquina/controllers/mobile_menu_controller.js +31 -0
  12. data/app/assets/javascripts/maquina/controllers/modal_controller.js +39 -0
  13. data/app/assets/javascripts/maquina/controllers/modal_open_controller.js +15 -0
  14. data/app/assets/javascripts/maquina/controllers/popup_menu_controller.js +17 -0
  15. data/app/assets/javascripts/maquina/controllers/submit_form_controller.js +11 -0
  16. data/app/assets/stylesheets/maquina/application.css +15 -0
  17. data/app/assets/stylesheets/maquina/application.tailwind.css +102 -0
  18. data/app/controllers/concerns/maquina/authenticate.rb +41 -0
  19. data/app/controllers/concerns/maquina/create.rb +27 -0
  20. data/app/controllers/concerns/maquina/destroy.rb +28 -0
  21. data/app/controllers/concerns/maquina/edit.rb +29 -0
  22. data/app/controllers/concerns/maquina/index.rb +33 -0
  23. data/app/controllers/concerns/maquina/new.rb +22 -0
  24. data/app/controllers/concerns/maquina/resourceful.rb +180 -0
  25. data/app/controllers/concerns/maquina/show.rb +27 -0
  26. data/app/controllers/concerns/maquina/update.rb +31 -0
  27. data/app/controllers/maquina/accept_invitations_controller.rb +28 -0
  28. data/app/controllers/maquina/application_controller.rb +19 -0
  29. data/app/controllers/maquina/dashboard_controller.rb +16 -0
  30. data/app/controllers/maquina/invitations_controller.rb +51 -0
  31. data/app/controllers/maquina/plans_controller.rb +56 -0
  32. data/app/controllers/maquina/sessions_controller.rb +56 -0
  33. data/app/controllers/maquina/unauthorized_controller.rb +9 -0
  34. data/app/controllers/maquina/users_controller.rb +24 -0
  35. data/app/helpers/maquina/application_helper.rb +19 -0
  36. data/app/helpers/maquina/navbar_menu_helper.rb +29 -0
  37. data/app/helpers/maquina/views_helper.rb +9 -0
  38. data/app/jobs/maquina/application_job.rb +4 -0
  39. data/app/mailers/maquina/application_mailer.rb +8 -0
  40. data/app/mailers/maquina/user_notifications_mailer.rb +16 -0
  41. data/app/models/concerns/maquina/authenticate_by.rb +33 -0
  42. data/app/models/concerns/maquina/blockeable.rb +28 -0
  43. data/app/models/concerns/maquina/multifactor.rb +80 -0
  44. data/app/models/concerns/maquina/retain_passwords.rb +32 -0
  45. data/app/models/concerns/maquina/searchable.rb +24 -0
  46. data/app/models/maquina/active_session.rb +33 -0
  47. data/app/models/maquina/application_record.rb +11 -0
  48. data/app/models/maquina/current.rb +21 -0
  49. data/app/models/maquina/invitation.rb +15 -0
  50. data/app/models/maquina/plan.rb +38 -0
  51. data/app/models/maquina/used_password.rb +17 -0
  52. data/app/models/maquina/user.rb +33 -0
  53. data/app/policies/maquina/application_policy.rb +50 -0
  54. data/app/policies/maquina/invitation_policy.rb +13 -0
  55. data/app/policies/maquina/navigation_policy.rb +13 -0
  56. data/app/policies/maquina/plan_policy.rb +49 -0
  57. data/app/policies/maquina/user_policy.rb +27 -0
  58. data/app/views/layouts/maquina/application.html.erb +26 -0
  59. data/app/views/layouts/maquina/mailer.html.erb +377 -0
  60. data/app/views/layouts/maquina/mailer.text.erb +12 -0
  61. data/app/views/layouts/maquina/sessions.html.erb +24 -0
  62. data/app/views/maquina/accept_invitations/new.html.erb +9 -0
  63. data/app/views/maquina/accept_invitations/new_view.rb +41 -0
  64. data/app/views/maquina/application/_navbar.html.erb +21 -0
  65. data/app/views/maquina/application/alert.rb +104 -0
  66. data/app/views/maquina/application/components/action_text_component.rb +20 -0
  67. data/app/views/maquina/application/components/checkbox_component.rb +21 -0
  68. data/app/views/maquina/application/components/component_base.rb +60 -0
  69. data/app/views/maquina/application/components/file_component.rb +59 -0
  70. data/app/views/maquina/application/components/input_component.rb +20 -0
  71. data/app/views/maquina/application/components/select_component.rb +44 -0
  72. data/app/views/maquina/application/create.turbo_stream.erb +11 -0
  73. data/app/views/maquina/application/edit.html.erb +9 -0
  74. data/app/views/maquina/application/edit.rb +17 -0
  75. data/app/views/maquina/application/form.rb +77 -0
  76. data/app/views/maquina/application/index.html.erb +10 -0
  77. data/app/views/maquina/application/index_header.rb +46 -0
  78. data/app/views/maquina/application/index_modal.rb +43 -0
  79. data/app/views/maquina/application/index_table.rb +121 -0
  80. data/app/views/maquina/application/new.html.erb +9 -0
  81. data/app/views/maquina/application/new.rb +18 -0
  82. data/app/views/maquina/application/sessions_header.rb +31 -0
  83. data/app/views/maquina/application/show.html.erb +1 -0
  84. data/app/views/maquina/application/update.turbo_stream.erb +11 -0
  85. data/app/views/maquina/application_view.rb +46 -0
  86. data/app/views/maquina/dashboard/index.html.erb +0 -0
  87. data/app/views/maquina/invitations/create.turbo_stream.erb +13 -0
  88. data/app/views/maquina/invitations/new.html.erb +3 -0
  89. data/app/views/maquina/navbar/menu.rb +62 -0
  90. data/app/views/maquina/navbar/menu_item_link.rb +34 -0
  91. data/app/views/maquina/navbar/mobile_button.rb +29 -0
  92. data/app/views/maquina/navbar/mobile_menu.rb +47 -0
  93. data/app/views/maquina/navbar/notification.rb +37 -0
  94. data/app/views/maquina/navbar/profile.rb +16 -0
  95. data/app/views/maquina/navbar/profile_button.rb +24 -0
  96. data/app/views/maquina/navbar/profile_menu.rb +108 -0
  97. data/app/views/maquina/navbar/profile_menu_item_link.rb +41 -0
  98. data/app/views/maquina/navbar/search.rb +40 -0
  99. data/app/views/maquina/navbar/title.rb +22 -0
  100. data/app/views/maquina/sessions/create.turbo_stream.erb +11 -0
  101. data/app/views/maquina/sessions/form.rb +56 -0
  102. data/app/views/maquina/sessions/new.html.erb +9 -0
  103. data/app/views/maquina/unauthorized/401.html.erb +1 -0
  104. data/app/views/maquina/user_notifications_mailer/invitation_email.html.erb +40 -0
  105. data/app/views/maquina/user_notifications_mailer/invitation_email.text.erb +12 -0
  106. data/config/definitions.rb +1 -0
  107. data/config/initializers/importmap.rb +17 -0
  108. data/config/initializers/money.rb +116 -0
  109. data/config/initializers/pagy.rb +235 -0
  110. data/config/locales/flash.en.yml +44 -0
  111. data/config/locales/forms.en.yml +58 -0
  112. data/config/locales/mailers.en.yml +35 -0
  113. data/config/locales/models.en.yml +34 -0
  114. data/config/locales/routes.en.yml +7 -0
  115. data/config/locales/views.en.yml +45 -0
  116. data/config/routes.rb +17 -0
  117. data/db/migrate/20221109010726_create_maquina_plans.rb +13 -0
  118. data/db/migrate/20221113000409_create_maquina_users.rb +19 -0
  119. data/db/migrate/20221113020108_create_maquina_used_passwords.rb +10 -0
  120. data/db/migrate/20221115223414_create_maquina_active_sessions.rb +15 -0
  121. data/db/migrate/20230201203922_create_maquina_invitations.rb +12 -0
  122. data/db/schema.rb +1 -0
  123. data/lib/generators/maquina/install_generator.rb +32 -0
  124. data/lib/generators/maquina/install_templates/install_templates_generator.rb +31 -0
  125. data/lib/generators/maquina/tailwind_config/tailwind_config_generator.rb +11 -0
  126. data/lib/generators/maquina/tailwind_config/templates/app/assets/config/maquina/tailwind.config.js.tt +68 -0
  127. data/lib/generators/maquina/templates/config/initializers/maquina.rb +3 -0
  128. data/lib/maquina/engine.rb +17 -0
  129. data/lib/maquina/version.rb +3 -0
  130. data/lib/maquina.rb +48 -0
  131. data/lib/tasks/install.rake +19 -0
  132. data/lib/tasks/maquina_tasks.rake +4 -0
  133. data/lib/tasks/tailwind.rake +25 -0
  134. metadata +456 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ module Components
6
+ class FileComponent < ComponentBase
7
+ include Phlex::Rails::Helpers::NumberToHumanSize
8
+
9
+ def template
10
+ div(**extend_control_options(attribute_name, control_html)) do
11
+ @form.label attribute_name, class: "label #{label_css_class}"
12
+ div(class: "mt-1") do
13
+ div(data_file_target: "container") do
14
+ @form.file_field attribute_name, **extend_input_options(input_html(no_helpers: true))
15
+ img(class: "hidden", data_file_target: "preview")
16
+ svg(viewbox: "0 0 24 24", fill: "currentColor", aria_hidden: "true", data_action: "click->file#select", data_file_target: "placeholder") do |s|
17
+ s.path(fill_rule: "evenodd", d: "M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z", clip_rule: "evenodd")
18
+ end
19
+ help_template
20
+ error_template
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def help_template
29
+ help = t("helpers.help.#{@resource.model_name.i18n_key}.#{attribute_name}", default: nil)
30
+ return if help.blank?
31
+
32
+ p(class: "help", data_action: "click->file#select") { unsafe_raw help }
33
+ end
34
+
35
+ def extend_input_options(input_html)
36
+ {
37
+ multiple: false,
38
+ data: {
39
+ "file-target": "fileInput"
40
+ }
41
+ }.deep_merge(input_html)
42
+ end
43
+
44
+ def extend_control_options(attribute_name, control_html)
45
+ max_size = control_html.dig(:data, :file_max_size_value) || 4_194_304
46
+ human_max_size = number_to_human_size(max_size)
47
+
48
+ {
49
+ data: {
50
+ controller: "file",
51
+ file_max_size_value: max_size,
52
+ file_validation_message_value: t("activerecord.errors.attributes.#{@form.object.model_name.i18n_key}.#{attribute_name}.oversize", size: human_max_size, default: t("activerecord.errors.attributes.file.oversize", size: human_max_size))
53
+ }
54
+ }.deep_merge(control_html)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ module Components
6
+ class InputComponent < ComponentBase
7
+ def template
8
+ div(**control_html) do
9
+ @form.label attribute_name, class: "label #{label_css_class}"
10
+ div(class: "mt-1") do
11
+ @form.text_field attribute_name, **input_html
12
+ help_template
13
+ error_template
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ module Components
6
+ class SelectComponent < ComponentBase
7
+ delegate :controller, :options_for_select, to: :helpers
8
+
9
+ def template
10
+ options = input_html(no_helpers: true).deep_dup
11
+
12
+ div(**control_html) do
13
+ @form.label attribute_name, class: "label #{label_css_class}"
14
+ div(class: "mt-1") do
15
+ @form.select attribute_name, to_values!(options), extract_options!(options), options
16
+ help_template
17
+ error_template
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def extract_options!(options)
25
+ options.extract!(:include_blank)
26
+ end
27
+
28
+ def to_values!(options)
29
+ values = options.delete(:values)
30
+ selected = options.delete(:selected)
31
+
32
+ if values.is_a?(Symbol)
33
+ values = controller.send(values) if controller.respond_to?(values, true)
34
+ else
35
+ # TODO: Add support for other value options
36
+ []
37
+ end
38
+
39
+ options_for_select(values, selected.present? ? selected : nil)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ <%= turbo_stream.replace class_to_form_frame(resource_class) do %>
2
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
3
+ <%= render Maquina::Application::New.new(resource: resource) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <%= turbo_stream.replace :alert do %>
8
+ <%= turbo_frame_tag :alert do %>
9
+ <%= render Maquina::Application::Alert.new(flash) %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <div class="bg-white">
2
+ <div class="mx-auto max-w-7xl py-12 px-4 sm:px-6 lg:px-8">
3
+ <div class="mx-auto max-w-3xl">
4
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
5
+ <%= render Maquina::Application::Edit.new(resource: resource) %>
6
+ <% end %>
7
+ </div>
8
+ </div>
9
+ </div>
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class Edit < Phlex::HTML
6
+ include ApplicationView
7
+
8
+ def initialize(resource:)
9
+ @resource = resource
10
+ end
11
+
12
+ def template
13
+ render Maquina::Application::Form.new(resource: @resource)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class Form < Phlex::HTML
6
+ include ApplicationView
7
+ include Phlex::Rails::Helpers::FormWith
8
+
9
+ delegate :resource_path, :collection_path, :submit_path, :form_attributes, to: :helpers
10
+
11
+ def initialize(resource:, modal: false)
12
+ @resource = resource
13
+ @modal = modal
14
+ end
15
+
16
+ def template
17
+ form_with(model: @resource, url: if @resource.persisted?
18
+ resource_path(@resource)
19
+ else
20
+ (submit_path.present? ? submit_path : collection_path)
21
+ end, data: {turbo_frame: :_top}) do |form|
22
+ div(class: "space-y-8 divide-y divide-gray-200") do
23
+ div do
24
+ form_header_template
25
+
26
+ div(class: "mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6") do
27
+ attributes_template(form)
28
+ end
29
+ end
30
+
31
+ div(class: "pt-5") do
32
+ div(class: "flex justify-end") do
33
+ link_to t("helpers.cancel"), collection_path, class: "button", data: @modal ? {action: "modal#toggleModal"} : {turbo_frame: :_top}
34
+ form.submit class: "ml-3 button button-accented"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def attributes_template(form)
44
+ attributes = form_attributes.deep_dup
45
+ attributes.each do |attribute|
46
+ component_resolver(form, attribute)
47
+ end
48
+ end
49
+
50
+ def form_header_template
51
+ div do
52
+ h3(class: "text-lg font-medium leading-6 text-skin-base") { t("new.#{resource_class.model_name.i18n_key}.title", model: model_human_name.downcase, default: t("new.title", default: model_human_name.downcase)) }
53
+ p(class: "mt-1 text-sm text-skin-dimmed") { t("new.#{resource_class.model_name.i18n_key}.description", model: model_human_name.downcase, default: t("new.description", model: model_human_name.downcase)) }
54
+ end
55
+ end
56
+
57
+ def component_resolver(form, attribute)
58
+ attribute_values = attribute.values.first
59
+ input_type = attribute_values.fetch(:type)
60
+ attribute_values[:name] = attribute.keys.first
61
+
62
+ case input_type
63
+ when :checkbox
64
+ render Maquina::Application::Components::CheckboxComponent.new(resource: @resource, form: form, options: attribute_values)
65
+ when :file
66
+ render Maquina::Application::Components::FileComponent.new(resource: @resource, form: form, options: attribute_values)
67
+ when :action_text
68
+ render Maquina::Application::Components::ActionTextComponent.new(resource: @resource, form: form, options: attribute_values)
69
+ when :select
70
+ render Maquina::Application::Components::SelectComponent.new(resource: @resource, form: form, options: attribute_values)
71
+ else
72
+ render Maquina::Application::Components::InputComponent.new(resource: @resource, form: form, options: attribute_values)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,10 @@
1
+ <div class="px-4 sm:px-6 lg:px-8">
2
+ <%= render Maquina::Application::IndexHeader.new(params[:q]) %>
3
+
4
+ <%= render Maquina::Application::IndexTable.new(
5
+ collection: collection,
6
+ pagination: @pagination,
7
+ list_attributes: list_attributes) %>
8
+ </div>
9
+
10
+ <%= render Maquina::Application::IndexModal.new %>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class IndexHeader < Phlex::HTML
6
+ include ApplicationView
7
+ delegate :new_resource_path, to: :helpers
8
+
9
+ def initialize(filter = nil)
10
+ @filter = filter
11
+ end
12
+
13
+ def template
14
+ div(class: "sm:flex sm:items-center") do
15
+ div(class: "sm:flex-auto") do
16
+ h1(class: "text-xl font-semibold text-skin-base") { model_human_name(plural: true) }
17
+ p(class: "mt-2 text-sm text-skin-muted") { description }
18
+ p(class: "mt-4 text-skin-base") { unsafe_raw t("index.search", search: @filter) } if @filter.present?
19
+ end
20
+ add_new_template
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def add_new_template
27
+ return if policy_class.blank? || !allowed_to?(:new?, with: policy_class)
28
+
29
+ options = {}
30
+ options[:data] = {controller: "modal-open", action: "modal-open#open"} if policy_class.present? && allowed_to?(:new_modal?, with: policy_class)
31
+
32
+ div(class: "mt-4 sm:mt-0 sm:ml-16 sm:flex-none") do
33
+ a(href: new_resource_path, class: "inline-flex items-center justify-center button button-accented", **options) { add_new }
34
+ end
35
+ end
36
+
37
+ def description
38
+ t("index.#{resource_class.model_name.i18n_key}.description", default: "")
39
+ end
40
+
41
+ def add_new
42
+ t("index.#{resource_class.model_name.i18n_key}.add_new", model: model_human_name.downcase, default: t("index.add_new", model: model_human_name.downcase))
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class IndexModal < Phlex::HTML
6
+ include ApplicationView
7
+ register_element :turbo_frame
8
+
9
+ def template
10
+ div(data_controller: "modal",
11
+ data_action: " modal-open:toggle@window->modal#toggleModal close-frame-modal:toggle@window->modal#toggleModal",
12
+ data_frame_src: "") do
13
+ div(class: "fixed inset-0 z-30 hidden overflow-y-auto", aria_labelledby: "modal-title", role: "dialog",
14
+ aria_modal: "true", data_modal_target: "container") do
15
+ div(class: "flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0") do
16
+ div(class: "fixed inset-0 transition-opacity",
17
+ data_transition_enter: "ease-out duration-300",
18
+ data_transition_enter_active: "opacity-0",
19
+ data_transition_enter_to: "opacity-100",
20
+ data_transition_leave: "ease-in duration-200",
21
+ data_transition_leave_active: "opacity-100",
22
+ data_transition_leave_to: "opacity-0",
23
+ aria_hidden: "true") do
24
+ div(class: "fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity", aria_hidden: "true")
25
+ end
26
+ span(class: "hidden sm:inline-block sm:align-middle sm:h-screen", aria_hidden: "true") { "​" }
27
+ div(class: "inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6",
28
+ data_modal_target: "content",
29
+ data_transition_enter: "ease-out duration-300",
30
+ data_transition_enter_active: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
31
+ data_transition_enter_to: "opacity-100 translate-y-0 sm:scale-100",
32
+ data_transition_leave: "ease-in duration-200",
33
+ data_transition_leave_active: "opacity-100 translate-y-0 sm:scale-100",
34
+ data_transition_leave_to: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95") do
35
+ turbo_frame(id: :modal_content, src: "", data: {"modal-target": "frame"}) {}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class IndexTable < Phlex::HTML
6
+ include ApplicationView
7
+
8
+ delegate :humanized_money_with_symbol, :edit_resource_path, to: :helpers
9
+
10
+ def initialize(collection: nil, pagination: nil, list_attributes: nil)
11
+ @collection = collection
12
+ @list_attributes = list_attributes
13
+ @pagination = pagination
14
+ end
15
+
16
+ def template
17
+ div(class: "mt-8 flex flex-col") do
18
+ div(class: "-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8") do
19
+ div(class: "inline-block min-w-full py-2 align-middle md:px-6 lg:px-8") do
20
+ div(class: "overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") do
21
+ table(class: "min-w-full divide-y divide-gray-300") do
22
+ thead(class: "bg-gray-50") do
23
+ tr do
24
+ table_headers_template
25
+ th(scope: "col", class: "relative py-3.5 pl-3 pr-4 sm:pr-6") do
26
+ span(class: "sr-only") { "Edit" }
27
+ end
28
+ end
29
+ end
30
+ tbody class: "divide-y divide-gray-200 bg-white" do
31
+ @collection.empty? ? no_items_template : @collection.each { |item| item_template(item) }
32
+ end
33
+ end
34
+ pagination_template
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def table_headers_template
42
+ base_css = "px-3 py-3.5 text-left text-sm font-semibold text-skin-base"
43
+ first_css = "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-skin-base sm:pl-6"
44
+
45
+ @list_attributes.each_with_index do |attribute, index|
46
+ current_css = if index == 0
47
+ first_css
48
+ else
49
+ base_css
50
+ end
51
+
52
+ th(scope: :col, class: current_css) { attribute_human_name(attribute) }
53
+ end
54
+ end
55
+
56
+ def no_items_template
57
+ col_span = @list_attributes.size + 1
58
+
59
+ tr do
60
+ th(scope: "col", colspan: col_span, class: "whitespace-nowrap px-3 py-4 text-sm text-skin-dimmed") do
61
+ t("index.no_items", model: model_human_name(plural: true))
62
+ end
63
+ end
64
+ end
65
+
66
+ def item_template(item)
67
+ base_css = "whitespace-nowrap px-3 py-4 text-sm text-skin-muted"
68
+ first_css = "whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-skin-base sm:pl-6"
69
+
70
+ tr do
71
+ @list_attributes.each_with_index do |attribute, index|
72
+ td(class: (index == 0) ? first_css : base_css, scope: "col") { attribute_value(item, attribute) }
73
+ end
74
+ td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6", scope: "col") do
75
+ a(href: edit_resource_path(item), class: "text-skin-accented hover:text-skin-accented-hover") { t("index.edit") } if policy_class.blank? || allowed_to?(:edit?, item, with: policy_class)
76
+ end
77
+ end
78
+ end
79
+
80
+ def pagination_template
81
+ nav(class: "flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6", aria_label: "Pagination") do
82
+ div(class: "hidden sm:block") do
83
+ p(class: "text-sm text-skin-muted") do
84
+ t("index.pagination.information", from: @pagination.from, to: @pagination.to, count: @pagination.count) if @collection.any? && @pagination.present?
85
+ end
86
+ end
87
+ div(class: "flex flex-1 justify-between sm:justify-end") do
88
+ if @pagination.present? && @pagination.prev.present?
89
+ a(href: "#", class: "relative button") { t("index.pagination.previous") }
90
+ else
91
+ span(class: "relative button", disabled: true) { t("index.pagination.previous") }
92
+ end
93
+
94
+ if @pagination.present? && @pagination.next.present?
95
+ a(href: "#", class: "relative ml-3 button") { t("index.pagination.next") }
96
+ else
97
+ span(class: "relative ml-3 button", disabled: true) { t("index.pagination.next") }
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def attribute_value(item, attribute)
104
+ value = item[attribute] || item.send(attribute)
105
+
106
+ case value
107
+ when TrueClass
108
+ t("yes_value")
109
+ when FalseClass
110
+ t("no_value")
111
+ when ActiveSupport::TimeWithZone, Date
112
+ I18n.l value, format: :short
113
+ when Money
114
+ humanized_money_with_symbol(value)
115
+ else
116
+ value
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,9 @@
1
+ <div class="bg-white">
2
+ <div class="mx-auto max-w-7xl py-12 px-4 sm:px-6 lg:px-8">
3
+ <div class="mx-auto max-w-3xl">
4
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
5
+ <%= render Maquina::Application::New.new(resource: resource) %>
6
+ <% end %>
7
+ </div>
8
+ </div>
9
+ </div>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class New < Phlex::HTML
6
+ include ApplicationView
7
+
8
+ def initialize(resource:, modal: false)
9
+ @resource = resource
10
+ @modal = modal
11
+ end
12
+
13
+ def template
14
+ render Maquina::Application::Form.new(resource: @resource, modal: @modal)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class SessionsHeader < Phlex::HTML
6
+ include Maquina::ApplicationView
7
+ include Phlex::DeferredRender
8
+
9
+ def initialize(brand_icon:)
10
+ @brand_icon = brand_icon
11
+ end
12
+
13
+ def template
14
+ div(class: "sm:mx-auto sm:w-full sm:max-w-md") do
15
+ image_tag(@brand_icon, class: "mx-auto h-12 w-auto", alt: t("application_name"))
16
+ h2(class: "mt-6 text-center text-3xl font-bold tracking-tight text-skin-base") { t(".title") }
17
+
18
+ if @description.present?
19
+ p(class: "mt-2 text-center text-sm text-skin-dimmed") do
20
+ yield_content(&@description)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def description(&block)
27
+ @description = block
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1 @@
1
+ <%= resource %>
@@ -0,0 +1,11 @@
1
+ <%= turbo_stream.replace class_to_form_frame(resource_class) do %>
2
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
3
+ <%= render Maquina::Application::Edit.new(resource: resource) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <%= turbo_stream.replace :alert do %>
8
+ <%= turbo_frame_tag :alert do %>
9
+ <%= render Maquina::Application::Alert.new(flash) %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module ApplicationView
5
+ include Maquina::Engine.routes.url_helpers
6
+ include Phlex::Rails::Helpers::T
7
+
8
+ delegate :resource_class, :l, :default_url_options, :policy_class, :allowed_to?, to: :helpers
9
+
10
+ # TODO: Revisit this later
11
+ def image_tag(source, options = {})
12
+ unsafe_raw helpers.image_tag(source, **options)
13
+ end
14
+
15
+ def link_to(body, url, html_options = {})
16
+ unsafe_raw helpers.link_to(body, url, html_options)
17
+ end
18
+
19
+ def button_to(name, options = nil, html_options = nil)
20
+ unsafe_raw helpers.button_to(name, options, html_options)
21
+ end
22
+
23
+ # type: :fill or :outline
24
+ def svg_icon(type, icon:, view_box: "0 0 24 24", stroke_width: 2, css_class: "block h-6 w-6", data: {})
25
+ svg_attributes = if type == :fill
26
+ {xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", viewBox: view_box, "aria-hidden": "true"}
27
+ else
28
+ {xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: view_box, "stroke-width": stroke_width, stroke: "currentColor", "aria-hidden": "true"}
29
+ end
30
+
31
+ path_attributes = (type == :fill) ? {} : {"stroke-linecap": "round", "stroke-linejoin": "round"}
32
+
33
+ svg(class: css_class, data: data, **svg_attributes) do |markup|
34
+ markup.path(d: icon, **path_attributes)
35
+ end
36
+ end
37
+
38
+ def model_human_name(plural: false)
39
+ resource_class.model_name.human(count: plural ? 2 : 1)
40
+ end
41
+
42
+ def attribute_human_name(attribute)
43
+ resource_class.human_attribute_name(attribute)
44
+ end
45
+ end
46
+ end
File without changes
@@ -0,0 +1,13 @@
1
+ <%= turbo_stream.replace :modal_content do %>
2
+ <%= turbo_frame_tag :modal_content, data: {modal_target: :frame} do %>
3
+ <%= render Maquina::Application::New.new(resource: resource, modal: true) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <% if resource.errors.empty? %>
8
+ <%= turbo_stream.replace :alert do %>
9
+ <%= turbo_frame_tag :alert do %>
10
+ <%= render Maquina::Application::Alert.new(flash) %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :modal_content, data: {modal_target: :frame} do %>
2
+ <%= render Maquina::Application::New.new(resource: resource, modal: true) %>
3
+ <% end %>
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Navbar
5
+ # == Navbar main Menu
6
+ #
7
+ # Renders main navbar Menu. It can render menu for desktop o mobile versions.
8
+ #
9
+ # Requires a Rails Helper method to get access to menu options. `menu_options` example:
10
+ #
11
+ # def menu_options
12
+ # {
13
+ # home: root_path,
14
+ # users: users_path
15
+ # accounts: accounts_path
16
+ # }
17
+ # end
18
+ #
19
+ # `Menu` uses I18n to translate menu options. Translation example:
20
+ #
21
+ # menu:
22
+ # main:
23
+ # home: Inicio
24
+ # users: Usuarios
25
+ # accounts: Cuentas
26
+ #
27
+ # `Menu` is initialized by default to render links for desktop. To render mobile links
28
+ # initialize `Menu` as follow:
29
+ #
30
+ # Views::Navbar::Menu.new(desktop: false)
31
+ #
32
+ class Menu < Phlex::HTML
33
+ include Maquina::ApplicationView
34
+ delegate :menu_options, to: :helpers
35
+
36
+ def initialize(desktop: true)
37
+ @desktop = desktop
38
+ end
39
+
40
+ def template
41
+ div class: "#{link_type}-menu" do
42
+ secure_menu_options.each_pair do |option, path|
43
+ render(Maquina::Navbar::MenuItemLink.new(option, path, desktop: @desktop))
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def secure_menu_options
51
+ menu_options
52
+ rescue NoMethodError => ex
53
+ Rails.logger.error "[#{self.class}] Please implement helper method :menu_options :: #{ex.message}"
54
+ {}
55
+ end
56
+
57
+ def link_type
58
+ @desktop ? "desktop" : "mobile"
59
+ end
60
+ end
61
+ end
62
+ end