diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex
index 0166020..ac86acf 100644
--- a/lib/bds/desktop/shell_live.ex
+++ b/lib/bds/desktop/shell_live.ex
@@ -885,6 +885,11 @@ defmodule BDS.Desktop.ShellLive 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
@@ -905,6 +910,11 @@ defmodule BDS.Desktop.ShellLive 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)}
diff --git a/lib/bds/desktop/shell_live/code_entity_editor.ex b/lib/bds/desktop/shell_live/code_entity_editor.ex
index a1e0718..e48847e 100644
--- a/lib/bds/desktop/shell_live/code_entity_editor.ex
+++ b/lib/bds/desktop/shell_live/code_entity_editor.ex
@@ -31,38 +31,12 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
@spec save_script(term(), term(), term()) :: term()
def save_script(socket, reload, append_output) do
- %{id: script_id} = socket.assigns.current_tab
+ persist_script(socket, :save, reload, append_output)
+ end
- 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)) 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
+ @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()
@@ -148,33 +122,12 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
@spec save_template(term(), term(), term()) :: term()
def save_template(socket, reload, append_output) do
- %{id: template_id} = socket.assigns.current_tab
+ persist_template(socket, :save, reload, append_output)
+ end
- 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)) 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
+ @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()
@@ -239,6 +192,8 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
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
}
@@ -263,6 +218,8 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
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
}
@@ -271,6 +228,9 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
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())
@@ -342,6 +302,82 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
}
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"
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
index 877fddd..7beb855 100644
--- 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
@@ -1,10 +1,17 @@
-
+
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
index 1c5e6ee..9ee6339 100644
--- 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
@@ -1,9 +1,16 @@
-
+
diff --git a/lib/bds/scripts.ex b/lib/bds/scripts.ex
index be3cac8..d420261 100644
--- a/lib/bds/scripts.ex
+++ b/lib/bds/scripts.ex
@@ -43,7 +43,12 @@ defmodule BDS.Scripts do
end
@spec get_script(String.t()) :: Script.t() | nil
- def get_script(script_id), do: Repo.get(Script, script_id)
+ def get_script(script_id) do
+ case Repo.get(Script, script_id) do
+ %Script{} = script -> hydrate_script_content(script)
+ nil -> nil
+ end
+ end
@spec publish_script(String.t()) :: script_result() | {:error, :not_found}
def publish_script(script_id) do
@@ -91,7 +96,8 @@ defmodule BDS.Scripts do
script.slug
end
- content_changed? = has_attr?(attrs, :content) and attr(attrs, :content) != script.content
+ content_changed? =
+ has_attr?(attrs, :content) and attr(attrs, :content) != effective_script_content(script)
now = Persistence.now_ms()
updates =
@@ -294,6 +300,24 @@ defmodule BDS.Scripts do
end
end
+ defp effective_script_content(%Script{} = script) do
+ case hydrate_script_content(script) do
+ %Script{content: content} when is_binary(content) -> content
+ _other -> ""
+ end
+ end
+
+ defp hydrate_script_content(%Script{} = script) do
+ case script do
+ %Script{content: content} when is_binary(content) -> script
+ %Script{status: :published, file_path: file_path} when file_path not in [nil, ""] ->
+ %{script | content: published_script_body(script)}
+
+ _other ->
+ script
+ end
+ end
+
defp upsert_script_from_file(project_id, project, path) do
contents = File.read!(path)
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
diff --git a/lib/bds/templates.ex b/lib/bds/templates.ex
index c457d07..65a341d 100644
--- a/lib/bds/templates.ex
+++ b/lib/bds/templates.ex
@@ -43,7 +43,12 @@ defmodule BDS.Templates do
end
@spec get_template(String.t()) :: Template.t() | nil
- def get_template(template_id), do: Repo.get(Template, template_id)
+ def get_template(template_id) do
+ case Repo.get(Template, template_id) do
+ %Template{} = template -> hydrate_template_content(template)
+ nil -> nil
+ end
+ end
@spec publish_template(String.t()) :: template_result() | {:error, :not_found}
def publish_template(template_id) do
@@ -97,7 +102,7 @@ defmodule BDS.Templates do
end
content_changed? =
- has_attr?(attrs, :content) and attr(attrs, :content) != template.content
+ has_attr?(attrs, :content) and attr(attrs, :content) != effective_template_content(template)
slug_changed? = next_slug != template.slug
now = Persistence.now_ms()
@@ -458,6 +463,24 @@ defmodule BDS.Templates do
end
end
+ defp effective_template_content(%Template{} = template) do
+ case hydrate_template_content(template) do
+ %Template{content: content} when is_binary(content) -> content
+ _other -> ""
+ end
+ end
+
+ defp hydrate_template_content(%Template{} = template) do
+ case template do
+ %Template{content: content} when is_binary(content) -> template
+ %Template{status: :published, file_path: file_path} when file_path not in [nil, ""] ->
+ %{template | content: published_template_body(template)}
+
+ _other ->
+ template
+ end
+ end
+
defp upsert_template_from_file(project_id, project, path) do
contents = File.read!(path)
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
diff --git a/priv/ui/app.css b/priv/ui/app.css
index 3e0324b..cb1b19a 100644
--- a/priv/ui/app.css
+++ b/priv/ui/app.css
@@ -899,7 +899,9 @@ button svg * {
border-bottom: 1px solid var(--vscode-panel-border);
}
-.post-editor.editor {
+.post-editor.editor,
+.scripts-view-shell.editor,
+.templates-view-shell.editor {
flex: 1;
display: flex;
flex-direction: column;
@@ -907,7 +909,9 @@ button svg * {
overflow: hidden;
}
-.post-editor .editor-header {
+.post-editor .editor-header,
+.scripts-view-shell.editor .editor-header,
+.templates-view-shell.editor .editor-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -918,14 +922,18 @@ button svg * {
border-bottom: 1px solid var(--vscode-panel-border);
}
-.post-editor .editor-tabs {
+.post-editor .editor-tabs,
+.scripts-view-shell.editor .editor-tabs,
+.templates-view-shell.editor .editor-tabs {
display: flex;
align-items: center;
gap: 2px;
min-width: 0;
}
-.post-editor .editor-tab {
+.post-editor .editor-tab,
+.scripts-view-shell.editor .editor-tab,
+.templates-view-shell.editor .editor-tab {
display: flex;
align-items: center;
gap: 6px;
@@ -937,12 +945,16 @@ button svg * {
border-radius: 4px 4px 0 0;
}
-.post-editor .editor-tab.active {
+.post-editor .editor-tab.active,
+.scripts-view-shell.editor .editor-tab.active,
+.templates-view-shell.editor .editor-tab.active {
background-color: var(--vscode-tab-activeBackground);
color: var(--vscode-tab-activeForeground);
}
-.post-editor .editor-tab-title {
+.post-editor .editor-tab-title,
+.scripts-view-shell.editor .editor-tab-title,
+.templates-view-shell.editor .editor-tab-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -960,7 +972,9 @@ button svg * {
white-space: nowrap;
}
-.post-editor .editor-actions {
+.post-editor .editor-actions,
+.scripts-view-shell.editor .editor-actions,
+.templates-view-shell.editor .editor-actions {
display: flex;
align-items: center;
gap: 8px;
@@ -1047,7 +1061,9 @@ button svg * {
opacity: 0.7;
}
-.post-editor .status-badge {
+.post-editor .status-badge,
+.scripts-view-shell.editor .status-badge,
+.templates-view-shell.editor .status-badge {
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
@@ -1055,17 +1071,23 @@ button svg * {
text-transform: uppercase;
}
-.post-editor .status-badge.status-draft {
+.post-editor .status-badge.status-draft,
+.scripts-view-shell.editor .status-badge.status-draft,
+.templates-view-shell.editor .status-badge.status-draft {
background-color: rgba(204, 167, 0, 0.2);
color: var(--vscode-notificationsWarningIcon-foreground, var(--vscode-editorWarning-foreground));
}
-.post-editor .status-badge.status-published {
+.post-editor .status-badge.status-published,
+.scripts-view-shell.editor .status-badge.status-published,
+.templates-view-shell.editor .status-badge.status-published {
background-color: rgba(115, 201, 145, 0.2);
color: var(--vscode-testing-iconPassed);
}
-.post-editor .status-badge.status-archived {
+.post-editor .status-badge.status-archived,
+.scripts-view-shell.editor .status-badge.status-archived,
+.templates-view-shell.editor .status-badge.status-archived {
background-color: rgba(133, 133, 133, 0.2);
color: var(--vscode-descriptionForeground);
}
@@ -1668,6 +1690,8 @@ button svg * {
@media (max-width: 980px) {
.post-editor .editor-header,
+ .scripts-view-shell.editor .editor-header,
+ .templates-view-shell.editor .editor-header,
.post-editor .metadata-toggle-header,
.post-editor .editor-toolbar {
display: flex;
@@ -1686,7 +1710,9 @@ button svg * {
}
.post-editor .editor-toolbar-right,
- .post-editor .editor-actions {
+ .post-editor .editor-actions,
+ .scripts-view-shell.editor .editor-actions,
+ .templates-view-shell.editor .editor-actions {
justify-content: flex-start;
}
}
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs
index bdfc499..3e69be3 100644
--- a/test/bds/desktop/shell_live_test.exs
+++ b/test/bds/desktop/shell_live_test.exs
@@ -2227,6 +2227,122 @@ defmodule BDS.Desktop.ShellLiveTest do
refute html =~ ~s(phx-value-mode="visual")
end
+ test "script and template editors surface lifecycle state, load published file content, and allow publishing drafts",
+ %{project: project} do
+ {:ok, draft_script} =
+ Scripts.create_script(%{
+ project_id: project.id,
+ title: "Draft Utility",
+ kind: :utility,
+ content: "function main() return 'draft' end"
+ })
+
+ {:ok, published_script_seed} =
+ Scripts.create_script(%{
+ project_id: project.id,
+ title: "Published Utility",
+ kind: :utility,
+ content: "function main() return 'published script' end"
+ })
+
+ assert {:ok, _published_script} = Scripts.publish_script(published_script_seed.id)
+ published_script = Scripts.get_script(published_script_seed.id)
+
+ {:ok, draft_template} =
+ Templates.create_template(%{
+ project_id: project.id,
+ title: "Draft Template",
+ kind: :post,
+ content: "
draft template"
+ })
+
+ {:ok, published_template_seed} =
+ Templates.create_template(%{
+ project_id: project.id,
+ title: "Published Template",
+ kind: :post,
+ content: "
published template"
+ })
+
+ assert {:ok, _published_template} = Templates.publish_template(published_template_seed.id)
+ published_template = Templates.get_template(published_template_seed.id)
+
+ {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
+
+ published_script_html =
+ render_click(view, "pin_sidebar_item", %{
+ "route" => "scripts",
+ "id" => published_script.id,
+ "title" => published_script.title,
+ "subtitle" => "published"
+ })
+
+ assert published_script_html =~ ~s(class="scripts-view-shell editor")
+ assert published_script_html =~ ~s(data-testid="script-editor")
+ assert published_script_html =~ ~s(data-testid="script-status-badge")
+ assert published_script_html =~ ~s(class="status-badge status-published")
+ assert published_script_html =~ ~s(class="secondary scripts-save-button")
+ assert published_script_html =~ ~s(class="secondary scripts-run-button")
+ assert published_script_html =~ ~s(class="secondary scripts-check-button")
+ assert published_script_html =~ "published"
+
+ assert published_script_html =~ "published script"
+
+ refute published_script_html =~ ~s(data-testid="script-publish-button")
+
+ published_template_html =
+ render_click(view, "pin_sidebar_item", %{
+ "route" => "templates",
+ "id" => published_template.id,
+ "title" => published_template.title,
+ "subtitle" => "published"
+ })
+
+ assert published_template_html =~ ~s(class="templates-view-shell editor")
+ assert published_template_html =~ ~s(data-testid="template-editor")
+ assert published_template_html =~ ~s(data-testid="template-status-badge")
+ assert published_template_html =~ ~s(class="status-badge status-published")
+ assert published_template_html =~ ~s(class="secondary templates-save-button")
+ assert published_template_html =~ ~s(class="secondary templates-validate-button")
+ assert published_template_html =~ "published"
+
+ assert published_template_html =~ "published template"
+
+ refute published_template_html =~ ~s(data-testid="template-publish-button")
+
+ draft_script_html =
+ render_click(view, "pin_sidebar_item", %{
+ "route" => "scripts",
+ "id" => draft_script.id,
+ "title" => draft_script.title,
+ "subtitle" => "draft"
+ })
+
+ 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})
+ 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")
+ assert draft_script_html =~ "published"
+
+ draft_template_html =
+ render_click(view, "pin_sidebar_item", %{
+ "route" => "templates",
+ "id" => draft_template.id,
+ "title" => draft_template.title,
+ "subtitle" => "draft"
+ })
+
+ 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})
+ 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")
+ assert draft_template_html =~ "published"
+ end
+
test "media tabs render a real editor and drive explicit save flows", %{
project: project,
temp_dir: temp_dir
@@ -2505,7 +2621,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => script.slug
})
- assert script_html =~ ~s(class="scripts-view-shell")
+ assert script_html =~ ~s(class="scripts-view-shell editor")
assert script_html =~ "scripts-monaco"
assert script_html =~ ~s(data-monaco-language="lua")
assert script_html =~ ~s(data-monaco-word-wrap="on")
@@ -2520,7 +2636,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => template.slug
})
- assert template_html =~ ~s(class="templates-view-shell")
+ assert template_html =~ ~s(class="templates-view-shell editor")
assert template_html =~ "templates-monaco"
assert template_html =~ ~s(data-monaco-language="liquid")
assert template_html =~ ~s(data-monaco-word-wrap="on")