From 0075f25ef7c489cf51a578b63aa4c23a01ea9ece Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 3 May 2026 10:00:22 +0200 Subject: [PATCH] chore: converted scripts and templates to live components --- lib/bds/desktop/shell_live.ex | 90 ++-- .../desktop/shell_live/code_entity_editor.ex | 397 ------------------ .../script_editor.html.heex | 49 --- .../template_editor.html.heex | 47 --- lib/bds/desktop/shell_live/index.html.heex | 8 +- lib/bds/desktop/shell_live/script_editor.ex | 293 +++++++++++++ .../script_editor.html.heex | 49 +++ lib/bds/desktop/shell_live/template_editor.ex | 241 +++++++++++ .../template_editor.html.heex | 47 +++ test/bds/desktop/shell_live_test.exs | 14 +- 10 files changed, 671 insertions(+), 564 deletions(-) delete mode 100644 lib/bds/desktop/shell_live/code_entity_editor_html/script_editor.html.heex delete mode 100644 lib/bds/desktop/shell_live/code_entity_editor_html/template_editor.html.heex create mode 100644 lib/bds/desktop/shell_live/script_editor.ex create mode 100644 lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex create mode 100644 lib/bds/desktop/shell_live/template_editor.ex create mode 100644 lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 1bbf505..2ee3e24 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -11,13 +11,14 @@ defmodule BDS.Desktop.ShellLive do alias BDS.Desktop.ShellLive.{ ChatEditor, - CodeEntityEditor, ImportEditor, MediaEditor, MenuEditor, MiscEditor, + ScriptEditor, SettingsEditor, - TagsEditor + TagsEditor, + TemplateEditor } alias BDS.Desktop.ShellLive.OverlayComponents, as: ShellOverlayComponents @@ -175,8 +176,6 @@ defmodule BDS.Desktop.ShellLive do |> assign(:media_editor_post_picker_queries, %{}) |> assign(:media_editor_save_states, %{}) |> assign(:media_editor_translation_forms, %{}) - |> assign(:script_editor_drafts, %{}) - |> assign(:template_editor_drafts, %{}) |> assign(:chat_editor_inputs, %{}) |> assign(:chat_model_selectors_open, %{}) |> assign(:chat_editor_requests, %{}) @@ -527,53 +526,6 @@ defmodule BDS.Desktop.ShellLive do {:noreply, apply_shell_command(socket, action)} end - def handle_event("change_script_editor", %{"script_editor" => params}, socket) do - {:noreply, CodeEntityEditor.update_script(socket, params, &reload_shell/2)} - end - - def handle_event("save_script_editor", _params, socket) do - {:noreply, CodeEntityEditor.save_script(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("publish_script_editor", %{"id" => _script_id}, socket) do - {:noreply, - CodeEntityEditor.publish_script(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("run_script_editor", _params, socket) do - {:noreply, CodeEntityEditor.run_script(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("check_script_editor", _params, socket) do - {:noreply, CodeEntityEditor.check_script(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("delete_script_editor", _params, socket) do - {:noreply, CodeEntityEditor.delete_script(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("change_template_editor", %{"template_editor" => params}, socket) do - {:noreply, CodeEntityEditor.update_template(socket, params, &reload_shell/2)} - end - - def handle_event("save_template_editor", _params, socket) do - {:noreply, CodeEntityEditor.save_template(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("publish_template_editor", %{"id" => _template_id}, socket) do - {:noreply, - CodeEntityEditor.publish_template(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("validate_template_editor", _params, socket) do - {:noreply, - CodeEntityEditor.validate_template(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("delete_template_editor", _params, socket) do - {:noreply, CodeEntityEditor.delete_template(socket, &reload_shell/2, &append_output_entry/5)} - end - def handle_event("change_chat_editor_input", %{"message" => message}, socket) do {:noreply, ChatEditor.update_input(socket, message, &reload_shell/2)} end @@ -1312,6 +1264,22 @@ defmodule BDS.Desktop.ShellLive do {:noreply, append_output_entry(socket, title, message, nil, level)} end + def handle_info({:script_editor_output, title, message, level}, socket) do + {:noreply, append_output_entry(socket, title, message, nil, level)} + end + + def handle_info({:template_editor_output, title, message, level}, socket) do + {:noreply, append_output_entry(socket, title, message, nil, level)} + end + + def handle_info(:reload_shell, socket) do + {:noreply, reload_shell(socket, socket.assigns.workbench)} + end + + def handle_info({:close_tab, type, id}, socket) do + {:noreply, reload_shell(socket, BDS.UI.Workbench.close_tab(socket.assigns.workbench, type, id))} + end + @impl true def render(assigns) do UILocale.put(assigns.page_language) @@ -1382,7 +1350,6 @@ defmodule BDS.Desktop.ShellLive do |> assign(:current_tab, current_tab(workbench)) |> assign_post_editor() |> assign_media_editor() - |> assign_code_entity_editor() |> assign_chat_editor() |> assign_import_editor() |> assign_misc_editor() @@ -1435,10 +1402,6 @@ defmodule BDS.Desktop.ShellLive do MediaEditor.assign_socket(socket) end - defp assign_code_entity_editor(socket) do - CodeEntityEditor.assign_socket(socket) - end - defp assign_chat_editor(socket) do ChatEditor.assign_socket(socket) end @@ -1621,12 +1584,14 @@ defmodule BDS.Desktop.ShellLive do socket end - defp save_current_tab(%{assigns: %{current_tab: %{type: :scripts}}} = socket) do - CodeEntityEditor.save_script(socket, &reload_shell/2, &append_output_entry/5) + defp save_current_tab(%{assigns: %{current_tab: %{type: :scripts, id: script_id}}} = socket) do + send_update(ScriptEditor, id: "script-editor-#{script_id}", action: :save) + socket end - defp save_current_tab(%{assigns: %{current_tab: %{type: :templates}}} = socket) do - CodeEntityEditor.save_template(socket, &reload_shell/2, &append_output_entry/5) + defp save_current_tab(%{assigns: %{current_tab: %{type: :templates, id: template_id}}} = socket) do + send_update(TemplateEditor, id: "template-editor-#{template_id}", action: :save) + socket end defp save_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench) @@ -1729,7 +1694,6 @@ defmodule BDS.Desktop.ShellLive do socket |> assign(:shell_overlay, nil) |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:scripts, script_id})) - |> assign(:script_editor_drafts, Map.delete(socket.assigns.script_editor_drafts, script_id)) |> reload_shell(workbench) {:error, reason} -> @@ -1748,10 +1712,6 @@ defmodule BDS.Desktop.ShellLive do socket |> assign(:shell_overlay, nil) |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:templates, template_id})) - |> assign( - :template_editor_drafts, - Map.delete(socket.assigns.template_editor_drafts, template_id) - ) |> reload_shell(workbench) {:error, reason} -> diff --git a/lib/bds/desktop/shell_live/code_entity_editor.ex b/lib/bds/desktop/shell_live/code_entity_editor.ex index e48847e..e69de29 100644 --- a/lib/bds/desktop/shell_live/code_entity_editor.ex +++ b/lib/bds/desktop/shell_live/code_entity_editor.ex @@ -1,397 +0,0 @@ -defmodule BDS.Desktop.ShellLive.CodeEntityEditor do - @moduledoc false - - use Phoenix.Component - - alias BDS.Desktop.ShellData - alias BDS.{MCP, Scripts, Scripting, Templates} - alias BDS.Scripts.Script - alias BDS.Templates.Template - - embed_templates("code_entity_editor_html/*") - - @spec assign_socket(term()) :: term() - def assign_socket(socket) do - socket - |> assign(:script_editor, build_script(socket.assigns)) - |> assign(:template_editor, build_template(socket.assigns)) - end - - @spec update_script(term(), term(), term()) :: term() - def update_script(socket, params, reload) do - %{id: script_id} = socket.assigns.current_tab - - socket - |> assign( - :script_editor_drafts, - Map.put(socket.assigns.script_editor_drafts, script_id, normalize_script_params(params)) - ) - |> reload.(socket.assigns.workbench) - end - - @spec save_script(term(), term(), term()) :: term() - def save_script(socket, reload, append_output) do - persist_script(socket, :save, reload, append_output) - end - - @spec publish_script(term(), term(), term()) :: term() - def publish_script(socket, reload, append_output) do - persist_script(socket, :publish, reload, append_output) - end - - @spec check_script(term(), term(), term()) :: term() - def check_script(socket, reload, append_output) do - %{id: script_id} = socket.assigns.current_tab - - case Scripts.get_script(script_id) do - nil -> - reload.(socket, socket.assigns.workbench) - - %Script{} = script -> - case Scripting.validate(current_script_draft(socket.assigns, script)["content"] || "") do - :ok -> - append_output.(socket, translated("Scripts"), translated("Syntax is valid")) - |> reload.(socket.assigns.workbench) - - {:error, reason} -> - append_output.(socket, translated("Scripts"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - end - - @spec run_script(term(), term(), term()) :: term() - def run_script(socket, reload, append_output) do - %{id: script_id} = socket.assigns.current_tab - - case Scripts.get_script(script_id) do - nil -> - reload.(socket, socket.assigns.workbench) - - %Script{} = script -> - draft = current_script_draft(socket.assigns, script) - - case Scripting.execute_project_script( - script.project_id, - draft["content"] || "", - draft["entrypoint"] || "main", - [] - ) do - {:ok, result} -> - socket - |> append_output.(translated("Scripts"), inspect(result)) - |> reload.(socket.assigns.workbench) - - {:error, reason} -> - socket - |> append_output.(translated("Scripts"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - end - - @spec delete_script(term(), term(), term()) :: term() - def delete_script(socket, reload, append_output) do - %{id: script_id} = socket.assigns.current_tab - - case Scripts.delete_script(script_id) do - {:ok, _deleted} -> - reload.(socket, BDS.UI.Workbench.close_tab(socket.assigns.workbench, :scripts, script_id)) - - {:error, reason} -> - append_output.(socket, translated("Scripts"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - - @spec update_template(term(), term(), term()) :: term() - def update_template(socket, params, reload) do - %{id: template_id} = socket.assigns.current_tab - - socket - |> assign( - :template_editor_drafts, - Map.put( - socket.assigns.template_editor_drafts, - template_id, - normalize_template_params(params) - ) - ) - |> reload.(socket.assigns.workbench) - end - - @spec save_template(term(), term(), term()) :: term() - def save_template(socket, reload, append_output) do - persist_template(socket, :save, reload, append_output) - end - - @spec publish_template(term(), term(), term()) :: term() - def publish_template(socket, reload, append_output) do - persist_template(socket, :publish, reload, append_output) - end - - @spec validate_template(term(), term(), term()) :: term() - def validate_template(socket, reload, append_output) do - %{id: template_id} = socket.assigns.current_tab - - case Templates.get_template(template_id) do - nil -> - reload.(socket, socket.assigns.workbench) - - %Template{} = template -> - case MCP.validate_template( - current_template_draft(socket.assigns, template)["content"] || "" - ) do - {:ok, %{valid: true}} -> - append_output.( - socket, - translated("Templates"), - translated("Template syntax is valid") - ) - |> reload.(socket.assigns.workbench) - - {:ok, %{valid: false, errors: errors}} -> - append_output.(socket, translated("Templates"), Enum.join(errors, "; "), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - end - - @spec delete_template(term(), term(), term()) :: term() - def delete_template(socket, reload, append_output) do - %{id: template_id} = socket.assigns.current_tab - - case Templates.delete_template(template_id, force: true) do - {:ok, _deleted} -> - reload.( - socket, - BDS.UI.Workbench.close_tab(socket.assigns.workbench, :templates, template_id) - ) - - {:error, reason} -> - append_output.(socket, translated("Templates"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - - @spec build_script(term()) :: term() - def build_script(%{current_tab: %{type: :scripts, id: script_id}} = assigns) do - case Scripts.get_script(script_id) do - nil -> - nil - - %Script{} = script -> - draft = current_script_draft(assigns, script) - - %{ - id: script.id, - title: draft["title"], - slug: draft["slug"], - kind: draft["kind"], - entrypoint: draft["entrypoint"], - enabled: draft["enabled"], - content: draft["content"], - entrypoints: discover_entrypoints(draft["content"]), - status: script.status || :draft, - can_publish?: script.status == :draft, - created_at: script.created_at, - updated_at: script.updated_at - } - end - end - - def build_script(_assigns), do: nil - - @spec build_template(term()) :: term() - def build_template(%{current_tab: %{type: :templates, id: template_id}} = assigns) do - case Templates.get_template(template_id) do - nil -> - nil - - %Template{} = template -> - draft = current_template_draft(assigns, template) - - %{ - id: template.id, - title: draft["title"], - slug: draft["slug"], - kind: draft["kind"], - enabled: draft["enabled"], - content: draft["content"], - status: template.status || :draft, - can_publish?: template.status == :draft, - created_at: template.created_at, - updated_at: template.updated_at - } - end - end - - def build_template(_assigns), do: nil - - @spec status_label(term()) :: term() - def status_label(status), do: ShellData.dashboard_status_label(status) - - @spec translated(term(), term()) :: term() - def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) - - @spec format_timestamp(term()) :: term() - def format_timestamp(nil), do: "" - def format_timestamp(timestamp), do: BDS.Persistence.timestamp_to_iso8601(timestamp) - - defp normalize_script_params(params) do - %{ - "title" => Map.get(params, "title", ""), - "slug" => Map.get(params, "slug", ""), - "kind" => Map.get(params, "kind", "utility"), - "entrypoint" => Map.get(params, "entrypoint", "main"), - "enabled" => Map.get(params, "enabled") in [true, "true", "on", "1", 1], - "content" => Map.get(params, "content", "") - } - end - - defp normalize_template_params(params) do - %{ - "title" => Map.get(params, "title", ""), - "slug" => Map.get(params, "slug", ""), - "kind" => Map.get(params, "kind", "post"), - "enabled" => Map.get(params, "enabled") in [true, "true", "on", "1", 1], - "content" => Map.get(params, "content", "") - } - end - - defp current_script_draft(assigns, %Script{} = script) do - Map.get(assigns.script_editor_drafts, script.id, %{ - "title" => script.title || "", - "slug" => script.slug || "", - "kind" => to_string(script.kind || :utility), - "entrypoint" => script.entrypoint || "main", - "enabled" => script.enabled != false, - "content" => script.content || "" - }) - end - - defp current_template_draft(assigns, %Template{} = template) do - Map.get(assigns.template_editor_drafts, template.id, %{ - "title" => template.title || "", - "slug" => template.slug || "", - "kind" => to_string(template.kind || :post), - "enabled" => template.enabled != false, - "content" => template.content || "" - }) - end - - defp script_attrs(draft) do - %{ - title: draft["title"], - slug: draft["slug"], - kind: BDS.BoundedAtoms.script_kind(draft["kind"], :utility), - entrypoint: draft["entrypoint"], - enabled: draft["enabled"], - content: draft["content"] - } - end - - defp template_attrs(draft) do - %{ - title: draft["title"], - slug: draft["slug"], - kind: normalize_template_kind(draft["kind"]), - enabled: draft["enabled"], - content: draft["content"] - } - end - - defp persist_script(socket, action, reload, append_output) do - %{id: script_id} = socket.assigns.current_tab - - case Scripts.get_script(script_id) do - nil -> - reload.(socket, socket.assigns.workbench) - - %Script{} = script -> - draft = current_script_draft(socket.assigns, script) - - case Scripting.validate(draft["content"] || "") do - :ok -> - case Scripts.update_script(script.id, script_attrs(draft)) - |> maybe_publish_script(script.id, action) do - {:ok, _updated} -> - socket - |> assign( - :script_editor_drafts, - Map.delete(socket.assigns.script_editor_drafts, script.id) - ) - |> reload.(socket.assigns.workbench) - - {:error, reason} -> - socket - |> append_output.(translated("Scripts"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - - {:error, reason} -> - socket - |> append_output.(translated("Scripts"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - end - - defp persist_template(socket, action, reload, append_output) do - %{id: template_id} = socket.assigns.current_tab - - case Templates.get_template(template_id) do - nil -> - reload.(socket, socket.assigns.workbench) - - %Template{} = template -> - draft = current_template_draft(socket.assigns, template) - - with {:ok, %{valid: true}} <- MCP.validate_template(draft["content"] || ""), - {:ok, _updated} <- - Templates.update_template(template.id, template_attrs(draft)) - |> maybe_publish_template(template.id, action) do - socket - |> assign( - :template_editor_drafts, - Map.delete(socket.assigns.template_editor_drafts, template.id) - ) - |> reload.(socket.assigns.workbench) - else - {:ok, %{valid: false, errors: errors}} -> - append_output.(socket, translated("Templates"), Enum.join(errors, "; "), nil, "error") - |> reload.(socket.assigns.workbench) - - {:error, reason} -> - append_output.(socket, translated("Templates"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - end - - defp maybe_publish_script({:ok, _script}, script_id, :publish), do: Scripts.publish_script(script_id) - defp maybe_publish_script(result, _script_id, _action), do: result - - defp maybe_publish_template({:ok, _template}, template_id, :publish), - do: Templates.publish_template(template_id) - - defp maybe_publish_template(result, _template_id, _action), do: result - - defp normalize_template_kind("post"), do: :post - defp normalize_template_kind("list"), do: :list - defp normalize_template_kind("not-found"), do: :"not-found" - defp normalize_template_kind("partial"), do: :partial - defp normalize_template_kind(_kind), do: :post - - defp discover_entrypoints(content) do - [ - "main" - | Regex.scan(~r/function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/, content || "", - capture: :all_but_first - ) - |> List.flatten() - |> Enum.reject(&(&1 == "main")) - ] - end -end diff --git a/lib/bds/desktop/shell_live/code_entity_editor_html/script_editor.html.heex b/lib/bds/desktop/shell_live/code_entity_editor_html/script_editor.html.heex deleted file mode 100644 index 7beb855..0000000 --- a/lib/bds/desktop/shell_live/code_entity_editor_html/script_editor.html.heex +++ /dev/null @@ -1,49 +0,0 @@ -
-
-
<%= @script_editor.title %>
-
- <%= status_label(@script_editor.status) %> - <%= if @script_editor.can_publish? do %> - - <% end %> - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
\ No newline at end of file diff --git a/lib/bds/desktop/shell_live/code_entity_editor_html/template_editor.html.heex b/lib/bds/desktop/shell_live/code_entity_editor_html/template_editor.html.heex deleted file mode 100644 index 9ee6339..0000000 --- a/lib/bds/desktop/shell_live/code_entity_editor_html/template_editor.html.heex +++ /dev/null @@ -1,47 +0,0 @@ -
-
-
<%= @template_editor.title %>
-
- <%= status_label(@template_editor.status) %> - <%= if @template_editor.can_publish? do %> - - <% end %> - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
\ No newline at end of file diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex index b85f7e5..26eaea1 100644 --- a/lib/bds/desktop/shell_live/index.html.heex +++ b/lib/bds/desktop/shell_live/index.html.heex @@ -409,11 +409,11 @@ <% @current_tab.type == :tags and @current_project -> %> <.live_component module={TagsEditor} id="tags-editor" project_id={@current_project.id} current_tab={@current_tab} tab_meta={@tab_meta} /> - <% @current_tab.type == :scripts and @script_editor -> %> - + <% @current_tab.type == :scripts -> %> + <.live_component module={ScriptEditor} id={"script-editor-#{@current_tab.id}"} current_tab={@current_tab} /> - <% @current_tab.type == :templates and @template_editor -> %> - + <% @current_tab.type == :templates -> %> + <.live_component module={TemplateEditor} id={"template-editor-#{@current_tab.id}"} current_tab={@current_tab} /> <% @current_tab.type == :chat and @chat_editor -> %> diff --git a/lib/bds/desktop/shell_live/script_editor.ex b/lib/bds/desktop/shell_live/script_editor.ex new file mode 100644 index 0000000..8d503a0 --- /dev/null +++ b/lib/bds/desktop/shell_live/script_editor.ex @@ -0,0 +1,293 @@ +defmodule BDS.Desktop.ShellLive.ScriptEditor do + @moduledoc false + + use Phoenix.LiveComponent + + alias BDS.{Scripts, Scripting} + alias BDS.Desktop.ShellData + alias BDS.Scripts.Script + + embed_templates("script_editor_html/*") + + @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} + @impl true + def update(%{action: :save} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action])) + |> do_save() + + {:ok, socket} + end + + def update(assigns, socket) do + socket = + socket + |> assign(assigns) + |> build_data() + + {:ok, socket} + end + + @spec render(map()) :: Phoenix.LiveView.Rendered.t() + @impl true + def render(%{script_editor: nil} = assigns), do: ~H"" + + def render(assigns) do + script_editor(assigns) + end + + @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) :: + {:noreply, Phoenix.LiveView.Socket.t()} + @impl true + def handle_event("change_script_editor", %{"script_editor" => params}, socket) do + socket = + socket + |> assign(:draft, normalize_params(params)) + |> build_data() + + {:noreply, socket} + end + + def handle_event("save_script_editor", _params, socket) do + {:noreply, do_save(socket)} + end + + def handle_event("publish_script_editor", _params, socket) do + {:noreply, do_publish(socket)} + end + + def handle_event("run_script_editor", _params, socket) do + {:noreply, do_run(socket)} + end + + def handle_event("check_script_editor", _params, socket) do + {:noreply, do_check(socket)} + end + + def handle_event("delete_script_editor", _params, socket) do + {:noreply, do_delete(socket)} + end + + defp build_data(socket) do + script_id = socket.assigns.current_tab.id + + case Scripts.get_script(script_id) do + nil -> + assign(socket, :script_editor, nil) + + %Script{} = script -> + draft = current_draft(socket.assigns, script) + + data = %{ + id: script.id, + title: draft["title"], + slug: draft["slug"], + kind: draft["kind"], + entrypoint: draft["entrypoint"], + enabled: draft["enabled"], + content: draft["content"], + entrypoints: discover_entrypoints(draft["content"]), + status: script.status || :draft, + can_publish?: script.status == :draft, + created_at: script.created_at, + updated_at: script.updated_at + } + + assign(socket, :script_editor, data) + end + end + + defp do_save(socket) do + script_id = socket.assigns.current_tab.id + + case Scripts.get_script(script_id) do + nil -> + socket + + %Script{} = script -> + draft = current_draft(socket.assigns, script) + + case Scripting.validate(draft["content"] || "") do + :ok -> + case Scripts.update_script(script.id, script_attrs(draft)) do + {:ok, _updated} -> + socket + |> assign(:draft, nil) + |> build_data() + |> notify_output(translated("Scripts"), translated("Script saved")) + |> notify_reload() + + {:error, reason} -> + socket + |> notify_output(translated("Scripts"), inspect(reason), "error") + |> notify_reload() + end + + {:error, reason} -> + socket + |> notify_output(translated("Scripts"), inspect(reason), "error") + |> notify_reload() + end + end + end + + defp do_publish(socket) do + script_id = socket.assigns.current_tab.id + + case Scripts.get_script(script_id) do + nil -> + socket + + %Script{} = script -> + draft = current_draft(socket.assigns, script) + + case Scripting.validate(draft["content"] || "") do + :ok -> + case Scripts.update_script(script.id, script_attrs(draft)) do + {:ok, _updated} -> + case Scripts.publish_script(script.id) do + {:ok, _published} -> + socket + |> assign(:draft, nil) + |> build_data() + |> notify_output(translated("Scripts"), translated("Script published")) + |> notify_reload() + + {:error, reason} -> + socket + |> notify_output(translated("Scripts"), inspect(reason), "error") + |> notify_reload() + end + + {:error, reason} -> + socket + |> notify_output(translated("Scripts"), inspect(reason), "error") + |> notify_reload() + end + + {:error, reason} -> + socket + |> notify_output(translated("Scripts"), inspect(reason), "error") + |> notify_reload() + end + end + end + + defp do_check(socket) do + script_id = socket.assigns.current_tab.id + + case Scripts.get_script(script_id) do + nil -> + socket + + %Script{} = script -> + case Scripting.validate(current_draft(socket.assigns, script)["content"] || "") do + :ok -> + notify_output(socket, translated("Scripts"), translated("Syntax is valid")) + + {:error, reason} -> + notify_output(socket, translated("Scripts"), inspect(reason), "error") + end + end + end + + defp do_run(socket) do + script_id = socket.assigns.current_tab.id + + case Scripts.get_script(script_id) do + nil -> + socket + + %Script{} = script -> + draft = current_draft(socket.assigns, script) + + case Scripting.execute_project_script( + script.project_id, + draft["content"] || "", + draft["entrypoint"] || "main", + [] + ) do + {:ok, result} -> + notify_output(socket, translated("Scripts"), inspect(result)) + + {:error, reason} -> + notify_output(socket, translated("Scripts"), inspect(reason), "error") + end + end + end + + defp do_delete(socket) do + script_id = socket.assigns.current_tab.id + + case Scripts.delete_script(script_id) do + {:ok, _deleted} -> + send(self(), {:close_tab, :scripts, script_id}) + socket + + {:error, reason} -> + socket + |> notify_output(translated("Scripts"), inspect(reason), "error") + |> notify_reload() + end + end + + defp current_draft(%{draft: draft}, _script) when is_map(draft), do: draft + + defp current_draft(_assigns, %Script{} = script) do + %{ + "title" => script.title || "", + "slug" => script.slug || "", + "kind" => to_string(script.kind || :utility), + "entrypoint" => script.entrypoint || "main", + "enabled" => script.enabled != false, + "content" => script.content || "" + } + end + + defp normalize_params(params) do + %{ + "title" => Map.get(params, "title", ""), + "slug" => Map.get(params, "slug", ""), + "kind" => Map.get(params, "kind", "utility"), + "entrypoint" => Map.get(params, "entrypoint", "main"), + "enabled" => Map.get(params, "enabled") in [true, "true", "on", "1", 1], + "content" => Map.get(params, "content", "") + } + end + + defp script_attrs(draft) do + %{ + title: draft["title"], + slug: draft["slug"], + kind: BDS.BoundedAtoms.script_kind(draft["kind"], :utility), + entrypoint: draft["entrypoint"], + enabled: draft["enabled"], + content: draft["content"] + } + end + + defp discover_entrypoints(content) do + [ + "main" + | Regex.scan(~r/function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/, content || "", + capture: :all_but_first + ) + |> List.flatten() + |> Enum.reject(&(&1 == "main")) + ] + end + + defp notify_output(socket, title, message, level \\ "info") do + send(self(), {:script_editor_output, title, message, level}) + socket + end + + defp notify_reload(socket) do + send(self(), :reload_shell) + socket + end + + defp translated(text, bindings \\ %{}), + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) +end \ No newline at end of file diff --git a/lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex b/lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex new file mode 100644 index 0000000..2db7e1c --- /dev/null +++ b/lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex @@ -0,0 +1,49 @@ +
+
+
<%= @script_editor.title %>
+
+ <%= BDS.Desktop.ShellData.dashboard_status_label(@script_editor.status) %> + <%= if @script_editor.can_publish? do %> + + <% end %> + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
diff --git a/lib/bds/desktop/shell_live/template_editor.ex b/lib/bds/desktop/shell_live/template_editor.ex new file mode 100644 index 0000000..af00ae9 --- /dev/null +++ b/lib/bds/desktop/shell_live/template_editor.ex @@ -0,0 +1,241 @@ +defmodule BDS.Desktop.ShellLive.TemplateEditor do + @moduledoc false + + use Phoenix.LiveComponent + + alias BDS.{MCP, Templates} + alias BDS.Desktop.ShellData + alias BDS.Templates.Template + + embed_templates("template_editor_html/*") + + @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} + @impl true + def update(%{action: :save} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action])) + |> do_save() + + {:ok, socket} + end + + def update(assigns, socket) do + socket = + socket + |> assign(assigns) + |> build_data() + + {:ok, socket} + end + + @spec render(map()) :: Phoenix.LiveView.Rendered.t() + @impl true + def render(%{template_editor: nil} = assigns), do: ~H"" + + def render(assigns) do + template_editor(assigns) + end + + @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) :: + {:noreply, Phoenix.LiveView.Socket.t()} + @impl true + def handle_event("change_template_editor", %{"template_editor" => params}, socket) do + socket = + socket + |> assign(:draft, normalize_params(params)) + |> build_data() + + {:noreply, socket} + end + + def handle_event("save_template_editor", _params, socket) do + {:noreply, do_save(socket)} + end + + def handle_event("publish_template_editor", _params, socket) do + {:noreply, do_publish(socket)} + end + + def handle_event("validate_template_editor", _params, socket) do + {:noreply, do_validate(socket)} + end + + def handle_event("delete_template_editor", _params, socket) do + {:noreply, do_delete(socket)} + end + + defp build_data(socket) do + template_id = socket.assigns.current_tab.id + + case Templates.get_template(template_id) do + nil -> + assign(socket, :template_editor, nil) + + %Template{} = template -> + draft = current_draft(socket.assigns, template) + + data = %{ + id: template.id, + title: draft["title"], + slug: draft["slug"], + kind: draft["kind"], + enabled: draft["enabled"], + content: draft["content"], + status: template.status || :draft, + can_publish?: template.status == :draft, + created_at: template.created_at, + updated_at: template.updated_at + } + + assign(socket, :template_editor, data) + end + end + + defp do_save(socket) do + template_id = socket.assigns.current_tab.id + + case Templates.get_template(template_id) do + nil -> + socket + + %Template{} = template -> + draft = current_draft(socket.assigns, template) + + with {:ok, %{valid: true}} <- MCP.validate_template(draft["content"] || ""), + {:ok, _updated} <- Templates.update_template(template.id, template_attrs(draft)) do + socket + |> assign(:draft, nil) + |> build_data() + |> notify_output(translated("Templates"), translated("Template saved")) + |> notify_reload() + else + {:ok, %{valid: false, errors: errors}} -> + socket + |> notify_output(translated("Templates"), Enum.join(errors, "; "), "error") + |> notify_reload() + + {:error, reason} -> + socket + |> notify_output(translated("Templates"), inspect(reason), "error") + |> notify_reload() + end + end + end + + defp do_publish(socket) do + template_id = socket.assigns.current_tab.id + + case Templates.get_template(template_id) do + nil -> + socket + + %Template{} = template -> + draft = current_draft(socket.assigns, template) + + with {:ok, %{valid: true}} <- MCP.validate_template(draft["content"] || ""), + {:ok, _updated} <- Templates.update_template(template.id, template_attrs(draft)), + {:ok, _published} <- Templates.publish_template(template.id) do + socket + |> assign(:draft, nil) + |> build_data() + |> notify_output(translated("Templates"), translated("Template published")) + |> notify_reload() + else + {:ok, %{valid: false, errors: errors}} -> + socket + |> notify_output(translated("Templates"), Enum.join(errors, "; "), "error") + |> notify_reload() + + {:error, reason} -> + socket + |> notify_output(translated("Templates"), inspect(reason), "error") + |> notify_reload() + end + end + end + + defp do_validate(socket) do + template_id = socket.assigns.current_tab.id + + case Templates.get_template(template_id) do + nil -> + socket + + %Template{} = template -> + case MCP.validate_template(current_draft(socket.assigns, template)["content"] || "") do + {:ok, %{valid: true}} -> + notify_output(socket, translated("Templates"), translated("Template syntax is valid")) + + {:ok, %{valid: false, errors: errors}} -> + notify_output(socket, translated("Templates"), Enum.join(errors, "; "), "error") + end + end + end + + defp do_delete(socket) do + template_id = socket.assigns.current_tab.id + + case Templates.delete_template(template_id, force: true) do + {:ok, _deleted} -> + send(self(), {:close_tab, :templates, template_id}) + socket + + {:error, reason} -> + socket + |> notify_output(translated("Templates"), inspect(reason), "error") + |> notify_reload() + end + end + + defp current_draft(%{draft: draft}, _template) when is_map(draft), do: draft + + defp current_draft(_assigns, %Template{} = template) do + %{ + "title" => template.title || "", + "slug" => template.slug || "", + "kind" => to_string(template.kind || :post), + "enabled" => template.enabled != false, + "content" => template.content || "" + } + end + + defp normalize_params(params) do + %{ + "title" => Map.get(params, "title", ""), + "slug" => Map.get(params, "slug", ""), + "kind" => Map.get(params, "kind", "post"), + "enabled" => Map.get(params, "enabled") in [true, "true", "on", "1", 1], + "content" => Map.get(params, "content", "") + } + end + + defp template_attrs(draft) do + %{ + title: draft["title"], + slug: draft["slug"], + kind: normalize_template_kind(draft["kind"]), + enabled: draft["enabled"], + content: draft["content"] + } + end + + defp normalize_template_kind("post"), do: :post + defp normalize_template_kind("list"), do: :list + defp normalize_template_kind("not-found"), do: :"not-found" + defp normalize_template_kind("partial"), do: :partial + defp normalize_template_kind(_kind), do: :post + + defp notify_output(socket, title, message, level \\ "info") do + send(self(), {:template_editor_output, title, message, level}) + socket + end + + defp notify_reload(socket) do + send(self(), :reload_shell) + socket + end + + defp translated(text, bindings \\ %{}), + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) +end \ No newline at end of file diff --git a/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex b/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex new file mode 100644 index 0000000..da3333a --- /dev/null +++ b/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex @@ -0,0 +1,47 @@ +
+
+
<%= @template_editor.title %>
+
+ <%= BDS.Desktop.ShellData.dashboard_status_label(@template_editor.status) %> + <%= if @template_editor.can_publish? do %> + + <% end %> + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index afb386a..2d94fe3 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -2344,7 +2344,12 @@ defmodule BDS.Desktop.ShellLiveTest do assert draft_script_html =~ ~s(data-testid="script-publish-button") assert draft_script_html =~ ~s(class="success") - draft_script_html = render_click(view, "publish_script_editor", %{"id" => draft_script.id}) + + draft_script_html = + view + |> element("[data-testid='script-publish-button']") + |> render_click() + assert Scripts.get_script(draft_script.id).status == :published refute draft_script_html =~ ~s(data-testid="script-publish-button") assert draft_script_html =~ ~s(data-testid="script-status-badge") @@ -2360,7 +2365,12 @@ defmodule BDS.Desktop.ShellLiveTest do assert draft_template_html =~ ~s(data-testid="template-publish-button") assert draft_template_html =~ ~s(class="success") - draft_template_html = render_click(view, "publish_template_editor", %{"id" => draft_template.id}) + + draft_template_html = + view + |> element("[data-testid='template-publish-button']") + |> render_click() + assert Templates.get_template(draft_template.id).status == :published refute draft_template_html =~ ~s(data-testid="template-publish-button") assert draft_template_html =~ ~s(data-testid="template-status-badge")