diff --git a/lib/beacon/live_admin/content.ex b/lib/beacon/live_admin/content.ex
index edae31c2..5f90d2d4 100644
--- a/lib/beacon/live_admin/content.ex
+++ b/lib/beacon/live_admin/content.ex
@@ -185,4 +185,56 @@ defmodule Beacon.LiveAdmin.Content do
def valid_error_statuses(site) do
call(site, Beacon.Content.ErrorPage, :valid_statuses, [])
end
+
+ def live_data_assign_formats(site) do
+ call(site, Beacon.Content, :live_data_assign_formats, [])
+ end
+
+ def change_live_data_path(site, live_data, attrs \\ %{}) do
+ call(site, Beacon.Content, :change_live_data_path, [live_data, attrs])
+ end
+
+ def change_live_data_assign(site, live_data_assign, attrs \\ %{}) do
+ call(site, Beacon.Content, :change_live_data_assign, [live_data_assign, attrs])
+ end
+
+ def create_live_data(site, attrs) do
+ call(site, Beacon.Content, :create_live_data, [attrs])
+ end
+
+ def create_assign_for_live_data(site, live_data, attrs) do
+ call(site, Beacon.Content, :create_assign_for_live_data, [live_data, attrs])
+ end
+
+ def get_live_data(site, path) do
+ call(site, Beacon.Content, :get_live_data, [site, path])
+ end
+
+ def live_data_for_site(site) do
+ call(site, Beacon.Content, :live_data_for_site, [site])
+ end
+
+ def live_data_paths_for_site(site) do
+ call(site, Beacon.Content, :live_data_paths_for_site, [site])
+ end
+
+ def live_data_paths_for_site(site, opts) do
+ call(site, Beacon.Content, :live_data_paths_for_site, [site, opts])
+ end
+
+ def update_live_data_path(site, live_data, attrs) do
+ call(site, Beacon.Content, :update_live_data_path, [live_data, attrs])
+ end
+
+ def update_live_data_assign(site, live_data_assign, attrs) do
+ call(site, Beacon.Content, :update_live_data_assign, [live_data_assign, attrs])
+ end
+
+ def delete_live_data(site, live_data) do
+ call(site, Beacon.Content, :delete_live_data, [live_data])
+ end
+
+ def delete_live_data_assign(site, live_data_assign) do
+ call(site, Beacon.Content, :delete_live_data_assign, [live_data_assign])
+ end
end
diff --git a/lib/beacon/live_admin/live/home_live.ex b/lib/beacon/live_admin/live/home_live.ex
index 8aefe83e..17affa9b 100644
--- a/lib/beacon/live_admin/live/home_live.ex
+++ b/lib/beacon/live_admin/live/home_live.ex
@@ -55,6 +55,13 @@ defmodule Beacon.LiveAdmin.HomeLive do
Pages
+ <.link
+ href={Beacon.LiveAdmin.Router.beacon_live_admin_path(@socket, site, "/live_data")}
+ class="whitespace-nowrap text-sm leading-5 py-3.5 font-bold tracking-widest text-center uppercase bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-200 active:bg-blue-800 px-6 text-gray-50"
+ >
+ Live Data
+
+
<.link
href={Beacon.LiveAdmin.Router.beacon_live_admin_path(@socket, site, "/error_pages")}
class="whitespace-nowrap text-sm leading-5 py-3.5 font-bold tracking-widest text-center uppercase bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-200 active:bg-blue-800 px-6 text-gray-50"
diff --git a/lib/beacon/live_admin/live/live_data_editor_live/assigns.ex b/lib/beacon/live_admin/live/live_data_editor_live/assigns.ex
new file mode 100644
index 00000000..11d8910c
--- /dev/null
+++ b/lib/beacon/live_admin/live/live_data_editor_live/assigns.ex
@@ -0,0 +1,320 @@
+defmodule Beacon.LiveAdmin.LiveDataEditorLive.Assigns do
+ @moduledoc false
+ use Beacon.LiveAdmin.PageBuilder
+
+ alias Beacon.LiveAdmin.Content
+
+ def menu_link("/live_data", :assigns), do: {:submenu, "Live Data"}
+ def menu_link(_, _), do: :skip
+
+ def handle_params(params, _url, socket) do
+ path = URI.decode_www_form(params["path"])
+
+ socket =
+ socket
+ |> assign(live_data: Content.get_live_data(socket.assigns.beacon_page.site, path))
+ |> assign(unsaved_changes: false)
+ |> assign(show_nav_modal: false)
+ |> assign(show_create_modal: false)
+ |> assign(show_delete_modal: false)
+ |> assign(new_assign_form: to_form(%{"key" => ""}))
+ |> assign(page_title: "Live Data")
+ |> assign_selected(params["key"])
+ |> assign_form()
+
+ {:noreply, socket}
+ end
+
+ def handle_event("select-" <> key, _, socket) do
+ %{live_data: %{site: site, path: path}} = socket.assigns
+
+ path =
+ beacon_live_admin_path(
+ socket,
+ site,
+ "/live_data/#{sanitize(path)}/#{sanitize(key)}"
+ )
+
+ if socket.assigns.unsaved_changes do
+ {:noreply, assign(socket, show_nav_modal: true, confirm_nav_path: path)}
+ else
+ {:noreply, push_redirect(socket, to: path)}
+ end
+ end
+
+ def handle_event("live_data_assign_editor_lost_focus", %{"value" => value}, socket) do
+ %{selected: selected, live_data: %{site: site}, form: form} = socket.assigns
+
+ changeset =
+ site
+ |> Content.change_live_data_assign(selected, %{
+ "value" => value,
+ "key" => form.params["key"] || Map.fetch!(form.data, :key),
+ "format" => form.params["format"] || Map.fetch!(form.data, :format)
+ })
+ |> Map.put(:action, :validate)
+
+ socket =
+ socket
+ |> assign(form: to_form(changeset))
+ |> assign(changed_value: value)
+ |> assign(unsaved_changes: !(changeset.changes == %{}))
+
+ {:noreply, socket}
+ end
+
+ def handle_event("validate", %{"live_data_assign" => params}, socket) do
+ %{selected: selected, live_data: %{site: site}} = socket.assigns
+
+ changeset =
+ site
+ |> Content.change_live_data_assign(selected, params)
+ |> Map.put(:action, :validate)
+
+ socket =
+ socket
+ |> assign(form: to_form(changeset))
+ |> assign(unsaved_changes: !(changeset.changes == %{}))
+
+ {:noreply, socket}
+ end
+
+ def handle_event("set_value", %{"value" => value}, socket) do
+ %{selected: selected, beacon_page: %{site: site}, form: form} = socket.assigns
+
+ params = Map.merge(form.params, %{"value" => value})
+ changeset = Content.change_live_data_assign(site, selected, params)
+
+ socket =
+ socket
+ |> assign_form(changeset)
+ |> assign(unsaved_changes: !(changeset.changes == %{}))
+
+ {:noreply, socket}
+ end
+
+ def handle_event("save_changes", %{"live_data_assign" => params}, socket) do
+ %{selected: selected, live_data: %{site: site, path: live_data_path}} = socket.assigns
+
+ attrs = %{key: params["key"], value: params["value"], format: params["format"]}
+
+ socket =
+ case Content.update_live_data_assign(site, selected, attrs) do
+ {:ok, live_data_assign} ->
+ path =
+ beacon_live_admin_path(
+ socket,
+ site,
+ "/live_data/#{sanitize(live_data_path)}/#{sanitize(live_data_assign.key)}"
+ )
+
+ socket
+ |> assign(live_data: Content.get_live_data(site, live_data_path))
+ |> assign_form()
+ |> assign(unsaved_changes: false)
+ |> push_patch(to: path)
+
+ {:error, changeset} ->
+ changeset = Map.put(changeset, :action, :update)
+ assign(socket, form: to_form(changeset))
+ end
+
+ {:noreply, socket}
+ end
+
+ def handle_event("show_create_modal", _params, socket) do
+ {:noreply, assign(socket, show_create_modal: true)}
+ end
+
+ def handle_event("submit_new", params, socket) do
+ %{live_data: %{site: site} = live_data, selected: selected} = socket.assigns
+ selected = selected || %{key: nil}
+
+ attrs = %{key: params["key"], value: "Your value here", format: :text}
+ # TODO: handle errors
+ {:ok, updated_live_data} = Content.create_assign_for_live_data(site, live_data, attrs)
+
+ socket =
+ socket
+ |> assign(live_data: updated_live_data)
+ |> assign_selected(selected.key)
+
+ {:noreply, assign(socket, show_create_modal: false)}
+ end
+
+ def handle_event("delete", _, socket) do
+ {:noreply, assign(socket, show_delete_modal: true)}
+ end
+
+ def handle_event("delete_confirm", _, socket) do
+ %{selected: selected, live_data: %{site: site, path: path}} = socket.assigns
+
+ {:ok, _} = Content.delete_live_data_assign(site, selected)
+
+ {:noreply,
+ push_redirect(socket,
+ to: beacon_live_admin_path(socket, site, "/live_data/#{sanitize(path)}")
+ )}
+ end
+
+ def handle_event("create_cancel", _, socket) do
+ {:noreply, assign(socket, show_create_modal: false)}
+ end
+
+ def handle_event("delete_cancel", _, socket) do
+ {:noreply, assign(socket, show_delete_modal: false)}
+ end
+
+ def handle_event("stay_here", _params, socket) do
+ {:noreply, assign(socket, show_nav_modal: false, confirm_nav_path: nil)}
+ end
+
+ def handle_event("discard_changes", _params, socket) do
+ {:noreply, push_redirect(socket, to: socket.assigns.confirm_nav_path)}
+ end
+
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ <%= @page_title %>
+ <:actions>
+ <.button type="button" phx-click="show_create_modal">New Live Data Assign
+
+
+ <.main_content class="h-[calc(100vh_-_223px)]">
+ <.modal :if={@show_nav_modal} id="confirm-nav" on_cancel={JS.push("stay_here")} show>
+
You've made unsaved changes to this assign!
+
Navigating to another assign without saving will cause these changes to be lost.
+ <.button type="button" phx-click="stay_here">
+ Stay here
+
+ <.button type="button" phx-click="discard_changes">
+ Discard changes
+
+
+
+ <.modal :if={@show_delete_modal} id="confirm-delete" on_cancel={JS.push("delete_cancel")} show>
+
Are you sure you want to delete this assign?
+
+ <.button id="delete-confirm" type="button" phx-click="delete_confirm">
+ Delete
+
+ <.button type="button" phx-click="delete_cancel">
+ Cancel
+
+
+
+
+ <.modal :if={@show_create_modal} id="create-modal" on_cancel={JS.push("create_cancel")} show>
+
New Assign
+ <.form id="new-assign-form" for={@new_assign_form} phx-submit="submit_new">
+ <.input type="text" field={@new_assign_form[:key]} placeholder="assign_key" />
+
+ <.button type="submit">Create
+ <.button type="button" phx-click={JS.push("create_cancel")}>Cancel
+
+
+
+
+
+
+
+
Path:
+
<%= @live_data.path %>
+
+ <.table :if={@selected} id="assigns" rows={@live_data.assigns} row_click={fn assign -> "select-#{assign.key}" end}>
+ <:col :let={assign} label="assign">
+ @<%= assign.key %>
+
+
+
+
+
+ <.form :let={f} id="edit-assign-form" for={@form} class="flex items-end gap-4" phx-change="validate" phx-submit="save_changes">
+ <.input label="Key" field={f[:key]} type="text" />
+ <.input label="Format" field={f[:format]} type="select" options={["elixir", "text"]} />
+
+ <.input type="hidden" field={f[:value]} name="live_data_assign[value]" id="live_data_assign-form_value" value={Phoenix.HTML.Form.input_value(f, :value)} />
+
+ <.button phx-disable-with="Saving..." class="ml-auto">Save Changes
+ <.button type="button" phx-click="delete" class="">Delete
+
+
+
Variables available:
+
<%= variables_available(@live_data.path) %>
+
+ <%= template_error(@form[:value]) %>
+
+
+
+
+
+ """
+ end
+
+ defp assign_selected(socket, nil) do
+ case socket.assigns.live_data.assigns do
+ [] ->
+ assign(socket, selected: nil, changed_value: "")
+
+ [live_data_assign | _] ->
+ assign(socket, selected: live_data_assign, changed_value: live_data_assign.value)
+ end
+ end
+
+ defp assign_selected(socket, key) do
+ key = URI.decode_www_form(key)
+ selected = Enum.find(socket.assigns.live_data.assigns, &(&1.key == key))
+
+ if selected do
+ assign(socket, selected: selected, changed_value: selected.value)
+ else
+ path = beacon_live_admin_path(socket, socket.assigns.beacon_page.site, "/live_data")
+
+ socket
+ |> assign(selected: nil)
+ |> push_navigate(to: path, replace: true)
+ end
+ end
+
+ defp assign_form(socket) do
+ form =
+ case socket.assigns do
+ %{selected: nil} ->
+ nil
+
+ %{selected: selected, live_data: %{site: site}} ->
+ site
+ |> Content.change_live_data_assign(selected)
+ |> to_form()
+ end
+
+ assign(socket, form: form)
+ end
+
+ defp assign_form(socket, changeset) do
+ assign(socket, :form, to_form(changeset))
+ end
+
+ defp sanitize(path_or_key), do: URI.encode_www_form(path_or_key)
+
+ defp variables_available(path) do
+ path
+ |> String.split("/", trim: true)
+ |> Enum.filter(&String.starts_with?(&1, ":"))
+ |> Enum.map(fn ":" <> param -> param end)
+ |> Kernel.++(["params"])
+ |> Enum.join(" ")
+ end
+end
diff --git a/lib/beacon/live_admin/live/live_data_editor_live/index.ex b/lib/beacon/live_admin/live/live_data_editor_live/index.ex
new file mode 100644
index 00000000..45a31e7d
--- /dev/null
+++ b/lib/beacon/live_admin/live/live_data_editor_live/index.ex
@@ -0,0 +1,177 @@
+defmodule Beacon.LiveAdmin.LiveDataEditorLive.Index do
+ @moduledoc false
+ use Beacon.LiveAdmin.PageBuilder
+
+ alias Beacon.LiveAdmin.Content
+
+ on_mount {Beacon.LiveAdmin.Hooks.Authorized, {:live_data, :index}}
+
+ def menu_link(_, :index), do: {:root, "Live Data"}
+ def menu_link(_, :new), do: {:root, "Live Data"}
+ def menu_link(_, :edit), do: {:root, "Live Data"}
+
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ def handle_params(%{"query" => query}, _uri, socket) do
+ %{beacon_page: %{site: site}} = socket.assigns
+ live_data_paths = Content.live_data_paths_for_site(site, query: query)
+
+ {:noreply, assign(socket, live_data_paths: live_data_paths)}
+ end
+
+ def handle_params(params, _uri, socket) do
+ %{beacon_page: %{site: site}, live_action: live_action} = socket.assigns
+
+ socket =
+ socket
+ |> assign(live_data_paths: Content.live_data_paths_for_site(site))
+ |> assign(show_new_path_modal: live_action == :new)
+ |> assign(show_edit_path_modal: live_action == :edit)
+ |> assign(show_delete_path_modal: live_action == :delete)
+ |> assign_edit_path_form(params["path"])
+ |> assign_selected(params["path"])
+
+ {:noreply, socket}
+ end
+
+ def handle_event("search", %{"search" => %{"query" => query}}, socket) do
+ %{beacon_page: %{site: site}} = socket.assigns
+ path = beacon_live_admin_path(socket, site, "/live_data?query=#{query}")
+
+ {:noreply, push_patch(socket, to: path)}
+ end
+
+ def handle_event("submit_path", %{"path" => path}, socket) do
+ %{beacon_page: %{site: site}} = socket.assigns
+
+ socket =
+ case Content.create_live_data(site, %{path: path, site: site}) do
+ {:ok, live_data} ->
+ socket
+ |> assign(live_data_paths: Content.live_data_paths_for_site(site))
+ |> push_navigate(
+ to:
+ beacon_live_admin_path(socket, site, "/live_data/#{sanitize_path(live_data.path)}")
+ )
+
+ {:error, _changeset} ->
+ socket
+ end
+
+ {:noreply, socket}
+ end
+
+ def handle_event("edit_path", params, socket) do
+ %{beacon_page: %{site: site}, selected: selected} = socket.assigns
+ %{"live_data" => %{"path" => path}} = params
+
+ {:ok, _live_data} = Content.update_live_data_path(site, selected, path)
+
+ path = beacon_live_admin_path(socket, socket.assigns.beacon_page.site, "/live_data")
+ {:noreply, push_navigate(socket, to: path, replace: true)}
+ end
+
+ def handle_event("delete_path", _params, socket) do
+ %{beacon_page: %{site: site}, selected: selected} = socket.assigns
+
+ {:ok, _live_data} = Content.delete_live_data(site, selected)
+
+ path = beacon_live_admin_path(socket, socket.assigns.beacon_page.site, "/live_data")
+ {:noreply, push_navigate(socket, to: path, replace: true)}
+ end
+
+ def handle_event("close_modal", _params, socket) do
+ path = beacon_live_admin_path(socket, socket.assigns.beacon_page.site, "/live_data")
+ {:noreply, push_navigate(socket, to: path, replace: true)}
+ end
+
+ defp assign_edit_path_form(socket, nil), do: assign(socket, edit_path_form: to_form(%{}))
+
+ defp assign_edit_path_form(socket, path) do
+ form =
+ {%{}, %{path: :string}}
+ |> Ecto.Changeset.cast(%{path: path}, [:path])
+ |> Ecto.Changeset.validate_required([:path])
+ |> to_form(as: :live_data)
+
+ assign(socket, edit_path_form: form)
+ end
+
+ defp assign_selected(socket, nil), do: assign(socket, selected: nil)
+
+ defp assign_selected(%{assigns: %{live_action: action}} = socket, _)
+ when action in [:index, :new],
+ do: assign(socket, selected: nil)
+
+ defp assign_selected(socket, path) do
+ %{beacon_page: %{site: site}} = socket.assigns
+ assign(socket, selected: Content.get_live_data(site, path))
+ end
+
+ def render(assigns) do
+ ~H"""
+ <.header>
+
+ <:actions>
+ <.link id="header-new-path-button" patch={beacon_live_admin_path(@socket, @beacon_page.site, "/live_data/new")}>
+ <.button class="uppercase">New Path
+
+
+
+
+ <.simple_form :let={f} id="live-data-path-search" for={%{}} as={:search} phx-change="search">
+ <.input field={f[:query]} type="search" autofocus={true} placeholder="Search by path (showing up to 20 results)" />
+
+
+ <.main_content class="h-[calc(100vh_-_210px)]">
+ <.table id="live_data" rows={@live_data_paths} row_click={fn live_data_path -> JS.navigate(beacon_live_admin_path(@socket, @beacon_page.site, "/live_data/#{sanitize_path(live_data_path)}")) end}>
+ <:col :let={live_data_path} label="Path"><%= live_data_path %>
+ <:action :let={live_data_path}>
+
+ <.link id={"edit-live-data-" <> live_data_path} navigate={beacon_live_admin_path(@socket, @beacon_page.site, "/live_data/edit/#{sanitize_path(live_data_path)}")} title="Edit live data">
+ Edit
+
+
+ <.link
+ patch={beacon_live_admin_path(@socket, @beacon_page.site, "/live_data/edit/#{sanitize_path(live_data_path)}")}
+ title="Edit live data"
+ aria-label="Edit live data"
+ class="flex items-center justify-center w-10 h-10 group"
+ >
+ <.icon name="hero-pencil-square text-[#61758A] hover:text-[#304254]" />
+
+
+
+
+
+ <.modal :if={@show_new_path_modal} id="new-path-modal" on_cancel={JS.push("close_modal")} show>
+ New Path
+ <.form id="new-path-form" for={%{}} phx-submit="submit_path">
+ <.input type="text" name="path" placeholder="/project/:project_id/comments" value="" />
+
+ <.button type="submit">Create
+ <.button type="button" phx-click={JS.push("close_modal")}>Cancel
+
+
+
+
+ <.modal :if={@show_edit_path_modal} id="edit-path-modal" on_cancel={JS.push("close_modal")} show>
+ Edit Path
+ <.form id="edit-path-form" for={@edit_path_form} phx-submit="edit_path">
+ <.input field={@edit_path_form[:path]} type="text" />
+
+ <.button type="submit">Update
+ <.button type="button" phx-click={JS.push("close_modal")}>Cancel
+ <.button type="button" class="!bg-red-600" phx-click="delete_path">Delete
+
+
+
+ """
+ end
+
+ defp sanitize_path(path) do
+ URI.encode_www_form(path)
+ end
+end
diff --git a/lib/beacon/live_admin/page_live.ex b/lib/beacon/live_admin/page_live.ex
index 90ce995c..07cbbafc 100644
--- a/lib/beacon/live_admin/page_live.ex
+++ b/lib/beacon/live_admin/page_live.ex
@@ -183,6 +183,8 @@ defmodule Beacon.LiveAdmin.PageLive do
{_, "/components"} -> false
{"/pages", _} -> true
{_, "/pages"} -> false
+ {"/live_data", _} -> true
+ {_, "/live_data"} -> false
{"/error_pages", _} -> true
{_, "/error_pages"} -> false
{"/media_library", _} -> true
diff --git a/lib/beacon/live_admin/router.ex b/lib/beacon/live_admin/router.ex
index 4c156e45..def87121 100644
--- a/lib/beacon/live_admin/router.ex
+++ b/lib/beacon/live_admin/router.ex
@@ -150,6 +150,12 @@ defmodule Beacon.LiveAdmin.Router do
# error pages
{"/error_pages", Beacon.LiveAdmin.ErrorPageEditorLive.Index, :index, %{}},
{"/error_pages/:status", Beacon.LiveAdmin.ErrorPageEditorLive.Index, :index, %{}},
+ # live data
+ {"/live_data", Beacon.LiveAdmin.LiveDataEditorLive.Index, :index, %{}},
+ {"/live_data/new", Beacon.LiveAdmin.LiveDataEditorLive.Index, :new, %{}},
+ {"/live_data/edit/:path", Beacon.LiveAdmin.LiveDataEditorLive.Index, :edit, %{}},
+ {"/live_data/:path", Beacon.LiveAdmin.LiveDataEditorLive.Assigns, :assigns, %{}},
+ {"/live_data/:path/:key", Beacon.LiveAdmin.LiveDataEditorLive.Assigns, :assigns, %{}},
# media library
{"/media_library", Beacon.LiveAdmin.MediaLibraryLive.Index, :index, %{}},
{"/media_library/upload", Beacon.LiveAdmin.MediaLibraryLive.Index, :upload, %{}},
diff --git a/mix.lock b/mix.lock
index 36716022..f65b0a86 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,6 +1,6 @@
%{
"accent": {:hex, :accent, "1.1.1", "20257356446d45078b19b91608f74669b407b39af891ee3db9ee6824d1cae19d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6d5afa50d4886e3370e04fa501468cbaa6c4b5fe926f72ccfa844ad9e259adae"},
- "beacon": {:git, "https://github.com/beaconCMS/beacon.git", "1f07ee18f1e6d152c910c1329184242356471590", []},
+ "beacon": {:git, "https://github.com/beaconCMS/beacon.git", "b03c7fcda24c2d335e6aa8cf73be572ff081e55d", []},
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
@@ -21,7 +21,6 @@
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
"gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"},
- "heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"},
"image": {:hex, :image, "0.34.0", "4bd8b5f6b0a979607e56a40996cf5509318b2de32acf6abb1f0c0ab2b48f5e65", [:mix], [{:bumblebee, "~> 0.2", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.26", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.5", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14 or ~> 3.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.17", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "5277d1864bb3fd44db80f6a4f244cf5a5cc5106bdf030931524f59db7080bb9f"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"live_monaco_editor": {:hex, :live_monaco_editor, "0.1.8", "149c02cab1c595fe2d2049cffb0a424db2a329a5fa848ee8b778d5acd8694733", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "9a56e88a61cdf6d58081627e4842f4e3a8e3a75dd8749f271a464164ad4530f1"},
diff --git a/test/beacon/live_admin/live/live_data_editor_live/assigns_test.exs b/test/beacon/live_admin/live/live_data_editor_live/assigns_test.exs
new file mode 100644
index 00000000..f1430a33
--- /dev/null
+++ b/test/beacon/live_admin/live/live_data_editor_live/assigns_test.exs
@@ -0,0 +1,65 @@
+defmodule Beacon.LiveAdmin.LiveDataEditorLive.AssignsTest do
+ use Beacon.LiveAdmin.ConnCase, async: false
+ import Beacon.LiveAdminTest.Cluster, only: [rpc: 4]
+
+ setup do
+ on_exit(fn ->
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.Page, [log: false]])
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.Layout, [log: false]])
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.LiveData, [log: false]])
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.LiveDataAssign, [log: false]])
+ end)
+
+ page_fixture()
+ live_data = live_data_fixture(node1(), path: "/home")
+
+ [live_data: live_data]
+ end
+
+ test "create new assign", %{conn: conn} do
+ {:ok, view, _html} = live(conn, "/admin/site_a/live_data/%2Fhome")
+
+ view
+ |> element("button", "New Live Data Assign")
+ |> render_click()
+
+ view
+ |> form("#new-assign-form", %{"key" => "valid?"})
+ |> render_submit()
+
+ {:ok, view, html} = live(conn, "/admin/site_a/live_data/%2Fhome/valid%3F")
+
+ assert html =~ "@valid?"
+ assert has_element?(view, ~S|input[value="valid?"]|)
+ end
+
+ test "edit existing assign", %{conn: conn, live_data: live_data} do
+ live_data_assign_fixture(node1(), live_data: live_data)
+ {:ok, view, _html} = live(conn, "/admin/site_a/live_data/%2Fhome/sum")
+
+ html =
+ view
+ |> form("#edit-assign-form", %{"live_data_assign" => %{"key" => "new_key"}})
+ |> render_submit()
+
+ assert html =~ "@new_key"
+ assert has_element?(view, ~S|input[value="new_key"]|)
+ end
+
+ test "delete existing assign", %{conn: conn, live_data: live_data} do
+ live_data_assign_fixture(node1(), live_data: live_data)
+ {:ok, view, _html} = live(conn, "/admin/site_a/live_data/%2Fhome/sum")
+
+ view
+ |> element("button", "Delete")
+ |> render_click()
+
+ {:ok, _view, html} =
+ view
+ |> element("#delete-confirm")
+ |> render_click()
+ |> follow_redirect(conn)
+
+ refute html =~ "@sum"
+ end
+end
diff --git a/test/beacon/live_admin/live/live_data_editor_live/index_test.exs b/test/beacon/live_admin/live/live_data_editor_live/index_test.exs
new file mode 100644
index 00000000..417e3627
--- /dev/null
+++ b/test/beacon/live_admin/live/live_data_editor_live/index_test.exs
@@ -0,0 +1,88 @@
+defmodule Beacon.LiveAdmin.LiveDataEditorLive.IndexTest do
+ use Beacon.LiveAdmin.ConnCase, async: false
+ import Beacon.LiveAdminTest.Cluster, only: [rpc: 4]
+
+ setup do
+ on_exit(fn ->
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.Page, [log: false]])
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.Layout, [log: false]])
+ rpc(node1(), Beacon.Repo, :delete_all, [Beacon.Content.LiveData, [log: false]])
+ end)
+
+ page_fixture()
+ live_data_fixture(node1(), path: "/testpages/:page_id")
+ live_data_fixture(node1(), path: "/testobjects/:object_id")
+
+ :ok
+ end
+
+ test "display header and all live data paths", %{conn: conn} do
+ {:ok, view, html} = live(conn, "/admin/site_a/live_data")
+
+ assert assert has_element?(view, "#header-page-title", "Live Data")
+ assert html =~ "/testpages/:page_id"
+ assert html =~ "/testobjects/:object_id"
+ end
+
+ test "search paths", %{conn: conn} do
+ {:ok, view, _html} = live(conn, "/admin/site_a/live_data")
+
+ view
+ |> form("#live-data-path-search")
+ |> render_change(%{"search" => %{"query" => "pages"}})
+
+ assert render(view) =~ "/testpages/:page_id"
+ refute render(view) =~ "/testobjects/:object_id"
+
+ view
+ |> form("#live-data-path-search")
+ |> render_change(%{"search" => %{"query" => "objects"}})
+
+ refute render(view) =~ "/testpages/:page_id"
+ assert render(view) =~ "/testobjects/:object_id"
+ end
+
+ test "create new path", %{conn: conn} do
+ {:ok, view, _html} = live(conn, "/admin/site_a/live_data")
+
+ view
+ |> element("#header-new-path-button")
+ |> render_click()
+
+ {:ok, view, _html} =
+ view
+ |> form("#new-path-form")
+ |> render_submit(%{"path" => "/my/fun/path"})
+ |> follow_redirect(conn, "/admin/site_a/live_data/%2Fmy%2Ffun%2Fpath")
+
+ assert render(view) =~ "/my/fun/path"
+ end
+
+ test "edit existing path", %{conn: conn} do
+ {:ok, view, _html} = live(conn, "/admin/site_a/live_data/edit/%2Ftestpages%2F%3Apage_id")
+
+ {:ok, _view, html} =
+ view
+ |> form("#edit-path-form", live_data: %{path: "/testposts/:post_id"})
+ |> render_submit()
+ |> follow_redirect(conn, "/admin/site_a/live_data")
+
+ assert html =~ "/testposts/:post_id"
+ refute html =~ "/testpages/:page_id"
+ end
+
+ test "raises when missing beacon_live_admin_url in the session" do
+ assert_raise RuntimeError, fn ->
+ conn =
+ Phoenix.ConnTest.dispatch(
+ build_conn(:get, "/admin/site_a/live_data"),
+ Beacon.LiveAdminTest.PluglessEndpoint,
+ :get,
+ "/admin/site_a/live_data",
+ nil
+ )
+
+ {:ok, _live, _html} = live(conn, "/admin/site_a/live_data")
+ end
+ end
+end
diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex
index 2acec53d..c28f5441 100644
--- a/test/support/fixtures.ex
+++ b/test/support/fixtures.ex
@@ -84,4 +84,30 @@ defmodule Beacon.LiveAdmin.Fixtures do
Path.join(["test", "support", "fixtures", file_name])
end
+
+ def live_data_fixture(node \\ node1(), attrs \\ %{}) do
+ attrs =
+ Enum.into(attrs, %{
+ site: "site_a",
+ path: "/foo/:id",
+ assign: "bar",
+ format: :elixir,
+ code: "id"
+ })
+
+ rpc(node, Beacon.Content, :create_live_data!, [attrs])
+ end
+
+ def live_data_assign_fixture(node \\ node1(), attrs \\ %{}) do
+ live_data = get_lazy(attrs, :live_data, fn -> live_data_fixture(node) end)
+
+ attrs =
+ Enum.into(attrs, %{
+ format: "elixir",
+ key: "sum",
+ value: "1 + 1"
+ })
+
+ rpc(node, Beacon.Content, :create_assign_for_live_data, [live_data, attrs])
+ end
end