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]) %> +
+
+ "elixir"})} + /> +
+
+
+
+ +
+ """ + 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> +

Live Data

+ <: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