diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4dceab..0511fa64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Enhancements + - Adds page to define custom JS Hooks - Updated UI styling with StationUI - Unsaved changes will now be saved when publishing, instead of discarded - Adds Unpublish button to Page editor diff --git a/lib/beacon/live_admin/client/content.ex b/lib/beacon/live_admin/client/content.ex index ba3dde33..23c52bdd 100644 --- a/lib/beacon/live_admin/client/content.ex +++ b/lib/beacon/live_admin/client/content.ex @@ -290,4 +290,24 @@ defmodule Beacon.LiveAdmin.Client.Content do def delete_info_handler(site, info_handler) do call(site, Beacon.Content, :delete_info_handler, [info_handler]) end + + def change_js_hook(site, js_hook, attrs \\ %{}) do + call(site, Beacon.Content, :change_js_hook, [js_hook, attrs]) + end + + def list_js_hooks(site) do + call(site, Beacon.Content, :list_js_hooks, [site]) + end + + def create_js_hook(site, attrs) do + call(site, Beacon.Content, :create_js_hook, [attrs]) + end + + def update_js_hook(site, js_hook, attrs) do + call(site, Beacon.Content, :update_js_hook, [js_hook, attrs]) + end + + def delete_js_hook(site, js_hook) do + call(site, Beacon.Content, :delete_js_hook, [js_hook]) + end end diff --git a/lib/beacon/live_admin/components/station_ui/html/form.ex b/lib/beacon/live_admin/components/station_ui/html/form.ex index a18f5bb0..0d0a7b35 100644 --- a/lib/beacon/live_admin/components/station_ui/html/form.ex +++ b/lib/beacon/live_admin/components/station_ui/html/form.ex @@ -86,9 +86,9 @@ defmodule Beacon.LiveAdmin.StationUI.HTML.Form do # with our gettext backend as first argument. Translations are # available in the errors.po file (as we use the "errors" domain). if count = opts[:count] do - Gettext.dngettext(StationUI.Gettext, "errors", msg, msg, count, opts) + Gettext.dngettext(Beacon.LiveAdmin.Gettext, "errors", msg, msg, count, opts) else - Gettext.dgettext(StationUI.Gettext, "errors", msg, opts) + Gettext.dgettext(Beacon.LiveAdmin.Gettext, "errors", msg, opts) end end diff --git a/lib/beacon/live_admin/live/home_live.ex b/lib/beacon/live_admin/live/home_live.ex index fe66451e..7b72a77e 100644 --- a/lib/beacon/live_admin/live/home_live.ex +++ b/lib/beacon/live_admin/live/home_live.ex @@ -57,6 +57,9 @@ defmodule Beacon.LiveAdmin.HomeLive do <.link href={Router.beacon_live_admin_path(@socket, site, "/error_pages")} class={nav_class()}> Error Pages + <.link href={Router.beacon_live_admin_path(@socket, site, "/hooks")} class={nav_class()}> + JS Hooks + <% end %> diff --git a/lib/beacon/live_admin/live/js_hook_editor_live/index.ex b/lib/beacon/live_admin/live/js_hook_editor_live/index.ex new file mode 100644 index 00000000..21d44750 --- /dev/null +++ b/lib/beacon/live_admin/live/js_hook_editor_live/index.ex @@ -0,0 +1,335 @@ +defmodule Beacon.LiveAdmin.JSHookEditorLive.Index do + @moduledoc false + use Beacon.LiveAdmin.PageBuilder + + alias Beacon.LiveAdmin.Client.Content + + def menu_link(_, :index), do: {:root, "JS Hooks"} + + def handle_params(params, _uri, socket) do + %{beacon_page: %{site: site}} = socket.assigns + + socket = + socket + |> assign(page_title: "JS Hooks") + |> assign(unsaved_changes: false) + |> assign(show_create_modal: false) + |> assign(show_nav_modal: false) + |> assign(show_delete_modal: false) + |> assign(create_form: to_form(%{}, as: :js_hook)) + |> assign_new(:js_hooks, fn -> Content.list_js_hooks(site) end) + |> assign_selected(params["id"]) + |> assign_form() + + {:noreply, socket} + end + + def handle_event("select-" <> id, _, socket) do + %{beacon_page: %{site: site}} = socket.assigns + + path = beacon_live_admin_path(socket, site, "/hooks/#{id}") + + if socket.assigns.unsaved_changes do + {:noreply, assign(socket, show_nav_modal: true, confirm_nav_path: path)} + else + {:noreply, push_navigate(socket, to: path)} + end + end + + def handle_event("set_" <> key, %{"value" => code}, socket) do + %{selected: selected, beacon_page: %{site: site}, form: form} = socket.assigns + + params = Map.merge(form.params, %{key => code}) |> IO.inspect(label: "params") + changeset = Content.change_js_hook(site, selected, params) |> IO.inspect(label: "changeset") + + socket = + socket + |> assign_form(changeset) + |> assign(unsaved_changes: !(changeset.changes == %{})) + + {:noreply, socket} + end + + def handle_event("create_new", _, socket) do + {:noreply, assign(socket, show_create_modal: true)} + end + + def handle_event("save_new", params, socket) do + %{beacon_page: %{site: site}} = socket.assigns + %{"js_hook" => %{"name" => name}} = params + + attrs = %{ + "name" => name, + "site" => site + } + + socket = + case Content.create_js_hook(site, attrs) do + {:ok, %{id: js_hook_id}} -> + socket + |> assign(js_hooks: Content.list_js_hooks(site)) + |> assign_selected(js_hook_id) + |> assign(show_create_modal: false) + |> push_navigate(to: beacon_live_admin_path(socket, site, "/hooks/#{js_hook_id}")) + + {:error, changeset} -> + assign(socket, create_form: to_form(changeset)) + end + + {:noreply, socket} + end + + def handle_event("save_changes", %{"js_hook" => params}, socket) do + %{selected: selected, beacon_page: %{site: site}} = socket.assigns + + # TODO: validate js code + + socket = + case Content.update_js_hook(site, selected, params) do + {:ok, updated_js_hook} -> + socket + |> assign_js_hook_update(updated_js_hook) + |> assign_selected(selected.id) + |> assign_form() + |> assign(unsaved_changes: false) + |> put_flash(:info, "JS Hook updated successfully") + + {:error, changeset} -> + changeset = Map.put(changeset, :action, :update) + assign(socket, form: to_form(changeset)) + end + + {:noreply, socket} + end + + def handle_event("delete", _, socket) do + {:noreply, assign(socket, show_delete_modal: true)} + end + + def handle_event("delete_confirm", _, socket) do + %{selected: js_hook, beacon_page: %{site: site}} = socket.assigns + + {:ok, _} = Content.delete_js_hook(site, js_hook) + + socket = + socket + |> assign(js_hooks: Content.list_js_hooks(site)) + |> push_patch(to: beacon_live_admin_path(socket, site, "/hooks")) + + {:noreply, socket} + 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_navigate(socket, to: socket.assigns.confirm_nav_path)} + end + + def handle_event("cancel_create", _params, socket) do + {:noreply, assign(socket, show_create_modal: false)} + end + + defp assign_selected(socket, nil) do + case socket.assigns.js_hooks do + [] -> assign(socket, selected: nil) + [hd | _] -> assign(socket, selected: hd) + end + end + + defp assign_selected(socket, id) when is_binary(id) do + selected = Enum.find(socket.assigns.js_hooks, &(&1.id == id)) + assign(socket, selected: selected) + end + + defp assign_form(socket) do + form = + case socket.assigns do + %{selected: nil} -> + nil + + %{selected: selected, beacon_page: %{site: site}} -> + site + |> Content.change_js_hook(selected) + |> to_form() + end + + assign(socket, form: form) + end + + defp assign_form(socket, changeset) do + assign(socket, form: to_form(changeset)) + end + + defp assign_js_hook_update(socket, updated_js_hook) do + %{id: js_hook_id} = updated_js_hook + + js_hooks = + Enum.map(socket.assigns.js_hooks, fn + %{id: ^js_hook_id} -> updated_js_hook + other -> other + end) + + assign(socket, js_hooks: js_hooks) + end + + def render(assigns) do + ~H""" +
You've made unsaved changes to this JS Hook!
+Navigating to another hook without saving will cause these changes to be lost.
+ <.button type="button" phx-click="stay_here" class="sui-secondary"> + Stay here + + <.button type="button" phx-click="discard_changes" class="sui-primary-destructive"> + Discard changes + + + + <.modal :if={@show_create_modal} id="create-modal" on_cancel={JS.push("cancel_create")} show> + <:title>New JS Hook + <.form :let={f} for={@create_form} id="create-form" phx-submit="save_new" class="px-4"> + <.input field={f[:name]} type="text" label="Hook name:" /> + <.button class="sui-primary mt-4">Save + + + + <.modal :if={@show_delete_modal} id="delete-modal" on_cancel={JS.push("delete_cancel")} show> +Are you sure you want to delete this JS Hook?
+ <.button type="button" id="confirm-delete-button" phx-click="delete_confirm" class="sui-primary-destructive"> + Delete + + <.button type="button" phx-click="delete_cancel" class="sui-secondary"> + Cancel + + + +mounted()
+ <%= template_error(@form[:mounted]) %> +beforeUpdate()
+ <%= template_error(@form[:beforeUpdate]) %> +updated()
+ <%= template_error(@form[:updated]) %> +destroyed()
+ <%= template_error(@form[:destroyed]) %> +disconnected()
+ <%= template_error(@form[:disconnected]) %> +reconnected()
+ <%= template_error(@form[:reconnected]) %> +