296 lines
9.2 KiB
Elixir
296 lines
9.2 KiB
Elixir
defmodule BDS.UI.Sidebar do
|
|
@moduledoc false
|
|
|
|
import Ecto.Query
|
|
|
|
alias BDS.AI.ChatConversation
|
|
alias BDS.Media.Media
|
|
alias BDS.Posts.Post
|
|
alias BDS.Posts.Translation
|
|
alias BDS.Repo
|
|
alias BDS.Scripts.Script
|
|
alias BDS.Tags.Tag
|
|
alias BDS.Templates.Template
|
|
|
|
@page_category "page"
|
|
|
|
def snapshot(nil), do: empty_snapshot()
|
|
|
|
def snapshot(project_id) when is_binary(project_id) do
|
|
posts = list_posts(project_id)
|
|
translation_counts = translation_counts(project_id)
|
|
media_items = list_media(project_id)
|
|
scripts = list_scripts(project_id)
|
|
templates = list_templates(project_id)
|
|
tags = list_tags(project_id)
|
|
conversations = list_conversations()
|
|
|
|
%{
|
|
"posts" => posts_view(posts, translation_counts, false),
|
|
"pages" => posts_view(posts, translation_counts, true),
|
|
"media" => media_view(media_items),
|
|
"scripts" => entity_list_view("Scripts", "Automation helpers", "scripts", scripts),
|
|
"templates" =>
|
|
entity_list_view("Templates", "Site rendering", "templates", templates),
|
|
"tags" => tags_nav_view(tags),
|
|
"chat" => entity_list_view("Chat", "AI conversations", "chat", conversations),
|
|
"import" => entity_list_view("Import", "Import definitions", "import", []),
|
|
"git" => git_view(),
|
|
"settings" => settings_nav_view()
|
|
}
|
|
end
|
|
|
|
def empty_snapshot do
|
|
%{
|
|
"posts" => posts_view([], %{}, false),
|
|
"pages" => posts_view([], %{}, true),
|
|
"media" => media_view([]),
|
|
"scripts" => entity_list_view("Scripts", "Automation helpers", "scripts", []),
|
|
"templates" => entity_list_view("Templates", "Site rendering", "templates", []),
|
|
"tags" => tags_nav_view([]),
|
|
"chat" => entity_list_view("Chat", "AI conversations", "chat", []),
|
|
"import" => entity_list_view("Import", "Import definitions", "import", []),
|
|
"git" => git_view(),
|
|
"settings" => settings_nav_view()
|
|
}
|
|
end
|
|
|
|
defp posts_view(posts, translation_counts, pages?) do
|
|
filtered_posts = Enum.filter(posts, &(page_post?(&1) == pages?))
|
|
grouped_posts = group_posts(filtered_posts)
|
|
|
|
%{
|
|
title: if(pages?, do: "Pages", else: "Posts"),
|
|
subtitle: if(pages?, do: "Standalone pages", else: "Drafts, published entries, and archive history"),
|
|
layout: "post_list",
|
|
empty_message: if(pages?, do: "No items", else: "No items"),
|
|
sections: [
|
|
build_post_section("Drafts", :draft, grouped_posts.draft, translation_counts, false),
|
|
build_post_section("Published", :published, grouped_posts.published, translation_counts, true),
|
|
build_post_section("Archived", :archived, grouped_posts.archived, translation_counts, false)
|
|
]
|
|
}
|
|
end
|
|
|
|
defp media_view(media_items) do
|
|
%{
|
|
title: "Media",
|
|
subtitle: "Images and files",
|
|
layout: "media_grid",
|
|
empty_message: "No items",
|
|
items:
|
|
Enum.map(media_items, fn media ->
|
|
%{
|
|
id: media.id,
|
|
title: display_media_title(media),
|
|
meta: media_size_label(media.size),
|
|
mime_type: media.mime_type,
|
|
route: "media"
|
|
}
|
|
end)
|
|
}
|
|
end
|
|
|
|
defp tags_nav_view(tags) do
|
|
%{
|
|
title: "Tags",
|
|
subtitle: "Tag management",
|
|
layout: "nav_list",
|
|
items: [
|
|
%{id: "tags-cloud", title: "Tag Cloud", icon: "☁️", route: "tags"},
|
|
%{id: "tags-manage", title: "Create / Edit", icon: "✏️", route: "tags"},
|
|
%{id: "tags-merge", title: "Merge Tags", icon: "🔀", route: "tags"}
|
|
],
|
|
summary_badge: length(tags)
|
|
}
|
|
end
|
|
|
|
defp settings_nav_view do
|
|
%{
|
|
title: "Settings",
|
|
subtitle: "Project and publishing",
|
|
layout: "nav_list",
|
|
items: [
|
|
%{id: "settings-project", title: "Project", icon: "📁", route: "settings"},
|
|
%{id: "settings-editor", title: "Editor", icon: "📝", route: "settings"},
|
|
%{id: "settings-content", title: "Content", icon: "📋", route: "settings"},
|
|
%{id: "settings-ai", title: "AI", icon: "🤖", route: "settings"},
|
|
%{id: "settings-technology", title: "Technology", icon: "⚙️", route: "settings"},
|
|
%{id: "settings-publishing", title: "Publishing", icon: "🚀", route: "settings"},
|
|
%{id: "settings-data", title: "Data", icon: "🗄️", route: "settings"},
|
|
%{id: "settings-mcp", title: "MCP", icon: "🔌", route: "settings"},
|
|
%{id: "settings-style", title: "Style", icon: "🎨", route: "style"}
|
|
]
|
|
}
|
|
end
|
|
|
|
defp git_view do
|
|
%{
|
|
title: "Git",
|
|
subtitle: "Working tree and history",
|
|
layout: "entity_list",
|
|
empty_message: "No items",
|
|
items: [
|
|
%{id: "git-working-tree", title: "Working tree", meta: "Working tree and history", route: "git_diff", updated_at: nil}
|
|
]
|
|
}
|
|
end
|
|
|
|
defp entity_list_view(title, subtitle, route, items) do
|
|
%{
|
|
title: title,
|
|
subtitle: subtitle,
|
|
layout: "entity_list",
|
|
empty_message: "No items",
|
|
items:
|
|
Enum.map(items, fn item ->
|
|
%{
|
|
id: item.id,
|
|
title: item.title,
|
|
meta: Map.get(item, :meta),
|
|
updated_at: Map.get(item, :updated_at),
|
|
route: route
|
|
}
|
|
end)
|
|
}
|
|
end
|
|
|
|
defp build_post_section(title, status, posts, translation_counts, published_meta?) do
|
|
%{
|
|
id: Atom.to_string(status),
|
|
title: title,
|
|
status: Atom.to_string(status),
|
|
count: length(posts),
|
|
items:
|
|
Enum.map(posts, fn post ->
|
|
%{
|
|
id: post.id,
|
|
title: display_post_title(post),
|
|
categories: post.categories || [],
|
|
language_count: 1 + Map.get(translation_counts, post.id, 0),
|
|
meta_timestamp: if(published_meta?, do: post.published_at || post.updated_at, else: post.updated_at),
|
|
route: "post"
|
|
}
|
|
end)
|
|
}
|
|
end
|
|
|
|
defp list_posts(project_id) do
|
|
Repo.all(
|
|
from post in Post,
|
|
where: post.project_id == ^project_id,
|
|
order_by: [desc: post.updated_at, desc: post.created_at],
|
|
select: %{
|
|
id: post.id,
|
|
title: post.title,
|
|
slug: post.slug,
|
|
status: post.status,
|
|
categories: post.categories,
|
|
updated_at: post.updated_at,
|
|
published_at: post.published_at
|
|
}
|
|
)
|
|
end
|
|
|
|
defp translation_counts(project_id) do
|
|
Repo.all(
|
|
from translation in Translation,
|
|
where: translation.project_id == ^project_id,
|
|
group_by: translation.translation_for,
|
|
select: {translation.translation_for, count(translation.id)}
|
|
)
|
|
|> Map.new()
|
|
end
|
|
|
|
defp list_media(project_id) do
|
|
Repo.all(
|
|
from media in Media,
|
|
where: media.project_id == ^project_id,
|
|
order_by: [desc: media.updated_at, desc: media.created_at],
|
|
select: %{
|
|
id: media.id,
|
|
title: media.title,
|
|
original_name: media.original_name,
|
|
mime_type: media.mime_type,
|
|
size: media.size
|
|
}
|
|
)
|
|
end
|
|
|
|
defp list_scripts(project_id) do
|
|
Repo.all(
|
|
from script in Script,
|
|
where: script.project_id == ^project_id,
|
|
order_by: [desc: script.updated_at, desc: script.created_at],
|
|
select: %{id: script.id, title: script.title, updated_at: script.updated_at}
|
|
)
|
|
end
|
|
|
|
defp list_templates(project_id) do
|
|
Repo.all(
|
|
from template in Template,
|
|
where: template.project_id == ^project_id,
|
|
order_by: [desc: template.updated_at, desc: template.created_at],
|
|
select: %{id: template.id, title: template.title, updated_at: template.updated_at}
|
|
)
|
|
end
|
|
|
|
defp list_tags(project_id) do
|
|
Repo.all(
|
|
from tag in Tag,
|
|
where: tag.project_id == ^project_id,
|
|
order_by: [asc: tag.name],
|
|
select: %{id: tag.id, title: tag.name, updated_at: tag.updated_at}
|
|
)
|
|
end
|
|
|
|
defp list_conversations do
|
|
Repo.all(
|
|
from conversation in ChatConversation,
|
|
order_by: [desc: conversation.updated_at, desc: conversation.created_at],
|
|
select: %{id: conversation.id, title: conversation.title, updated_at: conversation.updated_at}
|
|
)
|
|
end
|
|
|
|
defp group_posts(posts) do
|
|
Enum.reduce(posts, %{draft: [], published: [], archived: []}, fn post, acc ->
|
|
case post.status do
|
|
:draft -> %{acc | draft: acc.draft ++ [post]}
|
|
:published -> %{acc | published: acc.published ++ [post]}
|
|
:archived -> %{acc | archived: acc.archived ++ [post]}
|
|
_other -> acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp page_post?(post) do
|
|
Enum.any?(post.categories || [], &(String.downcase(to_string(&1)) == @page_category))
|
|
end
|
|
|
|
defp display_post_title(post) do
|
|
cond do
|
|
present?(post.title) -> post.title
|
|
present?(post.slug) -> post.slug
|
|
true -> "Untitled"
|
|
end
|
|
end
|
|
|
|
defp display_media_title(media) do
|
|
if present?(media.title), do: media.title, else: media.original_name || ""
|
|
end
|
|
|
|
defp media_size_label(size) when is_integer(size) and size < 1024, do: "#{size} B"
|
|
|
|
defp media_size_label(size) when is_integer(size) and size < 1024 * 1024 do
|
|
:erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
|
|
end
|
|
|
|
defp media_size_label(size) when is_integer(size) do
|
|
:erlang.float_to_binary(size / (1024 * 1024), decimals: 1) <> " MB"
|
|
end
|
|
|
|
defp media_size_label(_size), do: "0 B"
|
|
|
|
defp present?(value), do: value not in [nil, ""]
|
|
end
|