refactor: Rename Property/PropertyType to CustomFieldValue/CustomField
All checks were successful
continuous-integration/drone/push Build is passing

Complete refactoring of resources, database tables, code references, tests, and documentation for improved naming consistency.
This commit is contained in:
Moritz 2025-11-13 17:58:12 +01:00
parent 47f18e9ef3
commit 8400e727a7
Signed by: moritz
GPG key ID: 1020A035E5DD0824
31 changed files with 1002 additions and 647 deletions

View file

@ -1,10 +1,10 @@
defmodule MvWeb.PropertyTypeLive.Form do
defmodule MvWeb.CustomFieldLive.Form do
@moduledoc """
LiveView form for creating and editing property types (admin).
LiveView form for creating and editing custom fields (admin).
## Features
- Create new property type definitions
- Edit existing property types
- Create new custom field definitions
- Edit existing custom fields
- Select value type from supported types
- Set immutable and required flags
- Real-time validation
@ -17,7 +17,7 @@ defmodule MvWeb.PropertyTypeLive.Form do
**Optional:**
- description - Human-readable explanation
- immutable - If true, values cannot be changed after creation (default: false)
- required - If true, all members must have this property (default: false)
- required - If true, all members must have this custom field (default: false)
## Value Type Selection
- `:string` - Text data (unlimited length)
@ -28,10 +28,10 @@ defmodule MvWeb.PropertyTypeLive.Form do
## Events
- `validate` - Real-time form validation
- `save` - Submit form (create or update property type)
- `save` - Submit form (create or update custom field)
## Security
Property type management is restricted to admin users.
Custom field management is restricted to admin users.
"""
use MvWeb, :live_view
@ -42,18 +42,18 @@ defmodule MvWeb.PropertyTypeLive.Form do
<.header>
{@page_title}
<:subtitle>
{gettext("Use this form to manage property_type records in your database.")}
{gettext("Use this form to manage custom_field records in your database.")}
</:subtitle>
</.header>
<.form for={@form} id="property_type-form" phx-change="validate" phx-submit="save">
<.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label={gettext("Name")} />
<.input
field={@form[:value_type]}
type="select"
label={gettext("Value type")}
options={
Ash.Resource.Info.attribute(Mv.Membership.PropertyType, :value_type).constraints[:one_of]
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
}
/>
<.input field={@form[:description]} type="text" label={gettext("Description")} />
@ -61,9 +61,9 @@ defmodule MvWeb.PropertyTypeLive.Form do
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Property type")}
{gettext("Save Custom field")}
</.button>
<.button navigate={return_path(@return_to, @property_type)}>{gettext("Cancel")}</.button>
<.button navigate={return_path(@return_to, @custom_field)}>{gettext("Cancel")}</.button>
</.form>
</Layouts.app>
"""
@ -71,19 +71,19 @@ defmodule MvWeb.PropertyTypeLive.Form do
@impl true
def mount(params, _session, socket) do
property_type =
custom_field =
case params["id"] do
nil -> nil
id -> Ash.get!(Mv.Membership.PropertyType, id)
id -> Ash.get!(Mv.Membership.CustomField, id)
end
action = if is_nil(property_type), do: "New", else: "Edit"
page_title = action <> " " <> "Property type"
action = if is_nil(custom_field), do: "New", else: "Edit"
page_title = action <> " " <> "Custom field"
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(property_type: property_type)
|> assign(custom_field: custom_field)
|> assign(:page_title, page_title)
|> assign_form()}
end
@ -92,15 +92,15 @@ defmodule MvWeb.PropertyTypeLive.Form do
defp return_to(_), do: "index"
@impl true
def handle_event("validate", %{"property_type" => property_type_params}, socket) do
def handle_event("validate", %{"custom_field" => custom_field_params}, socket) do
{:noreply,
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, property_type_params))}
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, custom_field_params))}
end
def handle_event("save", %{"property_type" => property_type_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: property_type_params) do
{:ok, property_type} ->
notify_parent({:saved, property_type})
def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: custom_field_params) do
{:ok, custom_field} ->
notify_parent({:saved, custom_field})
action =
case socket.assigns.form.source.type do
@ -111,8 +111,8 @@ defmodule MvWeb.PropertyTypeLive.Form do
socket =
socket
|> put_flash(:info, gettext("Property type %{action} successfully", action: action))
|> push_navigate(to: return_path(socket.assigns.return_to, property_type))
|> put_flash(:info, gettext("Custom field %{action} successfully", action: action))
|> push_navigate(to: return_path(socket.assigns.return_to, custom_field))
{:noreply, socket}
@ -123,17 +123,17 @@ defmodule MvWeb.PropertyTypeLive.Form do
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{property_type: property_type}} = socket) do
defp assign_form(%{assigns: %{custom_field: custom_field}} = socket) do
form =
if property_type do
AshPhoenix.Form.for_update(property_type, :update, as: "property_type")
if custom_field do
AshPhoenix.Form.for_update(custom_field, :update, as: "custom_field")
else
AshPhoenix.Form.for_create(Mv.Membership.PropertyType, :create, as: "property_type")
AshPhoenix.Form.for_create(Mv.Membership.CustomField, :create, as: "custom_field")
end
assign(socket, form: to_form(form))
end
defp return_path("index", _property_type), do: ~p"/property_types"
defp return_path("show", property_type), do: ~p"/property_types/#{property_type.id}"
defp return_path("index", _custom_field), do: ~p"/custom_fields"
defp return_path("show", custom_field), do: ~p"/custom_fields/#{custom_field.id}"
end

View file

@ -0,0 +1,88 @@
defmodule MvWeb.CustomFieldLive.Index do
@moduledoc """
LiveView for managing custom field definitions (admin).
## Features
- List all custom fields
- Display type information (name, value type, description)
- Show immutable and required flags
- Create new custom fields
- Edit existing custom fields
- Delete custom fields (if no custom field values use them)
## Displayed Information
- Name: Unique identifier for the custom field
- Value type: Data type constraint (string, integer, boolean, date, email)
- Description: Human-readable explanation
- Immutable: Whether custom field values can be changed after creation
- Required: Whether all members must have this custom field (future feature)
## Events
- `delete` - Remove a custom field (only if no custom field values exist)
## Security
Custom field management is restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Custom fields
<:actions>
<.button variant="primary" navigate={~p"/custom_fields/new"}>
<.icon name="hero-plus" /> New Custom field
</.button>
</:actions>
</.header>
<.table
id="custom_fields"
rows={@streams.custom_fields}
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
>
<:col :let={{_id, custom_field}} label="Id">{custom_field.id}</:col>
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>
<:action :let={{_id, custom_field}}>
<div class="sr-only">
<.link navigate={~p"/custom_fields/#{custom_field}"}>Show</.link>
</div>
<.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit</.link>
</:action>
<:action :let={{id, custom_field}}>
<.link
phx-click={JS.push("delete", value: %{id: custom_field.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Custom fields")
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
custom_field = Ash.get!(Mv.Membership.CustomField, id)
Ash.destroy!(custom_field)
{:noreply, stream_delete(socket, :custom_fields, custom_field)}
end
end

View file

@ -0,0 +1,66 @@
defmodule MvWeb.CustomFieldLive.Show do
@moduledoc """
LiveView for displaying a single custom field's details (admin).
## Features
- Display custom field definition
- Show all attributes (name, value type, description, flags)
- Navigate to edit form
- Return to custom field list
## Displayed Information
- Name: Unique identifier
- Value type: Data type constraint
- Description: Optional explanation
- Immutable flag: Whether values can be changed
- Required flag: Whether all members need this custom field
## Navigation
- Back to custom field list
- Edit custom field
## Security
Custom field details are restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Custom field {@custom_field.id}
<:subtitle>This is a custom_field record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/custom_fields"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/custom_fields/#{@custom_field}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Custom field
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@custom_field.id}</:item>
<:item title="Name">{@custom_field.name}</:item>
<:item title="Description">{@custom_field.description}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Show Custom field")
|> assign(:custom_field, Ash.get!(Mv.Membership.CustomField, id))}
end
end

View file

@ -1,21 +1,21 @@
defmodule MvWeb.PropertyLive.Form do
defmodule MvWeb.CustomFieldValueLive.Form do
@moduledoc """
LiveView form for creating and editing properties.
LiveView form for creating and editing custom field values.
## Features
- Create new properties with member and type selection
- Edit existing property values
- Value input adapts to property type (string, integer, boolean, date, email)
- Create new custom field values with member and type selection
- Edit existing custom field values
- Value input adapts to custom field type (string, integer, boolean, date, email)
- Real-time validation
## Form Fields
**Required:**
- member - Select which member owns this property
- property_type - Select the type (defines value type)
- value - The actual value (input type depends on property type)
- member - Select which member owns this custom field value
- custom_field - Select the type (defines value type)
- value - The actual value (input type depends on custom field type)
## Value Types
The form dynamically renders appropriate inputs based on property type:
The form dynamically renders appropriate inputs based on custom field type:
- String: text input
- Integer: number input
- Boolean: checkbox
@ -24,10 +24,10 @@ defmodule MvWeb.PropertyLive.Form do
## Events
- `validate` - Real-time form validation
- `save` - Submit form (create or update property)
- `save` - Submit form (create or update custom field value)
## Note
Properties are typically managed through the member edit form,
Custom field values are typically managed through the member edit form,
not through this standalone form.
"""
use MvWeb, :live_view
@ -38,17 +38,19 @@ defmodule MvWeb.PropertyLive.Form do
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>{gettext("Use this form to manage property records in your database.")}</:subtitle>
<:subtitle>
{gettext("Use this form to manage custom_field_value records in your database.")}
</:subtitle>
</.header>
<.form for={@form} id="property-form" phx-change="validate" phx-submit="save">
<!-- Property Type Selection -->
<.form for={@form} id="custom_field_value-form" phx-change="validate" phx-submit="save">
<!-- Custom Field Selection -->
<.input
field={@form[:property_type_id]}
field={@form[:custom_field_id]}
type="select"
label={gettext("Property type")}
options={property_type_options(@property_types)}
prompt={gettext("Choose a property type")}
label={gettext("Custom field")}
options={custom_field_options(@custom_fields)}
prompt={gettext("Choose a custom field")}
/>
<!-- Member Selection -->
@ -61,18 +63,18 @@ defmodule MvWeb.PropertyLive.Form do
/>
<!-- Value Input - handles Union type -->
<%= if @selected_property_type do %>
<.union_value_input form={@form} property_type={@selected_property_type} />
<%= if @selected_custom_field do %>
<.union_value_input form={@form} custom_field={@selected_custom_field} />
<% else %>
<div class="text-sm text-gray-600">
{gettext("Please select a property type first")}
{gettext("Please select a custom field first")}
</div>
<% end %>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Property")}
{gettext("Save Custom field value")}
</.button>
<.button navigate={return_path(@return_to, @property)}>{gettext("Cancel")}</.button>
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
</.form>
</Layouts.app>
"""
@ -80,8 +82,8 @@ defmodule MvWeb.PropertyLive.Form do
# Helper function for Union-Value Input
defp union_value_input(assigns) do
# Extract the current value from the Property
current_value = extract_current_value(assigns.form.data, assigns.property_type.value_type)
# Extract the current value from the CustomFieldValue
current_value = extract_current_value(assigns.form.data, assigns.custom_field.value_type)
assigns = assign(assigns, :current_value, current_value)
~H"""
@ -90,7 +92,7 @@ defmodule MvWeb.PropertyLive.Form do
{gettext("Value")}
</label>
<%= case @property_type.value_type do %>
<%= case @custom_field.value_type do %>
<% :string -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="text" label="" value={@current_value} />
@ -123,16 +125,16 @@ defmodule MvWeb.PropertyLive.Form do
</.inputs_for>
<% _ -> %>
<div class="text-sm text-red-600">
{gettext("Unsupported value type: %{type}", type: @property_type.value_type)}
{gettext("Unsupported value type: %{type}", type: @custom_field.value_type)}
</div>
<% end %>
</div>
"""
end
# Helper function to extract the current value from the Property
# Helper function to extract the current value from the CustomFieldValue
defp extract_current_value(
%Mv.Membership.Property{value: %Ash.Union{value: value}},
%Mv.Membership.CustomFieldValue{value: %Ash.Union{value: value}},
_value_type
) do
value
@ -160,27 +162,27 @@ defmodule MvWeb.PropertyLive.Form do
@impl true
def mount(params, _session, socket) do
property =
custom_field_value =
case params["id"] do
nil -> nil
id -> Ash.get!(Mv.Membership.Property, id) |> Ash.load!([:property_type])
id -> Ash.get!(Mv.Membership.CustomFieldValue, id) |> Ash.load!([:custom_field])
end
action = if is_nil(property), do: "New", else: "Edit"
page_title = action <> " " <> "Property"
action = if is_nil(custom_field_value), do: "New", else: "Edit"
page_title = action <> " " <> "Custom field value"
# Load all PropertyTypes and Members for the selection fields
property_types = Ash.read!(Mv.Membership.PropertyType)
# Load all CustomFields and Members for the selection fields
custom_fields = Ash.read!(Mv.Membership.CustomField)
members = Ash.read!(Mv.Membership.Member)
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(property: property)
|> assign(custom_field_value: custom_field_value)
|> assign(:page_title, page_title)
|> assign(:property_types, property_types)
|> assign(:custom_fields, custom_fields)
|> assign(:members, members)
|> assign(:selected_property_type, property && property.property_type)
|> assign(:selected_custom_field, custom_field_value && custom_field_value.custom_field)
|> assign_form()}
end
@ -188,43 +190,43 @@ defmodule MvWeb.PropertyLive.Form do
defp return_to(_), do: "index"
@impl true
def handle_event("validate", %{"property" => property_params}, socket) do
# Find the selected PropertyType
selected_property_type =
case property_params["property_type_id"] do
def handle_event("validate", %{"custom_field_value" => custom_field_value_params}, socket) do
# Find the selected CustomField
selected_custom_field =
case custom_field_value_params["custom_field_id"] do
"" -> nil
nil -> nil
id -> Enum.find(socket.assigns.property_types, &(&1.id == id))
id -> Enum.find(socket.assigns.custom_fields, &(&1.id == id))
end
# Set the Union type based on the selected PropertyType
# Set the Union type based on the selected CustomField
updated_params =
if selected_property_type do
union_type = to_string(selected_property_type.value_type)
put_in(property_params, ["value", "_union_type"], union_type)
if selected_custom_field do
union_type = to_string(selected_custom_field.value_type)
put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else
property_params
custom_field_value_params
end
{:noreply,
socket
|> assign(:selected_property_type, selected_property_type)
|> assign(:selected_custom_field, selected_custom_field)
|> assign(form: AshPhoenix.Form.validate(socket.assigns.form, updated_params))}
end
def handle_event("save", %{"property" => property_params}, socket) do
# Set the Union type based on the selected PropertyType
def handle_event("save", %{"custom_field_value" => custom_field_value_params}, socket) do
# Set the Union type based on the selected CustomField
updated_params =
if socket.assigns.selected_property_type do
union_type = to_string(socket.assigns.selected_property_type.value_type)
put_in(property_params, ["value", "_union_type"], union_type)
if socket.assigns.selected_custom_field do
union_type = to_string(socket.assigns.selected_custom_field.value_type)
put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else
property_params
custom_field_value_params
end
case AshPhoenix.Form.submit(socket.assigns.form, params: updated_params) do
{:ok, property} ->
notify_parent({:saved, property})
{:ok, custom_field_value} ->
notify_parent({:saved, custom_field_value})
action =
case socket.assigns.form.source.type do
@ -235,8 +237,11 @@ defmodule MvWeb.PropertyLive.Form do
socket =
socket
|> put_flash(:info, gettext("Property %{action} successfully", action: action))
|> push_navigate(to: return_path(socket.assigns.return_to, property))
|> put_flash(
:info,
gettext("Custom field value %{action} successfully", action: action)
)
|> push_navigate(to: return_path(socket.assigns.return_to, custom_field_value))
{:noreply, socket}
@ -247,11 +252,11 @@ defmodule MvWeb.PropertyLive.Form do
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{property: property}} = socket) do
defp assign_form(%{assigns: %{custom_field_value: custom_field_value}} = socket) do
form =
if property do
# Determine the Union type based on the property_type
union_type = property.property_type && property.property_type.value_type
if custom_field_value do
# Determine the Union type based on the custom_field
union_type = custom_field_value.custom_field && custom_field_value.custom_field.value_type
params =
if union_type do
@ -260,20 +265,27 @@ defmodule MvWeb.PropertyLive.Form do
%{}
end
AshPhoenix.Form.for_update(property, :update, as: "property", params: params)
AshPhoenix.Form.for_update(custom_field_value, :update,
as: "custom_field_value",
params: params
)
else
AshPhoenix.Form.for_create(Mv.Membership.Property, :create, as: "property")
AshPhoenix.Form.for_create(Mv.Membership.CustomFieldValue, :create,
as: "custom_field_value"
)
end
assign(socket, form: to_form(form))
end
defp return_path("index", _property), do: ~p"/properties"
defp return_path("show", property), do: ~p"/properties/#{property.id}"
defp return_path("index", _custom_field_value), do: ~p"/custom_field_values"
defp return_path("show", custom_field_value),
do: ~p"/custom_field_values/#{custom_field_value.id}"
# Helper functions for selection options
defp property_type_options(property_types) do
Enum.map(property_types, &{&1.name, &1.id})
defp custom_field_options(custom_fields) do
Enum.map(custom_fields, &{&1.name, &1.id})
end
defp member_options(members) do

