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