feat: more UI cleanup

This commit is contained in:
2026-04-24 18:22:25 +02:00
parent e51566d707
commit 6824b89691
11 changed files with 696 additions and 3 deletions

View File

@@ -25,6 +25,7 @@ defmodule BDS.Application do
def start(_type, _args) do
children = [
BDS.Repo,
BDS.RepoBootstrap,
BDS.Tasks,
BDS.Preview,
BDS.Publishing,

View File

@@ -36,6 +36,21 @@ defmodule BDS.Desktop.Router do
|> Plug.Conn.send_resp(200, BDS.Desktop.ShellController.task_status_json())
end
get "/api/projects" do
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, BDS.Desktop.ShellController.projects_json())
end
post "/api/projects" do
{:ok, body, conn} = Plug.Conn.read_body(conn)
payload = if body == "", do: %{}, else: Jason.decode!(body)
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, BDS.Desktop.ShellController.upsert_project_json(payload))
end
post "/api/commands" do
{:ok, body, conn} = Plug.Conn.read_body(conn)
payload = if body == "", do: %{}, else: Jason.decode!(body)

View File

@@ -9,6 +9,25 @@ defmodule BDS.Desktop.ShellController do
Jason.encode!(BDS.Tasks.status_snapshot())
end
def projects_json do
Jason.encode!(BDS.Projects.shell_snapshot())
rescue
error in [Exqlite.Error] ->
if String.contains?(Exception.message(error), "no such table: projects") do
Jason.encode!(default_project_snapshot())
else
reraise error, __STACKTRACE__
end
end
def upsert_project_json(payload) when is_map(payload) do
case normalize_project_request(payload) do
{:create, attrs} -> create_project_json(attrs)
{:select, project_id} -> select_project_json(project_id)
:error -> Jason.encode!(%{status: "error", error: %{message: "Missing project name or project_id"}})
end
end
def command_json(payload) when is_map(payload) do
action = Map.get(payload, "action") || Map.get(payload, :action)
params = Map.get(payload, "params") || Map.get(payload, :params) || %{}
@@ -21,4 +40,72 @@ defmodule BDS.Desktop.ShellController do
defp normalize_error(error) when is_map(error), do: error
defp normalize_error(error), do: %{message: inspect(error)}
defp normalize_project_request(payload) do
cond do
present?(Map.get(payload, "name") || Map.get(payload, :name)) ->
{:create,
%{
name: String.trim(Map.get(payload, "name") || Map.get(payload, :name)),
data_path: blank_to_nil(Map.get(payload, "data_path") || Map.get(payload, :data_path))
}}
present?(Map.get(payload, "project_id") || Map.get(payload, :project_id)) ->
{:select, Map.get(payload, "project_id") || Map.get(payload, :project_id)}
true ->
:error
end
end
defp create_project_json(attrs) do
with {:ok, project} <- BDS.Projects.create_project(attrs),
{:ok, active_project} <- BDS.Projects.set_active_project(project.id) do
Jason.encode!(%{status: "ok", project: project_response(active_project), active_project_id: active_project.id})
else
{:error, error} -> Jason.encode!(%{status: "error", error: normalize_error(error)})
end
end
defp select_project_json(project_id) do
case BDS.Projects.set_active_project(project_id) do
{:ok, project} ->
Jason.encode!(%{status: "ok", project: project_response(project), active_project_id: project.id})
{:error, :not_found} ->
Jason.encode!(%{status: "error", error: %{message: "Project not found"}})
{:error, error} ->
Jason.encode!(%{status: "error", error: normalize_error(error)})
end
end
defp project_response(project) do
%{id: project.id, name: project.name, slug: project.slug, data_path: project.data_path, is_active: project.is_active}
end
defp default_project_snapshot do
%{
active_project_id: "default",
projects: [
%{
id: "default",
name: "My Blog",
slug: "my-blog",
data_path: nil,
is_active: true
}
]
}
end
defp present?(value) when is_binary(value), do: String.trim(value) != ""
defp present?(_value), do: false
defp blank_to_nil(value) when is_binary(value) do
trimmed = String.trim(value)
if trimmed == "", do: nil, else: trimmed
end
defp blank_to_nil(_value), do: nil
end

