Conversation
- Rewrite custom-admin-pages.md with explicit anti-pattern section covering layout loss and cross-live_session navigation failure modes - Correct claim that dynamic segments require a route module; tab_to_route/1 splices paths verbatim, so live_view: on a tab supports :id/:slug via visible: false hidden tabs (as real modules like posts/catalogue do) - Correct claim that admin_routes/0 accepts controller/forward/scope; the quoted block is spliced inside live_session :phoenix_kit_admin which only permits live declarations — redirect non-LiveView routes to generate/1 or public_routes/1 with phoenix_kit_sync and phoenix_kit_publishing as refs - Fix Mix task name (phoenix_kit.gen.admin.page) and argument shape in Igniter generator section to match actual parse_args/2 in the task - Fix LayoutWrapper attr name from url_path to current_path in AGENTS.md and remove non-existent current_locale_base example - Convert tab-path examples to the relative-form convention used by every real plugin across ADMIN_README.md, dashboard/README.md, tab.ex moduledoc, dashboard.ex, registry.ex, config.ex, tabs_initializer.ex, and the two dev_docs guides - Convert the built-in jobs tab from absolute to relative path form for consistency with the rest of the ecosystem - Add historical-exploration banner to 2025-12-30 routing-architecture guide so readers know Strategy 1 was not adopted - Add cross-session caveat note to PHOENIXKIT_AWS_COMPATIBILITY.md explaining the full-page-reload limitation of the override pattern - Fix Igniter generator After Generation section to stop teaching a separate-live_session pattern that recreates the known bug Motivation: these docs were drifting ahead of the implementation and would have led module authors into the exact "parent-router hand-registration" bug class (layout loss + cross-live_session navigation crash) that we just finished warning against in every other file. Every claim here is now traceable to a file:line ref in integration.ex, auth.ex, tab.ex, or layout_wrapper.ex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The enabled?/0 callback was checking "maintenance_enabled" (whether maintenance mode is active for end users) instead of "maintenance_module_enabled" (whether the module is toggled on). This caused the admin settings page to be inaccessible even after enabling the module, showing "Maintenance module is not enabled". Fixes #485 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align admin routing docs and tab-path conventions with implementation
Add reorder mode toggle to the languages settings page that enables drag-and-drop reordering of enabled languages using the DraggableList component. Hide the SortableJS fallback clone at the initial position to prevent a ghost chip visual artifact during drag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the CSS fix for hiding the SortableJS fallback clone from the
languages template into the SortableGrid hook so any draggable_list
with hide_source={true} gets the behavior automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added feature to make languages sortable in the language admin page
- Move wiggle keyframes from inline <style> in heex into phoenix_kit.js and phoenix_kit_sortable.js injectStyles(); rename class to pk-sortable-wiggle and respect prefers-reduced-motion. - Dedup ordered_codes in Languages.reorder_languages/1 (Enum.uniq) and use MapSet for the remaining filter. - Add reorder_languages/1 integration tests covering reorder, partial list, unknown codes, empty list, and duplicate codes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor maintenance mode with layout override and scheduled windows Replaces the old @show_maintenance assign approach with a dynamic layout swap via socket.private[:live_layout] — the underlying LiveView keeps running so form state and scroll position are preserved when maintenance toggles on or off. URL never changes. Core changes: - Layout override in on_mount hook instead of redirect. When maintenance turns on, put_in socket.private[:live_layout] swaps the layout live; when it ends, PubSub triggers restoration of the original layout - New PhoenixKitWeb.Layouts :maintenance template with countdown timer - HTTP plug renders inline 503 HTML (with Retry-After header) for controller routes, with proper Phoenix.HTML escaping to prevent XSS - Scheduled maintenance windows with start/end UTC datetimes, 1-year upper bound, and 60-second tolerance for datetime-local minute precision - cleanup_expired_schedule auto-disables stale state on every page access - Process.send_after timer unblocks users when scheduled end arrives (clamped to Erlang's 32-bit timeout limit) - PubSub broadcasts on every state change so all connected LiveViews react instantly; admin tabs stay put, user tabs swap layouts - Manual toggle clears expired schedule on enable to avoid stale locks - Activity logging for all admin actions (toggle, content, schedule) - All user-facing strings wrapped in gettext Schedule validation rejects: empty, past start/end, end before start, dates >1 year in the future. Datetime inputs use the system time_zone setting for display and convert to UTC for storage. Extracts timezone helpers (offset_to_seconds, shift_to_offset, parse_datetime_local, format_datetime_local) to PhoenixKit.Utils.Date so they can be tested in isolation. Adds 93 new tests: unit tests for validate_schedule and PubSub, integration tests for Maintenance context and the plug (including XSS regression test), and doctested timezone helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix maintenance settings: live preview + remove broken Preview button - Move phx-change from individual inputs to the form element so the live preview updates on every keystroke (input-level phx-change on text inputs only fires on blur, making the preview appear broken) - Remove the "Preview" link that navigated to /maintenance. The path went through locale-prefixed routes and got caught by the publishing module's /:language/:group catch-all. The settings page already has an inline Live Preview card rendering the same content, so the link was redundant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address PR review feedback on maintenance refactor HIGH (merge blockers): - Register MaintenanceCountdown hook in priv/static/assets/phoenix_kit.js alongside the other phoenix_kit hooks. Parent apps already include this file, so the hook is now reliably available (was previously injected by a plug script that wasn't guaranteed to run before LiveSocket init). Remove the fragile inline injection from the Integration plug. - Plug's 503 HTML now uses inline CSS instead of linking to /assets/css/app.css (which wasn't actually served — the real digested path is /assets/app.css). The page is now self-contained with light + dark mode support via prefers-color-scheme, so it works on any route regardless of the parent app's asset pipeline. - Replace String.contains?/2 with String.starts_with?/2 in the plug's auth_route?/1 and static_asset?/1. A parent-app path like /blog/users/log-in-to-us would have bypassed maintenance mode. Add a regression test covering parent-app look-alike paths. MEDIUM: - disable_system/0 now clears maintenance_scheduled_end in addition to maintenance_scheduled_start so a stale end time doesn't surprise-disable the next re-enable. Update the test to assert both fields are cleared. - Track the Process.send_after timer ref in socket assigns and cancel it on reschedule (via new reschedule_maintenance_end_timer/1) so schedule changes don't leave a stale "auto-off" signal in flight. - Settings LiveView's PubSub handler now re-reads header and subtext so multi-admin editing stays in sync. The save handler broadcasts status change to trigger this sync. - schedule_error_message/1 catch-all now logs a warning with the unknown atom so future validation additions surface instead of being silently swallowed. - Document check_maintenance_mode/1's required call sites (all 6 on_mount hooks listed in the @doc) so new live_sessions don't forget it. - Add a HACK comment near put_in socket.private[:live_layout] noting it relies on Phoenix LiveView internals (same pattern as maybe_apply_plugin_layout) and should be revisited on major LV upgrades. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fold check_maintenance_mode into shared scope-mount helper Addresses the final MEDIUM improvement from PR review: instead of each of the 6 on_mount hooks explicitly calling check_maintenance_mode/1, fold it into mount_phoenix_kit_current_scope/3 which all 6 already use. New live_sessions that use a scope-mounting on_mount hook now inherit maintenance mode enforcement automatically — no way to forget it. Removes 6 redundant call sites and updates the @doc comment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor maintenance mode with layout override and scheduled windows Replaces the old @show_maintenance assign approach with a dynamic layout swap via socket.private[:live_layout] — the underlying LiveView keeps running so form state and scroll position are preserved when maintenance toggles on or off. URL never changes. Core changes: - Layout override in on_mount hook instead of redirect. When maintenance turns on, put_in socket.private[:live_layout] swaps the layout live; when it ends, PubSub triggers restoration of the original layout - New PhoenixKitWeb.Layouts :maintenance template with countdown timer - HTTP plug renders inline 503 HTML (with Retry-After header) for controller routes, with proper Phoenix.HTML escaping to prevent XSS - Scheduled maintenance windows with start/end UTC datetimes, 1-year upper bound, and 60-second tolerance for datetime-local minute precision - cleanup_expired_schedule auto-disables stale state on every page access - Process.send_after timer unblocks users when scheduled end arrives (clamped to Erlang's 32-bit timeout limit) - PubSub broadcasts on every state change so all connected LiveViews react instantly; admin tabs stay put, user tabs swap layouts - Manual toggle clears expired schedule on enable to avoid stale locks - Activity logging for all admin actions (toggle, content, schedule) - All user-facing strings wrapped in gettext Schedule validation rejects: empty, past start/end, end before start, dates >1 year in the future. Datetime inputs use the system time_zone setting for display and convert to UTC for storage. Extracts timezone helpers (offset_to_seconds, shift_to_offset, parse_datetime_local, format_datetime_local) to PhoenixKit.Utils.Date so they can be tested in isolation. Adds 93 new tests: unit tests for validate_schedule and PubSub, integration tests for Maintenance context and the plug (including XSS regression test), and doctested timezone helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix maintenance settings: live preview + remove broken Preview button - Move phx-change from individual inputs to the form element so the live preview updates on every keystroke (input-level phx-change on text inputs only fires on blur, making the preview appear broken) - Remove the "Preview" link that navigated to /maintenance. The path went through locale-prefixed routes and got caught by the publishing module's /:language/:group catch-all. The settings page already has an inline Live Preview card rendering the same content, so the link was redundant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address PR review feedback on maintenance refactor HIGH (merge blockers): - Register MaintenanceCountdown hook in priv/static/assets/phoenix_kit.js alongside the other phoenix_kit hooks. Parent apps already include this file, so the hook is now reliably available (was previously injected by a plug script that wasn't guaranteed to run before LiveSocket init). Remove the fragile inline injection from the Integration plug. - Plug's 503 HTML now uses inline CSS instead of linking to /assets/css/app.css (which wasn't actually served — the real digested path is /assets/app.css). The page is now self-contained with light + dark mode support via prefers-color-scheme, so it works on any route regardless of the parent app's asset pipeline. - Replace String.contains?/2 with String.starts_with?/2 in the plug's auth_route?/1 and static_asset?/1. A parent-app path like /blog/users/log-in-to-us would have bypassed maintenance mode. Add a regression test covering parent-app look-alike paths. MEDIUM: - disable_system/0 now clears maintenance_scheduled_end in addition to maintenance_scheduled_start so a stale end time doesn't surprise-disable the next re-enable. Update the test to assert both fields are cleared. - Track the Process.send_after timer ref in socket assigns and cancel it on reschedule (via new reschedule_maintenance_end_timer/1) so schedule changes don't leave a stale "auto-off" signal in flight. - Settings LiveView's PubSub handler now re-reads header and subtext so multi-admin editing stays in sync. The save handler broadcasts status change to trigger this sync. - schedule_error_message/1 catch-all now logs a warning with the unknown atom so future validation additions surface instead of being silently swallowed. - Document check_maintenance_mode/1's required call sites (all 6 on_mount hooks listed in the @doc) so new live_sessions don't forget it. - Add a HACK comment near put_in socket.private[:live_layout] noting it relies on Phoenix LiveView internals (same pattern as maybe_apply_plugin_layout) and should be revisited on major LV upgrades. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fold check_maintenance_mode into shared scope-mount helper Addresses the final MEDIUM improvement from PR review: instead of each of the 6 on_mount hooks explicitly calling check_maintenance_mode/1, fold it into mount_phoenix_kit_current_scope/3 which all 6 already use. New live_sessions that use a scope-mounting on_mount hook now inherit maintenance mode enforcement automatically — no way to forget it. Removes 6 redundant call sites and updates the @doc comment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add automatic integration health tracking on OAuth refresh Token refresh failures used to be logged and forgotten — the admin UI continued to show the integration as "connected" until they manually clicked Test Connection. Now: - Integrations.record_validation/3 is the single source of truth for writing status + validation_status + last_validated_at, broadcasting integration_validated PubSub events, and no-oping when nothing changed. - refresh_access_token/1 calls it on both success (auto-recovering from a previously-errored state) and failure. - Activity entries integration.token_refresh_failed and integration.auto_recovered added. - IntegrationForm.save_validation_result/3 deleted; Test Connection now uses the same helper. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to #492. Three related issues found in review: - record_validation/3 silently no-oped when called with a settings-row UUID (e.g. from authenticated_request -> refresh_access_token via external modules that hold UUIDs). The automatic failure path then never flipped the admin UI, defeating the PR's stated goal. Resolves the key once via resolve_storage/1 which handles both "provider:name" keys and UUIDs, returning the canonical storage key plus decrypted data. - The actor_uuid parameter was ignored. Dropped to arity 2 — the manual path already attributes activity via validate_connection/2, and the two automatic activity entries are correctly auto-attributed. - Events.broadcast_validated fired even when save_integration failed. Guarded behind {:ok, _} so subscribers don't render stale state. Also adds integration tests for record_validation covering success, error formatting, no-op on unchanged status, missing-row case, and the UUID path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_cat_items (#493) * Refactor maintenance mode with layout override and scheduled windows Replaces the old @show_maintenance assign approach with a dynamic layout swap via socket.private[:live_layout] — the underlying LiveView keeps running so form state and scroll position are preserved when maintenance toggles on or off. URL never changes. Core changes: - Layout override in on_mount hook instead of redirect. When maintenance turns on, put_in socket.private[:live_layout] swaps the layout live; when it ends, PubSub triggers restoration of the original layout - New PhoenixKitWeb.Layouts :maintenance template with countdown timer - HTTP plug renders inline 503 HTML (with Retry-After header) for controller routes, with proper Phoenix.HTML escaping to prevent XSS - Scheduled maintenance windows with start/end UTC datetimes, 1-year upper bound, and 60-second tolerance for datetime-local minute precision - cleanup_expired_schedule auto-disables stale state on every page access - Process.send_after timer unblocks users when scheduled end arrives (clamped to Erlang's 32-bit timeout limit) - PubSub broadcasts on every state change so all connected LiveViews react instantly; admin tabs stay put, user tabs swap layouts - Manual toggle clears expired schedule on enable to avoid stale locks - Activity logging for all admin actions (toggle, content, schedule) - All user-facing strings wrapped in gettext Schedule validation rejects: empty, past start/end, end before start, dates >1 year in the future. Datetime inputs use the system time_zone setting for display and convert to UTC for storage. Extracts timezone helpers (offset_to_seconds, shift_to_offset, parse_datetime_local, format_datetime_local) to PhoenixKit.Utils.Date so they can be tested in isolation. Adds 93 new tests: unit tests for validate_schedule and PubSub, integration tests for Maintenance context and the plug (including XSS regression test), and doctested timezone helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix maintenance settings: live preview + remove broken Preview button - Move phx-change from individual inputs to the form element so the live preview updates on every keystroke (input-level phx-change on text inputs only fires on blur, making the preview appear broken) - Remove the "Preview" link that navigated to /maintenance. The path went through locale-prefixed routes and got caught by the publishing module's /:language/:group catch-all. The settings page already has an inline Live Preview card rendering the same content, so the link was redundant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address PR review feedback on maintenance refactor HIGH (merge blockers): - Register MaintenanceCountdown hook in priv/static/assets/phoenix_kit.js alongside the other phoenix_kit hooks. Parent apps already include this file, so the hook is now reliably available (was previously injected by a plug script that wasn't guaranteed to run before LiveSocket init). Remove the fragile inline injection from the Integration plug. - Plug's 503 HTML now uses inline CSS instead of linking to /assets/css/app.css (which wasn't actually served — the real digested path is /assets/app.css). The page is now self-contained with light + dark mode support via prefers-color-scheme, so it works on any route regardless of the parent app's asset pipeline. - Replace String.contains?/2 with String.starts_with?/2 in the plug's auth_route?/1 and static_asset?/1. A parent-app path like /blog/users/log-in-to-us would have bypassed maintenance mode. Add a regression test covering parent-app look-alike paths. MEDIUM: - disable_system/0 now clears maintenance_scheduled_end in addition to maintenance_scheduled_start so a stale end time doesn't surprise-disable the next re-enable. Update the test to assert both fields are cleared. - Track the Process.send_after timer ref in socket assigns and cancel it on reschedule (via new reschedule_maintenance_end_timer/1) so schedule changes don't leave a stale "auto-off" signal in flight. - Settings LiveView's PubSub handler now re-reads header and subtext so multi-admin editing stays in sync. The save handler broadcasts status change to trigger this sync. - schedule_error_message/1 catch-all now logs a warning with the unknown atom so future validation additions surface instead of being silently swallowed. - Document check_maintenance_mode/1's required call sites (all 6 on_mount hooks listed in the @doc) so new live_sessions don't forget it. - Add a HACK comment near put_in socket.private[:live_layout] noting it relies on Phoenix LiveView internals (same pattern as maybe_apply_plugin_layout) and should be revisited on major LV upgrades. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fold check_maintenance_mode into shared scope-mount helper Addresses the final MEDIUM improvement from PR review: instead of each of the 6 on_mount hooks explicitly calling check_maintenance_mode/1, fold it into mount_phoenix_kit_current_scope/3 which all 6 already use. New live_sessions that use a scope-mounting on_mount hook now inherit maintenance mode enforcement automatically — no way to forget it. Removes 6 redundant call sites and updates the @doc comment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add V97 migration: per-item markup_percentage override on phoenix_kit_cat_items Adds a nullable `markup_percentage DECIMAL(7, 2)` column on `phoenix_kit_cat_items`. When `NULL`, pricing falls back to the parent catalogue's `markup_percentage` (existing behavior); when set (including `0`), the item uses its own value. `NULL` vs. `0` is load-bearing: `0` means "explicitly sell at base price", `NULL` means "inherit whatever the catalogue currently uses". The column is nullable with no default, matching that distinction. Existing rows stay `NULL` and continue to inherit — no backfill is needed. The `up/1` operation is idempotent (`IF NOT EXISTS` guard inside a `DO $$` block), and the `down/1` rollback uses `DROP COLUMN IF EXISTS` so it's also idempotent. Lossy rollback note included in the down/1 docstring: any per-item overrides set after V97 are lost on rollback (items revert to the catalogue's markup). PG `ALTER TABLE ADD COLUMN` with no default and nullable = metadata- only operation, no full-table rewrite, AccessExclusive lock held briefly — safe for production-size tables. No new index — `markup_percentage` isn't queried as a filter, only SELECTed and computed in app code. Bumps `@current_version` to 97 and adds the `### V97` docblock entry above V96 (with the `⚡ LATEST` marker moved). Used by `phoenix_kit_catalogue` to support a per-item markup override field on the item form and import wizard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop language_switcher_dropdown from admin top bar in layout_wrapper.ex; locale selection now lives in the user avatar dropdown only - Remove dead admin_language_dropdown/1 and unused Phoenix.LiveView.JS alias from admin_nav.ex - Update Languages README: clarify that admin locale switches via user menu, and the pre-login globe on the sign-in page stays for unauthenticated visitors
- Extend table_default_with_cards with toolbar_title and toolbar_actions slots, rendered in the same row as the view-toggle buttons. Toolbar row is only shown when at least one of the three (title, actions, toggle) is present. Flex-wrap layout keeps the row adaptive on mobile; the view toggle stays desktop-only since mobile forces card view. - Apply to roles page: move 'Create New Role' button from a centered block above the table into toolbar_actions; add role count in toolbar_title. - Apply to activity page: move 'Clear filters' button from the filter card into toolbar_actions; show total count in toolbar_title and drop duplicate count from page subtitle. - Apply to integrations page: move 'Add Integration' from the page header actions into toolbar_actions; add connection count in toolbar_title. Empty-state CTA already provides its own button.
…#490) * Fix V95 migration to be truly idempotent for folder_uuid column Replace Ecto add_if_not_exists with raw SQL DO block that checks information_schema before adding the column, avoiding FK constraint conflicts when the migration runs against an existing schema. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add UUID search support to media page search bar Allow searching media files by UUID in addition to file name, enabling quick lookup of files by their identifier. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix long text overflow in media detail sidebar Add break-all and overflow constraints to the filename, title, and description fields in the media detail info panel to prevent long unbroken strings from overflowing the sidebar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add variant dimensions and file sizes to media detail page Display width×height and file size on each variant download button so users can quickly see the size details of each variant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix missing original file size in variant download buttons Fall back to the main file record's size and dimensions when the original file instance lacks them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add multi-format variant generation and image_set component Add alternative_formats field to storage dimensions so admins can configure additional output formats (WebP, AVIF) per dimension. The variant generator creates extra file instances for each alternative format alongside the primary. Add <.image_set> component that renders a <picture> element with <source> tags per format and srcset with width descriptors, letting browsers pick the best supported format and optimal size. - V97 migration adds alternative_formats column - Dimension schema validates alternative formats - Variant generator expands dimensions with alternatives - Admin UI: checkbox group on dimension form, badges on list - VariantNaming utility for parsing variant names - Storage helpers for bulk-loading variant data - Component groups by actual mime_type to handle failed conversions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add cross-user file deduplication with safe shared deletion When a different user uploads a file with the same checksum, clone the File record and instances to reuse the existing storage path instead of re-storing. On deletion, only remove physical files when no other File records share the same path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add All Files view and rename root button on media page Add an All Files button in the sidebar that shows every file across all folders in a flat grid/list without folder cards. Rename the previous All Files button to Root for clarity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix uploaded media going to root instead of current folder Pass the current folder UUID when reloading files after upload so the view stays in the current folder and new files appear there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address PR review feedback for image-set and alternative formats - Fix stray separator in dimensions table format display - Move alternative_formats template computation into LiveView assigns - Omit PNG from <picture><source> entries (keep as <img> fallback) - Guard empty srcset case when fallback_variants is empty - Bump migration from V97 to V98 with V97 as reserved placeholder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Alexander Don <alexdon20022@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump version 1.7.96 → 1.7.97 - CHANGELOG: remove duplicate 1.7.96 block, add 1.7.97 entry covering V97 per-item markup, V98 alternative_formats, ImageSet component, VariantNaming, multi-format variants, V95 idempotency fix, UI polish - Add PR #490 follow-up review section confirming all prior findings resolved Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract Users.Media LiveView into reusable PhoenixKitWeb.Components.MediaBrowser
accepting optional scope_folder_id for hard-scoped embedding (variant A).
/admin/media behavior preserved byte-for-byte (scope=nil).
Storage:
- Add within_scope?/2 predicate and scope-aware helpers:
list_folder_tree/1, folder_breadcrumbs/2, list_folders/2,
list_files_in_scope/2 (recursive CTE), count_orphaned_files/1
- Scope guards on all mutators (create_folder, update_folder, delete_folder,
move_file_to_folder, create_folder_link) return {:error, :out_of_scope}
Component:
- Parent LiveView shrunk to wrapper (1158 LOC to 38 LOC)
- Upload via progress: callback replaces Process.send_after polling
(handle_info absent, incompatible with live_components)
- Controlled mode detected by on_navigate attr presence: parent owns
navigation state and receives {MediaBrowser, id, {:navigate, params}}
notify messages; push_patch round-trip for ?folder/?q/?page/?orphaned
- scope_invalid detection with UI banner when scope folder deleted
- Upload target falls back to scope_folder_id at virtual root
- Orphan filter UI hidden when scope is set
- scoped_fallback? flash on URL-hack to out-of-scope folder
- Integer.parse fallback for malformed ?page param
- Guard update/2 first-mount check via Map.has_key? (not raises on nil)
- assign_new(:scope_folder_id) default so template @scope_folder_id
renders safely when parent omits the attr
Tests (52 total):
- 18 URL-builder unit tests (media_url_test.exs)
- 17 LiveView integration tests for URL sync and auth (media_test.exs)
- 17 component tests for scope behaviors (media_browser_test.exs)
- Storage scope contracts (scope_test.exs, media_browser_scope_test.exs)
- Extended ConnCase with sandbox + endpoint supervision
Out of scope (Task 7 follow-up):
- attr declarations on component
- Inline <script>/<style> extraction from template
- Bulk-mutator error aggregation for partial out-of-scope failures
- Extract build_url/parse_page as public helpers (test duplication)
Merge upstream/dev (commits a470bea..5f19ab8) which adds V97/V98 migrations, maintenance mode, OAuth health check, image_set component, and a new ?view=all "All Files" feature in the media browser. Conflicts in media.ex and media.html.heex resolved with our refactor (MediaBrowser live_component), then upstream's ?view=all feature ported into the new architecture: - Add :file_view assign and navigate_view_all event to MediaBrowser - Extend Storage.list_files_in_scope/2 with UUID-search via `fragment("CAST(? AS TEXT) ILIKE ?", f.uuid, search)` - URL round-trip for ?view=all (parent handle_params ↔ component navigate/apply_nav_params); search and clear_search now preserve file_view; toggle_orphan_filter explicitly resets view to nil - Template: All Files sidebar button with active-state highlight, All Files ({count}) section title, empty state, hide folder rows and new-folder controls when file_view == "all" - Preserves scope semantics: view=all under scope shows all files within scope subtree via existing recursive CTE (no leakage) Tests added: 2 URL-builder tests (view=all + q roundtrip, view=all standalone), 2 UUID-search tests in scope_test.exs, 1 deep-link test for /admin/media?view=all. Refactor bonus: resolve_folder/2 and load_nav_files/7 extracted from apply_nav_params to reduce complexity.
Runtime fixes discovered during browser smoke testing on Decor3D Print.
MediaBrowser event routing:
- Add phx-target={@Myself} to every phx-click/submit/change/keydown binding
in media_browser.html.heex (57 occurrences) and function components
folder_tree_node/move_folder_option (8 occurrences). phx-target does NOT
cascade from the root div; each event element needs its own target.
Without this, all clicks routed to the parent LiveView and crashed with
UndefinedFunctionError since the parent only defines mount/render.
- Add :myself attr to folder_tree_node and move_folder_option function
components so recursive calls propagate it correctly.
MediaBrowser KeyError on first mount:
- Replace `not socket.assigns[:uploaded_files]` with `not Map.has_key?(...)`
in update/2. Elixir's `not` is strict-boolean and raises ArgumentError
on nil or list values.
- Add assign_new(:scope_folder_id, fn -> nil end) so templates that use
@scope_folder_id don't crash with KeyError when parent omits the attr.
AssetsController — serve phoenix_kit_consent.js:
- Extend the valid-assets map to route by OTP app, not just filename.
phoenix_kit_consent.js lives in the phoenix_kit_legal package;
previously the controller returned 404 text/plain, breaking the strict
MIME check on any page that injects the consent banner script (that
then also broke other <script> tags and in effect killed LiveSocket
init on many pages).
- delete_selected: check within_scope? for each file before calling delete_file_completely. Previously scope was read but not applied, allowing crafted WebSocket events to delete files outside scope. - navigate_to_folder (uncontrolled mode): add within_scope? guard before rendering folder contents. Previously a crafted navigate_folder event with an out-of-scope UUID could expose sibling folder names. Both are defense-in-depth fixes — admin-only, require crafted events, but the scope enforcement contract should hold uniformly across all code paths. Found during independent PR review by verifier agent.
Add MediaBrowser live_component with scope_folder_id
Covers scope enforcement across all mutators, event routing verification (62 bindings), AssetsController widening for Legal app, and follow-up on commit 4a7057d closing two defense-in-depth holes in delete_selected and navigate_to_folder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show "Trash is empty" with appropriate icon and hint instead of the generic upload prompt when viewing an empty trash. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reset filter_trash and filter_orphaned in navigate_to_folder and navigate_view_all so switching views doesn't carry over stale state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reset filter_trash in apply_nav_params so controlled mode navigation (All Files, Root, folders) properly exits trash view without requiring a page refresh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass initial URL params to MediaBrowser on first mount so it loads the correct view immediately instead of defaulting to root and then correcting after WebSocket connects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove show_upload call from FolderDropUpload hook so files inject directly into the hidden upload input. Add inline progress bars in the card body. Fix upload input selector to search parent content area instead of the card-body scope. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changes requested. Flags two critical scope-isolation holes in trash view (list_trashed_files/count_trashed_files ignore scope) and permanent delete from trash (no within_scope? guard), plus PruneTrashJob defined but never scheduled in any crontab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. CRITICAL: Scope trash queries — list_trashed_files, count_trashed_files, and empty_trash now accept scope_folder_id and use recursive CTE to constrain results to the scope subtree. 2. CRITICAL: Permanent delete from trash now checks within_scope? before calling delete_file_completely, matching the soft-delete branch. 3. HIGH: Wire PruneTrashJob into Oban cron config in installer so parent apps pick up the daily 3 AM cleanup schedule. 4. HIGH: Add trashed_at to @type t spec and trashed to Status Flow docs. 5. Filter trashed files from Storage.list_files/1 for external callers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updated media browser component
…review restore_selected iterated client-trusted UUIDs from the selected_files MapSet and called Storage.restore_file/1 without a within_scope? check, mirroring the class of bug the PR #497 follow-up commit fixed for the permanent-delete branch. A scoped embed could push any UUID via toggle_select and restore files outside its scope. Apply the same repo.get + within_scope? guard the delete branch uses, and switch the flash to the actually-restored count since the guard may skip some selections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uard - Bump version 1.7.97 → 1.7.98 - CHANGELOG 1.7.98 entry covering PR #497 (V99 trash migration, PruneTrashJob daily cron, drag-drop upload via FolderDropUpload hook, URL-param first-mount hydration) and the restore_selected scope-guard fix Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V100 creates the four staff tables (`phoenix_kit_staff_departments`, `phoenix_kit_staff_teams`, `phoenix_kit_staff_people`, `phoenix_kit_staff_team_memberships`) used by the new `phoenix_kit_staff` module — UUIDv7 PKs, cascading deletes dept → team → memberships and user → person → memberships. V101 creates the five projects tables (`phoenix_kit_project_tasks`, `phoenix_kit_project_task_dependencies`, `phoenix_kit_projects`, `phoenix_kit_project_assignments`, `phoenix_kit_project_dependencies`) used by `phoenix_kit_projects`. Assignment/task templates carry a polymorphic assignee (team/department/person) and a `CHECK (num_nonnulls(...) <= 1)` constraint enforces at-most-one assignee on both tables. V101 depends on V100 for the assignee FKs. Bumps @current_version 99 → 101 and extends the moduledoc changelog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add V100 staff tables and V101 projects tables migrations
Move project logo from authorization to main settings page. Add site icon (favicon) setting with browser tab preview mockup. Add default tab title setting for browser tab fallback text. Layout wrapper reads both settings to render dynamic favicon and tab title. Also: fix recursive CTE in scoped trash query, use scope folder name instead of hardcoded "Root" in MediaBrowser, remove file counts from content area headers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix scope guard gap in restore_selected and append PR #497 follow-up review Address PR review: make MediaBrowser uploads work in embedded components Parent LiveViews embedding MediaBrowser must register uploads on their own socket since LiveView routes upload channel events to the parent. Add setup_uploads/1 helper and handle_parent_info/2 catch-all. The component tracks its id with the parent via :register_component message, and uploads are routed back to the component via send_update with a :pending_upload key so folder placement uses the component's current state. - Hide All Files button when scoped (only on admin page) - Show scope folder name instead of "Root" in sidebar/header - Remove file count from header titles - Bypass scope check on initial upload placement (files start at root) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove phx-target from the hidden upload form so validate events route to the parent LiveView where uploads are registered. Enable the FolderDropUpload hook whenever buckets are available (not only when in a subfolder). Drop a file anywhere in the media browser to auto-upload with inline progress bars. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hide the search bar by default behind a magnifying glass icon button in the header. Click to expand the search input, click again to collapse. The bar stays visible when a search query is active, and the toggle button highlights to indicate the active state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Attach MediaDragDrop hook to the component root so existing data-draggable-file and data-drop-folder attributes on files/folders become functional. Use pushEventTo(self.el) to route the move event to the component instead of the parent LiveView. Fix FolderDropUpload hook to ignore internal drags by checking for "Files" in dataTransfer.types, so device uploads and folder moves don't interfere. Change scoped root query to show only direct children (not the full subtree) so files moved into subfolders visually disappear from the root view. Searches still walk the full subtree via recursive CTE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap sidebar and content area in a shared card with rounded corners and shadow so they look like one component instead of separate pieces. Remove the inner content card's shadow, add left padding to the content area for breathing room, and hide the "Folders" sidebar header in scoped embeds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed the `[data-media-view]` display rules and the pre-render script that synced `html.dataset.mediaView` from localStorage. The server now conditionally renders only one of the grid or list view based on @view_mode, so those CSS rules could incorrectly hide the list view when localStorage held a stale value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The inline "Folder name" input at the scope root was indented less than the folder rows above it because it lacked the chevron spacer and used a different outer gap. Added the w-5 spacer, matched gap-0.5 on the form, and set ml-1.5 before the input so icons line up vertically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Click on a file now only pushes to /admin/media/:uuid when the component
is rendered with admin={true}. In any other embedding the click toggles
the file into the selection and switches select_mode on, so the component
acts as a picker by default. The admin media page opts in explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selection header gains a `…` dropdown with Download and Delete. Download pushes a `download_files` client event consumed by the MediaDragDrop hook, which fires one staggered `<a download>` click per file. Delete covers both files (trashed) and folders (removed) in a single action, with a context-aware confirmation built by `delete_selected_confirm/3`. Introduces `PhoenixKitWeb.Components.MediaBrowser.Embed` so embedding the browser is a single `use` line on the parent LiveView. The macro attaches an `on_mount` that calls `setup_uploads/1` and injects the `"validate"` upload-channel stub and `handle_info` delegator via `@before_compile`, so user clauses for other events/messages still match first. `users/media.ex` is migrated to the macro as the reference caller. AGENTS.md gets a MediaBrowser Component section covering the one-line embed, the `parent_uploads` template requirement, the `admin` attr, the built-in selection actions, and the manual-wiring fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fully-qualified PhoenixKitWeb.Components.MediaBrowser references inside __before_compile__ are intentional: the quote is injected into the caller's module, where any alias declared in Embed wouldn't be in scope. Suppress the Credo suggestion locally and leave a note about why. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- V102: `kind` + `discount_percentage` on catalogues, `discount_percentage` + `default_value` / `default_unit` on items, new `phoenix_kit_cat_item_catalogue_rules` table with CHECK constraints, UNIQUE (item_uuid, referenced_catalogue_uuid), and ON DELETE CASCADE on both FKs. Idempotent up/down; partial index on `kind='smart'`. - MultilangForm: trailing-debounce `attach_hook/4` + client-side skeleton toggle via composed JS. Rapid language-tab clicks no longer flash stale content or re-mount non-translatable fields. - Core form components (input/select/textarea/checkbox): realign `class` attr so it merges onto the styled element (input / daisyUI select wrapper / textarea / checkbox). `<.input>` gains `wrapper_class` for the outer `phx-feedback-for` div — matches the Phoenix 1.7 generator convention. - AGENTS.md: document the Core Form Components and Multilang Form Components sections, including the wrapper-scope rule and why language switching is client-side-first. - Bump to 1.7.99 and add test_load_filters / test_ignore_filters for Elixir 1.19 mix test hygiene. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated the media browser component
Add V102 smart catalogues migration and multilang debounce flow
Two coupled changes to the parent → component pending-upload handoff:
- The Embed macro's `handle_info({Mod, _, _}, socket)` clause only matches
3-tuples, but the pending-upload message was a 4-tuple so the clause
silently skipped it. Wrap the payload as `{path, entry}` to keep the
outer message 3-tuple.
- When more than one MediaBrowser is mounted on the same page, the first
`send_update` recipient would consume (and delete) the shared temp
file, and subsequent recipients would crash on `File.stat/1`. Copy the
temp file per additional recipient so each component owns its own path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers three merged PRs since 1.7.98: - #498: V100 staff + V101 projects migrations - #499: MediaBrowser Embed macro, selection menu/bulk download, admin attr, drag-drop file→folder move, toggleable search, drag-drop upload at any folder level, unified sidebar/content card, site icon + tab title + logo settings - #500: V102 smart-catalogue + discount migration, multilang debounce flow (attach_hook + client-side skeleton toggles), core form class/wrapper_class realignment, CSS sources abs-path fix, Elixir 1.19 test filters, AGENTS.md core form + multilang docs + CHANGELOG ownership rule Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rolls up everything on
devsince the lastmainupdate (1.7.95) into a release merge. Hex package already published: https://hex.pm/packages/phoenix_kit/1.7.99 · docs: https://hexdocs.pm/phoenix_kit/1.7.99 · tag:v1.7.99.Summary
Versions covered: 1.7.96, 1.7.97, 1.7.98, 1.7.99 (see
CHANGELOG.mdfor per-version entries).Highlights from 1.7.99 (newest)
phoenix_kit_cat_item_catalogue_rulestable (PR Add V102 smart catalogues migration and multilang debounce flow #500)Embedmacro for one-line integration, selection menu + bulk download,adminattr for picker vs admin modes, drag-drop file→folder move, toggleable search, drag-drop upload at any level, unified sidebar/content card (PR Updated the media browser component #499)attach_hook-based:handle_infointerception + client-side skeleton toggles; 150 ms trailing debounce viaProcess.send_afterwith timer ref insocket.private(PR Add V102 smart catalogues migration and multilang debounce flow #500)classattr aligned with Phoenix 1.7 generator convention; newwrapper_classon<.input>(PR Add V102 smart catalogues migration and multilang debounce flow #500)test_load_filters/test_ignore_filtersinmix.exsIncluded prior versions (already on hex, just catching main up)
alternative_formats,ImageSetresponsive<picture>, multi-format variant generation (PR Add V97 migration: per-item markup_percentage override on phoenix_kit_cat_items #493)DraggableList.hide_source, wiggle animation (PR Added feature to make languages sortable in the language admin page #489)Test plan
mix precommit(compile + format + credo --strict + dialyzer) — greenmix hex.publish— succeeded, package live at https://hex.pm/packages/phoenix_kit/1.7.99v1.7.99pushed🤖 Generated with Claude Code