118 lines
3.3 KiB
Elixir
118 lines
3.3 KiB
Elixir
defmodule Mv.Membership.CustomField.Changes.GenerateSlug do
|
|
@moduledoc """
|
|
Ash Change that automatically generates a URL-friendly slug from the `name` attribute.
|
|
|
|
## Behavior
|
|
|
|
- **On Create**: Generates a slug from the name attribute using slugify
|
|
- **On Update**: Slug remains unchanged (immutable after creation)
|
|
- **Slug Generation**: Uses the `slugify` library to convert name to slug
|
|
- Converts to lowercase
|
|
- Replaces spaces with hyphens
|
|
- Removes special characters
|
|
- Handles UTF-8 characters (e.g., ä → a, ß → ss)
|
|
- Trims leading/trailing hyphens
|
|
- Truncates to max 100 characters
|
|
|
|
## Examples
|
|
|
|
# Create with automatic slug generation
|
|
CustomField.create!(%{name: "Mobile Phone"})
|
|
# => %CustomField{name: "Mobile Phone", slug: "mobile-phone"}
|
|
|
|
# German umlauts are converted
|
|
CustomField.create!(%{name: "Café Müller"})
|
|
# => %CustomField{name: "Café Müller", slug: "cafe-muller"}
|
|
|
|
# Slug is immutable on update
|
|
custom_field = CustomField.create!(%{name: "Original"})
|
|
CustomField.update!(custom_field, %{name: "New Name"})
|
|
# => %CustomField{name: "New Name", slug: "original"} # slug unchanged!
|
|
|
|
## Implementation Note
|
|
|
|
This change only runs on `:create` actions. The slug is immutable by design,
|
|
as changing slugs would break external references (e.g., CSV imports/exports).
|
|
"""
|
|
use Ash.Resource.Change
|
|
|
|
@doc """
|
|
Generates a slug from the changeset's `name` attribute.
|
|
|
|
Only runs on create actions. Returns the changeset unchanged if:
|
|
- The action is not :create
|
|
- The name is not being changed
|
|
- The name is nil or empty
|
|
|
|
## Parameters
|
|
|
|
- `changeset` - The Ash changeset
|
|
|
|
## Returns
|
|
|
|
The changeset with the `:slug` attribute set to the generated slug.
|
|
"""
|
|
def change(changeset, _opts, _context) do
|
|
# Only generate slug on create, not on update (immutability)
|
|
if changeset.action_type == :create do
|
|
case Ash.Changeset.get_attribute(changeset, :name) do
|
|
nil ->
|
|
changeset
|
|
|
|
name when is_binary(name) ->
|
|
slug = generate_slug(name)
|
|
Ash.Changeset.force_change_attribute(changeset, :slug, slug)
|
|
end
|
|
else
|
|
# On update, don't touch the slug (immutable)
|
|
changeset
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generates a URL-friendly slug from a given string.
|
|
|
|
Uses the `slugify` library to create a clean, lowercase slug with:
|
|
- Spaces replaced by hyphens
|
|
- Special characters removed
|
|
- UTF-8 characters transliterated (ä → a, ß → ss, etc.)
|
|
- Multiple consecutive hyphens reduced to single hyphen
|
|
- Leading/trailing hyphens removed
|
|
- Maximum length of 100 characters
|
|
|
|
## Examples
|
|
|
|
iex> generate_slug("Mobile Phone")
|
|
"mobile-phone"
|
|
|
|
iex> generate_slug("Café Müller")
|
|
"cafe-muller"
|
|
|
|
iex> generate_slug("TEST NAME")
|
|
"test-name"
|
|
|
|
iex> generate_slug("E-Mail & Address!")
|
|
"e-mail-address"
|
|
|
|
iex> generate_slug("Multiple Spaces")
|
|
"multiple-spaces"
|
|
|
|
iex> generate_slug("-Test-")
|
|
"test"
|
|
|
|
iex> generate_slug("Straße")
|
|
"strasse"
|
|
|
|
"""
|
|
def generate_slug(name) when is_binary(name) do
|
|
slug = Slug.slugify(name)
|
|
|
|
case slug do
|
|
nil -> ""
|
|
"" -> ""
|
|
slug when is_binary(slug) -> String.slice(slug, 0, 100)
|
|
end
|
|
end
|
|
|
|
def generate_slug(_), do: ""
|
|
end
|