Packages

phoenix_kit

1.7.162
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit module.ex
Raw

lib/phoenix_kit/module.ex

defmodule PhoenixKit.Module do
@moduledoc """
Behaviour for PhoenixKit feature modules (internal and external).
Any module that implements this behaviour can register itself with PhoenixKit's
tab registry, permission system, supervisor tree, and route system.
## Usage
defmodule PhoenixKitHelloWorld do
use PhoenixKit.Module
@impl true
def module_key, do: "hello_world"
@impl true
def module_name, do: "Hello World"
@impl true
def enabled?, do: PhoenixKit.Settings.get_boolean_setting("hello_world_enabled", false)
@impl true
def enable_system do
PhoenixKit.Settings.update_boolean_setting_with_module("hello_world_enabled", true, "hello_world")
end
@impl true
def disable_system do
PhoenixKit.Settings.update_boolean_setting_with_module("hello_world_enabled", false, "hello_world")
end
@impl true
def permission_metadata do
%{
key: "hello_world",
label: "Hello World",
icon: "hero-hand-raised",
description: "A demo module"
}
end
@impl true
def admin_tabs do
[%PhoenixKit.Dashboard.Tab{
id: :admin_hello_world,
label: "Hello World",
icon: "hero-hand-raised",
path: "hello-world",
priority: 640,
level: :admin,
permission: "hello_world",
match: :prefix,
group: :admin_modules
}]
end
end
## Required Callbacks
- `module_key/0` - Unique string key (e.g., `"tickets"`, `"billing"`)
- `module_name/0` - Human-readable display name
- `enabled?/0` - Whether the module is currently enabled
- `enable_system/0` - Enable the module system-wide
- `disable_system/0` - Disable the module system-wide
## Optional Callbacks
All optional callbacks have sensible defaults provided by `use PhoenixKit.Module`:
- `get_config/0` - Module stats/config map (default: `%{enabled: enabled?()}`).
The default calls `enabled?()` which may hit the database. External modules
with expensive config should override this with a cached implementation.
- `permission_metadata/0` - Permission key, label, icon, description (default: `nil`)
- `admin_tabs/0` - Admin navigation tabs (default: `[]`)
- `settings_tabs/0` - Settings subtabs (default: `[]`)
- `user_dashboard_tabs/0` - User-facing dashboard tabs (default: `[]`)
- `children/0` - Supervisor child specs (default: `[]`)
- `route_module/0` - Module providing route macros (default: `nil`)
- `version/0` - Module version string (default: `"0.0.0"`)
- `migration_module/0` - Module implementing versioned migrations (default: `nil`).
When set, `mix phoenix_kit.update` will automatically run this module's migrations
alongside the core PhoenixKit migrations.
- `required_integrations/0` - Integration provider keys this module needs (default: `[]`).
Used by the Integrations settings page to show relevant providers.
- `integration_providers/0` - Additional provider definitions this module contributes (default: `[]`).
"""
@typedoc "Permission metadata for the module"
@type permission_meta :: %{
key: String.t(),
label: String.t(),
icon: String.t(),
description: String.t()
}
# Required callbacks
@callback module_key() :: String.t()
@callback module_name() :: String.t()
@callback enabled?() :: boolean()
@callback enable_system() :: :ok | {:ok, term()} | {:error, term()}
@callback disable_system() :: :ok | {:ok, term()} | {:error, term()}
# Optional callbacks with defaults provided by __using__
@callback get_config() :: map()
@callback permission_metadata() :: permission_meta() | nil
@callback admin_tabs() :: [PhoenixKit.Dashboard.Tab.t()]
@callback settings_tabs() :: [PhoenixKit.Dashboard.Tab.t()]
@callback user_dashboard_tabs() :: [PhoenixKit.Dashboard.Tab.t()]
@callback children() :: [Supervisor.child_spec() | module() | {module(), term()}]
@callback route_module() :: module() | nil
@callback version() :: String.t()
@callback migration_module() :: module() | nil
@callback required_modules() :: [String.t()]
@callback required_integrations() :: [String.t()]
@callback integration_providers() :: [map()]
@doc """
Returns a list of notification types this module contributes.
Each type is a map with:
* `:key` — binary, stable identifier used in user prefs (e.g. `"posts"`)
* `:label` — binary, user-facing display (e.g. `"Posts"`)
* `:description` — binary, short explainer shown under the toggle
* `:actions` — list of dotted action strings (`["post.liked", "post.commented"]`)
* `:default` — boolean, the toggle's default state for users who haven't
set a preference
Types merge with core PhoenixKit types (`account`, `posts`, `comments`) and
show up automatically in the UserSettings "Notifications" section. The
filter in `PhoenixKit.Notifications.maybe_create_from_activity/1` resolves
each action to a type via `:actions` and skips the fan-out when the user
has muted that type.
Headless modules (no user-facing actions) can skip this callback — the
default is `[]`.
## Example
def notification_types do
[
%{
key: "reviews",
label: "Reviews",
description: "When someone leaves you a review",
actions: ["review.submitted", "review.edited"],
default: true
}
]
end
"""
@callback notification_types() :: [map()]
@doc """
Returns Tailwind CSS source roots for scanning.
Each entry is either:
* an atom — the OTP app name. The compiler resolves it via the parent
app's `mix.exs` deps (`deps/<app>` for Hex, `path:` value for path deps).
* a string — a literal path. Absolute paths (starting with `/`) emit as
`@source "<abs>";` verbatim; relative paths emit as `@source "../../<path>";`
(relative to `assets/css/_phoenix_kit_sources.css`). Useful when a module
wants to add a path-dep absolute fallback alongside the OTP-app entry,
so both Hex and path-dep installs work without parent-app toggles.
## Examples
def css_sources, do: [:phoenix_kit_publishing]
# Path-dep friendly:
@source_root Path.expand(Path.join(__DIR__, "../.."))
def css_sources, do: [:phoenix_kit_publishing, @source_root]
Headless modules (no templates) can skip this callback — the default is `[]`.
"""
@callback css_sources() :: [atom() | String.t()]
@doc """
Returns JavaScript hook bundles this module needs registered in the host's
`LiveSocket`.
A LiveView JS hook must be present in the host's single `LiveSocket` at
construction time — a nested LiveView cannot register one at runtime. This
callback lets a module declare a prebuilt bundle (e.g. a standalone Hex
package's hooks) so the `:phoenix_kit_js_sources` compiler can wire it into
the host automatically, the same way `css_sources/0` wires Tailwind sources.
Each entry is a map:
* `:app` — the OTP app shipping the bundle. Resolved at compile time via
`:code.priv_dir/1`, so it works for Hex installs and path deps alike (no
`deps/<app>` path arithmetic).
* `:file` — path to the prebuilt bundle **inside that app's `priv/`**, e.g.
`"static/assets/my_hooks.js"`. The file must ship in the app's `priv/`.
* `:global` — the `window.<Name>` the bundle assigns its hooks to. The
compiler folds it into `window.PhoenixKitHooks` (which the host already
spreads into `LiveSocket`), so no per-module `app.js` edit is needed. Must
be unique across all modules — two bundles sharing a global would clobber
each other, so the compiler fails loudly on a collision.
## Hook names must be globally unique too
The compiler enforces unique `:global` names, but it cannot see *inside* a
prebuilt bundle. The final fold is `Object.assign(window.PhoenixKitHooks,
<bundle globals…>)`, which is last-write-wins on the **hook names** each
bundle exports. So two modules with distinct globals that happen to export a
hook of the same name (e.g. both define `Chart`) will silently clobber one
another — and a bundle hook whose name matches a core PhoenixKit hook (e.g.
`RowMenu`, `SortableGrid`) overrides the core one. Namespace your hook names
(e.g. prefix them with the module name) to keep them unique across every
module and the core set.
## Example
@impl PhoenixKit.Module
def js_sources do
[%{app: :phoenix_live_gantt,
file: "static/assets/phoenix_live_gantt.js",
global: "PhoenixLiveGanttHooks"}]
end
Modules with no JS hooks skip this callback — the default is `[]`.
"""
@callback js_sources() :: [
%{
required(:app) => atom(),
required(:file) => String.t(),
required(:global) => String.t()
}
]
@doc """
Run any one-shot legacy data migrations this module owns.
Two transitions every module that touches Integrations may need:
1. **Local credentials → Integrations** — the module used to store API
keys / OAuth tokens itself; move them into a `PhoenixKit.Integrations`
row and point the module's records at that row by uuid.
2. **Name-string references → uuid references** — the module already
used Integrations but referenced rows by `provider:name` strings;
resolve those to uuids and persist the cleaner reference.
Implementations should:
- Be idempotent — safe to call on every host-app boot. Use cheap
short-circuit guards (a "completed_at" setting, "no rows need
migration" check, etc.) so repeat runs do nothing.
- Log activity (`PhoenixKit.Activity.log/1`) for every record actually
migrated, with `mode: "auto"`. Operators can audit the migration
via the activity feed.
- Never raise — wrap risky paths in `try/rescue` and return
`{:error, reason}` for the orchestrator to log. A failed migration
must not crash the host app.
- Redact PII in metadata: log uuids and resource refs, never the
decrypted API key / OAuth tokens / etc.
Default implementation returns `:ok` (modules that don't have legacy
data don't need to override this).
## Orchestration
Host apps call `PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0`
from `Application.start/2`; that walks every registered module and
invokes this callback. Per-module errors are caught + logged; the
boot doesn't fail.
"""
@callback migrate_legacy() :: :ok | {:ok, map()} | {:error, term()}
@optional_callbacks [
get_config: 0,
permission_metadata: 0,
admin_tabs: 0,
settings_tabs: 0,
user_dashboard_tabs: 0,
children: 0,
route_module: 0,
version: 0,
migration_module: 0,
required_modules: 0,
required_integrations: 0,
integration_providers: 0,
notification_types: 0,
css_sources: 0,
js_sources: 0,
migrate_legacy: 0
]
defmacro __using__(_opts) do
quote do
@behaviour PhoenixKit.Module
# Persist marker in .beam file for zero-config auto-discovery.
# Same pattern as Elixir's protocol consolidation — scannable via :beam_lib.chunks/2
# without loading the module.
Module.register_attribute(__MODULE__, :phoenix_kit_module, persist: true)
@phoenix_kit_module true
@impl PhoenixKit.Module
def get_config, do: %{enabled: enabled?()}
@impl PhoenixKit.Module
def permission_metadata, do: nil
@impl PhoenixKit.Module
def admin_tabs, do: []
@impl PhoenixKit.Module
def settings_tabs, do: []
@impl PhoenixKit.Module
def user_dashboard_tabs, do: []
@impl PhoenixKit.Module
def children, do: []
@impl PhoenixKit.Module
def route_module, do: nil
@impl PhoenixKit.Module
def version, do: "0.0.0"
@impl PhoenixKit.Module
def migration_module, do: nil
@impl PhoenixKit.Module
def required_modules, do: []
@impl PhoenixKit.Module
def required_integrations, do: []
@impl PhoenixKit.Module
def integration_providers, do: []
@impl PhoenixKit.Module
def notification_types, do: []
@impl PhoenixKit.Module
def css_sources, do: []
@impl PhoenixKit.Module
def js_sources, do: []
@impl PhoenixKit.Module
def migrate_legacy, do: :ok
defoverridable get_config: 0,
permission_metadata: 0,
admin_tabs: 0,
settings_tabs: 0,
user_dashboard_tabs: 0,
children: 0,
route_module: 0,
version: 0,
migration_module: 0,
required_modules: 0,
required_integrations: 0,
integration_providers: 0,
notification_types: 0,
css_sources: 0,
js_sources: 0,
migrate_legacy: 0
end
end
end