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,18 +1,18 @@
defmodule Mv.Membership.PropertyType do
defmodule Mv.Membership.CustomField do
@moduledoc """
Ash resource defining the schema for custom member properties.
Ash resource defining the schema for custom member fields.
## Overview
PropertyTypes define the "schema" for custom fields in the membership system.
Each PropertyType specifies the name, data type, and behavior of a custom field
that can be attached to members via Property resources.
CustomFields define the "schema" for custom fields in the membership system.
Each CustomField specifies the name, data type, and behavior of a custom field
that can be attached to members via CustomFieldValue resources.
## Attributes
- `name` - Unique identifier for the property (e.g., "phone_mobile", "birthday")
- `name` - Unique identifier for the custom field (e.g., "phone_mobile", "birthday")
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
- `description` - Optional human-readable description
- `immutable` - If true, property values cannot be changed after creation
- `required` - If true, all members must have this property (future feature)
- `immutable` - If true, custom field values cannot be changed after creation
- `required` - If true, all members must have this custom field (future feature)
## Supported Value Types
- `:string` - Text data (unlimited length)
@ -22,22 +22,22 @@ defmodule Mv.Membership.PropertyType do
- `:email` - Validated email addresses
## Relationships
- `has_many :properties` - All property values of this type
- `has_many :custom_field_values` - All custom field values of this type
## Constraints
- Name must be unique across all property types
- Cannot delete a property type that has existing property values (RESTRICT)
- Name must be unique across all custom fields
- Cannot delete a custom field that has existing custom field values (RESTRICT)
## Examples
# Create a new property type
PropertyType.create!(%{
# Create a new custom field
CustomField.create!(%{
name: "phone_mobile",
value_type: :string,
description: "Mobile phone number"
})
# Create a required property type
PropertyType.create!(%{
# Create a required custom field
CustomField.create!(%{
name: "emergency_contact",
value_type: :string,
required: true
@ -48,7 +48,7 @@ defmodule Mv.Membership.PropertyType do
data_layer: AshPostgres.DataLayer
postgres do
table "property_types"
table "custom_fields"
repo Mv.Repo
end
@ -65,7 +65,7 @@ defmodule Mv.Membership.PropertyType do
attribute :value_type, :atom,
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
allow_nil?: false,
description: "Defines the datatype `Property.value` is interpreted as"
description: "Defines the datatype `CustomFieldValue.value` is interpreted as"
attribute :description, :string, allow_nil?: true, public?: true
@ -79,7 +79,7 @@ defmodule Mv.Membership.PropertyType do
end
relationships do
has_many :properties, Mv.Membership.Property
has_many :custom_field_values, Mv.Membership.CustomFieldValue
end
identities do

View file

@ -1,11 +1,11 @@
defmodule Mv.Membership.Property do
defmodule Mv.Membership.CustomFieldValue do
@moduledoc """
Ash resource representing a custom property value for a member.
Ash resource representing a custom field value for a member.
## Overview
Properties implement the Entity-Attribute-Value (EAV) pattern, allowing
dynamic custom fields to be attached to members. Each property links a
member to a property type and stores the actual value.
CustomFieldValues implement the Entity-Attribute-Value (EAV) pattern, allowing
dynamic custom fields to be attached to members. Each custom field value links a
member to a custom field and stores the actual value.
## Value Storage
Values are stored using Ash's union type with JSONB storage format:
@ -24,19 +24,19 @@ defmodule Mv.Membership.Property do
- `:email` - Validated email addresses (custom type)
## Relationships
- `belongs_to :member` - The member this property belongs to (CASCADE delete)
- `belongs_to :property_type` - The property type definition
- `belongs_to :member` - The member this custom field value belongs to (CASCADE delete)
- `belongs_to :custom_field` - The custom field definition
## Constraints
- Each member can have only one property per property type (unique composite index)
- Properties are deleted when the associated member is deleted (CASCADE)
- Each member can have only one custom field value per custom field (unique composite index)
- Custom field values are deleted when the associated member is deleted (CASCADE)
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
table "properties"
table "custom_field_values"
repo Mv.Repo
references do
@ -46,7 +46,7 @@ defmodule Mv.Membership.Property do
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:value, :member_id, :property_type_id]
default_accept [:value, :member_id, :custom_field_id]
end
attributes do
@ -68,16 +68,16 @@ defmodule Mv.Membership.Property do
relationships do
belongs_to :member, Mv.Membership.Member
belongs_to :property_type, Mv.Membership.PropertyType
belongs_to :custom_field, Mv.Membership.CustomField
end
calculations do
calculate :value_to_string, :string, expr(value[:value] <> "")
end
# Ensure a member can only have one property per property type
# For example: A member can have only one "email" property, one "phone" property, etc.
# Ensure a member can only have one custom field value per custom field
# For example: A member can have only one "phone" custom field value, one "email" custom field value, etc.
identities do
identity :unique_property_per_member, [:member_id, :property_type_id]
identity :unique_custom_field_per_member, [:member_id, :custom_field_id]
end
end

View file

@ -4,7 +4,7 @@ defmodule Mv.Membership.Email do
## Overview
This type extends `:string` with email-specific validation constraints.
It ensures that email values stored in Property resources are valid email
It ensures that email values stored in CustomFieldValue resources are valid email
addresses according to a standard regex pattern.
## Validation Rules
@ -14,12 +14,12 @@ defmodule Mv.Membership.Email do
- Automatic trimming of leading/trailing whitespace
## Usage
This type is used in the Property union type for properties with
`value_type: :email` in PropertyType definitions.
This type is used in the CustomFieldValue union type for custom fields with
`value_type: :email` in CustomField definitions.
## Example
# In a property type definition
PropertyType.create!(%{
# In a custom field definition
CustomField.create!(%{
name: "work_email",
value_type: :email
})

View file

@ -7,7 +7,7 @@ defmodule Mv.Membership.Member do
can have:
- Personal information (name, email, phone, address)
- Optional link to a User account (1:1 relationship)
- Dynamic custom properties via PropertyType system
- Dynamic custom field values via CustomField system
- Full-text searchable profile
## Email Synchronization
@ -16,7 +16,7 @@ defmodule Mv.Membership.Member do
See `Mv.EmailSync` for details.
## Relationships
- `has_many :properties` - Dynamic custom fields
- `has_many :custom_field_values` - Dynamic custom fields
- `has_one :user` - Optional authentication account link
## Validations
@ -48,8 +48,8 @@ defmodule Mv.Membership.Member do
create :create_member do
primary? true
# Properties can be created along with member
argument :properties, {:array, :map}
# Custom field values can be created along with member
argument :custom_field_values, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
@ -70,7 +70,7 @@ defmodule Mv.Membership.Member do
:postal_code
]
change manage_relationship(:properties, type: :create)
change manage_relationship(:custom_field_values, type: :create)
# Manage the user relationship during member creation
change manage_relationship(:user, :user,
@ -95,8 +95,8 @@ defmodule Mv.Membership.Member do
primary? true
# Required because custom validation function cannot be done atomically
require_atomic? false
# Properties can be updated or created along with member
argument :properties, {:array, :map}
# Custom field values can be updated or created along with member
argument :custom_field_values, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
@ -117,7 +117,7 @@ defmodule Mv.Membership.Member do
:postal_code
]
change manage_relationship(:properties, on_match: :update, on_no_match: :create)
change manage_relationship(:custom_field_values, on_match: :update, on_no_match: :create)
# Manage the user relationship during member update
change manage_relationship(:user, :user,
@ -349,7 +349,7 @@ defmodule Mv.Membership.Member do
end
relationships do
has_many :properties, Mv.Membership.Property
has_many :custom_field_values, Mv.Membership.CustomFieldValue
# 1:1 relationship - Member can optionally have one User
# This references the User's member_id attribute
# The relationship is optional (allow_nil? true by default)

View file

@ -3,15 +3,15 @@ defmodule Mv.Membership do
Ash Domain for membership management.
## Resources
- `Member` - Club members with personal information and custom properties
- `Property` - Dynamic custom field values attached to members
- `PropertyType` - Schema definitions for custom properties
- `Member` - Club members with personal information and custom field values
- `CustomFieldValue` - Dynamic custom field values attached to members
- `CustomField` - Schema definitions for custom fields
## Public API
The domain exposes these main actions:
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
- Property management: `create_property/1`, `list_property/0`, etc.
- PropertyType management: `create_property_type/1`, `list_property_types/0`, etc.
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, etc.
## Admin Interface
The domain is configured with AshAdmin for management UI.
@ -31,18 +31,18 @@ defmodule Mv.Membership do
define :destroy_member, action: :destroy
end
resource Mv.Membership.Property do
define :create_property, action: :create
define :list_property, action: :read
define :update_property, action: :update
define :destroy_property, action: :destroy
resource Mv.Membership.CustomFieldValue do
define :create_custom_field_value, action: :create
define :list_custom_field_values, action: :read
define :update_custom_field_value, action: :update
define :destroy_custom_field_value, action: :destroy
end
resource Mv.Membership.PropertyType do
define :create_property_type, action: :create
define :list_property_types, action: :read
define :update_property_type, action: :update
define :destroy_property_type, action: :destroy
resource Mv.Membership.CustomField do
define :create_custom_field, action: :create
define :list_custom_fields, action: :read
define :update_custom_field, action: :update
define :destroy_custom_field, action: :destroy
end
end
end

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