Setting the actor for resources from live views and ash_admin?

Hi,

I have the following code in my resource

  actions do
    create :create do
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
    end
  end

This works with ash_admin. When I add records based on the user ive selected as the actor is automatically assigned.

In my live view I have current_user on the socket and would like to associate it with the record when adding, updating or validating the form etc.

defmodule WedEventsWeb.EventLive.FormComponent do
  use WedEventsWeb, :live_component

  alias WedEvents.Events

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage event records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="event-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:title]} label="Title" />
        <.input field={@form[:description]} label="Description" />
        <.input field={@form[:location]} label="Location" />
        <.input field={@form[:start_time]} label="Start Time" type="datetime-local" />
        <.input field={@form[:end_time]} label="End Time" type="datetime-local" />
        <:actions>
          <.button phx-disable-with="Saving...">Save event</.button>
        </:actions>
      </.simple_form>

      <%= if @form.errors != [] do %>
        <div class="alert alert-danger">
          <p>Oops, something went wrong! Please check the errors below.</p>
          <ul>
            <%= for {field, error} <- @form.errors do %>
              <li><strong><%= field %>:</strong> <%= translate_error(error) %></li>
            <% end %>
          </ul>
        </div>
      <% end %>
    </div>
    """
  end

  @impl true
  def update(%{event: event} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_new(:form, fn ->
       to_form(AshPhoenix.Form.for_create(Events.Event, :create, api: Events))
     end)}
  end

  @impl true
  def handle_event("validate", %{"form" => event_params}, socket) do
    # IO.inspect(socket.assigns.current_user, label: "current_user")
    form = AshPhoenix.Form.validate(socket.assigns.form, event_params)
    {:noreply, assign(socket, form: form)}
  end

  def handle_event("save", %{"form" => event_params}, socket) do
    save_event(socket, socket.assigns.action, event_params)
  end

  defp save_event(socket, :edit, event_params) do
    case Events.update_event(socket.assigns.event, event_params) do
      {:ok, event} ->
        notify_parent({:saved, event})

        {:noreply,
         socket
         |> put_flash(:info, "Event updated successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp save_event(socket, :new, event_params) do
    case AshPhoenix.Form.submit(socket.assigns.form, params: event_params) do
      {:ok, event} ->
        notify_parent({:saved, event})

        {:noreply,
         socket
         |> put_flash(:info, "Event created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, form} ->
        {:noreply, assign(socket, form: form)}
    end
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

defmodule WedEventsWeb.EventLive.Index do
  use WedEventsWeb, :live_view
  import Ash.Query
  alias WedEvents.Events
  alias WedEvents.Events.Event

  @impl true
  def mount(_params, _session, socket) do
    {:ok, events} = Event |> Ash.read()
    {:ok, stream(socket, :events, events)}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Event")
    |> assign(:event, Events.get_by_id(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Event")
    |> assign(
      :event,
      AshPhoenix.Form.for_create(Events.Event, :create, api: Events)
    )
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Events")
    |> assign(:event, nil)
  end

  @impl true
  def handle_info({WedEventsWeb.EventLive.FormComponent, {:saved, event}}, socket) do
    {:noreply, stream_insert(socket, :events, event)}
  end

  @impl true
  # def handle_event("delete", %{"id" => id}, socket) do
  #   event = Events.get_by_id(id)
  #   {:ok, _} = Events.delete_event(event)

  #   {:noreply, stream_delete(socket, :events, event)}
  # end
end

Where do I “associate” the current_user for those fields on the form?

Seems like a silly question but I want to know how I can make it work for both ash_admin and live views too

Thanks

Small note: I notice an api option, so either you’re on 2.x or you should remove that option :slight_smile:

AshPhoenix.Form.for_create(Events.Event, :create, actor: socket.assigns.current_user)

Thanks but I’m still having issues

When a new form is created here

 defp apply_action(socket, :new, _params) do
    form =
      AshPhoenix.Form.for_create(Events.Event, :create, actor: socket.assigns.current_user)

    IO.inspect(form, label: "form")

    socket
    |> assign(:page_title, "New Event")
    |> assign(
      :event,
      form
    )
  end

I can see the actor being set against the relationship.

    relationships: %{
      created_by: [
        {[
           #WedEvents.Accounts.User<
             __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
             id: "db31c5a6-85ab-4a5e-ae60-229e3f19a217",
             email: #Ash.CiString<"admin@example.com">,
             aggregates: %{},
             calculations: %{},
             ...
           >
         ],
         [
           ignore?: false,
           on_missing: :unrelate,
           on_match: :ignore,
           on_lookup: :relate,
           on_no_match: :error,
           eager_validate_with: false,
           authorize?: true,
           type: :append_and_remove
         ]}
      ],
      updated_by: [
        {[
           #WedEvents.Accounts.User<
             __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
             id: "db31c5a6-85ab-4a5e-ae60-229e3f19a217",
             email: #Ash.CiString<"admin@example.com">,
             aggregates: %{},
             calculations: %{},
             ...
           >
         ],
         [
           ignore?: false,
           on_missing: :unrelate,
           on_match: :ignore,
           on_lookup: :relate,
           on_no_match: :error,
           eager_validate_with: false,
           authorize?: true,
           type: :append_and_remove
         ]}
      ]
    },

However in the following validation handler in a different file for form_component.ex the actor is not set

  @impl true
  def handle_event("validate", %{"form" => event_params}, socket) do
    form =
      AshPhoenix.Form.validate(socket.assigns.form, event_params,
        actor: socket.assigns.current_user
      )

    IO.inspect(form, label: "form")

    {:noreply, assign(socket, form: form)}
  end

Here

      relationships: %{},
      errors: [
        %Ash.Error.Changes.InvalidRelationship{
          relationship: :updated_by,
          message: "could not relate to actor, as no actor was found (and :allow_nil? is false)",
          splode: nil,
          bread_crumbs: [],
          vars: [],
          path: [],
          stacktrace: #Splode.Stacktrace<>,
          class: :invalid
        },
        %Ash.Error.Changes.InvalidRelationship{
          relationship: :created_by,
          message: "could not relate to actor, as no actor was found (and :allow_nil? is false)",
          splode: nil,
          bread_crumbs: [],
          vars: [],
          path: [],
          stacktrace: #Splode.Stacktrace<>,
          class: :invalid
        }
      ],

Do I need to pass the actor to the live_component (as below, event is the new form) or do I need to update the form rendered in my form_component which renders that live component to include something for the actor somehow?

  <.live_component
    module={WedEventsWeb.EventLive.FormComponent}
    id={@event.id || :new}
    title={@page_title}
    action={@live_action}
    event={@event}
    patch={~p"/manage/events"}
    current_user={@current_user}
  />

Thanks

Figured it out I needed to make the following changes

Not sure if Ive setup my form using best practices tho seeing as the new action goes through the update function :thinking:

  @impl true
  def update(assigns, socket) do
    form =
      case assigns.action do
        :new ->
          AshPhoenix.Form.for_create(Events.Event, :create, actor: assigns.current_user)

        :edit ->
          AshPhoenix.Form.for_update(assigns.event, :update, actor: assigns.current_user)
      end

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:form, to_form(form))}
  end