chore: section 12 closed, had to do with map and atoms
This commit is contained in:
@@ -121,11 +121,9 @@ _None._ All modules previously on the queue have been split; refresh the queue i
|
|||||||
|
|
||||||
## 12. Atom/String Key Duality
|
## 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.
|
**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.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -161,6 +159,8 @@ Most tests share the SQLite repo and named GenServers (`BDS.Tasks`, `BDS.Search`
|
|||||||
|
|
||||||
### 2026-05-01
|
### 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.
|
- **`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
|
### 2026-05-10
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ defmodule BDS.AI do
|
|||||||
alias BDS.AI.OneShot
|
alias BDS.AI.OneShot
|
||||||
alias BDS.AI.Runtime
|
alias BDS.AI.Runtime
|
||||||
alias BDS.AI.SecretBackend
|
alias BDS.AI.SecretBackend
|
||||||
|
alias BDS.MapUtils
|
||||||
|
|
||||||
import BDS.AI.SettingsStore,
|
import BDS.AI.SettingsStore,
|
||||||
only: [
|
only: [
|
||||||
@@ -21,20 +22,26 @@ defmodule BDS.AI do
|
|||||||
@type endpoint_kind :: atom()
|
@type endpoint_kind :: atom()
|
||||||
|
|
||||||
@typedoc "Endpoint configuration map."
|
@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."
|
@typedoc "Attribute map for endpoint operations."
|
||||||
@type endpoint_attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
|
@type endpoint_attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
|
||||||
|
|
||||||
@spec put_endpoint(endpoint_kind(), endpoint_attrs(), keyword()) ::
|
@spec put_endpoint(endpoint_kind(), endpoint_attrs(), keyword()) ::
|
||||||
{:ok, endpoint()} | {:error, term()}
|
{: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)
|
backend = Keyword.get(opts, :secret_backend, SecretBackend)
|
||||||
kind_key = Atom.to_string(kind)
|
kind_key = Atom.to_string(kind)
|
||||||
|
|
||||||
url = Map.get(attrs, :url) || Map.get(attrs, "url")
|
url = MapUtils.attr(attrs, :url)
|
||||||
model = Map.get(attrs, :model) || Map.get(attrs, "model")
|
model = MapUtils.attr(attrs, :model)
|
||||||
api_key = Map.get(attrs, :api_key) || Map.get(attrs, "api_key")
|
api_key = MapUtils.attr(attrs, :api_key)
|
||||||
|
|
||||||
with :ok <- put_setting("ai.#{kind_key}.url", url),
|
with :ok <- put_setting("ai.#{kind_key}.url", url),
|
||||||
:ok <- put_setting("ai.#{kind_key}.model", model),
|
:ok <- put_setting("ai.#{kind_key}.model", model),
|
||||||
@@ -103,7 +110,8 @@ defmodule BDS.AI do
|
|||||||
end
|
end
|
||||||
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
|
def put_model_preference(key, model) when is_atom(key) and is_binary(model) do
|
||||||
case Map.fetch(Runtime.model_preference_keys(), key) do
|
case Map.fetch(Runtime.model_preference_keys(), key) do
|
||||||
{:ok, setting_key} -> put_setting(setting_key, model)
|
{:ok, setting_key} -> put_setting(setting_key, model)
|
||||||
@@ -111,7 +119,8 @@ defmodule BDS.AI do
|
|||||||
end
|
end
|
||||||
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
|
def get_model_preference(key) when is_atom(key) do
|
||||||
case Map.fetch(Runtime.model_preference_keys(), key) do
|
case Map.fetch(Runtime.model_preference_keys(), key) do
|
||||||
{:ok, setting_key} -> {:ok, get_setting(setting_key)}
|
{: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()}
|
@spec analyze_post(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
||||||
defdelegate analyze_post(post_input, opts \\ []), to: OneShot
|
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
|
defdelegate translate_post(post_input, target_language, opts \\ []), to: OneShot
|
||||||
|
|
||||||
@spec analyze_image(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
@spec analyze_image(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
||||||
defdelegate analyze_image(media_input, opts \\ []), to: OneShot
|
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
|
defdelegate translate_media(media_input, target_language, opts \\ []), to: OneShot
|
||||||
|
|
||||||
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
|
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ defmodule BDS.AI.Catalog do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
import BDS.AI.SettingsStore,
|
import BDS.AI.SettingsStore,
|
||||||
only: [
|
only: [
|
||||||
get_setting: 1,
|
get_setting: 1,
|
||||||
@@ -21,7 +22,13 @@ defmodule BDS.AI.Catalog do
|
|||||||
|
|
||||||
@spec list_endpoint_models(map(), keyword()) :: {:ok, [map()]} | {:error, term()}
|
@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
|
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)
|
OpenAICompatibleRuntime.list_models(endpoint, http_client: http_client)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,8 +110,8 @@ defmodule BDS.AI.Catalog do
|
|||||||
@spec put_model_capabilities(String.t(), map()) :: :ok | {:error, term()}
|
@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
|
def put_model_capabilities(model_id, attrs) when is_binary(model_id) and is_map(attrs) do
|
||||||
capabilities = %{
|
capabilities = %{
|
||||||
supports_attachment: truthy?(Map.get(attrs, :supports_attachment) || Map.get(attrs, "supports_attachment")),
|
supports_attachment: truthy?(BDS.MapUtils.attr(attrs, :supports_attachment)),
|
||||||
supports_tool_calls: truthy?(Map.get(attrs, :supports_tool_calls) || Map.get(attrs, "supports_tool_calls"))
|
supports_tool_calls: truthy?(BDS.MapUtils.attr(attrs, :supports_tool_calls))
|
||||||
}
|
}
|
||||||
|
|
||||||
put_setting("ai.model_capabilities.#{model_id}", Jason.encode!(capabilities))
|
put_setting("ai.model_capabilities.#{model_id}", Jason.encode!(capabilities))
|
||||||
@@ -154,7 +161,10 @@ defmodule BDS.AI.Catalog do
|
|||||||
}
|
}
|
||||||
end
|
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
|
def model_capabilities(model_id) do
|
||||||
overrides = decode_model_capabilities_override(model_id)
|
overrides = decode_model_capabilities_override(model_id)
|
||||||
|
|
||||||
@@ -162,7 +172,7 @@ defmodule BDS.AI.Catalog do
|
|||||||
case get_catalog_model(model_id) do
|
case get_catalog_model(model_id) do
|
||||||
{:ok, model} ->
|
{: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
|
supports_tool_calls: model.supports_tool_calls
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,8 +267,19 @@ defmodule BDS.AI.Catalog do
|
|||||||
|> Model.changeset(model_attrs)
|
|> Model.changeset(model_attrs)
|
||||||
|> Repo.insert!()
|
|> Repo.insert!()
|
||||||
|
|
||||||
insert_modalities(provider_id, model_id, Map.get(model_data, "input_modalities", []), :input)
|
insert_modalities(
|
||||||
insert_modalities(provider_id, model_id, Map.get(model_data, "output_modalities", []), :output)
|
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
|
inner_count + 1
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ defmodule BDS.AI.Chat do
|
|||||||
alias BDS.AI.OpenAICompatibleRuntime
|
alias BDS.AI.OpenAICompatibleRuntime
|
||||||
alias BDS.AI.Runtime
|
alias BDS.AI.Runtime
|
||||||
alias BDS.AI.SecretBackend
|
alias BDS.AI.SecretBackend
|
||||||
|
alias BDS.MapUtils
|
||||||
import BDS.AI.SettingsStore, only: [get_setting: 1]
|
import BDS.AI.SettingsStore, only: [get_setting: 1]
|
||||||
alias BDS.Media.Media
|
alias BDS.Media.Media
|
||||||
alias BDS.Persistence
|
alias BDS.Persistence
|
||||||
@@ -28,15 +29,15 @@ defmodule BDS.AI.Chat do
|
|||||||
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
|
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
|
||||||
def start_chat(attrs \\ %{}) when is_map(attrs) do
|
def start_chat(attrs \\ %{}) when is_map(attrs) do
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
model = Map.get(attrs, :model) || Map.get(attrs, "model")
|
model = MapUtils.attr(attrs, :model)
|
||||||
title = Map.get(attrs, :title) || Map.get(attrs, "title") || generated_chat_title(model)
|
title = MapUtils.attr(attrs, :title) || generated_chat_title(model)
|
||||||
|
|
||||||
%ChatConversation{}
|
%ChatConversation{}
|
||||||
|> ChatConversation.changeset(%{
|
|> ChatConversation.changeset(%{
|
||||||
id: Ecto.UUID.generate(),
|
id: Ecto.UUID.generate(),
|
||||||
title: title,
|
title: title,
|
||||||
model: model,
|
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,
|
created_at: now,
|
||||||
updated_at: now
|
updated_at: now
|
||||||
})
|
})
|
||||||
@@ -120,7 +121,8 @@ defmodule BDS.AI.Chat do
|
|||||||
def send_chat_message(conversation_id, content, opts \\ [])
|
def send_chat_message(conversation_id, content, opts \\ [])
|
||||||
when is_binary(conversation_id) and is_binary(content) and is_list(opts) do
|
when is_binary(conversation_id) and is_binary(content) and is_list(opts) do
|
||||||
with %ChatConversation{} = conversation <- Repo.get(ChatConversation, conversation_id),
|
with %ChatConversation{} = conversation <- Repo.get(ChatConversation, conversation_id),
|
||||||
{:ok, user_message} <- persist_chat_message(%{
|
{:ok, user_message} <-
|
||||||
|
persist_chat_message(%{
|
||||||
conversation_id: conversation.id,
|
conversation_id: conversation.id,
|
||||||
role: :user,
|
role: :user,
|
||||||
content: content,
|
content: content,
|
||||||
@@ -153,7 +155,9 @@ defmodule BDS.AI.Chat do
|
|||||||
@spec cancel_chat(String.t()) :: :ok
|
@spec cancel_chat(String.t()) :: :ok
|
||||||
def cancel_chat(conversation_id) when is_binary(conversation_id) do
|
def cancel_chat(conversation_id) when is_binary(conversation_id) do
|
||||||
case InFlight.lookup(conversation_id) do
|
case InFlight.lookup(conversation_id) do
|
||||||
nil -> :ok
|
nil ->
|
||||||
|
:ok
|
||||||
|
|
||||||
pid ->
|
pid ->
|
||||||
_ = Task.Supervisor.terminate_child(BDS.Tasks.TaskSupervisor, pid)
|
_ = Task.Supervisor.terminate_child(BDS.Tasks.TaskSupervisor, pid)
|
||||||
:ok
|
:ok
|
||||||
@@ -162,7 +166,11 @@ defmodule BDS.AI.Chat do
|
|||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def count_distinct_string_list(schema, field, project_id) do
|
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()
|
|> List.flatten()
|
||||||
|> Enum.reject(&blank?/1)
|
|> Enum.reject(&blank?/1)
|
||||||
|> MapSet.new()
|
|> MapSet.new()
|
||||||
@@ -267,9 +275,14 @@ defmodule BDS.AI.Chat do
|
|||||||
normalized_url = String.downcase(url)
|
normalized_url = String.downcase(url)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
String.contains?(normalized_url, "11434") or String.contains?(normalized_url, "ollama") -> "ollama"
|
String.contains?(normalized_url, "11434") or String.contains?(normalized_url, "ollama") ->
|
||||||
String.contains?(normalized_url, "1234") or String.contains?(normalized_url, "lmstudio") -> "lmstudio"
|
"ollama"
|
||||||
true -> "generic-openai"
|
|
||||||
|
String.contains?(normalized_url, "1234") or String.contains?(normalized_url, "lmstudio") ->
|
||||||
|
"lmstudio"
|
||||||
|
|
||||||
|
true ->
|
||||||
|
"generic-openai"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -303,23 +316,58 @@ defmodule BDS.AI.Chat do
|
|||||||
:ok <- Runtime.validate_target(:chat, model, mode),
|
:ok <- Runtime.validate_target(:chat, model, mode),
|
||||||
messages <- load_chat_messages(conversation.id),
|
messages <- load_chat_messages(conversation.id),
|
||||||
tools <- available_chat_tools(project_id, model),
|
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}
|
{:ok, reply}
|
||||||
end
|
end
|
||||||
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}}
|
{:error, %{kind: :tool_loop_exhausted}}
|
||||||
end
|
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)
|
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, assistant_message} <- persist_assistant_response(conversation.id, response),
|
||||||
:ok <- touch_conversation(conversation.id) do
|
:ok <- touch_conversation(conversation.id) do
|
||||||
if is_binary(Map.get(response, :content)) and String.trim(Map.get(response, :content)) != "" 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
|
end
|
||||||
|
|
||||||
tool_calls = decode_tool_calls(Map.get(response, :tool_calls))
|
tool_calls = decode_tool_calls(Map.get(response, :tool_calls))
|
||||||
@@ -330,7 +378,8 @@ defmodule BDS.AI.Chat do
|
|||||||
|
|
||||||
cond do
|
cond do
|
||||||
tool_calls != [] and tools != [] ->
|
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),
|
updated_messages <- load_chat_messages(conversation.id),
|
||||||
{:ok, reply} <-
|
{:ok, reply} <-
|
||||||
chat_round(
|
chat_round(
|
||||||
@@ -420,8 +469,13 @@ defmodule BDS.AI.Chat do
|
|||||||
defp message_for_runtime(%ChatMessage{} = message) do
|
defp message_for_runtime(%ChatMessage{} = message) do
|
||||||
base = %{"role" => Atom.to_string(message.role)}
|
base = %{"role" => Atom.to_string(message.role)}
|
||||||
|
|
||||||
base = if is_binary(message.content), do: Map.put(base, "content", message.content), else: base
|
base =
|
||||||
base = if is_binary(message.tool_call_id), do: Map.put(base, "tool_call_id", message.tool_call_id), else: 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
|
case Catalog.decode_nullable_json(message.tool_calls) do
|
||||||
nil -> base
|
nil -> base
|
||||||
@@ -438,7 +492,9 @@ defmodule BDS.AI.Chat do
|
|||||||
[system | remainder] = messages
|
[system | remainder] = messages
|
||||||
|
|
||||||
{kept, _tokens} =
|
{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)
|
message_tokens = approximate_message_tokens(message)
|
||||||
|
|
||||||
if used + message_tokens <= max_budget do
|
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(nil), do: nil
|
||||||
|
|
||||||
defp project_stats_summary(project_id) do
|
defp project_stats_summary(project_id) do
|
||||||
post_count = Repo.aggregate(from(post in Post, where: post.project_id == ^project_id), :count, :id)
|
post_count =
|
||||||
media_count = Repo.aggregate(from(media in Media, where: media.project_id == ^project_id), :count, :id)
|
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)
|
tag_count = count_distinct_string_list(Post, :tags, project_id)
|
||||||
category_count = count_distinct_string_list(Post, :categories, project_id)
|
category_count = count_distinct_string_list(Post, :categories, project_id)
|
||||||
|
|
||||||
@@ -528,9 +588,14 @@ defmodule BDS.AI.Chat do
|
|||||||
10 -> {:error, :cancelled}
|
10 -> {:error, :cancelled}
|
||||||
end
|
end
|
||||||
|
|
||||||
:shutdown -> {:error, :cancelled}
|
:shutdown ->
|
||||||
{:shutdown, _detail} -> {:error, :cancelled}
|
{:error, :cancelled}
|
||||||
_other -> {:error, :cancelled}
|
|
||||||
|
{:shutdown, _detail} ->
|
||||||
|
{:error, :cancelled}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
{:error, :cancelled}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -556,14 +621,22 @@ defmodule BDS.AI.Chat do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp approximate_value_tokens(value) when is_binary(value), do: div(String.length(value), 4) + 1
|
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 approximate_value_tokens(_value), do: 1
|
||||||
|
|
||||||
defp model_context_window(model_id) do
|
defp model_context_window(model_id) do
|
||||||
case Catalog.get_catalog_model(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
|
{:ok, model} when is_integer(model.context_window) and model.context_window > 0 ->
|
||||||
_other -> @default_context_window
|
model.context_window
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
@default_context_window
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ defmodule BDS.AI.OneShot do
|
|||||||
alias BDS.AI.OpenAICompatibleRuntime
|
alias BDS.AI.OpenAICompatibleRuntime
|
||||||
alias BDS.AI.Runtime
|
alias BDS.AI.Runtime
|
||||||
alias BDS.Media.Media
|
alias BDS.Media.Media
|
||||||
|
alias BDS.MapUtils
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
|
|
||||||
@@ -45,10 +46,10 @@ defmodule BDS.AI.OneShot do
|
|||||||
def analyze_import_taxonomy(import_terms, existing_terms, opts \\ [])
|
def analyze_import_taxonomy(import_terms, existing_terms, opts \\ [])
|
||||||
when is_map(import_terms) and is_map(existing_terms) and is_list(opts) do
|
when is_map(import_terms) and is_map(existing_terms) and is_list(opts) do
|
||||||
payload = %{
|
payload = %{
|
||||||
import_categories: normalize_string_list(Map.get(import_terms, :categories) || Map.get(import_terms, "categories")),
|
import_categories: normalize_string_list(MapUtils.attr(import_terms, :categories)),
|
||||||
import_tags: normalize_string_list(Map.get(import_terms, :tags) || Map.get(import_terms, "tags")),
|
import_tags: normalize_string_list(MapUtils.attr(import_terms, :tags)),
|
||||||
existing_categories: normalize_string_list(Map.get(existing_terms, :categories) || Map.get(existing_terms, "categories")),
|
existing_categories: normalize_string_list(MapUtils.attr(existing_terms, :categories)),
|
||||||
existing_tags: normalize_string_list(Map.get(existing_terms, :tags) || Map.get(existing_terms, "tags"))
|
existing_tags: normalize_string_list(MapUtils.attr(existing_terms, :tags))
|
||||||
}
|
}
|
||||||
|
|
||||||
run_one_shot(
|
run_one_shot(
|
||||||
@@ -96,7 +97,8 @@ defmodule BDS.AI.OneShot do
|
|||||||
end
|
end
|
||||||
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 \\ [])
|
def translate_post(post_input, target_language, opts \\ [])
|
||||||
when is_binary(target_language) and is_list(opts) do
|
when is_binary(target_language) and is_list(opts) do
|
||||||
with {:ok, post} <- normalize_post_input(post_input) do
|
with {:ok, post} <- normalize_post_input(post_input) do
|
||||||
@@ -138,7 +140,8 @@ defmodule BDS.AI.OneShot do
|
|||||||
end
|
end
|
||||||
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 \\ [])
|
def translate_media(media_input, target_language, opts \\ [])
|
||||||
when is_binary(target_language) and is_list(opts) do
|
when is_binary(target_language) and is_list(opts) do
|
||||||
with {:ok, media} <- normalize_media_input(media_input) 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),
|
with {:ok, endpoint, model, mode} <- Runtime.resolve_target(operation, opts),
|
||||||
:ok <- Runtime.validate_target(operation, model, mode),
|
:ok <- Runtime.validate_target(operation, model, mode),
|
||||||
request <- build_one_shot_request(operation, payload, model),
|
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),
|
{:ok, json} <- extract_json_response(response),
|
||||||
usage <- Chat.normalize_usage(response.usage),
|
usage <- Chat.normalize_usage(response.usage),
|
||||||
{:ok, result} <- formatter.(json, usage) do
|
{:ok, result} <- formatter.(json, usage) do
|
||||||
@@ -252,7 +256,10 @@ defmodule BDS.AI.OneShot do
|
|||||||
|
|
||||||
defp one_shot_user_content(:analyze_image, media) 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}}
|
%{"type" => "image_url", "image_url" => %{"url" => media.image_url}}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -286,9 +293,9 @@ defmodule BDS.AI.OneShot do
|
|||||||
defp normalize_post_input(attrs) when is_map(attrs) do
|
defp normalize_post_input(attrs) when is_map(attrs) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
title: Map.get(attrs, :title) || Map.get(attrs, "title") || "",
|
title: MapUtils.attr(attrs, :title) || "",
|
||||||
excerpt: Map.get(attrs, :excerpt) || Map.get(attrs, "excerpt") || "",
|
excerpt: MapUtils.attr(attrs, :excerpt) || "",
|
||||||
content: Map.get(attrs, :content) || Map.get(attrs, "content") || ""
|
content: MapUtils.attr(attrs, :content) || ""
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -313,11 +320,11 @@ defmodule BDS.AI.OneShot do
|
|||||||
defp normalize_media_input(attrs) when is_map(attrs) do
|
defp normalize_media_input(attrs) when is_map(attrs) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
mime_type: Map.get(attrs, :mime_type) || Map.get(attrs, "mime_type"),
|
mime_type: MapUtils.attr(attrs, :mime_type),
|
||||||
title: Map.get(attrs, :title) || Map.get(attrs, "title") || "",
|
title: MapUtils.attr(attrs, :title) || "",
|
||||||
alt: Map.get(attrs, :alt) || Map.get(attrs, "alt") || "",
|
alt: MapUtils.attr(attrs, :alt) || "",
|
||||||
caption: Map.get(attrs, :caption) || Map.get(attrs, "caption") || "",
|
caption: MapUtils.attr(attrs, :caption) || "",
|
||||||
image_url: Map.get(attrs, :image_url) || Map.get(attrs, "image_url")
|
image_url: MapUtils.attr(attrs, :image_url)
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -336,7 +343,8 @@ defmodule BDS.AI.OneShot do
|
|||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
end
|
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)
|
import_lookup = canonical_term_lookup(import_terms)
|
||||||
existing_lookup = canonical_term_lookup(existing_terms)
|
existing_lookup = canonical_term_lookup(existing_terms)
|
||||||
|
|
||||||
|
|||||||
@@ -80,18 +80,26 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
attrs = %{group_id: group_id, group_name: "Search"}
|
attrs = %{group_id: group_id, group_name: "Search"}
|
||||||
|
|
||||||
{:ok, posts_task} =
|
{: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)
|
:ok = Search.reindex_posts(project.id, on_progress: report)
|
||||||
report.(1.0, "Post search text reindexed")
|
report.(1.0, "Post search text reindexed")
|
||||||
%{project_id: project.id, entity: "posts"}
|
%{project_id: project.id, entity: "posts"}
|
||||||
end, attrs)
|
end,
|
||||||
|
attrs
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, _media_task} =
|
{: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)
|
:ok = Search.reindex_media(project.id, on_progress: report)
|
||||||
report.(1.0, "Media search text reindexed")
|
report.(1.0, "Media search text reindexed")
|
||||||
%{project_id: project.id, entity: "media"}
|
%{project_id: project.id, entity: "media"}
|
||||||
end, attrs)
|
end,
|
||||||
|
attrs
|
||||||
|
)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
@@ -107,43 +115,86 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_embedding_index", project, _params) do
|
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)
|
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
|
||||||
report.(1.0, "Embedding index rebuilt")
|
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
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_posts_from_files", project, _params) do
|
defp dispatch("rebuild_posts_from_files", project, _params) do
|
||||||
queue_task(project, "rebuild_posts_from_files", "Rebuild Posts From Files", "Maintenance", fn report ->
|
queue_task(
|
||||||
{:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report)
|
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")
|
report.(1.0, "Post rebuild complete")
|
||||||
%{project_id: project.id, counts: %{posts: length(posts)}}
|
%{project_id: project.id, counts: %{posts: length(posts)}}
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_media_from_files", project, _params) do
|
defp dispatch("rebuild_media_from_files", project, _params) do
|
||||||
queue_task(project, "rebuild_media_from_files", "Rebuild Media From Files", "Maintenance", fn report ->
|
queue_task(
|
||||||
{:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
|
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")
|
report.(1.0, "Media rebuild complete")
|
||||||
%{project_id: project.id, counts: %{media: length(media)}}
|
%{project_id: project.id, counts: %{media: length(media)}}
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_scripts_from_files", project, _params) do
|
defp dispatch("rebuild_scripts_from_files", project, _params) do
|
||||||
queue_task(project, "rebuild_scripts_from_files", "Rebuild Scripts From Files", "Maintenance", fn report ->
|
queue_task(
|
||||||
{:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
|
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")
|
report.(1.0, "Script rebuild complete")
|
||||||
%{project_id: project.id, counts: %{scripts: length(scripts)}}
|
%{project_id: project.id, counts: %{scripts: length(scripts)}}
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_templates_from_files", project, _params) do
|
defp dispatch("rebuild_templates_from_files", project, _params) do
|
||||||
queue_task(project, "rebuild_templates_from_files", "Rebuild Templates From Files", "Maintenance", fn report ->
|
queue_task(
|
||||||
{:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
|
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")
|
report.(1.0, "Template rebuild complete")
|
||||||
%{project_id: project.id, counts: %{templates: length(templates)}}
|
%{project_id: project.id, counts: %{templates: length(templates)}}
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_post_links", project, _params) do
|
defp dispatch("rebuild_post_links", project, _params) do
|
||||||
@@ -155,11 +206,17 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("regenerate_missing_thumbnails", project, _params) do
|
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)
|
result = BDS.Media.regenerate_missing_thumbnails(project.id, on_progress: report)
|
||||||
report.(1.0, "Missing thumbnails regenerated")
|
report.(1.0, "Missing thumbnails regenerated")
|
||||||
Map.put(result, :project_id, project.id)
|
Map.put(result, :project_id, project.id)
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("rebuild_database", project, _params) do
|
defp dispatch("rebuild_database", project, _params) do
|
||||||
@@ -192,15 +249,24 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
|
|
||||||
defp dispatch("generate_sitemap", project, _params) do
|
defp dispatch("generate_sitemap", project, _params) do
|
||||||
queue_task(project, "generate_sitemap", "Generate Site", "Generation", fn report ->
|
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")
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("validate_site", project, _params) do
|
defp dispatch("validate_site", project, _params) do
|
||||||
queue_task(project, "validate_site", "Validate Site", "Validation", fn report ->
|
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)
|
site_validation_result(project.id, validation)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -214,13 +280,18 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("repair_metadata_diff", project, params) do
|
defp dispatch("repair_metadata_diff", project, params) do
|
||||||
items = normalize_metadata_diff_items(Map.get(params, "items", Map.get(params, :items, [])))
|
items = normalize_metadata_diff_items(BDS.MapUtils.attr(params, :items, []))
|
||||||
direction = Map.get(params, "direction", Map.get(params, :direction))
|
direction = BDS.MapUtils.attr(params, :direction)
|
||||||
|
|
||||||
if items == [] do
|
if items == [] do
|
||||||
{:error, %{action: "repair_metadata_diff", message: "No metadata diff items selected"}}
|
{:error, %{action: "repair_metadata_diff", message: "No metadata diff items selected"}}
|
||||||
else
|
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} =
|
{:ok, _repair} =
|
||||||
Maintenance.repair_metadata_diff(project.id, direction, items,
|
Maintenance.repair_metadata_diff(project.id, direction, items,
|
||||||
on_progress: scaled_progress_reporter(report, 0.0, 0.8)
|
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")
|
report.(1.0, "Metadata diff repair complete")
|
||||||
metadata_diff_result(project.id, metadata_diff)
|
metadata_diff_result(project.id, metadata_diff)
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("import_metadata_diff_orphans", project, params) do
|
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
|
if orphans == [] do
|
||||||
{:error, %{action: "import_metadata_diff_orphans", message: "No orphan files selected"}}
|
{:error, %{action: "import_metadata_diff_orphans", message: "No orphan files selected"}}
|
||||||
else
|
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} =
|
{:ok, _import} =
|
||||||
Maintenance.import_metadata_diff_orphans(project.id, orphans,
|
Maintenance.import_metadata_diff_orphans(project.id, orphans,
|
||||||
on_progress: scaled_progress_reporter(report, 0.0, 0.8)
|
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")
|
report.(1.0, "Metadata diff import complete")
|
||||||
metadata_diff_result(project.id, metadata_diff)
|
metadata_diff_result(project.id, metadata_diff)
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("validate_translations", project, _params) do
|
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)
|
{:ok, translation_report} = Posts.validate_translations(project.id, on_progress: report)
|
||||||
report.(1.0, "Translation validation complete")
|
report.(1.0, "Translation validation complete")
|
||||||
translation_validation_result(project.id, translation_report)
|
translation_validation_result(project.id, translation_report)
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch("find_duplicates", project, _params) do
|
defp dispatch("find_duplicates", project, _params) do
|
||||||
@@ -342,7 +426,9 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
%{
|
%{
|
||||||
name: "Rebuild Media From Files",
|
name: "Rebuild Media From Files",
|
||||||
work: fn report ->
|
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")
|
report.(1.0, "Media rebuild complete")
|
||||||
%{project_id: project.id, counts: %{media: length(media)}}
|
%{project_id: project.id, counts: %{media: length(media)}}
|
||||||
end
|
end
|
||||||
@@ -350,7 +436,9 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
%{
|
%{
|
||||||
name: "Rebuild Scripts From Files",
|
name: "Rebuild Scripts From Files",
|
||||||
work: fn report ->
|
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")
|
report.(1.0, "Script rebuild complete")
|
||||||
%{project_id: project.id, counts: %{scripts: length(scripts)}}
|
%{project_id: project.id, counts: %{scripts: length(scripts)}}
|
||||||
end
|
end
|
||||||
@@ -358,7 +446,9 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
%{
|
%{
|
||||||
name: "Rebuild Templates From Files",
|
name: "Rebuild Templates From Files",
|
||||||
work: fn report ->
|
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")
|
report.(1.0, "Template rebuild complete")
|
||||||
%{project_id: project.id, counts: %{templates: length(templates)}}
|
%{project_id: project.id, counts: %{templates: length(templates)}}
|
||||||
end
|
end
|
||||||
@@ -384,7 +474,12 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
work: fn report ->
|
work: fn report ->
|
||||||
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
|
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
|
||||||
report.(1.0, "Embedding index rebuilt")
|
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
|
end
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -531,7 +626,10 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
subtitle: "Database rows and translation files checked for invalid state",
|
subtitle: "Database rows and translation files checked for invalid state",
|
||||||
editorMeta: [
|
editorMeta: [
|
||||||
%{label: "Invalid DB", value: Integer.to_string(length(report.invalid_database_rows))},
|
%{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)
|
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
|
defp normalize_metadata_diff_items(items) when is_list(items) do
|
||||||
Enum.map(items, fn item ->
|
Enum.map(items, fn item ->
|
||||||
%{
|
%{
|
||||||
entity_type: Map.get(item, :entity_type) || Map.get(item, "entity_type"),
|
entity_type: BDS.MapUtils.attr(item, :entity_type),
|
||||||
entity_id: Map.get(item, :entity_id) || Map.get(item, "entity_id")
|
entity_id: BDS.MapUtils.attr(item, :entity_id)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -574,7 +672,7 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
|
|
||||||
defp normalize_metadata_diff_orphans(orphans) when is_list(orphans) do
|
defp normalize_metadata_diff_orphans(orphans) when is_list(orphans) do
|
||||||
Enum.map(orphans, fn orphan ->
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -593,7 +691,10 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
ssh_mode: Map.get(prefs, "ssh_mode")
|
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}
|
{:ok, credentials}
|
||||||
else
|
else
|
||||||
{:error, %{action: "upload_site", message: "Publishing preferences are incomplete"}}
|
{:error, %{action: "upload_site", message: "Publishing preferences are incomplete"}}
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
|
|||||||
@tool_args_max_length 30
|
@tool_args_max_length 30
|
||||||
|
|
||||||
def tool_call_name(tool_call) when is_map(tool_call) do
|
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
|
end
|
||||||
|
|
||||||
def tool_call_arguments(tool_call) when is_map(tool_call) do
|
def tool_call_arguments(tool_call) when is_map(tool_call) do
|
||||||
Map.get(tool_call, "arguments") || Map.get(tool_call, :arguments) ||
|
BDS.MapUtils.attr(tool_call, :arguments) || BDS.MapUtils.attr(tool_call, :args) || %{}
|
||||||
Map.get(tool_call, "args") || Map.get(tool_call, :args) || %{}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_tool_calls(tool_calls) when is_list(tool_calls) do
|
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)
|
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),
|
name: tool_call_name(tool_call),
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
args_preview: tool_arguments_preview(arguments),
|
args_preview: tool_arguments_preview(arguments),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.CliSync do
|
|||||||
import Phoenix.Component, only: [assign: 3]
|
import Phoenix.Component, only: [assign: 3]
|
||||||
|
|
||||||
alias BDS.{Media, Posts}
|
alias BDS.{Media, Posts}
|
||||||
|
alias BDS.MapUtils
|
||||||
alias BDS.Media.Media, as: MediaRecord
|
alias BDS.Media.Media, as: MediaRecord
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
alias BDS.UI.Workbench
|
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
|
Apply a CLI entity change payload to the shell socket. `reload_fun` is
|
||||||
called with `(socket, workbench)` to refresh derived data.
|
called with `(socket, workbench)` to refresh derived data.
|
||||||
"""
|
"""
|
||||||
@spec apply_entity_change(Phoenix.LiveView.Socket.t(), map(),
|
@spec apply_entity_change(Phoenix.LiveView.Socket.t(), map(), (Phoenix.LiveView.Socket.t(),
|
||||||
(Phoenix.LiveView.Socket.t(), map() -> Phoenix.LiveView.Socket.t())) ::
|
map() ->
|
||||||
|
Phoenix.LiveView.Socket.t())) ::
|
||||||
Phoenix.LiveView.Socket.t()
|
Phoenix.LiveView.Socket.t()
|
||||||
def apply_entity_change(socket, payload, reload_fun) do
|
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 =
|
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")
|
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
|
if is_binary(entity) and entity != "" and is_binary(entity_id) and entity_id != "" and
|
||||||
action in [:created, :updated, :deleted] do
|
action in [:created, :updated, :deleted] do
|
||||||
@@ -45,13 +47,28 @@ defmodule BDS.Desktop.ShellLive.CliSync do
|
|||||||
|> assign(:shell_overlay, nil)
|
|> assign(:shell_overlay, nil)
|
||||||
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, post_id}))
|
|> 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_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(
|
||||||
|> assign(:post_editor_tag_queries, Map.delete(socket.assigns.post_editor_tag_queries, post_id))
|
:post_editor_active_languages,
|
||||||
|> assign(:post_editor_category_queries, Map.delete(socket.assigns.post_editor_category_queries, post_id))
|
Map.delete(socket.assigns.post_editor_active_languages, post_id)
|
||||||
|> assign(:post_editor_quick_actions_open, Map.delete(socket.assigns.post_editor_quick_actions_open, 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_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_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}
|
{socket, workbench}
|
||||||
end
|
end
|
||||||
@@ -65,31 +82,58 @@ defmodule BDS.Desktop.ShellLive.CliSync do
|
|||||||
|> assign(:shell_overlay, nil)
|
|> assign(:shell_overlay, nil)
|
||||||
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, media_id}))
|
|> 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_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(
|
||||||
|> assign(:media_editor_post_pickers_open, Map.delete(socket.assigns.media_editor_post_pickers_open, media_id))
|
:media_editor_quick_actions_open,
|
||||||
|> assign(:media_editor_post_picker_queries, Map.delete(socket.assigns.media_editor_post_picker_queries, media_id))
|
Map.delete(socket.assigns.media_editor_quick_actions_open, 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_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}
|
{socket, workbench}
|
||||||
end
|
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 ->
|
maybe_put_tab_meta(socket, :post, post_id, fn ->
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
%Post{} = post -> %{title: post.title || post.slug || post.id, subtitle: Atom.to_string(post.status)}
|
%Post{} = post ->
|
||||||
_other -> nil
|
%{title: post.title || post.slug || post.id, subtitle: Atom.to_string(post.status)}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end)
|
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 ->
|
maybe_put_tab_meta(socket, :media, media_id, fn ->
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
%MediaRecord{} = media -> %{title: media.title || media.filename || media.id, subtitle: media.filename || media.mime_type || "media"}
|
%MediaRecord{} = media ->
|
||||||
_other -> nil
|
%{
|
||||||
|
title: media.title || media.filename || media.id,
|
||||||
|
subtitle: media.filename || media.mime_type || "media"
|
||||||
|
}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end)
|
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
|
if tab_present?(socket.assigns.workbench, key) or Map.has_key?(socket.assigns.tab_meta, key) do
|
||||||
case meta_fun.() do
|
case meta_fun.() do
|
||||||
%{} = fresh_meta ->
|
%{} = 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)
|
assign(socket, :tab_meta, updated_meta)
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
|
|||||||
@@ -7,11 +7,18 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|
|
||||||
alias BDS.{Embeddings, Generation, Git, Posts, Repo}
|
alias BDS.{Embeddings, Generation, Git, Posts, Repo}
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
|
alias BDS.MapUtils
|
||||||
alias BDS.Settings.Setting
|
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
|
def assign_socket(socket) do
|
||||||
assign(socket, :misc_editor, build(socket.assigns))
|
assign(socket, :misc_editor, build(socket.assigns))
|
||||||
@@ -19,7 +26,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|
|
||||||
def rerun(socket) do
|
def rerun(socket) do
|
||||||
case meta(socket.assigns) do
|
case meta(socket.assigns) do
|
||||||
%{action: action} when is_binary(action) -> {:command, action}
|
%{action: action} when is_binary(action) ->
|
||||||
|
{:command, action}
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
case misc_route_action(socket.assigns.current_tab.type) do
|
case misc_route_action(socket.assigns.current_tab.type) do
|
||||||
nil -> {:noop, socket}
|
nil -> {:noop, socket}
|
||||||
@@ -47,10 +56,16 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
{:ok, result} ->
|
{:ok, result} ->
|
||||||
{:rerun,
|
{:rerun,
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Site Validation"), translated("Validation changes applied"), inspect(result))}
|
|> append_output.(
|
||||||
|
translated("Site Validation"),
|
||||||
|
translated("Validation changes applied"),
|
||||||
|
inspect(result)
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
rescue
|
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
|
end
|
||||||
|
|
||||||
def toggle_duplicate(socket, pair_id, reload) do
|
def toggle_duplicate(socket, pair_id, reload) do
|
||||||
@@ -65,7 +80,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
socket
|
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)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -75,7 +93,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
socket
|
socket
|
||||||
|> update_payload(fn payload ->
|
|> update_payload(fn payload ->
|
||||||
update_in(payload[:pairs], fn pairs ->
|
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)
|
||||||
end)
|
end)
|
||||||
|> clear_selected_pair(pair_id(post_id_a, post_id_b))
|
|> 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
|
def dismiss_selected(socket, reload, append_output) do
|
||||||
tab_id = socket.assigns.current_tab.id
|
tab_id = socket.assigns.current_tab.id
|
||||||
|
|
||||||
selected =
|
selected =
|
||||||
socket.assigns.misc_editor_selected_pairs
|
socket.assigns.misc_editor_selected_pairs
|
||||||
|> Map.get(tab_id, MapSet.new())
|
|> 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)
|
Enum.reject(pairs || [], fn pair -> pair_identity(pair) in selected end)
|
||||||
end)
|
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"))
|
|> append_output.(translated("Find Duplicates"), translated("Selected pairs dismissed"))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
@@ -137,14 +161,20 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
rescue
|
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
|
end
|
||||||
|
|
||||||
def select_git_diff_file(socket, file_path) do
|
def select_git_diff_file(socket, file_path) do
|
||||||
assign(
|
assign(
|
||||||
socket,
|
socket,
|
||||||
:misc_editor_git_selected_files,
|
: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
|
end
|
||||||
|
|
||||||
@@ -183,7 +213,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
meta = meta(socket.assigns)
|
meta = meta(socket.assigns)
|
||||||
payload = Map.get(meta, :payload, %{})
|
payload = Map.get(meta, :payload, %{})
|
||||||
items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)
|
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)
|
tabs = metadata_diff_tabs(items, orphan_files)
|
||||||
active_tab = metadata_diff_active_tab(socket.assigns, tabs)
|
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 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(:site_validation), do: "site-validation-view"
|
||||||
def misc_class(:metadata_diff), do: "metadata-diff-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
|
defp build_metadata_diff(assigns, meta, payload) do
|
||||||
items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)
|
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)
|
tabs = metadata_diff_tabs(items, orphan_files)
|
||||||
active_tab = metadata_diff_active_tab(assigns, tabs)
|
active_tab = metadata_diff_active_tab(assigns, tabs)
|
||||||
active_field = metadata_diff_active_field(assigns)
|
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)
|
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")),
|
title: Map.get(meta, :title, translated("Metadata Diff")),
|
||||||
subtitle: Map.get(meta, :subtitle, ""),
|
subtitle: Map.get(meta, :subtitle, ""),
|
||||||
summary: Map.get(payload, :summary, %{}),
|
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_tab: current_tab.id,
|
||||||
active_field: active_field,
|
active_field: active_field,
|
||||||
repair_enabled: metadata_diff_repairable_tab?(current_tab.id),
|
repair_enabled: metadata_diff_repairable_tab?(current_tab.id),
|
||||||
@@ -300,7 +341,8 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp build_duplicates(assigns, meta, payload) do
|
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,
|
kind: :find_duplicates,
|
||||||
@@ -319,8 +361,15 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
{files, diff, error_message} =
|
{files, diff, error_message} =
|
||||||
case Git.status(project_id) do
|
case Git.status(project_id) do
|
||||||
{:ok, %{files: files}} ->
|
{:ok, %{files: files}} ->
|
||||||
file_paths = files |> Enum.map(&Map.get(&1, :path)) |> Enum.reject(&is_nil/1) |> Enum.uniq() |> Enum.sort()
|
file_paths =
|
||||||
selected_file_path = select_git_diff_path(assigns.current_tab.id, file_paths, selected_files)
|
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 =
|
diff =
|
||||||
case selected_file_path do
|
case selected_file_path do
|
||||||
@@ -329,8 +378,14 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|
|
||||||
file_path ->
|
file_path ->
|
||||||
case Git.get_diff_content(project_id, file_path) do
|
case Git.get_diff_content(project_id, file_path) do
|
||||||
{:ok, diff} -> diff
|
{:ok, diff} ->
|
||||||
{:error, reason} -> Map.merge(empty_git_diff(project_id), %{file_path: file_path, error: inspect(reason)})
|
diff
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Map.merge(empty_git_diff(project_id), %{
|
||||||
|
file_path: file_path,
|
||||||
|
error: inspect(reason)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -357,10 +412,17 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|
|
||||||
def translation_issue_label(issue) do
|
def translation_issue_label(issue) do
|
||||||
case issue_value(issue, :issue) do
|
case issue_value(issue, :issue) do
|
||||||
"same-language-as-canonical" -> translated("translationValidation.issue.sameLanguage")
|
"same-language-as-canonical" ->
|
||||||
"do-not-translate-has-translations" -> translated("translationValidation.issue.doNotTranslate")
|
translated("translationValidation.issue.sameLanguage")
|
||||||
"content-in-database" -> translated("translationValidation.issue.contentInDatabase")
|
|
||||||
_other -> translated("translationValidation.issue.missingSource")
|
"do-not-translate-has-translations" ->
|
||||||
|
translated("translationValidation.issue.doNotTranslate")
|
||||||
|
|
||||||
|
"content-in-database" ->
|
||||||
|
translated("translationValidation.issue.contentInDatabase")
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
translated("translationValidation.issue.missingSource")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -414,12 +476,17 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
defp clear_selected_pair(socket, pair_id) do
|
defp clear_selected_pair(socket, pair_id) do
|
||||||
tab_id = socket.assigns.current_tab.id
|
tab_id = socket.assigns.current_tab.id
|
||||||
current = Map.get(socket.assigns.misc_editor_selected_pairs, tab_id, MapSet.new())
|
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)
|
assign(socket, :misc_editor_selected_pairs, next_pairs)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp pair_id(post_id_a, post_id_b), do: Enum.sort([post_id_a, post_id_b]) |> Enum.join("::")
|
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
|
defp decode_pair_id(encoded) when is_binary(encoded) do
|
||||||
case String.split(encoded, "::", parts: 2) do
|
case String.split(encoded, "::", parts: 2) do
|
||||||
@@ -432,7 +499,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|
|
||||||
defp field_summaries(items) do
|
defp field_summaries(items) do
|
||||||
items
|
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.group_by(&diff_name/1)
|
||||||
|> Enum.map(fn {field, diffs} -> %{field_name: field, diff_count: length(diffs)} end)
|
|> Enum.map(fn {field, diffs} -> %{field_name: field, diff_count: length(diffs)} end)
|
||||||
|> Enum.sort_by(&{&1.diff_count * -1, &1.field_name})
|
|> Enum.sort_by(&{&1.diff_count * -1, &1.field_name})
|
||||||
@@ -465,7 +532,15 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp empty_metadata_diff_tab do
|
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
|
end
|
||||||
|
|
||||||
defp metadata_diff_active_tab(assigns, tabs) do
|
defp metadata_diff_active_tab(assigns, tabs) do
|
||||||
@@ -489,11 +564,12 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_metadata_diff_item(item) do
|
defp normalize_metadata_diff_item(item) do
|
||||||
entity_type = Map.get(item, :entity_type) || Map.get(item, "entity_type") || "post"
|
entity_type = MapUtils.attr(item, :entity_type) || "post"
|
||||||
entity_id = Map.get(item, :entity_id) || Map.get(item, "entity_id") || ""
|
entity_id = MapUtils.attr(item, :entity_id) || ""
|
||||||
|
|
||||||
differences =
|
differences =
|
||||||
item
|
item
|
||||||
|> Map.get(:differences, Map.get(item, "differences", []))
|
|> MapUtils.attr(:differences, [])
|
||||||
|> Enum.map(&normalize_metadata_diff_difference/1)
|
|> Enum.map(&normalize_metadata_diff_difference/1)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
@@ -510,30 +586,31 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
defp normalize_metadata_diff_difference(diff) do
|
defp normalize_metadata_diff_difference(diff) do
|
||||||
%{
|
%{
|
||||||
field: diff_name(diff),
|
field: diff_name(diff),
|
||||||
db_value: format_metadata_diff_value(Map.get(diff, :db_value) || Map.get(diff, "db_value")),
|
db_value: format_metadata_diff_value(MapUtils.attr(diff, :db_value)),
|
||||||
file_value: format_metadata_diff_value(Map.get(diff, :file_value) || Map.get(diff, "file_value"))
|
file_value: format_metadata_diff_value(MapUtils.attr(diff, :file_value))
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_metadata_diff_orphan(orphan) do
|
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") || ""
|
path = MapUtils.attr(orphan, :file_path) || MapUtils.attr(orphan, :path) || ""
|
||||||
entity_type = Map.get(orphan, :entity_type) || Map.get(orphan, "entity_type") || metadata_diff_orphan_entity_type(path)
|
entity_type = MapUtils.attr(orphan, :entity_type) || metadata_diff_orphan_entity_type(path)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
tab_id: metadata_diff_tab_id(entity_type),
|
tab_id: metadata_diff_tab_id(entity_type),
|
||||||
entity_type: entity_type,
|
entity_type: entity_type,
|
||||||
file_path: path,
|
file_path: path,
|
||||||
slug: Path.basename(path) |> String.trim(),
|
slug: Path.basename(path) |> String.trim(),
|
||||||
id: Map.get(orphan, :id) || Map.get(orphan, "id")
|
id: MapUtils.attr(orphan, :id)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp metadata_diff_item_label(item, entity_id) do
|
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
|
end
|
||||||
|
|
||||||
defp metadata_diff_item_meta_label(item, entity_id) do
|
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
|
end
|
||||||
|
|
||||||
defp metadata_diff_item_type_label("post"), do: translated("Post")
|
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("categories"), do: translated("Categories")
|
||||||
defp metadata_diff_item_type_label("category_meta"), 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("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"), do: "posts"
|
||||||
defp metadata_diff_tab_id("post_translation"), 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("templates"), do: translated("Templates")
|
||||||
defp metadata_diff_tab_label("project"), do: translated("Project")
|
defp metadata_diff_tab_label("project"), do: translated("Project")
|
||||||
defp metadata_diff_tab_label("embeddings"), do: translated("Embeddings")
|
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("posts"), do: 0
|
||||||
defp metadata_diff_tab_sort_key("media"), do: 1
|
defp metadata_diff_tab_sort_key("media"), do: 1
|
||||||
@@ -588,7 +669,8 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
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(:site_validation), do: "validate_site"
|
||||||
defp misc_route_action(:metadata_diff), do: "metadata_diff"
|
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 format_metadata_diff_value(value), do: to_string(value)
|
||||||
|
|
||||||
defp diff_name(diff) do
|
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
|
end
|
||||||
|
|
||||||
defp normalize_translation_validation_report(payload) when is_map(payload) do
|
defp normalize_translation_validation_report(payload) when is_map(payload) do
|
||||||
|
|||||||
@@ -261,11 +261,11 @@
|
|||||||
<%= for pair <- @misc_editor.pairs do %>
|
<%= for pair <- @misc_editor.pairs do %>
|
||||||
<article class="misc-list-item duplicate-pair-row">
|
<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>
|
<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>
|
<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>
|
<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(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>
|
<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={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="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>
|
</article>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ defmodule BDS.Maintenance.Repair do
|
|||||||
import BDS.Maintenance.Progress, only: [report_progress: 4]
|
import BDS.Maintenance.Progress, only: [report_progress: 4]
|
||||||
|
|
||||||
alias BDS.Embeddings
|
alias BDS.Embeddings
|
||||||
|
alias BDS.MapUtils
|
||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
|
|
||||||
def normalize_entity_type(:post), do: :post
|
def normalize_entity_type(:post), do: :post
|
||||||
@@ -33,31 +34,62 @@ defmodule BDS.Maintenance.Repair do
|
|||||||
def normalize_repair_direction(_direction), do: :unsupported
|
def normalize_repair_direction(_direction), do: :unsupported
|
||||||
|
|
||||||
def repair_metadata_diff_item(project_id, direction, item) do
|
def repair_metadata_diff_item(project_id, direction, item) do
|
||||||
entity_type = Map.get(item, :entity_type) || Map.get(item, "entity_type")
|
entity_type = MapUtils.attr(item, :entity_type)
|
||||||
entity_id = Map.get(item, :entity_id) || Map.get(item, "entity_id")
|
entity_id = MapUtils.attr(item, :entity_id)
|
||||||
|
|
||||||
case {normalize_repair_direction(direction), entity_type} do
|
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)
|
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)
|
Metadata.flush_project_metadata_to_filesystem(project_id)
|
||||||
|
|
||||||
{:file_to_db, "post"} -> BDS.Posts.sync_post_from_file(entity_id)
|
{:file_to_db, "post"} ->
|
||||||
{:db_to_file, "post"} -> BDS.Posts.rewrite_published_post(entity_id)
|
BDS.Posts.sync_post_from_file(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)
|
{:db_to_file, "post"} ->
|
||||||
{:file_to_db, "media"} -> BDS.Media.sync_media_from_sidecar(entity_id)
|
BDS.Posts.rewrite_published_post(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)
|
{:file_to_db, "post_translation"} ->
|
||||||
{:db_to_file, "media_translation"} -> BDS.Media.sync_media_translation_sidecar(entity_id)
|
BDS.Posts.sync_post_translation_from_file(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)
|
{:db_to_file, "post_translation"} ->
|
||||||
{:file_to_db, "template"} -> BDS.Templates.sync_template_from_file(entity_id)
|
BDS.Posts.rewrite_published_post_translation(entity_id)
|
||||||
{:db_to_file, "template"} -> BDS.Templates.sync_published_template_file(entity_id)
|
|
||||||
{:file_to_db, "embedding"} -> BDS.Embeddings.sync_post(entity_id)
|
{:file_to_db, "media"} ->
|
||||||
{:db_to_file, "embedding"} -> BDS.Embeddings.refresh_snapshot(project_id)
|
BDS.Media.sync_media_from_sidecar(entity_id)
|
||||||
_other -> {:error, :unsupported}
|
|
||||||
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -87,7 +119,8 @@ defmodule BDS.Maintenance.Repair do
|
|||||||
end
|
end
|
||||||
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
|
defp build_batch_repair_result(items, total, on_progress, repaired?) do
|
||||||
items
|
items
|
||||||
@@ -106,15 +139,15 @@ defmodule BDS.Maintenance.Repair do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp metadata_diff_item_entity_type(item) do
|
defp metadata_diff_item_entity_type(item) do
|
||||||
Map.get(item, :entity_type) || Map.get(item, "entity_type")
|
MapUtils.attr(item, :entity_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp metadata_diff_item_entity_id(item) do
|
defp metadata_diff_item_entity_id(item) do
|
||||||
Map.get(item, :entity_id) || Map.get(item, "entity_id")
|
MapUtils.attr(item, :entity_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_metadata_diff_orphan(project_id, orphan) do
|
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
|
cond do
|
||||||
is_nil(file_path) ->
|
is_nil(file_path) ->
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ defmodule BDS.MapUtils do
|
|||||||
end
|
end
|
||||||
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()
|
@spec maybe_put(map(), term(), term()) :: map()
|
||||||
def maybe_put(map, _key, nil), do: map
|
def maybe_put(map, _key, nil), do: map
|
||||||
def maybe_put(map, key, value), do: Map.put(map, key, value)
|
def maybe_put(map, key, value), do: Map.put(map, key, value)
|
||||||
|
|||||||
@@ -173,7 +173,14 @@ defmodule BDS.Metadata do
|
|||||||
|> Repo.update!()
|
|> Repo.update!()
|
||||||
|
|
||||||
persist_setting(project_id, "project", stringify_project_metadata(filesystem_state), now)
|
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(
|
persist_setting(
|
||||||
project_id,
|
project_id,
|
||||||
"category_meta",
|
"category_meta",
|
||||||
@@ -247,10 +254,15 @@ defmodule BDS.Metadata do
|
|||||||
read_json(project, "project.json") ||
|
read_json(project, "project.json") ||
|
||||||
stringify_project_metadata(default_project_metadata(project))
|
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 =
|
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"}
|
publishing_preferences = read_json(project, "publishing.json") || %{"ssh_mode" => "scp"}
|
||||||
|
|
||||||
@@ -305,14 +317,11 @@ defmodule BDS.Metadata do
|
|||||||
|
|
||||||
defp normalize_category_settings(settings) do
|
defp normalize_category_settings(settings) do
|
||||||
%{
|
%{
|
||||||
"render_in_lists" =>
|
"render_in_lists" => attr(settings, :render_in_lists, true),
|
||||||
Map.get(settings, :render_in_lists, Map.get(settings, "render_in_lists", true)),
|
"show_title" => attr(settings, :show_title, true),
|
||||||
"show_title" => Map.get(settings, :show_title, Map.get(settings, "show_title", true)),
|
"post_template_slug" => attr(settings, :post_template_slug),
|
||||||
"post_template_slug" =>
|
"list_template_slug" => attr(settings, :list_template_slug),
|
||||||
Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")),
|
"title" => attr(settings, :title)
|
||||||
"list_template_slug" =>
|
|
||||||
Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug")),
|
|
||||||
"title" => Map.get(settings, :title, Map.get(settings, "title"))
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -447,7 +456,9 @@ defmodule BDS.Metadata do
|
|||||||
|> Map.new()
|
|> Map.new()
|
||||||
end
|
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(categories) when is_list(categories), do: categories
|
||||||
defp normalized_categories(_payload), do: @default_categories
|
defp normalized_categories(_payload), do: @default_categories
|
||||||
|
|
||||||
@@ -459,13 +470,25 @@ defmodule BDS.Metadata do
|
|||||||
{category,
|
{category,
|
||||||
%{
|
%{
|
||||||
"render_in_lists" =>
|
"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" =>
|
"show_title" =>
|
||||||
Map.get(category_settings, "show_title", Map.get(category_settings, "showTitle", true)),
|
Map.get(category_settings, "show_title", Map.get(category_settings, "showTitle", true)),
|
||||||
"post_template_slug" =>
|
"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" =>
|
"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")
|
"title" => Map.get(category_settings, "title")
|
||||||
}
|
}
|
||||||
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
@@ -480,10 +503,12 @@ defmodule BDS.Metadata do
|
|||||||
"publicUrl" => Map.get(project_metadata, "public_url"),
|
"publicUrl" => Map.get(project_metadata, "public_url"),
|
||||||
"mainLanguage" => Map.get(project_metadata, "main_language"),
|
"mainLanguage" => Map.get(project_metadata, "main_language"),
|
||||||
"defaultAuthor" => Map.get(project_metadata, "default_author"),
|
"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"),
|
"blogmarkCategory" => Map.get(project_metadata, "blogmark_category"),
|
||||||
"picoTheme" => Map.get(project_metadata, "pico_theme"),
|
"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", [])
|
"blogLanguages" => Map.get(project_metadata, "blog_languages", [])
|
||||||
}
|
}
|
||||||
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
|> 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({:ok, result}), do: {:ok, result}
|
||||||
defp unwrap_transaction({:error, reason}), do: {:error, reason}
|
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
|
if previous_state.semantic_similarity_enabled != true and
|
||||||
project_metadata.semantic_similarity_enabled == true do
|
project_metadata.semantic_similarity_enabled == true do
|
||||||
{:ok, _indexed_post_ids} = Embeddings.index_unindexed(project_id)
|
{:ok, _indexed_post_ids} = Embeddings.index_unindexed(project_id)
|
||||||
@@ -596,7 +626,8 @@ defmodule BDS.Metadata do
|
|||||||
result
|
result
|
||||||
end
|
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
|
defp attr(attrs, key) do
|
||||||
cond do
|
cond do
|
||||||
@@ -605,4 +636,12 @@ defmodule BDS.Metadata do
|
|||||||
true -> nil
|
true -> nil
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
alias BDS.I18n
|
alias BDS.I18n
|
||||||
|
alias BDS.MapUtils
|
||||||
alias BDS.Persistence
|
alias BDS.Persistence
|
||||||
alias BDS.Rendering.LinksAndLanguages
|
alias BDS.Rendering.LinksAndLanguages
|
||||||
alias BDS.Rendering.Metadata, as: RenderMetadata
|
alias BDS.Rendering.Metadata, as: RenderMetadata
|
||||||
@@ -12,18 +13,19 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
metadata = RenderMetadata.project_metadata(project_id)
|
metadata = RenderMetadata.project_metadata(project_id)
|
||||||
template_context = TemplateSelection.template_render_context(project_id)
|
template_context = TemplateSelection.template_render_context(project_id)
|
||||||
|
|
||||||
language =
|
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
|
||||||
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
|
||||||
|
|
||||||
main_language = metadata.main_language || language
|
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)
|
canonical_media_paths = LinksAndLanguages.canonical_media_path_by_source_path(project_id)
|
||||||
|
|
||||||
posts =
|
posts =
|
||||||
normalize_list_posts(
|
normalize_list_posts(
|
||||||
Map.get(assigns, :posts, Map.get(assigns, "posts", [])),
|
MapUtils.attr(assigns, :posts, []),
|
||||||
canonical_post_paths,
|
canonical_post_paths,
|
||||||
canonical_media_paths,
|
canonical_media_paths,
|
||||||
language,
|
language,
|
||||||
@@ -31,7 +33,7 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
)
|
)
|
||||||
|
|
||||||
pagination =
|
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)
|
day_blocks = build_day_blocks(posts)
|
||||||
min_date = min_date(posts)
|
min_date = min_date(posts)
|
||||||
@@ -44,15 +46,23 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:language_prefix,
|
: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,
|
posts: posts,
|
||||||
pico_stylesheet_href:
|
pico_stylesheet_href:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:pico_stylesheet_href,
|
: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:
|
html_theme_attribute:
|
||||||
Map.get(
|
Map.get(
|
||||||
@@ -66,7 +76,8 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
calendar_initial_year: calendar_initial_year_from_posts(posts),
|
calendar_initial_year: calendar_initial_year_from_posts(posts),
|
||||||
calendar_initial_month: calendar_initial_month_from_posts(posts),
|
calendar_initial_month: calendar_initial_month_from_posts(posts),
|
||||||
archive_context: normalized_archive_context,
|
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,
|
min_date: min_date,
|
||||||
max_date: max_date,
|
max_date: max_date,
|
||||||
is_list_page: true,
|
is_list_page: true,
|
||||||
@@ -91,25 +102,32 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
def not_found_assigns(project_id, assigns) do
|
def not_found_assigns(project_id, assigns) do
|
||||||
metadata = RenderMetadata.project_metadata(project_id)
|
metadata = RenderMetadata.project_metadata(project_id)
|
||||||
|
|
||||||
language =
|
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
|
||||||
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
|
||||||
|
|
||||||
main_language = metadata.main_language || language
|
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: language,
|
||||||
language_prefix:
|
language_prefix:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:language_prefix,
|
: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:
|
pico_stylesheet_href:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:pico_stylesheet_href,
|
: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:
|
html_theme_attribute:
|
||||||
Map.get(
|
Map.get(
|
||||||
@@ -143,20 +161,27 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
}
|
}
|
||||||
end
|
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 ->
|
Enum.map(posts, fn post ->
|
||||||
post_record = PostRendering.load_post_record(post)
|
post_record = PostRendering.load_post_record(post)
|
||||||
|
|
||||||
raw_content =
|
raw_content =
|
||||||
Map.get(
|
Map.get(
|
||||||
post,
|
post,
|
||||||
:content,
|
: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")),
|
id: MapUtils.attr(post, :id),
|
||||||
slug: Map.get(post, :slug, Map.get(post, "slug")),
|
slug: MapUtils.attr(post, :slug),
|
||||||
title: Map.get(post, :title, Map.get(post, "title")),
|
title: MapUtils.attr(post, :title),
|
||||||
content:
|
content:
|
||||||
PostRendering.render_post_content(
|
PostRendering.render_post_content(
|
||||||
raw_content,
|
raw_content,
|
||||||
@@ -166,29 +191,30 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
template_context
|
template_context
|
||||||
),
|
),
|
||||||
raw_content: raw_content,
|
raw_content: raw_content,
|
||||||
excerpt:
|
excerpt: MapUtils.attr(post, :excerpt, Map.get(post_record || %{}, :excerpt)),
|
||||||
Map.get(post, :excerpt, Map.get(post, "excerpt", Map.get(post_record || %{}, :excerpt))),
|
author: MapUtils.attr(post, :author, Map.get(post_record || %{}, :author)),
|
||||||
author: Map.get(post, :author, Map.get(post, "author", Map.get(post_record || %{}, :author))),
|
|
||||||
language:
|
language:
|
||||||
Map.get(
|
Map.get(
|
||||||
post,
|
post,
|
||||||
:language,
|
:language,
|
||||||
Map.get(post, "language", Map.get(post_record || %{}, :language))
|
Map.get(post_record || %{}, :language)
|
||||||
),
|
),
|
||||||
published_at:
|
published_at:
|
||||||
Map.get(post, :published_at, Map.get(post, "published_at", Map.get(post_record || %{}, :published_at))),
|
MapUtils.attr(post, :published_at, Map.get(post_record || %{}, :published_at)),
|
||||||
created_at:
|
created_at: MapUtils.attr(post, :created_at, Map.get(post_record || %{}, :created_at)),
|
||||||
Map.get(post, :created_at, Map.get(post, "created_at", Map.get(post_record || %{}, :created_at))),
|
updated_at: MapUtils.attr(post, :updated_at, Map.get(post_record || %{}, :updated_at)),
|
||||||
updated_at:
|
tags: MapUtils.attr(post, :tags, Map.get(post_record || %{}, :tags, [])) || [],
|
||||||
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, []))) || [],
|
|
||||||
categories:
|
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:
|
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:
|
do_not_translate:
|
||||||
Map.get(post, :do_not_translate, Map.get(post, "do_not_translate", Map.get(post_record || %{}, :do_not_translate, false))),
|
MapUtils.attr(
|
||||||
href: Map.get(post, :href, Map.get(post, "href")),
|
post,
|
||||||
|
:do_not_translate,
|
||||||
|
Map.get(post_record || %{}, :do_not_translate, false)
|
||||||
|
),
|
||||||
|
href: MapUtils.attr(post, :href),
|
||||||
show_title: true,
|
show_title: true,
|
||||||
linked_media: [],
|
linked_media: [],
|
||||||
outgoing_links: [],
|
outgoing_links: [],
|
||||||
@@ -214,24 +240,20 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
|
|
||||||
defp normalize_pagination(%{} = pagination, posts) do
|
defp normalize_pagination(%{} = pagination, posts) do
|
||||||
total_items =
|
total_items =
|
||||||
Map.get(pagination, :total_items, Map.get(pagination, "total_items", length(posts)))
|
MapUtils.attr(pagination, :total_items, length(posts))
|
||||||
|
|
||||||
items_per_page =
|
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)),
|
current_page: MapUtils.attr(pagination, :current_page, 1),
|
||||||
total_pages: Map.get(pagination, :total_pages, Map.get(pagination, "total_pages", 1)),
|
total_pages: MapUtils.attr(pagination, :total_pages, 1),
|
||||||
total_items: total_items,
|
total_items: total_items,
|
||||||
items_per_page: items_per_page,
|
items_per_page: items_per_page,
|
||||||
has_prev_page:
|
has_prev_page: MapUtils.attr(pagination, :has_prev_page, false),
|
||||||
Map.get(pagination, :has_prev_page, Map.get(pagination, "has_prev_page", false)),
|
prev_page_href: MapUtils.attr(pagination, :prev_page_href, ""),
|
||||||
prev_page_href:
|
has_next_page: MapUtils.attr(pagination, :has_next_page, false),
|
||||||
Map.get(pagination, :prev_page_href, Map.get(pagination, "prev_page_href", "")),
|
next_page_href: MapUtils.attr(pagination, :next_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", ""))
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -239,11 +261,11 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
|
|
||||||
defp normalize_archive_context(%{} = archive_context) do
|
defp normalize_archive_context(%{} = archive_context) do
|
||||||
%{
|
%{
|
||||||
kind: Map.get(archive_context, :kind, Map.get(archive_context, "kind")),
|
kind: MapUtils.attr(archive_context, :kind),
|
||||||
name: Map.get(archive_context, :name, Map.get(archive_context, "name")),
|
name: MapUtils.attr(archive_context, :name),
|
||||||
month: Map.get(archive_context, :month, Map.get(archive_context, "month")),
|
month: MapUtils.attr(archive_context, :month),
|
||||||
year: Map.get(archive_context, :year, Map.get(archive_context, "year")),
|
year: MapUtils.attr(archive_context, :year),
|
||||||
day: Map.get(archive_context, :day, Map.get(archive_context, "day"))
|
day: MapUtils.attr(archive_context, :day)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -251,7 +273,12 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
grouped_blocks =
|
grouped_blocks =
|
||||||
posts
|
posts
|
||||||
|> Enum.filter(&is_integer(Map.get(&1, :created_at)))
|
|> 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)
|
|> Enum.sort_by(fn {label, _posts} -> label end)
|
||||||
|
|
||||||
grouped_blocks
|
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?(%{kind: "date"}, _day_blocks), do: true
|
||||||
defp show_archive_range_heading?(_archive_context, _day_blocks), do: false
|
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_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
|
defp calendar_initial_month_from_posts([]), do: nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
alias BDS.Rendering.LinksAndLanguages
|
alias BDS.Rendering.LinksAndLanguages
|
||||||
alias BDS.Rendering.Metadata, as: RenderMetadata
|
alias BDS.Rendering.Metadata, as: RenderMetadata
|
||||||
alias BDS.Rendering.TemplateSelection
|
alias BDS.Rendering.TemplateSelection
|
||||||
|
alias BDS.MapUtils
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
alias BDS.Posts.Translation
|
alias BDS.Posts.Translation
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
@@ -13,8 +14,7 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
metadata = RenderMetadata.project_metadata(project_id)
|
metadata = RenderMetadata.project_metadata(project_id)
|
||||||
template_context = TemplateSelection.template_render_context(project_id)
|
template_context = TemplateSelection.template_render_context(project_id)
|
||||||
|
|
||||||
language =
|
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
|
||||||
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
|
||||||
|
|
||||||
main_language = metadata.main_language || language
|
main_language = metadata.main_language || language
|
||||||
post_record = load_post_record(assigns)
|
post_record = load_post_record(assigns)
|
||||||
@@ -22,12 +22,27 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
post_id = canonical_post_id(post_record, assigns)
|
post_id = canonical_post_id(post_record, assigns)
|
||||||
post_categories = Map.get(post_record || %{}, :categories, []) || []
|
post_categories = Map.get(post_record || %{}, :categories, []) || []
|
||||||
post_tags = Map.get(post_record || %{}, :tags, []) || []
|
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)
|
canonical_media_paths = LinksAndLanguages.canonical_media_path_by_source_path(project_id)
|
||||||
raw_content = Map.get(assigns, :content, Map.get(assigns, "content"))
|
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)
|
rendered_content =
|
||||||
outgoing_links = LinksAndLanguages.link_contexts(project_id, post_id, :outgoing, main_language)
|
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 =
|
post_assigns =
|
||||||
assigns
|
assigns
|
||||||
@@ -40,19 +55,27 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:language_prefix,
|
: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:
|
page_title:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:page_title,
|
:page_title,
|
||||||
Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))
|
MapUtils.attr(assigns, :title)
|
||||||
),
|
),
|
||||||
pico_stylesheet_href:
|
pico_stylesheet_href:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:pico_stylesheet_href,
|
: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:
|
html_theme_attribute:
|
||||||
Map.get(
|
Map.get(
|
||||||
@@ -77,7 +100,7 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_post_record(assigns) do
|
def load_post_record(assigns) do
|
||||||
case Map.get(assigns, :id, Map.get(assigns, "id")) do
|
case MapUtils.attr(assigns, :id) do
|
||||||
nil -> nil
|
nil -> nil
|
||||||
post_id -> Repo.get(Post, post_id) || Repo.get(Translation, post_id)
|
post_id -> Repo.get(Post, post_id) || Repo.get(Translation, post_id)
|
||||||
end
|
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(%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{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
|
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
|
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))
|
incoming_links =
|
||||||
outgoing_links = LinksAndLanguages.link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :outgoing, Map.get(post_record || %{}, :language))
|
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
|
else
|
||||||
%{}
|
%{}
|
||||||
@@ -124,23 +163,23 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
|
|
||||||
defp build_post_context(assigns, post_record, incoming_links, outgoing_links) do
|
defp build_post_context(assigns, post_record, incoming_links, outgoing_links) do
|
||||||
%{
|
%{
|
||||||
id: Map.get(assigns, :id, Map.get(assigns, "id")),
|
id: MapUtils.attr(assigns, :id),
|
||||||
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
|
slug: MapUtils.attr(assigns, :slug),
|
||||||
title: Map.get(assigns, :title, Map.get(assigns, "title")),
|
title: MapUtils.attr(assigns, :title),
|
||||||
content: Map.get(assigns, :content, Map.get(assigns, "content")),
|
content: MapUtils.attr(assigns, :content),
|
||||||
raw_content: Map.get(assigns, :raw_content, Map.get(assigns, "raw_content")),
|
raw_content: MapUtils.attr(assigns, :raw_content),
|
||||||
excerpt:
|
excerpt:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:excerpt,
|
:excerpt,
|
||||||
Map.get(assigns, "excerpt", Map.get(post_record || %{}, :excerpt))
|
Map.get(post_record || %{}, :excerpt)
|
||||||
),
|
),
|
||||||
author: Map.get(post_record || %{}, :author),
|
author: Map.get(post_record || %{}, :author),
|
||||||
language:
|
language:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:language,
|
:language,
|
||||||
Map.get(assigns, "language", Map.get(post_record || %{}, :language))
|
Map.get(post_record || %{}, :language)
|
||||||
),
|
),
|
||||||
show_title: true,
|
show_title: true,
|
||||||
published_at: Map.get(post_record || %{}, :published_at),
|
published_at: Map.get(post_record || %{}, :published_at),
|
||||||
@@ -152,7 +191,7 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
Map.get(
|
Map.get(
|
||||||
post_record || %{},
|
post_record || %{},
|
||||||
:template_slug,
|
: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),
|
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
|
||||||
linked_media: [],
|
linked_media: [],
|
||||||
@@ -161,7 +200,19 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_post_content(content, canonical_post_paths, canonical_media_paths, language, template_context) do
|
def render_post_content(
|
||||||
Filters.render_markdown(content, canonical_post_paths, canonical_media_paths, language, template_context)
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ defmodule BDS.UI.Commands do
|
|||||||
]
|
]
|
||||||
|
|
||||||
def handle_shortcut(state, shortcut) when is_map(shortcut) do
|
def handle_shortcut(state, shortcut) when is_map(shortcut) do
|
||||||
key = shortcut |> Map.get(:key, Map.get(shortcut, "key", "")) |> String.downcase()
|
key = shortcut |> BDS.MapUtils.attr(:key, "") |> String.downcase()
|
||||||
primary =
|
|
||||||
Map.get(shortcut, :meta, Map.get(shortcut, "meta", false)) or
|
|
||||||
Map.get(shortcut, :ctrl, Map.get(shortcut, "ctrl", false))
|
|
||||||
|
|
||||||
shift = Map.get(shortcut, :shift, Map.get(shortcut, "shift", false))
|
primary =
|
||||||
alt = Map.get(shortcut, :alt, Map.get(shortcut, "alt", false))
|
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
|
case Enum.find(@menu_shortcuts, &shortcut_match?(&1, key, primary, shift, alt)) do
|
||||||
%{id: command_id} -> MenuBar.execute(state, command_id)
|
%{id: command_id} -> MenuBar.execute(state, command_id)
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ defmodule BDS.UI.Sidebar do
|
|||||||
"templates" => view(project_id, "templates"),
|
"templates" => view(project_id, "templates"),
|
||||||
"tags" => view(project_id, "tags"),
|
"tags" => view(project_id, "tags"),
|
||||||
"chat" => view(project_id, "chat"),
|
"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(),
|
"git" => git_view(),
|
||||||
"settings" => settings_nav_view()
|
"settings" => settings_nav_view()
|
||||||
}
|
}
|
||||||
@@ -41,17 +47,43 @@ defmodule BDS.UI.Sidebar do
|
|||||||
normalized_view = normalize_view_id(view_id)
|
normalized_view = normalize_view_id(view_id)
|
||||||
|
|
||||||
case normalized_view do
|
case normalized_view do
|
||||||
"posts" -> posts_view(project_id, params, false)
|
"posts" ->
|
||||||
"pages" -> posts_view(project_id, params, true)
|
posts_view(project_id, params, false)
|
||||||
"media" -> media_view(project_id, params)
|
|
||||||
"scripts" -> entity_list_view("Scripts", "Automation helpers", "scripts", list_scripts(project_id))
|
"pages" ->
|
||||||
"templates" -> entity_list_view("Templates", "Site rendering", "templates", list_templates(project_id))
|
posts_view(project_id, params, true)
|
||||||
"tags" -> tags_nav_view(list_tags(project_id))
|
|
||||||
"chat" -> entity_list_view("Chat", "AI conversations", "chat", list_conversations())
|
"media" ->
|
||||||
"import" -> entity_list_view("Import", "Import definitions", "import", list_import_definitions(project_id))
|
media_view(project_id, params)
|
||||||
"git" -> git_view()
|
|
||||||
"settings" -> settings_nav_view()
|
"scripts" ->
|
||||||
_other -> empty_view(normalized_view)
|
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
|
||||||
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("pages"), do: posts_view_data([], [], %{}, true, empty_filter_params(), %{})
|
||||||
defp empty_view("media"), do: media_view_data([], [], 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("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("tags"), do: tags_nav_view([])
|
||||||
defp empty_view("chat"), do: entity_list_view("Chat", "AI conversations", "chat", [])
|
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("import"), do: entity_list_view("Import", "Import definitions", "import", [])
|
||||||
defp empty_view("git"), do: git_view()
|
defp empty_view("git"), do: git_view()
|
||||||
defp empty_view("settings"), do: settings_nav_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
|
defp posts_view(project_id, params, pages?) do
|
||||||
posts = list_posts(project_id)
|
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)
|
posts_view_data(base_posts, filtered_posts, translation_counts, pages?, filters, tag_colors)
|
||||||
end
|
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)
|
limited_posts = Enum.take(filtered_posts, filters.display_limit)
|
||||||
grouped_posts = group_posts(limited_posts)
|
grouped_posts = group_posts(limited_posts)
|
||||||
available_tags = available_tags(base_posts, & &1.tags)
|
available_tags = available_tags(base_posts, & &1.tags)
|
||||||
@@ -101,12 +145,14 @@ defmodule BDS.UI.Sidebar do
|
|||||||
|
|
||||||
%{
|
%{
|
||||||
title: if(pages?, do: "Pages", else: "Posts"),
|
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",
|
layout: "post_list",
|
||||||
empty_message: if(pages?, do: "sidebar.noPagesYet", else: "sidebar.noPostsYet"),
|
empty_message: if(pages?, do: "sidebar.noPagesYet", else: "sidebar.noPostsYet"),
|
||||||
filters: %{
|
filters: %{
|
||||||
enabled: true,
|
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",
|
toggle_filters_label: "sidebar.toggleFilters",
|
||||||
archive_label: "render.archive",
|
archive_label: "render.archive",
|
||||||
tags_label: "sidebar.tags",
|
tags_label: "sidebar.tags",
|
||||||
@@ -137,8 +183,20 @@ defmodule BDS.UI.Sidebar do
|
|||||||
},
|
},
|
||||||
sections: [
|
sections: [
|
||||||
build_post_section("Drafts", :draft, grouped_posts.draft, translation_counts, false),
|
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(
|
||||||
build_post_section("Archived", :archived, grouped_posts.archived, translation_counts, false)
|
"Published",
|
||||||
|
:published,
|
||||||
|
grouped_posts.published,
|
||||||
|
translation_counts,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
build_post_section(
|
||||||
|
"Archived",
|
||||||
|
:archived,
|
||||||
|
grouped_posts.archived,
|
||||||
|
translation_counts,
|
||||||
|
false
|
||||||
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -246,7 +304,13 @@ defmodule BDS.UI.Sidebar do
|
|||||||
layout: "entity_list",
|
layout: "entity_list",
|
||||||
empty_message: "No items",
|
empty_message: "No items",
|
||||||
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
|
end
|
||||||
@@ -285,7 +349,8 @@ defmodule BDS.UI.Sidebar do
|
|||||||
tags: post.tags || [],
|
tags: post.tags || [],
|
||||||
status: Atom.to_string(post.status),
|
status: Atom.to_string(post.status),
|
||||||
language_count: 1 + Map.get(translation_counts, post.id, 0),
|
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",
|
route: "post",
|
||||||
search_blob: post_search_blob(post)
|
search_blob: post_search_blob(post)
|
||||||
}
|
}
|
||||||
@@ -377,7 +442,11 @@ defmodule BDS.UI.Sidebar do
|
|||||||
Repo.all(
|
Repo.all(
|
||||||
from conversation in ChatConversation,
|
from conversation in ChatConversation,
|
||||||
order_by: [desc: conversation.updated_at, desc: conversation.created_at],
|
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
|
end
|
||||||
|
|
||||||
@@ -402,15 +471,15 @@ defmodule BDS.UI.Sidebar do
|
|||||||
|
|
||||||
defp normalize_filter_params(params) when is_map(params) do
|
defp normalize_filter_params(params) when is_map(params) do
|
||||||
%{
|
%{
|
||||||
search: normalize_string(Map.get(params, "search") || Map.get(params, :search)),
|
search: normalize_string(BDS.MapUtils.attr(params, :search)),
|
||||||
year: normalize_integer(Map.get(params, "year") || Map.get(params, :year)),
|
year: normalize_integer(BDS.MapUtils.attr(params, :year)),
|
||||||
month: normalize_integer(Map.get(params, "month") || Map.get(params, :month)),
|
month: normalize_integer(BDS.MapUtils.attr(params, :month)),
|
||||||
tags: normalize_string_list(Map.get(params, "tags") || Map.get(params, :tags)),
|
tags: normalize_string_list(BDS.MapUtils.attr(params, :tags)),
|
||||||
categories: normalize_string_list(Map.get(params, "categories") || Map.get(params, :categories)),
|
categories: normalize_string_list(BDS.MapUtils.attr(params, :categories)),
|
||||||
display_limit:
|
display_limit:
|
||||||
max(
|
max(
|
||||||
@default_page_size,
|
@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
|
end
|
||||||
@@ -418,11 +487,19 @@ defmodule BDS.UI.Sidebar do
|
|||||||
defp normalize_filter_params(_params), do: empty_filter_params()
|
defp normalize_filter_params(_params), do: empty_filter_params()
|
||||||
|
|
||||||
defp empty_filter_params do
|
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
|
end
|
||||||
|
|
||||||
defp filter_active?(filters) do
|
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
|
end
|
||||||
|
|
||||||
defp apply_post_filters(posts, filters) do
|
defp apply_post_filters(posts, filters) do
|
||||||
@@ -497,7 +574,9 @@ defmodule BDS.UI.Sidebar do
|
|||||||
posts
|
posts
|
||||||
|> Enum.flat_map(&filtered_categories(&1.categories || []))
|
|> Enum.flat_map(&filtered_categories(&1.categories || []))
|
||||||
|> then(fn 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)
|
end)
|
||||||
|> Enum.map(&to_string/1)
|
|> Enum.map(&to_string/1)
|
||||||
|> Enum.reject(&(&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_filter_timestamp(post), do: post.published_at || post.updated_at
|
||||||
|
|
||||||
defp post_search_blob(post) do
|
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.reject(&is_nil/1)
|
||||||
|> Enum.join(" ")
|
|> Enum.join(" ")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ defmodule BDS.MapUtilsTest do
|
|||||||
assert MapUtils.attr(%{"title" => "fallback", title: nil}, :title) == nil
|
assert MapUtils.attr(%{"title" => "fallback", title: nil}, :title) == nil
|
||||||
assert MapUtils.attr(%{}, :title) == nil
|
assert MapUtils.attr(%{}, :title) == nil
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "maybe_put/3" do
|
describe "maybe_put/3" do
|
||||||
@@ -28,4 +34,47 @@ defmodule BDS.MapUtilsTest do
|
|||||||
assert MapUtils.blank_to_nil(42) == 42
|
assert MapUtils.blank_to_nil(42) == 42
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user