panda-core 0.12.5 → 1.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 +4 -4
- data/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +236 -36
- data/app/builders/panda/core/form_builder.rb +120 -18
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +11 -15
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +36 -31
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +94 -16
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -76
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +3 -6
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +108 -38
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +17 -8
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +203 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -4,122 +4,89 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class TableComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
attr_reader :term, :rows, :icon, :sort, :sort_direction
|
|
8
|
+
|
|
9
|
+
def initialize(term: "", rows: [], icon: "", responsive: true, sort: nil, sort_direction: nil, **attrs, &block)
|
|
10
|
+
@term = term
|
|
11
|
+
@rows = rows
|
|
12
|
+
@icon = icon
|
|
13
|
+
@responsive = responsive
|
|
14
|
+
@sort = sort&.to_s
|
|
15
|
+
@sort_direction = (sort_direction&.to_s == "desc") ? "desc" : "asc"
|
|
15
16
|
@columns = []
|
|
17
|
+
@setup_block = block
|
|
18
|
+
super(**attrs)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
instance_eval(&block) if block_given?
|
|
21
|
-
|
|
22
|
-
if @rows.any?
|
|
23
|
-
render_table_with_rows
|
|
24
|
-
else
|
|
25
|
-
render_empty_table
|
|
26
|
-
end
|
|
21
|
+
def responsive?
|
|
22
|
+
@responsive
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
def column(label, width: nil, &cell_block)
|
|
30
|
-
@columns << Column.new(label, width, &cell_block)
|
|
25
|
+
def column(label, width: nil, sortable: false, sort_key: nil, &cell_block)
|
|
26
|
+
@columns << Column.new(label, width, sortable: sortable, sort_key: sort_key, &cell_block)
|
|
27
|
+
self # Allow chaining
|
|
31
28
|
end
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
render_rows
|
|
39
|
-
end
|
|
30
|
+
def render_cell_content(row, cell_block)
|
|
31
|
+
# Use capture to properly handle ERB blocks that output to the template buffer.
|
|
32
|
+
# Directly calling cell_block.call(row) causes double-rendering because ERB blocks
|
|
33
|
+
# both output to the buffer AND return their last expression.
|
|
34
|
+
helpers.capture(row, &cell_block)
|
|
40
35
|
end
|
|
41
36
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
def sort_url_for(column)
|
|
38
|
+
return unless column.sortable?
|
|
39
|
+
|
|
40
|
+
new_direction = (sort == column.sort_key && sort_direction == "asc") ? "desc" : "asc"
|
|
41
|
+
request = helpers.request
|
|
42
|
+
query_params = request.query_parameters.merge("sort" => column.sort_key, "direction" => new_direction)
|
|
43
|
+
"#{request.path}?#{query_params.to_query}"
|
|
48
44
|
end
|
|
49
45
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
div(class: "table-row text-base font-medium text-white bg-gray-800") do
|
|
53
|
-
@columns.each_with_index do |column, i|
|
|
54
|
-
header_classes = "table-cell sticky top-0 z-10 p-4"
|
|
55
|
-
header_classes += " rounded-tl-md" if i.zero?
|
|
56
|
-
header_classes += " rounded-tr-md" if i == @columns.size - 1
|
|
46
|
+
def sort_indicator_for(column)
|
|
47
|
+
return unless sort == column.sort_key
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
div(class: header_classes, style: header_style) { column.label }
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
49
|
+
(sort_direction == "asc") ? "↑" : "↓"
|
|
63
50
|
end
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end
|
|
78
|
-
end
|
|
52
|
+
# Lazy accessor that ensures columns are registered before returning them.
|
|
53
|
+
# Supports two patterns:
|
|
54
|
+
# 1. Block passed to new() - executed here (for tests)
|
|
55
|
+
# 2. Block passed to render() - executed via content (for ERB templates)
|
|
56
|
+
def columns
|
|
57
|
+
unless @columns_registered
|
|
58
|
+
if @setup_block
|
|
59
|
+
# Block was passed to new() - execute it
|
|
60
|
+
@setup_block.call(self)
|
|
61
|
+
else
|
|
62
|
+
# Block was passed to render() - execute via content
|
|
63
|
+
content
|
|
79
64
|
end
|
|
65
|
+
@columns_registered = true
|
|
80
66
|
end
|
|
67
|
+
@columns
|
|
81
68
|
end
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
# When called from ERB, we need to capture the block's output buffer
|
|
85
|
-
# When called from Phlex, evaluate directly
|
|
86
|
-
if defined?(view_context) && view_context
|
|
87
|
-
# Use capture to get ERB output buffer content
|
|
88
|
-
captured_html = view_context.capture(row, &cell_block)
|
|
89
|
-
# Render the captured HTML (already html_safe from capture)
|
|
90
|
-
raw(captured_html)
|
|
91
|
-
else
|
|
92
|
-
# Pure Phlex context - execute block directly
|
|
93
|
-
result = cell_block.call(row)
|
|
70
|
+
private
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
plain(result)
|
|
98
|
-
elsif result.respond_to?(:render_in)
|
|
99
|
-
render(result)
|
|
100
|
-
else
|
|
101
|
-
plain(result.to_s)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
72
|
+
def pluralized_term
|
|
73
|
+
@pluralized_term ||= ActiveSupport::Inflector.pluralize(term)
|
|
104
74
|
end
|
|
105
75
|
end
|
|
106
76
|
|
|
107
77
|
class Column
|
|
108
|
-
attr_reader :label, :cell, :width
|
|
78
|
+
attr_reader :label, :cell, :width, :sort_key
|
|
109
79
|
|
|
110
|
-
def initialize(label, width = nil, &block)
|
|
80
|
+
def initialize(label, width = nil, sortable: false, sort_key: nil, &block)
|
|
111
81
|
@label = label
|
|
112
82
|
@width = width
|
|
83
|
+
@sortable = sortable
|
|
84
|
+
@sort_key = sort_key || ActiveSupport::Inflector.parameterize(label.to_s, separator: "_")
|
|
113
85
|
@cell = block
|
|
114
86
|
end
|
|
115
|
-
end
|
|
116
87
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def initialize(label, &block)
|
|
121
|
-
@label = label
|
|
122
|
-
@cell = Panda::Core::Admin::TagComponent.new(status: block)
|
|
88
|
+
def sortable?
|
|
89
|
+
@sortable
|
|
123
90
|
end
|
|
124
91
|
end
|
|
125
92
|
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
2
|
+
style="background-color: <%= colour %>1a; color: <%= colour %>; border: 1px solid <%= colour %>33;">
|
|
3
|
+
<%= tag.name %>
|
|
4
|
+
<% if removable %>
|
|
5
|
+
<button type="button" class="ml-0.5 hover:opacity-70" data-action="tag-input#removeTag" data-tag-id="<%= tag.id %>">×</button>
|
|
6
|
+
<% end %>
|
|
7
|
+
</span>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class TagBadgeComponent < Panda::Core::Base
|
|
7
|
+
attr_reader :tag, :removable
|
|
8
|
+
|
|
9
|
+
def initialize(tag:, removable: false, **attrs)
|
|
10
|
+
@tag = tag
|
|
11
|
+
@removable = removable
|
|
12
|
+
super(**attrs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def colour
|
|
16
|
+
tag.display_colour
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= content_tag :span, computed_text, class: tag_classes %>
|
|
@@ -4,15 +4,14 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class TagComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
span(class: tag_classes) { computed_text }
|
|
7
|
+
def initialize(text: nil, page_type: nil, status: :active, **attrs)
|
|
8
|
+
@status = status
|
|
9
|
+
@text = text
|
|
10
|
+
@page_type = page_type
|
|
11
|
+
super(**attrs)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
attr_reader :status, :text, :page_type
|
|
16
15
|
|
|
17
16
|
def computed_text
|
|
18
17
|
if @page_type
|
|
@@ -33,44 +32,76 @@ module Panda
|
|
|
33
32
|
end
|
|
34
33
|
end
|
|
35
34
|
|
|
35
|
+
# Badge colours are themeable via the --panda-badge-{success,error,
|
|
36
|
+
# warning,info,neutral}-{bg,fg} tokens (see
|
|
37
|
+
# app/assets/tailwind/application.css). Each helper below returns an
|
|
38
|
+
# arbitrary-value Tailwind utility pair whose var() fallback resolves
|
|
39
|
+
# to exactly today's literal emerald/amber/sky/rose/gray-100
|
|
40
|
+
# utility classes, so this is a no-op unless a host app sets
|
|
41
|
+
# data-theme.
|
|
36
42
|
def tag_classes
|
|
37
|
-
base = "inline-flex items-center
|
|
43
|
+
base = "inline-flex items-center px-2.5 py-0.5 text-xs font-medium rounded-full "
|
|
38
44
|
base + (@page_type ? type_classes : status_classes)
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def type_classes
|
|
42
48
|
case @page_type
|
|
43
49
|
when :system
|
|
44
|
-
|
|
50
|
+
badge_classes(:error)
|
|
45
51
|
when :posts
|
|
46
|
-
|
|
52
|
+
badge_classes(:info)
|
|
47
53
|
when :code
|
|
48
|
-
|
|
54
|
+
badge_classes(:info)
|
|
49
55
|
when :standard
|
|
50
|
-
|
|
56
|
+
badge_classes(:success)
|
|
51
57
|
when :hidden_type
|
|
52
|
-
|
|
58
|
+
badge_classes(:neutral)
|
|
53
59
|
else
|
|
54
|
-
|
|
60
|
+
badge_classes(:neutral)
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def status_classes
|
|
59
65
|
case @status
|
|
60
66
|
when :active
|
|
61
|
-
|
|
67
|
+
badge_classes(:success)
|
|
68
|
+
when :live
|
|
69
|
+
badge_classes(:success)
|
|
62
70
|
when :draft
|
|
63
|
-
|
|
71
|
+
badge_classes(:warning)
|
|
64
72
|
when :inactive, :hidden
|
|
65
|
-
|
|
73
|
+
badge_classes(:neutral)
|
|
66
74
|
when :auto
|
|
67
|
-
|
|
75
|
+
badge_classes(:info)
|
|
76
|
+
when :warning
|
|
77
|
+
badge_classes(:error)
|
|
68
78
|
when :static
|
|
69
|
-
|
|
79
|
+
badge_classes(:neutral)
|
|
70
80
|
else
|
|
71
|
-
|
|
81
|
+
badge_classes(:neutral)
|
|
72
82
|
end
|
|
73
83
|
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Tailwind's content scanner extracts candidate class names by
|
|
88
|
+
# scanning raw source text — it does not evaluate Ruby, so every
|
|
89
|
+
# arbitrary-value class must appear as a complete literal string
|
|
90
|
+
# somewhere in this file (interpolating the tone into the class name
|
|
91
|
+
# would produce unscannable text like "bg-[var(--panda-badge-#{tone}
|
|
92
|
+
# -bg...)]" and silently generate no CSS).
|
|
93
|
+
BADGE_CLASSES = {
|
|
94
|
+
success: "text-[var(--panda-badge-success-fg,var(--color-emerald-600))] bg-[var(--panda-badge-success-bg,var(--color-emerald-50))]",
|
|
95
|
+
warning: "text-[var(--panda-badge-warning-fg,var(--color-amber-600))] bg-[var(--panda-badge-warning-bg,var(--color-amber-50))]",
|
|
96
|
+
info: "text-[var(--panda-badge-info-fg,var(--color-sky-600))] bg-[var(--panda-badge-info-bg,var(--color-sky-50))]",
|
|
97
|
+
error: "text-[var(--panda-badge-error-fg,var(--color-rose-600))] bg-[var(--panda-badge-error-bg,var(--color-rose-50))]",
|
|
98
|
+
neutral: "text-[var(--panda-badge-neutral-fg,var(--color-gray-600))] bg-[var(--panda-badge-neutral-bg,var(--color-gray-100))]"
|
|
99
|
+
}.freeze
|
|
100
|
+
private_constant :BADGE_CLASSES
|
|
101
|
+
|
|
102
|
+
def badge_classes(tone)
|
|
103
|
+
BADGE_CLASSES.fetch(tone)
|
|
104
|
+
end
|
|
74
105
|
end
|
|
75
106
|
end
|
|
76
107
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div data-controller="tag-input"
|
|
2
|
+
data-tag-input-url-value="<%= tags_url %>"
|
|
3
|
+
data-tag-input-selected-value="<%= selected_tags_json %>"
|
|
4
|
+
class="relative">
|
|
5
|
+
|
|
6
|
+
<%# Selected tags as pills + hidden inputs %>
|
|
7
|
+
<div data-tag-input-target="selected" class="flex flex-wrap gap-1.5 mb-2">
|
|
8
|
+
<% selected_tags.each do |tag| %>
|
|
9
|
+
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
10
|
+
style="background-color: <%= tag.display_colour %>1a; color: <%= tag.display_colour %>; border: 1px solid <%= tag.display_colour %>33;"
|
|
11
|
+
data-tag-id="<%= tag.id %>">
|
|
12
|
+
<%= tag.name %>
|
|
13
|
+
<button type="button" class="ml-0.5 hover:opacity-70" data-action="tag-input#removeTag" data-tag-id="<%= tag.id %>">×</button>
|
|
14
|
+
</span>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<%# Hidden inputs for form submission %>
|
|
19
|
+
<div data-tag-input-target="hiddenInputs">
|
|
20
|
+
<% selected_tags.each do |tag| %>
|
|
21
|
+
<input type="hidden" name="<%= field_name %>" value="<%= tag.id %>" data-tag-id="<%= tag.id %>">
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<%# Search input %>
|
|
26
|
+
<input type="text"
|
|
27
|
+
data-tag-input-target="input"
|
|
28
|
+
data-action="focus->tag-input#showAll input->tag-input#search keydown->tag-input#keydown"
|
|
29
|
+
placeholder="Search or add tags..."
|
|
30
|
+
autocomplete="off"
|
|
31
|
+
class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-primary">
|
|
32
|
+
|
|
33
|
+
<%# Results dropdown %>
|
|
34
|
+
<div data-tag-input-target="results"
|
|
35
|
+
class="absolute z-10 mt-1 hidden w-full rounded-lg border border-gray-200 bg-white shadow-lg max-h-48 overflow-y-auto">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class TagInputComponent < Panda::Core::Base
|
|
7
|
+
attr_reader :field_name, :tags_url, :selected_tags
|
|
8
|
+
|
|
9
|
+
def initialize(tags_url:, field_name: "tag_ids[]", selected_tags: [], **attrs)
|
|
10
|
+
@field_name = field_name
|
|
11
|
+
@tags_url = tags_url
|
|
12
|
+
@selected_tags = selected_tags
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def selected_tags_json
|
|
17
|
+
selected_tags.map { |t| {id: t.id, name: t.name, colour: t.display_colour} }.to_json
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
2
|
+
<% panel.with_heading_slot { heading } %>
|
|
3
|
+
<div class="flex flex-wrap gap-2">
|
|
4
|
+
<% tags.each do |tag| %>
|
|
5
|
+
<%= render Panda::Core::Admin::TagBadgeComponent.new(tag: tag) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
</div>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Renders a PanelComponent containing TagBadgeComponents for a collection of tags.
|
|
7
|
+
# Self-hides via `render?` when the tags collection is empty.
|
|
8
|
+
#
|
|
9
|
+
# Lives in panda-core because tags are a core Panda concept (Panda::Core::Tag)
|
|
10
|
+
# and this component composes two existing panda-core components (PanelComponent
|
|
11
|
+
# and TagBadgeComponent) into a pattern used on most admin show pages.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# render Panda::Core::Admin::TagsPanelComponent.new(tags: @person.tags)
|
|
15
|
+
#
|
|
16
|
+
# @example Custom heading
|
|
17
|
+
# render Panda::Core::Admin::TagsPanelComponent.new(tags: @item.tags, heading: "Categories")
|
|
18
|
+
class TagsPanelComponent < Panda::Core::Base
|
|
19
|
+
attr_reader :tags, :heading
|
|
20
|
+
|
|
21
|
+
def initialize(tags:, heading: "Tags", **attrs)
|
|
22
|
+
@tags = tags
|
|
23
|
+
@heading = heading
|
|
24
|
+
super(**attrs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render?
|
|
28
|
+
tags.present? && tags.any?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% if should_render? %>
|
|
2
|
+
<% if @user.present? && time %>
|
|
3
|
+
<%= render Panda::Core::Admin::UserDisplayComponent.new(
|
|
4
|
+
user: @user,
|
|
5
|
+
metadata: "#{time_ago_in_words(time)} ago"
|
|
6
|
+
) %>
|
|
7
|
+
<% elsif @user.present? %>
|
|
8
|
+
<%= render Panda::Core::Admin::UserDisplayComponent.new(
|
|
9
|
+
user: @user,
|
|
10
|
+
metadata: "Not published"
|
|
11
|
+
) %>
|
|
12
|
+
<% elsif time %>
|
|
13
|
+
<div class="text-black/60"><%= "#{time_ago_in_words(time)} ago" %></div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
@@ -4,29 +4,16 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class UserActivityComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
def initialize(model: nil, at: nil, user: nil, **attrs)
|
|
8
|
+
@model = model
|
|
9
|
+
@at = at
|
|
10
|
+
@user = user
|
|
11
|
+
super(**attrs)
|
|
12
|
+
end
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
return unless should_render?
|
|
14
|
+
attr_reader :model, :at, :user
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
render Panda::Core::Admin::UserDisplayComponent.new(
|
|
18
|
-
user: @user,
|
|
19
|
-
metadata: "#{time_ago_in_words(time)} ago"
|
|
20
|
-
)
|
|
21
|
-
elsif @user.is_a?(Panda::Core::User)
|
|
22
|
-
render Panda::Core::Admin::UserDisplayComponent.new(
|
|
23
|
-
user: @user,
|
|
24
|
-
metadata: "Not published"
|
|
25
|
-
)
|
|
26
|
-
elsif time
|
|
27
|
-
div(class: "text-black/60") { "#{time_ago_in_words(time)} ago" }
|
|
28
|
-
end
|
|
29
|
-
end
|
|
16
|
+
include ActionView::Helpers::DateHelper
|
|
30
17
|
|
|
31
18
|
private
|
|
32
19
|
|
|
@@ -35,7 +22,7 @@ module Panda
|
|
|
35
22
|
end
|
|
36
23
|
|
|
37
24
|
def should_render?
|
|
38
|
-
@user.
|
|
25
|
+
@user.present? || time
|
|
39
26
|
end
|
|
40
27
|
end
|
|
41
28
|
end
|
|
@@ -4,21 +4,15 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
6
|
class UserDisplayComponent < Panda::Core::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return unless resolved_user
|
|
13
|
-
|
|
14
|
-
div(class: "block flex-shrink-0 group") do
|
|
15
|
-
div(class: "flex items-center") do
|
|
16
|
-
render_avatar
|
|
17
|
-
render_user_info
|
|
18
|
-
end
|
|
19
|
-
end
|
|
7
|
+
def initialize(user_id: nil, user: nil, metadata: "", **attrs)
|
|
8
|
+
@user_id = user_id
|
|
9
|
+
@user = user
|
|
10
|
+
@metadata = metadata
|
|
11
|
+
super(**attrs)
|
|
20
12
|
end
|
|
21
13
|
|
|
14
|
+
attr_reader :user_id, :user, :metadata
|
|
15
|
+
|
|
22
16
|
private
|
|
23
17
|
|
|
24
18
|
def resolved_user
|
|
@@ -34,8 +28,8 @@ module Panda
|
|
|
34
28
|
resolved_user.avatar_url(size: :small).present?
|
|
35
29
|
|
|
36
30
|
if has_image
|
|
37
|
-
div do
|
|
38
|
-
img(
|
|
31
|
+
content_tag(:div) do
|
|
32
|
+
tag.img(
|
|
39
33
|
class: "inline-block w-10 h-10 rounded-full object-cover",
|
|
40
34
|
src: resolved_user.avatar_url(size: :small),
|
|
41
35
|
alt: "",
|
|
@@ -43,8 +37,8 @@ module Panda
|
|
|
43
37
|
)
|
|
44
38
|
end
|
|
45
39
|
else
|
|
46
|
-
div
|
|
47
|
-
span
|
|
40
|
+
content_tag(:div, class: "inline-block w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center") do
|
|
41
|
+
content_tag(:span, user_initials, class: "text-sm font-medium text-gray-600")
|
|
48
42
|
end
|
|
49
43
|
end
|
|
50
44
|
end
|
|
@@ -63,13 +57,16 @@ module Panda
|
|
|
63
57
|
end
|
|
64
58
|
|
|
65
59
|
def render_user_info
|
|
66
|
-
div
|
|
67
|
-
p
|
|
68
|
-
if @metadata.present?
|
|
69
|
-
p
|
|
60
|
+
content_tag(:div, class: "ml-3") do
|
|
61
|
+
name_html = content_tag(:p, resolved_user.name, class: "text-sm text-black")
|
|
62
|
+
meta_html = if @metadata.present?
|
|
63
|
+
content_tag(:p, @metadata, class: "text-sm text-black/60")
|
|
70
64
|
elsif resolved_user.respond_to?(:email) && resolved_user.email.present?
|
|
71
|
-
p
|
|
65
|
+
content_tag(:p, resolved_user.email, class: "text-sm text-gray-500")
|
|
66
|
+
else
|
|
67
|
+
"".html_safe
|
|
72
68
|
end
|
|
69
|
+
(name_html + meta_html).html_safe
|
|
73
70
|
end
|
|
74
71
|
end
|
|
75
72
|
end
|