Compare commits
1 commit
6f6808d2ad
...
dec7550420
| Author | SHA1 | Date | |
|---|---|---|---|
| dec7550420 |
20 changed files with 70 additions and 1741 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
// - https://dbdocs.io
|
// - https://dbdocs.io
|
||||||
// - VS Code Extensions: "DBML Language" or "dbdiagram.io"
|
// - VS Code Extensions: "DBML Language" or "dbdiagram.io"
|
||||||
//
|
//
|
||||||
// Version: 1.2
|
// Version: 1.1
|
||||||
// Last Updated: 2025-11-13
|
// Last Updated: 2025-11-13
|
||||||
|
|
||||||
Project mila_membership_management {
|
Project mila_membership_management {
|
||||||
|
|
@ -236,7 +236,6 @@ Table custom_field_values {
|
||||||
Table custom_fields {
|
Table custom_fields {
|
||||||
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
|
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
|
||||||
name text [not null, unique, note: 'CustomFieldValue name/identifier (e.g., "membership_number")']
|
name text [not null, unique, note: 'CustomFieldValue name/identifier (e.g., "membership_number")']
|
||||||
slug text [not null, unique, note: 'URL-friendly, immutable identifier (e.g., "membership-number"). Auto-generated from name.']
|
|
||||||
value_type text [not null, note: 'Data type: string | integer | boolean | date | email']
|
value_type text [not null, note: 'Data type: string | integer | boolean | date | email']
|
||||||
description text [null, note: 'Human-readable description']
|
description text [null, note: 'Human-readable description']
|
||||||
immutable boolean [not null, default: false, note: 'If true, value cannot be changed after creation']
|
immutable boolean [not null, default: false, note: 'If true, value cannot be changed after creation']
|
||||||
|
|
@ -244,7 +243,6 @@ Table custom_fields {
|
||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
name [unique, name: 'custom_fields_unique_name_index']
|
name [unique, name: 'custom_fields_unique_name_index']
|
||||||
slug [unique, name: 'custom_fields_unique_slug_index']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Note: '''
|
Note: '''
|
||||||
|
|
@ -254,32 +252,21 @@ Table custom_fields {
|
||||||
|
|
||||||
**Attributes:**
|
**Attributes:**
|
||||||
- `name`: Unique identifier for the custom field
|
- `name`: Unique identifier for the custom field
|
||||||
- `slug`: URL-friendly, human-readable identifier (auto-generated, immutable)
|
|
||||||
- `value_type`: Enforces data type consistency
|
- `value_type`: Enforces data type consistency
|
||||||
- `description`: Documentation for users/admins
|
- `description`: Documentation for users/admins
|
||||||
- `immutable`: Prevents changes after initial creation (e.g., membership numbers)
|
- `immutable`: Prevents changes after initial creation (e.g., membership numbers)
|
||||||
- `required`: Enforces that all members must have this custom field
|
- `required`: Enforces that all members must have this custom field
|
||||||
|
|
||||||
**Slug Generation:**
|
|
||||||
- Automatically generated from `name` on creation
|
|
||||||
- Immutable after creation (does not change when name is updated)
|
|
||||||
- Lowercase, spaces replaced with hyphens, special characters removed
|
|
||||||
- UTF-8 support (ä → a, ß → ss, etc.)
|
|
||||||
- Used for human-readable identifiers (CSV export/import, API, etc.)
|
|
||||||
- Examples: "Mobile Phone" → "mobile-phone", "Café Müller" → "cafe-muller"
|
|
||||||
|
|
||||||
**Constraints:**
|
**Constraints:**
|
||||||
- `value_type` must be one of: string, integer, boolean, date, email
|
- `value_type` must be one of: string, integer, boolean, date, email
|
||||||
- `name` must be unique across all custom fields
|
- `name` must be unique across all custom fields
|
||||||
- `slug` must be unique across all custom fields
|
|
||||||
- `slug` cannot be empty (validated on creation)
|
|
||||||
- Cannot be deleted if custom_field_values reference it (ON DELETE RESTRICT)
|
- Cannot be deleted if custom_field_values reference it (ON DELETE RESTRICT)
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
- Membership Number (string, immutable, required) → slug: "membership-number"
|
- Membership Number (string, immutable, required)
|
||||||
- Emergency Contact (string, mutable, optional) → slug: "emergency-contact"
|
- Emergency Contact (string, mutable, optional)
|
||||||
- Certified Trainer (boolean, mutable, optional) → slug: "certified-trainer"
|
- Certified Trainer (boolean, mutable, optional)
|
||||||
- Certification Date (date, immutable, optional) → slug: "certification-date"
|
- Certification Date (date, immutable, optional)
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ defmodule Mv.Membership.CustomField do
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
- `name` - Unique identifier for the custom field (e.g., "phone_mobile", "birthday")
|
- `name` - Unique identifier for the custom field (e.g., "phone_mobile", "birthday")
|
||||||
- `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile")
|
|
||||||
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
|
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
|
||||||
- `description` - Optional human-readable description
|
- `description` - Optional human-readable description
|
||||||
- `immutable` - If true, custom field values cannot be changed after creation
|
- `immutable` - If true, custom field values cannot be changed after creation
|
||||||
|
|
@ -28,10 +27,7 @@ defmodule Mv.Membership.CustomField do
|
||||||
## Constraints
|
## Constraints
|
||||||
- Name must be unique across all custom fields
|
- Name must be unique across all custom fields
|
||||||
- Name maximum length: 100 characters
|
- Name maximum length: 100 characters
|
||||||
- Deleting a custom field will cascade delete all associated custom field values
|
- Cannot delete a custom field that has existing custom field values (RESTRICT)
|
||||||
|
|
||||||
## Calculations
|
|
||||||
- `assigned_members_count` - Returns the number of distinct members with values for this custom field
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
# Create a new custom field
|
# Create a new custom field
|
||||||
|
|
@ -58,25 +54,8 @@ defmodule Mv.Membership.CustomField do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read, :update]
|
defaults [:create, :read, :update, :destroy]
|
||||||
default_accept [:name, :value_type, :description, :immutable, :required]
|
default_accept [:name, :value_type, :description, :immutable, :required]
|
||||||
|
|
||||||
create :create do
|
|
||||||
accept [:name, :value_type, :description, :immutable, :required]
|
|
||||||
change Mv.Membership.CustomField.Changes.GenerateSlug
|
|
||||||
validate string_length(:slug, min: 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
destroy :destroy_with_values do
|
|
||||||
primary? true
|
|
||||||
end
|
|
||||||
|
|
||||||
read :prepare_deletion do
|
|
||||||
argument :id, :uuid, allow_nil?: false
|
|
||||||
|
|
||||||
filter expr(id == ^arg(:id))
|
|
||||||
prepare build(load: [:assigned_members_count])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
|
@ -90,15 +69,6 @@ defmodule Mv.Membership.CustomField do
|
||||||
trim?: true
|
trim?: true
|
||||||
]
|
]
|
||||||
|
|
||||||
attribute :slug, :string,
|
|
||||||
allow_nil?: false,
|
|
||||||
public?: true,
|
|
||||||
writable?: false,
|
|
||||||
constraints: [
|
|
||||||
max_length: 100,
|
|
||||||
trim?: true
|
|
||||||
]
|
|
||||||
|
|
||||||
attribute :value_type, :atom,
|
attribute :value_type, :atom,
|
||||||
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
|
|
@ -125,19 +95,7 @@ defmodule Mv.Membership.CustomField do
|
||||||
has_many :custom_field_values, Mv.Membership.CustomFieldValue
|
has_many :custom_field_values, Mv.Membership.CustomFieldValue
|
||||||
end
|
end
|
||||||
|
|
||||||
calculations do
|
|
||||||
calculate :assigned_members_count,
|
|
||||||
:integer,
|
|
||||||
expr(
|
|
||||||
fragment(
|
|
||||||
"(SELECT COUNT(DISTINCT member_id) FROM custom_field_values WHERE custom_field_id = ?)",
|
|
||||||
id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
identities do
|
identities do
|
||||||
identity :unique_name, [:name]
|
identity :unique_name, [:name]
|
||||||
identity :unique_slug, [:slug]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -25,12 +25,11 @@ defmodule Mv.Membership.CustomFieldValue do
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
- `belongs_to :member` - The member this custom field value belongs to (CASCADE delete)
|
- `belongs_to :member` - The member this custom field value belongs to (CASCADE delete)
|
||||||
- `belongs_to :custom_field` - The custom field definition (CASCADE delete)
|
- `belongs_to :custom_field` - The custom field definition
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
- Each member can have only one custom field value per custom field (unique composite index)
|
- Each member can have only one custom field value per custom field (unique composite index)
|
||||||
- Custom field values are deleted when the associated member is deleted (CASCADE)
|
- Custom field values are deleted when the associated member is deleted (CASCADE)
|
||||||
- Custom field values are deleted when the associated custom field is deleted (CASCADE)
|
|
||||||
- String values maximum length: 10,000 characters
|
- String values maximum length: 10,000 characters
|
||||||
- Email values maximum length: 254 characters (RFC 5321)
|
- Email values maximum length: 254 characters (RFC 5321)
|
||||||
|
|
||||||
|
|
@ -47,19 +46,12 @@ defmodule Mv.Membership.CustomFieldValue do
|
||||||
|
|
||||||
references do
|
references do
|
||||||
reference :member, on_delete: :delete
|
reference :member, on_delete: :delete
|
||||||
reference :custom_field, on_delete: :delete
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:create, :read, :update, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
default_accept [:value, :member_id, :custom_field_id]
|
default_accept [:value, :member_id, :custom_field_id]
|
||||||
|
|
||||||
read :by_custom_field_id do
|
|
||||||
argument :custom_field_id, :uuid, allow_nil?: false
|
|
||||||
|
|
||||||
filter expr(custom_field_id == ^arg(:custom_field_id))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ defmodule Mv.Membership do
|
||||||
define :create_custom_field, action: :create
|
define :create_custom_field, action: :create
|
||||||
define :list_custom_fields, action: :read
|
define :list_custom_fields, action: :read
|
||||||
define :update_custom_field, action: :update
|
define :update_custom_field, action: :update
|
||||||
define :destroy_custom_field, action: :destroy_with_values
|
define :destroy_custom_field, action: :destroy
|
||||||
define :prepare_custom_field_deletion, action: :prepare_deletion, args: [:id]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ defmodule MvWeb.CustomFieldLive.Form do
|
||||||
|
|
||||||
<.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
|
<.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
|
||||||
<.input field={@form[:name]} type="text" label={gettext("Name")} />
|
<.input field={@form[:name]} type="text" label={gettext("Name")} />
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={@form[:value_type]}
|
field={@form[:value_type]}
|
||||||
type="select"
|
type="select"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule MvWeb.CustomFieldLive.Index do
|
||||||
- Show immutable and required flags
|
- Show immutable and required flags
|
||||||
- Create new custom fields
|
- Create new custom fields
|
||||||
- Edit existing custom fields
|
- Edit existing custom fields
|
||||||
- Delete custom fields with confirmation (cascades to all custom field values)
|
- Delete custom fields (if no custom field values use them)
|
||||||
|
|
||||||
## Displayed Information
|
## Displayed Information
|
||||||
- Name: Unique identifier for the custom field
|
- Name: Unique identifier for the custom field
|
||||||
|
|
@ -18,14 +18,10 @@ defmodule MvWeb.CustomFieldLive.Index do
|
||||||
- Required: Whether all members must have this custom field (future feature)
|
- Required: Whether all members must have this custom field (future feature)
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
- `prepare_delete` - Opens deletion confirmation modal with member count
|
- `delete` - Remove a custom field (only if no custom field values exist)
|
||||||
- `confirm_delete` - Executes deletion after slug verification
|
|
||||||
- `cancel_delete` - Cancels deletion and closes modal
|
|
||||||
- `update_slug_confirmation` - Updates slug input state
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
Custom field management is restricted to admin users.
|
Custom field management is restricted to admin users.
|
||||||
Deletion requires entering the custom field's slug to prevent accidental deletions.
|
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
|
@ -47,6 +43,8 @@ defmodule MvWeb.CustomFieldLive.Index do
|
||||||
rows={@streams.custom_fields}
|
rows={@streams.custom_fields}
|
||||||
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
|
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
|
||||||
>
|
>
|
||||||
|
<:col :let={{_id, custom_field}} label="Id">{custom_field.id}</:col>
|
||||||
|
|
||||||
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
|
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
|
||||||
|
|
||||||
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>
|
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>
|
||||||
|
|
@ -59,76 +57,15 @@ defmodule MvWeb.CustomFieldLive.Index do
|
||||||
<.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit</.link>
|
<.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit</.link>
|
||||||
</:action>
|
</:action>
|
||||||
|
|
||||||
<:action :let={{_id, custom_field}}>
|
<:action :let={{id, custom_field}}>
|
||||||
<.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id})}>
|
<.link
|
||||||
|
phx-click={JS.push("delete", value: %{id: custom_field.id}) |> hide("##{id}")}
|
||||||
|
data-confirm="Are you sure?"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</.link>
|
</.link>
|
||||||
</:action>
|
</:action>
|
||||||
</.table>
|
</.table>
|
||||||
|
|
||||||
<%!-- Delete Confirmation Modal --%>
|
|
||||||
<dialog :if={@show_delete_modal} id="delete-custom-field-modal" class="modal modal-open">
|
|
||||||
<div class="modal-box">
|
|
||||||
<h3 class="font-bold text-lg">{gettext("Delete Custom Field")}</h3>
|
|
||||||
|
|
||||||
<div class="py-4 space-y-4">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
|
|
||||||
<div>
|
|
||||||
<p class="font-semibold">
|
|
||||||
{ngettext(
|
|
||||||
"%{count} member has a value assigned for this custom field.",
|
|
||||||
"%{count} members have values assigned for this custom field.",
|
|
||||||
@custom_field_to_delete.assigned_members_count,
|
|
||||||
count: @custom_field_to_delete.assigned_members_count
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm mt-2">
|
|
||||||
{gettext(
|
|
||||||
"All custom field values will be permanently deleted when you delete this custom field."
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="slug-confirmation" class="label">
|
|
||||||
<span class="label-text">
|
|
||||||
{gettext("To confirm deletion, please enter this text:")}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="font-mono font-bold text-lg mb-2 p-2 bg-base-200 rounded break-all">
|
|
||||||
{@custom_field_to_delete.slug}
|
|
||||||
</div>
|
|
||||||
<form phx-change="update_slug_confirmation">
|
|
||||||
<input
|
|
||||||
id="slug-confirmation"
|
|
||||||
name="slug"
|
|
||||||
type="text"
|
|
||||||
value={@slug_confirmation}
|
|
||||||
placeholder={gettext("Enter the text above to confirm")}
|
|
||||||
autocomplete="off"
|
|
||||||
phx-mounted={JS.focus()}
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<button phx-click="cancel_delete" class="btn">
|
|
||||||
{gettext("Cancel")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
phx-click="confirm_delete"
|
|
||||||
class="btn btn-error"
|
|
||||||
disabled={@slug_confirmation != @custom_field_to_delete.slug}
|
|
||||||
>
|
|
||||||
{gettext("Delete Custom Field and All Values")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
@ -138,62 +75,14 @@ defmodule MvWeb.CustomFieldLive.Index do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Listing Custom fields")
|
|> assign(:page_title, "Listing Custom fields")
|
||||||
|> assign(:show_delete_modal, false)
|
|
||||||
|> assign(:custom_field_to_delete, nil)
|
|
||||||
|> assign(:slug_confirmation, "")
|
|
||||||
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField))}
|
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField))}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("prepare_delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
|
custom_field = Ash.get!(Mv.Membership.CustomField, id)
|
||||||
|
Ash.destroy!(custom_field)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply, stream_delete(socket, :custom_fields, custom_field)}
|
||||||
socket
|
|
||||||
|> assign(:custom_field_to_delete, custom_field)
|
|
||||||
|> assign(:show_delete_modal, true)
|
|
||||||
|> assign(:slug_confirmation, "")}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("update_slug_confirmation", %{"slug" => slug}, socket) do
|
|
||||||
{:noreply, assign(socket, :slug_confirmation, slug)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("confirm_delete", _params, socket) do
|
|
||||||
custom_field = socket.assigns.custom_field_to_delete
|
|
||||||
|
|
||||||
if socket.assigns.slug_confirmation == custom_field.slug do
|
|
||||||
# Delete the custom field (CASCADE will handle custom field values)
|
|
||||||
case Ash.destroy(custom_field) do
|
|
||||||
:ok ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, "Custom field deleted successfully")
|
|
||||||
|> assign(:show_delete_modal, false)
|
|
||||||
|> assign(:custom_field_to_delete, nil)
|
|
||||||
|> assign(:slug_confirmation, "")
|
|
||||||
|> stream_delete(:custom_fields, custom_field)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:error, "Failed to delete custom field: #{inspect(error)}")}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:error, "Slug does not match. Deletion cancelled.")}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("cancel_delete", _params, socket) do
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> assign(:show_delete_modal, false)
|
|
||||||
|> assign(:custom_field_to_delete, nil)
|
|
||||||
|> assign(:slug_confirmation, "")}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ defmodule MvWeb.CustomFieldLive.Show do
|
||||||
- Return to custom field list
|
- Return to custom field list
|
||||||
|
|
||||||
## Displayed Information
|
## Displayed Information
|
||||||
- ID: Internal UUID identifier
|
|
||||||
- Slug: URL-friendly identifier (auto-generated, immutable)
|
|
||||||
- Name: Unique identifier
|
- Name: Unique identifier
|
||||||
- Value type: Data type constraint
|
- Value type: Data type constraint
|
||||||
- Description: Optional explanation
|
- Description: Optional explanation
|
||||||
|
|
@ -31,7 +29,7 @@ defmodule MvWeb.CustomFieldLive.Show do
|
||||||
~H"""
|
~H"""
|
||||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||||
<.header>
|
<.header>
|
||||||
Custom field {@custom_field.slug}
|
Custom field {@custom_field.id}
|
||||||
<:subtitle>This is a custom_field record from your database.</:subtitle>
|
<:subtitle>This is a custom_field record from your database.</:subtitle>
|
||||||
|
|
||||||
<:actions>
|
<:actions>
|
||||||
|
|
@ -50,13 +48,6 @@ defmodule MvWeb.CustomFieldLive.Show do
|
||||||
<.list>
|
<.list>
|
||||||
<:item title="Id">{@custom_field.id}</:item>
|
<:item title="Id">{@custom_field.id}</:item>
|
||||||
|
|
||||||
<:item title="Slug">
|
|
||||||
{@custom_field.slug}
|
|
||||||
<p class="mt-2 text-sm leading-6 text-zinc-600">
|
|
||||||
{gettext("Auto-generated identifier (immutable)")}
|
|
||||||
</p>
|
|
||||||
</:item>
|
|
||||||
|
|
||||||
<:item title="Name">{@custom_field.name}</:item>
|
<:item title="Name">{@custom_field.name}</:item>
|
||||||
|
|
||||||
<:item title="Description">{@custom_field.description}</:item>
|
<:item title="Description">{@custom_field.description}</:item>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ defmodule MvWeb.CustomFieldValueLive.Form do
|
||||||
<.header>
|
<.header>
|
||||||
{@page_title}
|
{@page_title}
|
||||||
<:subtitle>
|
<:subtitle>
|
||||||
{gettext("Use this form to manage Custom Field Value records in your database.")}
|
{gettext("Use this form to manage custom_field_value records in your database.")}
|
||||||
</:subtitle>
|
</:subtitle>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
|
|
||||||
3
mix.exs
3
mix.exs
|
|
@ -75,8 +75,7 @@ defmodule Mv.MixProject do
|
||||||
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
|
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
|
||||||
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:ecto_commons, "~> 0.3"},
|
{:ecto_commons, "~> 0.3"}
|
||||||
{:slugify, "~> 1.3"}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ msgstr "Postleitzahl"
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr "Mitglied speichern"
|
msgstr "Mitglied speichern"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
#: lib/mv_web/live/custom_field_live/form.ex:63
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#: lib/mv_web/live/user_live/form.ex:124
|
#: lib/mv_web/live/user_live/form.ex:124
|
||||||
|
|
@ -203,14 +203,14 @@ msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:108
|
#: lib/mv_web/live/custom_field_live/form.ex:107
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr "erstellt"
|
msgstr "erstellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:109
|
#: lib/mv_web/live/custom_field_live/form.ex:108
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:139
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -252,8 +252,7 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
|
||||||
msgid "Your password has successfully been reset"
|
msgid "Your password has successfully been reset"
|
||||||
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:82
|
||||||
#: lib/mv_web/live/user_live/form.ex:127
|
#: lib/mv_web/live/user_live/form.ex:127
|
||||||
|
|
@ -266,7 +265,7 @@ msgstr "Abbrechen"
|
||||||
msgid "Choose a member"
|
msgid "Choose a member"
|
||||||
msgstr "Mitglied auswählen"
|
msgstr "Mitglied auswählen"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:60
|
#: lib/mv_web/live/custom_field_live/form.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Beschreibung"
|
msgstr "Beschreibung"
|
||||||
|
|
@ -286,7 +285,7 @@ msgstr "Aktiviert"
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr "ID"
|
msgstr "ID"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
#: lib/mv_web/live/custom_field_live/form.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr "Unveränderlich"
|
msgstr "Unveränderlich"
|
||||||
|
|
@ -356,7 +355,7 @@ msgstr "Passwort-Authentifizierung"
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr "Profil"
|
msgstr "Profil"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr "Erforderlich"
|
msgstr "Erforderlich"
|
||||||
|
|
@ -412,7 +411,7 @@ msgstr "Benutzer*in"
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr "Wert"
|
msgstr "Wert"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:55
|
#: lib/mv_web/live/custom_field_live/form.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type"
|
msgid "Value type"
|
||||||
msgstr "Wertetyp"
|
msgstr "Wertetyp"
|
||||||
|
|
@ -617,7 +616,7 @@ msgstr "Benutzerdefinierte Feldwerte"
|
||||||
msgid "Custom field"
|
msgid "Custom field"
|
||||||
msgstr "Benutzerdefiniertes Feld"
|
msgstr "Benutzerdefiniertes Feld"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:115
|
#: lib/mv_web/live/custom_field_live/form.ex:114
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom field %{action} successfully"
|
msgid "Custom field %{action} successfully"
|
||||||
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
|
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
|
||||||
|
|
@ -632,7 +631,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:65
|
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Custom field"
|
msgid "Save Custom field"
|
||||||
msgstr "Benutzerdefiniertes Feld speichern"
|
msgstr "Benutzerdefiniertes Feld speichern"
|
||||||
|
|
@ -647,54 +646,12 @@ msgstr "Benutzerdefinierten Feldwert speichern"
|
||||||
msgid "Use this form to manage custom_field records in your database."
|
msgid "Use this form to manage custom_field records in your database."
|
||||||
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
|
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/custom_field_value_live/form.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Use this form to manage custom_field_value records in your database."
|
||||||
|
msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:20
|
#: lib/mv_web/components/layouts/navbar.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr "Benutzerdefinierte Felder"
|
msgstr "Benutzerdefinierte Felder"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:42
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Use this form to manage Custom Field Value records in your database."
|
|
||||||
msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/show.ex:56
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Auto-generated identifier (immutable)"
|
|
||||||
msgstr "Automatisch generierter Bezeichner (unveränderlich)"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:79
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "%{count} member has a value assigned for this custom field."
|
|
||||||
msgid_plural "%{count} members have values assigned for this custom field."
|
|
||||||
msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen."
|
|
||||||
msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen."
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:87
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "All custom field values will be permanently deleted when you delete this custom field."
|
|
||||||
msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht."
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:72
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Delete Custom Field"
|
|
||||||
msgstr "Benutzerdefiniertes Feld löschen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:127
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Delete Custom Field and All Values"
|
|
||||||
msgstr "Benutzerdefiniertes Feld und alle Werte löschen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:109
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Enter the text above to confirm"
|
|
||||||
msgstr "Obigen Text zur Bestätigung eingeben"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:97
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "To confirm deletion, please enter this text:"
|
|
||||||
msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:"
|
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
|
|
||||||
#~ #, elixir-autogen, elixir-format
|
|
||||||
#~ msgid "To confirm deletion, please enter the custom field slug:"
|
|
||||||
#~ msgstr "Um die Löschung zu bestätigen, gib bitte den Slug des benutzerdefinierten Feldes ein:"
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ msgstr ""
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
#: lib/mv_web/live/custom_field_live/form.ex:63
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#: lib/mv_web/live/user_live/form.ex:124
|
#: lib/mv_web/live/user_live/form.ex:124
|
||||||
|
|
@ -204,14 +204,14 @@ msgstr ""
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:108
|
#: lib/mv_web/live/custom_field_live/form.ex:107
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:109
|
#: lib/mv_web/live/custom_field_live/form.ex:108
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:139
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -253,8 +253,7 @@ msgstr ""
|
||||||
msgid "Your password has successfully been reset"
|
msgid "Your password has successfully been reset"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:82
|
||||||
#: lib/mv_web/live/user_live/form.ex:127
|
#: lib/mv_web/live/user_live/form.ex:127
|
||||||
|
|
@ -267,7 +266,7 @@ msgstr ""
|
||||||
msgid "Choose a member"
|
msgid "Choose a member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:60
|
#: lib/mv_web/live/custom_field_live/form.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -287,7 +286,7 @@ msgstr ""
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
#: lib/mv_web/live/custom_field_live/form.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -357,7 +356,7 @@ msgstr ""
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -413,7 +412,7 @@ msgstr ""
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:55
|
#: lib/mv_web/live/custom_field_live/form.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type"
|
msgid "Value type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -618,7 +617,7 @@ msgstr ""
|
||||||
msgid "Custom field"
|
msgid "Custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:115
|
#: lib/mv_web/live/custom_field_live/form.ex:114
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom field %{action} successfully"
|
msgid "Custom field %{action} successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -633,7 +632,7 @@ msgstr ""
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:65
|
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Custom field"
|
msgid "Save Custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -648,49 +647,12 @@ msgstr ""
|
||||||
msgid "Use this form to manage custom_field records in your database."
|
msgid "Use this form to manage custom_field records in your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/custom_field_value_live/form.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Use this form to manage custom_field_value records in your database."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:20
|
#: lib/mv_web/components/layouts/navbar.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:42
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Use this form to manage Custom Field Value records in your database."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/show.ex:56
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Auto-generated identifier (immutable)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:79
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "%{count} member has a value assigned for this custom field."
|
|
||||||
msgid_plural "%{count} members have values assigned for this custom field."
|
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:87
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "All custom field values will be permanently deleted when you delete this custom field."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:72
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Delete Custom Field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:127
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Delete Custom Field and All Values"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:109
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Enter the text above to confirm"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:97
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "To confirm deletion, please enter this text:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ msgstr ""
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
#: lib/mv_web/live/custom_field_live/form.ex:63
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#: lib/mv_web/live/user_live/form.ex:124
|
#: lib/mv_web/live/user_live/form.ex:124
|
||||||
|
|
@ -204,14 +204,14 @@ msgstr ""
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:108
|
#: lib/mv_web/live/custom_field_live/form.ex:107
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:109
|
#: lib/mv_web/live/custom_field_live/form.ex:108
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:139
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -253,8 +253,7 @@ msgstr ""
|
||||||
msgid "Your password has successfully been reset"
|
msgid "Your password has successfully been reset"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:82
|
||||||
#: lib/mv_web/live/user_live/form.ex:127
|
#: lib/mv_web/live/user_live/form.ex:127
|
||||||
|
|
@ -267,7 +266,7 @@ msgstr ""
|
||||||
msgid "Choose a member"
|
msgid "Choose a member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:60
|
#: lib/mv_web/live/custom_field_live/form.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -287,7 +286,7 @@ msgstr ""
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
#: lib/mv_web/live/custom_field_live/form.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -357,7 +356,7 @@ msgstr ""
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -413,7 +412,7 @@ msgstr ""
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:55
|
#: lib/mv_web/live/custom_field_live/form.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type"
|
msgid "Value type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -618,7 +617,7 @@ msgstr ""
|
||||||
msgid "Custom field"
|
msgid "Custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:115
|
#: lib/mv_web/live/custom_field_live/form.ex:114
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom field %{action} successfully"
|
msgid "Custom field %{action} successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -633,7 +632,7 @@ msgstr ""
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:65
|
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Custom field"
|
msgid "Save Custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -648,54 +647,12 @@ msgstr ""
|
||||||
msgid "Use this form to manage custom_field records in your database."
|
msgid "Use this form to manage custom_field records in your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/custom_field_value_live/form.ex:42
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Use this form to manage custom_field_value records in your database."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:20
|
#: lib/mv_web/components/layouts/navbar.ex:20
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:42
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Use this form to manage Custom Field Value records in your database."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/show.ex:56
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Auto-generated identifier (immutable)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:79
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "%{count} member has a value assigned for this custom field."
|
|
||||||
msgid_plural "%{count} members have values assigned for this custom field."
|
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:87
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "All custom field values will be permanently deleted when you delete this custom field."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:72
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Delete Custom Field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:127
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Delete Custom Field and All Values"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:109
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Enter the text above to confirm"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:97
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "To confirm deletion, please enter this text:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
|
|
||||||
#~ #, elixir-autogen, elixir-format
|
|
||||||
#~ msgid "To confirm deletion, please enter the custom field slug:"
|
|
||||||
#~ msgstr ""
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
defmodule Mv.Repo.Migrations.AddSlugToCustomFields do
|
|
||||||
@moduledoc """
|
|
||||||
Updates resources based on their most recent snapshots.
|
|
||||||
|
|
||||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def up do
|
|
||||||
# Step 1: Add slug column as nullable first
|
|
||||||
alter table(:custom_fields) do
|
|
||||||
add :slug, :text, null: true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Step 2: Generate slugs for existing custom fields
|
|
||||||
execute("""
|
|
||||||
UPDATE custom_fields
|
|
||||||
SET slug = lower(
|
|
||||||
regexp_replace(
|
|
||||||
regexp_replace(
|
|
||||||
regexp_replace(name, '[^a-zA-Z0-9\\s-]', '', 'g'),
|
|
||||||
'\\s+', '-', 'g'
|
|
||||||
),
|
|
||||||
'-+', '-', 'g'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE slug IS NULL
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Step 3: Make slug NOT NULL
|
|
||||||
alter table(:custom_fields) do
|
|
||||||
modify :slug, :text, null: false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Step 4: Create unique index
|
|
||||||
create unique_index(:custom_fields, [:slug], name: "custom_fields_unique_slug_index")
|
|
||||||
end
|
|
||||||
|
|
||||||
def down do
|
|
||||||
drop_if_exists unique_index(:custom_fields, [:slug], name: "custom_fields_unique_slug_index")
|
|
||||||
|
|
||||||
alter table(:custom_fields) do
|
|
||||||
remove :slug
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
defmodule Mv.Repo.Migrations.ChangeCustomFieldDeleteCascade do
|
|
||||||
@moduledoc """
|
|
||||||
Updates resources based on their most recent snapshots.
|
|
||||||
|
|
||||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def up do
|
|
||||||
drop constraint(:custom_field_values, "custom_field_values_custom_field_id_fkey")
|
|
||||||
|
|
||||||
alter table(:custom_field_values) do
|
|
||||||
modify :custom_field_id,
|
|
||||||
references(:custom_fields,
|
|
||||||
column: :id,
|
|
||||||
name: "custom_field_values_custom_field_id_fkey",
|
|
||||||
type: :uuid,
|
|
||||||
prefix: "public",
|
|
||||||
on_delete: :delete_all
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def down do
|
|
||||||
drop constraint(:custom_field_values, "custom_field_values_custom_field_id_fkey")
|
|
||||||
|
|
||||||
alter table(:custom_field_values) do
|
|
||||||
modify :custom_field_id,
|
|
||||||
references(:custom_fields,
|
|
||||||
column: :id,
|
|
||||||
name: "custom_field_values_custom_field_id_fkey",
|
|
||||||
type: :uuid,
|
|
||||||
prefix: "public"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
{
|
|
||||||
"attributes": [
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "fragment(\"gen_random_uuid()\")",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": true,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "id",
|
|
||||||
"type": "uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "value",
|
|
||||||
"type": "map"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": {
|
|
||||||
"deferrable": false,
|
|
||||||
"destination_attribute": "id",
|
|
||||||
"destination_attribute_default": null,
|
|
||||||
"destination_attribute_generated": null,
|
|
||||||
"index?": false,
|
|
||||||
"match_type": null,
|
|
||||||
"match_with": null,
|
|
||||||
"multitenancy": {
|
|
||||||
"attribute": null,
|
|
||||||
"global": null,
|
|
||||||
"strategy": null
|
|
||||||
},
|
|
||||||
"name": "custom_field_values_member_id_fkey",
|
|
||||||
"on_delete": "delete",
|
|
||||||
"on_update": null,
|
|
||||||
"primary_key?": true,
|
|
||||||
"schema": "public",
|
|
||||||
"table": "members"
|
|
||||||
},
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "member_id",
|
|
||||||
"type": "uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": {
|
|
||||||
"deferrable": false,
|
|
||||||
"destination_attribute": "id",
|
|
||||||
"destination_attribute_default": null,
|
|
||||||
"destination_attribute_generated": null,
|
|
||||||
"index?": false,
|
|
||||||
"match_type": null,
|
|
||||||
"match_with": null,
|
|
||||||
"multitenancy": {
|
|
||||||
"attribute": null,
|
|
||||||
"global": null,
|
|
||||||
"strategy": null
|
|
||||||
},
|
|
||||||
"name": "custom_field_values_custom_field_id_fkey",
|
|
||||||
"on_delete": "delete",
|
|
||||||
"on_update": null,
|
|
||||||
"primary_key?": true,
|
|
||||||
"schema": "public",
|
|
||||||
"table": "custom_fields"
|
|
||||||
},
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "custom_field_id",
|
|
||||||
"type": "uuid"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"base_filter": null,
|
|
||||||
"check_constraints": [],
|
|
||||||
"custom_indexes": [],
|
|
||||||
"custom_statements": [],
|
|
||||||
"has_create_action": true,
|
|
||||||
"hash": "BDEC02A7F12B14AB65FBA1A4BD834D291E3BEC61D065473D51BBE453486512ED",
|
|
||||||
"identities": [
|
|
||||||
{
|
|
||||||
"all_tenants?": false,
|
|
||||||
"base_filter": null,
|
|
||||||
"index_name": "custom_field_values_unique_custom_field_per_member_index",
|
|
||||||
"keys": [
|
|
||||||
{
|
|
||||||
"type": "atom",
|
|
||||||
"value": "member_id"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "atom",
|
|
||||||
"value": "custom_field_id"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "unique_custom_field_per_member",
|
|
||||||
"nils_distinct?": true,
|
|
||||||
"where": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"multitenancy": {
|
|
||||||
"attribute": null,
|
|
||||||
"global": null,
|
|
||||||
"strategy": null
|
|
||||||
},
|
|
||||||
"repo": "Elixir.Mv.Repo",
|
|
||||||
"schema": null,
|
|
||||||
"table": "custom_field_values"
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
{
|
|
||||||
"attributes": [
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "fragment(\"gen_random_uuid()\")",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": true,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "id",
|
|
||||||
"type": "uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "name",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "slug",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "value_type",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "description",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "false",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "immutable",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "false",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "required",
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"base_filter": null,
|
|
||||||
"check_constraints": [],
|
|
||||||
"custom_indexes": [],
|
|
||||||
"custom_statements": [],
|
|
||||||
"has_create_action": true,
|
|
||||||
"hash": "DB1D3D9F2F76F518CAEEA2CC855996CCD87FC4C8FDD3A37345CEF2980674D8F3",
|
|
||||||
"identities": [
|
|
||||||
{
|
|
||||||
"all_tenants?": false,
|
|
||||||
"base_filter": null,
|
|
||||||
"index_name": "custom_fields_unique_name_index",
|
|
||||||
"keys": [
|
|
||||||
{
|
|
||||||
"type": "atom",
|
|
||||||
"value": "name"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "unique_name",
|
|
||||||
"nils_distinct?": true,
|
|
||||||
"where": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"all_tenants?": false,
|
|
||||||
"base_filter": null,
|
|
||||||
"index_name": "custom_fields_unique_slug_index",
|
|
||||||
"keys": [
|
|
||||||
{
|
|
||||||
"type": "atom",
|
|
||||||
"value": "slug"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "unique_slug",
|
|
||||||
"nils_distinct?": true,
|
|
||||||
"where": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"multitenancy": {
|
|
||||||
"attribute": null,
|
|
||||||
"global": null,
|
|
||||||
"strategy": null
|
|
||||||
},
|
|
||||||
"repo": "Elixir.Mv.Repo",
|
|
||||||
"schema": null,
|
|
||||||
"table": "custom_fields"
|
|
||||||
}
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
defmodule Mv.Membership.CustomFieldDeletionTest do
|
|
||||||
@moduledoc """
|
|
||||||
Tests for CustomField deletion with CASCADE behavior.
|
|
||||||
|
|
||||||
Tests cover:
|
|
||||||
- Deletion of custom fields without assigned values
|
|
||||||
- Deletion of custom fields with assigned values (CASCADE)
|
|
||||||
- assigned_members_count calculation
|
|
||||||
- prepare_deletion action with count loading
|
|
||||||
- CASCADE deletion only affects specific custom field values
|
|
||||||
"""
|
|
||||||
use Mv.DataCase, async: true
|
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
|
||||||
|
|
||||||
describe "assigned_members_count calculation" do
|
|
||||||
test "returns 0 for custom field without any values" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "test_field",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
|
||||||
assert custom_field_with_count.assigned_members_count == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns correct count for custom field with one member" do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, _custom_field_value} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
|
||||||
assert custom_field_with_count.assigned_members_count == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns correct count for custom field with multiple members" do
|
|
||||||
{:ok, member1} = create_member()
|
|
||||||
{:ok, member2} = create_member()
|
|
||||||
{:ok, member3} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
# Create custom field value for each member
|
|
||||||
for member <- [member1, member2, member3] do
|
|
||||||
{:ok, _} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
end
|
|
||||||
|
|
||||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
|
||||||
assert custom_field_with_count.assigned_members_count == 3
|
|
||||||
end
|
|
||||||
|
|
||||||
test "counts distinct members (not multiple values per member)" do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
# Create custom field value for member
|
|
||||||
{:ok, _} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
custom_field_with_count = Ash.load!(custom_field, :assigned_members_count)
|
|
||||||
|
|
||||||
# Should still be 1, not 2, even if we tried to create multiple (which would fail due to uniqueness)
|
|
||||||
assert custom_field_with_count.assigned_members_count == 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "prepare_deletion action" do
|
|
||||||
test "loads assigned_members_count for deletion preparation" do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, _} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Use prepare_deletion action
|
|
||||||
[prepared_custom_field] =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Query.for_read(:prepare_deletion, %{id: custom_field.id})
|
|
||||||
|> Ash.read!()
|
|
||||||
|
|
||||||
assert prepared_custom_field.assigned_members_count == 1
|
|
||||||
assert prepared_custom_field.id == custom_field.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns empty list for non-existent custom field" do
|
|
||||||
non_existent_id = Ash.UUID.generate()
|
|
||||||
|
|
||||||
result =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Query.for_read(:prepare_deletion, %{id: non_existent_id})
|
|
||||||
|> Ash.read!()
|
|
||||||
|
|
||||||
assert result == []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "destroy_with_values action" do
|
|
||||||
test "deletes custom field without any values" do
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
assert :ok = Ash.destroy(custom_field)
|
|
||||||
|
|
||||||
# Verify custom field is deleted
|
|
||||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes custom field and cascades to all its values" do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, custom_field_value} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Delete custom field
|
|
||||||
assert :ok = Ash.destroy(custom_field)
|
|
||||||
|
|
||||||
# Verify custom field is deleted
|
|
||||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
|
||||||
|
|
||||||
# Verify custom field value is also deleted (CASCADE)
|
|
||||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
|
||||||
|
|
||||||
# Verify member still exists
|
|
||||||
assert {:ok, _} = Ash.get(Member, member.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes only values of the specific custom field" do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field1} = create_custom_field("field1", :string)
|
|
||||||
{:ok, custom_field2} = create_custom_field("field2", :string)
|
|
||||||
|
|
||||||
# Create value for custom_field1
|
|
||||||
{:ok, value1} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field1.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "value1"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Create value for custom_field2
|
|
||||||
{:ok, value2} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field2.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "value2"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Delete custom_field1
|
|
||||||
assert :ok = Ash.destroy(custom_field1)
|
|
||||||
|
|
||||||
# Verify custom_field1 and value1 are deleted
|
|
||||||
assert {:error, _} = Ash.get(CustomField, custom_field1.id)
|
|
||||||
assert {:error, _} = Ash.get(CustomFieldValue, value1.id)
|
|
||||||
|
|
||||||
# Verify custom_field2 and value2 still exist
|
|
||||||
assert {:ok, _} = Ash.get(CustomField, custom_field2.id)
|
|
||||||
assert {:ok, _} = Ash.get(CustomFieldValue, value2.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes custom field with values from multiple members" do
|
|
||||||
{:ok, member1} = create_member()
|
|
||||||
{:ok, member2} = create_member()
|
|
||||||
{:ok, member3} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
# Create value for each member
|
|
||||||
values =
|
|
||||||
for member <- [member1, member2, member3] do
|
|
||||||
{:ok, value} =
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => "test"}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
value
|
|
||||||
end
|
|
||||||
|
|
||||||
# Delete custom field
|
|
||||||
assert :ok = Ash.destroy(custom_field)
|
|
||||||
|
|
||||||
# Verify all values are deleted
|
|
||||||
for value <- values do
|
|
||||||
assert {:error, _} = Ash.get(CustomFieldValue, value.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Verify all members still exist
|
|
||||||
for member <- [member1, member2, member3] do
|
|
||||||
assert {:ok, _} = Ash.get(Member, member.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper functions
|
|
||||||
defp create_member do
|
|
||||||
Member
|
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
|
||||||
first_name: "Test",
|
|
||||||
last_name: "User#{System.unique_integer([:positive])}",
|
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_custom_field(name, value_type) do
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
|
||||||
value_type: value_type
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,397 +0,0 @@
|
||||||
defmodule Mv.Membership.CustomFieldSlugTest do
|
|
||||||
@moduledoc """
|
|
||||||
Tests for automatic slug generation on CustomField resource.
|
|
||||||
|
|
||||||
This test suite verifies:
|
|
||||||
1. Slugs are automatically generated from the name attribute
|
|
||||||
2. Slugs are unique (cannot have duplicates)
|
|
||||||
3. Slugs are immutable (don't change when name changes)
|
|
||||||
4. Slugs handle various edge cases (unicode, special chars, etc.)
|
|
||||||
5. Slugs can be used for lookups
|
|
||||||
"""
|
|
||||||
use Mv.DataCase, async: true
|
|
||||||
|
|
||||||
alias Mv.Membership.CustomField
|
|
||||||
|
|
||||||
describe "automatic slug generation on create" do
|
|
||||||
test "generates slug from name with simple ASCII text" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Mobile Phone",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "mobile-phone"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "generates slug from name with German umlauts" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Café Müller",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "cafe-muller"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "generates slug with lowercase conversion" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "TEST NAME",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "test-name"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "generates slug by removing special characters" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "E-Mail & Address!",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "e-mail-address"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "generates slug by replacing multiple spaces with single hyphen" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Multiple Spaces",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "multiple-spaces"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "trims leading and trailing hyphens" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "-Test-",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "test"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles unicode characters properly (ß becomes ss)" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Straße",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "strasse"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "slug uniqueness" do
|
|
||||||
test "prevents creating custom field with duplicate slug" do
|
|
||||||
# Create first custom field
|
|
||||||
{:ok, _custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Attempt to create second custom field with same slug (different case in name)
|
|
||||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "test",
|
|
||||||
value_type: :integer
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert Exception.message(error) =~ "has already been taken"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "allows custom fields with different slugs" do
|
|
||||||
{:ok, custom_field1} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test One",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
{:ok, custom_field2} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test Two",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field1.slug == "test-one"
|
|
||||||
assert custom_field2.slug == "test-two"
|
|
||||||
assert custom_field1.slug != custom_field2.slug
|
|
||||||
end
|
|
||||||
|
|
||||||
test "prevents duplicate slugs when names differ only in special characters" do
|
|
||||||
{:ok, custom_field1} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test!!!",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field1.slug == "test"
|
|
||||||
|
|
||||||
# Second custom field with name that generates the same slug should fail
|
|
||||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test???",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Should fail with uniqueness constraint error
|
|
||||||
assert Exception.message(error) =~ "has already been taken"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "slug immutability" do
|
|
||||||
test "slug cannot be manually set on create" do
|
|
||||||
# Attempting to set slug manually should fail because slug is not writable
|
|
||||||
result =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test",
|
|
||||||
value_type: :string,
|
|
||||||
slug: "custom-slug"
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Should fail because slug is not an accepted input
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} = result
|
|
||||||
assert Exception.message(elem(result, 1)) =~ "No such input"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "slug does not change when name is updated" do
|
|
||||||
# Create custom field
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Original Name",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
original_slug = custom_field.slug
|
|
||||||
assert original_slug == "original-name"
|
|
||||||
|
|
||||||
# Update the name
|
|
||||||
{:ok, updated_custom_field} =
|
|
||||||
custom_field
|
|
||||||
|> Ash.Changeset.for_update(:update, %{
|
|
||||||
name: "New Different Name"
|
|
||||||
})
|
|
||||||
|> Ash.update()
|
|
||||||
|
|
||||||
# Slug should remain unchanged
|
|
||||||
assert updated_custom_field.slug == original_slug
|
|
||||||
assert updated_custom_field.slug == "original-name"
|
|
||||||
assert updated_custom_field.name == "New Different Name"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "slug cannot be manually updated" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
original_slug = custom_field.slug
|
|
||||||
assert original_slug == "test"
|
|
||||||
|
|
||||||
# Attempt to manually update slug should fail because slug is not writable
|
|
||||||
result =
|
|
||||||
custom_field
|
|
||||||
|> Ash.Changeset.for_update(:update, %{
|
|
||||||
slug: "new-slug"
|
|
||||||
})
|
|
||||||
|> Ash.update()
|
|
||||||
|
|
||||||
# Should fail because slug is not an accepted input
|
|
||||||
assert {:error, %Ash.Error.Invalid{}} = result
|
|
||||||
assert Exception.message(elem(result, 1)) =~ "No such input"
|
|
||||||
|
|
||||||
# Reload to verify slug hasn't changed
|
|
||||||
reloaded = Ash.get!(CustomField, custom_field.id)
|
|
||||||
assert reloaded.slug == "test"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "slug edge cases" do
|
|
||||||
test "handles very long names by truncating slug" do
|
|
||||||
# Create a name at the maximum length (100 chars)
|
|
||||||
long_name = String.duplicate("abcdefghij", 10)
|
|
||||||
# 100 characters exactly
|
|
||||||
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: long_name,
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Slug should be truncated to maximum 100 characters
|
|
||||||
assert String.length(custom_field.slug) <= 100
|
|
||||||
# Should be the full slugified version since name is exactly 100 chars
|
|
||||||
assert custom_field.slug == long_name
|
|
||||||
end
|
|
||||||
|
|
||||||
test "rejects name with only special characters" do
|
|
||||||
# When name contains only special characters, slug would be empty
|
|
||||||
# This should fail validation
|
|
||||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "!!!",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Should fail because slug would be empty
|
|
||||||
error_message = Exception.message(error)
|
|
||||||
assert error_message =~ "Slug cannot be empty" or error_message =~ "is required"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles mixed special characters and text" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test@#$%Name",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# slugify keeps the hyphen between words
|
|
||||||
assert custom_field.slug == "test-name"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles numbers in name" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Field 123 Test",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
assert custom_field.slug == "field-123-test"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles consecutive hyphens in name" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test---Name",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Should reduce multiple hyphens to single hyphen
|
|
||||||
assert custom_field.slug == "test-name"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles name with dots and underscores" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "test.field_name",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Dots and underscores should be handled (either kept or converted)
|
|
||||||
assert custom_field.slug =~ ~r/^[a-z0-9-]+$/
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "slug in queries and responses" do
|
|
||||||
test "slug is included in struct after create" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Slug should be present in the struct
|
|
||||||
assert Map.has_key?(custom_field, :slug)
|
|
||||||
assert custom_field.slug != nil
|
|
||||||
end
|
|
||||||
|
|
||||||
test "can load custom field and slug is present" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# Load it back
|
|
||||||
loaded_custom_field = Ash.get!(CustomField, custom_field.id)
|
|
||||||
|
|
||||||
assert loaded_custom_field.slug == "test"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "slug is returned in list queries" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
custom_fields = Ash.read!(CustomField)
|
|
||||||
|
|
||||||
found = Enum.find(custom_fields, &(&1.id == custom_field.id))
|
|
||||||
assert found.slug == "test"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "slug-based lookup (future feature)" do
|
|
||||||
@tag :skip
|
|
||||||
test "can find custom field by slug" do
|
|
||||||
{:ok, custom_field} =
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "Test Field",
|
|
||||||
value_type: :string
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
# This test is for future implementation
|
|
||||||
# We might add a custom action like :by_slug
|
|
||||||
found = Ash.get!(CustomField, custom_field.slug, load: [:slug])
|
|
||||||
assert found.id == custom_field.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,251 +0,0 @@
|
||||||
defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|
||||||
@moduledoc """
|
|
||||||
Tests for CustomFieldLive.Index deletion modal and slug confirmation.
|
|
||||||
|
|
||||||
Tests cover:
|
|
||||||
- Opening deletion confirmation modal
|
|
||||||
- Displaying correct member count
|
|
||||||
- Slug confirmation input
|
|
||||||
- Successful deletion with correct slug
|
|
||||||
- Failed deletion with incorrect slug
|
|
||||||
- Canceling deletion
|
|
||||||
- Button states (enabled/disabled based on slug match)
|
|
||||||
"""
|
|
||||||
use MvWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
|
|
||||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
|
||||||
|
|
||||||
setup do
|
|
||||||
# Create admin user for testing
|
|
||||||
{:ok, user} =
|
|
||||||
Mv.Accounts.User
|
|
||||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
|
||||||
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
|
||||||
password: "testpassword123"
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
|
|
||||||
conn = log_in_user(build_conn(), user)
|
|
||||||
%{conn: conn, user: user}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "delete button and modal" do
|
|
||||||
test "opens modal with correct member count when delete is clicked", %{conn: conn} do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
# Create custom field value
|
|
||||||
create_custom_field_value(member, custom_field, "test")
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
# Click delete button
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Modal should be visible
|
|
||||||
assert has_element?(view, "#delete-custom-field-modal")
|
|
||||||
|
|
||||||
# Should show correct member count (1 member)
|
|
||||||
assert render(view) =~ "1 member has a value assigned for this custom field"
|
|
||||||
|
|
||||||
# Should show the slug
|
|
||||||
assert render(view) =~ custom_field.slug
|
|
||||||
end
|
|
||||||
|
|
||||||
test "shows correct plural form for multiple members", %{conn: conn} do
|
|
||||||
{:ok, member1} = create_member()
|
|
||||||
{:ok, member2} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
# Create values for both members
|
|
||||||
create_custom_field_value(member1, custom_field, "test1")
|
|
||||||
create_custom_field_value(member2, custom_field, "test2")
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Should show plural form
|
|
||||||
assert render(view) =~ "2 members have values assigned for this custom field"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "shows 0 members for custom field without values", %{conn: conn} do
|
|
||||||
{:ok, _custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Should show 0 members
|
|
||||||
assert render(view) =~ "0 members have values assigned for this custom field"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "slug confirmation input" do
|
|
||||||
test "updates confirmation state when typing", %{conn: conn} do
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Type in slug input
|
|
||||||
view
|
|
||||||
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug})
|
|
||||||
|
|
||||||
# Confirm button should be enabled now (no disabled attribute)
|
|
||||||
html = render(view)
|
|
||||||
refute html =~ ~r/disabled(?:=""|(?!\w))/
|
|
||||||
end
|
|
||||||
|
|
||||||
test "delete button is disabled when slug doesn't match", %{conn: conn} do
|
|
||||||
{:ok, _custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Type wrong slug
|
|
||||||
view
|
|
||||||
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"})
|
|
||||||
|
|
||||||
# Button should be disabled
|
|
||||||
html = render(view)
|
|
||||||
assert html =~ ~r/disabled(?:=""|(?!\w))/
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "confirm deletion" do
|
|
||||||
test "successfully deletes custom field with correct slug", %{conn: conn} do
|
|
||||||
{:ok, member} = create_member()
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
{:ok, custom_field_value} = create_custom_field_value(member, custom_field, "test")
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
# Open modal
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Enter correct slug
|
|
||||||
view
|
|
||||||
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug})
|
|
||||||
|
|
||||||
# Click confirm
|
|
||||||
view
|
|
||||||
|> element("button", "Delete Custom Field and All Values")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Should show success message
|
|
||||||
assert render(view) =~ "Custom field deleted successfully"
|
|
||||||
|
|
||||||
# Custom field should be gone from database
|
|
||||||
assert {:error, _} = Ash.get(CustomField, custom_field.id)
|
|
||||||
|
|
||||||
# Custom field value should also be gone (CASCADE)
|
|
||||||
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
|
|
||||||
|
|
||||||
# Member should still exist
|
|
||||||
assert {:ok, _} = Ash.get(Member, member.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "shows error when slug doesn't match", %{conn: conn} do
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Enter wrong slug
|
|
||||||
view
|
|
||||||
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"})
|
|
||||||
|
|
||||||
# Try to confirm (button should be disabled, but test the handler anyway)
|
|
||||||
view
|
|
||||||
|> render_click("confirm_delete", %{})
|
|
||||||
|
|
||||||
# Should show error message
|
|
||||||
assert render(view) =~ "Slug does not match"
|
|
||||||
|
|
||||||
# Custom field should still exist
|
|
||||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "cancel deletion" do
|
|
||||||
test "closes modal without deleting", %{conn: conn} do
|
|
||||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("a", "Delete")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Modal should be visible
|
|
||||||
assert has_element?(view, "#delete-custom-field-modal")
|
|
||||||
|
|
||||||
# Click cancel
|
|
||||||
view
|
|
||||||
|> element("button", "Cancel")
|
|
||||||
|> render_click()
|
|
||||||
|
|
||||||
# Modal should be gone
|
|
||||||
refute has_element?(view, "#delete-custom-field-modal")
|
|
||||||
|
|
||||||
# Custom field should still exist
|
|
||||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper functions
|
|
||||||
defp create_member do
|
|
||||||
Member
|
|
||||||
|> Ash.Changeset.for_create(:create_member, %{
|
|
||||||
first_name: "Test",
|
|
||||||
last_name: "User#{System.unique_integer([:positive])}",
|
|
||||||
email: "test#{System.unique_integer([:positive])}@example.com"
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_custom_field(name, value_type) do
|
|
||||||
CustomField
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
name: "#{name}_#{System.unique_integer([:positive])}",
|
|
||||||
value_type: value_type
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_custom_field_value(member, custom_field, value) do
|
|
||||||
CustomFieldValue
|
|
||||||
|> Ash.Changeset.for_create(:create, %{
|
|
||||||
member_id: member.id,
|
|
||||||
custom_field_id: custom_field.id,
|
|
||||||
value: %{"_union_type" => "string", "_union_value" => value}
|
|
||||||
})
|
|
||||||
|> Ash.create()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp log_in_user(conn, user) do
|
|
||||||
conn
|
|
||||||
|> Phoenix.ConnTest.init_test_session(%{})
|
|
||||||
|> AshAuthentication.Plug.Helpers.store_in_session(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue