Skip to content

Releases: BeamLabEU/phoenix_kit

v1.7.132

07 Jun 18:18

Choose a tag to compare

1.7.132 - 2026-06-07

Added

  • PhoenixKitWeb.Components.AITranslate.Embed — a use-able macro that
    wires the host side of the AI-translate modal so consumers stop hand-copying
    it. Via attach_hook lifecycle hooks it injects the six ai_* handle_event
    clauses (ai_toggle_modal, ai_select_endpoint, ai_select_prompt,
    ai_select_scope, ai_generate_prompt, ai_translate_lang) and the
    {:ai_translation, …} handle_info clause, composing with the host's own
    handlers (non-AI events/messages pass straight through). Form re-sync defaults
    to assigning both :changeset and :form; hosts whose sync differs override
    ai_translate_assign_form/2. Because lifecycle hooks run before the host's
    own callbacks and :halt on the events they own, a host clause for those AI
    events is shadowed — the moduledoc documents this so the AI clauses aren't
    re-implemented in the host.
  • V131 migration adds a generic metadata JSONB NOT NULL DEFAULT '{}'
    column to phoenix_kit_staff_people (mirrors the entity_data shape). The
    first consumer is staff soft-delete, which stashes the prior lifecycle status
    under metadata["trashed_from_status"]; the column is general-purpose so
    future per-person metadata needs no new migration. Idempotent
    ADD COLUMN IF NOT EXISTS; @current_version → 131.

Fixed

  • Media detail comments: forward Leaf editor events to the embedded
    CommentsComponent. The Leaf rich-text composer reports its content to the
    host LiveView via a {:leaf_changed, …} process message; media_detail
    wasn't forwarding it, so "Post Comment" silently posted empty content. Adds
    the runtime forward (resolved against the optional phoenix_kit_comments dep)
    plus a handle_info catch-all so unmatched messages don't crash the LV.

Changed

  • Extract PhoenixKitWeb.CommentsForwarding.forward_leaf_changed/2 — the
    {:leaf_changed, _} forwarding contract (optional-dep guard + runtime
    apply/3 + :pass/contract-drift handling, logging unexpected returns) now
    lives in one module. MediaBrowser.Embed and MediaDetail both delegate to
    it instead of carrying near-verbatim copies, so a future
    forward_leaf_event/2 contract change touches one call site. As a side
    effect the MediaBrowser.Embed macro no longer injects require Logger into
    every host (the only Logger use moved into the shared module).
  • Document required host wiring on the callback-message components
    MediaSelectorModal, MarkdownEditor, and MediaGallery — loud
    "required host wiring / silent failure otherwise" moduledoc contracts. These
    stay docs-only by design (per-consumer handling, not uniform boilerplate, so
    no Embed macro). MediaSelectorModal also documents the :notify alternative
    for LiveComponent consumers.
  • Dependency bumps (mix.lock): bandit 1.11.1 → 1.12.0, etcher
    0.6.5 → 0.6.6, fresco 0.6.3 → 0.7.1, spitfire 0.3.12 → 0.3.13, tesla
    1.18.3 → 1.20.0.

v1.7.130

04 Jun 11:08

Choose a tag to compare

