feat: preview working and template delete is in, too

This commit is contained in:
2026-04-26 19:53:29 +02:00
parent 09a1dcede3
commit 92e5c2ccfd
15 changed files with 401 additions and 42 deletions

View File

@@ -17,6 +17,7 @@ defmodule BDS.Desktop.ShellLive do
alias BDS.Posts.Post
alias BDS.Projects
alias BDS.Repo
alias BDS.Templates
alias BDS.UI.{Commands, MenuBar, Registry, Session, Workbench}
@refresh_interval 1_500
@@ -286,6 +287,34 @@ defmodule BDS.Desktop.ShellLive do
|> reload_shell(workbench)}
end
def handle_event("delete_sidebar_template", %{"id" => template_id}, socket) do
case Repo.get(Templates.Template, template_id) do
%Templates.Template{project_id: project_id} when project_id == socket.assigns.projects.active_project_id ->
case Templates.delete_template(template_id) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :templates, template_id)
tab_meta = Map.delete(socket.assigns.tab_meta, {:templates, template_id})
{:noreply,
socket
|> assign(:tab_meta, tab_meta)
|> reload_shell(workbench)}
{:error, reason} ->
{:noreply,
socket
|> append_output_entry(translated("Delete") <> " " <> translated("Template"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)}
end
_other ->
{:noreply,
socket
|> append_output_entry(translated("Delete") <> " " <> translated("Template"), inspect(:not_found), nil, "error")
|> reload_shell(socket.assigns.workbench)}
end
end
def handle_event("toggle_offline_mode", _params, socket) do
socket = assign(socket, :offline_mode, not socket.assigns.offline_mode)
{:noreply, reload_shell(socket, socket.assigns.workbench)}

View File

@@ -350,27 +350,65 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
defp render_entity_sidebar(assigns) do
~H"""
<%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
<div class="settings-nav-list">
<div class={if(template_sidebar?(@sidebar_data), do: "chat-list-items", else: "settings-nav-list")}>
<%= for item <- Map.get(@sidebar_data, :items, []) do %>
<button
class={["chat-list-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "active")]}
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={item.title}
data-open-subtitle={translated(item.meta || "")}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
phx-value-subtitle={translated(item.meta || "")}
>
<span class="chat-item-content">
<span class="chat-item-title"><%= item.title %></span>
<span class="chat-item-date"><%= translated(item.meta || "") %></span>
</span>
</button>
<%= if item.route == "templates" do %>
<div
class={["chat-list-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "active")]}
data-item-id={item.id}
>
<button
class="chat-item-open"
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={item.title}
data-open-subtitle={translated(item.meta || "")}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
phx-value-subtitle={translated(item.meta || "")}
>
<span class="chat-item-content">
<span class="chat-item-title"><%= item.title %></span>
<span class="chat-item-date"><%= translated(item.meta || "") %></span>
</span>
</button>
<button
class="chat-item-delete"
data-testid="sidebar-delete-template"
data-item-id={item.id}
type="button"
title={translated("Delete") <> " " <> translated("Template")}
phx-click="delete_sidebar_template"
phx-value-id={item.id}
>
×
</button>
</div>
<% else %>
<button
class={["chat-list-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "active")]}
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={item.title}
data-open-subtitle={translated(item.meta || "")}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
phx-value-subtitle={translated(item.meta || "")}
>
<span class="chat-item-content">
<span class="chat-item-title"><%= item.title %></span>
<span class="chat-item-date"><%= translated(item.meta || "") %></span>
</span>
</button>
<% end %>
<% end %>
</div>
<% else %>
@@ -426,6 +464,8 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
defp template_sidebar?(sidebar_data), do: Map.get(sidebar_data, :title) == "Templates"
defp group_year_month_counts(entries) do
entries
|> Enum.group_by(& &1.year)

View File

@@ -52,7 +52,13 @@ defmodule BDS.Rendering do
case select_template(project_id, kind, slug) do
%Template{} = template ->
published_template_body(template)
case published_template_body(template) do
{:ok, _source} = ok ->
ok
{:error, reason} = error ->
maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
end
nil ->
load_bundled_template_source(project, kind, slug)
@@ -70,6 +76,23 @@ defmodule BDS.Rendering do
)
end
defp select_template(project_id, :post, nil) do
case StarterTemplates.default_slug(:post) do
nil ->
nil
default_slug ->
Repo.one(
from template in Template,
where:
template.project_id == ^project_id and template.kind == :post and
template.status == :published and
template.enabled == true and template.slug == ^default_slug,
limit: 1
)
end
end
defp select_template(project_id, kind, nil) do
Repo.one(
from template in Template,
@@ -139,11 +162,24 @@ defmodule BDS.Rendering do
{:error, :template_not_found}
end
defp maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
when reason in [:enoent, :template_not_found] do
if template.content in [nil, ""] and StarterTemplates.default_template?(kind, template.slug) do
load_bundled_template_source(project, kind, slug)
else
error
end
end
defp maybe_load_bundled_template_source(_project, _kind, _slug, _template, _reason, error),
do: error
defp bundled_template_slug(_kind, slug) when is_binary(slug) and slug != "", do: slug
defp bundled_template_slug(kind, _slug), do: StarterTemplates.default_slug(kind)
defp post_assigns(project_id, assigns) do
metadata = project_metadata(project_id)
template_context = template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
@@ -156,9 +192,16 @@ defmodule BDS.Rendering do
post_tags = Map.get(post_record || %{}, :tags, []) || []
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = canonical_media_path_by_source_path(project_id)
raw_content = Map.get(assigns, :content, Map.get(assigns, "content"))
rendered_content = render_post_content(raw_content, canonical_post_paths, canonical_media_paths, language, template_context)
incoming_links = link_contexts(project_id, post_id, :incoming, main_language)
outgoing_links = link_contexts(project_id, post_id, :outgoing, main_language)
post_assigns =
assigns
|> Map.put(:content, rendered_content)
|> Map.put(:raw_content, raw_content)
%{
language: language,
language_prefix:
@@ -196,26 +239,35 @@ defmodule BDS.Rendering do
backlinks: backlinks(incoming_links),
canonical_post_path_by_slug: canonical_post_paths,
canonical_media_path_by_source_path: canonical_media_paths,
post_data_json_by_id: post_data_json(assigns, post_record),
post: build_post_context(assigns, post_record, incoming_links, outgoing_links)
post_data_json_by_id: post_data_json(post_assigns, post_record),
post: build_post_context(post_assigns, post_record, incoming_links, outgoing_links)
}
end
defp list_assigns(project_id, assigns) do
metadata = project_metadata(project_id)
template_context = template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
main_language = metadata.main_language || language
posts = normalize_list_posts(Map.get(assigns, :posts, Map.get(assigns, "posts", [])))
archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = canonical_media_path_by_source_path(project_id)
posts =
normalize_list_posts(
Map.get(assigns, :posts, Map.get(assigns, "posts", [])),
canonical_post_paths,
canonical_media_paths,
language,
template_context
)
pagination =
normalize_pagination(Map.get(assigns, :pagination, Map.get(assigns, "pagination")), posts)
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = canonical_media_path_by_source_path(project_id)
day_blocks = build_day_blocks(posts)
min_date = min_date(posts)
max_date = max_date(posts)
@@ -551,20 +603,29 @@ defmodule BDS.Rendering do
post_path(post, prefix)
end
defp normalize_list_posts(posts) do
defp normalize_list_posts(posts, canonical_post_paths, canonical_media_paths, language, template_context) do
Enum.map(posts, fn post ->
post_record = load_post_record(post)
raw_content =
Map.get(
post,
:content,
Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
)
%{
id: Map.get(post, :id, Map.get(post, "id")),
slug: Map.get(post, :slug, Map.get(post, "slug")),
title: Map.get(post, :title, Map.get(post, "title")),
content:
Map.get(
post,
:content,
Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
render_post_content(
raw_content,
canonical_post_paths,
canonical_media_paths,
language,
template_context
),
raw_content: raw_content,
excerpt:
Map.get(post, :excerpt, Map.get(post, "excerpt", Map.get(post_record || %{}, :excerpt))),
author: Map.get(post, :author, Map.get(post, "author", Map.get(post_record || %{}, :author))),
@@ -602,6 +663,7 @@ defmodule BDS.Rendering do
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
title: Map.get(assigns, :title, Map.get(assigns, "title")),
content: Map.get(assigns, :content, Map.get(assigns, "content")),
raw_content: Map.get(assigns, :raw_content, Map.get(assigns, "raw_content")),
excerpt:
Map.get(
assigns,
@@ -634,6 +696,20 @@ defmodule BDS.Rendering do
}
end
defp render_post_content(content, canonical_post_paths, canonical_media_paths, language, template_context) do
Filters.render_markdown(content, canonical_post_paths, canonical_media_paths, language, template_context)
end
defp template_render_context(project_id) do
project = Projects.get_project!(project_id)
Liquex.Context.new(%{},
static_environment: %{},
filter_module: Filters,
file_system: FileSystem.new(StarterTemplates.template_roots(project))
)
end
defp normalize_pagination(nil, posts) do
total_items = length(posts)

View File

@@ -26,6 +26,10 @@ defmodule BDS.Rendering.Filters do
_language_prefix,
context
) do
render_markdown(value, canonical_post_paths, canonical_media_paths, language, context)
end
def render_markdown(value, canonical_post_paths, canonical_media_paths, language, context) do
value
|> to_string()
|> replace_built_in_macros(language, context)

View File

@@ -155,6 +155,8 @@ defmodule BDS.Templates do
template
end)
remove_stale_published_templates(project_id, project, template_paths)
{:ok, templates}
end
@@ -380,6 +382,29 @@ defmodule BDS.Templates do
|> Repo.insert_or_update!()
end
defp remove_stale_published_templates(project_id, project, template_paths) do
tracked_paths =
template_paths
|> Enum.map(&Path.relative_to(&1, Projects.project_data_dir(project)))
|> MapSet.new()
Repo.all(
from template in Template,
where:
template.project_id == ^project_id and
template.status == :published and
template.file_path != "" and
not is_nil(template.file_path)
)
|> Enum.reject(&(MapSet.member?(tracked_paths, &1.file_path) or File.exists?(full_file_path(project_id, &1.file_path))))
|> Enum.each(fn template ->
clear_template_references(template)
Repo.delete!(template)
end)
:ok
end
defp parse_template_kind(kind) when is_atom(kind), do: kind
defp parse_template_kind("post"), do: :post
defp parse_template_kind("list"), do: :list