Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS Hooks #325

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions lib/beacon/live_admin/client/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions lib/beacon/live_admin/components/station_ui/html/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions lib/beacon/live_admin/live/home_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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>
<.link href={Router.beacon_live_admin_path(@socket, site, "/hooks")} class={nav_class()}>
JS Hooks
</.link>
</div>
</div>
<% end %>
Expand Down
335 changes: 335 additions & 0 deletions lib/beacon/live_admin/live/js_hook_editor_live/index.ex
Original file line number Diff line number Diff line change
@@ -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"""
<div>
<.header>
<%= @page_title %>
<:actions>
<.button type="button" id="new-js-hook-button" phx-click="create_new" class="sui-primary uppercase">
New JS Hook
</.button>
</:actions>
</.header>

<.main_content>
<.modal :if={@show_nav_modal} id="confirm-nav" on_cancel={JS.push("stay_here")} show>
<p>You've made unsaved changes to this JS Hook!</p>
<p>Navigating to another hook without saving will cause these changes to be lost.</p>
<.button type="button" phx-click="stay_here" class="sui-secondary">
Stay here
</.button>
<.button type="button" phx-click="discard_changes" class="sui-primary-destructive">
Discard changes
</.button>
</.modal>

<.modal :if={@show_create_modal} id="create-modal" on_cancel={JS.push("cancel_create")} show>
<:title>New JS Hook</:title>
<.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</.button>
</.form>
</.modal>

<.modal :if={@show_delete_modal} id="delete-modal" on_cancel={JS.push("delete_cancel")} show>
<p>Are you sure you want to delete this JS Hook?</p>
<.button type="button" id="confirm-delete-button" phx-click="delete_confirm" class="sui-primary-destructive">
Delete
</.button>
<.button type="button" phx-click="delete_cancel" class="sui-secondary">
Cancel
</.button>
</.modal>

<div class="grid items-start grid-cols-1 grid-rows-1 mx-auto gap-x-8 gap-y-8 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<div class="h-full lg:overflow-y-auto pb-4 lg:h-[calc(100vh_-_239px)]">
<.table id="js-hooks" rows={@js_hooks} row_click={fn row -> "select-#{row.id}" end}>
<:col :let={js_hook} label="name">
<%= Map.fetch!(js_hook, :name) %>
</:col>
</.table>
</div>

<div :if={@form} class="w-full col-span-2">
<.form :let={f} for={@form} id="js-hook-form" class="flex items-end gap-4 mb-2" phx-submit="save_changes">
<.input label="Name" field={f[:name]} type="text" />
<input type="hidden" name="js_hook[mounted]" id="js_hook-form_mounted" value={Phoenix.HTML.Form.input_value(f, :mounted)} />
<input type="hidden" name="js_hook[beforeUpdate]" id="js_hook-form_mounted" value={Phoenix.HTML.Form.input_value(f, :beforeUpdate)} />
<input type="hidden" name="js_hook[updated]" id="js_hook-form_mounted" value={Phoenix.HTML.Form.input_value(f, :updated)} />
<input type="hidden" name="js_hook[destroyed]" id="js_hook-form_mounted" value={Phoenix.HTML.Form.input_value(f, :destroyed)} />
<input type="hidden" name="js_hook[disconnected]" id="js_hook-form_mounted" value={Phoenix.HTML.Form.input_value(f, :disconnected)} />
<input type="hidden" name="js_hook[reconnected]" id="js_hook-form_mounted" value={Phoenix.HTML.Form.input_value(f, :reconnected)} />

<.button phx-disable-with="Saving..." class="sui-primary ml-auto">Save Changes</.button>
<.button id="delete-js-hook-button" type="button" phx-click="delete" class="sui-primary-destructive">Delete</.button>
</.form>

<p class="text-xl font-bold pt-2">mounted()</p>
<%= template_error(@form[:mounted]) %>
<div class="w-full mt-2 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="js_hook_mounted"
class="col-span-full lg:col-span-2"
value={@selected.mounted}
change="set_mounted"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "javascript"})}
/>
</div>
</div>

<p class="text-xl font-bold pt-2">beforeUpdate()</p>
<%= template_error(@form[:beforeUpdate]) %>
<div class="w-full mt-2 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="js_hook_beforeUpdate"
class="col-span-full lg:col-span-2"
value={@selected.beforeUpdate}
change="set_beforeUpdate"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "javascript"})}
/>
</div>
</div>

<p class="text-xl font-bold pt-2">updated()</p>
<%= template_error(@form[:updated]) %>
<div class="w-full mt-2 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="js_hook_updated"
class="col-span-full lg:col-span-2"
value={@selected.updated}
change="set_updated"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "javascript"})}
/>
</div>
</div>

<p class="text-xl font-bold pt-2">destroyed()</p>
<%= template_error(@form[:destroyed]) %>
<div class="w-full mt-2 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="js_hook_destroyed"
class="col-span-full lg:col-span-2"
value={@selected.destroyed}
change="set_destroyed"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "javascript"})}
/>
</div>
</div>

<p class="text-xl font-bold pt-2">disconnected()</p>
<%= template_error(@form[:disconnected]) %>
<div class="w-full mt-2 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="js_hook_disconnected"
class="col-span-full lg:col-span-2"
value={@selected.disconnected}
change="set_disconnected"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "javascript"})}
/>
</div>
</div>

<p class="text-xl font-bold pt-2">reconnected()</p>
<%= template_error(@form[:reconnected]) %>
<div class="w-full mt-2 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="js_hook_reconnected"
class="col-span-full lg:col-span-2"
value={@selected.reconnected}
change="set_reconnected"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "javascript"})}
/>
</div>
</div>
</div>
</div>
</.main_content>
</div>
"""
end
end
2 changes: 2 additions & 0 deletions lib/beacon/live_admin/live/page_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ defmodule Beacon.LiveAdmin.PageLive do
{_, "/info_handlers"} -> false
{"/error_pages", _} -> true
{_, "/error_pages"} -> false
{"/hooks", _} -> true
{_, "/hooks"} -> false
{a, b} -> a <= b
end
end)
Expand Down
Loading
Loading