maquina 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|