View file

@ -0,0 +1,86 @@
defmodule MvWeb.CustomFieldValueLive.Index do
@moduledoc """
LiveView for displaying and managing custom field values.
## Features
- List all custom field values with their values and types
- Show which member each custom field value belongs to
- Display custom field information
- Navigate to custom field value details and edit forms
- Delete custom field values
## Relationships
Each custom field value is linked to:
- A member (the custom field value owner)
- A custom field (defining value type and behavior)
## Events
- `delete` - Remove a custom field value from the database
## Note
Custom field values are typically managed through the member edit form.
This view provides a global overview of all custom field values.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Custom field values
<:actions>
<.button variant="primary" navigate={~p"/custom_field_values/new"}>
<.icon name="hero-plus" /> New Custom field value
</.button>
</:actions>
</.header>
<.table
id="custom_field_values"
rows={@streams.custom_field_values}
row_click={
fn {_id, custom_field_value} ->
JS.navigate(~p"/custom_field_values/#{custom_field_value}")
end
}
>
<:col :let={{_id, custom_field_value}} label="Id">{custom_field_value.id}</:col>
<:action :let={{_id, custom_field_value}}>
<div class="sr-only">
<.link navigate={~p"/custom_field_values/#{custom_field_value}"}>Show</.link>
</div>
<.link navigate={~p"/custom_field_values/#{custom_field_value}/edit"}>Edit</.link>
</:action>
<:action :let={{id, custom_field_value}}>
<.link
phx-click={JS.push("delete", value: %{id: custom_field_value.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Custom field values")
|> stream(:custom_field_values, Ash.read!(Mv.Membership.CustomFieldValue))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
custom_field_value = Ash.get!(Mv.Membership.CustomFieldValue, id)
Ash.destroy!(custom_field_value)
{:noreply, stream_delete(socket, :custom_field_values, custom_field_value)}
end
end

View file

@ -0,0 +1,67 @@
defmodule MvWeb.CustomFieldValueLive.Show do
@moduledoc """
LiveView for displaying a single custom field value's details.
## Features
- Display custom field value and type
- Show linked member
- Show custom field definition
- Navigate to edit form
- Return to custom field value list
## Displayed Information
- Custom field value (formatted based on type)
- Custom field name and description
- Member information (who owns this custom field value)
- Custom field value metadata (ID, timestamps if added)
## Navigation
- Back to custom field value list
- Edit custom field value
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Custom field value {@custom_field_value.id}
<:subtitle>This is a custom_field_value record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/custom_field_values"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/custom_field_values/#{@custom_field_value}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Custom field value
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@custom_field_value.id}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:custom_field_value, Ash.get!(Mv.Membership.CustomFieldValue, id))}
end
defp page_title(:show), do: "Show Custom field value"
defp page_title(:edit), do: "Edit Custom field value"
end

