chore: section 12 closed, had to do with map and atoms

This commit is contained in:
2026-05-01 17:37:08 +02:00
parent eb358bf512
commit c12001307f
18 changed files with 1025 additions and 386 deletions

View File

@@ -121,11 +121,9 @@ _None._ All modules previously on the queue have been split; refresh the queue i
## 12. Atom/String Key Duality
**Status:** open, low priority.
**Status:** ✅ done (2026-05-01). Same-name atom/string boundary reads now use `BDS.MapUtils.attr/2` or `attr/3` instead of nested `Map.get/3` or `Map.get/2 || Map.get/2` fallbacks. The cleanup covers AI endpoint/chat/one-shot attrs, model capabilities, rendering assigns and list pagination/archive contexts, UI shortcut/filter params, metadata category settings, metadata-diff repair payloads, CLI sync payloads, chat tool calls, and duplicate/metadata-diff editor payloads.
**Pattern:** `Map.get(assigns, :language, Map.get(assigns, "language", default))` in many editors and capability bridges.
**Plan:** normalize at boundaries. Adopt the rule "atoms internally, strings only at JSON/HTTP boundaries", and use `attr/2` (post-#6 consolidation) at every boundary point.
**Rule:** atoms internally, strings only at JSON/HTTP/form/render boundaries. If a boundary must accept both atom and string keys for the same snake_case field, use `BDS.MapUtils.attr/2` or `attr/3`; do not open-code same-name dual-key reads.
---
@@ -161,6 +159,8 @@ Most tests share the SQLite repo and named GenServers (`BDS.Tasks`, `BDS.Search`
### 2026-05-01
- **Atom/string key duality**: added `BDS.MapUtils.attr/3` and a regression test that scans `lib/**/*.ex` and `lib/**/*.heex` for same-name atom/string `Map.get` fallback reads. Replaced same-name atom/string boundary reads across AI attrs, rendering assigns, pagination/archive contexts, UI command/filter params, metadata category settings, metadata-diff repair payloads, CLI sync payloads, chat tool call normalization, and misc editor duplicate/metadata-diff payload rendering. Remaining mixed-key scan hits are intentionally different-key fallbacks (for example camelCase/snake_case JSON compatibility) or atom-only/string-only boundaries. Section 12 is closed.
- **`Jason.decode!/1` on external HTTP responses**: replaced the 2 scoped OpenAI-compatible runtime response decodes with `Jason.decode/1` and tagged `{:error, %{kind: :invalid_json_response, reason: reason}}` propagation for malformed `/models` and `/chat/completions` bodies. Added regressions covering endpoint model listing through a fake HTTP client and generation through a local Bandit server. Section 9 is closed.
### 2026-05-10

View File

@@ -6,6 +6,7 @@ defmodule BDS.AI do
alias BDS.AI.OneShot
alias BDS.AI.Runtime
alias BDS.AI.SecretBackend
alias BDS.MapUtils
import BDS.AI.SettingsStore,
only: [
@@ -21,20 +22,26 @@ defmodule BDS.AI do
@type endpoint_kind :: atom()
@typedoc "Endpoint configuration map."
@type endpoint :: %{kind: endpoint_kind(), url: String.t() | nil, api_key: String.t() | nil, model: String.t() | nil}
@type endpoint :: %{
kind: endpoint_kind(),
url: String.t() | nil,
api_key: String.t() | nil,
model: String.t() | nil
}
@typedoc "Attribute map for endpoint operations."
@type endpoint_attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
@spec put_endpoint(endpoint_kind(), endpoint_attrs(), keyword()) ::
{:ok, endpoint()} | {:error, term()}
def put_endpoint(kind, attrs, opts \\ []) when is_atom(kind) and is_map(attrs) and is_list(opts) do
def put_endpoint(kind, attrs, opts \\ [])
when is_atom(kind) and is_map(attrs) and is_list(opts) do
backend = Keyword.get(opts, :secret_backend, SecretBackend)
kind_key = Atom.to_string(kind)
url = Map.get(attrs, :url) || Map.get(attrs, "url")
model = Map.get(attrs, :model) || Map.get(attrs, "model")
api_key = Map.get(attrs, :api_key) || Map.get(attrs, "api_key")
url = MapUtils.attr(attrs, :url)
model = MapUtils.attr(attrs, :model)
api_key = MapUtils.attr(attrs, :api_key)
with :ok <- put_setting("ai.#{kind_key}.url", url),
:ok <- put_setting("ai.#{kind_key}.model", model),
@@ -103,7 +110,8 @@ defmodule BDS.AI do
end
end
@spec put_model_preference(atom(), String.t()) :: :ok | {:error, :unknown_model_preference | term()}
@spec put_model_preference(atom(), String.t()) ::
:ok | {:error, :unknown_model_preference | term()}
def put_model_preference(key, model) when is_atom(key) and is_binary(model) do
case Map.fetch(Runtime.model_preference_keys(), key) do
{:ok, setting_key} -> put_setting(setting_key, model)
@@ -111,7 +119,8 @@ defmodule BDS.AI do
end
end
@spec get_model_preference(atom()) :: {:ok, String.t() | nil} | {:error, :unknown_model_preference}
@spec get_model_preference(atom()) ::
{:ok, String.t() | nil} | {:error, :unknown_model_preference}
def get_model_preference(key) when is_atom(key) do
case Map.fetch(Runtime.model_preference_keys(), key) do
{:ok, setting_key} -> {:ok, get_setting(setting_key)}
@@ -134,13 +143,15 @@ defmodule BDS.AI do
@spec analyze_post(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate analyze_post(post_input, opts \\ []), to: OneShot
@spec translate_post(map() | String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
@spec translate_post(map() | String.t(), String.t(), keyword()) ::
{:ok, map()} | {:error, term()}
defdelegate translate_post(post_input, target_language, opts \\ []), to: OneShot
@spec analyze_image(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate analyze_image(media_input, opts \\ []), to: OneShot
@spec translate_media(map() | String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
@spec translate_media(map() | String.t(), String.t(), keyword()) ::
{:ok, map()} | {:error, term()}
defdelegate translate_media(media_input, target_language, opts \\ []), to: OneShot
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}

View File

@@ -2,6 +2,7 @@ defmodule BDS.AI.Catalog do
@moduledoc false
import Ecto.Query
import BDS.AI.SettingsStore,
only: [
get_setting: 1,
@@ -21,7 +22,13 @@ defmodule BDS.AI.Catalog do
@spec list_endpoint_models(map(), keyword()) :: {:ok, [map()]} | {:error, term()}
def list_endpoint_models(endpoint, opts \\ []) when is_map(endpoint) and is_list(opts) do
http_client = Keyword.get(opts, :http_client, Application.get_env(:bds, :ai_http_client, BDS.AI.HttpClient))
http_client =
Keyword.get(
opts,
:http_client,
Application.get_env(:bds, :ai_http_client, BDS.AI.HttpClient)
)
OpenAICompatibleRuntime.list_models(endpoint, http_client: http_client)
end
@@ -103,8 +110,8 @@ defmodule BDS.AI.Catalog do
@spec put_model_capabilities(String.t(), map()) :: :ok | {:error, term()}
def put_model_capabilities(model_id, attrs) when is_binary(model_id) and is_map(attrs) do
capabilities = %{
supports_attachment: truthy?(Map.get(attrs, :supports_attachment) || Map.get(attrs, "supports_attachment")),
supports_tool_calls: truthy?(Map.get(attrs, :supports_tool_calls) || Map.get(attrs, "supports_tool_calls"))
supports_attachment: truthy?(BDS.MapUtils.attr(attrs, :supports_attachment)),
supports_tool_calls: truthy?(BDS.MapUtils.attr(attrs, :supports_tool_calls))
}
put_setting("ai.model_capabilities.#{model_id}", Jason.encode!(capabilities))
@@ -154,7 +161,10 @@ defmodule BDS.AI.Catalog do
}
end
@spec model_capabilities(String.t()) :: %{supports_attachment: boolean(), supports_tool_calls: boolean()}
@spec model_capabilities(String.t()) :: %{
supports_attachment: boolean(),
supports_tool_calls: boolean()
}
def model_capabilities(model_id) do
overrides = decode_model_capabilities_override(model_id)
@@ -162,7 +172,7 @@ defmodule BDS.AI.Catalog do
case get_catalog_model(model_id) do
{:ok, model} ->
%{
supports_attachment: model.supports_attachment or ("image" in model.input_modalities),
supports_attachment: model.supports_attachment or "image" in model.input_modalities,
supports_tool_calls: model.supports_tool_calls
}
@@ -257,8 +267,19 @@ defmodule BDS.AI.Catalog do
|> Model.changeset(model_attrs)
|> Repo.insert!()
insert_modalities(provider_id, model_id, Map.get(model_data, "input_modalities", []), :input)
insert_modalities(provider_id, model_id, Map.get(model_data, "output_modalities", []), :output)
insert_modalities(
provider_id,
model_id,
Map.get(model_data, "input_modalities", []),
:input
)
insert_modalities(
provider_id,
model_id,
Map.get(model_data, "output_modalities", []),
:output
)
inner_count + 1
end)

View File

@@ -13,6 +13,7 @@ defmodule BDS.AI.Chat do
alias BDS.AI.OpenAICompatibleRuntime
alias BDS.AI.Runtime
alias BDS.AI.SecretBackend
alias BDS.MapUtils
import BDS.AI.SettingsStore, only: [get_setting: 1]
alias BDS.Media.Media
alias BDS.Persistence
@@ -28,15 +29,15 @@ defmodule BDS.AI.Chat do
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
def start_chat(attrs \\ %{}) when is_map(attrs) do
now = Persistence.now_ms()
model = Map.get(attrs, :model) || Map.get(attrs, "model")
title = Map.get(attrs, :title) || Map.get(attrs, "title") || generated_chat_title(model)
model = MapUtils.attr(attrs, :model)
title = MapUtils.attr(attrs, :title) || generated_chat_title(model)
%ChatConversation{}
|> ChatConversation.changeset(%{
id: Ecto.UUID.generate(),
title: title,
model: model,
copilot_session_id: Map.get(attrs, :copilot_session_id) || Map.get(attrs, "copilot_session_id"),
copilot_session_id: MapUtils.attr(attrs, :copilot_session_id),
created_at: now,
updated_at: now
})
@@ -120,7 +121,8 @@ defmodule BDS.AI.Chat do
def send_chat_message(conversation_id, content, opts \\ [])
when is_binary(conversation_id) and is_binary(content) and is_list(opts) do
with %ChatConversation{} = conversation <- Repo.get(ChatConversation, conversation_id),
{:ok, user_message} <- persist_chat_message(%{
{:ok, user_message} <-
persist_chat_message(%{
conversation_id: conversation.id,
role: :user,
content: content,
@@ -153,7 +155,9 @@ defmodule BDS.AI.Chat do
@spec cancel_chat(String.t()) :: :ok
def cancel_chat(conversation_id) when is_binary(conversation_id) do
case InFlight.lookup(conversation_id) do
nil -> :ok
nil ->
:ok
pid ->
_ = Task.Supervisor.terminate_child(BDS.Tasks.TaskSupervisor, pid)
:ok
@@ -162,7 +166,11 @@ defmodule BDS.AI.Chat do
@doc false
def count_distinct_string_list(schema, field, project_id) do
Repo.all(from record in schema, where: field(record, :project_id) == ^project_id, select: field(record, ^field))
Repo.all(
from record in schema,
where: field(record, :project_id) == ^project_id,
select: field(record, ^field)
)
|> List.flatten()
|> Enum.reject(&blank?/1)
|> MapSet.new()
@@ -267,9 +275,14 @@ defmodule BDS.AI.Chat do
normalized_url = String.downcase(url)
cond do
String.contains?(normalized_url, "11434") or String.contains?(normalized_url, "ollama") -> "ollama"
String.contains?(normalized_url, "1234") or String.contains?(normalized_url, "lmstudio") -> "lmstudio"
true -> "generic-openai"
String.contains?(normalized_url, "11434") or String.contains?(normalized_url, "ollama") ->
"ollama"
String.contains?(normalized_url, "1234") or String.contains?(normalized_url, "lmstudio") ->
"lmstudio"
true ->
"generic-openai"
end
end
@@ -303,23 +316,58 @@ defmodule BDS.AI.Chat do
:ok <- Runtime.validate_target(:chat, model, mode),
messages <- load_chat_messages(conversation.id),
tools <- available_chat_tools(project_id, model),
{:ok, reply} <- chat_round(conversation, messages, endpoint, model, project_id, tools, runtime, opts, @chat_max_tool_rounds) do
{:ok, reply} <-
chat_round(
conversation,
messages,
endpoint,
model,
project_id,
tools,
runtime,
opts,
@chat_max_tool_rounds
) do
{:ok, reply}
end
end
defp chat_round(_conversation, _messages, _endpoint, _model, _project_id, _tools, _runtime, _opts, 0) do
defp chat_round(
_conversation,
_messages,
_endpoint,
_model,
_project_id,
_tools,
_runtime,
_opts,
0
) do
{:error, %{kind: :tool_loop_exhausted}}
end
defp chat_round(conversation, messages, endpoint, model, project_id, tools, runtime, opts, rounds_left) do
defp chat_round(
conversation,
messages,
endpoint,
model,
project_id,
tools,
runtime,
opts,
rounds_left
) do
request = build_chat_request(conversation, messages, model, project_id, tools)
with {:ok, response} <- runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts),
with {:ok, response} <-
runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts),
{:ok, assistant_message} <- persist_assistant_response(conversation.id, response),
:ok <- touch_conversation(conversation.id) do
if is_binary(Map.get(response, :content)) and String.trim(Map.get(response, :content)) != "" do
notify_chat_event(opts, {:chat_streaming_content, conversation.id, Map.get(response, :content)})
notify_chat_event(
opts,
{:chat_streaming_content, conversation.id, Map.get(response, :content)}
)
end
tool_calls = decode_tool_calls(Map.get(response, :tool_calls))
@@ -330,7 +378,8 @@ defmodule BDS.AI.Chat do
cond do
tool_calls != [] and tools != [] ->
with {:ok, tool_messages} <- execute_tool_calls(conversation.id, tool_calls, project_id, opts),
with {:ok, tool_messages} <-
execute_tool_calls(conversation.id, tool_calls, project_id, opts),
updated_messages <- load_chat_messages(conversation.id),
{:ok, reply} <-
chat_round(
@@ -420,8 +469,13 @@ defmodule BDS.AI.Chat do
defp message_for_runtime(%ChatMessage{} = message) do
base = %{"role" => Atom.to_string(message.role)}
base = if is_binary(message.content), do: Map.put(base, "content", message.content), else: base
base = if is_binary(message.tool_call_id), do: Map.put(base, "tool_call_id", message.tool_call_id), else: base
base =
if is_binary(message.content), do: Map.put(base, "content", message.content), else: base
base =
if is_binary(message.tool_call_id),
do: Map.put(base, "tool_call_id", message.tool_call_id),
else: base
case Catalog.decode_nullable_json(message.tool_calls) do
nil -> base
@@ -438,7 +492,9 @@ defmodule BDS.AI.Chat do
[system | remainder] = messages
{kept, _tokens} =
Enum.reduce(Enum.reverse(remainder), {[], approximate_message_tokens(system)}, fn message, {acc, used} ->
Enum.reduce(Enum.reverse(remainder), {[], approximate_message_tokens(system)}, fn message,
{acc,
used} ->
message_tokens = approximate_message_tokens(message)
if used + message_tokens <= max_budget do
@@ -467,8 +523,12 @@ defmodule BDS.AI.Chat do
defp project_stats_summary(nil), do: nil
defp project_stats_summary(project_id) do
post_count = Repo.aggregate(from(post in Post, where: post.project_id == ^project_id), :count, :id)
media_count = Repo.aggregate(from(media in Media, where: media.project_id == ^project_id), :count, :id)
post_count =
Repo.aggregate(from(post in Post, where: post.project_id == ^project_id), :count, :id)
media_count =
Repo.aggregate(from(media in Media, where: media.project_id == ^project_id), :count, :id)
tag_count = count_distinct_string_list(Post, :tags, project_id)
category_count = count_distinct_string_list(Post, :categories, project_id)
@@ -528,9 +588,14 @@ defmodule BDS.AI.Chat do
10 -> {:error, :cancelled}
end
:shutdown -> {:error, :cancelled}
{:shutdown, _detail} -> {:error, :cancelled}
_other -> {:error, :cancelled}
:shutdown ->
{:error, :cancelled}
{:shutdown, _detail} ->
{:error, :cancelled}
_other ->
{:error, :cancelled}
end
end
end
@@ -556,14 +621,22 @@ defmodule BDS.AI.Chat do
end
defp approximate_value_tokens(value) when is_binary(value), do: div(String.length(value), 4) + 1
defp approximate_value_tokens(value) when is_list(value), do: Enum.map(value, &approximate_value_tokens/1) |> Enum.sum()
defp approximate_value_tokens(value) when is_map(value), do: Jason.encode!(value) |> approximate_value_tokens()
defp approximate_value_tokens(value) when is_list(value),
do: Enum.map(value, &approximate_value_tokens/1) |> Enum.sum()
defp approximate_value_tokens(value) when is_map(value),
do: Jason.encode!(value) |> approximate_value_tokens()
defp approximate_value_tokens(_value), do: 1
defp model_context_window(model_id) do
case Catalog.get_catalog_model(model_id) do
{:ok, model} when is_integer(model.context_window) and model.context_window > 0 -> model.context_window
_other -> @default_context_window
{:ok, model} when is_integer(model.context_window) and model.context_window > 0 ->
model.context_window
_other ->
@default_context_window
end
end

View File

@@ -5,6 +5,7 @@ defmodule BDS.AI.OneShot do
alias BDS.AI.OpenAICompatibleRuntime
alias BDS.AI.Runtime
alias BDS.Media.Media
alias BDS.MapUtils
alias BDS.Posts.Post
alias BDS.Repo
@@ -45,10 +46,10 @@ defmodule BDS.AI.OneShot do
def analyze_import_taxonomy(import_terms, existing_terms, opts \\ [])
when is_map(import_terms) and is_map(existing_terms) and is_list(opts) do
payload = %{
import_categories: normalize_string_list(Map.get(import_terms, :categories) || Map.get(import_terms, "categories")),
import_tags: normalize_string_list(Map.get(import_terms, :tags) || Map.get(import_terms, "tags")),
existing_categories: normalize_string_list(Map.get(existing_terms, :categories) || Map.get(existing_terms, "categories")),
existing_tags: normalize_string_list(Map.get(existing_terms, :tags) || Map.get(existing_terms, "tags"))
import_categories: normalize_string_list(MapUtils.attr(import_terms, :categories)),
import_tags: normalize_string_list(MapUtils.attr(import_terms, :tags)),
existing_categories: normalize_string_list(MapUtils.attr(existing_terms, :categories)),
existing_tags: normalize_string_list(MapUtils.attr(existing_terms, :tags))
}
run_one_shot(
@@ -96,7 +97,8 @@ defmodule BDS.AI.OneShot do
end
end
@spec translate_post(map() | String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
@spec translate_post(map() | String.t(), String.t(), keyword()) ::
{:ok, map()} | {:error, term()}
def translate_post(post_input, target_language, opts \\ [])
when is_binary(target_language) and is_list(opts) do
with {:ok, post} <- normalize_post_input(post_input) do
@@ -138,7 +140,8 @@ defmodule BDS.AI.OneShot do
end
end
@spec translate_media(map() | String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
@spec translate_media(map() | String.t(), String.t(), keyword()) ::
{:ok, map()} | {:error, term()}
def translate_media(media_input, target_language, opts \\ [])
when is_binary(target_language) and is_list(opts) do
with {:ok, media} <- normalize_media_input(media_input) do
@@ -165,7 +168,8 @@ defmodule BDS.AI.OneShot do
with {:ok, endpoint, model, mode} <- Runtime.resolve_target(operation, opts),
:ok <- Runtime.validate_target(operation, model, mode),
request <- build_one_shot_request(operation, payload, model),
{:ok, response} <- runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts),
{:ok, response} <-
runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts),
{:ok, json} <- extract_json_response(response),
usage <- Chat.normalize_usage(response.usage),
{:ok, result} <- formatter.(json, usage) do
@@ -252,7 +256,10 @@ defmodule BDS.AI.OneShot do
defp one_shot_user_content(:analyze_image, media) do
[
%{"type" => "text", "text" => "Analyze this image and return title, alt text, and caption."},
%{
"type" => "text",
"text" => "Analyze this image and return title, alt text, and caption."
},
%{"type" => "image_url", "image_url" => %{"url" => media.image_url}}
]
end
@@ -286,9 +293,9 @@ defmodule BDS.AI.OneShot do
defp normalize_post_input(attrs) when is_map(attrs) do
{:ok,
%{
title: Map.get(attrs, :title) || Map.get(attrs, "title") || "",
excerpt: Map.get(attrs, :excerpt) || Map.get(attrs, "excerpt") || "",
content: Map.get(attrs, :content) || Map.get(attrs, "content") || ""
title: MapUtils.attr(attrs, :title) || "",
excerpt: MapUtils.attr(attrs, :excerpt) || "",
content: MapUtils.attr(attrs, :content) || ""
}}
end
@@ -313,11 +320,11 @@ defmodule BDS.AI.OneShot do
defp normalize_media_input(attrs) when is_map(attrs) do
{:ok,
%{
mime_type: Map.get(attrs, :mime_type) || Map.get(attrs, "mime_type"),
title: Map.get(attrs, :title) || Map.get(attrs, "title") || "",
alt: Map.get(attrs, :alt) || Map.get(attrs, "alt") || "",
caption: Map.get(attrs, :caption) || Map.get(attrs, "caption") || "",
image_url: Map.get(attrs, :image_url) || Map.get(attrs, "image_url")
mime_type: MapUtils.attr(attrs, :mime_type),
title: MapUtils.attr(attrs, :title) || "",
alt: MapUtils.attr(attrs, :alt) || "",
caption: MapUtils.attr(attrs, :caption) || "",
image_url: MapUtils.attr(attrs, :image_url)
}}
end
@@ -336,7 +343,8 @@ defmodule BDS.AI.OneShot do
|> Enum.uniq()
end
defp filter_taxonomy_mapping_response(mappings, import_terms, existing_terms) when is_map(mappings) do
defp filter_taxonomy_mapping_response(mappings, import_terms, existing_terms)
when is_map(mappings) do
import_lookup = canonical_term_lookup(import_terms)
existing_lookup = canonical_term_lookup(existing_terms)

View File

@@ -80,18 +80,26 @@ defmodule BDS.Desktop.ShellCommands do
attrs = %{group_id: group_id, group_name: "Search"}
{:ok, posts_task} =
Tasks.submit_task("Reindex Search Text", fn report ->
Tasks.submit_task(
"Reindex Search Text",
fn report ->
:ok = Search.reindex_posts(project.id, on_progress: report)
report.(1.0, "Post search text reindexed")
%{project_id: project.id, entity: "posts"}
end, attrs)
end,
attrs
)
{:ok, _media_task} =
Tasks.submit_task("Reindex Media Search Text", fn report ->
Tasks.submit_task(
"Reindex Media Search Text",
fn report ->
:ok = Search.reindex_media(project.id, on_progress: report)
report.(1.0, "Media search text reindexed")
%{project_id: project.id, entity: "media"}
end, attrs)
end,
attrs
)
{:ok,
%{
@@ -107,43 +115,86 @@ defmodule BDS.Desktop.ShellCommands do
end
defp dispatch("rebuild_embedding_index", project, _params) do
queue_task(project, "rebuild_embedding_index", "Rebuild Embedding Index", "Embeddings", fn report ->
queue_task(
project,
"rebuild_embedding_index",
"Rebuild Embedding Index",
"Embeddings",
fn report ->
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
report.(1.0, "Embedding index rebuilt")
%{project_id: project.id, rebuilt_post_ids: rebuilt_post_ids, rebuilt_count: length(rebuilt_post_ids)}
end)
%{
project_id: project.id,
rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids)
}
end
)
end
defp dispatch("rebuild_posts_from_files", project, _params) do
queue_task(project, "rebuild_posts_from_files", "Rebuild Posts From Files", "Maintenance", fn report ->
{:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report)
queue_task(
project,
"rebuild_posts_from_files",
"Rebuild Posts From Files",
"Maintenance",
fn report ->
{:ok, posts} =
Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report)
report.(1.0, "Post rebuild complete")
%{project_id: project.id, counts: %{posts: length(posts)}}
end)
end
)
end
defp dispatch("rebuild_media_from_files", project, _params) do
queue_task(project, "rebuild_media_from_files", "Rebuild Media From Files", "Maintenance", fn report ->
{:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
queue_task(
project,
"rebuild_media_from_files",
"Rebuild Media From Files",
"Maintenance",
fn report ->
{:ok, media} =
Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
report.(1.0, "Media rebuild complete")
%{project_id: project.id, counts: %{media: length(media)}}
end)
end
)
end
defp dispatch("rebuild_scripts_from_files", project, _params) do
queue_task(project, "rebuild_scripts_from_files", "Rebuild Scripts From Files", "Maintenance", fn report ->
{:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
queue_task(
project,
"rebuild_scripts_from_files",
"Rebuild Scripts From Files",
"Maintenance",
fn report ->
{:ok, scripts} =
Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
report.(1.0, "Script rebuild complete")
%{project_id: project.id, counts: %{scripts: length(scripts)}}
end)
end
)
end
defp dispatch("rebuild_templates_from_files", project, _params) do
queue_task(project, "rebuild_templates_from_files", "Rebuild Templates From Files", "Maintenance", fn report ->
{:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
queue_task(
project,
"rebuild_templates_from_files",
"Rebuild Templates From Files",
"Maintenance",
fn report ->
{:ok, templates} =
Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
report.(1.0, "Template rebuild complete")
%{project_id: project.id, counts: %{templates: length(templates)}}
end)
end
)
end
defp dispatch("rebuild_post_links", project, _params) do
@@ -155,11 +206,17 @@ defmodule BDS.Desktop.ShellCommands do
end
defp dispatch("regenerate_missing_thumbnails", project, _params) do
queue_task(project, "regenerate_missing_thumbnails", "Regenerate Missing Thumbnails", "Maintenance", fn report ->
queue_task(
project,
"regenerate_missing_thumbnails",
"Regenerate Missing Thumbnails",
"Maintenance",
fn report ->
result = BDS.Media.regenerate_missing_thumbnails(project.id, on_progress: report)
report.(1.0, "Missing thumbnails regenerated")
Map.put(result, :project_id, project.id)
end)
end
)
end
defp dispatch("rebuild_database", project, _params) do
@@ -192,15 +249,24 @@ defmodule BDS.Desktop.ShellCommands do
defp dispatch("generate_sitemap", project, _params) do
queue_task(project, "generate_sitemap", "Generate Site", "Generation", fn report ->
{:ok, generation} = Generation.generate_site(project.id, @site_sections, on_progress: report)
{:ok, generation} =
Generation.generate_site(project.id, @site_sections, on_progress: report)
report.(1.0, "Generated site output")
%{project_id: project.id, sections: generation.sections, generated_count: length(generation.generated_files)}
%{
project_id: project.id,
sections: generation.sections,
generated_count: length(generation.generated_files)
}
end)
end
defp dispatch("validate_site", project, _params) do
queue_task(project, "validate_site", "Validate Site", "Validation", fn report ->
{:ok, validation} = Generation.validate_site(project.id, @site_sections, on_progress: report)
{:ok, validation} =
Generation.validate_site(project.id, @site_sections, on_progress: report)
site_validation_result(project.id, validation)
end)
end
@@ -214,13 +280,18 @@ defmodule BDS.Desktop.ShellCommands do
end
defp dispatch("repair_metadata_diff", project, params) do
items = normalize_metadata_diff_items(Map.get(params, "items", Map.get(params, :items, [])))
direction = Map.get(params, "direction", Map.get(params, :direction))
items = normalize_metadata_diff_items(BDS.MapUtils.attr(params, :items, []))
direction = BDS.MapUtils.attr(params, :direction)
if items == [] do
{:error, %{action: "repair_metadata_diff", message: "No metadata diff items selected"}}
else
queue_task(project, "repair_metadata_diff", "Repair Metadata Diff", "Maintenance", fn report ->
queue_task(
project,
"repair_metadata_diff",
"Repair Metadata Diff",
"Maintenance",
fn report ->
{:ok, _repair} =
Maintenance.repair_metadata_diff(project.id, direction, items,
on_progress: scaled_progress_reporter(report, 0.0, 0.8)
@@ -233,17 +304,23 @@ defmodule BDS.Desktop.ShellCommands do
report.(1.0, "Metadata diff repair complete")
metadata_diff_result(project.id, metadata_diff)
end)
end
)
end
end
defp dispatch("import_metadata_diff_orphans", project, params) do
orphans = normalize_metadata_diff_orphans(Map.get(params, "orphans", Map.get(params, :orphans, [])))
orphans = normalize_metadata_diff_orphans(BDS.MapUtils.attr(params, :orphans, []))
if orphans == [] do
{:error, %{action: "import_metadata_diff_orphans", message: "No orphan files selected"}}
else
queue_task(project, "import_metadata_diff_orphans", "Import Metadata Diff Orphans", "Maintenance", fn report ->
queue_task(
project,
"import_metadata_diff_orphans",
"Import Metadata Diff Orphans",
"Maintenance",
fn report ->
{:ok, _import} =
Maintenance.import_metadata_diff_orphans(project.id, orphans,
on_progress: scaled_progress_reporter(report, 0.0, 0.8)
@@ -256,16 +333,23 @@ defmodule BDS.Desktop.ShellCommands do
report.(1.0, "Metadata diff import complete")
metadata_diff_result(project.id, metadata_diff)
end)
end
)
end
end
defp dispatch("validate_translations", project, _params) do
queue_task(project, "validate_translations", "Validate Translations", "Validation", fn report ->
queue_task(
project,
"validate_translations",
"Validate Translations",
"Validation",
fn report ->
{:ok, translation_report} = Posts.validate_translations(project.id, on_progress: report)
report.(1.0, "Translation validation complete")
translation_validation_result(project.id, translation_report)
end)
end
)
end
defp dispatch("find_duplicates", project, _params) do
@@ -342,7 +426,9 @@ defmodule BDS.Desktop.ShellCommands do
%{
name: "Rebuild Media From Files",
work: fn report ->
{:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
{:ok, media} =
Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
report.(1.0, "Media rebuild complete")
%{project_id: project.id, counts: %{media: length(media)}}
end
@@ -350,7 +436,9 @@ defmodule BDS.Desktop.ShellCommands do
%{
name: "Rebuild Scripts From Files",
work: fn report ->
{:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
{:ok, scripts} =
Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
report.(1.0, "Script rebuild complete")
%{project_id: project.id, counts: %{scripts: length(scripts)}}
end
@@ -358,7 +446,9 @@ defmodule BDS.Desktop.ShellCommands do
%{
name: "Rebuild Templates From Files",
work: fn report ->
{:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
{:ok, templates} =
Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
report.(1.0, "Template rebuild complete")
%{project_id: project.id, counts: %{templates: length(templates)}}
end
@@ -384,7 +474,12 @@ defmodule BDS.Desktop.ShellCommands do
work: fn report ->
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
report.(1.0, "Embedding index rebuilt")
%{project_id: project.id, rebuilt_post_ids: rebuilt_post_ids, rebuilt_count: length(rebuilt_post_ids)}
%{
project_id: project.id,
rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids)
}
end
}
]
@@ -531,7 +626,10 @@ defmodule BDS.Desktop.ShellCommands do
subtitle: "Database rows and translation files checked for invalid state",
editorMeta: [
%{label: "Invalid DB", value: Integer.to_string(length(report.invalid_database_rows))},
%{label: "Invalid Files", value: Integer.to_string(length(report.invalid_filesystem_files))}
%{
label: "Invalid Files",
value: Integer.to_string(length(report.invalid_filesystem_files))
}
],
payload: normalize_translation_validation(report)
}
@@ -564,8 +662,8 @@ defmodule BDS.Desktop.ShellCommands do
defp normalize_metadata_diff_items(items) when is_list(items) do
Enum.map(items, fn item ->
%{
entity_type: Map.get(item, :entity_type) || Map.get(item, "entity_type"),
entity_id: Map.get(item, :entity_id) || Map.get(item, "entity_id")
entity_type: BDS.MapUtils.attr(item, :entity_type),
entity_id: BDS.MapUtils.attr(item, :entity_id)
}
end)
end
@@ -574,7 +672,7 @@ defmodule BDS.Desktop.ShellCommands do
defp normalize_metadata_diff_orphans(orphans) when is_list(orphans) do
Enum.map(orphans, fn orphan ->
%{file_path: Map.get(orphan, :file_path) || Map.get(orphan, "file_path")}
%{file_path: BDS.MapUtils.attr(orphan, :file_path)}
end)
end
@@ -593,7 +691,10 @@ defmodule BDS.Desktop.ShellCommands do
ssh_mode: Map.get(prefs, "ssh_mode")
}
if Enum.all?([credentials.ssh_host, credentials.ssh_user, credentials.ssh_remote_path], &is_binary/1) do
if Enum.all?(
[credentials.ssh_host, credentials.ssh_user, credentials.ssh_remote_path],
&is_binary/1
) do
{:ok, credentials}
else
{:error, %{action: "upload_site", message: "Publishing preferences are incomplete"}}

View File

@@ -4,12 +4,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
@tool_args_max_length 30
def tool_call_name(tool_call) when is_map(tool_call) do
Map.get(tool_call, "name") || Map.get(tool_call, :name) || "tool"
BDS.MapUtils.attr(tool_call, :name) || "tool"
end
def tool_call_arguments(tool_call) when is_map(tool_call) do
Map.get(tool_call, "arguments") || Map.get(tool_call, :arguments) ||
Map.get(tool_call, "args") || Map.get(tool_call, :args) || %{}
BDS.MapUtils.attr(tool_call, :arguments) || BDS.MapUtils.attr(tool_call, :args) || %{}
end
def normalize_tool_calls(tool_calls) when is_list(tool_calls) do
@@ -17,7 +16,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
arguments = tool_call_arguments(tool_call)
%{
id: Map.get(tool_call, "id") || Map.get(tool_call, :id),
id: BDS.MapUtils.attr(tool_call, :id),
name: tool_call_name(tool_call),
arguments: arguments,
args_preview: tool_arguments_preview(arguments),

View File

@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.CliSync do
import Phoenix.Component, only: [assign: 3]
alias BDS.{Media, Posts}
alias BDS.MapUtils
alias BDS.Media.Media, as: MediaRecord
alias BDS.Posts.Post
alias BDS.UI.Workbench
@@ -12,17 +13,18 @@ defmodule BDS.Desktop.ShellLive.CliSync do
Apply a CLI entity change payload to the shell socket. `reload_fun` is
called with `(socket, workbench)` to refresh derived data.
"""
@spec apply_entity_change(Phoenix.LiveView.Socket.t(), map(),
(Phoenix.LiveView.Socket.t(), map() -> Phoenix.LiveView.Socket.t())) ::
@spec apply_entity_change(Phoenix.LiveView.Socket.t(), map(), (Phoenix.LiveView.Socket.t(),
map() ->
Phoenix.LiveView.Socket.t())) ::
Phoenix.LiveView.Socket.t()
def apply_entity_change(socket, payload, reload_fun) do
entity = Map.get(payload, :entity) || Map.get(payload, "entity") || Map.get(payload, :entity_type) || Map.get(payload, "entity_type")
entity = MapUtils.attr(payload, :entity) || MapUtils.attr(payload, :entity_type)
entity_id =
Map.get(payload, :entity_id) || Map.get(payload, "entity_id") || Map.get(payload, :entityId) ||
MapUtils.attr(payload, :entity_id) || Map.get(payload, :entityId) ||
Map.get(payload, "entityId")
action = normalize_action(Map.get(payload, :action) || Map.get(payload, "action"))
action = normalize_action(MapUtils.attr(payload, :action))
if is_binary(entity) and entity != "" and is_binary(entity_id) and entity_id != "" and
action in [:created, :updated, :deleted] do
@@ -45,13 +47,28 @@ defmodule BDS.Desktop.ShellLive.CliSync do
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, post_id}))
|> assign(:post_editor_drafts, Map.delete(socket.assigns.post_editor_drafts, post_id))
|> assign(:post_editor_active_languages, Map.delete(socket.assigns.post_editor_active_languages, post_id))
|> assign(:post_editor_tag_queries, Map.delete(socket.assigns.post_editor_tag_queries, post_id))
|> assign(:post_editor_category_queries, Map.delete(socket.assigns.post_editor_category_queries, post_id))
|> assign(:post_editor_quick_actions_open, Map.delete(socket.assigns.post_editor_quick_actions_open, post_id))
|> assign(
:post_editor_active_languages,
Map.delete(socket.assigns.post_editor_active_languages, post_id)
)
|> assign(
:post_editor_tag_queries,
Map.delete(socket.assigns.post_editor_tag_queries, post_id)
)
|> assign(
:post_editor_category_queries,
Map.delete(socket.assigns.post_editor_category_queries, post_id)
)
|> assign(
:post_editor_quick_actions_open,
Map.delete(socket.assigns.post_editor_quick_actions_open, post_id)
)
|> assign(:post_editor_modes, Map.delete(socket.assigns.post_editor_modes, post_id))
|> assign(:post_editor_expanded, Map.delete(socket.assigns.post_editor_expanded, post_id))
|> assign(:post_editor_save_states, Map.delete(socket.assigns.post_editor_save_states, post_id))
|> assign(
:post_editor_save_states,
Map.delete(socket.assigns.post_editor_save_states, post_id)
)
{socket, workbench}
end
@@ -65,31 +82,58 @@ defmodule BDS.Desktop.ShellLive.CliSync do
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, media_id}))
|> assign(:media_editor_drafts, Map.delete(socket.assigns.media_editor_drafts, media_id))
|> assign(:media_editor_quick_actions_open, Map.delete(socket.assigns.media_editor_quick_actions_open, media_id))
|> assign(:media_editor_post_pickers_open, Map.delete(socket.assigns.media_editor_post_pickers_open, media_id))
|> assign(:media_editor_post_picker_queries, Map.delete(socket.assigns.media_editor_post_picker_queries, media_id))
|> assign(:media_editor_save_states, Map.delete(socket.assigns.media_editor_save_states, media_id))
|> assign(:media_editor_translation_forms, Map.delete(socket.assigns.media_editor_translation_forms, media_id))
|> assign(
:media_editor_quick_actions_open,
Map.delete(socket.assigns.media_editor_quick_actions_open, media_id)
)
|> assign(
:media_editor_post_pickers_open,
Map.delete(socket.assigns.media_editor_post_pickers_open, media_id)
)
|> assign(
:media_editor_post_picker_queries,
Map.delete(socket.assigns.media_editor_post_picker_queries, media_id)
)
|> assign(
:media_editor_save_states,
Map.delete(socket.assigns.media_editor_save_states, media_id)
)
|> assign(
:media_editor_translation_forms,
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
)
{socket, workbench}
end
defp maybe_close_deleted_tab(socket, _entity, _entity_id, _action), do: {socket, socket.assigns.workbench}
defp maybe_close_deleted_tab(socket, _entity, _entity_id, _action),
do: {socket, socket.assigns.workbench}
defp maybe_refresh_tab_meta(socket, "post", post_id, action) when action in [:created, :updated] do
defp maybe_refresh_tab_meta(socket, "post", post_id, action)
when action in [:created, :updated] do
maybe_put_tab_meta(socket, :post, post_id, fn ->
case Posts.get_post(post_id) do
%Post{} = post -> %{title: post.title || post.slug || post.id, subtitle: Atom.to_string(post.status)}
_other -> nil
%Post{} = post ->
%{title: post.title || post.slug || post.id, subtitle: Atom.to_string(post.status)}
_other ->
nil
end
end)
end
defp maybe_refresh_tab_meta(socket, "media", media_id, action) when action in [:created, :updated] do
defp maybe_refresh_tab_meta(socket, "media", media_id, action)
when action in [:created, :updated] do
maybe_put_tab_meta(socket, :media, media_id, fn ->
case Media.get_media(media_id) do
%MediaRecord{} = media -> %{title: media.title || media.filename || media.id, subtitle: media.filename || media.mime_type || "media"}
_other -> nil
%MediaRecord{} = media ->
%{
title: media.title || media.filename || media.id,
subtitle: media.filename || media.mime_type || "media"
}
_other ->
nil
end
end)
end
@@ -102,7 +146,9 @@ defmodule BDS.Desktop.ShellLive.CliSync do
if tab_present?(socket.assigns.workbench, key) or Map.has_key?(socket.assigns.tab_meta, key) do
case meta_fun.() do
%{} = fresh_meta ->
updated_meta = Map.update(socket.assigns.tab_meta, key, fresh_meta, &Map.merge(&1, fresh_meta))
updated_meta =
Map.update(socket.assigns.tab_meta, key, fresh_meta, &Map.merge(&1, fresh_meta))
assign(socket, :tab_meta, updated_meta)
_other ->

View File

@@ -7,11 +7,18 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
alias BDS.{Embeddings, Generation, Git, Posts, Repo}
alias BDS.Desktop.ShellData
alias BDS.MapUtils
alias BDS.Settings.Setting
embed_templates "misc_editor_html/*"
embed_templates("misc_editor_html/*")
@misc_routes [:site_validation, :metadata_diff, :translation_validation, :find_duplicates, :git_diff]
@misc_routes [
:site_validation,
:metadata_diff,
:translation_validation,
:find_duplicates,
:git_diff
]
def assign_socket(socket) do
assign(socket, :misc_editor, build(socket.assigns))
@@ -19,7 +26,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
def rerun(socket) do
case meta(socket.assigns) do
%{action: action} when is_binary(action) -> {:command, action}
%{action: action} when is_binary(action) ->
{:command, action}
_other ->
case misc_route_action(socket.assigns.current_tab.type) do
nil -> {:noop, socket}
@@ -47,10 +56,16 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
{:ok, result} ->
{:rerun,
socket
|> append_output.(translated("Site Validation"), translated("Validation changes applied"), inspect(result))}
|> append_output.(
translated("Site Validation"),
translated("Validation changes applied"),
inspect(result)
)}
end
rescue
error -> {:socket, append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")}
error ->
{:socket,
append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")}
end
def toggle_duplicate(socket, pair_id, reload) do
@@ -65,7 +80,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
socket
|> assign(:misc_editor_selected_pairs, Map.put(selected_by_tab, socket.assigns.current_tab.id, next))
|> assign(
:misc_editor_selected_pairs,
Map.put(selected_by_tab, socket.assigns.current_tab.id, next)
)
|> reload.(socket.assigns.workbench)
end
@@ -75,7 +93,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
socket
|> update_payload(fn payload ->
update_in(payload[:pairs], fn pairs ->
Enum.reject(pairs || [], fn pair -> pair_identity(pair) == pair_id(post_id_a, post_id_b) end)
Enum.reject(pairs || [], fn pair ->
pair_identity(pair) == pair_id(post_id_a, post_id_b)
end)
end)
end)
|> clear_selected_pair(pair_id(post_id_a, post_id_b))
@@ -91,6 +111,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
def dismiss_selected(socket, reload, append_output) do
tab_id = socket.assigns.current_tab.id
selected =
socket.assigns.misc_editor_selected_pairs
|> Map.get(tab_id, MapSet.new())
@@ -106,7 +127,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
Enum.reject(pairs || [], fn pair -> pair_identity(pair) in selected end)
end)
end)
|> assign(:misc_editor_selected_pairs, Map.put(socket.assigns.misc_editor_selected_pairs, tab_id, MapSet.new()))
|> assign(
:misc_editor_selected_pairs,
Map.put(socket.assigns.misc_editor_selected_pairs, tab_id, MapSet.new())
)
|> append_output.(translated("Find Duplicates"), translated("Selected pairs dismissed"))
|> reload.(socket.assigns.workbench)
@@ -137,14 +161,20 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
})
)}
rescue
error -> {:socket, append_output.(socket, translated("Translation Validation"), inspect(error), nil, "error")}
error ->
{:socket,
append_output.(socket, translated("Translation Validation"), inspect(error), nil, "error")}
end
def select_git_diff_file(socket, file_path) do
assign(
socket,
:misc_editor_git_selected_files,
Map.put(socket.assigns.misc_editor_git_selected_files, socket.assigns.current_tab.id, file_path)
Map.put(
socket.assigns.misc_editor_git_selected_files,
socket.assigns.current_tab.id,
file_path
)
)
end
@@ -183,7 +213,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
meta = meta(socket.assigns)
payload = Map.get(meta, :payload, %{})
items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)
orphan_files = Enum.map(Map.get(payload, :orphan_reports, []), &normalize_metadata_diff_orphan/1)
orphan_files =
Enum.map(Map.get(payload, :orphan_reports, []), &normalize_metadata_diff_orphan/1)
tabs = metadata_diff_tabs(items, orphan_files)
active_tab = metadata_diff_active_tab(socket.assigns, tabs)
@@ -214,7 +247,8 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
def build(_assigns), do: nil
def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
def misc_class(:site_validation), do: "site-validation-view"
def misc_class(:metadata_diff), do: "metadata-diff-view"
@@ -255,11 +289,17 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp build_metadata_diff(assigns, meta, payload) do
items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)
orphan_files = Enum.map(Map.get(payload, :orphan_reports, []), &normalize_metadata_diff_orphan/1)
orphan_files =
Enum.map(Map.get(payload, :orphan_reports, []), &normalize_metadata_diff_orphan/1)
tabs = metadata_diff_tabs(items, orphan_files)
active_tab = metadata_diff_active_tab(assigns, tabs)
active_field = metadata_diff_active_field(assigns)
current_tab = Enum.find(tabs, &(&1.id == active_tab)) || List.first(tabs) || empty_metadata_diff_tab()
current_tab =
Enum.find(tabs, &(&1.id == active_tab)) || List.first(tabs) || empty_metadata_diff_tab()
filtered_items = metadata_diff_filtered_items(current_tab.items, active_field)
%{
@@ -267,7 +307,8 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
title: Map.get(meta, :title, translated("Metadata Diff")),
subtitle: Map.get(meta, :subtitle, ""),
summary: Map.get(payload, :summary, %{}),
tabs: Enum.map(tabs, &Map.take(&1, [:id, :label, :badge_count, :diff_count, :orphan_count])),
tabs:
Enum.map(tabs, &Map.take(&1, [:id, :label, :badge_count, :diff_count, :orphan_count])),
active_tab: current_tab.id,
active_field: active_field,
repair_enabled: metadata_diff_repairable_tab?(current_tab.id),
@@ -300,7 +341,8 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
defp build_duplicates(assigns, meta, payload) do
selected_pairs = Map.get(assigns.misc_editor_selected_pairs, assigns.current_tab.id, MapSet.new())
selected_pairs =
Map.get(assigns.misc_editor_selected_pairs, assigns.current_tab.id, MapSet.new())
%{
kind: :find_duplicates,
@@ -319,8 +361,15 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
{files, diff, error_message} =
case Git.status(project_id) do
{:ok, %{files: files}} ->
file_paths = files |> Enum.map(&Map.get(&1, :path)) |> Enum.reject(&is_nil/1) |> Enum.uniq() |> Enum.sort()
selected_file_path = select_git_diff_path(assigns.current_tab.id, file_paths, selected_files)
file_paths =
files
|> Enum.map(&Map.get(&1, :path))
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
|> Enum.sort()
selected_file_path =
select_git_diff_path(assigns.current_tab.id, file_paths, selected_files)
diff =
case selected_file_path do
@@ -329,8 +378,14 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
file_path ->
case Git.get_diff_content(project_id, file_path) do
{:ok, diff} -> diff
{:error, reason} -> Map.merge(empty_git_diff(project_id), %{file_path: file_path, error: inspect(reason)})
{:ok, diff} ->
diff
{:error, reason} ->
Map.merge(empty_git_diff(project_id), %{
file_path: file_path,
error: inspect(reason)
})
end
end
@@ -357,10 +412,17 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
def translation_issue_label(issue) do
case issue_value(issue, :issue) do
"same-language-as-canonical" -> translated("translationValidation.issue.sameLanguage")
"do-not-translate-has-translations" -> translated("translationValidation.issue.doNotTranslate")
"content-in-database" -> translated("translationValidation.issue.contentInDatabase")
_other -> translated("translationValidation.issue.missingSource")
"same-language-as-canonical" ->
translated("translationValidation.issue.sameLanguage")
"do-not-translate-has-translations" ->
translated("translationValidation.issue.doNotTranslate")
"content-in-database" ->
translated("translationValidation.issue.contentInDatabase")
_other ->
translated("translationValidation.issue.missingSource")
end
end
@@ -414,12 +476,17 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp clear_selected_pair(socket, pair_id) do
tab_id = socket.assigns.current_tab.id
current = Map.get(socket.assigns.misc_editor_selected_pairs, tab_id, MapSet.new())
next_pairs = Map.put(socket.assigns.misc_editor_selected_pairs, tab_id, MapSet.delete(current, pair_id))
next_pairs =
Map.put(socket.assigns.misc_editor_selected_pairs, tab_id, MapSet.delete(current, pair_id))
assign(socket, :misc_editor_selected_pairs, next_pairs)
end
defp pair_id(post_id_a, post_id_b), do: Enum.sort([post_id_a, post_id_b]) |> Enum.join("::")
defp pair_identity(pair), do: pair_id(Map.get(pair, :post_id_a) || Map.get(pair, "post_id_a"), Map.get(pair, :post_id_b) || Map.get(pair, "post_id_b"))
defp pair_identity(pair),
do: pair_id(MapUtils.attr(pair, :post_id_a), MapUtils.attr(pair, :post_id_b))
defp decode_pair_id(encoded) when is_binary(encoded) do
case String.split(encoded, "::", parts: 2) do
@@ -432,7 +499,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp field_summaries(items) do
items
|> Enum.flat_map(fn item -> Map.get(item, :differences) || Map.get(item, "differences") || [] end)
|> Enum.flat_map(fn item -> MapUtils.attr(item, :differences) || [] end)
|> Enum.group_by(&diff_name/1)
|> Enum.map(fn {field, diffs} -> %{field_name: field, diff_count: length(diffs)} end)
|> Enum.sort_by(&{&1.diff_count * -1, &1.field_name})
@@ -465,7 +532,15 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
defp empty_metadata_diff_tab do
%{id: "posts", label: translated("Posts"), items: [], orphan_files: [], diff_count: 0, orphan_count: 0, badge_count: 0}
%{
id: "posts",
label: translated("Posts"),
items: [],
orphan_files: [],
diff_count: 0,
orphan_count: 0,
badge_count: 0
}
end
defp metadata_diff_active_tab(assigns, tabs) do
@@ -489,11 +564,12 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
defp normalize_metadata_diff_item(item) do
entity_type = Map.get(item, :entity_type) || Map.get(item, "entity_type") || "post"
entity_id = Map.get(item, :entity_id) || Map.get(item, "entity_id") || ""
entity_type = MapUtils.attr(item, :entity_type) || "post"
entity_id = MapUtils.attr(item, :entity_id) || ""
differences =
item
|> Map.get(:differences, Map.get(item, "differences", []))
|> MapUtils.attr(:differences, [])
|> Enum.map(&normalize_metadata_diff_difference/1)
%{
@@ -510,30 +586,31 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp normalize_metadata_diff_difference(diff) do
%{
field: diff_name(diff),
db_value: format_metadata_diff_value(Map.get(diff, :db_value) || Map.get(diff, "db_value")),
file_value: format_metadata_diff_value(Map.get(diff, :file_value) || Map.get(diff, "file_value"))
db_value: format_metadata_diff_value(MapUtils.attr(diff, :db_value)),
file_value: format_metadata_diff_value(MapUtils.attr(diff, :file_value))
}
end
defp normalize_metadata_diff_orphan(orphan) do
path = Map.get(orphan, :file_path) || Map.get(orphan, "file_path") || Map.get(orphan, :path) || Map.get(orphan, "path") || ""
entity_type = Map.get(orphan, :entity_type) || Map.get(orphan, "entity_type") || metadata_diff_orphan_entity_type(path)
path = MapUtils.attr(orphan, :file_path) || MapUtils.attr(orphan, :path) || ""
entity_type = MapUtils.attr(orphan, :entity_type) || metadata_diff_orphan_entity_type(path)
%{
tab_id: metadata_diff_tab_id(entity_type),
entity_type: entity_type,
file_path: path,
slug: Path.basename(path) |> String.trim(),
id: Map.get(orphan, :id) || Map.get(orphan, "id")
id: MapUtils.attr(orphan, :id)
}
end
defp metadata_diff_item_label(item, entity_id) do
Map.get(item, :label) || Map.get(item, "label") || Map.get(item, :title) || Map.get(item, "title") || Map.get(item, :slug) || Map.get(item, "slug") || entity_id
MapUtils.attr(item, :label) || MapUtils.attr(item, :title) || MapUtils.attr(item, :slug) ||
entity_id
end
defp metadata_diff_item_meta_label(item, entity_id) do
Map.get(item, :meta_label) || Map.get(item, "meta_label") || entity_id
MapUtils.attr(item, :meta_label) || entity_id
end
defp metadata_diff_item_type_label("post"), do: translated("Post")
@@ -547,7 +624,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp metadata_diff_item_type_label("categories"), do: translated("Categories")
defp metadata_diff_item_type_label("category_meta"), do: translated("Categories")
defp metadata_diff_item_type_label("embedding"), do: translated("Embeddings")
defp metadata_diff_item_type_label(entity_type), do: entity_type |> String.replace("_", " ") |> String.capitalize()
defp metadata_diff_item_type_label(entity_type),
do: entity_type |> String.replace("_", " ") |> String.capitalize()
defp metadata_diff_tab_id("post"), do: "posts"
defp metadata_diff_tab_id("post_translation"), do: "posts"
@@ -568,7 +647,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp metadata_diff_tab_label("templates"), do: translated("Templates")
defp metadata_diff_tab_label("project"), do: translated("Project")
defp metadata_diff_tab_label("embeddings"), do: translated("Embeddings")
defp metadata_diff_tab_label(tab_id), do: tab_id |> String.replace("_", " ") |> String.capitalize()
defp metadata_diff_tab_label(tab_id),
do: tab_id |> String.replace("_", " ") |> String.capitalize()
defp metadata_diff_tab_sort_key("posts"), do: 0
defp metadata_diff_tab_sort_key("media"), do: 1
@@ -588,7 +669,8 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
defp metadata_diff_repairable_tab?(tab_id), do: tab_id in ["posts", "media", "scripts", "templates", "project", "embeddings"]
defp metadata_diff_repairable_tab?(tab_id),
do: tab_id in ["posts", "media", "scripts", "templates", "project", "embeddings"]
defp misc_route_action(:site_validation), do: "validate_site"
defp misc_route_action(:metadata_diff), do: "metadata_diff"
@@ -601,7 +683,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
defp format_metadata_diff_value(value), do: to_string(value)
defp diff_name(diff) do
Map.get(diff, :field) || Map.get(diff, "field") || Map.get(diff, :name) || Map.get(diff, "name") || "value"
MapUtils.attr(diff, :field) || MapUtils.attr(diff, :name) || "value"
end
defp normalize_translation_validation_report(payload) when is_map(payload) do

View File

@@ -261,11 +261,11 @@
<%= for pair <- @misc_editor.pairs do %>
<article class="misc-list-item duplicate-pair-row">
<label><input type="checkbox" checked={duplicate_checked?(@misc_editor, pair_id_from_pair(pair))} phx-click="toggle_duplicate_pair" phx-value-pair-id={pair_id_from_pair(pair)} /> <span></span></label>
<button class="linkish" type="button" phx-click="open_duplicate_post" phx-value-id={Map.get(pair, :post_id_a) || Map.get(pair, "post_id_a")} phx-value-title={Map.get(pair, :title_a) || Map.get(pair, "title_a") }><%= Map.get(pair, :title_a) || Map.get(pair, "title_a") %></button>
<button class="linkish" type="button" phx-click="open_duplicate_post" phx-value-id={BDS.MapUtils.attr(pair, :post_id_a)} phx-value-title={BDS.MapUtils.attr(pair, :title_a)}><%= BDS.MapUtils.attr(pair, :title_a) %></button>
<span>→</span>
<button class="linkish" type="button" phx-click="open_duplicate_post" phx-value-id={Map.get(pair, :post_id_b) || Map.get(pair, "post_id_b")} phx-value-title={Map.get(pair, :title_b) || Map.get(pair, "title_b") }><%= Map.get(pair, :title_b) || Map.get(pair, "title_b") %></button>
<span class="misc-summary-pill"><%= if(Map.get(pair, :exact_match) || Map.get(pair, "exact_match"), do: translated("Exact Match"), else: "#{Float.round((Map.get(pair, :similarity) || Map.get(pair, "similarity") || 0.0) * 100, 1)}%") %></span>
<button class="secondary" type="button" phx-click="dismiss_duplicate_pair" phx-value-post-id-a={Map.get(pair, :post_id_a) || Map.get(pair, "post_id_a")} phx-value-post-id-b={Map.get(pair, :post_id_b) || Map.get(pair, "post_id_b")}><%= translated("Dismiss") %></button>
<button class="linkish" type="button" phx-click="open_duplicate_post" phx-value-id={BDS.MapUtils.attr(pair, :post_id_b)} phx-value-title={BDS.MapUtils.attr(pair, :title_b)}><%= BDS.MapUtils.attr(pair, :title_b) %></button>
<span class="misc-summary-pill"><%= if(BDS.MapUtils.attr(pair, :exact_match), do: translated("Exact Match"), else: "#{Float.round((BDS.MapUtils.attr(pair, :similarity) || 0.0) * 100, 1)}%") %></span>
<button class="secondary" type="button" phx-click="dismiss_duplicate_pair" phx-value-post-id-a={BDS.MapUtils.attr(pair, :post_id_a)} phx-value-post-id-b={BDS.MapUtils.attr(pair, :post_id_b)}><%= translated("Dismiss") %></button>
</article>
<% end %>
</div>

View File

@@ -11,6 +11,7 @@ defmodule BDS.Maintenance.Repair do
import BDS.Maintenance.Progress, only: [report_progress: 4]
alias BDS.Embeddings
alias BDS.MapUtils
alias BDS.Metadata
def normalize_entity_type(:post), do: :post
@@ -33,31 +34,62 @@ defmodule BDS.Maintenance.Repair do
def normalize_repair_direction(_direction), do: :unsupported
def repair_metadata_diff_item(project_id, direction, item) do
entity_type = Map.get(item, :entity_type) || Map.get(item, "entity_type")
entity_id = Map.get(item, :entity_id) || Map.get(item, "entity_id")
entity_type = MapUtils.attr(item, :entity_type)
entity_id = MapUtils.attr(item, :entity_id)
case {normalize_repair_direction(direction), entity_type} do
{:file_to_db, entity_type} when entity_type in ["project", "categories", "category_meta", "publishing"] ->
{:file_to_db, entity_type}
when entity_type in ["project", "categories", "category_meta", "publishing"] ->
Metadata.sync_project_metadata_from_filesystem(project_id)
{:db_to_file, entity_type} when entity_type in ["project", "categories", "category_meta", "publishing"] ->
{:db_to_file, entity_type}
when entity_type in ["project", "categories", "category_meta", "publishing"] ->
Metadata.flush_project_metadata_to_filesystem(project_id)
{:file_to_db, "post"} -> BDS.Posts.sync_post_from_file(entity_id)
{:db_to_file, "post"} -> BDS.Posts.rewrite_published_post(entity_id)
{:file_to_db, "post_translation"} -> BDS.Posts.sync_post_translation_from_file(entity_id)
{:db_to_file, "post_translation"} -> BDS.Posts.rewrite_published_post_translation(entity_id)
{:file_to_db, "media"} -> BDS.Media.sync_media_from_sidecar(entity_id)
{:db_to_file, "media"} -> BDS.Media.sync_media_sidecar(entity_id)
{:file_to_db, "media_translation"} -> BDS.Media.sync_media_translation_from_sidecar(entity_id)
{:db_to_file, "media_translation"} -> BDS.Media.sync_media_translation_sidecar(entity_id)
{:file_to_db, "script"} -> BDS.Scripts.sync_script_from_file(entity_id)
{:db_to_file, "script"} -> BDS.Scripts.sync_published_script_file(entity_id)
{:file_to_db, "template"} -> BDS.Templates.sync_template_from_file(entity_id)
{:db_to_file, "template"} -> BDS.Templates.sync_published_template_file(entity_id)
{:file_to_db, "embedding"} -> BDS.Embeddings.sync_post(entity_id)
{:db_to_file, "embedding"} -> BDS.Embeddings.refresh_snapshot(project_id)
_other -> {:error, :unsupported}
{:file_to_db, "post"} ->
BDS.Posts.sync_post_from_file(entity_id)
{:db_to_file, "post"} ->
BDS.Posts.rewrite_published_post(entity_id)
{:file_to_db, "post_translation"} ->
BDS.Posts.sync_post_translation_from_file(entity_id)
{:db_to_file, "post_translation"} ->
BDS.Posts.rewrite_published_post_translation(entity_id)
{:file_to_db, "media"} ->
BDS.Media.sync_media_from_sidecar(entity_id)
{:db_to_file, "media"} ->
BDS.Media.sync_media_sidecar(entity_id)
{:file_to_db, "media_translation"} ->
BDS.Media.sync_media_translation_from_sidecar(entity_id)
{:db_to_file, "media_translation"} ->
BDS.Media.sync_media_translation_sidecar(entity_id)
{:file_to_db, "script"} ->
BDS.Scripts.sync_script_from_file(entity_id)
{:db_to_file, "script"} ->
BDS.Scripts.sync_published_script_file(entity_id)
{:file_to_db, "template"} ->
BDS.Templates.sync_template_from_file(entity_id)
{:db_to_file, "template"} ->
BDS.Templates.sync_published_template_file(entity_id)
{:file_to_db, "embedding"} ->
BDS.Embeddings.sync_post(entity_id)
{:db_to_file, "embedding"} ->
BDS.Embeddings.refresh_snapshot(project_id)
_other ->
{:error, :unsupported}
end
end
@@ -87,7 +119,8 @@ defmodule BDS.Maintenance.Repair do
end
end
def repair_embedding_batch(_project_id, _direction, _items, _on_progress, _total), do: :unsupported
def repair_embedding_batch(_project_id, _direction, _items, _on_progress, _total),
do: :unsupported
defp build_batch_repair_result(items, total, on_progress, repaired?) do
items
@@ -106,15 +139,15 @@ defmodule BDS.Maintenance.Repair do
end
defp metadata_diff_item_entity_type(item) do
Map.get(item, :entity_type) || Map.get(item, "entity_type")
MapUtils.attr(item, :entity_type)
end
defp metadata_diff_item_entity_id(item) do
Map.get(item, :entity_id) || Map.get(item, "entity_id")
MapUtils.attr(item, :entity_id)
end
def import_metadata_diff_orphan(project_id, orphan) do
file_path = Map.get(orphan, :file_path) || Map.get(orphan, "file_path")
file_path = MapUtils.attr(orphan, :file_path)
cond do
is_nil(file_path) ->

View File

@@ -13,6 +13,15 @@ defmodule BDS.MapUtils do
end
end
@spec attr(attrs(), atom(), term()) :: term()
def attr(attrs, key, default) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> default
end
end
@spec maybe_put(map(), term(), term()) :: map()
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)

View File

@@ -173,7 +173,14 @@ defmodule BDS.Metadata do
|> Repo.update!()
persist_setting(project_id, "project", stringify_project_metadata(filesystem_state), now)
persist_setting(project_id, "categories", %{"categories" => filesystem_state.categories}, now)
persist_setting(
project_id,
"categories",
%{"categories" => filesystem_state.categories},
now
)
persist_setting(
project_id,
"category_meta",
@@ -247,10 +254,15 @@ defmodule BDS.Metadata do
read_json(project, "project.json") ||
stringify_project_metadata(default_project_metadata(project))
categories = normalized_categories(read_json(project, "categories.json") || %{"categories" => @default_categories})
categories =
normalized_categories(
read_json(project, "categories.json") || %{"categories" => @default_categories}
)
category_settings =
normalized_category_settings(read_json(project, "category-meta.json") || %{"categories" => %{}})
normalized_category_settings(
read_json(project, "category-meta.json") || %{"categories" => %{}}
)
publishing_preferences = read_json(project, "publishing.json") || %{"ssh_mode" => "scp"}
@@ -305,14 +317,11 @@ defmodule BDS.Metadata do
defp normalize_category_settings(settings) do
%{
"render_in_lists" =>
Map.get(settings, :render_in_lists, Map.get(settings, "render_in_lists", true)),
"show_title" => Map.get(settings, :show_title, Map.get(settings, "show_title", true)),
"post_template_slug" =>
Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")),
"list_template_slug" =>
Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug")),
"title" => Map.get(settings, :title, Map.get(settings, "title"))
"render_in_lists" => attr(settings, :render_in_lists, true),
"show_title" => attr(settings, :show_title, true),
"post_template_slug" => attr(settings, :post_template_slug),
"list_template_slug" => attr(settings, :list_template_slug),
"title" => attr(settings, :title)
}
end
@@ -447,7 +456,9 @@ defmodule BDS.Metadata do
|> Map.new()
end
defp normalized_categories(%{"categories" => categories}) when is_list(categories), do: categories
defp normalized_categories(%{"categories" => categories}) when is_list(categories),
do: categories
defp normalized_categories(categories) when is_list(categories), do: categories
defp normalized_categories(_payload), do: @default_categories
@@ -459,13 +470,25 @@ defmodule BDS.Metadata do
{category,
%{
"render_in_lists" =>
Map.get(category_settings, "render_in_lists", Map.get(category_settings, "renderInLists", true)),
Map.get(
category_settings,
"render_in_lists",
Map.get(category_settings, "renderInLists", true)
),
"show_title" =>
Map.get(category_settings, "show_title", Map.get(category_settings, "showTitle", true)),
"post_template_slug" =>
Map.get(category_settings, "post_template_slug", Map.get(category_settings, "postTemplateSlug")),
Map.get(
category_settings,
"post_template_slug",
Map.get(category_settings, "postTemplateSlug")
),
"list_template_slug" =>
Map.get(category_settings, "list_template_slug", Map.get(category_settings, "listTemplateSlug")),
Map.get(
category_settings,
"list_template_slug",
Map.get(category_settings, "listTemplateSlug")
),
"title" => Map.get(category_settings, "title")
}
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
@@ -480,10 +503,12 @@ defmodule BDS.Metadata do
"publicUrl" => Map.get(project_metadata, "public_url"),
"mainLanguage" => Map.get(project_metadata, "main_language"),
"defaultAuthor" => Map.get(project_metadata, "default_author"),
"maxPostsPerPage" => Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
"maxPostsPerPage" =>
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
"blogmarkCategory" => Map.get(project_metadata, "blogmark_category"),
"picoTheme" => Map.get(project_metadata, "pico_theme"),
"semanticSimilarityEnabled" => Map.get(project_metadata, "semantic_similarity_enabled", false),
"semanticSimilarityEnabled" =>
Map.get(project_metadata, "semantic_similarity_enabled", false),
"blogLanguages" => Map.get(project_metadata, "blog_languages", [])
}
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
@@ -587,7 +612,12 @@ defmodule BDS.Metadata do
defp unwrap_transaction({:ok, result}), do: {:ok, result}
defp unwrap_transaction({:error, reason}), do: {:error, reason}
defp maybe_backfill_embeddings({:ok, _metadata} = result, project_id, previous_state, project_metadata) do
defp maybe_backfill_embeddings(
{:ok, _metadata} = result,
project_id,
previous_state,
project_metadata
) do
if previous_state.semantic_similarity_enabled != true and
project_metadata.semantic_similarity_enabled == true do
{:ok, _indexed_post_ids} = Embeddings.index_unindexed(project_id)
@@ -596,7 +626,8 @@ defmodule BDS.Metadata do
result
end
defp maybe_backfill_embeddings(result, _project_id, _previous_state, _project_metadata), do: result
defp maybe_backfill_embeddings(result, _project_id, _previous_state, _project_metadata),
do: result
defp attr(attrs, key) do
cond do
@@ -605,4 +636,12 @@ defmodule BDS.Metadata do
true -> nil
end
end
defp attr(attrs, key, default) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> default
end
end
end

View File

@@ -2,6 +2,7 @@ defmodule BDS.Rendering.ListArchive do
@moduledoc false
alias BDS.I18n
alias BDS.MapUtils
alias BDS.Persistence
alias BDS.Rendering.LinksAndLanguages
alias BDS.Rendering.Metadata, as: RenderMetadata
@@ -12,18 +13,19 @@ defmodule BDS.Rendering.ListArchive do
metadata = RenderMetadata.project_metadata(project_id)
template_context = TemplateSelection.template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
main_language = metadata.main_language || language
archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))
archive_context = MapUtils.attr(assigns, :archive_context, %{})
canonical_post_paths =
LinksAndLanguages.canonical_post_path_by_slug(project_id, main_language)
canonical_post_paths = LinksAndLanguages.canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = LinksAndLanguages.canonical_media_path_by_source_path(project_id)
posts =
normalize_list_posts(
Map.get(assigns, :posts, Map.get(assigns, "posts", [])),
MapUtils.attr(assigns, :posts, []),
canonical_post_paths,
canonical_media_paths,
language,
@@ -31,7 +33,7 @@ defmodule BDS.Rendering.ListArchive do
)
pagination =
normalize_pagination(Map.get(assigns, :pagination, Map.get(assigns, "pagination")), posts)
normalize_pagination(MapUtils.attr(assigns, :pagination), posts)
day_blocks = build_day_blocks(posts)
min_date = min_date(posts)
@@ -44,15 +46,23 @@ defmodule BDS.Rendering.ListArchive do
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", LinksAndLanguages.language_prefix(language, main_language))
Map.get(
assigns,
"language_prefix",
LinksAndLanguages.language_prefix(language, main_language)
)
),
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title")),
page_title: MapUtils.attr(assigns, :page_title),
posts: posts,
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme))
Map.get(
assigns,
"pico_stylesheet_href",
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
)
),
html_theme_attribute:
Map.get(
@@ -66,7 +76,8 @@ defmodule BDS.Rendering.ListArchive do
calendar_initial_year: calendar_initial_year_from_posts(posts),
calendar_initial_month: calendar_initial_month_from_posts(posts),
archive_context: normalized_archive_context,
show_archive_range_heading: show_archive_range_heading?(normalized_archive_context, day_blocks),
show_archive_range_heading:
show_archive_range_heading?(normalized_archive_context, day_blocks),
min_date: min_date,
max_date: max_date,
is_list_page: true,
@@ -91,25 +102,32 @@ defmodule BDS.Rendering.ListArchive do
def not_found_assigns(project_id, assigns) do
metadata = RenderMetadata.project_metadata(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
main_language = metadata.main_language || language
%{
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", "404")),
page_title: MapUtils.attr(assigns, :page_title, "404"),
language: language,
language_prefix:
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", LinksAndLanguages.language_prefix(language, main_language))
Map.get(
assigns,
"language_prefix",
LinksAndLanguages.language_prefix(language, main_language)
)
),
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme))
Map.get(
assigns,
"pico_stylesheet_href",
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
)
),
html_theme_attribute:
Map.get(
@@ -143,20 +161,27 @@ defmodule BDS.Rendering.ListArchive do
}
end
defp normalize_list_posts(posts, canonical_post_paths, canonical_media_paths, language, template_context) do
defp normalize_list_posts(
posts,
canonical_post_paths,
canonical_media_paths,
language,
template_context
) do
Enum.map(posts, fn post ->
post_record = PostRendering.load_post_record(post)
raw_content =
Map.get(
post,
:content,
Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
MapUtils.attr(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")),
id: MapUtils.attr(post, :id),
slug: MapUtils.attr(post, :slug),
title: MapUtils.attr(post, :title),
content:
PostRendering.render_post_content(
raw_content,
@@ -166,29 +191,30 @@ defmodule BDS.Rendering.ListArchive do
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))),
excerpt: MapUtils.attr(post, :excerpt, Map.get(post_record || %{}, :excerpt)),
author: MapUtils.attr(post, :author, Map.get(post_record || %{}, :author)),
language:
Map.get(
post,
:language,
Map.get(post, "language", Map.get(post_record || %{}, :language))
Map.get(post_record || %{}, :language)
),
published_at:
Map.get(post, :published_at, Map.get(post, "published_at", Map.get(post_record || %{}, :published_at))),
created_at:
Map.get(post, :created_at, Map.get(post, "created_at", Map.get(post_record || %{}, :created_at))),
updated_at:
Map.get(post, :updated_at, Map.get(post, "updated_at", Map.get(post_record || %{}, :updated_at))),
tags: Map.get(post, :tags, Map.get(post, "tags", Map.get(post_record || %{}, :tags, []))) || [],
MapUtils.attr(post, :published_at, Map.get(post_record || %{}, :published_at)),
created_at: MapUtils.attr(post, :created_at, Map.get(post_record || %{}, :created_at)),
updated_at: MapUtils.attr(post, :updated_at, Map.get(post_record || %{}, :updated_at)),
tags: MapUtils.attr(post, :tags, Map.get(post_record || %{}, :tags, [])) || [],
categories:
Map.get(post, :categories, Map.get(post, "categories", Map.get(post_record || %{}, :categories, []))) || [],
MapUtils.attr(post, :categories, Map.get(post_record || %{}, :categories, [])) || [],
template_slug:
Map.get(post, :template_slug, Map.get(post, "template_slug", Map.get(post_record || %{}, :template_slug))),
MapUtils.attr(post, :template_slug, Map.get(post_record || %{}, :template_slug)),
do_not_translate:
Map.get(post, :do_not_translate, Map.get(post, "do_not_translate", Map.get(post_record || %{}, :do_not_translate, false))),
href: Map.get(post, :href, Map.get(post, "href")),
MapUtils.attr(
post,
:do_not_translate,
Map.get(post_record || %{}, :do_not_translate, false)
),
href: MapUtils.attr(post, :href),
show_title: true,
linked_media: [],
outgoing_links: [],
@@ -214,24 +240,20 @@ defmodule BDS.Rendering.ListArchive do
defp normalize_pagination(%{} = pagination, posts) do
total_items =
Map.get(pagination, :total_items, Map.get(pagination, "total_items", length(posts)))
MapUtils.attr(pagination, :total_items, length(posts))
items_per_page =
Map.get(pagination, :items_per_page, Map.get(pagination, "items_per_page", total_items))
MapUtils.attr(pagination, :items_per_page, total_items)
%{
current_page: Map.get(pagination, :current_page, Map.get(pagination, "current_page", 1)),
total_pages: Map.get(pagination, :total_pages, Map.get(pagination, "total_pages", 1)),
current_page: MapUtils.attr(pagination, :current_page, 1),
total_pages: MapUtils.attr(pagination, :total_pages, 1),
total_items: total_items,
items_per_page: items_per_page,
has_prev_page:
Map.get(pagination, :has_prev_page, Map.get(pagination, "has_prev_page", false)),
prev_page_href:
Map.get(pagination, :prev_page_href, Map.get(pagination, "prev_page_href", "")),
has_next_page:
Map.get(pagination, :has_next_page, Map.get(pagination, "has_next_page", false)),
next_page_href:
Map.get(pagination, :next_page_href, Map.get(pagination, "next_page_href", ""))
has_prev_page: MapUtils.attr(pagination, :has_prev_page, false),
prev_page_href: MapUtils.attr(pagination, :prev_page_href, ""),
has_next_page: MapUtils.attr(pagination, :has_next_page, false),
next_page_href: MapUtils.attr(pagination, :next_page_href, "")
}
end
@@ -239,11 +261,11 @@ defmodule BDS.Rendering.ListArchive do
defp normalize_archive_context(%{} = archive_context) do
%{
kind: Map.get(archive_context, :kind, Map.get(archive_context, "kind")),
name: Map.get(archive_context, :name, Map.get(archive_context, "name")),
month: Map.get(archive_context, :month, Map.get(archive_context, "month")),
year: Map.get(archive_context, :year, Map.get(archive_context, "year")),
day: Map.get(archive_context, :day, Map.get(archive_context, "day"))
kind: MapUtils.attr(archive_context, :kind),
name: MapUtils.attr(archive_context, :name),
month: MapUtils.attr(archive_context, :month),
year: MapUtils.attr(archive_context, :year),
day: MapUtils.attr(archive_context, :day)
}
end
@@ -251,7 +273,12 @@ defmodule BDS.Rendering.ListArchive do
grouped_blocks =
posts
|> Enum.filter(&is_integer(Map.get(&1, :created_at)))
|> Enum.group_by(&(Map.get(&1, :created_at) |> Persistence.from_unix_ms!() |> DateTime.to_date() |> Date.to_iso8601()))
|> Enum.group_by(
&(Map.get(&1, :created_at)
|> Persistence.from_unix_ms!()
|> DateTime.to_date()
|> Date.to_iso8601())
)
|> Enum.sort_by(fn {label, _posts} -> label end)
grouped_blocks
@@ -287,9 +314,13 @@ defmodule BDS.Rendering.ListArchive do
defp show_archive_range_heading?(%{kind: "date"}, _day_blocks), do: true
defp show_archive_range_heading?(_archive_context, _day_blocks), do: false
defp calendar_initial_year_from_posts([post | _rest]), do: RenderMetadata.calendar_initial_year(post)
defp calendar_initial_year_from_posts([post | _rest]),
do: RenderMetadata.calendar_initial_year(post)
defp calendar_initial_year_from_posts([]), do: nil
defp calendar_initial_month_from_posts([post | _rest]), do: RenderMetadata.calendar_initial_month(post)
defp calendar_initial_month_from_posts([post | _rest]),
do: RenderMetadata.calendar_initial_month(post)
defp calendar_initial_month_from_posts([]), do: nil
end

View File

@@ -5,6 +5,7 @@ defmodule BDS.Rendering.PostRendering do
alias BDS.Rendering.LinksAndLanguages
alias BDS.Rendering.Metadata, as: RenderMetadata
alias BDS.Rendering.TemplateSelection
alias BDS.MapUtils
alias BDS.Posts.Post
alias BDS.Posts.Translation
alias BDS.Repo
@@ -13,8 +14,7 @@ defmodule BDS.Rendering.PostRendering do
metadata = RenderMetadata.project_metadata(project_id)
template_context = TemplateSelection.template_render_context(project_id)
language =
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
main_language = metadata.main_language || language
post_record = load_post_record(assigns)
@@ -22,12 +22,27 @@ defmodule BDS.Rendering.PostRendering do
post_id = canonical_post_id(post_record, assigns)
post_categories = Map.get(post_record || %{}, :categories, []) || []
post_tags = Map.get(post_record || %{}, :tags, []) || []
canonical_post_paths = LinksAndLanguages.canonical_post_path_by_slug(project_id, main_language)
canonical_post_paths =
LinksAndLanguages.canonical_post_path_by_slug(project_id, main_language)
canonical_media_paths = LinksAndLanguages.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 = LinksAndLanguages.link_contexts(project_id, post_id, :incoming, main_language)
outgoing_links = LinksAndLanguages.link_contexts(project_id, post_id, :outgoing, main_language)
raw_content = MapUtils.attr(assigns, :content)
rendered_content =
render_post_content(
raw_content,
canonical_post_paths,
canonical_media_paths,
language,
template_context
)
incoming_links =
LinksAndLanguages.link_contexts(project_id, post_id, :incoming, main_language)
outgoing_links =
LinksAndLanguages.link_contexts(project_id, post_id, :outgoing, main_language)
post_assigns =
assigns
@@ -40,19 +55,27 @@ defmodule BDS.Rendering.PostRendering do
Map.get(
assigns,
:language_prefix,
Map.get(assigns, "language_prefix", LinksAndLanguages.language_prefix(language, main_language))
Map.get(
assigns,
"language_prefix",
LinksAndLanguages.language_prefix(language, main_language)
)
),
page_title:
Map.get(
assigns,
:page_title,
Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))
MapUtils.attr(assigns, :title)
),
pico_stylesheet_href:
Map.get(
assigns,
:pico_stylesheet_href,
Map.get(assigns, "pico_stylesheet_href", RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme))
Map.get(
assigns,
"pico_stylesheet_href",
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
)
),
html_theme_attribute:
Map.get(
@@ -77,7 +100,7 @@ defmodule BDS.Rendering.PostRendering do
end
def load_post_record(assigns) do
case Map.get(assigns, :id, Map.get(assigns, "id")) do
case MapUtils.attr(assigns, :id) do
nil -> nil
post_id -> Repo.get(Post, post_id) || Repo.get(Translation, post_id)
end
@@ -89,17 +112,33 @@ defmodule BDS.Rendering.PostRendering do
defp canonical_post_id(%Translation{translation_for: post_id}, _assigns), do: post_id
defp canonical_post_id(%Post{id: post_id}, _assigns), do: post_id
defp canonical_post_id(_post_record, assigns), do: Map.get(assigns, :id, Map.get(assigns, "id"))
defp canonical_post_id(_post_record, assigns), do: MapUtils.attr(assigns, :id)
defp post_data_json(assigns, post_record) do
id = Map.get(assigns, :id, Map.get(assigns, "id"))
id = MapUtils.attr(assigns, :id)
if is_binary(id) do
incoming_links = LinksAndLanguages.link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :incoming, Map.get(post_record || %{}, :language))
outgoing_links = LinksAndLanguages.link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :outgoing, Map.get(post_record || %{}, :language))
incoming_links =
LinksAndLanguages.link_contexts(
Map.get(post_record || %{}, :project_id),
canonical_post_id(post_record, assigns),
:incoming,
Map.get(post_record || %{}, :language)
)
outgoing_links =
LinksAndLanguages.link_contexts(
Map.get(post_record || %{}, :project_id),
canonical_post_id(post_record, assigns),
:outgoing,
Map.get(post_record || %{}, :language)
)
%{
id => post_data_json_value(build_post_context(assigns, post_record, incoming_links, outgoing_links))
id =>
post_data_json_value(
build_post_context(assigns, post_record, incoming_links, outgoing_links)
)
}
else
%{}
@@ -124,23 +163,23 @@ defmodule BDS.Rendering.PostRendering do
defp build_post_context(assigns, post_record, incoming_links, outgoing_links) do
%{
id: Map.get(assigns, :id, Map.get(assigns, "id")),
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")),
id: MapUtils.attr(assigns, :id),
slug: MapUtils.attr(assigns, :slug),
title: MapUtils.attr(assigns, :title),
content: MapUtils.attr(assigns, :content),
raw_content: MapUtils.attr(assigns, :raw_content),
excerpt:
Map.get(
assigns,
:excerpt,
Map.get(assigns, "excerpt", Map.get(post_record || %{}, :excerpt))
Map.get(post_record || %{}, :excerpt)
),
author: Map.get(post_record || %{}, :author),
language:
Map.get(
assigns,
:language,
Map.get(assigns, "language", Map.get(post_record || %{}, :language))
Map.get(post_record || %{}, :language)
),
show_title: true,
published_at: Map.get(post_record || %{}, :published_at),
@@ -152,7 +191,7 @@ defmodule BDS.Rendering.PostRendering do
Map.get(
post_record || %{},
:template_slug,
Map.get(assigns, :template_slug, Map.get(assigns, "template_slug"))
MapUtils.attr(assigns, :template_slug)
),
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
linked_media: [],
@@ -161,7 +200,19 @@ defmodule BDS.Rendering.PostRendering do
}
end
def 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)
def 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
end

View File

@@ -32,13 +32,14 @@ defmodule BDS.UI.Commands do
]
def handle_shortcut(state, shortcut) when is_map(shortcut) do
key = shortcut |> Map.get(:key, Map.get(shortcut, "key", "")) |> String.downcase()
primary =
Map.get(shortcut, :meta, Map.get(shortcut, "meta", false)) or
Map.get(shortcut, :ctrl, Map.get(shortcut, "ctrl", false))
key = shortcut |> BDS.MapUtils.attr(:key, "") |> String.downcase()
shift = Map.get(shortcut, :shift, Map.get(shortcut, "shift", false))
alt = Map.get(shortcut, :alt, Map.get(shortcut, "alt", false))
primary =
BDS.MapUtils.attr(shortcut, :meta, false) or
BDS.MapUtils.attr(shortcut, :ctrl, false)
shift = BDS.MapUtils.attr(shortcut, :shift, false)
alt = BDS.MapUtils.attr(shortcut, :alt, false)
case Enum.find(@menu_shortcuts, &shortcut_match?(&1, key, primary, shift, alt)) do
%{id: command_id} -> MenuBar.execute(state, command_id)

View File

@@ -27,7 +27,13 @@ defmodule BDS.UI.Sidebar do
"templates" => view(project_id, "templates"),
"tags" => view(project_id, "tags"),
"chat" => view(project_id, "chat"),
"import" => entity_list_view("Import", "Import definitions", "import", list_import_definitions(project_id)),
"import" =>
entity_list_view(
"Import",
"Import definitions",
"import",
list_import_definitions(project_id)
),
"git" => git_view(),
"settings" => settings_nav_view()
}
@@ -41,17 +47,43 @@ defmodule BDS.UI.Sidebar do
normalized_view = normalize_view_id(view_id)
case normalized_view do
"posts" -> posts_view(project_id, params, false)
"pages" -> posts_view(project_id, params, true)
"media" -> media_view(project_id, params)
"scripts" -> entity_list_view("Scripts", "Automation helpers", "scripts", list_scripts(project_id))
"templates" -> entity_list_view("Templates", "Site rendering", "templates", list_templates(project_id))
"tags" -> tags_nav_view(list_tags(project_id))
"chat" -> entity_list_view("Chat", "AI conversations", "chat", list_conversations())
"import" -> entity_list_view("Import", "Import definitions", "import", list_import_definitions(project_id))
"git" -> git_view()
"settings" -> settings_nav_view()
_other -> empty_view(normalized_view)
"posts" ->
posts_view(project_id, params, false)
"pages" ->
posts_view(project_id, params, true)
"media" ->
media_view(project_id, params)
"scripts" ->
entity_list_view("Scripts", "Automation helpers", "scripts", list_scripts(project_id))
"templates" ->
entity_list_view("Templates", "Site rendering", "templates", list_templates(project_id))
"tags" ->
tags_nav_view(list_tags(project_id))
"chat" ->
entity_list_view("Chat", "AI conversations", "chat", list_conversations())
"import" ->
entity_list_view(
"Import",
"Import definitions",
"import",
list_import_definitions(project_id)
)
"git" ->
git_view()
"settings" ->
settings_nav_view()
_other ->
empty_view(normalized_view)
end
end
@@ -74,13 +106,18 @@ defmodule BDS.UI.Sidebar do
defp empty_view("pages"), do: posts_view_data([], [], %{}, true, empty_filter_params(), %{})
defp empty_view("media"), do: media_view_data([], [], empty_filter_params(), %{})
defp empty_view("scripts"), do: entity_list_view("Scripts", "Automation helpers", "scripts", [])
defp empty_view("templates"), do: entity_list_view("Templates", "Site rendering", "templates", [])
defp empty_view("templates"),
do: entity_list_view("Templates", "Site rendering", "templates", [])
defp empty_view("tags"), do: tags_nav_view([])
defp empty_view("chat"), do: entity_list_view("Chat", "AI conversations", "chat", [])
defp empty_view("import"), do: entity_list_view("Import", "Import definitions", "import", [])
defp empty_view("git"), do: git_view()
defp empty_view("settings"), do: settings_nav_view()
defp empty_view(_other), do: %{title: "", subtitle: "", layout: "entity_list", items: [], empty_message: "No items"}
defp empty_view(_other),
do: %{title: "", subtitle: "", layout: "entity_list", items: [], empty_message: "No items"}
defp posts_view(project_id, params, pages?) do
posts = list_posts(project_id)
@@ -93,7 +130,14 @@ defmodule BDS.UI.Sidebar do
posts_view_data(base_posts, filtered_posts, translation_counts, pages?, filters, tag_colors)
end
defp posts_view_data(base_posts, filtered_posts, translation_counts, pages?, filters, tag_colors) do
defp posts_view_data(
base_posts,
filtered_posts,
translation_counts,
pages?,
filters,
tag_colors
) do
limited_posts = Enum.take(filtered_posts, filters.display_limit)
grouped_posts = group_posts(limited_posts)
available_tags = available_tags(base_posts, & &1.tags)
@@ -101,12 +145,14 @@ defmodule BDS.UI.Sidebar do
%{
title: if(pages?, do: "Pages", else: "Posts"),
subtitle: if(pages?, do: "Standalone pages", else: "Drafts, published entries, and archive history"),
subtitle:
if(pages?, do: "Standalone pages", else: "Drafts, published entries, and archive history"),
layout: "post_list",
empty_message: if(pages?, do: "sidebar.noPagesYet", else: "sidebar.noPostsYet"),
filters: %{
enabled: true,
search_placeholder: if(pages?, do: "sidebar.searchPagesPlaceholder", else: "sidebar.searchPostsPlaceholder"),
search_placeholder:
if(pages?, do: "sidebar.searchPagesPlaceholder", else: "sidebar.searchPostsPlaceholder"),
toggle_filters_label: "sidebar.toggleFilters",
archive_label: "render.archive",
tags_label: "sidebar.tags",
@@ -137,8 +183,20 @@ defmodule BDS.UI.Sidebar do
},
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)
build_post_section(
"Published",
:published,
grouped_posts.published,
translation_counts,
true
),
build_post_section(
"Archived",
:archived,
grouped_posts.archived,
translation_counts,
false
)
]
}
end
@@ -246,7 +304,13 @@ defmodule BDS.UI.Sidebar do
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}
%{
id: "git-working-tree",
title: "Working tree",
meta: "Working tree and history",
route: "git_diff",
updated_at: nil
}
]
}
end
@@ -285,7 +349,8 @@ defmodule BDS.UI.Sidebar do
tags: post.tags || [],
status: Atom.to_string(post.status),
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),
meta_timestamp:
if(published_meta?, do: post.published_at || post.updated_at, else: post.updated_at),
route: "post",
search_blob: post_search_blob(post)
}
@@ -377,7 +442,11 @@ defmodule BDS.UI.Sidebar 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}
select: %{
id: conversation.id,
title: conversation.title,
updated_at: conversation.updated_at
}
)
end
@@ -402,15 +471,15 @@ defmodule BDS.UI.Sidebar do
defp normalize_filter_params(params) when is_map(params) do
%{
search: normalize_string(Map.get(params, "search") || Map.get(params, :search)),
year: normalize_integer(Map.get(params, "year") || Map.get(params, :year)),
month: normalize_integer(Map.get(params, "month") || Map.get(params, :month)),
tags: normalize_string_list(Map.get(params, "tags") || Map.get(params, :tags)),
categories: normalize_string_list(Map.get(params, "categories") || Map.get(params, :categories)),
search: normalize_string(BDS.MapUtils.attr(params, :search)),
year: normalize_integer(BDS.MapUtils.attr(params, :year)),
month: normalize_integer(BDS.MapUtils.attr(params, :month)),
tags: normalize_string_list(BDS.MapUtils.attr(params, :tags)),
categories: normalize_string_list(BDS.MapUtils.attr(params, :categories)),
display_limit:
max(
@default_page_size,
normalize_integer(Map.get(params, "display_limit") || Map.get(params, :display_limit)) || @default_page_size
normalize_integer(BDS.MapUtils.attr(params, :display_limit)) || @default_page_size
)
}
end
@@ -418,11 +487,19 @@ defmodule BDS.UI.Sidebar do
defp normalize_filter_params(_params), do: empty_filter_params()
defp empty_filter_params do
%{search: nil, year: nil, month: nil, tags: [], categories: [], display_limit: @default_page_size}
%{
search: nil,
year: nil,
month: nil,
tags: [],
categories: [],
display_limit: @default_page_size
}
end
defp filter_active?(filters) do
present?(filters.search) or not is_nil(filters.year) or filters.tags != [] or filters.categories != []
present?(filters.search) or not is_nil(filters.year) or filters.tags != [] or
filters.categories != []
end
defp apply_post_filters(posts, filters) do
@@ -497,7 +574,9 @@ defmodule BDS.UI.Sidebar do
posts
|> Enum.flat_map(&filtered_categories(&1.categories || []))
|> then(fn categories ->
if pages?, do: Enum.reject(categories, &(normalize_term(&1) == @page_category)), else: categories
if pages?,
do: Enum.reject(categories, &(normalize_term(&1) == @page_category)),
else: categories
end)
|> Enum.map(&to_string/1)
|> Enum.reject(&(&1 == ""))
@@ -522,7 +601,13 @@ defmodule BDS.UI.Sidebar do
defp post_filter_timestamp(post), do: post.published_at || post.updated_at
defp post_search_blob(post) do
[post.title, post.slug, post.excerpt, Enum.join(post.tags || [], " "), Enum.join(post.categories || [], " ")]
[
post.title,
post.slug,
post.excerpt,
Enum.join(post.tags || [], " "),
Enum.join(post.categories || [], " ")
]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end

View File

@@ -10,6 +10,12 @@ defmodule BDS.MapUtilsTest do
assert MapUtils.attr(%{"title" => "fallback", title: nil}, :title) == nil
assert MapUtils.attr(%{}, :title) == nil
end
test "reads with a default while preserving explicit nil and false" do
assert MapUtils.attr(%{}, :published, true) == true
assert MapUtils.attr(%{"published" => false}, :published, true) == false
assert MapUtils.attr(%{"published" => nil}, :published, true) == nil
end
end
describe "maybe_put/3" do
@@ -28,4 +34,47 @@ defmodule BDS.MapUtilsTest do
assert MapUtils.blank_to_nil(42) == 42
end
end
describe "atom/string key duality" do
test "shared attr helper is used for same-name atom and string reads" do
root = File.cwd!()
offenders =
[Path.join(root, "lib/**/*.ex"), Path.join(root, "lib/**/*.heex")]
|> Enum.flat_map(&Path.wildcard/1)
|> Enum.flat_map(fn path ->
path
|> File.stream!()
|> Stream.with_index(1)
|> Enum.flat_map(fn {line, line_number} ->
if same_name_dual_key_read?(line) do
["#{Path.relative_to(path, root)}:#{line_number}:#{String.trim(line)}"]
else
[]
end
end)
end)
assert offenders == []
end
end
defp same_name_dual_key_read?(line) do
Regex.match?(
~r/Map\.get\((\w+),\s*:([a-zA-Z_][a-zA-Z0-9_?!]*)\).{0,120}Map\.get\(\1,\s*"\2"\)/,
line
) or
Regex.match?(
~r/Map\.get\((\w+),\s*:([a-zA-Z_][a-zA-Z0-9_?!]*),\s*Map\.get\(\1,\s*"\2"/,
line
) or
Regex.match?(
~r/Map\.get\((\w+),\s*"([a-zA-Z_][a-zA-Z0-9_?!]*)"\).{0,120}Map\.get\(\1,\s*:\2\)/,
line
) or
Regex.match?(
~r/Map\.get\((\w+),\s*"([a-zA-Z_][a-zA-Z0-9_?!]*)",\s*Map\.get\(\1,\s*:\2/,
line
)
end
end