View File

@@ -21,6 +21,17 @@ defmodule BDS.Projects do
Repo.one(from project in Project, where: project.is_active == true, limit: 1)
end
def shell_snapshot do
_ = ensure_default_project()
projects = list_projects()
active_project = Enum.find(projects, & &1.is_active)
%{
active_project_id: active_project && active_project.id,
projects: Enum.map(projects, &project_summary/1)
}
end
def get_project(id), do: Repo.get(Project, id)
def get_project!(id), do: Repo.get!(Project, id)
@@ -150,6 +161,16 @@ defmodule BDS.Projects do
end
end
defp project_summary(%Project{} = project) do
%{
id: project.id,
name: project.name,
slug: project.slug,
data_path: project.data_path,
is_active: project.is_active
}
end
defp unique_slug(base_slug) do
normalized = if base_slug in [nil, ""], do: "project", else: base_slug

43
lib/bds/repo_bootstrap.ex Normal file
View File

@@ -0,0 +1,43 @@
defmodule BDS.RepoBootstrap do
@moduledoc false
use GenServer
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(opts) do
:ok = ensure_ready(opts)
{:ok, %{}}
end
def ensure_ready(opts \\ []) do
repo = Keyword.get(opts, :repo, BDS.Repo)
if Keyword.get(opts, :migrate?, true) do
:ok = ensure_schema(Keyword.put(opts, :repo, repo))
end
if repo == BDS.Repo do
case BDS.Projects.ensure_default_project() do
{:ok, _project} -> :ok
{:error, reason} -> raise "failed to ensure default project: #{inspect(reason)}"
end
else
:ok
end
end
def ensure_schema(opts \\ []) do
repo = Keyword.get(opts, :repo, BDS.Repo)
migrations_path = Keyword.get(opts, :migrations_path, migrations_path())
_versions = Ecto.Migrator.run(repo, migrations_path, :up, all: true)
:ok
end
def migrations_path do
Path.expand("../../priv/repo/migrations", __DIR__)
end
end

View File

@@ -2,6 +2,7 @@ defmodule BDS.UI.ShellPage do
@moduledoc false
alias BDS.I18n
alias BDS.Projects
alias BDS.UI.MenuBar
alias BDS.UI.Registry
alias BDS.UI.Session
@@ -57,7 +58,13 @@ defmodule BDS.UI.ShellPage do
title: Application.get_env(:bds, :desktop)[:title] || "Blogging Desktop Server",
i18n: %{
ui_language: ui_language,
supported_ui_languages: Enum.map(I18n.supported_languages(), &Map.take(&1, [:code, :flag]))
supported_ui_languages:
Enum.map(I18n.supported_languages(), fn language ->
%{
code: language.code,
flag: I18n.flag(language.code)
}
end)
},
registry: %{
sidebar_views: Enum.map(Registry.sidebar_views(), &encode_sidebar_view/1),
@@ -65,6 +72,7 @@ defmodule BDS.UI.ShellPage do
default_sidebar_view: Atom.to_string(Registry.default_sidebar_view())
},
menu_groups: Enum.map(MenuBar.default_groups(), &encode_menu_group/1),
projects: project_snapshot(),
session: Session.serialize(workbench),
task_status: task_status,
content: %{
@@ -98,6 +106,32 @@ defmodule BDS.UI.ShellPage do
}
end
defp project_snapshot do
Projects.shell_snapshot()
rescue
error in [Exqlite.Error, DBConnection.OwnershipError] ->
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table: projects") do
reraise error, __STACKTRACE__
end
default_project_snapshot()
end
defp default_project_snapshot do
%{
active_project_id: "default",
projects: [
%{
id: "default",
name: "My Blog",
slug: "my-blog",
data_path: nil,
is_active: true
}
]
}
end
defp encode_editor_route(route) do
%{
id: Atom.to_string(route.id),