View file

@ -19,14 +19,14 @@ defmodule MvWeb.MemberLive.Form do
- paid status
- notes
## Custom Properties
Members can have dynamic custom properties defined by PropertyTypes.
The form dynamically renders inputs based on available PropertyTypes.
## Custom Field Values
Members can have dynamic custom field values defined by CustomFields.
The form dynamically renders inputs based on available CustomFields.
## Events
- `validate` - Real-time form validation
- `save` - Submit form (create or update member)
- Property management events for adding/removing custom fields
- Custom field value management events for adding/removing custom fields
"""
use MvWeb, :live_view
@ -56,10 +56,11 @@ defmodule MvWeb.MemberLive.Form do
<.input field={@form[:house_number]} label={gettext("House Number")} />
<.input field={@form[:postal_code]} label={gettext("Postal Code")} />
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
<.inputs_for :let={f_property} field={@form[:properties]}>
<% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %>
<.inputs_for :let={value_form} field={f_property[:value]}>
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
<.inputs_for :let={f_custom_field_value} field={@form[:custom_field_values]}>
<% type =
Enum.find(@custom_fields, &(&1.id == f_custom_field_value[:custom_field_id].value)) %>
<.inputs_for :let={value_form} field={f_custom_field_value[:value]}>
<% input_type =
cond do
type && type.value_type == :boolean -> "checkbox"
@ -70,8 +71,8 @@ defmodule MvWeb.MemberLive.Form do
</.inputs_for>
<input
type="hidden"
name={f_property[:property_type_id].name}
value={f_property[:property_type_id].value}
name={f_custom_field_value[:custom_field_id].name}
value={f_custom_field_value[:custom_field_id].value}
/>
</.inputs_for>
@ -86,16 +87,16 @@ defmodule MvWeb.MemberLive.Form do
@impl true
def mount(params, _session, socket) do
{:ok, property_types} = Mv.Membership.list_property_types()
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
initial_properties =
Enum.map(property_types, fn pt ->
initial_custom_field_values =
Enum.map(custom_fields, fn cf ->
%{
"property_type_id" => pt.id,
"custom_field_id" => cf.id,
"value" => %{
"type" => pt.value_type,
"type" => cf.value_type,
"value" => nil,
"_union_type" => Atom.to_string(pt.value_type)
"_union_type" => Atom.to_string(cf.value_type)
}
}
end)
@ -112,8 +113,8 @@ defmodule MvWeb.MemberLive.Form do
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(:property_types, property_types)
|> assign(:initial_properties, initial_properties)
|> assign(:custom_fields, custom_fields)
|> assign(:initial_custom_field_values, initial_custom_field_values)
|> assign(member: member)
|> assign(:page_title, page_title)
|> assign_form()}
@ -156,25 +157,25 @@ defmodule MvWeb.MemberLive.Form do
defp assign_form(%{assigns: %{member: member}} = socket) do
form =
if member do
{:ok, member} = Ash.load(member, properties: [:property_type])
{:ok, member} = Ash.load(member, custom_field_values: [:custom_field])
existing_properties =
member.properties
|> Enum.map(& &1.property_type_id)
existing_custom_field_values =
member.custom_field_values
|> Enum.map(& &1.custom_field_id)
is_missing_property = fn i ->
not Enum.member?(existing_properties, Map.get(i, "property_type_id"))
is_missing_custom_field_value = fn i ->
not Enum.member?(existing_custom_field_values, Map.get(i, "custom_field_id"))
end
params = %{
"properties" =>
Enum.map(member.properties, fn prop ->
"custom_field_values" =>
Enum.map(member.custom_field_values, fn cfv ->
%{
"property_type_id" => prop.property_type_id,
"custom_field_id" => cfv.custom_field_id,
"value" => %{
"_union_type" => Atom.to_string(prop.value.type),
"type" => prop.value.type,
"value" => prop.value.value
"_union_type" => Atom.to_string(cfv.value.type),
"type" => cfv.value.type,
"value" => cfv.value.value
}
}
end)
@ -190,12 +191,13 @@ defmodule MvWeb.MemberLive.Form do
forms: [auto?: true]
)
missing_properties = Enum.filter(socket.assigns[:initial_properties], is_missing_property)
missing_custom_field_values =
Enum.filter(socket.assigns[:initial_custom_field_values], is_missing_custom_field_value)
Enum.reduce(
missing_properties,
missing_custom_field_values,
form,
&AshPhoenix.Form.add_form(&2, [:properties], params: &1)
&AshPhoenix.Form.add_form(&2, [:custom_field_values], params: &1)
)
else
AshPhoenix.Form.for_create(
@ -203,7 +205,7 @@ defmodule MvWeb.MemberLive.Form do
:create_member,
api: Mv.Membership,
as: "member",
params: %{"properties" => socket.assigns[:initial_properties]},
params: %{"custom_field_values" => socket.assigns[:initial_custom_field_values]},
forms: [auto?: true]
)
end

View file

@ -5,7 +5,7 @@ defmodule MvWeb.MemberLive.Show do
## Features
- Display all member information (personal, contact, address)
- Show linked user account (if exists)
- Display custom properties
- Display custom field values
- Navigate to edit form
- Return to member list
@ -15,7 +15,7 @@ defmodule MvWeb.MemberLive.Show do
- Address: street, house number, postal code, city
- Status: paid flag
- Relationships: linked user account
- Custom: dynamic properties from PropertyTypes
- Custom: dynamic custom field values from CustomFields
## Navigation
- Back to member list
@ -75,14 +75,14 @@ defmodule MvWeb.MemberLive.Show do
</:item>
</.list>
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
<.generic_list items={
Enum.map(@member.properties, fn p ->
Enum.map(@member.custom_field_values, fn cfv ->
{
# name
p.property_type && p.property_type.name,
cfv.custom_field && cfv.custom_field.name,
# value
case p.value do
case cfv.value do
%{value: v} -> v
v -> v
end
@ -103,7 +103,7 @@ defmodule MvWeb.MemberLive.Show do
query =
Mv.Membership.Member
|> filter(id == ^id)
|> load([:user, properties: [:property_type]])
|> load([:user, custom_field_values: [:custom_field]])
member = Ash.read_one!(query)

View file

@ -1,82 +0,0 @@
defmodule MvWeb.PropertyLive.Index do
@moduledoc """
LiveView for displaying and managing properties.
## Features
- List all properties with their values and types
- Show which member each property belongs to
- Display property type information
- Navigate to property details and edit forms
- Delete properties
## Relationships
Each property is linked to:
- A member (the property owner)
- A property type (defining value type and behavior)
## Events
- `delete` - Remove a property from the database
## Note
Properties are typically managed through the member edit form.
This view provides a global overview of all properties.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Properties
<:actions>
<.button variant="primary" navigate={~p"/properties/new"}>
<.icon name="hero-plus" /> New Property
</.button>
</:actions>
</.header>
<.table
id="properties"
rows={@streams.properties}
row_click={fn {_id, property} -> JS.navigate(~p"/properties/#{property}") end}
>
<:col :let={{_id, property}} label="Id">{property.id}</:col>
<:action :let={{_id, property}}>
<div class="sr-only">
<.link navigate={~p"/properties/#{property}"}>Show</.link>
</div>
<.link navigate={~p"/properties/#{property}/edit"}>Edit</.link>
</:action>
<:action :let={{id, property}}>
<.link
phx-click={JS.push("delete", value: %{id: property.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Properties")
|> stream(:properties, Ash.read!(Mv.Membership.Property))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
property = Ash.get!(Mv.Membership.Property, id)
Ash.destroy!(property)
{:noreply, stream_delete(socket, :properties, property)}
end
end

View file

@ -1,64 +0,0 @@
defmodule MvWeb.PropertyLive.Show do
@moduledoc """
LiveView for displaying a single property's details.
## Features
- Display property value and type
- Show linked member
- Show property type definition
- Navigate to edit form
- Return to property list
## Displayed Information
- Property value (formatted based on type)
- Property type name and description
- Member information (who owns this property)
- Property metadata (ID, timestamps if added)
## Navigation
- Back to property list
- Edit property
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Property {@property.id}
<:subtitle>This is a property record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/properties"}>
<.icon name="hero-arrow-left" />
</.button>
<.button variant="primary" navigate={~p"/properties/#{@property}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> Edit Property
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@property.id}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:property, Ash.get!(Mv.Membership.Property, id))}
end
defp page_title(:show), do: "Show Property"
defp page_title(:edit), do: "Edit Property"
end

View file

@ -1,88 +0,0 @@
defmodule MvWeb.PropertyTypeLive.Index do
@moduledoc """
LiveView for managing property type definitions (admin).
## Features
- List all property types
- Display type information (name, value type, description)
- Show immutable and required flags
- Create new property types
- Edit existing property types
- Delete property types (if no properties use them)
## Displayed Information
- Name: Unique identifier for the property type
- Value type: Data type constraint (string, integer, boolean, date, email)
- Description: Human-readable explanation
- Immutable: Whether property values can be changed after creation
- Required: Whether all members must have this property (future feature)
## Events
- `delete` - Remove a property type (only if no properties exist)
## Security
Property type management is restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Property types
<:actions>
<.button variant="primary" navigate={~p"/property_types/new"}>
<.icon name="hero-plus" /> New Property type
</.button>
</:actions>
</.header>
<.table
id="property_types"
rows={@streams.property_types}
row_click={fn {_id, property_type} -> JS.navigate(~p"/property_types/#{property_type}") end}
>
<:col :let={{_id, property_type}} label="Id">{property_type.id}</:col>
<:col :let={{_id, property_type}} label="Name">{property_type.name}</:col>
<:col :let={{_id, property_type}} label="Description">{property_type.description}</:col>
<:action :let={{_id, property_type}}>
<div class="sr-only">
<.link navigate={~p"/property_types/#{property_type}"}>Show</.link>
</div>
<.link navigate={~p"/property_types/#{property_type}/edit"}>Edit</.link>
</:action>
<:action :let={{id, property_type}}>
<.link
phx-click={JS.push("delete", value: %{id: property_type.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Property types")
|> stream(:property_types, Ash.read!(Mv.Membership.PropertyType))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
property_type = Ash.get!(Mv.Membership.PropertyType, id)
Ash.destroy!(property_type)
{:noreply, stream_delete(socket, :property_types, property_type)}
end
end

View file

@ -1,66 +0,0 @@
defmodule MvWeb.PropertyTypeLive.Show do
@moduledoc """
LiveView for displaying a single property type's details (admin).
## Features
- Display property type definition
- Show all attributes (name, value type, description, flags)
- Navigate to edit form
- Return to property type list
## Displayed Information
- Name: Unique identifier
- Value type: Data type constraint
- Description: Optional explanation
- Immutable flag: Whether values can be changed
- Required flag: Whether all members need this property
## Navigation
- Back to property type list
- Edit property type
## Security
Property type details are restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Property type {@property_type.id}
<:subtitle>This is a property_type record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/property_types"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/property_types/#{@property_type}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Property type
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@property_type.id}</:item>
<:item title="Name">{@property_type.name}</:item>
<:item title="Description">{@property_type.description}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Show Property type")
|> assign(:property_type, Ash.get!(Mv.Membership.PropertyType, id))}
end
end

View file

@ -55,17 +55,17 @@ defmodule MvWeb.Router do
live "/members/:id", MemberLive.Show, :show
live "/members/:id/show/edit", MemberLive.Show, :edit
live "/property_types", PropertyTypeLive.Index, :index
live "/property_types/new", PropertyTypeLive.Form, :new
live "/property_types/:id/edit", PropertyTypeLive.Form, :edit
live "/property_types/:id", PropertyTypeLive.Show, :show
live "/property_types/:id/show/edit", PropertyTypeLive.Show, :edit
live "/custom_fields", CustomFieldLive.Index, :index
live "/custom_fields/new", CustomFieldLive.Form, :new
live "/custom_fields/:id/edit", CustomFieldLive.Form, :edit
live "/custom_fields/:id", CustomFieldLive.Show, :show
live "/custom_fields/:id/show/edit", CustomFieldLive.Show, :edit
live "/properties", PropertyLive.Index, :index
live "/properties/new", PropertyLive.Form, :new
live "/properties/:id/edit", PropertyLive.Form, :edit
live "/properties/:id", PropertyLive.Show, :show
live "/properties/:id/show/edit", PropertyLive.Show, :edit
live "/custom_field_values", CustomFieldValueLive.Index, :index
live "/custom_field_values/new", CustomFieldValueLive.Form, :new
live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit
live "/custom_field_values/:id", CustomFieldValueLive.Show, :show
live "/custom_field_values/:id/show/edit", CustomFieldValueLive.Show, :edit
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Form, :new