Cumulative release since 1.7.128 — bundles the generic AI-translation pipeline (PR #582), the per-user Etcher color palette (PR #581), and follow-up security/quality fixes. No new migration (still V128).

Added

  • Generic AI-driven translation pipeline in core, so feature modules plug in via a small adapter instead of re-implementing the whole stack:
    • PhoenixKit.Modules.AI.Translatable behaviour (fetch/2, source_fields/2, put_translation/4, optional pubsub_topics/1), exposed via the optional ai_translatables/0 callback on PhoenixKit.Module and discovered through PhoenixKit.ModuleRegistry.
    • PhoenixKit.Modules.AI.Translations orchestration — availability, endpoint/prompt defaults, idempotent shared prompt, enqueue/1 + enqueue_all_missing/2, per-resource + global PubSub topics, missing_languages/3.
    • PhoenixKit.Modules.AI.TranslateWorker — generic one-job-per-language Oban worker with retry classification, {:snooze, 30} on rate-limit, and an ai.translation_added audit entry.
  • Shared AI-translate UIPhoenixKitWeb.Components.AITranslate (button/modal/progress/stall hint) + AITranslate.{FormGlue,FormBinding} LiveView glue behind a 3-callback binding.

Changed

  • AI-translation broadcasts keep translated content off broad topics: the full payload (with :fields) goes only to the per-resource topic; the global + adapter topics receive a content-free summary.

Fixed

  • Sanitize the per-user Etcher color palette on both write and read. MediaCanvasViewer filters the client-supplied etcher:colors-changed payload to short, color-shaped strings (deduped, capped at 24) before persisting, and load_user_colors/1 re-sanitizes on read — so a palette stored before the guard shipped, or written by any other path, never reaches <Etcher.layer colors={…}> untrusted.

📦 https://hex.pm/packages/phoenix_kit/1.7.130 · 📖 https://hexdocs.pm/phoenix_kit/1.7.130

1.7.106 - 2026-05-08

08 May 23:13

Choose a tag to compare

Added

  • V111 migration: PDF library tables for the upcoming catalogue PDF subtab (PR #516)
    • phoenix_kit_cat_pdfs — thin per-upload row. file_uuid FK to phoenix_kit_files(uuid) ON DELETE RESTRICT (catalogue manages the file lifecycle; core prune can't remove files referenced by a live catalogue row). Soft-delete via status sentinel (active / trashed) + trashed_at. Two uploads of identical content (different filenames) → two rows sharing one phoenix_kit_files row + one extraction
    • phoenix_kit_cat_pdf_extractions — keyed by file_uuid PK. Worker state machine (pending → extracting → extracted | scanned_no_text | failed) + page_count + extracted_at + error_message. Cascades on file hard delete
    • phoenix_kit_cat_pdf_page_contents — content-addressed dedup cache. PK on content_hash (SHA-256 hex of normalized page text). Same page text across multiple PDFs is stored once. GIN trigram index lives here so the search index doesn't grow with cross-PDF duplication
    • phoenix_kit_cat_pdf_pages — composite PK (file_uuid, page_number); content_hash FK to the dedup cache (RESTRICT — orphaned content rows GC'd by a catalogue-side helper, not by FK cascade)
    • Enables pg_trgm extension; @current_version 110 → 111
  • PhoenixKit.KnownPackages — live catalog of known external PhoenixKit packages, replacing the previously hardcoded list in ModuleRegistry.known_external_packages/0 (PR #523)
    • Fetched on demand from https://hex.pm/api/packages?search=phoenix_kit_&sort=name and cached for 10 minutes in an ETS named table (:phoenix_kit_known_packages_cache)
    • Stale-while-revalidate with cap: on Hex failure, serves cached data up to :max_stale_age_ms (default 24h); beyond that, drops the cache and falls back to :extra_known_packages config entries only
    • :warning log on stale-served and empty-cache-extras-only; :error log when cache exceeds max stale age — operationally distinct alert levels
    • Link-header pagination with a 20-page cap (@max_pages) so a malformed Link header pointing back to the same page can't loop forever
    • extra_known_packages config knob — parent apps with private/forked packages declare them inline and they take precedence over Hex entries on the package dedup key (source: "config" baked in)
    • hex_docs_icon_name: hero-<name> convention — package authors append the marker to their Hex package description and the catalog UI picks it up; default is hero-puzzle-piece
  • Per-module gettext support on Dashboard sidebar labels and tooltips (PR #522)
    • PhoenixKit.Dashboard.Tab gains gettext_backend: module() | nil (default nil) and gettext_domain: String.t() (default "default") fields, plus localized_label/1 and localized_tooltip/1 resolvers that call Gettext.dgettext/3 when a backend is set and fall back to the raw label otherwise
    • PhoenixKit.Dashboard.Group gains the same two fields plus localized_label/1
    • Tab.divider/1 and Tab.group_header/1 accept the new opts; Tab.new/1 round-trips both via get_attr/2
    • 14 render sites in Sidebar, AdminSidebar, TabItem swap tab.labelTab.localized_label(tab) and equivalents — mechanically uniform, no shape changes
    • Hot-reload safety via Map.get/2 (not pattern matching) on the new fields — old-shape %Tab{} cached in ETS or :persistent_term from before the upgrade falls through as if gettext_backend were nil rather than raising FunctionClauseError. Pinned by an explicit Map.delete(:gettext_backend) regression test
    • guides/per-module-i18n.md — public guide for module developers (setup checklist, mix.exs / backend / .po flow, dynamic_children/2 locale handling, dividers and group headers, tooltips, greenfield template, retrofitting checklist, smoke test pattern, common pitfalls including the hot-reload safety contract)
    • dev_docs/instructions/2026-05-08-per-module-i18n-procedure.md — internal operational procedure capturing every gotcha hit during the Newsletters pilot (skip-worktree on mix.exs, path-dep workflow during local dev, conditional CI skip pattern for graceful degradation)
  • :per_translation_urls attr on the three LanguageSwitcher variants — language_switcher_dropdown/1, language_switcher_buttons/1, language_switcher_inline/1 (PR #525)
    • Each entry is %{code: <display_code>, url: <full_url>}. Both atom-keyed and string-keyed entries accepted (useful when the list comes from JSON/JSONB rather than Elixir code)
    • Resolves each language's base_code against the list via DialectMapper.extract_base/1 so "en-US" and "en" both resolve cleanly. Falls back to the locale-rewrite default when no entry matches OR the matched entry has a nil URL (e.g. an unpublished draft)
    • Useful when a feature module has computed canonical URLs that the simple locale-rewrite default can't reproduce — for example publishing's per-language URL slugs where /en/blog/my-post and /fr/blog/mon-article aren't related by segment swap. Pass assigns[:phoenix_kit_publishing_translations] from the layout
    • 7 new tests in test/phoenix_kit_web/components/core/language_switcher_test.exs pin the contract (atom-keyed, string-keyed, full-dialect normalization, per-language fallback, nil/empty/missing-attr pass-through)
  • Drag-handle scoping + sortable feedback infrastructure (PR #525)
    • <.table_default> emits data-sortable-handle=".pk-drag-handle" when @on_reorder is set; only the .pk-drag-handle element gets cursor-grab styling. Click-to-expand / button-press / text-selection on a card no longer fights with SortableJS drag detection
    • SortableGrid JS hook: new sortable:flash LV→client event handler. The host LV pushes {uuid: "...", status: "ok" | "error"} after each reorder_items attempt; the hook applies pk-sortable-flash-{ok,err} class for ~1.2s, idempotent via reflow trigger. Queries every [data-id] element so table-view + card-view both animate. Defensive status-validation guard — unknown values bail rather than falling into the err-class branch
    • <tr> cell-width preservation via onChoose / onUnchoose — SortableJS's forceFallback: true + fallbackOnBody: true clones the dragged <tr> to document.body, where it loses its <table> ancestor and <td>s collapse to content width. The hook now snapshots computed widths and pins them inline before the drag preview renders; onUnchoose restores them
    • data-sortable-handle attr threads to SortableJS's handle option for any caller; moved_id always included in the reorder_items payload (was only on cross-container moves) so the LV can push back a sortable:flash keyed to the just-moved row
  • MediaBrowser modal viewer becomes the default click target for non-admin / non-select_mode browsers, with read-only image / video / PDF / icon preview, metadata sidebar, Download button, prev/next chevrons (and ←/→ keyboard shortcuts), and Esc / backdrop close (PR #519)
    • Mobile-fullscreen layout via position: fixed; inset: 0 — bypasses daisyUI's grid + iOS Safari's 100vh/100dvh quirks. Desktop reverts to 95vw × 90vh centered modal with rounded corners. The !-prefix utility chain on .modal-box is required because daisyUI v5's defaults win the cascade over plain Tailwind utilities
    • MediaImageZoom JS hook lazy-loads Panzoom 4.6.0 from jsDelivr when the modal opens; image supports wheel/pinch/double-tap zoom and drag-pan. Listener attaches to the parent so the cursor doesn't have to land on the image; destroyed cleanup removes the wheel listener and destroys the Panzoom instance
    • Bulk-select still reachable — clicking the toolbar's Select button flips select_mode on, and from then on clicks toggle selection instead of opening the modal
  • LiveView login redirect now carries the original request path as ?return_to= (PR #519)
    • New login_path_with_return_to/1 private helper in PhoenixKitWeb.Users.Auth reads Phoenix.LiveView.get_connect_info(socket, :uri), encodes path?query via URI.encode_www_form/1, and threads it into the redirect target. Wired into the four redirect_require_login paths in on_mount hooks
    • Trailing-slash self-loop guard: String.trim_trailing(path, "/") on both sides of the equality check, so /users/log-in and /users/log-in/ are treated as the same path and no return-to round-trips back to itself
    • Pairs with the existing ?return_to= flow in login.ex (sanitize_return_to/1:user_return_to session → log_in_user/3)
  • PhoenixKit.ModuleRegistry.get_module_key_for_namespace/1 — symmetric with the existing get_by_key/1. Resolves a top-level Elixir namespace string (e.g. "PhoenixKitEntities") to the registered plugin's module_key/0 (PR #521)
    • Iterates all_modules/0, matches on Module.split(mod) == [top_namespace] (exact, single segment), returns the key string or nil for unmatched
    • Reads from :persistent_term so there's no GenServer roundtrip on the hot path
  • Microsoft 365 OAuth tenant override + generic interpolate_url/3 helper in PhoenixKit.Integrations.OAuth — providers can now substitute {key} placeholders in auth_url / token_url from per-row integration_data, falling back to a provider-level :url_defaults map (PR #516)
    • Closes the previously hardcoded /common/ Microsoft tenant — single-tenant operators got AADSTS50194 errors. New tenant_id setup field with common default; multi-tenant remains the default behavior. Three pinning tests in test/phoenix_kit/integrations/oauth_test.exs
    • Wired into authorization_url/5, exchange_code/4, refresh_access_token/2. URLs without { pass through unchanged (zero impact on Google / OpenRouter / Mistral / DeepSeek)
  • "Resolve a LiveView module to its permission key" block-comment on PhoenixKitWeb.Users.Auth.permission_key_for_admin_view/1 documenting the four-step resolution order (static map → custom-tabs → PhoenixKit.Modules.<X>.Web.* namespace → registered-...
Read more

1.7.100 - 2026-04-22

22 Apr 19:37

Choose a tag to compare

Added

  • V103 migration: nullable self-FK parent_uuid on phoenix_kit_cat_categories with b-tree index on (parent_uuid) for arbitrary-depth category trees. Existing rows stay NULL and become roots — no backfill. No DB-level ON DELETE cascade (subtree cascades are owned by the context layer so they go through soft-delete + activity log) (PR #503)
  • scope_folder_id attr on PhoenixKitWeb.Live.Components.MediaSelectorModal — filters the browse query to the given folder plus any files reached via FolderLink, and assigns newly-uploaded files into that folder (adopt as home if orphan, else add a FolderLink). Plugins scoping the picker to a single domain object (e.g. a catalogue item) pass this after lazy-creating their folder (PR #503)
  • PhoenixKit.Settings.Setting.optional_settings/0 accessor exposing @optional_settings for invariant tests
  • Invariant test (test/phoenix_kit/settings/setting_test.exs) asserting every empty-string default in PhoenixKit.Settings.get_defaults/0 is also in @optional_settings, to prevent the class of bug fixed in PR #502 from recurring

Changed

  • PhoenixKit.Modules.Storage.File changeset file_type allowlist widened from ["image", "video", "document", "archive"] to include "audio" and "other" so non-image/video uploads bucket cleanly (PR #503)
  • MediaSelectorModal.load_files/2 refactored into four composable scope_files_by_{user,folder,type,search} helpers — credo cyclomatic-complexity fix from adding the new scope branch (PR #503)

Fixed

  • Settings batch save no longer rolls back when site_icon_file_uuid or default_tab_title is left empty on the General Settings form. Both keys added to @optional_settings in PhoenixKit.Settings.Setting and seeded with empty-string defaults in PhoenixKit.Settings.get_defaults/0 (PR #502)
  • MediaSelectorModal.maybe_set_folder/2 errors (from the folder_uuid update or FolderLink insert) now log a warning via warn_on_folder_error/3 instead of being silently discarded by _ =. Previously, a failed scope assignment after a successful upload left no trace

1.7.98 - 2026-04-16

17 Apr 03:02

Choose a tag to compare

Changes

  • Add MediaBrowser live_component with scope_folder_id scoping (PR #495)
  • Add trash bucket, drag-drop upload, and breadcrumb improvements to media browser (PR #497)
  • Scope-aware Storage helpers; tenant isolation fixes; cron wiring
  • Fix scope guard gaps in restore, delete_selected, navigate_to_folder
  • Fix flash of root view on page refresh with URL params
  • Fix trash filter persisting across URL-driven navigation

v1.7.97

15 Apr 17:44

Choose a tag to compare

Added

  • V97 migration: per-item markup_percentage override on catalogue items (#493)
  • V98 migration: alternative_formats column on storage dimensions
  • PhoenixKit.Modules.Shared.Components.ImageSet — responsive <picture> component with AVIF/WebP/JPEG <source> entries (#490)
  • PhoenixKit.Modules.Storage.VariantNaming — format-suffix parsing utility
  • Multi-format variant generation (WebP/AVIF alongside primary format per dimension)
  • Variant dimensions and file sizes on media detail page
  • UUID search support on media page search bar

Changed

  • V95 migration made truly idempotent for folder_uuid column
  • Dimensions table format cell renders as JPEG + WEBP, AVIF (fixed stray separator)

Fixed

  • Long text overflow in media detail sidebar
  • Missing original file size in variant download buttons

Hex: https://hex.pm/packages/phoenix_kit/1.7.97
Docs: https://hexdocs.pm/phoenix_kit/1.7.97

1.7.96 - 2026-04-13

13 Apr 20:14

Choose a tag to compare

Added

  • Sortable languages in admin (drag-and-drop reorder)
  • hide_source option on DraggableList component
  • Wiggle animation for reorder mode with prefers-reduced-motion support

Changed

  • Dedup language codes in reorder, use MapSet for lookup
  • Extract wiggle CSS to JS-injected styles with pk- prefix

1.7.95 - 2026-04-11

11 Apr 19:58

Choose a tag to compare

Added

  • V95 migration: media folders and folder links tables
  • V96 migration: catalogue_uuid FK on catalogue items for direct catalogue membership

1.7.93 - 2026-04-08

08 Apr 10:17

Choose a tag to compare

  • Fix installer to auto-inject PhoenixKitHooks into app.js
  • Fix decrypt after legacy integration migration
  • Activity logging for Integrations
  • Simplify integration status (connected/disconnected)
  • Cookie max_age set to 60 days
  • OAuth remember_me by default

1.7.92 - 2026-04-07

07 Apr 22:12

Choose a tag to compare

  • Fix Google token refresh for named integration connections
  • Add V94 migration for Document Creator sync