PhoenixKitTemplatesEditor

A PhoenixKit module that edits your app's email template files — the phoenix_kit_templates overrides PhoenixKit renders its emails from — in a tab of Settings › Emails Transactional.

Your templates stay yours: one directory of files in your app, deployed with your code. This package is only the tool, plus a block of config that tells it which templates are yours and how to show them.

Status: 0.1.0, unreleased.

Installation

# mix.exs
{:phoenix_kit_templates_editor, "~> 0.1"}

Then mix deps.get. PhoenixKit discovers the module by itself — no config line in :phoenix_kit, :modules is needed. Switch it on at Admin › Modules (Templates editor); it is off by default, and a switched-off module shows no tab.

Requires phoenix_kit 2.57.0 or later.

Let the tab know who is acting

PhoenixKit mounts the tab with nothing but its id, so the module needs a hook in PhoenixKit's live sessions to check, on every save, who is saving:

# config/config.exs
config :phoenix_kit,
  extra_live_session_on_mount: [PhoenixKitTemplatesEditor.Web.ScopeHook]

This is read when your router compiles. Without it the tab works, but shows the templates read-only and says why.

The hook is a stopgap: once PhoenixKit's email settings page passes its phoenix_kit_current_scope to the tabs it mounts (a one-line change there), the tab takes the scope from that and the hook can go.

The variables panel's hook

A click on a variable puts it at the cursor through a LiveView hook the module ships prebuilt (priv/static/assets/phoenix_kit_templates_editor.js, declared by js_sources/0). PhoenixKit's :phoenix_kit_js_sources compiler folds it into your LiveSocket — no inline script, no app.js edit — provided your mix.exs lists the compiler:

compilers: [:phoenix_kit_js_sources, :phoenix_live_view] ++ Mix.compilers()

Without it the panel still works: a click puts the variable at the end of the body instead.

Where templates are edited: the directory

The module never changes a file's owner. Where editable: true, make the templates root a setgid directory of the group that owns your tree, so what the server writes stays editable by the people who commit it:

chgrp -R devs priv/phoenix_kit_templates
chmod -R g+rwX priv/phoenix_kit_templates
find priv/phoenix_kit_templates -type d -exec chmod g+s {} +

Then give the server process the umask 0002 (supervisord umask=002, systemd UMask=0002): new files come out 0664 and new directories 2775, in the root's group. The module sets no mode and no owner itself: doing that after a write, as the server's user, in a directory other people can write to, could be turned onto another file through a link put there in the meantime.

Who sees the tab

The email settings page is behind PhoenixKit's settings permission. The tab adds the module's own key, templates_editor, so it can only narrow that:

To A role needs
see the tab settings and templates_editor, with the module switched on
save, create or delete files the above and templates_editor.write, and editable: true in config

Owner holds every permission. Admin is granted the module's keys when PhoenixKit first discovers it; revoke them in the roles matrix to hide the editor from Admins.

For your own "Edit templates" links:

if PhoenixKitTemplatesEditor.accessible?(scope) do
  # href = PhoenixKitTemplatesEditor.tab_path()        # URL prefix and locale applied
end

Configuration

# config/config.exs
config :phoenix_kit_templates_editor,
  root: nil,                    # nil: the first of PhoenixKit.Email.Content.override_paths/0
  name_prefixes: ["shop_order_", "_header-shop", "_footer-shop", "_layout-shop"],
  locales: ~w(et ru en),
  base_locale: "et",
  layout_group: "shop",
  preview: {MyApp.Emails, :preview},                    # (name, locale) -> {subject, html, text}, see below
  sample_variables: {MyApp.Emails, :sample_variables},  # () -> map
  block_placeholders: ["documents_list"],
  catalog: true

# config/dev.exs — only where templates are edited
config :phoenix_kit_templates_editor, editable: true
Key Default Meaning
root first of PhoenixKit.Email.Content.override_paths/0 the templates directory; it must exist
editable false true allows writing, false shows the files read-only; any other value is a config error (the tab lists it, no editor). Leave it unset in production
name_prefixes [] which templates the editor sees and may write — nothing until you say. A prefix shaped like a layout part (_header-shop) allows that part of layout shop and shop-…, not every name starting with the string
locales PhoenixKit's enabled languages (base codes), else just base_locale the language tabs, in order
base_locale first of locales, else PhoenixKit's default language, else "en" the language labels and notes fall back to
layout_group nil the layout (_header-<g>, _footer-<g>, _layout-<g>) of an email without layout.txt
layout_groups [layout_group] layout namespaces the editor may show and create: g and g-… (not gx)
preview nil your preview callback (below); without one the module renders its own
sample_variables %{} a map, or a callback returning one (below)
describe_variables nil (name) -> %{variable => description}, shown in the variables panel (below)
complete? nil (name, base_locale) -> boolean (below); default: subject and body on base_locale
sent_by nil (name) -> :core | :host — who sends the email (below); default: PhoenixKit for the emails of its catalogue, your app for your own
test_send nil (name, locale, user) — your own way to send "Send to me" for your own emails (below); default, and always for the emails PhoenixKit and its modules send: PhoenixKit.Mailer
block_placeholders [] variables that stand as a block of their own
raw_variables [] variables allowed in {{{x}}} besides content, header, footer
email_fallback true whether emails get a locale-less Fallback tab
shared_layout_editable false whether the shared _header/_footer/_layout (PhoenixKit's own system emails) may be edited
overridable [] which emails PhoenixKit and its modules send may be given a file here — none until you say: a list — exactly those names; :all, your explicit choice — every one but the emails about getting into an account (account confirmation, password reset, email change, magic link sign-in and registration, organization invitation, the new-login and failed-sign-in alerts), which only a list names; "every one but" is this package's own list of those emails (Names.auth_emails/0), not core's current set, so a sign-in email a newer core adds is overridable under :all until the package is updated
catalog false whether your templates are offered to PhoenixKit's email preview
watch same as editable whether the files are re-read when they change on disk
max_templates 500 the most templates the root may hold: a new one past it is refused (nil — no limit)
max_root_bytes 52_428_800 (50 MiB) the most bytes the root's files may take: a save, copy or new file past it is refused (nil — no limit)

A config that does not check out is shown on the tab as a list of problems — the page itself keeps working.

Callbacks

Each is {module, function} or a function of the arity shown. One that raises, throws or exits is logged and never takes the page down.

The tab

What this module does NOT do

Development

mix deps.get
mix test.setup     # once; creates the test database
mix test
mix precommit

See AGENTS.md for conventions and the test setup.

License

MIT — see LICENSE, which also carries the notice for code derived from phoenix_kit_templates.