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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/images/maquina/maquina.svg +18 -0
- data/app/assets/javascripts/maquina/application.js +4 -0
- data/app/assets/javascripts/maquina/controllers/alert_controller.js +29 -0
- data/app/assets/javascripts/maquina/controllers/application.js +9 -0
- data/app/assets/javascripts/maquina/controllers/file_controller.js +60 -0
- data/app/assets/javascripts/maquina/controllers/index.js +11 -0
- data/app/assets/javascripts/maquina/controllers/mobile_menu_controller.js +31 -0
- data/app/assets/javascripts/maquina/controllers/modal_controller.js +39 -0
- data/app/assets/javascripts/maquina/controllers/modal_open_controller.js +15 -0
- data/app/assets/javascripts/maquina/controllers/popup_menu_controller.js +17 -0
- data/app/assets/javascripts/maquina/controllers/submit_form_controller.js +11 -0
- data/app/assets/stylesheets/maquina/application.css +15 -0
- data/app/assets/stylesheets/maquina/application.tailwind.css +102 -0
- data/app/controllers/concerns/maquina/authenticate.rb +41 -0
- data/app/controllers/concerns/maquina/create.rb +27 -0
- data/app/controllers/concerns/maquina/destroy.rb +28 -0
- data/app/controllers/concerns/maquina/edit.rb +29 -0
- data/app/controllers/concerns/maquina/index.rb +33 -0
- data/app/controllers/concerns/maquina/new.rb +22 -0
- data/app/controllers/concerns/maquina/resourceful.rb +180 -0
- data/app/controllers/concerns/maquina/show.rb +27 -0
- data/app/controllers/concerns/maquina/update.rb +31 -0
- data/app/controllers/maquina/accept_invitations_controller.rb +28 -0
- data/app/controllers/maquina/application_controller.rb +19 -0
- data/app/controllers/maquina/dashboard_controller.rb +16 -0
- data/app/controllers/maquina/invitations_controller.rb +51 -0
- data/app/controllers/maquina/plans_controller.rb +56 -0
- data/app/controllers/maquina/sessions_controller.rb +56 -0
- data/app/controllers/maquina/unauthorized_controller.rb +9 -0
- data/app/controllers/maquina/users_controller.rb +24 -0
- data/app/helpers/maquina/application_helper.rb +19 -0
- data/app/helpers/maquina/navbar_menu_helper.rb +29 -0
- data/app/helpers/maquina/views_helper.rb +9 -0
- data/app/jobs/maquina/application_job.rb +4 -0
- data/app/mailers/maquina/application_mailer.rb +8 -0
- data/app/mailers/maquina/user_notifications_mailer.rb +16 -0
- data/app/models/concerns/maquina/authenticate_by.rb +33 -0
- data/app/models/concerns/maquina/blockeable.rb +28 -0
- data/app/models/concerns/maquina/multifactor.rb +80 -0
- data/app/models/concerns/maquina/retain_passwords.rb +32 -0
- data/app/models/concerns/maquina/searchable.rb +24 -0
- data/app/models/maquina/active_session.rb +33 -0
- data/app/models/maquina/application_record.rb +11 -0
- data/app/models/maquina/current.rb +21 -0
- data/app/models/maquina/invitation.rb +15 -0
- data/app/models/maquina/plan.rb +38 -0
- data/app/models/maquina/used_password.rb +17 -0
- data/app/models/maquina/user.rb +33 -0
- data/app/policies/maquina/application_policy.rb +50 -0
- data/app/policies/maquina/invitation_policy.rb +13 -0
- data/app/policies/maquina/navigation_policy.rb +13 -0
- data/app/policies/maquina/plan_policy.rb +49 -0
- data/app/policies/maquina/user_policy.rb +27 -0
- data/app/views/layouts/maquina/application.html.erb +26 -0
- data/app/views/layouts/maquina/mailer.html.erb +377 -0
- data/app/views/layouts/maquina/mailer.text.erb +12 -0
- data/app/views/layouts/maquina/sessions.html.erb +24 -0
- data/app/views/maquina/accept_invitations/new.html.erb +9 -0
- data/app/views/maquina/accept_invitations/new_view.rb +41 -0
- data/app/views/maquina/application/_navbar.html.erb +21 -0
- data/app/views/maquina/application/alert.rb +104 -0
- data/app/views/maquina/application/components/action_text_component.rb +20 -0
- data/app/views/maquina/application/components/checkbox_component.rb +21 -0
- data/app/views/maquina/application/components/component_base.rb +60 -0
- data/app/views/maquina/application/components/file_component.rb +59 -0
- data/app/views/maquina/application/components/input_component.rb +20 -0
- data/app/views/maquina/application/components/select_component.rb +44 -0
- data/app/views/maquina/application/create.turbo_stream.erb +11 -0
- data/app/views/maquina/application/edit.html.erb +9 -0
- data/app/views/maquina/application/edit.rb +17 -0
- data/app/views/maquina/application/form.rb +77 -0
- data/app/views/maquina/application/index.html.erb +10 -0
- data/app/views/maquina/application/index_header.rb +46 -0
- data/app/views/maquina/application/index_modal.rb +43 -0
- data/app/views/maquina/application/index_table.rb +121 -0
- data/app/views/maquina/application/new.html.erb +9 -0
- data/app/views/maquina/application/new.rb +18 -0
- data/app/views/maquina/application/sessions_header.rb +31 -0
- data/app/views/maquina/application/show.html.erb +1 -0
- data/app/views/maquina/application/update.turbo_stream.erb +11 -0
- data/app/views/maquina/application_view.rb +46 -0
- data/app/views/maquina/dashboard/index.html.erb +0 -0
- data/app/views/maquina/invitations/create.turbo_stream.erb +13 -0
- data/app/views/maquina/invitations/new.html.erb +3 -0
- data/app/views/maquina/navbar/menu.rb +62 -0
- data/app/views/maquina/navbar/menu_item_link.rb +34 -0
- data/app/views/maquina/navbar/mobile_button.rb +29 -0
- data/app/views/maquina/navbar/mobile_menu.rb +47 -0
- data/app/views/maquina/navbar/notification.rb +37 -0
- data/app/views/maquina/navbar/profile.rb +16 -0
- data/app/views/maquina/navbar/profile_button.rb +24 -0
- data/app/views/maquina/navbar/profile_menu.rb +108 -0
- data/app/views/maquina/navbar/profile_menu_item_link.rb +41 -0
- data/app/views/maquina/navbar/search.rb +40 -0
- data/app/views/maquina/navbar/title.rb +22 -0
- data/app/views/maquina/sessions/create.turbo_stream.erb +11 -0
- data/app/views/maquina/sessions/form.rb +56 -0
- data/app/views/maquina/sessions/new.html.erb +9 -0
- data/app/views/maquina/unauthorized/401.html.erb +1 -0
- data/app/views/maquina/user_notifications_mailer/invitation_email.html.erb +40 -0
- data/app/views/maquina/user_notifications_mailer/invitation_email.text.erb +12 -0
- data/config/definitions.rb +1 -0
- data/config/initializers/importmap.rb +17 -0
- data/config/initializers/money.rb +116 -0
- data/config/initializers/pagy.rb +235 -0
- data/config/locales/flash.en.yml +44 -0
- data/config/locales/forms.en.yml +58 -0
- data/config/locales/mailers.en.yml +35 -0
- data/config/locales/models.en.yml +34 -0
- data/config/locales/routes.en.yml +7 -0
- data/config/locales/views.en.yml +45 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20221109010726_create_maquina_plans.rb +13 -0
- data/db/migrate/20221113000409_create_maquina_users.rb +19 -0
- data/db/migrate/20221113020108_create_maquina_used_passwords.rb +10 -0
- data/db/migrate/20221115223414_create_maquina_active_sessions.rb +15 -0
- data/db/migrate/20230201203922_create_maquina_invitations.rb +12 -0
- data/db/schema.rb +1 -0
- data/lib/generators/maquina/install_generator.rb +32 -0
- data/lib/generators/maquina/install_templates/install_templates_generator.rb +31 -0
- data/lib/generators/maquina/tailwind_config/tailwind_config_generator.rb +11 -0
- data/lib/generators/maquina/tailwind_config/templates/app/assets/config/maquina/tailwind.config.js.tt +68 -0
- data/lib/generators/maquina/templates/config/initializers/maquina.rb +3 -0
- data/lib/maquina/engine.rb +17 -0
- data/lib/maquina/version.rb +3 -0
- data/lib/maquina.rb +48 -0
- data/lib/tasks/install.rake +19 -0
- data/lib/tasks/maquina_tasks.rake +4 -0
- data/lib/tasks/tailwind.rake +25 -0
- 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,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
|