easy-admin-rails 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/builds/easy_admin.base.js +43505 -0
- data/app/assets/builds/easy_admin.base.js.map +7 -0
- data/app/assets/builds/easy_admin.css +6141 -0
- data/app/assets/config/easy_admin_manifest.js +1 -0
- data/app/assets/images/jsoneditor-icons.svg +749 -0
- data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
- data/app/components/easy_admin/base_component.rb +35 -0
- data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
- data/app/components/easy_admin/batch_action_form_component.rb +124 -0
- data/app/components/easy_admin/combined_filters_component.rb +232 -0
- data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
- data/app/components/easy_admin/context_menu_component.rb +161 -0
- data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
- data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
- data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
- data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
- data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
- data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
- data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
- data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
- data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
- data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
- data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
- data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
- data/app/components/easy_admin/dashboards/show_component.rb +163 -0
- data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
- data/app/components/easy_admin/date_picker_component.rb +188 -0
- data/app/components/easy_admin/fields/base_component.rb +101 -0
- data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
- data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
- data/app/components/easy_admin/fields/form/date_component.rb +55 -0
- data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
- data/app/components/easy_admin/fields/form/email_component.rb +55 -0
- data/app/components/easy_admin/fields/form/file_component.rb +190 -0
- data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
- data/app/components/easy_admin/fields/form/json_component.rb +81 -0
- data/app/components/easy_admin/fields/form/number_component.rb +55 -0
- data/app/components/easy_admin/fields/form/select_component.rb +326 -0
- data/app/components/easy_admin/fields/form/text_component.rb +55 -0
- data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
- data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
- data/app/components/easy_admin/fields/index/date_component.rb +13 -0
- data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/index/email_component.rb +24 -0
- data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
- data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
- data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
- data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
- data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
- data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
- data/app/components/easy_admin/fields/index/json_component.rb +23 -0
- data/app/components/easy_admin/fields/index/number_component.rb +20 -0
- data/app/components/easy_admin/fields/index/select_component.rb +25 -0
- data/app/components/easy_admin/fields/index/text_component.rb +20 -0
- data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
- data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
- data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
- data/app/components/easy_admin/fields/show/date_component.rb +13 -0
- data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/show/email_component.rb +19 -0
- data/app/components/easy_admin/fields/show/file_component.rb +304 -0
- data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
- data/app/components/easy_admin/fields/show/json_component.rb +45 -0
- data/app/components/easy_admin/fields/show/number_component.rb +20 -0
- data/app/components/easy_admin/fields/show/select_component.rb +25 -0
- data/app/components/easy_admin/fields/show/text_component.rb +17 -0
- data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
- data/app/components/easy_admin/filters_component.rb +120 -0
- data/app/components/easy_admin/form_tabs_component.rb +166 -0
- data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
- data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
- data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
- data/app/components/easy_admin/modal_frame_component.rb +26 -0
- data/app/components/easy_admin/navbar_component.rb +226 -0
- data/app/components/easy_admin/notification_component.rb +83 -0
- data/app/components/easy_admin/pagination_component.rb +188 -0
- data/app/components/easy_admin/quick_filters_component.rb +65 -0
- data/app/components/easy_admin/resource_pagination_component.rb +14 -0
- data/app/components/easy_admin/resources/index_component.rb +211 -0
- data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
- data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
- data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
- data/app/components/easy_admin/resources/table_component.rb +206 -0
- data/app/components/easy_admin/resources/table_row_component.rb +160 -0
- data/app/components/easy_admin/row_action_form_component.rb +127 -0
- data/app/components/easy_admin/scopes_component.rb +224 -0
- data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
- data/app/components/easy_admin/show_layout_component.rb +600 -0
- data/app/components/easy_admin/sidebar_component.rb +174 -0
- data/app/components/easy_admin/turbo/response_component.rb +40 -0
- data/app/components/easy_admin/turbo/stream_component.rb +28 -0
- data/app/controllers/easy_admin/application_controller.rb +66 -0
- data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
- data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
- data/app/controllers/easy_admin/passwords_controller.rb +15 -0
- data/app/controllers/easy_admin/registrations_controller.rb +52 -0
- data/app/controllers/easy_admin/resources_controller.rb +907 -0
- data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
- data/app/controllers/easy_admin/sessions_controller.rb +32 -0
- data/app/controllers/easy_admin/settings_controller.rb +94 -0
- data/app/helpers/easy_admin/application_helper.rb +4 -0
- data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
- data/app/helpers/easy_admin/fields_helper.rb +27 -0
- data/app/helpers/easy_admin/pagy_helper.rb +30 -0
- data/app/helpers/easy_admin/resources_helper.rb +39 -0
- data/app/javascript/easy_admin/application.js +12 -0
- data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
- data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
- data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
- data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
- data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
- data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
- data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
- data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
- data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
- data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
- data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
- data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
- data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
- data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
- data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
- data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
- data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
- data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
- data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
- data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
- data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
- data/app/javascript/easy_admin/controllers.js +54 -0
- data/app/javascript/easy_admin.base.js +4 -0
- data/app/models/easy_admin/admin_user.rb +53 -0
- data/app/models/easy_admin/application_record.rb +5 -0
- data/app/views/easy_admin/dashboard/index.html.erb +3 -0
- data/app/views/easy_admin/dashboards/show.html.erb +7 -0
- data/app/views/easy_admin/passwords/edit.html.erb +42 -0
- data/app/views/easy_admin/passwords/new.html.erb +41 -0
- data/app/views/easy_admin/registrations/new.html.erb +65 -0
- data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
- data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
- data/app/views/easy_admin/resources/edit.html.erb +151 -0
- data/app/views/easy_admin/resources/index.html.erb +12 -0
- data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
- data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
- data/app/views/easy_admin/resources/new.html.erb +100 -0
- data/app/views/easy_admin/resources/show.html.erb +31 -0
- data/app/views/easy_admin/sessions/new.html.erb +55 -0
- data/app/views/easy_admin/settings/_form.html.erb +51 -0
- data/app/views/easy_admin/settings/index.html.erb +53 -0
- data/app/views/layouts/easy_admin/application.html.erb +48 -0
- data/app/views/layouts/easy_admin/auth.html.erb +34 -0
- data/config/initializers/easy_admin_card_factory.rb +27 -0
- data/config/initializers/pagy.rb +15 -0
- data/config/initializers/rack_mini_profiler.rb +67 -0
- data/config/routes.rb +70 -0
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
- data/lib/easy-admin.rb +32 -0
- data/lib/easy_admin/action.rb +159 -0
- data/lib/easy_admin/batch_action.rb +134 -0
- data/lib/easy_admin/configuration.rb +75 -0
- data/lib/easy_admin/dashboard.rb +110 -0
- data/lib/easy_admin/dashboard_registry.rb +30 -0
- data/lib/easy_admin/delete_action.rb +22 -0
- data/lib/easy_admin/engine.rb +54 -0
- data/lib/easy_admin/field.rb +118 -0
- data/lib/easy_admin/resource.rb +806 -0
- data/lib/easy_admin/resource_registry.rb +22 -0
- data/lib/easy_admin/types/json_type.rb +25 -0
- data/lib/easy_admin/version.rb +3 -0
- data/lib/generators/easy_admin/auth_generator.rb +69 -0
- data/lib/generators/easy_admin/card/card_generator.rb +94 -0
- data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
- data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
- data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
- data/lib/generators/easy_admin/install_generator.rb +25 -0
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
- data/lib/generators/easy_admin/resource_generator.rb +43 -0
- data/lib/generators/easy_admin/templates/AUTH_README +35 -0
- data/lib/generators/easy_admin/templates/README +27 -0
- data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
- data/lib/generators/easy_admin/templates/devise.rb +267 -0
- data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
- data/lib/generators/easy_admin/templates/resource.rb +29 -0
- data/lib/tasks/easy_admin_tasks.rake +4 -0
- metadata +445 -0
@@ -0,0 +1,324 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Resources
|
3
|
+
class ShowPageActionsComponent < BaseComponent
|
4
|
+
def initialize(record:, resource_class:, current_user: nil)
|
5
|
+
@record = record
|
6
|
+
@resource_class = resource_class
|
7
|
+
@current_user = current_user
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(class: "mt-4 sm:ml-16 sm:mt-0 sm:flex-none") do
|
12
|
+
# Desktop: Show buttons inline (hidden on mobile)
|
13
|
+
div(class: "hidden sm:flex sm:items-center sm:space-x-3") do
|
14
|
+
render_edit_action
|
15
|
+
render_row_actions
|
16
|
+
render_delete_action
|
17
|
+
end
|
18
|
+
|
19
|
+
# Mobile: Show dropdown (hidden on desktop)
|
20
|
+
div(class: "sm:hidden relative") do
|
21
|
+
render_mobile_dropdown
|
22
|
+
end
|
23
|
+
|
24
|
+
# Context menu container for this record
|
25
|
+
div(
|
26
|
+
id: "context-menu-#{@record.id}",
|
27
|
+
class: "fixed z-50 pointer-events-none"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def render_mobile_dropdown
|
35
|
+
div(
|
36
|
+
class: "relative",
|
37
|
+
data: { controller: "dropdown" }
|
38
|
+
) do
|
39
|
+
# Dropdown trigger button
|
40
|
+
button(
|
41
|
+
type: "button",
|
42
|
+
class: "w-full flex items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-colors duration-150",
|
43
|
+
data: { action: "click->dropdown#toggle" }
|
44
|
+
) do
|
45
|
+
plain "Actions"
|
46
|
+
unsafe_raw <<~SVG
|
47
|
+
<svg class="ml-2 h-4 w-4 text-white transition-transform duration-200" data-dropdown-target="chevron" viewBox="0 0 20 20" fill="currentColor">
|
48
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
|
49
|
+
</svg>
|
50
|
+
SVG
|
51
|
+
end
|
52
|
+
|
53
|
+
# Dropdown menu
|
54
|
+
div(
|
55
|
+
class: "absolute left-0 right-0 z-10 mt-2 origin-top-left rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none hidden",
|
56
|
+
data: { dropdown_target: "menu" }
|
57
|
+
) do
|
58
|
+
div(class: "py-1") do
|
59
|
+
# Edit action in dropdown
|
60
|
+
render_mobile_edit_action
|
61
|
+
|
62
|
+
# Custom row actions in dropdown
|
63
|
+
render_mobile_row_actions
|
64
|
+
|
65
|
+
# Delete action in dropdown (with separator)
|
66
|
+
if has_actions_to_show?
|
67
|
+
div(class: "border-t border-gray-100 my-1")
|
68
|
+
end
|
69
|
+
render_mobile_delete_action
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def render_edit_action
|
76
|
+
a(
|
77
|
+
href: easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record),
|
78
|
+
class: "inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors duration-150"
|
79
|
+
) do
|
80
|
+
unsafe_raw <<~SVG
|
81
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
82
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
83
|
+
</svg>
|
84
|
+
SVG
|
85
|
+
plain "Edit"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def render_row_actions
|
90
|
+
return unless @resource_class.has_row_actions?
|
91
|
+
|
92
|
+
available_actions = get_available_actions
|
93
|
+
|
94
|
+
available_actions.each do |action_data|
|
95
|
+
action_instance = action_data[:instance]
|
96
|
+
action_class = action_data[:class]
|
97
|
+
|
98
|
+
render_action_button(action_class, action_instance)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def render_action_button(action_class, action_instance)
|
103
|
+
button_class = case action_instance.class.style
|
104
|
+
when :danger
|
105
|
+
"inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 transition-colors duration-150"
|
106
|
+
when :warning
|
107
|
+
"inline-flex items-center rounded-md bg-yellow-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-yellow-500 transition-colors duration-150"
|
108
|
+
when :success
|
109
|
+
"inline-flex items-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 transition-colors duration-150"
|
110
|
+
else
|
111
|
+
"inline-flex items-center rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 transition-colors duration-150"
|
112
|
+
end
|
113
|
+
|
114
|
+
button(
|
115
|
+
type: "button",
|
116
|
+
class: button_class,
|
117
|
+
data: {
|
118
|
+
controller: "row-action",
|
119
|
+
action: "click->row-action#execute",
|
120
|
+
row_action_action_class_value: action_class.name,
|
121
|
+
row_action_execution_mode_value: action_instance.class.execution_mode.to_s,
|
122
|
+
row_action_confirm_value: action_instance.class.confirm_message,
|
123
|
+
row_action_record_id_value: @record.id,
|
124
|
+
row_action_resource_name_value: @resource_class.route_key
|
125
|
+
}
|
126
|
+
) do
|
127
|
+
if action_instance.class.icon
|
128
|
+
span(class: "mr-2") { action_instance.class.icon }
|
129
|
+
else
|
130
|
+
render_default_icon(action_instance)
|
131
|
+
end
|
132
|
+
plain action_instance.class.label
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def render_delete_action
|
137
|
+
button(
|
138
|
+
type: "button",
|
139
|
+
class: "inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 transition-colors duration-150",
|
140
|
+
data: {
|
141
|
+
controller: "row-action",
|
142
|
+
action: "click->row-action#execute",
|
143
|
+
row_action_action_class_value: "EasyAdmin::DeleteAction",
|
144
|
+
row_action_execution_mode_value: "instant",
|
145
|
+
row_action_confirm_value: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
|
146
|
+
row_action_record_id_value: @record.id,
|
147
|
+
row_action_resource_name_value: @resource_class.route_key
|
148
|
+
}
|
149
|
+
) do
|
150
|
+
unsafe_raw <<~SVG
|
151
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
152
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
153
|
+
</svg>
|
154
|
+
SVG
|
155
|
+
plain "Delete"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def render_mobile_edit_action
|
160
|
+
a(
|
161
|
+
href: easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record),
|
162
|
+
class: "group flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
163
|
+
) do
|
164
|
+
unsafe_raw <<~SVG
|
165
|
+
<svg class="mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
166
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
167
|
+
</svg>
|
168
|
+
SVG
|
169
|
+
plain "Edit"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def render_mobile_row_actions
|
174
|
+
return unless @resource_class.has_row_actions?
|
175
|
+
|
176
|
+
available_actions = get_available_actions
|
177
|
+
|
178
|
+
available_actions.each do |action_data|
|
179
|
+
action_instance = action_data[:instance]
|
180
|
+
action_class = action_data[:class]
|
181
|
+
|
182
|
+
render_mobile_action_item(action_class, action_instance)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def render_mobile_action_item(action_class, action_instance)
|
187
|
+
text_color = case action_instance.class.style
|
188
|
+
when :danger
|
189
|
+
"text-red-700 hover:bg-red-50 hover:text-red-900"
|
190
|
+
when :warning
|
191
|
+
"text-yellow-700 hover:bg-yellow-50 hover:text-yellow-900"
|
192
|
+
when :success
|
193
|
+
"text-green-700 hover:bg-green-50 hover:text-green-900"
|
194
|
+
else
|
195
|
+
"text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
196
|
+
end
|
197
|
+
|
198
|
+
button(
|
199
|
+
type: "button",
|
200
|
+
class: "group flex w-full items-center px-4 py-2 text-sm #{text_color}",
|
201
|
+
data: {
|
202
|
+
controller: "row-action",
|
203
|
+
action: "click->row-action#execute",
|
204
|
+
row_action_action_class_value: action_class.name,
|
205
|
+
row_action_execution_mode_value: action_instance.class.execution_mode.to_s,
|
206
|
+
row_action_confirm_value: action_instance.class.confirm_message,
|
207
|
+
row_action_record_id_value: @record.id,
|
208
|
+
row_action_resource_name_value: @resource_class.route_key
|
209
|
+
}
|
210
|
+
) do
|
211
|
+
if action_instance.class.icon
|
212
|
+
span(class: "mr-3 text-base") { action_instance.class.icon }
|
213
|
+
else
|
214
|
+
render_mobile_default_icon(action_instance)
|
215
|
+
end
|
216
|
+
plain action_instance.class.label
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def render_mobile_delete_action
|
221
|
+
button(
|
222
|
+
type: "button",
|
223
|
+
class: "group flex w-full items-center px-4 py-2 text-sm text-red-700 hover:bg-red-50 hover:text-red-900",
|
224
|
+
data: {
|
225
|
+
controller: "row-action",
|
226
|
+
action: "click->row-action#execute",
|
227
|
+
row_action_action_class_value: "EasyAdmin::DeleteAction",
|
228
|
+
row_action_execution_mode_value: "instant",
|
229
|
+
row_action_confirm_value: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
|
230
|
+
row_action_record_id_value: @record.id,
|
231
|
+
row_action_resource_name_value: @resource_class.route_key
|
232
|
+
}
|
233
|
+
) do
|
234
|
+
unsafe_raw <<~SVG
|
235
|
+
<svg class="mr-3 h-4 w-4 text-red-400 group-hover:text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
236
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
237
|
+
</svg>
|
238
|
+
SVG
|
239
|
+
plain "Delete"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def render_mobile_default_icon(action_instance)
|
244
|
+
icon_color = case action_instance.class.style
|
245
|
+
when :danger
|
246
|
+
"text-red-400 group-hover:text-red-500"
|
247
|
+
when :warning
|
248
|
+
"text-yellow-400 group-hover:text-yellow-500"
|
249
|
+
when :success
|
250
|
+
"text-green-400 group-hover:text-green-500"
|
251
|
+
else
|
252
|
+
"text-gray-400 group-hover:text-gray-500"
|
253
|
+
end
|
254
|
+
|
255
|
+
case action_instance.class.style
|
256
|
+
when :danger
|
257
|
+
unsafe_raw <<~SVG
|
258
|
+
<svg class="mr-3 h-4 w-4 #{icon_color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
259
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
260
|
+
</svg>
|
261
|
+
SVG
|
262
|
+
when :success
|
263
|
+
unsafe_raw <<~SVG
|
264
|
+
<svg class="mr-3 h-4 w-4 #{icon_color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
265
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
266
|
+
</svg>
|
267
|
+
SVG
|
268
|
+
else
|
269
|
+
unsafe_raw <<~SVG
|
270
|
+
<svg class="mr-3 h-4 w-4 #{icon_color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
271
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
272
|
+
</svg>
|
273
|
+
SVG
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def has_actions_to_show?
|
278
|
+
@resource_class.has_row_actions? && get_available_actions.any?
|
279
|
+
end
|
280
|
+
|
281
|
+
def get_available_actions
|
282
|
+
return [] unless @resource_class.has_row_actions?
|
283
|
+
|
284
|
+
@resource_class.row_actions.map do |action_config|
|
285
|
+
action_instance = action_config[:class].new(
|
286
|
+
record: @record,
|
287
|
+
current_user: @current_user,
|
288
|
+
resource_class: @resource_class
|
289
|
+
)
|
290
|
+
|
291
|
+
next unless action_instance.visible? && action_instance.permitted?
|
292
|
+
|
293
|
+
{
|
294
|
+
class: action_config[:class],
|
295
|
+
instance: action_instance
|
296
|
+
}
|
297
|
+
end.compact
|
298
|
+
end
|
299
|
+
|
300
|
+
def render_default_icon(action_instance)
|
301
|
+
case action_instance.class.style
|
302
|
+
when :danger
|
303
|
+
unsafe_raw <<~SVG
|
304
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
305
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
306
|
+
</svg>
|
307
|
+
SVG
|
308
|
+
when :success
|
309
|
+
unsafe_raw <<~SVG
|
310
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
311
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
312
|
+
</svg>
|
313
|
+
SVG
|
314
|
+
else
|
315
|
+
unsafe_raw <<~SVG
|
316
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
317
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
318
|
+
</svg>
|
319
|
+
SVG
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Resources
|
3
|
+
class TableCellComponent < BaseComponent
|
4
|
+
def initialize(record:, field_config:, resource_class:, mobile_hide: false)
|
5
|
+
@record = record
|
6
|
+
@field_config = field_config
|
7
|
+
@resource_class = resource_class
|
8
|
+
@mobile_hide = mobile_hide
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
td(id: cell_id, class: cell_classes) do
|
13
|
+
div(class: "flex items-center justify-between group-hover:bg-transparent") do
|
14
|
+
div(class: "flex-1 text-gray-900 font-medium") do
|
15
|
+
render_field_value
|
16
|
+
end
|
17
|
+
|
18
|
+
if field_editable?
|
19
|
+
div(class: "ml-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200") do
|
20
|
+
render_inline_edit_trigger
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def render_field_value
|
30
|
+
# Use the existing field rendering logic from the Rails views
|
31
|
+
field_value = @record.public_send(@field_config[:name])
|
32
|
+
|
33
|
+
# Use the same render_field helper that the ERB templates use
|
34
|
+
unsafe_raw render_field(
|
35
|
+
@field_config,
|
36
|
+
action: :index,
|
37
|
+
value: field_value,
|
38
|
+
record: @record
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def render_inline_edit_trigger
|
43
|
+
div do
|
44
|
+
if field_editable_via_menu?
|
45
|
+
render_context_menu_trigger
|
46
|
+
else
|
47
|
+
render_modal_trigger
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_modal_trigger
|
53
|
+
# Simple modal trigger - create a field-like object for the component
|
54
|
+
field_obj = create_field_for_trigger
|
55
|
+
render EasyAdmin::Fields::InlineEditTriggerComponent.new(
|
56
|
+
record: @record,
|
57
|
+
field: field_obj
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_context_menu_trigger
|
62
|
+
# Simple context menu trigger - create a field-like object for the component
|
63
|
+
field_obj = create_field_for_trigger
|
64
|
+
render EasyAdmin::Fields::InlineEditTriggerComponent.new(
|
65
|
+
record: @record,
|
66
|
+
field: field_obj,
|
67
|
+
position: :left
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_field_for_trigger
|
72
|
+
# Create a simple object that has the methods the InlineEditTriggerComponent needs
|
73
|
+
OpenStruct.new(
|
74
|
+
name: field[:name],
|
75
|
+
type: field[:type],
|
76
|
+
label: field[:label] || field[:name].to_s.humanize,
|
77
|
+
editable: field[:editable]
|
78
|
+
).tap do |f|
|
79
|
+
def f.editable?
|
80
|
+
editable.present?
|
81
|
+
end
|
82
|
+
|
83
|
+
def f.editable_via_menu?
|
84
|
+
type == :belongs_to && editable&.dig(:via) == :menu_or_modal
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def render_pencil_icon
|
90
|
+
unsafe_raw <<~SVG
|
91
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
92
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
|
93
|
+
</svg>
|
94
|
+
SVG
|
95
|
+
end
|
96
|
+
|
97
|
+
def cell_classes
|
98
|
+
[
|
99
|
+
"whitespace-nowrap px-6 py-4 text-sm relative",
|
100
|
+
@mobile_hide ? "mobile-hide" : nil
|
101
|
+
].compact.join(" ")
|
102
|
+
end
|
103
|
+
|
104
|
+
def cell_id
|
105
|
+
"#{helpers.dom_id(@record)}_#{@field_config[:name]}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def field
|
109
|
+
@field ||= @field_config
|
110
|
+
end
|
111
|
+
|
112
|
+
def field_editable?
|
113
|
+
field[:editable].present?
|
114
|
+
end
|
115
|
+
|
116
|
+
def field_editable_via_menu?
|
117
|
+
field[:type] == :belongs_to && field[:editable]&.dig(:via) == :menu_or_modal
|
118
|
+
end
|
119
|
+
|
120
|
+
def edit_field_url
|
121
|
+
easy_admin_url_helpers.edit_field_resource_path(
|
122
|
+
@resource_class.route_key,
|
123
|
+
id: @record.id,
|
124
|
+
field: @field_config[:name]
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def belongs_to_reattach_url
|
129
|
+
easy_admin_url_helpers.belongs_to_reattach_resource_path(
|
130
|
+
@resource_class.route_key,
|
131
|
+
id: @record.id,
|
132
|
+
field: @field_config[:name]
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
def belongs_to_edit_attached_url
|
137
|
+
easy_admin_url_helpers.belongs_to_edit_attached_resource_path(
|
138
|
+
@resource_class.route_key,
|
139
|
+
id: @record.id,
|
140
|
+
field: @field_config[:name]
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Resources
|
3
|
+
class TableComponent < BaseComponent
|
4
|
+
def initialize(resource_class:, records:, current_params: {}, current_user: nil)
|
5
|
+
@resource_class = resource_class
|
6
|
+
@records = records
|
7
|
+
@current_params = current_params
|
8
|
+
@current_user = current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
div(data: batch_actions_enabled? ? {
|
13
|
+
controller: "batch-selection",
|
14
|
+
batch_selection_resource_name_value: @resource_class.route_key,
|
15
|
+
action: "batch-action:completed@window->batch-selection#clearSelection"
|
16
|
+
} : {}) do
|
17
|
+
table(class: "table-striped min-w-full divide-y divide-gray-300") do
|
18
|
+
render_table_header
|
19
|
+
render_table_body
|
20
|
+
end
|
21
|
+
|
22
|
+
# Render batch action bar if enabled
|
23
|
+
render_batch_action_bar if batch_actions_enabled?
|
24
|
+
|
25
|
+
# Context menu containers for each record
|
26
|
+
render_context_menu_containers
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def render_table_header
|
33
|
+
thead(class: "bg-gray-50") do
|
34
|
+
tr do
|
35
|
+
# Checkbox column for batch actions
|
36
|
+
if batch_actions_enabled?
|
37
|
+
th(scope: "col", class: "relative w-16 sm:w-12 px-3 sm:px-6 py-3") do
|
38
|
+
div(class: "absolute left-2 sm:left-4 top-1/2 -mt-3") do
|
39
|
+
render_ios_checkbox(
|
40
|
+
target: "selectAll",
|
41
|
+
action: "change->batch-selection#toggleAll",
|
42
|
+
data: { select_all: true }
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
@resource_class.index_fields.each_with_index do |field, index|
|
49
|
+
render_header_cell(field, index)
|
50
|
+
end
|
51
|
+
th(scope: "col", class: "relative px-6 py-3") do
|
52
|
+
span(class: "sr-only") { "Actions" }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_header_cell(field, index)
|
59
|
+
th(
|
60
|
+
scope: "col",
|
61
|
+
class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide #{'mobile-hide' if index > 2}"
|
62
|
+
) do
|
63
|
+
if field[:sortable]
|
64
|
+
render_sortable_header(field)
|
65
|
+
else
|
66
|
+
field[:label]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def render_sortable_header(field)
|
72
|
+
div(class: "flex items-center justify-between") do
|
73
|
+
next_direction = determine_next_direction(field)
|
74
|
+
|
75
|
+
a(
|
76
|
+
href: sort_url(field[:name], next_direction),
|
77
|
+
class: "group hover:text-gray-900 transition-colors duration-150"
|
78
|
+
) do
|
79
|
+
field[:label]
|
80
|
+
end
|
81
|
+
|
82
|
+
span(class: "ml-2 flex-none text-gray-400 hover:text-gray-600") do
|
83
|
+
render_sort_icon(field)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def render_sort_icon(field)
|
89
|
+
if currently_sorted_by?(field)
|
90
|
+
if current_sort_ascending?
|
91
|
+
# Up arrow for ascending
|
92
|
+
unsafe_raw <<~SVG
|
93
|
+
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
94
|
+
<path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" clip-rule="evenodd"/>
|
95
|
+
</svg>
|
96
|
+
SVG
|
97
|
+
else
|
98
|
+
# Down arrow for descending
|
99
|
+
unsafe_raw <<~SVG
|
100
|
+
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
101
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
|
102
|
+
</svg>
|
103
|
+
SVG
|
104
|
+
end
|
105
|
+
else
|
106
|
+
# Default down arrow for unsorted
|
107
|
+
unsafe_raw <<~SVG
|
108
|
+
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
109
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
|
110
|
+
</svg>
|
111
|
+
SVG
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def render_table_body
|
116
|
+
tbody(id: "records-container", class: "divide-y divide-gray-200 bg-white") do
|
117
|
+
@records.each do |record|
|
118
|
+
render EasyAdmin::Resources::TableRowComponent.new(
|
119
|
+
record: record,
|
120
|
+
resource_class: @resource_class
|
121
|
+
)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def determine_next_direction(field)
|
127
|
+
if currently_sorted_by?(field) && current_sort_ascending?
|
128
|
+
'desc'
|
129
|
+
else
|
130
|
+
'asc'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def currently_sorted_by?(field)
|
135
|
+
@current_params[:sort] == field[:name].to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
def current_sort_ascending?
|
139
|
+
@current_params[:direction] == 'asc'
|
140
|
+
end
|
141
|
+
|
142
|
+
def sort_url(field_name, direction)
|
143
|
+
# Build URL with current params but update sort and direction
|
144
|
+
updated_params = @current_params.merge(sort: field_name, direction: direction)
|
145
|
+
base_url = easy_admin_url_helpers.resources_path(@resource_class.route_key)
|
146
|
+
query_string = updated_params.present? ? updated_params.to_query : ""
|
147
|
+
query_string.present? ? "#{base_url}?#{query_string}" : base_url
|
148
|
+
end
|
149
|
+
|
150
|
+
def batch_actions_enabled?
|
151
|
+
@resource_class.batch_actions_enabled
|
152
|
+
end
|
153
|
+
|
154
|
+
def render_batch_action_bar
|
155
|
+
render EasyAdmin::BatchActionBarComponent.new(
|
156
|
+
resource_class: @resource_class,
|
157
|
+
current_user: @current_user
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
def render_context_menu_containers
|
162
|
+
@records.each do |record|
|
163
|
+
div(
|
164
|
+
id: "context-menu-#{record.id}",
|
165
|
+
class: "fixed z-50 pointer-events-none"
|
166
|
+
)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def render_ios_checkbox(target: nil, action: nil, value: nil, data: {})
|
171
|
+
label(class: "inline-flex items-center cursor-pointer group") do
|
172
|
+
input(
|
173
|
+
type: "checkbox",
|
174
|
+
value: value,
|
175
|
+
class: "sr-only peer",
|
176
|
+
data: data.merge({
|
177
|
+
batch_selection_target: target,
|
178
|
+
action: action
|
179
|
+
}.compact)
|
180
|
+
)
|
181
|
+
|
182
|
+
# Custom checkbox design
|
183
|
+
div(
|
184
|
+
class: "relative w-6 h-6 bg-white border-2 border-gray-300 rounded-md
|
185
|
+
transition-all duration-200 ease-out
|
186
|
+
peer-checked:bg-blue-500 peer-checked:border-blue-500
|
187
|
+
peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-opacity-50
|
188
|
+
group-hover:border-gray-400 peer-checked:group-hover:border-blue-600
|
189
|
+
shadow-sm"
|
190
|
+
) do
|
191
|
+
# Checkmark icon (hidden by default, shown when checked)
|
192
|
+
unsafe_raw <<~SVG
|
193
|
+
<svg class="absolute inset-0 w-4 h-4 m-auto text-white opacity-0
|
194
|
+
transition-opacity duration-200 ease-out
|
195
|
+
peer-checked:opacity-100 pointer-events-none"
|
196
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
197
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
|
198
|
+
d="M5 13l4 4L19 7"/>
|
199
|
+
</svg>
|
200
|
+
SVG
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|