+ {gettext("Auto-generated identifier (immutable)")} +
+ + <:item title="Name">{@custom_field.name} <:item title="Description">{@custom_field.description} diff --git a/lib/mv_web/live/custom_field_value_live/form.ex b/lib/mv_web/live/custom_field_value_live/form.ex index 7df4c69..4a7b02d 100644 --- a/lib/mv_web/live/custom_field_value_live/form.ex +++ b/lib/mv_web/live/custom_field_value_live/form.ex @@ -39,7 +39,7 @@ defmodule MvWeb.CustomFieldValueLive.Form do <.header> {@page_title} <: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.")} diff --git a/mix.exs b/mix.exs index b215d59..c6e4fb5 100644 --- a/mix.exs +++ b/mix.exs @@ -75,7 +75,8 @@ defmodule Mv.MixProject do {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.14", 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 diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index f6acdca..842ab40 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -158,7 +158,7 @@ msgstr "Postleitzahl" msgid "Save Member" msgstr "Mitglied speichern" -#: lib/mv_web/live/custom_field_live/form.ex:63 +#: lib/mv_web/live/custom_field_live/form.ex:64 #: lib/mv_web/live/custom_field_value_live/form.ex:74 #: lib/mv_web/live/member_live/form.ex:79 #: lib/mv_web/live/user_live/form.ex:124 @@ -203,14 +203,14 @@ msgstr "Dies ist ein Mitglied aus deiner Datenbank." msgid "Yes" msgstr "Ja" -#: lib/mv_web/live/custom_field_live/form.ex:107 +#: lib/mv_web/live/custom_field_live/form.ex:108 #: lib/mv_web/live/custom_field_value_live/form.ex:233 #: lib/mv_web/live/member_live/form.ex:138 #, elixir-autogen, elixir-format msgid "create" msgstr "erstellt" -#: lib/mv_web/live/custom_field_live/form.ex:108 +#: lib/mv_web/live/custom_field_live/form.ex:109 #: lib/mv_web/live/custom_field_value_live/form.ex:234 #: lib/mv_web/live/member_live/form.ex:139 #, elixir-autogen, elixir-format @@ -252,7 +252,8 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt" msgid "Your password has successfully been reset" msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" -#: lib/mv_web/live/custom_field_live/form.ex:66 +#: 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:127 @@ -265,7 +266,7 @@ msgstr "Abbrechen" msgid "Choose a member" msgstr "Mitglied auswählen" -#: lib/mv_web/live/custom_field_live/form.ex:59 +#: lib/mv_web/live/custom_field_live/form.ex:60 #, elixir-autogen, elixir-format msgid "Description" msgstr "Beschreibung" @@ -285,7 +286,7 @@ msgstr "Aktiviert" msgid "ID" msgstr "ID" -#: lib/mv_web/live/custom_field_live/form.ex:60 +#: lib/mv_web/live/custom_field_live/form.ex:61 #, elixir-autogen, elixir-format msgid "Immutable" msgstr "Unveränderlich" @@ -355,7 +356,7 @@ msgstr "Passwort-Authentifizierung" msgid "Profil" msgstr "Profil" -#: lib/mv_web/live/custom_field_live/form.ex:61 +#: lib/mv_web/live/custom_field_live/form.ex:62 #, elixir-autogen, elixir-format msgid "Required" msgstr "Erforderlich" @@ -411,7 +412,7 @@ msgstr "Benutzer*in" msgid "Value" msgstr "Wert" -#: lib/mv_web/live/custom_field_live/form.ex:54 +#: lib/mv_web/live/custom_field_live/form.ex:55 #, elixir-autogen, elixir-format msgid "Value type" msgstr "Wertetyp" @@ -616,7 +617,7 @@ msgstr "Benutzerdefinierte Feldwerte" msgid "Custom field" msgstr "Benutzerdefiniertes Feld" -#: lib/mv_web/live/custom_field_live/form.ex:114 +#: lib/mv_web/live/custom_field_live/form.ex:115 #, elixir-autogen, elixir-format msgid "Custom field %{action} successfully" msgstr "Benutzerdefiniertes Feld erfolgreich %{action}" @@ -631,7 +632,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}" msgid "Please select a custom field first" msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld" -#: lib/mv_web/live/custom_field_live/form.ex:64 +#: lib/mv_web/live/custom_field_live/form.ex:65 #, elixir-autogen, elixir-format msgid "Save Custom field" msgstr "Benutzerdefiniertes Feld speichern" @@ -646,12 +647,54 @@ msgstr "Benutzerdefinierten Feldwert speichern" msgid "Use this form to manage custom_field records in your database." 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 #, elixir-autogen, elixir-format msgid "Custom Fields" 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:" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index d150a60..5942951 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -159,7 +159,7 @@ msgstr "" msgid "Save Member" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:63 +#: lib/mv_web/live/custom_field_live/form.ex:64 #: lib/mv_web/live/custom_field_value_live/form.ex:74 #: lib/mv_web/live/member_live/form.ex:79 #: lib/mv_web/live/user_live/form.ex:124 @@ -204,14 +204,14 @@ msgstr "" msgid "Yes" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:107 +#: lib/mv_web/live/custom_field_live/form.ex:108 #: lib/mv_web/live/custom_field_value_live/form.ex:233 #: lib/mv_web/live/member_live/form.ex:138 #, elixir-autogen, elixir-format msgid "create" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:108 +#: lib/mv_web/live/custom_field_live/form.ex:109 #: lib/mv_web/live/custom_field_value_live/form.ex:234 #: lib/mv_web/live/member_live/form.ex:139 #, elixir-autogen, elixir-format @@ -253,7 +253,8 @@ msgstr "" msgid "Your password has successfully been reset" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:66 +#: 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:127 @@ -266,7 +267,7 @@ msgstr "" msgid "Choose a member" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:59 +#: lib/mv_web/live/custom_field_live/form.ex:60 #, elixir-autogen, elixir-format msgid "Description" msgstr "" @@ -286,7 +287,7 @@ msgstr "" msgid "ID" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:60 +#: lib/mv_web/live/custom_field_live/form.ex:61 #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" @@ -356,7 +357,7 @@ msgstr "" msgid "Profil" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:61 +#: lib/mv_web/live/custom_field_live/form.ex:62 #, elixir-autogen, elixir-format msgid "Required" msgstr "" @@ -412,7 +413,7 @@ msgstr "" msgid "Value" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:54 +#: lib/mv_web/live/custom_field_live/form.ex:55 #, elixir-autogen, elixir-format msgid "Value type" msgstr "" @@ -617,7 +618,7 @@ msgstr "" msgid "Custom field" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:114 +#: lib/mv_web/live/custom_field_live/form.ex:115 #, elixir-autogen, elixir-format msgid "Custom field %{action} successfully" msgstr "" @@ -632,7 +633,7 @@ msgstr "" msgid "Please select a custom field first" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:64 +#: lib/mv_web/live/custom_field_live/form.ex:65 #, elixir-autogen, elixir-format msgid "Save Custom field" msgstr "" @@ -647,12 +648,49 @@ msgstr "" msgid "Use this form to manage custom_field records in your database." 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 #, elixir-autogen, elixir-format msgid "Custom Fields" 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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index df56e75..32a2d76 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -159,7 +159,7 @@ msgstr "" msgid "Save Member" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:63 +#: lib/mv_web/live/custom_field_live/form.ex:64 #: lib/mv_web/live/custom_field_value_live/form.ex:74 #: lib/mv_web/live/member_live/form.ex:79 #: lib/mv_web/live/user_live/form.ex:124 @@ -204,14 +204,14 @@ msgstr "" msgid "Yes" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:107 +#: lib/mv_web/live/custom_field_live/form.ex:108 #: lib/mv_web/live/custom_field_value_live/form.ex:233 #: lib/mv_web/live/member_live/form.ex:138 #, elixir-autogen, elixir-format msgid "create" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:108 +#: lib/mv_web/live/custom_field_live/form.ex:109 #: lib/mv_web/live/custom_field_value_live/form.ex:234 #: lib/mv_web/live/member_live/form.ex:139 #, elixir-autogen, elixir-format @@ -253,7 +253,8 @@ msgstr "" msgid "Your password has successfully been reset" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:66 +#: 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:127 @@ -266,7 +267,7 @@ msgstr "" msgid "Choose a member" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:59 +#: lib/mv_web/live/custom_field_live/form.ex:60 #, elixir-autogen, elixir-format msgid "Description" msgstr "" @@ -286,7 +287,7 @@ msgstr "" msgid "ID" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:60 +#: lib/mv_web/live/custom_field_live/form.ex:61 #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" @@ -356,7 +357,7 @@ msgstr "" msgid "Profil" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:61 +#: lib/mv_web/live/custom_field_live/form.ex:62 #, elixir-autogen, elixir-format msgid "Required" msgstr "" @@ -412,7 +413,7 @@ msgstr "" msgid "Value" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:54 +#: lib/mv_web/live/custom_field_live/form.ex:55 #, elixir-autogen, elixir-format msgid "Value type" msgstr "" @@ -617,7 +618,7 @@ msgstr "" msgid "Custom field" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:114 +#: lib/mv_web/live/custom_field_live/form.ex:115 #, elixir-autogen, elixir-format msgid "Custom field %{action} successfully" msgstr "" @@ -632,7 +633,7 @@ msgstr "" msgid "Please select a custom field first" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:64 +#: lib/mv_web/live/custom_field_live/form.ex:65 #, elixir-autogen, elixir-format msgid "Save Custom field" msgstr "" @@ -647,12 +648,54 @@ msgstr "" msgid "Use this form to manage custom_field records in your database." 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 #, elixir-autogen, elixir-format, fuzzy msgid "Custom Fields" 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 "" diff --git a/priv/repo/migrations/20251113180429_add_slug_to_custom_fields.exs b/priv/repo/migrations/20251113180429_add_slug_to_custom_fields.exs new file mode 100644 index 0000000..bebf799 --- /dev/null +++ b/priv/repo/migrations/20251113180429_add_slug_to_custom_fields.exs @@ -0,0 +1,47 @@ +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 diff --git a/priv/repo/migrations/20251113183538_change_custom_field_delete_cascade.exs b/priv/repo/migrations/20251113183538_change_custom_field_delete_cascade.exs new file mode 100644 index 0000000..32b8037 --- /dev/null +++ b/priv/repo/migrations/20251113183538_change_custom_field_delete_cascade.exs @@ -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 diff --git a/priv/resource_snapshots/repo/custom_field_values/20251113183538.json b/priv/resource_snapshots/repo/custom_field_values/20251113183538.json new file mode 100644 index 0000000..fc27f19 --- /dev/null +++ b/priv/resource_snapshots/repo/custom_field_values/20251113183538.json @@ -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" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/custom_fields/20251113180429.json b/priv/resource_snapshots/repo/custom_fields/20251113180429.json new file mode 100644 index 0000000..5a89de9 --- /dev/null +++ b/priv/resource_snapshots/repo/custom_fields/20251113180429.json @@ -0,0 +1,132 @@ +{ + "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" +} \ No newline at end of file diff --git a/test/membership/custom_field_deletion_test.exs b/test/membership/custom_field_deletion_test.exs new file mode 100644 index 0000000..50623b6 --- /dev/null +++ b/test/membership/custom_field_deletion_test.exs @@ -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 diff --git a/test/membership/custom_field_slug_test.exs b/test/membership/custom_field_slug_test.exs new file mode 100644 index 0000000..ae6c42e --- /dev/null +++ b/test/membership/custom_field_slug_test.exs @@ -0,0 +1,397 @@ +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 diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs new file mode 100644 index 0000000..f0317e0 --- /dev/null +++ b/test/mv_web/live/custom_field_live/deletion_test.exs @@ -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