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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class FilesController < ::Panda::Core::Admin::BaseController
|
|
7
|
+
before_action :set_initial_breadcrumb, only: [:index]
|
|
8
|
+
before_action :set_blob, only: [:show, :update, :destroy]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
# Exclude variant blobs — they are attached to VariantRecord, not user-uploaded
|
|
12
|
+
variant_blob_ids = ActiveStorage::Attachment
|
|
13
|
+
.where(record_type: "ActiveStorage::VariantRecord")
|
|
14
|
+
.select(:blob_id)
|
|
15
|
+
|
|
16
|
+
@files = ActiveStorage::Blob
|
|
17
|
+
.where.not(id: variant_blob_ids)
|
|
18
|
+
.includes(:variant_records)
|
|
19
|
+
.order(created_at: :desc)
|
|
20
|
+
|
|
21
|
+
@file_categories = Panda::Core::FileCategory.ordered
|
|
22
|
+
@active_category = params[:category]
|
|
23
|
+
|
|
24
|
+
# Apply category filter
|
|
25
|
+
if @active_category == "uncategorized"
|
|
26
|
+
categorized_blob_ids = Panda::Core::FileCategorization.select(:blob_id)
|
|
27
|
+
@files = @files.where.not(id: categorized_blob_ids)
|
|
28
|
+
elsif @active_category.present?
|
|
29
|
+
category = Panda::Core::FileCategory.find_by(slug: @active_category)
|
|
30
|
+
if category
|
|
31
|
+
blob_ids = Panda::Core::FileCategorization.where(file_category: category).select(:blob_id)
|
|
32
|
+
@files = @files.where(id: blob_ids)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Preload category names for gallery display
|
|
37
|
+
categorizations = Panda::Core::FileCategorization
|
|
38
|
+
.where(blob_id: @files.select(:id))
|
|
39
|
+
.includes(:file_category)
|
|
40
|
+
@blob_categories = categorizations.each_with_object({}) do |cat, hash|
|
|
41
|
+
hash[cat.blob_id] = cat.file_category
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def show
|
|
46
|
+
@file_categories = Panda::Core::FileCategory.ordered
|
|
47
|
+
|
|
48
|
+
if request.xhr? || request.headers["Turbo-Frame"].present?
|
|
49
|
+
render partial: "file_details", locals: {file: @blob, file_categories: @file_categories}
|
|
50
|
+
else
|
|
51
|
+
redirect_to admin_files_path
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def create
|
|
56
|
+
# JSON uploads from EditorJS
|
|
57
|
+
if params[:image].present?
|
|
58
|
+
return create_from_editor
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# File uploads from EditorJS AttachesTool
|
|
62
|
+
if params[:file].present?
|
|
63
|
+
return create_file_from_editor
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# HTML form uploads from the file gallery
|
|
67
|
+
file = params.dig(:file_upload, :file)
|
|
68
|
+
category_id = params.dig(:file_upload, :file_category_id)
|
|
69
|
+
|
|
70
|
+
if file.blank?
|
|
71
|
+
redirect_to admin_files_path, alert: "Please select a file to upload."
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if category_id.blank?
|
|
76
|
+
redirect_to admin_files_path, alert: "Please select a category."
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
unless (category = Panda::Core::FileCategory.find_by(id: category_id))
|
|
81
|
+
redirect_to admin_files_path, alert: "Selected category not found."
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
blob = find_existing_blob(file) || ActiveStorage::Blob.create_and_upload!(
|
|
86
|
+
io: file,
|
|
87
|
+
filename: file.original_filename,
|
|
88
|
+
content_type: file.content_type
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
stamp_uploader(blob)
|
|
92
|
+
|
|
93
|
+
Panda::Core::FileCategorization.find_or_create_by!(file_category: category, blob: blob)
|
|
94
|
+
|
|
95
|
+
redirect_to admin_files_path, notice: "File uploaded successfully."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def update
|
|
99
|
+
blob_params = params.require(:blob).permit(:filename, :description, :file_category_id)
|
|
100
|
+
|
|
101
|
+
# Update filename — always re-append the original extension
|
|
102
|
+
new_filename = blob_params[:filename].to_s.strip
|
|
103
|
+
if new_filename.present?
|
|
104
|
+
original_ext = File.extname(@blob.filename.to_s)
|
|
105
|
+
new_filename += original_ext if File.extname(new_filename).blank? && original_ext.present?
|
|
106
|
+
@blob.filename = new_filename
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Update description in metadata
|
|
110
|
+
metadata = @blob.metadata || {}
|
|
111
|
+
metadata["description"] = blob_params[:description].to_s.strip
|
|
112
|
+
@blob.metadata = metadata
|
|
113
|
+
|
|
114
|
+
@blob.save!
|
|
115
|
+
update_file_category(blob_params[:file_category_id])
|
|
116
|
+
|
|
117
|
+
@file_categories = Panda::Core::FileCategory.ordered
|
|
118
|
+
locals = {file: @blob, file_categories: @file_categories, notice: "File updated successfully."}
|
|
119
|
+
respond_to do |format|
|
|
120
|
+
format.turbo_stream do
|
|
121
|
+
render turbo_stream: turbo_stream.update(
|
|
122
|
+
"file-gallery-slideover-content",
|
|
123
|
+
partial: "file_details",
|
|
124
|
+
locals: locals
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
format.html do
|
|
128
|
+
redirect_to admin_files_path, notice: "File updated successfully.", status: :see_other
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def destroy
|
|
134
|
+
# Remove variant records and their blobs first (each variant has its own blob)
|
|
135
|
+
@blob.variant_records.includes(image_attachment: :blob).find_each do |vr|
|
|
136
|
+
vr.image.purge_later if vr.image.attached?
|
|
137
|
+
vr.destroy
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Remove remaining FK references before purging the blob
|
|
141
|
+
ActiveStorage::Attachment.where(blob_id: @blob.id).delete_all
|
|
142
|
+
Panda::Core::FileCategorization.where(blob_id: @blob.id).delete_all
|
|
143
|
+
@blob.purge_later
|
|
144
|
+
|
|
145
|
+
redirect_to admin_files_path, notice: "File was successfully deleted.", status: :see_other
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def set_blob
|
|
151
|
+
@blob = ActiveStorage::Blob.find(params[:id])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def set_initial_breadcrumb
|
|
155
|
+
add_breadcrumb "Files", admin_files_path
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_from_editor
|
|
159
|
+
file = params[:image]
|
|
160
|
+
return render json: {success: 0} unless file
|
|
161
|
+
|
|
162
|
+
blob = find_existing_blob(file) || ActiveStorage::Blob.create_and_upload!(
|
|
163
|
+
io: file,
|
|
164
|
+
filename: file.original_filename,
|
|
165
|
+
content_type: file.content_type
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
stamp_uploader(blob)
|
|
169
|
+
|
|
170
|
+
render json: {
|
|
171
|
+
success: true,
|
|
172
|
+
file: {
|
|
173
|
+
url: Rails.application.routes.url_helpers.rails_blob_url(blob, only_path: true),
|
|
174
|
+
name: blob.filename.to_s,
|
|
175
|
+
size: blob.byte_size,
|
|
176
|
+
signed_id: blob.signed_id
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def create_file_from_editor
|
|
182
|
+
file = params[:file]
|
|
183
|
+
return render json: {success: 0} unless file
|
|
184
|
+
|
|
185
|
+
blob = find_existing_blob(file) || ActiveStorage::Blob.create_and_upload!(
|
|
186
|
+
io: file,
|
|
187
|
+
filename: file.original_filename,
|
|
188
|
+
content_type: file.content_type
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
stamp_uploader(blob)
|
|
192
|
+
|
|
193
|
+
extension = File.extname(blob.filename.to_s).delete_prefix(".")
|
|
194
|
+
|
|
195
|
+
render json: {
|
|
196
|
+
success: 1,
|
|
197
|
+
file: {
|
|
198
|
+
url: Rails.application.routes.url_helpers.rails_blob_url(blob, only_path: true),
|
|
199
|
+
name: blob.filename.to_s,
|
|
200
|
+
size: blob.byte_size,
|
|
201
|
+
extension: extension,
|
|
202
|
+
signed_id: blob.signed_id
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def find_existing_blob(file)
|
|
208
|
+
checksum = Digest::MD5.file(file.tempfile.path).base64digest
|
|
209
|
+
ActiveStorage::Blob.find_by(checksum: checksum, byte_size: file.size)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def stamp_uploader(blob)
|
|
213
|
+
return unless current_user
|
|
214
|
+
metadata = blob.metadata || {}
|
|
215
|
+
metadata["uploaded_by_id"] = current_user.id
|
|
216
|
+
metadata["uploaded_by_name"] = current_user.name
|
|
217
|
+
blob.update_column(:metadata, metadata)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def update_file_category(category_id)
|
|
221
|
+
Panda::Core::FileCategorization.where(blob_id: @blob.id).destroy_all
|
|
222
|
+
return if category_id.blank?
|
|
223
|
+
|
|
224
|
+
category = Panda::Core::FileCategory.find_by(id: category_id)
|
|
225
|
+
Panda::Core::FileCategorization.create!(file_category: category, blob: @blob) if category
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class ImportSessionsController < BaseController
|
|
7
|
+
before_action :set_initial_breadcrumb
|
|
8
|
+
before_action :set_import_session, only: %i[show column_map update_mapping preview import]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@import_sessions = scoped_import_sessions.recent
|
|
12
|
+
@import_sessions = @import_sessions.where(importable_type: params[:type]) if params[:type].present?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@importable_type = params[:importable_type]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
tenant = resolve_tenant
|
|
21
|
+
uploaded_file = params[:import_file]
|
|
22
|
+
|
|
23
|
+
unless uploaded_file.present?
|
|
24
|
+
@importable_type = params[:importable_type]
|
|
25
|
+
flash.now[:error] = "Please select a file to upload."
|
|
26
|
+
render :new, status: :unprocessable_entity
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if FileParser.xls?(uploaded_file.original_filename)
|
|
31
|
+
@importable_type = params[:importable_type]
|
|
32
|
+
flash.now[:error] = "XLS format is not supported. Please save your spreadsheet as XLSX, CSV, or TSV and try again."
|
|
33
|
+
render :new, status: :unprocessable_entity
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
unless FileParser.supported?(uploaded_file.original_filename)
|
|
38
|
+
@importable_type = params[:importable_type]
|
|
39
|
+
flash.now[:error] = "Unsupported file format. Please upload a CSV, TSV, or XLSX file."
|
|
40
|
+
render :new, status: :unprocessable_entity
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@import_session = ImportSession.new(
|
|
45
|
+
importable_type: params[:importable_type],
|
|
46
|
+
user: current_user,
|
|
47
|
+
tenant: tenant,
|
|
48
|
+
status: "mapping"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if uploaded_file.present?
|
|
52
|
+
@import_session.import_file.attach(uploaded_file)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if @import_session.save
|
|
56
|
+
redirect_to column_map_admin_import_session_path(@import_session)
|
|
57
|
+
else
|
|
58
|
+
@importable_type = params[:importable_type]
|
|
59
|
+
render :new, status: :unprocessable_entity
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def show
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def column_map
|
|
67
|
+
@headers = @import_session.file_headers
|
|
68
|
+
@field_options = @import_session.column_options
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_mapping
|
|
72
|
+
mapping = {}
|
|
73
|
+
(params[:mapping] || {}).each do |csv_col, field_name|
|
|
74
|
+
mapping[csv_col] = field_name if field_name.present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@import_session.update!(column_mapping: mapping, status: "previewing")
|
|
78
|
+
redirect_to preview_admin_import_session_path(@import_session)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def preview
|
|
82
|
+
@preview_rows = @import_session.preview_rows(limit: 5)
|
|
83
|
+
@mapping = @import_session.column_mapping
|
|
84
|
+
@field_definitions = @import_session.column_options
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def import
|
|
88
|
+
@import_session.update!(status: "importing")
|
|
89
|
+
|
|
90
|
+
# Run synchronously for now — swap to CSVImportJob.perform_later for async
|
|
91
|
+
CSVImportService.new(@import_session).call
|
|
92
|
+
|
|
93
|
+
redirect_to admin_import_session_path(@import_session), success: "Import complete."
|
|
94
|
+
rescue => e
|
|
95
|
+
redirect_to admin_import_session_path(@import_session), error: "Import failed: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def set_import_session
|
|
101
|
+
@import_session = scoped_import_sessions.find(params[:id])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def set_initial_breadcrumb
|
|
105
|
+
add_breadcrumb "Import", admin_import_sessions_path
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def scoped_import_sessions
|
|
109
|
+
tenant = resolve_tenant
|
|
110
|
+
tenant ? ImportSession.where(tenant: tenant) : ImportSession.where(tenant_type: nil)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resolve_tenant
|
|
114
|
+
return ActsAsTenant.current_tenant if defined?(ActsAsTenant) && ActsAsTenant.current_tenant
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
module MyProfile
|
|
7
|
+
class LoginsController < BaseController
|
|
8
|
+
before_action :set_initial_breadcrumb
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
render :show, locals: {user: current_user}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def set_initial_breadcrumb
|
|
17
|
+
add_breadcrumb "My Profile", edit_admin_my_profile_path
|
|
18
|
+
add_breadcrumb "Login & Security", admin_my_profile_logins_path
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -24,7 +24,11 @@ module Panda
|
|
|
24
24
|
# @type PATCH/PUT
|
|
25
25
|
# @return void
|
|
26
26
|
def update
|
|
27
|
+
purge_old_avatar_if_replacing
|
|
28
|
+
|
|
27
29
|
if current_user.update(user_params)
|
|
30
|
+
rename_avatar_blob
|
|
31
|
+
clear_oauth_avatar_url_after_manual_upload
|
|
28
32
|
flash[:success] = "Your profile has been updated successfully."
|
|
29
33
|
redirect_to edit_admin_my_profile_path
|
|
30
34
|
else
|
|
@@ -38,6 +42,33 @@ module Panda
|
|
|
38
42
|
add_breadcrumb "My Profile", edit_admin_my_profile_path
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
# Purge the existing avatar synchronously when a replacement is being uploaded.
|
|
46
|
+
# This ensures the old blob is fully removed (DB + storage) before the new
|
|
47
|
+
# one is attached, preventing orphaned blobs that show as "in use".
|
|
48
|
+
def purge_old_avatar_if_replacing
|
|
49
|
+
return unless params.dig(:user, :avatar).present? && current_user.avatar.attached?
|
|
50
|
+
current_user.avatar.purge
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Rename the avatar blob to a consistent "avatar-full-name.ext" format
|
|
54
|
+
# so it's identifiable in the file gallery.
|
|
55
|
+
def rename_avatar_blob
|
|
56
|
+
upload = params.dig(:user, :avatar)
|
|
57
|
+
return unless upload.present? && current_user.avatar.attached?
|
|
58
|
+
|
|
59
|
+
extension = File.extname(upload.original_filename)
|
|
60
|
+
new_filename = "avatar-#{current_user.name.parameterize}#{extension}"
|
|
61
|
+
current_user.avatar.blob.update!(filename: new_filename)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Clear oauth_avatar_url when user manually uploads an avatar.
|
|
65
|
+
# This signals to find_or_create_from_auth_hash that the avatar
|
|
66
|
+
# was manually uploaded and should not be overwritten on OAuth re-login.
|
|
67
|
+
def clear_oauth_avatar_url_after_manual_upload
|
|
68
|
+
return unless params.dig(:user, :avatar).present?
|
|
69
|
+
current_user.update_column(:oauth_avatar_url, nil)
|
|
70
|
+
end
|
|
71
|
+
|
|
41
72
|
# Only allow a list of trusted parameters through
|
|
42
73
|
# @type private
|
|
43
74
|
# @return ActionController::StrongParameters
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class SearchController < ::Panda::Core::Admin::BaseController
|
|
7
|
+
def index
|
|
8
|
+
query = params[:q].to_s.strip
|
|
9
|
+
results = Panda::Core::SearchRegistry.admin_search(query, limit: 5)
|
|
10
|
+
render json: results
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -10,9 +10,14 @@ module Panda
|
|
|
10
10
|
skip_before_action :authenticate_admin_user!, only: [:new, :create, :destroy, :failure]
|
|
11
11
|
|
|
12
12
|
def new
|
|
13
|
-
|
|
14
|
-
@providers =
|
|
15
|
-
|
|
13
|
+
providers = Core.config.resolved_authentication_providers(request)
|
|
14
|
+
@providers = providers.select do |key, config|
|
|
15
|
+
# Developer provider doesn't need credentials, only shown in development
|
|
16
|
+
if key.to_sym == :developer
|
|
17
|
+
Rails.env.development?
|
|
18
|
+
else
|
|
19
|
+
config[:client_id].present? && config[:client_secret].present?
|
|
20
|
+
end
|
|
16
21
|
end.keys
|
|
17
22
|
end
|
|
18
23
|
|
|
@@ -23,7 +28,8 @@ module Panda
|
|
|
23
28
|
# Find the actual provider key (might be using path_name override)
|
|
24
29
|
provider = find_provider_by_path(provider_path)
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
resolved = Core.config.resolved_authentication_providers(request)
|
|
32
|
+
unless provider && resolved.key?(provider)
|
|
27
33
|
redirect_to admin_login_path, flash: {error: "Authentication provider not enabled"}
|
|
28
34
|
return
|
|
29
35
|
end
|
|
@@ -31,21 +37,69 @@ module Panda
|
|
|
31
37
|
user = User.find_or_create_from_auth_hash(auth)
|
|
32
38
|
|
|
33
39
|
if user.persisted?
|
|
34
|
-
#
|
|
35
|
-
|
|
40
|
+
# Per-login validation hook (e.g. email domain restrictions)
|
|
41
|
+
if Core.config.authentication_validator
|
|
42
|
+
validation_error = Core.config.authentication_validator.call(user, auth, request)
|
|
43
|
+
if validation_error
|
|
44
|
+
redirect_to admin_login_path, flash: {error: validation_error}
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if user is authorized to access the admin area.
|
|
50
|
+
# Admin users always have access. Non-admin users are checked
|
|
51
|
+
# via the configurable authorization_policy (e.g. role-based access
|
|
52
|
+
# from panda-cms-pro).
|
|
53
|
+
policy = Panda::Core.config.authorization_policy
|
|
54
|
+
unless user.admin? || policy.call(user, :access_admin, nil)
|
|
36
55
|
flash[:error] = "You do not have permission to access the admin area"
|
|
37
56
|
flash.keep(:error) if Rails.env.test?
|
|
38
57
|
redirect_to admin_login_path
|
|
39
58
|
return
|
|
40
59
|
end
|
|
41
60
|
|
|
42
|
-
session[
|
|
61
|
+
session[Panda::Core::ADMIN_SESSION_KEY] = user.id
|
|
43
62
|
Panda::Core::Current.user = user
|
|
44
63
|
|
|
64
|
+
user.track_login!(request) if user.respond_to?(:track_login!)
|
|
65
|
+
Panda::Core::UserActivity.log!(user: user, action: "login", request: request) if defined?(Panda::Core::UserActivity)
|
|
66
|
+
|
|
45
67
|
ActiveSupport::Notifications.instrument("panda.core.user_login",
|
|
46
68
|
user: user,
|
|
47
69
|
provider: provider)
|
|
48
70
|
|
|
71
|
+
# Consume stored redirect path once so it is cleared regardless of
|
|
72
|
+
# which redirect branch is taken below.
|
|
73
|
+
post_auth_path = session.delete(:post_auth_redirect_path)
|
|
74
|
+
|
|
75
|
+
# Post-authentication redirect hook (e.g. workspace picker).
|
|
76
|
+
# Wrapped in its own rescue so a broken hook falls through to the
|
|
77
|
+
# default redirect rather than hitting the outer rescue (which
|
|
78
|
+
# would redirect to login with the session already created).
|
|
79
|
+
if (redirect_proc = Panda::Core.config.post_authentication_redirect)
|
|
80
|
+
begin
|
|
81
|
+
redirect_url = redirect_proc.call(user, request)
|
|
82
|
+
if redirect_url.present?
|
|
83
|
+
redirect_to append_admin_path_suffix(redirect_url, post_auth_path), allow_other_host: true, flash: {success: "Successfully logged in as #{user.name}"}
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
rescue => e
|
|
87
|
+
Rails.logger.error "post_authentication_redirect hook failed: #{e.class}: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Redirect to originating subdomain if auth callback landed on root domain
|
|
92
|
+
origin = session.delete(:origin_subdomain)
|
|
93
|
+
if origin.present? && request.host.split(".").first != origin
|
|
94
|
+
scheme = request.scheme
|
|
95
|
+
base_domain = request.host.sub(/\A[^.]+\./, "")
|
|
96
|
+
port = request.port
|
|
97
|
+
port_suffix = [80, 443].include?(port) ? "" : ":#{port}"
|
|
98
|
+
base_url = "#{scheme}://#{origin}.#{base_domain}#{port_suffix}#{Core.config.admin_path}"
|
|
99
|
+
redirect_to append_admin_path_suffix(base_url, post_auth_path), allow_other_host: true, flash: {success: "Successfully logged in as #{user.name}"}
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
49
103
|
# Use configured dashboard path or default to admin_root_path
|
|
50
104
|
redirect_path = Panda::Core.config.dashboard_redirect_path || admin_root_path
|
|
51
105
|
redirect_path = redirect_path.call if redirect_path.respond_to?(:call)
|
|
@@ -69,7 +123,7 @@ module Panda
|
|
|
69
123
|
end
|
|
70
124
|
|
|
71
125
|
def destroy
|
|
72
|
-
session.delete(
|
|
126
|
+
session.delete(Panda::Core::ADMIN_SESSION_KEY)
|
|
73
127
|
Panda::Core::Current.user = nil
|
|
74
128
|
|
|
75
129
|
ActiveSupport::Notifications.instrument("panda.core.user_logout")
|
|
@@ -79,13 +133,33 @@ module Panda
|
|
|
79
133
|
|
|
80
134
|
private
|
|
81
135
|
|
|
136
|
+
# Append the path segment that follows the admin prefix from +stored_path+
|
|
137
|
+
# to +base_url+. Returns +base_url+ unchanged when there is no meaningful
|
|
138
|
+
# suffix (e.g. no stored path, or the stored path is the admin root itself).
|
|
139
|
+
#
|
|
140
|
+
# Example: base_url="https://workspace.example.com/admin", stored_path="/admin/users/123"
|
|
141
|
+
# → "https://workspace.example.com/admin/users/123"
|
|
142
|
+
def append_admin_path_suffix(base_url, stored_path)
|
|
143
|
+
return base_url if stored_path.blank?
|
|
144
|
+
|
|
145
|
+
admin_prefix = Core.config.admin_path
|
|
146
|
+
return base_url unless stored_path.start_with?(admin_prefix)
|
|
147
|
+
|
|
148
|
+
suffix = stored_path[admin_prefix.length..]
|
|
149
|
+
return base_url if suffix.blank? || suffix == "/"
|
|
150
|
+
|
|
151
|
+
base_url.chomp("/") + suffix
|
|
152
|
+
end
|
|
153
|
+
|
|
82
154
|
# Find the provider key by path name (handles path_name override)
|
|
83
155
|
def find_provider_by_path(provider_path)
|
|
156
|
+
providers = Core.config.resolved_authentication_providers(request)
|
|
157
|
+
|
|
84
158
|
# First check if it's a direct match
|
|
85
|
-
return provider_path if
|
|
159
|
+
return provider_path if providers.key?(provider_path)
|
|
86
160
|
|
|
87
161
|
# Then check if any provider has a matching path_name
|
|
88
|
-
|
|
162
|
+
providers.each do |key, config|
|
|
89
163
|
return key if config[:path_name]&.to_sym == provider_path
|
|
90
164
|
end
|
|
91
165
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class TagsController < BaseController
|
|
7
|
+
before_action :set_initial_breadcrumb
|
|
8
|
+
before_action :set_tag, only: %i[edit update destroy]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
tenant = resolve_tenant
|
|
12
|
+
@tags = Tag.for_tenant(tenant).ordered
|
|
13
|
+
|
|
14
|
+
if params[:q].present?
|
|
15
|
+
@tags = @tags.search_by_name(params[:q])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
respond_to do |format|
|
|
19
|
+
format.html
|
|
20
|
+
format.json { render json: @tags.limit(10).map { |t| {id: t.id, name: t.name, colour: t.display_colour} } }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def new
|
|
25
|
+
@tag = Tag.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create
|
|
29
|
+
tenant = resolve_tenant
|
|
30
|
+
@tag = Tag.new(tag_params.merge(tenant: tenant))
|
|
31
|
+
|
|
32
|
+
if @tag.save
|
|
33
|
+
redirect_to admin_tags_path, success: "Tag created."
|
|
34
|
+
else
|
|
35
|
+
render :new, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def edit
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update
|
|
43
|
+
if @tag.update(tag_params)
|
|
44
|
+
redirect_to admin_tags_path, success: "Tag updated."
|
|
45
|
+
else
|
|
46
|
+
render :edit, status: :unprocessable_entity
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def destroy
|
|
51
|
+
@tag.destroy
|
|
52
|
+
redirect_to admin_tags_path, success: "Tag deleted."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def set_tag
|
|
58
|
+
@tag = Tag.for_tenant(resolve_tenant).find(params[:id])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def tag_params
|
|
62
|
+
params.require(:tag).permit(:name, :colour)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_initial_breadcrumb
|
|
66
|
+
add_breadcrumb "Settings", admin_settings_path
|
|
67
|
+
add_breadcrumb "Tags", admin_tags_path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_tenant
|
|
71
|
+
return ActsAsTenant.current_tenant if defined?(ActsAsTenant) && ActsAsTenant.current_tenant
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -18,19 +18,18 @@ module Panda
|
|
|
18
18
|
def create
|
|
19
19
|
user = Panda::Core::User.find(params[:user_id])
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
# Mirror real SessionsController: admin users always pass,
|
|
22
|
+
# non-admin users checked via authorization_policy
|
|
23
|
+
policy = Panda::Core.config.authorization_policy
|
|
24
|
+
unless user.admin? || policy.call(user, :access_admin, nil)
|
|
24
25
|
flash[:alert] = "You do not have permission to access the admin area."
|
|
25
|
-
# Keep flash for one more request to survive redirect in tests
|
|
26
26
|
flash.keep(:alert) if Rails.env.test?
|
|
27
|
-
# Use string path since route helpers aren't available in ActionController::Base
|
|
28
27
|
redirect_to "#{Panda::Core.config.admin_path || "/admin"}/login", allow_other_host: false, status: :found
|
|
29
28
|
return
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
# Set session (mimics real OAuth callback)
|
|
33
|
-
session[
|
|
32
|
+
session[Panda::Core::ADMIN_SESSION_KEY] = user.id
|
|
34
33
|
Panda::Core::Current.user = user
|
|
35
34
|
|
|
36
35
|
# Support custom redirect path for test flexibility
|