feat: add groups resource #371
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
8e9fbe76cf
commit
6db64bf996
12 changed files with 742 additions and 18 deletions
147
lib/membership/changes/generate_slug.ex
Normal file
147
lib/membership/changes/generate_slug.ex
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
defmodule Mv.Membership.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
|
||||
|
||||
## Usage
|
||||
|
||||
Works for any resource with `name` and `slug` attributes.
|
||||
Used by CustomField and Group resources.
|
||||
|
||||
create :create do
|
||||
accept [:name, :description]
|
||||
change Mv.Membership.Changes.GenerateSlug
|
||||
validate string_length(:slug, min: 1)
|
||||
end
|
||||
|
||||
## Examples
|
||||
|
||||
# Create with automatic slug generation
|
||||
CustomField.create!(%{name: "Mobile Phone"})
|
||||
# => %CustomField{name: "Mobile Phone", slug: "mobile-phone"}
|
||||
|
||||
Group.create!(%{name: "Test Group"})
|
||||
# => %Group{name: "Test Group", slug: "test-group"}
|
||||
|
||||
# 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, URL routes).
|
||||
"""
|
||||
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
|
||||
- `_opts` - Options passed to the change (unused)
|
||||
- `_context` - Ash context map (unused)
|
||||
|
||||
## Returns
|
||||
|
||||
The changeset with the `:slug` attribute set to the generated slug.
|
||||
"""
|
||||
@impl true
|
||||
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)
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
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
|
||||
|
||||
## Parameters
|
||||
|
||||
- `name` - The string to convert to a slug
|
||||
|
||||
## Returns
|
||||
|
||||
A URL-friendly slug string, or empty string if input is invalid.
|
||||
|
||||
## 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"
|
||||
|
||||
"""
|
||||
@spec generate_slug(String.t()) :: String.t()
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue