diff --git a/lib/membership/custom_field.ex b/lib/membership/custom_field.ex index e1cf397..4c84c20 100644 --- a/lib/membership/custom_field.ex +++ b/lib/membership/custom_field.ex @@ -28,10 +28,7 @@ defmodule Mv.Membership.CustomField do ## Constraints - Name must be unique across all custom fields - Name maximum length: 100 characters - - 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 + - Cannot delete a custom field that has existing custom field values (RESTRICT) ## Examples # Create a new custom field @@ -58,7 +55,7 @@ defmodule Mv.Membership.CustomField do end actions do - defaults [:read, :update] + defaults [:read, :update, :destroy] default_accept [:name, :value_type, :description, :immutable, :required] create :create do @@ -66,17 +63,6 @@ defmodule Mv.Membership.CustomField do 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 attributes do @@ -125,17 +111,6 @@ defmodule Mv.Membership.CustomField do has_many :custom_field_values, Mv.Membership.CustomFieldValue 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 identity :unique_name, [:name] identity :unique_slug, [:slug] diff --git a/lib/membership/custom_field_value.ex b/lib/membership/custom_field_value.ex index 232ba99..2d6c025 100644 --- a/lib/membership/custom_field_value.ex +++ b/lib/membership/custom_field_value.ex @@ -25,12 +25,11 @@ defmodule Mv.Membership.CustomFieldValue do ## Relationships - `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 - 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 custom field is deleted (CASCADE) - String values maximum length: 10,000 characters - Email values maximum length: 254 characters (RFC 5321) @@ -47,19 +46,12 @@ defmodule Mv.Membership.CustomFieldValue do references do reference :member, on_delete: :delete - reference :custom_field, on_delete: :delete end end actions do defaults [:create, :read, :update, :destroy] 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 attributes do diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index 7891d2e..f51c2b9 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -42,8 +42,7 @@ defmodule Mv.Membership do define :create_custom_field, action: :create define :list_custom_fields, action: :read define :update_custom_field, action: :update - define :destroy_custom_field, action: :destroy_with_values - define :prepare_custom_field_deletion, action: :prepare_deletion, args: [:id] + define :destroy_custom_field, action: :destroy end end end diff --git a/lib/mv_web/live/custom_field_live/index.ex b/lib/mv_web/live/custom_field_live/index.ex index f711323..65a3ab3 100644 --- a/lib/mv_web/live/custom_field_live/index.ex +++ b/lib/mv_web/live/custom_field_live/index.ex @@ -8,7 +8,7 @@ defmodule MvWeb.CustomFieldLive.Index do - Show immutable and required flags - Create new 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 - 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) ## Events - - `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 + - `delete` - Remove a custom field (only if no custom field values exist) ## Security Custom field management is restricted to admin users. - Deletion requires entering the custom field's slug to prevent accidental deletions. """ use MvWeb, :live_view @@ -59,76 +55,15 @@ defmodule MvWeb.CustomFieldLive.Index do <.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit - <:action :let={{_id, custom_field}}> - <.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id})}> + <:action :let={{id, custom_field}}> + <.link + phx-click={JS.push("delete", value: %{id: custom_field.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > Delete - - <%!-- Delete Confirmation Modal --%> - - - """ end @@ -138,62 +73,14 @@ defmodule MvWeb.CustomFieldLive.Index do {:ok, socket |> 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))} end @impl true - def handle_event("prepare_delete", %{"id" => id}, socket) do - custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count]) + def handle_event("delete", %{"id" => id}, socket) do + custom_field = Ash.get!(Mv.Membership.CustomField, id) + Ash.destroy!(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, "")} + {:noreply, stream_delete(socket, :custom_fields, custom_field)} end end diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 18e1053..b7f472d 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -253,7 +253,6 @@ msgid "Your password has successfully been reset" msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" #: 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/member_live/form.ex:82 #: lib/mv_web/live/user_live/form.ex:237 @@ -664,39 +663,7 @@ msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Date #: 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:" +msgstr "Automatisch generierte Kennung (unveränderlich)" #: lib/mv_web/live/user_live/form.ex:210 #, elixir-autogen, elixir-format @@ -742,3 +709,8 @@ msgstr "Mitglied entverknüpfen" #, elixir-autogen, elixir-format msgid "Unlinking scheduled" msgstr "Entverknüpfung geplant" + +#~ #: lib/mv_web/live/custom_field_live/form.ex:58 +#~ #, elixir-autogen, elixir-format +#~ msgid "Slug" +#~ msgstr "Slug" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index a87d935..75cb2b1 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -254,7 +254,6 @@ msgid "Your password has successfully been reset" msgstr "" #: 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/member_live/form.ex:82 #: lib/mv_web/live/user_live/form.ex:237 @@ -667,38 +666,6 @@ msgstr "" 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 "" - #: lib/mv_web/live/user_live/form.ex:210 #, elixir-autogen, elixir-format msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first." diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index e12b489..7cae329 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -254,7 +254,6 @@ msgid "Your password has successfully been reset" msgstr "" #: 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/member_live/form.ex:82 #: lib/mv_web/live/user_live/form.ex:237 @@ -667,38 +666,6 @@ msgstr "" 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/user_live/form.ex:210 #, elixir-autogen, elixir-format msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first." @@ -743,3 +710,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Unlinking scheduled" msgstr "" + +#~ #: lib/mv_web/live/custom_field_live/form.ex:58 +#~ #, elixir-autogen, elixir-format +#~ msgid "Slug" +#~ msgstr "" diff --git a/priv/repo/migrations/20251113183538_change_custom_field_delete_cascade.exs b/priv/repo/migrations/20251113183538_change_custom_field_delete_cascade.exs deleted file mode 100644 index 32b8037..0000000 --- a/priv/repo/migrations/20251113183538_change_custom_field_delete_cascade.exs +++ /dev/null @@ -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 diff --git a/priv/resource_snapshots/repo/custom_field_values/20251113183538.json b/priv/resource_snapshots/repo/custom_field_values/20251113183538.json deleted file mode 100644 index fc27f19..0000000 --- a/priv/resource_snapshots/repo/custom_field_values/20251113183538.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/test/accounts/user_member_linking_email_test.exs b/test/accounts/user_member_linking_email_test.exs index d7c2817..5d72ac9 100644 --- a/test/accounts/user_member_linking_email_test.exs +++ b/test/accounts/user_member_linking_email_test.exs @@ -5,7 +5,7 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do Tests for Issue #168, specifically Problem #4: Email validation bug. """ - use Mv.DataCase, async: false + use Mv.DataCase, async: true alias Mv.Accounts alias Mv.Membership diff --git a/test/membership/custom_field_deletion_test.exs b/test/membership/custom_field_deletion_test.exs deleted file mode 100644 index 50623b6..0000000 --- a/test/membership/custom_field_deletion_test.exs +++ /dev/null @@ -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 diff --git a/test/membership/member_available_for_linking_test.exs b/test/membership/member_available_for_linking_test.exs index af293e1..602fdfd 100644 --- a/test/membership/member_available_for_linking_test.exs +++ b/test/membership/member_available_for_linking_test.exs @@ -8,7 +8,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do - Special email-match logic: if user_email matches member email, only return that member - Optional search query filtering by name and email """ - use Mv.DataCase, async: false + use Mv.DataCase, async: true alias Mv.Membership describe "available_for_linking/2" do diff --git a/test/membership/member_fuzzy_search_linking_test.exs b/test/membership/member_fuzzy_search_linking_test.exs index 4cbd8d9..fcaf5fd 100644 --- a/test/membership/member_fuzzy_search_linking_test.exs +++ b/test/membership/member_fuzzy_search_linking_test.exs @@ -4,7 +4,7 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do Verifies PostgreSQL trigram matching for member search. """ - use Mv.DataCase, async: false + use Mv.DataCase, async: true alias Mv.Accounts alias Mv.Membership diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs deleted file mode 100644 index f0317e0..0000000 --- a/test/mv_web/live/custom_field_live/deletion_test.exs +++ /dev/null @@ -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