feat(custom-field): add join_description attribute for GDPR join-form labels
This commit is contained in:
parent
d51dcb1ac3
commit
b6c2cf58b1
4 changed files with 242 additions and 3 deletions
|
|
@ -12,6 +12,8 @@ defmodule Mv.Membership.CustomField do
|
||||||
- `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile")
|
- `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile")
|
||||||
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`). Immutable after creation.
|
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`). Immutable after creation.
|
||||||
- `description` - Optional human-readable description
|
- `description` - Optional human-readable description
|
||||||
|
- `join_description` - Optional label shown for this field on the public join form
|
||||||
|
(e.g., a GDPR confirmation text); supports inline external links. Falls back to `name` when nil.
|
||||||
- `required` - If true, all members must have this custom field (future feature)
|
- `required` - If true, all members must have this custom field (future feature)
|
||||||
- `show_in_overview` - If true, this custom field will be displayed in the member overview table and can be sorted
|
- `show_in_overview` - If true, this custom field will be displayed in the member overview table and can be sorted
|
||||||
|
|
||||||
|
|
@ -61,7 +63,14 @@ defmodule Mv.Membership.CustomField do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
default_accept [:name, :value_type, :description, :required, :show_in_overview]
|
default_accept [
|
||||||
|
:name,
|
||||||
|
:value_type,
|
||||||
|
:description,
|
||||||
|
:join_description,
|
||||||
|
:required,
|
||||||
|
:show_in_overview
|
||||||
|
]
|
||||||
|
|
||||||
read :read do
|
read :read do
|
||||||
primary? true
|
primary? true
|
||||||
|
|
@ -69,13 +78,13 @@ defmodule Mv.Membership.CustomField do
|
||||||
end
|
end
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
accept [:name, :value_type, :description, :required, :show_in_overview]
|
accept [:name, :value_type, :description, :join_description, :required, :show_in_overview]
|
||||||
change Mv.Membership.Changes.GenerateSlug
|
change Mv.Membership.Changes.GenerateSlug
|
||||||
validate string_length(:slug, min: 1)
|
validate string_length(:slug, min: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
accept [:name, :description, :required, :show_in_overview]
|
accept [:name, :description, :join_description, :required, :show_in_overview]
|
||||||
require_atomic? false
|
require_atomic? false
|
||||||
|
|
||||||
validate fn changeset, _context ->
|
validate fn changeset, _context ->
|
||||||
|
|
@ -139,6 +148,15 @@ defmodule Mv.Membership.CustomField do
|
||||||
trim?: true
|
trim?: true
|
||||||
]
|
]
|
||||||
|
|
||||||
|
attribute :join_description, :string,
|
||||||
|
allow_nil?: true,
|
||||||
|
public?: true,
|
||||||
|
description: "Label shown for this field on the public join form; supports external links",
|
||||||
|
constraints: [
|
||||||
|
max_length: 1000,
|
||||||
|
trim?: true
|
||||||
|
]
|
||||||
|
|
||||||
attribute :required, :boolean,
|
attribute :required, :boolean,
|
||||||
default: false,
|
default: false,
|
||||||
allow_nil?: false
|
allow_nil?: false
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Mv.Repo.Migrations.AddJoinDescriptionToCustomFields 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
|
||||||
|
alter table(:custom_fields) do
|
||||||
|
add :join_description, :text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:custom_fields) do
|
||||||
|
remove :join_description
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
145
priv/resource_snapshots/repo/custom_fields/20260603000204.json
Normal file
145
priv/resource_snapshots/repo/custom_fields/20260603000204.json
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
{
|
||||||
|
"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?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "join_description",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "false",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "required",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "true",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "show_in_overview",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"create_table_options": null,
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "2600667D140A2A846F9A848ACEFCADA1F1206950B38EF407B0BB13816E508A2A",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
|
@ -159,6 +159,61 @@ defmodule Mv.Membership.CustomFieldValidationTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "join_description" do
|
||||||
|
test "persists join_description when set", %{actor: actor} do
|
||||||
|
assert {:ok, custom_field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "dsgvo_field",
|
||||||
|
value_type: :boolean,
|
||||||
|
join_description: "hereby I confirm the GDPR"
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
assert custom_field.join_description == "hereby I confirm the GDPR"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults to nil when not given", %{actor: actor} do
|
||||||
|
assert {:ok, custom_field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "no_join_desc",
|
||||||
|
value_type: :boolean
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
assert custom_field.join_description == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects join_description longer than 1000 characters", %{actor: actor} do
|
||||||
|
assert {:error, changeset} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "too_long_join_desc",
|
||||||
|
value_type: :boolean,
|
||||||
|
join_description: String.duplicate("a", 1001)
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
assert [%{field: :join_description, message: message}] = changeset.errors
|
||||||
|
assert message =~ "max" or message =~ "length" or message =~ "1000"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is writable via the update action", %{actor: actor} do
|
||||||
|
{:ok, custom_field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "updatable", value_type: :boolean})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
custom_field
|
||||||
|
|> Ash.Changeset.for_update(:update, %{join_description: "Accept the GDPR"})
|
||||||
|
|> Ash.update(actor: actor)
|
||||||
|
|
||||||
|
assert updated.join_description == "Accept the GDPR"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "name uniqueness" do
|
describe "name uniqueness" do
|
||||||
test "rejects duplicate names", %{actor: actor} do
|
test "rejects duplicate names", %{actor: actor} do
|
||||||
assert {:ok, _} =
|
assert {:ok, _} =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue