259 lines
8.5 KiB
Elixir
259 lines
8.5 KiB
Elixir
defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|
@moduledoc """
|
|
LiveComponent for managing custom field definitions (embedded in settings).
|
|
|
|
## 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 with confirmation (cascades to all custom field values)
|
|
"""
|
|
use MvWeb, :live_component
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<div id={@id}>
|
|
<.header>
|
|
{gettext("Custom Fields")}
|
|
<:subtitle>
|
|
{gettext("These will appear in addition to other data when adding new members.")}
|
|
</:subtitle>
|
|
<:actions>
|
|
<.button variant="primary" phx-click="new_custom_field" phx-target={@myself}>
|
|
<.icon name="hero-plus" /> {gettext("New Custom field")}
|
|
</.button>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<%!-- Show form when creating or editing --%>
|
|
<div :if={@show_form} class="mb-8">
|
|
<.live_component
|
|
module={MvWeb.CustomFieldLive.FormComponent}
|
|
id={@form_id}
|
|
custom_field={@editing_custom_field}
|
|
on_save={fn custom_field, action -> send(self(), {:custom_field_saved, custom_field, action}) end}
|
|
on_cancel={fn -> send_update(__MODULE__, id: @id, show_form: false) end}
|
|
/>
|
|
</div>
|
|
|
|
<%!-- Hide table when form is visible --%>
|
|
<.table
|
|
:if={!@show_form}
|
|
id="custom_fields"
|
|
rows={@streams.custom_fields}
|
|
row_click={
|
|
fn {_id, custom_field} ->
|
|
JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself)
|
|
end
|
|
}
|
|
>
|
|
<:col :let={{_id, custom_field}} label={gettext("Name")}>{custom_field.name}</:col>
|
|
|
|
<:col :let={{_id, custom_field}} label={gettext("Value Type")}>
|
|
{custom_field.value_type}
|
|
</:col>
|
|
|
|
<:col :let={{_id, custom_field}} label={gettext("Description")}>
|
|
{custom_field.description}
|
|
</:col>
|
|
|
|
<:col :let={{_id, custom_field}} label={gettext("Show in Overview")}>
|
|
<span :if={custom_field.show_in_overview} class="badge badge-success">
|
|
{gettext("Yes")}
|
|
</span>
|
|
<span :if={!custom_field.show_in_overview} class="badge badge-ghost">
|
|
{gettext("No")}
|
|
</span>
|
|
</:col>
|
|
|
|
<:action :let={{_id, custom_field}}>
|
|
<.link phx-click={
|
|
JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself)
|
|
}>
|
|
{gettext("Edit")}
|
|
</.link>
|
|
</:action>
|
|
|
|
<:action :let={{_id, custom_field}}>
|
|
<.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id}, target: @myself)}>
|
|
{gettext("Delete")}
|
|
</.link>
|
|
</:action>
|
|
</.table>
|
|
|
|
<%!-- Delete Confirmation Modal --%>
|
|
<dialog :if={@show_delete_modal} id="delete-custom-field-modal" class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="text-lg font-bold">{gettext("Delete Custom Field")}</h3>
|
|
|
|
<div class="py-4 space-y-4">
|
|
<div class="alert alert-warning">
|
|
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
|
|
<div>
|
|
<p class="font-semibold">
|
|
{ngettext(
|
|
"%{count} member has a value assigned for this custom field.",
|
|
"%{count} members have values assigned for this custom field.",
|
|
@custom_field_to_delete.assigned_members_count,
|
|
count: @custom_field_to_delete.assigned_members_count
|
|
)}
|
|
</p>
|
|
<p class="mt-2 text-sm">
|
|
{gettext(
|
|
"All custom field values will be permanently deleted when you delete this custom field."
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="slug-confirmation" class="label">
|
|
<span class="label-text">
|
|
{gettext("To confirm deletion, please enter this text:")}
|
|
</span>
|
|
</label>
|
|
<div class="p-2 mb-2 font-mono text-lg font-bold break-all rounded bg-base-200">
|
|
{@custom_field_to_delete.slug}
|
|
</div>
|
|
<form phx-change="update_slug_confirmation" phx-target={@myself}>
|
|
<input
|
|
id="slug-confirmation"
|
|
name="slug"
|
|
type="text"
|
|
value={@slug_confirmation}
|
|
placeholder={gettext("Enter the text above to confirm")}
|
|
autocomplete="off"
|
|
phx-mounted={JS.focus()}
|
|
class="w-full input input-bordered"
|
|
/>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button phx-click="cancel_delete" phx-target={@myself} class="btn">
|
|
{gettext("Cancel")}
|
|
</button>
|
|
<button
|
|
phx-click="confirm_delete"
|
|
phx-target={@myself}
|
|
class="btn btn-error"
|
|
disabled={@slug_confirmation != @custom_field_to_delete.slug}
|
|
>
|
|
{gettext("Delete Custom Field and All Values")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
@impl true
|
|
def update(assigns, socket) do
|
|
# If show_form is explicitly provided in assigns, reset editing state
|
|
socket =
|
|
if Map.has_key?(assigns, :show_form) and assigns.show_form == false do
|
|
socket
|
|
|> assign(:editing_custom_field, nil)
|
|
|> assign(:form_id, "custom-field-form-new")
|
|
else
|
|
socket
|
|
end
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(assigns)
|
|
|> assign_new(:show_form, fn -> false end)
|
|
|> assign_new(:form_id, fn -> "custom-field-form-new" end)
|
|
|> assign_new(:editing_custom_field, fn -> nil end)
|
|
|> assign_new(:show_delete_modal, fn -> false end)
|
|
|> assign_new(:custom_field_to_delete, fn -> nil end)
|
|
|> assign_new(:slug_confirmation, fn -> "" end)
|
|
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField), reset: true)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("new_custom_field", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_form, true)
|
|
|> assign(:editing_custom_field, nil)
|
|
|> assign(:form_id, "custom-field-form-new")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("edit_custom_field", %{"id" => id}, socket) do
|
|
custom_field = Ash.get!(Mv.Membership.CustomField, id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_form, true)
|
|
|> assign(:editing_custom_field, custom_field)
|
|
|> assign(:form_id, "custom-field-form-#{id}")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("prepare_delete", %{"id" => id}, socket) do
|
|
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:custom_field_to_delete, custom_field)
|
|
|> assign(:show_delete_modal, true)
|
|
|> assign(:slug_confirmation, "")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("update_slug_confirmation", %{"slug" => slug}, socket) do
|
|
{:noreply, assign(socket, :slug_confirmation, slug)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("confirm_delete", _params, socket) do
|
|
custom_field = socket.assigns.custom_field_to_delete
|
|
|
|
if socket.assigns.slug_confirmation == custom_field.slug do
|
|
case Ash.destroy(custom_field) do
|
|
:ok ->
|
|
send(self(), {:custom_field_deleted, custom_field})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_delete_modal, false)
|
|
|> assign(:custom_field_to_delete, nil)
|
|
|> assign(:slug_confirmation, "")
|
|
|> stream_delete(:custom_fields, custom_field)}
|
|
|
|
{:error, error} ->
|
|
send(self(), {:custom_field_delete_error, error})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_delete_modal, false)
|
|
|> assign(:custom_field_to_delete, nil)
|
|
|> assign(:slug_confirmation, "")}
|
|
end
|
|
else
|
|
send(self(), :custom_field_slug_mismatch)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_delete_modal, false)
|
|
|> assign(:custom_field_to_delete, nil)
|
|
|> assign(:slug_confirmation, "")}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("cancel_delete", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_delete_modal, false)
|
|
|> assign(:custom_field_to_delete, nil)
|
|
|> assign(:slug_confirmation, "")}
|
|
end
|
|
end
|