Merge pull request 'Custom Fields: Handle Deletion of custom fields, closes #199' (#206) from feature/custom-field-deletion into main
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #206 Reviewed-by: carla <carla@noreply.git.local-it.org>
This commit is contained in:
commit
70b3875154
11 changed files with 939 additions and 16 deletions
|
|
@ -28,7 +28,10 @@ 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
|
||||||
- Cannot delete a custom field that has existing custom field values (RESTRICT)
|
- Deleting a custom field will cascade delete all associated custom field values
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
@ -55,7 +58,7 @@ defmodule Mv.Membership.CustomField do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read, :update, :destroy]
|
defaults [:read, :update]
|
||||||
default_accept [:name, :value_type, :description, :immutable, :required]
|
default_accept [:name, :value_type, :description, :immutable, :required]
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
|
|
@ -63,6 +66,17 @@ defmodule Mv.Membership.CustomField do
|
||||||
change Mv.Membership.CustomField.Changes.GenerateSlug
|
change Mv.Membership.CustomField.Changes.GenerateSlug
|
||||||
validate string_length(:slug, min: 1)
|
validate string_length(:slug, min: 1)
|
||||||
end
|
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
|
||||||
|
|
@ -111,6 +125,17 @@ 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]
|
identity :unique_slug, [:slug]
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@ 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
|
- `belongs_to :custom_field` - The custom field definition (CASCADE delete)
|
||||||
|
|
||||||
## 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)
|
||||||
|
|
||||||
|
|
@ -46,12 +47,19 @@ 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,7 +42,8 @@ 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
|
define :destroy_custom_field, action: :destroy_with_values
|
||||||
|
define :prepare_custom_field_deletion, action: :prepare_deletion, args: [:id]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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 (if no custom field values use them)
|
- Delete custom fields with confirmation (cascades to all custom field values)
|
||||||
|
|
||||||
## Displayed Information
|
## Displayed Information
|
||||||
- Name: Unique identifier for the custom field
|
- Name: Unique identifier for the custom field
|
||||||
|
|
@ -18,10 +18,14 @@ 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
|
||||||
- `delete` - Remove a custom field (only if no custom field values exist)
|
- `prepare_delete` - Opens deletion confirmation modal with member count
|
||||||
|
- `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
|
||||||
|
|
||||||
|
|
@ -55,15 +59,76 @@ 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
|
<.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id})}>
|
||||||
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
|
||||||
|
|
@ -73,14 +138,62 @@ 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("delete", %{"id" => id}, socket) do
|
def handle_event("prepare_delete", %{"id" => id}, socket) do
|
||||||
custom_field = Ash.get!(Mv.Membership.CustomField, id)
|
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
|
||||||
Ash.destroy!(custom_field)
|
|
||||||
|
|
||||||
{:noreply, stream_delete(socket, :custom_fields, custom_field)}
|
{:noreply,
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@ 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:67
|
||||||
|
#: 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
|
||||||
|
|
@ -659,4 +660,41 @@ msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Date
|
||||||
#: lib/mv_web/live/custom_field_live/show.ex:56
|
#: lib/mv_web/live/custom_field_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Auto-generated identifier (immutable)"
|
msgid "Auto-generated identifier (immutable)"
|
||||||
msgstr "Automatisch generierter Identifier"
|
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:"
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ 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:67
|
||||||
|
#: 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
|
||||||
|
|
@ -661,3 +662,35 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Auto-generated identifier (immutable)"
|
msgid "Auto-generated identifier (immutable)"
|
||||||
msgstr ""
|
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 ""
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ 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:67
|
||||||
|
#: 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
|
||||||
|
|
@ -661,3 +662,40 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Auto-generated identifier (immutable)"
|
msgid "Auto-generated identifier (immutable)"
|
||||||
msgstr ""
|
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 ""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
254
test/membership/custom_field_deletion_test.exs
Normal file
254
test/membership/custom_field_deletion_test.exs
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
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
|
||||||
251
test/mv_web/live/custom_field_live/deletion_test.exs
Normal file
251
test/mv_web/live/custom_field_live/deletion_test.exs
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
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