chore: added more @spec
This commit is contained in:
@@ -107,7 +107,7 @@ _None._ All modules previously on the queue have been split; refresh the queue i
|
|||||||
|
|
||||||
## 10. Missing `@spec`
|
## 10. Missing `@spec`
|
||||||
|
|
||||||
**Status:** ✅ done for core contexts (2026-04-30). Open for LiveView editor modules and the smaller contexts (`Tags`, `Templates`, `Scripts`, `PostLinks`).
|
**Status:** ✅ done (2026-05-01). Core contexts, LiveView editor modules, and the smaller contexts (`Tags`, `Templates`, `Scripts`, `PostLinks`) now have public `@spec` coverage. `BDS.SpecCoverageTest` scans the Section 10 module set and fails when a public function/arity lacks a matching spec.
|
||||||
|
|
||||||
**Convention reminder:** Ecto schemas need explicit `@type t`; use `term()` for associations; use `@typedoc` + named types for repeated map shapes; for attrs maps use `%{optional(atom()) => term(), optional(String.t()) => term()}`.
|
**Convention reminder:** Ecto schemas need explicit `@type t`; use `term()` for associations; use `@typedoc` + named types for repeated map shapes; for attrs maps use `%{optional(atom()) => term(), optional(String.t()) => term()}`.
|
||||||
|
|
||||||
@@ -155,6 +155,8 @@ Most tests share the SQLite repo and named GenServers (`BDS.Tasks`, `BDS.Search`
|
|||||||
|
|
||||||
### 2026-05-01
|
### 2026-05-01
|
||||||
|
|
||||||
|
- **Missing `@spec`**: closed Section 10 by adding public specs to the remaining smaller contexts (`BDS.Tags`, `BDS.Templates`, `BDS.Scripts`, `BDS.PostLinks`) and LiveView editor modules under `BDS.Desktop.ShellLive.*Editor`. Added `BDS.Tags.Tag.t/0` to match the existing schema typing convention and introduced `BDS.SpecCoverageTest`, which scans the Section 10 module set for public function/arity entries without matching specs.
|
||||||
|
|
||||||
- **`BDS.Tasks` memory growth**: added configurable TTL eviction for terminal tasks in the `BDS.Tasks` GenServer (`:finished_task_ttl_ms`, default 1 h). Finished tasks are pruned by delayed GenServer cleanup and on task read calls; active tasks are preserved. Added regression coverage that completes a task with a tiny TTL and verifies it disappears without `clear_finished/0` while a running task remains. Section 13 is closed.
|
- **`BDS.Tasks` memory growth**: added configurable TTL eviction for terminal tasks in the `BDS.Tasks` GenServer (`:finished_task_ttl_ms`, default 1 h). Finished tasks are pruned by delayed GenServer cleanup and on task read calls; active tasks are preserved. Added regression coverage that completes a task with a tiny TTL and verifies it disappears without `clear_finished/0` while a running task remains. Section 13 is closed.
|
||||||
|
|
||||||
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ defmodule BDS.AI.CatalogProvider do
|
|||||||
|
|
||||||
def changeset(provider, attrs) do
|
def changeset(provider, attrs) do
|
||||||
provider
|
provider
|
||||||
|> cast(attrs, [:id, :name, :env_keys, :package_ref, :api_url, :doc_url, :updated_at], empty_values: [nil])
|
|> cast(attrs, [:id, :name, :env_keys, :package_ref, :api_url, :doc_url, :updated_at],
|
||||||
|
empty_values: [nil]
|
||||||
|
)
|
||||||
|> validate_required([:id, :name, :updated_at])
|
|> validate_required([:id, :name, :updated_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ defmodule BDS.AI.ChatConversation do
|
|||||||
|
|
||||||
def changeset(conversation, attrs) do
|
def changeset(conversation, attrs) do
|
||||||
conversation
|
conversation
|
||||||
|> cast(attrs, [:id, :title, :model, :copilot_session_id, :created_at, :updated_at], empty_values: [nil])
|
|> cast(attrs, [:id, :title, :model, :copilot_session_id, :created_at, :updated_at],
|
||||||
|
empty_values: [nil]
|
||||||
|
)
|
||||||
|> validate_required([:id, :title, :created_at, :updated_at])
|
|> validate_required([:id, :title, :created_at, :updated_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,18 +23,20 @@ defmodule BDS.AI.ChatMessage do
|
|||||||
|
|
||||||
def changeset(message, attrs) do
|
def changeset(message, attrs) do
|
||||||
message
|
message
|
||||||
|> cast(attrs, [
|
|> cast(
|
||||||
:conversation_id,
|
attrs,
|
||||||
:role,
|
[
|
||||||
:content,
|
:conversation_id,
|
||||||
:tool_call_id,
|
:role,
|
||||||
:tool_calls,
|
:content,
|
||||||
:token_usage_input,
|
:tool_call_id,
|
||||||
:token_usage_output,
|
:tool_calls,
|
||||||
:cache_read_tokens,
|
:token_usage_input,
|
||||||
:cache_write_tokens,
|
:token_usage_output,
|
||||||
:created_at
|
:cache_read_tokens,
|
||||||
], empty_values: [nil])
|
:cache_write_tokens,
|
||||||
|
:created_at
|
||||||
|
], empty_values: [nil])
|
||||||
|> validate_required([:conversation_id, :role, :created_at])
|
|> validate_required([:conversation_id, :role, :created_at])
|
||||||
|> assoc_constraint(:conversation)
|
|> assoc_constraint(:conversation)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ defmodule BDS.AI.ChatTools do
|
|||||||
project_id = project_id || active_project_id()
|
project_id = project_id || active_project_id()
|
||||||
|
|
||||||
%{
|
%{
|
||||||
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: Chat.count_distinct_string_list(Post, :tags, project_id),
|
tag_count: Chat.count_distinct_string_list(Post, :tags, project_id),
|
||||||
category_count: Chat.count_distinct_string_list(Post, :categories, project_id)
|
category_count: Chat.count_distinct_string_list(Post, :categories, project_id)
|
||||||
}
|
}
|
||||||
@@ -132,9 +134,28 @@ defmodule BDS.AI.ChatTools do
|
|||||||
project_tools =
|
project_tools =
|
||||||
if is_binary(project_id) do
|
if is_binary(project_id) do
|
||||||
[
|
[
|
||||||
%{name: "blog_stats", spec: tool_spec("blog_stats", "Return aggregate blog statistics", %{"type" => "object", "properties" => %{}})},
|
%{
|
||||||
%{name: "list_posts", spec: tool_spec("list_posts", "List recent posts in the active project", limit_schema())},
|
name: "blog_stats",
|
||||||
%{name: "list_media", spec: tool_spec("list_media", "List recent media items in the active project", limit_schema())}
|
spec:
|
||||||
|
tool_spec("blog_stats", "Return aggregate blog statistics", %{
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => %{}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "list_posts",
|
||||||
|
spec:
|
||||||
|
tool_spec("list_posts", "List recent posts in the active project", limit_schema())
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "list_media",
|
||||||
|
spec:
|
||||||
|
tool_spec(
|
||||||
|
"list_media",
|
||||||
|
"List recent media items in the active project",
|
||||||
|
limit_schema()
|
||||||
|
)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
@@ -142,14 +163,62 @@ defmodule BDS.AI.ChatTools do
|
|||||||
|
|
||||||
project_tools ++
|
project_tools ++
|
||||||
[
|
[
|
||||||
%{name: "render_card", spec: tool_spec("render_card", "Return a structured card payload", render_card_schema())},
|
%{
|
||||||
%{name: "render_table", spec: tool_spec("render_table", "Return a structured table payload", render_table_schema())},
|
name: "render_card",
|
||||||
%{name: "render_chart", spec: tool_spec("render_chart", "Return a structured chart payload", render_chart_schema())},
|
spec:
|
||||||
%{name: "render_form", spec: tool_spec("render_form", "Return a structured form payload", render_form_schema())},
|
tool_spec("render_card", "Return a structured card payload", render_card_schema())
|
||||||
%{name: "render_metric", spec: tool_spec("render_metric", "Return a structured metric payload", render_metric_schema())},
|
},
|
||||||
%{name: "render_list", spec: tool_spec("render_list", "Return a structured list payload", render_list_schema())},
|
%{
|
||||||
%{name: "render_tabs", spec: tool_spec("render_tabs", "Return a structured tabs payload", render_tabs_schema())},
|
name: "render_table",
|
||||||
%{name: "render_mindmap", spec: tool_spec("render_mindmap", "Return a structured mindmap payload", render_mindmap_schema())}
|
spec:
|
||||||
|
tool_spec(
|
||||||
|
"render_table",
|
||||||
|
"Return a structured table payload",
|
||||||
|
render_table_schema()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "render_chart",
|
||||||
|
spec:
|
||||||
|
tool_spec(
|
||||||
|
"render_chart",
|
||||||
|
"Return a structured chart payload",
|
||||||
|
render_chart_schema()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "render_form",
|
||||||
|
spec:
|
||||||
|
tool_spec("render_form", "Return a structured form payload", render_form_schema())
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "render_metric",
|
||||||
|
spec:
|
||||||
|
tool_spec(
|
||||||
|
"render_metric",
|
||||||
|
"Return a structured metric payload",
|
||||||
|
render_metric_schema()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "render_list",
|
||||||
|
spec:
|
||||||
|
tool_spec("render_list", "Return a structured list payload", render_list_schema())
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "render_tabs",
|
||||||
|
spec:
|
||||||
|
tool_spec("render_tabs", "Return a structured tabs payload", render_tabs_schema())
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "render_mindmap",
|
||||||
|
spec:
|
||||||
|
tool_spec(
|
||||||
|
"render_mindmap",
|
||||||
|
"Return a structured mindmap payload",
|
||||||
|
render_mindmap_schema()
|
||||||
|
)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ defmodule BDS.AI.HttpClient do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
def get(url, headers) when is_binary(url) and is_map(headers) do
|
def get(url, headers) when is_binary(url) and is_map(headers) do
|
||||||
request = {String.to_charlist(url), Enum.map(headers, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end)}
|
request =
|
||||||
|
{String.to_charlist(url),
|
||||||
|
Enum.map(headers, fn {key, value} ->
|
||||||
|
{String.to_charlist(key), String.to_charlist(value)}
|
||||||
|
end)}
|
||||||
|
|
||||||
:inets.start()
|
:inets.start()
|
||||||
:ssl.start()
|
:ssl.start()
|
||||||
@@ -24,7 +28,10 @@ defmodule BDS.AI.HttpClient do
|
|||||||
def post(url, headers, body)
|
def post(url, headers, body)
|
||||||
when is_binary(url) and is_map(headers) and is_binary(body) do
|
when is_binary(url) and is_map(headers) and is_binary(body) do
|
||||||
request =
|
request =
|
||||||
{String.to_charlist(url), Enum.map(headers, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end), ~c"application/json", body}
|
{String.to_charlist(url),
|
||||||
|
Enum.map(headers, fn {key, value} ->
|
||||||
|
{String.to_charlist(key), String.to_charlist(value)}
|
||||||
|
end), ~c"application/json", body}
|
||||||
|
|
||||||
:inets.start()
|
:inets.start()
|
||||||
:ssl.start()
|
:ssl.start()
|
||||||
|
|||||||
@@ -34,31 +34,41 @@ defmodule BDS.AI.Model do
|
|||||||
|
|
||||||
def changeset(model, attrs) do
|
def changeset(model, attrs) do
|
||||||
model
|
model
|
||||||
|> cast(attrs, [
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[
|
||||||
|
:provider,
|
||||||
|
:model_id,
|
||||||
|
:name,
|
||||||
|
:family,
|
||||||
|
:supports_attachment,
|
||||||
|
:supports_reasoning,
|
||||||
|
:supports_tool_calls,
|
||||||
|
:supports_structured_output,
|
||||||
|
:supports_temperature,
|
||||||
|
:knowledge,
|
||||||
|
:release_date,
|
||||||
|
:last_updated_date,
|
||||||
|
:open_weights,
|
||||||
|
:input_price,
|
||||||
|
:output_price,
|
||||||
|
:cache_read_price,
|
||||||
|
:cache_write_price,
|
||||||
|
:context_window,
|
||||||
|
:max_input_tokens,
|
||||||
|
:max_output_tokens,
|
||||||
|
:interleaved,
|
||||||
|
:status,
|
||||||
|
:updated_at
|
||||||
|
], empty_values: [nil])
|
||||||
|
|> validate_required([
|
||||||
:provider,
|
:provider,
|
||||||
:model_id,
|
:model_id,
|
||||||
:name,
|
:name,
|
||||||
:family,
|
|
||||||
:supports_attachment,
|
|
||||||
:supports_reasoning,
|
|
||||||
:supports_tool_calls,
|
|
||||||
:supports_structured_output,
|
|
||||||
:supports_temperature,
|
|
||||||
:knowledge,
|
|
||||||
:release_date,
|
|
||||||
:last_updated_date,
|
|
||||||
:open_weights,
|
|
||||||
:input_price,
|
|
||||||
:output_price,
|
|
||||||
:cache_read_price,
|
|
||||||
:cache_write_price,
|
|
||||||
:context_window,
|
:context_window,
|
||||||
:max_input_tokens,
|
:max_input_tokens,
|
||||||
:max_output_tokens,
|
:max_output_tokens,
|
||||||
:interleaved,
|
|
||||||
:status,
|
|
||||||
:updated_at
|
:updated_at
|
||||||
], empty_values: [nil])
|
])
|
||||||
|> validate_required([:provider, :model_id, :name, :context_window, :max_input_tokens, :max_output_tokens, :updated_at])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
}
|
}
|
||||||
|> maybe_put_auth(endpoint.api_key)
|
|> maybe_put_auth(endpoint.api_key)
|
||||||
|
|
||||||
payload = %{
|
payload =
|
||||||
"model" => request.model,
|
%{
|
||||||
"messages" => request.messages,
|
"model" => request.model,
|
||||||
"max_tokens" => request.max_output_tokens
|
"messages" => request.messages,
|
||||||
}
|
"max_tokens" => request.max_output_tokens
|
||||||
|> maybe_put_tools(request.tools)
|
}
|
||||||
|
|> maybe_put_tools(request.tools)
|
||||||
|
|
||||||
with {:ok, response} <- HttpClient.post(url, headers, Jason.encode!(payload)),
|
with {:ok, response} <- HttpClient.post(url, headers, Jason.encode!(payload)),
|
||||||
200 <- response.status do
|
200 <- response.status do
|
||||||
@@ -55,7 +56,9 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
|
|
||||||
json =
|
json =
|
||||||
case content do
|
case content do
|
||||||
nil -> nil
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
value when is_binary(value) ->
|
value when is_binary(value) ->
|
||||||
case Jason.decode(value) do
|
case Jason.decode(value) do
|
||||||
{:ok, decoded} when is_map(decoded) -> decoded
|
{:ok, decoded} when is_map(decoded) -> decoded
|
||||||
@@ -77,10 +80,17 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
|
|
||||||
defp models_url(url) do
|
defp models_url(url) do
|
||||||
cond do
|
cond do
|
||||||
String.ends_with?(url, "/chat/completions") -> String.replace_suffix(url, "/chat/completions", "/models")
|
String.ends_with?(url, "/chat/completions") ->
|
||||||
String.ends_with?(url, "/models") -> url
|
String.replace_suffix(url, "/chat/completions", "/models")
|
||||||
String.ends_with?(url, "/") -> url <> "models"
|
|
||||||
true -> url <> "/models"
|
String.ends_with?(url, "/models") ->
|
||||||
|
url
|
||||||
|
|
||||||
|
String.ends_with?(url, "/") ->
|
||||||
|
url <> "models"
|
||||||
|
|
||||||
|
true ->
|
||||||
|
url <> "/models"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -114,7 +124,9 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
|||||||
|
|
||||||
defp maybe_put_auth(headers, nil), do: headers
|
defp maybe_put_auth(headers, nil), do: headers
|
||||||
defp maybe_put_auth(headers, ""), do: headers
|
defp maybe_put_auth(headers, ""), do: headers
|
||||||
defp maybe_put_auth(headers, api_key), do: Map.put(headers, "authorization", "Bearer #{api_key}")
|
|
||||||
|
defp maybe_put_auth(headers, api_key),
|
||||||
|
do: Map.put(headers, "authorization", "Bearer #{api_key}")
|
||||||
|
|
||||||
defp maybe_put_tools(payload, []), do: payload
|
defp maybe_put_tools(payload, []), do: payload
|
||||||
defp maybe_put_tools(payload, nil), do: payload
|
defp maybe_put_tools(payload, nil), do: payload
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ defmodule BDS.AI.Runtime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp resolve_model_for_operation(:analyze_image, :airplane, endpoint, extra) do
|
defp resolve_model_for_operation(:analyze_image, :airplane, endpoint, extra) do
|
||||||
{:ok, Keyword.get(extra, :model) || model_preference_value(:airplane_image_analysis) || endpoint.model}
|
{:ok,
|
||||||
|
Keyword.get(extra, :model) || model_preference_value(:airplane_image_analysis) ||
|
||||||
|
endpoint.model}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp resolve_model_for_operation(:analyze_image, :online, endpoint, extra) do
|
defp resolve_model_for_operation(:analyze_image, :online, endpoint, extra) do
|
||||||
@@ -83,7 +85,8 @@ defmodule BDS.AI.Runtime do
|
|||||||
defp fetch_endpoint_for_mode(mode, secret_backend) do
|
defp fetch_endpoint_for_mode(mode, secret_backend) do
|
||||||
with {:ok, endpoint} <- AI.get_endpoint(mode, secret_backend: secret_backend) do
|
with {:ok, endpoint} <- AI.get_endpoint(mode, secret_backend: secret_backend) do
|
||||||
case endpoint do
|
case endpoint do
|
||||||
%{url: url, model: model} = loaded when is_binary(url) and url != "" and is_binary(model) and model != "" ->
|
%{url: url, model: model} = loaded
|
||||||
|
when is_binary(url) and url != "" and is_binary(model) and model != "" ->
|
||||||
if mode == :online and blank?(loaded.api_key) do
|
if mode == :online and blank?(loaded.api_key) do
|
||||||
{:error, %{kind: :endpoint_not_configured, endpoint: mode}}
|
{:error, %{kind: :endpoint_not_configured, endpoint: mode}}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ defmodule BDS.AI.SecretBackend do
|
|||||||
with {:ok, binary} <- Base.decode64(encoded),
|
with {:ok, binary} <- Base.decode64(encoded),
|
||||||
<<iv::binary-size(12), tag::binary-size(16), ciphertext::binary>> <- binary,
|
<<iv::binary-size(12), tag::binary-size(16), ciphertext::binary>> <- binary,
|
||||||
plaintext when is_binary(plaintext) <-
|
plaintext when is_binary(plaintext) <-
|
||||||
:crypto.crypto_one_time_aead(:aes_256_gcm, secret_key(), iv, ciphertext, @aad, tag, false) do
|
:crypto.crypto_one_time_aead(
|
||||||
|
:aes_256_gcm,
|
||||||
|
secret_key(),
|
||||||
|
iv,
|
||||||
|
ciphertext,
|
||||||
|
@aad,
|
||||||
|
tag,
|
||||||
|
false
|
||||||
|
) do
|
||||||
{:ok, plaintext}
|
{:ok, plaintext}
|
||||||
else
|
else
|
||||||
_other -> {:error, :invalid_ciphertext}
|
_other -> {:error, :invalid_ciphertext}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ defmodule BDS.AI.SettingsStore do
|
|||||||
def put_setting(key, value) when is_binary(key) and is_binary(value) do
|
def put_setting(key, value) when is_binary(key) and is_binary(value) do
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
|
|
||||||
(%Setting{}
|
%Setting{}
|
||||||
|> Setting.changeset(%{key: key, value: value, updated_at: now}))
|
|> Setting.changeset(%{key: key, value: value, updated_at: now})
|
||||||
|> Repo.insert(
|
|> Repo.insert(
|
||||||
on_conflict: [set: [value: value, updated_at: now]],
|
on_conflict: [set: [value: value, updated_at: now]],
|
||||||
conflict_target: [:key]
|
conflict_target: [:key]
|
||||||
|
|||||||
@@ -39,12 +39,18 @@ defmodule BDS.CliSync do
|
|||||||
ids = Enum.map(notifications, & &1.id)
|
ids = Enum.map(notifications, & &1.id)
|
||||||
|
|
||||||
if ids != [] do
|
if ids != [] do
|
||||||
Repo.update_all(from(notification in Notification, where: notification.id in ^ids), set: [seen_at: now])
|
Repo.update_all(from(notification in Notification, where: notification.id in ^ids),
|
||||||
|
set: [seen_at: now]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
Enum.map(notifications, fn notification ->
|
Enum.map(notifications, fn notification ->
|
||||||
%{entity_type: notification.entity_type, entity_id: notification.entity_id, action: notification.action}
|
%{
|
||||||
|
entity_type: notification.entity_type,
|
||||||
|
entity_id: notification.entity_id,
|
||||||
|
action: notification.action
|
||||||
|
}
|
||||||
end)}
|
end)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -52,13 +58,17 @@ defmodule BDS.CliSync do
|
|||||||
{processed_count, _} =
|
{processed_count, _} =
|
||||||
Repo.delete_all(
|
Repo.delete_all(
|
||||||
from notification in Notification,
|
from notification in Notification,
|
||||||
where: not is_nil(notification.seen_at) and notification.created_at <= ^(now - @processed_ttl_ms)
|
where:
|
||||||
|
not is_nil(notification.seen_at) and
|
||||||
|
notification.created_at <= ^(now - @processed_ttl_ms)
|
||||||
)
|
)
|
||||||
|
|
||||||
{unprocessed_count, _} =
|
{unprocessed_count, _} =
|
||||||
Repo.delete_all(
|
Repo.delete_all(
|
||||||
from notification in Notification,
|
from notification in Notification,
|
||||||
where: is_nil(notification.seen_at) and notification.created_at <= ^(now - @unprocessed_ttl_ms)
|
where:
|
||||||
|
is_nil(notification.seen_at) and
|
||||||
|
notification.created_at <= ^(now - @unprocessed_ttl_ms)
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, %{processed: processed_count, unprocessed: unprocessed_count}}
|
{:ok, %{processed: processed_count, unprocessed: unprocessed_count}}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ defmodule BDS.CliSync.Notification do
|
|||||||
|
|
||||||
def changeset(notification, attrs) do
|
def changeset(notification, attrs) do
|
||||||
notification
|
notification
|
||||||
|> cast(attrs, [:entity_type, :entity_id, :action, :from_cli, :seen_at, :created_at], empty_values: [nil])
|
|> cast(attrs, [:entity_type, :entity_id, :action, :from_cli, :seen_at, :created_at],
|
||||||
|
empty_values: [nil]
|
||||||
|
)
|
||||||
|> validate_required([:entity_type, :entity_id, :action, :from_cli, :created_at])
|
|> validate_required([:entity_type, :entity_id, :action, :from_cli, :created_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ defmodule BDS.CliSync.Watcher do
|
|||||||
@impl true
|
@impl true
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
state = %{
|
state = %{
|
||||||
poll_interval_ms: normalize_positive_integer(Keyword.get(opts, :poll_interval_ms), @default_poll_interval_ms),
|
poll_interval_ms:
|
||||||
|
normalize_positive_integer(
|
||||||
|
Keyword.get(opts, :poll_interval_ms),
|
||||||
|
@default_poll_interval_ms
|
||||||
|
),
|
||||||
pubsub: Keyword.get(opts, :pubsub, BDS.PubSub)
|
pubsub: Keyword.get(opts, :pubsub, BDS.PubSub)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +53,11 @@ defmodule BDS.CliSync.Watcher do
|
|||||||
{:ok, _pruned} = CliSync.prune_notifications()
|
{:ok, _pruned} = CliSync.prune_notifications()
|
||||||
|
|
||||||
Enum.each(notifications, fn notification ->
|
Enum.each(notifications, fn notification ->
|
||||||
Phoenix.PubSub.broadcast(state.pubsub, topic(), {:entity_changed, notification_payload(notification)})
|
Phoenix.PubSub.broadcast(
|
||||||
|
state.pubsub,
|
||||||
|
topic(),
|
||||||
|
{:entity_changed, notification_payload(notification)}
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ defmodule BDS.Desktop.Automation do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:native_menu_action, action}, _from, state) do
|
def handle_call({:native_menu_action, action}, _from, state) do
|
||||||
{reply, state} = driver_request(state, %{"command" => "native_menu_action", "action" => action})
|
{reply, state} =
|
||||||
|
driver_request(state, %{"command" => "native_menu_action", "action" => action})
|
||||||
|
|
||||||
{:reply, normalize_simple_reply(reply), state}
|
{:reply, normalize_simple_reply(reply), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -204,7 +206,9 @@ defmodule BDS.Desktop.Automation do
|
|||||||
|
|
||||||
receive_driver_message(state, @request_timeout, fn message ->
|
receive_driver_message(state, @request_timeout, fn message ->
|
||||||
case message do
|
case message do
|
||||||
%{"ref" => ^ref, "status" => "ok", "result" => result} -> {:ok, result}
|
%{"ref" => ^ref, "status" => "ok", "result" => result} ->
|
||||||
|
{:ok, result}
|
||||||
|
|
||||||
%{"ref" => ^ref, "status" => "error", "message" => reason} ->
|
%{"ref" => ^ref, "status" => "error", "message" => reason} ->
|
||||||
raise "desktop automation request failed: #{reason}"
|
raise "desktop automation request failed: #{reason}"
|
||||||
|
|
||||||
@@ -242,7 +246,8 @@ defmodule BDS.Desktop.Automation do
|
|||||||
defp process_driver_messages(state, deadline, matcher) do
|
defp process_driver_messages(state, deadline, matcher) do
|
||||||
{messages, buffer} = split_driver_buffer(state.driver_buffer)
|
{messages, buffer} = split_driver_buffer(state.driver_buffer)
|
||||||
|
|
||||||
case Enum.reduce_while(messages, {%{state | driver_buffer: buffer}, nil}, fn message, {acc, _} ->
|
case Enum.reduce_while(messages, {%{state | driver_buffer: buffer}, nil}, fn message,
|
||||||
|
{acc, _} ->
|
||||||
case decode_driver_message(message) do
|
case decode_driver_message(message) do
|
||||||
:skip ->
|
:skip ->
|
||||||
{:cont, {acc, nil}}
|
{:cont, {acc, nil}}
|
||||||
@@ -259,7 +264,11 @@ defmodule BDS.Desktop.Automation do
|
|||||||
|
|
||||||
receive do
|
receive do
|
||||||
{port, {:data, data}} when port == state.driver_port ->
|
{port, {:data, data}} when port == state.driver_port ->
|
||||||
process_driver_messages(%{state | driver_buffer: state.driver_buffer <> data}, deadline, matcher)
|
process_driver_messages(
|
||||||
|
%{state | driver_buffer: state.driver_buffer <> data},
|
||||||
|
deadline,
|
||||||
|
matcher
|
||||||
|
)
|
||||||
|
|
||||||
{port, {:exit_status, status}} when port == state.driver_port ->
|
{port, {:exit_status, status}} when port == state.driver_port ->
|
||||||
raise "desktop automation driver exited with status #{status}"
|
raise "desktop automation driver exited with status #{status}"
|
||||||
@@ -311,7 +320,9 @@ defmodule BDS.Desktop.Automation do
|
|||||||
|
|
||||||
defp do_wait_for_server(base_url, deadline) do
|
defp do_wait_for_server(base_url, deadline) do
|
||||||
case :httpc.request(:get, {String.to_charlist(base_url <> "health"), []}, [], []) do
|
case :httpc.request(:get, {String.to_charlist(base_url <> "health"), []}, [], []) do
|
||||||
{:ok, {{_, 200, _}, _headers, _body}} -> :ok
|
{:ok, {{_, 200, _}, _headers, _body}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
if System.monotonic_time(:millisecond) >= deadline do
|
if System.monotonic_time(:millisecond) >= deadline do
|
||||||
raise "desktop app process did not become healthy in time"
|
raise "desktop app process did not become healthy in time"
|
||||||
|
|||||||
@@ -9,28 +9,30 @@ defmodule BDS.Desktop.Endpoint do
|
|||||||
signing_salt: "desktop-shell"
|
signing_salt: "desktop-shell"
|
||||||
]
|
]
|
||||||
|
|
||||||
socket "/live", Phoenix.LiveView.Socket,
|
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
|
||||||
websocket: [connect_info: [session: @session_options]]
|
|
||||||
|
|
||||||
plug Plug.Session, @session_options
|
plug(Plug.Session, @session_options)
|
||||||
plug :maybe_require_desktop_auth
|
plug(:maybe_require_desktop_auth)
|
||||||
|
|
||||||
plug Plug.Static,
|
plug(Plug.Static,
|
||||||
at: "/assets",
|
at: "/assets",
|
||||||
from: {:bds, "priv/ui"},
|
from: {:bds, "priv/ui"},
|
||||||
only: ["app.css", "live.js", "monaco"]
|
only: ["app.css", "live.js", "monaco"]
|
||||||
|
)
|
||||||
|
|
||||||
plug Plug.Static,
|
plug(Plug.Static,
|
||||||
at: "/vendor/phoenix",
|
at: "/vendor/phoenix",
|
||||||
from: {:phoenix, "priv/static"},
|
from: {:phoenix, "priv/static"},
|
||||||
only: ["phoenix.min.js"]
|
only: ["phoenix.min.js"]
|
||||||
|
)
|
||||||
|
|
||||||
plug Plug.Static,
|
plug(Plug.Static,
|
||||||
at: "/vendor/live_view",
|
at: "/vendor/live_view",
|
||||||
from: {:phoenix_live_view, "priv/static"},
|
from: {:phoenix_live_view, "priv/static"},
|
||||||
only: ["phoenix_live_view.min.js"]
|
only: ["phoenix_live_view.min.js"]
|
||||||
|
)
|
||||||
|
|
||||||
plug BDS.Desktop.Router
|
plug(BDS.Desktop.Router)
|
||||||
|
|
||||||
defp maybe_require_desktop_auth(conn, _opts) do
|
defp maybe_require_desktop_auth(conn, _opts) do
|
||||||
if System.get_env("BDS_DESKTOP_AUTOMATION") in ["1", "true", "TRUE"] do
|
if System.get_env("BDS_DESKTOP_AUTOMATION") in ["1", "true", "TRUE"] do
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ defmodule BDS.Desktop.MainWindow do
|
|||||||
restored = restore_bounds()
|
restored = restore_bounds()
|
||||||
{default_width, default_height} = Keyword.get(desktop_config, :window_size, @default_size)
|
{default_width, default_height} = Keyword.get(desktop_config, :window_size, @default_size)
|
||||||
{min_width, min_height} = Keyword.get(desktop_config, :window_min_size, @default_min_size)
|
{min_width, min_height} = Keyword.get(desktop_config, :window_min_size, @default_min_size)
|
||||||
startup_bounds = clamp_startup_bounds(restored || %{width: default_width, height: default_height})
|
|
||||||
|
startup_bounds =
|
||||||
|
clamp_startup_bounds(restored || %{width: default_width, height: default_height})
|
||||||
|
|
||||||
base_opts = [
|
base_opts = [
|
||||||
app: :bds,
|
app: :bds,
|
||||||
@@ -70,7 +72,9 @@ defmodule BDS.Desktop.MainWindow do
|
|||||||
frame ->
|
frame ->
|
||||||
apply_restored_bounds(frame)
|
apply_restored_bounds(frame)
|
||||||
schedule_persist()
|
schedule_persist()
|
||||||
{:noreply, %{state | frame: frame, last_bounds: current_bounds(frame) || state.last_bounds}}
|
|
||||||
|
{:noreply,
|
||||||
|
%{state | frame: frame, last_bounds: current_bounds(frame) || state.last_bounds}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -124,9 +128,15 @@ defmodule BDS.Desktop.MainWindow do
|
|||||||
defp current_bounds(frame) do
|
defp current_bounds(frame) do
|
||||||
with_wx_env(fn ->
|
with_wx_env(fn ->
|
||||||
cond do
|
cond do
|
||||||
not :wxWindow.isShown(frame) -> nil
|
not :wxWindow.isShown(frame) ->
|
||||||
:wxTopLevelWindow.isFullScreen(frame) -> nil
|
nil
|
||||||
:wxTopLevelWindow.isMaximized(frame) -> nil
|
|
||||||
|
:wxTopLevelWindow.isFullScreen(frame) ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
:wxTopLevelWindow.isMaximized(frame) ->
|
||||||
|
nil
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{x, y} = :wxWindow.getPosition(frame)
|
{x, y} = :wxWindow.getPosition(frame)
|
||||||
{width, height} = :wxWindow.getSize(frame)
|
{width, height} = :wxWindow.getSize(frame)
|
||||||
@@ -160,7 +170,8 @@ defmodule BDS.Desktop.MainWindow do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_bounds(%{x: x, y: y, width: width, height: height})
|
defp normalize_bounds(%{x: x, y: y, width: width, height: height})
|
||||||
when is_integer(x) and is_integer(y) and is_integer(width) and is_integer(height) and width > 0 and height > 0 do
|
when is_integer(x) and is_integer(y) and is_integer(width) and is_integer(height) and
|
||||||
|
width > 0 and height > 0 do
|
||||||
{:ok, %{x: x, y: y, width: width, height: height}}
|
{:ok, %{x: x, y: y, width: width, height: height}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -180,7 +191,8 @@ defmodule BDS.Desktop.MainWindow do
|
|||||||
desktop_config = Application.get_env(:bds, :desktop, [])
|
desktop_config = Application.get_env(:bds, :desktop, [])
|
||||||
|
|
||||||
case Keyword.get(desktop_config, :window_client_area_override) do
|
case Keyword.get(desktop_config, :window_client_area_override) do
|
||||||
{x, y, width, height} when is_integer(x) and is_integer(y) and is_integer(width) and is_integer(height) ->
|
{x, y, width, height}
|
||||||
|
when is_integer(x) and is_integer(y) and is_integer(width) and is_integer(height) ->
|
||||||
%{x: x, y: y, width: width, height: height}
|
%{x: x, y: y, width: width, height: height}
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ defmodule BDS.Desktop.MediaController do
|
|||||||
with %{} = project <- Projects.get_active_project(),
|
with %{} = project <- Projects.get_active_project(),
|
||||||
%MediaRecord{} = media <- Repo.get(MediaRecord, media_id),
|
%MediaRecord{} = media <- Repo.get(MediaRecord, media_id),
|
||||||
true <- media.project_id == project.id,
|
true <- media.project_id == project.id,
|
||||||
relative_path when is_binary(relative_path) <- Media.thumbnail_paths(media)[thumbnail_size(size)],
|
relative_path when is_binary(relative_path) <-
|
||||||
|
Media.thumbnail_paths(media)[thumbnail_size(size)],
|
||||||
absolute_path = Path.join(Projects.project_data_dir(project), relative_path),
|
absolute_path = Path.join(Projects.project_data_dir(project), relative_path),
|
||||||
true <- File.exists?(absolute_path) do
|
true <- File.exists?(absolute_path) do
|
||||||
{:ok, thumbnail_content_type(relative_path), absolute_path}
|
{:ok, thumbnail_content_type(relative_path), absolute_path}
|
||||||
@@ -33,7 +34,8 @@ defmodule BDS.Desktop.MediaController do
|
|||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
||||||
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do
|
if match?(%Exqlite.Error{}, error) and
|
||||||
|
not String.contains?(Exception.message(error), "no such table") do
|
||||||
reraise error, __STACKTRACE__
|
reraise error, __STACKTRACE__
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -168,14 +168,16 @@ defmodule BDS.Desktop.Overlay do
|
|||||||
def close_lightbox(%{kind: :gallery} = overlay), do: %{overlay | lightbox: nil}
|
def close_lightbox(%{kind: :gallery} = overlay), do: %{overlay | lightbox: nil}
|
||||||
def close_lightbox(overlay), do: overlay
|
def close_lightbox(overlay), do: overlay
|
||||||
|
|
||||||
def lightbox_next(%{kind: :gallery, lightbox: lightbox, images: images} = overlay) when is_map(lightbox) and images != [] do
|
def lightbox_next(%{kind: :gallery, lightbox: lightbox, images: images} = overlay)
|
||||||
|
when is_map(lightbox) and images != [] do
|
||||||
next_index = rem(lightbox.current_index + 1, length(images))
|
next_index = rem(lightbox.current_index + 1, length(images))
|
||||||
%{overlay | lightbox: lightbox_from_index(images, next_index)}
|
%{overlay | lightbox: lightbox_from_index(images, next_index)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def lightbox_next(overlay), do: overlay
|
def lightbox_next(overlay), do: overlay
|
||||||
|
|
||||||
def lightbox_previous(%{kind: :gallery, lightbox: lightbox, images: images} = overlay) when is_map(lightbox) and images != [] do
|
def lightbox_previous(%{kind: :gallery, lightbox: lightbox, images: images} = overlay)
|
||||||
|
when is_map(lightbox) and images != [] do
|
||||||
next_index = rem(lightbox.current_index - 1 + length(images), length(images))
|
next_index = rem(lightbox.current_index - 1 + length(images), length(images))
|
||||||
%{overlay | lightbox: lightbox_from_index(images, next_index)}
|
%{overlay | lightbox: lightbox_from_index(images, next_index)}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ defmodule BDS.Desktop.Router do
|
|||||||
import Phoenix.LiveView.Router
|
import Phoenix.LiveView.Router
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug(:accepts, ["html"])
|
||||||
plug :fetch_session
|
plug(:fetch_session)
|
||||||
plug :fetch_live_flash
|
plug(:fetch_live_flash)
|
||||||
plug :put_root_layout, html: {BDS.Desktop.Layouts, :root}
|
plug(:put_root_layout, html: {BDS.Desktop.Layouts, :root})
|
||||||
plug :protect_from_forgery
|
plug(:protect_from_forgery)
|
||||||
plug :put_secure_browser_headers
|
plug(:put_secure_browser_headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", BDS.Desktop do
|
scope "/", BDS.Desktop do
|
||||||
pipe_through :browser
|
pipe_through(:browser)
|
||||||
|
|
||||||
get "/health", HealthController, :show
|
get("/health", HealthController, :show)
|
||||||
get "/media-thumbnail/:media_id", MediaController, :thumbnail
|
get("/media-thumbnail/:media_id", MediaController, :thumbnail)
|
||||||
|
|
||||||
live_session :desktop_shell,
|
live_session :desktop_shell,
|
||||||
root_layout: {BDS.Desktop.Layouts, :root} do
|
root_layout: {BDS.Desktop.Layouts, :root} do
|
||||||
live "/", ShellLive, :index
|
live("/", ShellLive, :index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
Projects.shell_snapshot()
|
Projects.shell_snapshot()
|
||||||
rescue
|
rescue
|
||||||
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
||||||
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table: projects") do
|
if match?(%Exqlite.Error{}, error) and
|
||||||
|
not String.contains?(Exception.message(error), "no such table: projects") do
|
||||||
reraise error, __STACKTRACE__
|
reraise error, __STACKTRACE__
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -54,7 +55,8 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
Dashboard.snapshot(project_id)
|
Dashboard.snapshot(project_id)
|
||||||
rescue
|
rescue
|
||||||
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
||||||
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do
|
if match?(%Exqlite.Error{}, error) and
|
||||||
|
not String.contains?(Exception.message(error), "no such table") do
|
||||||
reraise error, __STACKTRACE__
|
reraise error, __STACKTRACE__
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -65,7 +67,8 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
Sidebar.view(project_id, view_id, params)
|
Sidebar.view(project_id, view_id, params)
|
||||||
rescue
|
rescue
|
||||||
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
||||||
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do
|
if match?(%Exqlite.Error{}, error) and
|
||||||
|
not String.contains?(Exception.message(error), "no such table") do
|
||||||
reraise error, __STACKTRACE__
|
reraise error, __STACKTRACE__
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -75,7 +78,10 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
def assistant_cards do
|
def assistant_cards do
|
||||||
[
|
[
|
||||||
%{label: "Offline Gate", text: "Automatic AI actions stay gated by airplane mode."},
|
%{label: "Offline Gate", text: "Automatic AI actions stay gated by airplane mode."},
|
||||||
%{label: "Filesystem Sync", text: "Metadata flush, diffing, and rebuild hooks still need editor wiring."},
|
%{
|
||||||
|
label: "Filesystem Sync",
|
||||||
|
text: "Metadata flush, diffing, and rebuild hooks still need editor wiring."
|
||||||
|
},
|
||||||
%{label: "Desktop Runtime", text: "The app window is now served from LiveView state."}
|
%{label: "Desktop Runtime", text: "The app window is now served from LiveView state."}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -117,7 +123,8 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
error in [DBConnection.OwnershipError, Exqlite.Error] ->
|
error in [DBConnection.OwnershipError, Exqlite.Error] ->
|
||||||
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do
|
if match?(%Exqlite.Error{}, error) and
|
||||||
|
not String.contains?(Exception.message(error), "no such table") do
|
||||||
reraise error, __STACKTRACE__
|
reraise error, __STACKTRACE__
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -146,17 +153,38 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
|
|
||||||
def activity_icon(id) do
|
def activity_icon(id) do
|
||||||
case to_string(id) do
|
case to_string(id) do
|
||||||
"posts" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"></path><path d="M8 12h8v2H8zm0 4h8v2H8z"></path></svg>)
|
"posts" ->
|
||||||
"pages" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h10v4h6v12H4V4zm10 1.5V9h4.5L14 5.5zM7 12h10v1.5H7V12zm0 3h10v1.5H7V15z"></path></svg>)
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"></path><path d="M8 12h8v2H8zm0 4h8v2H8z"></path></svg>)
|
||||||
"media" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg>)
|
|
||||||
"scripts" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"></path></svg>)
|
"pages" ->
|
||||||
"templates" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h7v7H4V4zm9 0h7v7h-7V4zM4 13h7v7H4v-7zm9 0h7v7h-7v-7zM5.5 5.5v4h4v-4h-4zm9 0v4h4v-4h-4zm-9 9v4h4v-4h-4zm9 0v4h4v-4h-4z"></path></svg>)
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h10v4h6v12H4V4zm10 1.5V9h4.5L14 5.5zM7 12h10v1.5H7V12zm0 3h10v1.5H7V15z"></path></svg>)
|
||||||
"tags" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"></path></svg>)
|
|
||||||
"chat" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"></path><circle cx="8" cy="10" r="1.5"></circle><circle cx="12" cy="10" r="1.5"></circle><circle cx="16" cy="10" r="1.5"></circle></svg>)
|
"media" ->
|
||||||
"import" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"></path></svg>)
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg>)
|
||||||
"git" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M22 11.73L12.27 2a1 1 0 0 0-1.41 0L8.84 4.02l2.56 2.56a1.2 1.2 0 0 1 1.52 1.53l2.47 2.47a1.2 1.2 0 1 1-.72.67l-2.3-2.3v6.06a1.2 1.2 0 1 1-.85 0V8.9a1.2 1.2 0 0 1-.66-1.59L8.35 4.8 2 11.16a1 1 0 0 0 0 1.41L11.73 22a1 1 0 0 0 1.41 0L22 13.14a1 1 0 0 0 0-1.41z"></path></svg>)
|
|
||||||
"settings" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg>)
|
"scripts" ->
|
||||||
_other -> activity_icon("posts")
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"></path></svg>)
|
||||||
|
|
||||||
|
"templates" ->
|
||||||
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h7v7H4V4zm9 0h7v7h-7V4zM4 13h7v7H4v-7zm9 0h7v7h-7v-7zM5.5 5.5v4h4v-4h-4zm9 0v4h4v-4h-4zm-9 9v4h4v-4h-4zm9 0v4h4v-4h-4z"></path></svg>)
|
||||||
|
|
||||||
|
"tags" ->
|
||||||
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"></path></svg>)
|
||||||
|
|
||||||
|
"chat" ->
|
||||||
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"></path><circle cx="8" cy="10" r="1.5"></circle><circle cx="12" cy="10" r="1.5"></circle><circle cx="16" cy="10" r="1.5"></circle></svg>)
|
||||||
|
|
||||||
|
"import" ->
|
||||||
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"></path></svg>)
|
||||||
|
|
||||||
|
"git" ->
|
||||||
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M22 11.73L12.27 2a1 1 0 0 0-1.41 0L8.84 4.02l2.56 2.56a1.2 1.2 0 0 1 1.52 1.53l2.47 2.47a1.2 1.2 0 1 1-.72.67l-2.3-2.3v6.06a1.2 1.2 0 1 1-.85 0V8.9a1.2 1.2 0 0 1-.66-1.59L8.35 4.8 2 11.16a1 1 0 0 0 0 1.41L11.73 22a1 1 0 0 0 1.41 0L22 13.14a1 1 0 0 0 0-1.41z"></path></svg>)
|
||||||
|
|
||||||
|
"settings" ->
|
||||||
|
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg>)
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
activity_icon("posts")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -171,7 +199,10 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
|
|
||||||
def dashboard_post_count_label(count) do
|
def dashboard_post_count_label(count) do
|
||||||
normalized_count = count || 0
|
normalized_count = count || 0
|
||||||
key = if normalized_count == 1, do: "dashboard.postCount.one", else: "dashboard.postCount.other"
|
|
||||||
|
key =
|
||||||
|
if normalized_count == 1, do: "dashboard.postCount.one", else: "dashboard.postCount.other"
|
||||||
|
|
||||||
translate(key, %{count: normalized_count})
|
translate(key, %{count: normalized_count})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -188,7 +219,7 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
|
|
||||||
top_items
|
top_items
|
||||||
|> Enum.map(fn item ->
|
|> Enum.map(fn item ->
|
||||||
font_size = 11 + (((item.count || 0) - min_count) / range) * 11
|
font_size = 11 + ((item.count || 0) - min_count) / range * 11
|
||||||
Map.merge(item, %{font_size: font_size, color: normalize_dashboard_tag_color(item.color)})
|
Map.merge(item, %{font_size: font_size, color: normalize_dashboard_tag_color(item.color)})
|
||||||
end)
|
end)
|
||||||
|> Enum.sort_by(&String.downcase(to_string(&1.tag || "")))
|
|> Enum.sort_by(&String.downcase(to_string(&1.tag || "")))
|
||||||
@@ -199,10 +230,11 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
|
|
||||||
declarations =
|
declarations =
|
||||||
if item.color do
|
if item.color do
|
||||||
declarations ++ [
|
declarations ++
|
||||||
"background-color: #{item.color};",
|
[
|
||||||
"color: #{dashboard_contrast_color(item.color)};"
|
"background-color: #{item.color};",
|
||||||
]
|
"color: #{dashboard_contrast_color(item.color)};"
|
||||||
|
]
|
||||||
else
|
else
|
||||||
declarations
|
declarations
|
||||||
end
|
end
|
||||||
@@ -225,9 +257,17 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
|
|
||||||
def route_label(route) do
|
def route_label(route) do
|
||||||
case to_string(route) do
|
case to_string(route) do
|
||||||
"git_log" -> "Git Log"
|
"git_log" ->
|
||||||
"post_links" -> "Post Links"
|
"Git Log"
|
||||||
other -> other |> String.replace("_", " ") |> String.split() |> Enum.map_join(" ", &String.capitalize/1)
|
|
||||||
|
"post_links" ->
|
||||||
|
"Post Links"
|
||||||
|
|
||||||
|
other ->
|
||||||
|
other
|
||||||
|
|> String.replace("_", " ")
|
||||||
|
|> String.split()
|
||||||
|
|> Enum.map_join(" ", &String.capitalize/1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -255,7 +295,10 @@ defmodule BDS.Desktop.ShellData do
|
|||||||
defp effective_ui_language(locale), do: locale
|
defp effective_ui_language(locale), do: locale
|
||||||
|
|
||||||
defp maybe_add_panel_tab(tabs, :post, :post_links), do: tabs ++ [:post_links]
|
defp maybe_add_panel_tab(tabs, :post, :post_links), do: tabs ++ [:post_links]
|
||||||
defp maybe_add_panel_tab(tabs, route, :git_log) when route in [:post, :media], do: tabs ++ [:git_log]
|
|
||||||
|
defp maybe_add_panel_tab(tabs, route, :git_log) when route in [:post, :media],
|
||||||
|
do: tabs ++ [:git_log]
|
||||||
|
|
||||||
defp maybe_add_panel_tab(tabs, _route, _tab), do: tabs
|
defp maybe_add_panel_tab(tabs, _route, _tab), do: tabs
|
||||||
|
|
||||||
defp default_project_snapshot do
|
defp default_project_snapshot do
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking}
|
alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking}
|
||||||
|
|
||||||
embed_templates "chat_editor_html/*"
|
embed_templates("chat_editor_html/*")
|
||||||
|
|
||||||
# ── Public API: state assignment ───────────────────────────────────────────
|
# ── Public API: state assignment ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
assign(socket, :chat_editor, MessageBuild.build(socket.assigns))
|
assign(socket, :chat_editor, MessageBuild.build(socket.assigns))
|
||||||
end
|
end
|
||||||
@@ -25,6 +26,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|
|
||||||
# ── Public API: input + surface state ──────────────────────────────────────
|
# ── Public API: input + surface state ──────────────────────────────────────
|
||||||
|
|
||||||
|
@spec update_input(term(), term(), term()) :: term()
|
||||||
def update_input(socket, value, reload) do
|
def update_input(socket, value, reload) do
|
||||||
%{id: conversation_id} = socket.assigns.current_tab
|
%{id: conversation_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_surface_form(term(), term(), term(), term()) :: term()
|
||||||
def update_surface_form(socket, surface_id, fields, reload)
|
def update_surface_form(socket, surface_id, fields, reload)
|
||||||
when is_binary(surface_id) and is_map(fields) do
|
when is_binary(surface_id) and is_map(fields) do
|
||||||
next_data = Map.put(socket.assigns.chat_editor_surface_data, surface_id, fields)
|
next_data = Map.put(socket.assigns.chat_editor_surface_data, surface_id, fields)
|
||||||
@@ -45,6 +48,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_surface_tab(term(), term(), term(), term()) :: term()
|
||||||
def select_surface_tab(socket, surface_id, index, reload)
|
def select_surface_tab(socket, surface_id, index, reload)
|
||||||
when is_binary(surface_id) and is_integer(index) and index >= 0 do
|
when is_binary(surface_id) and is_integer(index) and index >= 0 do
|
||||||
socket
|
socket
|
||||||
@@ -55,10 +59,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec current_surface_data(term(), term()) :: term()
|
||||||
def current_surface_data(socket, surface_id) when is_binary(surface_id) do
|
def current_surface_data(socket, surface_id) when is_binary(surface_id) do
|
||||||
Map.get(socket.assigns.chat_editor_surface_data, surface_id, %{})
|
Map.get(socket.assigns.chat_editor_surface_data, surface_id, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec set_action_error(term(), term(), term(), term()) :: term()
|
||||||
def set_action_error(socket, conversation_id, message, reload)
|
def set_action_error(socket, conversation_id, message, reload)
|
||||||
when is_binary(conversation_id) and is_binary(message) do
|
when is_binary(conversation_id) and is_binary(message) do
|
||||||
socket
|
socket
|
||||||
@@ -69,6 +75,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec clear_action_error(term(), term(), term()) :: term()
|
||||||
def clear_action_error(socket, conversation_id, reload) when is_binary(conversation_id) do
|
def clear_action_error(socket, conversation_id, reload) when is_binary(conversation_id) do
|
||||||
socket
|
socket
|
||||||
|> assign(
|
|> assign(
|
||||||
@@ -80,6 +87,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|
|
||||||
# ── Public API: messaging ──────────────────────────────────────────────────
|
# ── Public API: messaging ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@spec send_message(term(), term(), term()) :: term()
|
||||||
def send_message(socket, reload, append_output) do
|
def send_message(socket, reload, append_output) do
|
||||||
%{id: conversation_id} = socket.assigns.current_tab
|
%{id: conversation_id} = socket.assigns.current_tab
|
||||||
message = socket.assigns.chat_editor_inputs |> Map.get(conversation_id, "") |> String.trim()
|
message = socket.assigns.chat_editor_inputs |> Map.get(conversation_id, "") |> String.trim()
|
||||||
@@ -144,6 +152,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec abort_message(term(), term()) :: term()
|
||||||
def abort_message(socket, reload) do
|
def abort_message(socket, reload) do
|
||||||
%{id: conversation_id} = socket.assigns.current_tab
|
%{id: conversation_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -167,6 +176,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec note_tool_call(term(), term(), term(), term()) :: term()
|
||||||
def note_tool_call(socket, conversation_id, tool_call, reload)
|
def note_tool_call(socket, conversation_id, tool_call, reload)
|
||||||
when is_binary(conversation_id) and is_map(tool_call) do
|
when is_binary(conversation_id) and is_map(tool_call) do
|
||||||
update_request(
|
update_request(
|
||||||
@@ -189,6 +199,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec note_tool_result(term(), term(), term(), term()) :: term()
|
||||||
def note_tool_result(socket, conversation_id, name, reload)
|
def note_tool_result(socket, conversation_id, name, reload)
|
||||||
when is_binary(conversation_id) and is_binary(name) do
|
when is_binary(conversation_id) and is_binary(name) do
|
||||||
update_request(
|
update_request(
|
||||||
@@ -201,6 +212,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec note_streaming_content(term(), term(), term(), term()) :: term()
|
||||||
def note_streaming_content(socket, conversation_id, content, reload)
|
def note_streaming_content(socket, conversation_id, content, reload)
|
||||||
when is_binary(conversation_id) and is_binary(content) do
|
when is_binary(conversation_id) and is_binary(content) do
|
||||||
update_request(
|
update_request(
|
||||||
@@ -211,6 +223,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec finish_request(term(), term(), term(), term(), term()) :: term()
|
||||||
def finish_request(socket, ref, result, reload, append_output) when is_reference(ref) do
|
def finish_request(socket, ref, result, reload, append_output) when is_reference(ref) do
|
||||||
case Map.pop(socket.assigns.chat_editor_request_refs, ref) do
|
case Map.pop(socket.assigns.chat_editor_request_refs, ref) do
|
||||||
{nil, _remaining_refs} ->
|
{nil, _remaining_refs} ->
|
||||||
@@ -245,12 +258,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|
|
||||||
# ── HEEx-callable helpers ─────────────────────────────────────────────────
|
# ── HEEx-callable helpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@spec message_role_label(term()) :: term()
|
||||||
def message_role_label(:user), do: translated("chat.role.you")
|
def message_role_label(:user), do: translated("chat.role.you")
|
||||||
def message_role_label(_role), do: translated("chat.role.assistant")
|
def message_role_label(_role), do: translated("chat.role.assistant")
|
||||||
|
|
||||||
defdelegate tool_call_name(tool_call), to: ToolTracking
|
defdelegate tool_call_name(tool_call), to: ToolTracking
|
||||||
defdelegate tool_call_arguments(tool_call), to: ToolTracking
|
defdelegate tool_call_arguments(tool_call), to: ToolTracking
|
||||||
|
|
||||||
|
@spec tool_surface_type(term()) :: term()
|
||||||
def tool_surface_type(surface), do: Map.get(surface, :type, "json")
|
def tool_surface_type(surface), do: Map.get(surface, :type, "json")
|
||||||
|
|
||||||
def markdown_html(content) when is_binary(content) do
|
def markdown_html(content) when is_binary(content) do
|
||||||
@@ -264,8 +279,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
raw(html)
|
raw(html)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec markdown_html(term()) :: term()
|
||||||
def markdown_html(_content), do: ""
|
def markdown_html(_content), do: ""
|
||||||
|
|
||||||
|
@spec payload_json(term()) :: term()
|
||||||
def payload_json(nil), do: "{}"
|
def payload_json(nil), do: "{}"
|
||||||
def payload_json(payload) when is_map(payload), do: Jason.encode!(payload)
|
def payload_json(payload) when is_map(payload), do: Jason.encode!(payload)
|
||||||
|
|
||||||
@@ -280,15 +297,18 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|> Float.round(2)
|
|> Float.round(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec chart_width(term(), term()) :: term()
|
||||||
def chart_width(_max_value, _value), do: 0
|
def chart_width(_max_value, _value), do: 0
|
||||||
|
|
||||||
def truthy?(value) when value in [true, "true", 1, "1", "on"], do: true
|
def truthy?(value) when value in [true, "true", 1, "1", "on"], do: true
|
||||||
|
@spec truthy?(term()) :: term()
|
||||||
def truthy?(_value), do: false
|
def truthy?(_value), do: false
|
||||||
|
|
||||||
# ── HEEx components ───────────────────────────────────────────────────────
|
# ── HEEx components ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
attr :markers, :list, required: true
|
attr(:markers, :list, required: true)
|
||||||
|
|
||||||
|
@spec chat_tool_markers(term()) :: term()
|
||||||
def chat_tool_markers(assigns) do
|
def chat_tool_markers(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<%= if @markers != [] do %>
|
<%= if @markers != [] do %>
|
||||||
@@ -307,8 +327,9 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
attr :surface, :map, required: true
|
attr(:surface, :map, required: true)
|
||||||
|
|
||||||
|
@spec chat_surface(term()) :: term()
|
||||||
def chat_surface(assigns) do
|
def chat_surface(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<article class={["chat-inline-surface", "chat-inline-surface-#{@surface.type}"]} data-testid="chat-inline-surface">
|
<article class={["chat-inline-surface", "chat-inline-surface-#{@surface.type}"]} data-testid="chat-inline-surface">
|
||||||
@@ -548,7 +569,8 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
fn _match, src, alt -> external_image_link(src, alt) end
|
fn _match, src, alt -> external_image_link(src, alt) end
|
||||||
)
|
)
|
||||||
|
|
||||||
Regex.replace(~r/<img\b(?=[^>]*\bsrc="(https?:\/\/[^\"]+)")[^>]*\/?>/i, html, fn _match, src ->
|
Regex.replace(~r/<img\b(?=[^>]*\bsrc="(https?:\/\/[^\"]+)")[^>]*\/?>/i, html, fn _match,
|
||||||
|
src ->
|
||||||
external_image_link(src, src)
|
external_image_link(src, src)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -571,6 +593,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
|
|
||||||
defp format_error(reason), do: inspect(reason)
|
defp format_error(reason), do: inspect(reason)
|
||||||
|
|
||||||
|
@spec translated(term(), term()) :: term()
|
||||||
def translated(text, bindings \\ %{}),
|
def translated(text, bindings \\ %{}),
|
||||||
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
|
|||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.Desktop.ShellLive.ChatEditor.{ModelSelection, ToolSurfaces, ToolTracking}
|
alias BDS.Desktop.ShellLive.ChatEditor.{ModelSelection, ToolSurfaces, ToolTracking}
|
||||||
|
|
||||||
|
@spec build(term()) :: term()
|
||||||
def build(%{current_tab: %{type: :chat, id: conversation_id}} = assigns) do
|
def build(%{current_tab: %{type: :chat, id: conversation_id}} = assigns) do
|
||||||
case AI.get_chat_conversation(conversation_id) do
|
case AI.get_chat_conversation(conversation_id) do
|
||||||
nil ->
|
nil ->
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do
|
|||||||
|
|
||||||
import Phoenix.Component, only: [assign: 3]
|
import Phoenix.Component, only: [assign: 3]
|
||||||
|
|
||||||
|
@spec toggle_model_selector(term(), term()) :: term()
|
||||||
def toggle_model_selector(socket, reload) do
|
def toggle_model_selector(socket, reload) do
|
||||||
%{id: conversation_id} = socket.assigns.current_tab
|
%{id: conversation_id} = socket.assigns.current_tab
|
||||||
current = Map.get(socket.assigns.chat_model_selectors_open, conversation_id, false)
|
current = Map.get(socket.assigns.chat_model_selectors_open, conversation_id, false)
|
||||||
@@ -18,6 +19,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec set_model(term(), term(), term(), term()) :: term()
|
||||||
def set_model(socket, model_id, reload, append_output) do
|
def set_model(socket, model_id, reload, append_output) do
|
||||||
%{id: conversation_id} = socket.assigns.current_tab
|
%{id: conversation_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec group_available_models(term()) :: term()
|
||||||
def group_available_models(models) when is_list(models) do
|
def group_available_models(models) when is_list(models) do
|
||||||
models
|
models
|
||||||
|> Enum.group_by(&Map.get(&1, :provider, "other"))
|
|> Enum.group_by(&Map.get(&1, :provider, "other"))
|
||||||
@@ -54,6 +57,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do
|
|||||||
|> Enum.sort_by(&String.downcase(to_string(&1.label)))
|
|> Enum.sort_by(&String.downcase(to_string(&1.label)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec needs_api_key?(term()) :: term()
|
||||||
def needs_api_key?(true), do: false
|
def needs_api_key?(true), do: false
|
||||||
|
|
||||||
def needs_api_key?(false) do
|
def needs_api_key?(false) do
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
"render_tabs"
|
"render_tabs"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@spec render_tool?(term()) :: term()
|
||||||
def render_tool?(name) when is_binary(name), do: MapSet.member?(@render_tool_names, name)
|
def render_tool?(name) when is_binary(name), do: MapSet.member?(@render_tool_names, name)
|
||||||
|
@spec render_tool?(term()) :: term()
|
||||||
def render_tool?(_name), do: false
|
def render_tool?(_name), do: false
|
||||||
|
|
||||||
|
@spec build_render_surfaces(term(), term(), term()) :: term()
|
||||||
def build_render_surfaces(tool_calls, message_id, assigns) do
|
def build_render_surfaces(tool_calls, message_id, assigns) do
|
||||||
tool_calls
|
tool_calls
|
||||||
|> Enum.with_index()
|
|> Enum.with_index()
|
||||||
@@ -28,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build_render_surface(term(), term(), term()) :: term()
|
||||||
def build_render_surface(%{name: name, arguments: arguments}, surface_id, assigns) do
|
def build_render_surface(%{name: name, arguments: arguments}, surface_id, assigns) do
|
||||||
if MapSet.member?(@render_tool_names, name) do
|
if MapSet.member?(@render_tool_names, name) do
|
||||||
do_build_render_surface(name, arguments || %{}, surface_id, assigns)
|
do_build_render_surface(name, arguments || %{}, surface_id, assigns)
|
||||||
@@ -51,6 +55,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec normalize_tool_surface(term()) :: term()
|
||||||
def normalize_tool_surface(_content), do: nil
|
def normalize_tool_surface(_content), do: nil
|
||||||
|
|
||||||
defp do_build_render_surface("render_card", arguments, surface_id, _assigns) do
|
defp do_build_render_surface("render_card", arguments, surface_id, _assigns) do
|
||||||
@@ -150,7 +155,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
label: map_value(field, "label", key),
|
label: map_value(field, "label", key),
|
||||||
input_type: map_value(field, "inputType") || map_value(field, "input_type", "text"),
|
input_type: map_value(field, "inputType") || map_value(field, "input_type", "text"),
|
||||||
placeholder: map_value(field, "placeholder"),
|
placeholder: map_value(field, "placeholder"),
|
||||||
value: Map.get(stored_fields, key, map_value(field, "defaultValue") || map_value(field, "default_value")),
|
value:
|
||||||
|
Map.get(
|
||||||
|
stored_fields,
|
||||||
|
key,
|
||||||
|
map_value(field, "defaultValue") || map_value(field, "default_value")
|
||||||
|
),
|
||||||
options: decode_surface_options(map_value(field, "options", [])),
|
options: decode_surface_options(map_value(field, "options", [])),
|
||||||
required?: truthy?(map_value(field, "required", false))
|
required?: truthy?(map_value(field, "required", false))
|
||||||
}
|
}
|
||||||
@@ -161,8 +171,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
type: "form",
|
type: "form",
|
||||||
title: map_value(arguments, "title"),
|
title: map_value(arguments, "title"),
|
||||||
fields: fields,
|
fields: fields,
|
||||||
submit_label: map_value(arguments, "submitLabel") || map_value(arguments, "submit_label", translated("chat.stop")),
|
submit_label:
|
||||||
submit_action: map_value(arguments, "submitAction") || map_value(arguments, "submit_action", "submitForm")
|
map_value(arguments, "submitLabel") ||
|
||||||
|
map_value(arguments, "submit_label", translated("chat.stop")),
|
||||||
|
submit_action:
|
||||||
|
map_value(arguments, "submitAction") ||
|
||||||
|
map_value(arguments, "submit_action", "submitForm")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -181,7 +195,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
|> List.wrap()
|
|> List.wrap()
|
||||||
|> Enum.with_index()
|
|> Enum.with_index()
|
||||||
|> Enum.map(fn {content, content_index} ->
|
|> Enum.map(fn {content, content_index} ->
|
||||||
build_tab_surface(content, "#{surface_id}-tab-#{tab_index}-#{content_index}", assigns)
|
build_tab_surface(
|
||||||
|
content,
|
||||||
|
"#{surface_id}-tab-#{tab_index}-#{content_index}",
|
||||||
|
assigns
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
@@ -203,11 +221,21 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|
|||||||
type = map_value(content, "type", "text")
|
type = map_value(content, "type", "text")
|
||||||
|
|
||||||
case type do
|
case type do
|
||||||
render_type when render_type in ["card", "chart", "form", "list", "metric", "mindmap", "table", "tabs"] ->
|
render_type
|
||||||
do_build_render_surface("render_#{render_type}", Map.delete(content, "type"), surface_id, assigns)
|
when render_type in ["card", "chart", "form", "list", "metric", "mindmap", "table", "tabs"] ->
|
||||||
|
do_build_render_surface(
|
||||||
|
"render_#{render_type}",
|
||||||
|
Map.delete(content, "type"),
|
||||||
|
surface_id,
|
||||||
|
assigns
|
||||||
|
)
|
||||||
|
|
||||||
"text" ->
|
"text" ->
|
||||||
%{id: surface_id, type: "text", body: map_value(content, "body") || map_value(content, "text", "")}
|
%{
|
||||||
|
id: surface_id,
|
||||||
|
type: "text",
|
||||||
|
body: map_value(content, "body") || map_value(content, "text", "")
|
||||||
|
}
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
%{id: surface_id, type: "json", raw: content}
|
%{id: surface_id, type: "json", raw: content}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
|
|||||||
|
|
||||||
@tool_args_max_length 30
|
@tool_args_max_length 30
|
||||||
|
|
||||||
|
@spec tool_call_name(term()) :: term()
|
||||||
def tool_call_name(tool_call) when is_map(tool_call) do
|
def tool_call_name(tool_call) when is_map(tool_call) do
|
||||||
BDS.MapUtils.attr(tool_call, :name) || "tool"
|
BDS.MapUtils.attr(tool_call, :name) || "tool"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec tool_call_arguments(term()) :: term()
|
||||||
def tool_call_arguments(tool_call) when is_map(tool_call) do
|
def tool_call_arguments(tool_call) when is_map(tool_call) do
|
||||||
BDS.MapUtils.attr(tool_call, :arguments) || BDS.MapUtils.attr(tool_call, :args) || %{}
|
BDS.MapUtils.attr(tool_call, :arguments) || BDS.MapUtils.attr(tool_call, :args) || %{}
|
||||||
end
|
end
|
||||||
@@ -25,6 +27,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec normalize_tool_calls(term()) :: term()
|
||||||
def normalize_tool_calls(_tool_calls), do: []
|
def normalize_tool_calls(_tool_calls), do: []
|
||||||
|
|
||||||
def tool_arguments_preview(arguments) when is_map(arguments) do
|
def tool_arguments_preview(arguments) when is_map(arguments) do
|
||||||
@@ -33,6 +36,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
|
|||||||
|> Enum.join(", ")
|
|> Enum.join(", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec tool_arguments_preview(term()) :: term()
|
||||||
def tool_arguments_preview(_arguments), do: ""
|
def tool_arguments_preview(_arguments), do: ""
|
||||||
|
|
||||||
def mark_tool_call_completed(entry, tool_call_id) when is_binary(tool_call_id) do
|
def mark_tool_call_completed(entry, tool_call_id) when is_binary(tool_call_id) do
|
||||||
@@ -47,8 +51,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec mark_tool_call_completed(term(), term()) :: term()
|
||||||
def mark_tool_call_completed(entry, _tool_call_id), do: entry
|
def mark_tool_call_completed(entry, _tool_call_id), do: entry
|
||||||
|
|
||||||
|
@spec tool_markers_from_events(term()) :: term()
|
||||||
def tool_markers_from_events(nil), do: []
|
def tool_markers_from_events(nil), do: []
|
||||||
|
|
||||||
def tool_markers_from_events(%{tool_events: tool_events}) do
|
def tool_markers_from_events(%{tool_events: tool_events}) do
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
|
|
||||||
embed_templates("code_entity_editor_html/*")
|
embed_templates("code_entity_editor_html/*")
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
socket
|
socket
|
||||||
|> assign(:script_editor, build_script(socket.assigns))
|
|> assign(:script_editor, build_script(socket.assigns))
|
||||||
|> assign(:template_editor, build_template(socket.assigns))
|
|> assign(:template_editor, build_template(socket.assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_script(term(), term(), term()) :: term()
|
||||||
def update_script(socket, params, reload) do
|
def update_script(socket, params, reload) do
|
||||||
%{id: script_id} = socket.assigns.current_tab
|
%{id: script_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_script(term(), term(), term()) :: term()
|
||||||
def save_script(socket, reload, append_output) do
|
def save_script(socket, reload, append_output) do
|
||||||
%{id: script_id} = socket.assigns.current_tab
|
%{id: script_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -62,6 +65,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec check_script(term(), term(), term()) :: term()
|
||||||
def check_script(socket, reload, append_output) do
|
def check_script(socket, reload, append_output) do
|
||||||
%{id: script_id} = socket.assigns.current_tab
|
%{id: script_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -82,6 +86,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec run_script(term(), term(), term()) :: term()
|
||||||
def run_script(socket, reload, append_output) do
|
def run_script(socket, reload, append_output) do
|
||||||
%{id: script_id} = socket.assigns.current_tab
|
%{id: script_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -111,6 +116,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_script(term(), term(), term()) :: term()
|
||||||
def delete_script(socket, reload, append_output) do
|
def delete_script(socket, reload, append_output) do
|
||||||
%{id: script_id} = socket.assigns.current_tab
|
%{id: script_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_template(term(), term(), term()) :: term()
|
||||||
def update_template(socket, params, reload) do
|
def update_template(socket, params, reload) do
|
||||||
%{id: template_id} = socket.assigns.current_tab
|
%{id: template_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -139,6 +146,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_template(term(), term(), term()) :: term()
|
||||||
def save_template(socket, reload, append_output) do
|
def save_template(socket, reload, append_output) do
|
||||||
%{id: template_id} = socket.assigns.current_tab
|
%{id: template_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -169,6 +177,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec validate_template(term(), term(), term()) :: term()
|
||||||
def validate_template(socket, reload, append_output) do
|
def validate_template(socket, reload, append_output) do
|
||||||
%{id: template_id} = socket.assigns.current_tab
|
%{id: template_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -195,6 +204,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_template(term(), term(), term()) :: term()
|
||||||
def delete_template(socket, reload, append_output) do
|
def delete_template(socket, reload, append_output) do
|
||||||
%{id: template_id} = socket.assigns.current_tab
|
%{id: template_id} = socket.assigns.current_tab
|
||||||
|
|
||||||
@@ -211,6 +221,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build_script(term()) :: term()
|
||||||
def build_script(%{current_tab: %{type: :scripts, id: script_id}} = assigns) do
|
def build_script(%{current_tab: %{type: :scripts, id: script_id}} = assigns) do
|
||||||
case Scripts.get_script(script_id) do
|
case Scripts.get_script(script_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -236,6 +247,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
|
|
||||||
def build_script(_assigns), do: nil
|
def build_script(_assigns), do: nil
|
||||||
|
|
||||||
|
@spec build_template(term()) :: term()
|
||||||
def build_template(%{current_tab: %{type: :templates, id: template_id}} = assigns) do
|
def build_template(%{current_tab: %{type: :templates, id: template_id}} = assigns) do
|
||||||
case Templates.get_template(template_id) do
|
case Templates.get_template(template_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -259,9 +271,11 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|
|||||||
|
|
||||||
def build_template(_assigns), do: nil
|
def build_template(_assigns), do: nil
|
||||||
|
|
||||||
|
@spec translated(term(), term()) :: term()
|
||||||
def translated(text, bindings \\ %{}),
|
def translated(text, bindings \\ %{}),
|
||||||
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
|
@spec format_timestamp(term()) :: term()
|
||||||
def format_timestamp(nil), do: ""
|
def format_timestamp(nil), do: ""
|
||||||
def format_timestamp(timestamp), do: BDS.Persistence.timestamp_to_iso8601(timestamp)
|
def format_timestamp(timestamp), do: BDS.Persistence.timestamp_to_iso8601(timestamp)
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
total,
|
total,
|
||||||
detail,
|
detail,
|
||||||
reload
|
reload
|
||||||
), to: ProgressTracking
|
),
|
||||||
|
to: ProgressTracking
|
||||||
|
|
||||||
defdelegate finish_execution(socket, ref, result, reload, append_output), to: ProgressTracking
|
defdelegate finish_execution(socket, ref, result, reload, append_output), to: ProgressTracking
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
defdelegate clear_taxonomy_mapping(socket, params, reload), to: TaxonomyEditing
|
defdelegate clear_taxonomy_mapping(socket, params, reload), to: TaxonomyEditing
|
||||||
defdelegate analyze_taxonomy_ai(socket, reload, append_output), to: TaxonomyEditing
|
defdelegate analyze_taxonomy_ai(socket, reload, append_output), to: TaxonomyEditing
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
case socket.assigns[:current_tab] do
|
case socket.assigns[:current_tab] do
|
||||||
%{type: :import, id: definition_id} ->
|
%{type: :import, id: definition_id} ->
|
||||||
@@ -140,6 +142,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_section(term(), term(), term()) :: term()
|
||||||
def toggle_section(socket, section, reload) do
|
def toggle_section(socket, section, reload) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab,
|
with %{id: definition_id} <- socket.assigns.current_tab,
|
||||||
section_key
|
section_key
|
||||||
@@ -171,6 +174,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_model_selector(term(), term()) :: term()
|
||||||
def toggle_model_selector(socket, reload) do
|
def toggle_model_selector(socket, reload) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab do
|
with %{id: definition_id} <- socket.assigns.current_tab do
|
||||||
current = Map.get(socket.assigns.import_editor_model_selectors_open, definition_id, false)
|
current = Map.get(socket.assigns.import_editor_model_selectors_open, definition_id, false)
|
||||||
@@ -186,6 +190,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_ai_model(term(), term(), term()) :: term()
|
||||||
def select_ai_model(socket, model_id, reload) do
|
def select_ai_model(socket, model_id, reload) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab do
|
with %{id: definition_id} <- socket.assigns.current_tab do
|
||||||
socket
|
socket
|
||||||
@@ -205,6 +210,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
|
|
||||||
attr(:import_editor, :map, required: true)
|
attr(:import_editor, :map, required: true)
|
||||||
|
|
||||||
|
@spec import_editor(term()) :: term()
|
||||||
def import_editor(assigns) do
|
def import_editor(assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
assigns
|
assigns
|
||||||
@@ -547,6 +553,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:expanded, :boolean, required: true)
|
attr(:expanded, :boolean, required: true)
|
||||||
attr(:section, :string, required: true)
|
attr(:section, :string, required: true)
|
||||||
|
|
||||||
|
@spec conflict_section(term()) :: term()
|
||||||
def conflict_section(assigns) do
|
def conflict_section(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<section class="import-detail-section conflicts-section">
|
<section class="import-detail-section conflicts-section">
|
||||||
@@ -597,6 +604,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:section, :string, required: true)
|
attr(:section, :string, required: true)
|
||||||
attr(:show_type, :boolean, default: false)
|
attr(:show_type, :boolean, default: false)
|
||||||
|
|
||||||
|
@spec post_detail_section(term()) :: term()
|
||||||
def post_detail_section(assigns) do
|
def post_detail_section(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<section class="import-detail-section">
|
<section class="import-detail-section">
|
||||||
@@ -646,6 +654,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:expanded, :boolean, required: true)
|
attr(:expanded, :boolean, required: true)
|
||||||
attr(:section, :string, required: true)
|
attr(:section, :string, required: true)
|
||||||
|
|
||||||
|
@spec media_detail_section(term()) :: term()
|
||||||
def media_detail_section(assigns) do
|
def media_detail_section(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<section class="import-detail-section">
|
<section class="import-detail-section">
|
||||||
@@ -685,6 +694,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:label, :string, required: true)
|
attr(:label, :string, required: true)
|
||||||
attr(:stats, :map, required: true)
|
attr(:stats, :map, required: true)
|
||||||
|
|
||||||
|
@spec stat_card(term()) :: term()
|
||||||
def stat_card(assigns) do
|
def stat_card(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="import-stat-card">
|
<div class="import-stat-card">
|
||||||
@@ -703,6 +713,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:label, :string, required: true)
|
attr(:label, :string, required: true)
|
||||||
attr(:stats, :map, required: true)
|
attr(:stats, :map, required: true)
|
||||||
|
|
||||||
|
@spec other_stat_card(term()) :: term()
|
||||||
def other_stat_card(assigns) do
|
def other_stat_card(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="import-stat-card import-stat-card-other">
|
<div class="import-stat-card import-stat-card-other">
|
||||||
@@ -720,6 +731,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:label, :string, required: true)
|
attr(:label, :string, required: true)
|
||||||
attr(:stats, :map, required: true)
|
attr(:stats, :map, required: true)
|
||||||
|
|
||||||
|
@spec media_stat_card(term()) :: term()
|
||||||
def media_stat_card(assigns) do
|
def media_stat_card(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="import-stat-card">
|
<div class="import-stat-card">
|
||||||
@@ -739,6 +751,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:label, :string, required: true)
|
attr(:label, :string, required: true)
|
||||||
attr(:stats, :map, required: true)
|
attr(:stats, :map, required: true)
|
||||||
|
|
||||||
|
@spec taxonomy_stat_card(term()) :: term()
|
||||||
def taxonomy_stat_card(assigns) do
|
def taxonomy_stat_card(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="import-stat-card">
|
<div class="import-stat-card">
|
||||||
@@ -759,6 +772,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
attr(:edit, :map, default: nil)
|
attr(:edit, :map, default: nil)
|
||||||
attr(:type, :string, required: true)
|
attr(:type, :string, required: true)
|
||||||
|
|
||||||
|
@spec taxonomy_group(term()) :: term()
|
||||||
def taxonomy_group(assigns) do
|
def taxonomy_group(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="taxonomy-group">
|
<div class="taxonomy-group">
|
||||||
|
|||||||
@@ -4,20 +4,27 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
alias BDS.{ImportAnalysis, ImportDefinitions, Metadata}
|
alias BDS.{ImportAnalysis, ImportDefinitions, Metadata}
|
||||||
alias BDS.Desktop.{FilePicker, FolderPicker, ShellData}
|
alias BDS.Desktop.{FilePicker, FolderPicker, ShellData}
|
||||||
|
|
||||||
|
@spec change_definition(term(), term(), term()) :: term()
|
||||||
def change_definition(socket, params, reload) do
|
def change_definition(socket, params, reload) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab,
|
with %{id: definition_id} <- socket.assigns.current_tab,
|
||||||
{:ok, _definition} <- ImportDefinitions.update_definition(definition_id, %{name: Map.get(params, "name", "")}) do
|
{:ok, _definition} <-
|
||||||
|
ImportDefinitions.update_definition(definition_id, %{name: Map.get(params, "name", "")}) do
|
||||||
reload.(socket, socket.assigns.workbench)
|
reload.(socket, socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
_other -> reload.(socket, socket.assigns.workbench)
|
_other -> reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_uploads_folder(term(), term(), term()) :: term()
|
||||||
def select_uploads_folder(socket, reload, append_output) do
|
def select_uploads_folder(socket, reload, append_output) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab do
|
with %{id: definition_id} <- socket.assigns.current_tab do
|
||||||
case FolderPicker.choose_directory(translated("importAnalysis.uploadsFolder")) do
|
case FolderPicker.choose_directory(translated("importAnalysis.uploadsFolder")) do
|
||||||
{:ok, uploads_folder_path} ->
|
{:ok, uploads_folder_path} ->
|
||||||
{:ok, _definition} = ImportDefinitions.update_definition(definition_id, %{uploads_folder_path: uploads_folder_path})
|
{:ok, _definition} =
|
||||||
|
ImportDefinitions.update_definition(definition_id, %{
|
||||||
|
uploads_folder_path: uploads_folder_path
|
||||||
|
})
|
||||||
|
|
||||||
reload.(socket, socket.assigns.workbench)
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
:cancel ->
|
:cancel ->
|
||||||
@@ -33,6 +40,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_and_analyze(term(), term(), term()) :: term()
|
||||||
def select_and_analyze(socket, reload, append_output) do
|
def select_and_analyze(socket, reload, append_output) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab,
|
with %{id: definition_id} <- socket.assigns.current_tab,
|
||||||
%{} = definition <- ImportDefinitions.get_definition(definition_id) do
|
%{} = definition <- ImportDefinitions.get_definition(definition_id) do
|
||||||
@@ -50,9 +58,15 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
|
|
||||||
task =
|
task =
|
||||||
Task.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn ->
|
Task.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn ->
|
||||||
ImportAnalysis.analyze_wxr(project_id, wxr_file_path, definition.uploads_folder_path,
|
ImportAnalysis.analyze_wxr(
|
||||||
|
project_id,
|
||||||
|
wxr_file_path,
|
||||||
|
definition.uploads_folder_path,
|
||||||
on_progress: fn step, detail ->
|
on_progress: fn step, detail ->
|
||||||
send(live_view_pid, {:import_analysis_progress, definition_id, translate_phase(step), detail})
|
send(
|
||||||
|
live_view_pid,
|
||||||
|
{:import_analysis_progress, definition_id, translate_phase(step), detail}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
@@ -70,8 +84,14 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
ref: task.ref
|
ref: task.ref
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|> Phoenix.Component.assign(:import_editor_analysis_task_refs, Map.put(socket.assigns.import_editor_analysis_task_refs, task.ref, definition_id))
|
|> Phoenix.Component.assign(
|
||||||
|> Phoenix.Component.assign(:import_editor_execution_states, Map.delete(socket.assigns.import_editor_execution_states, definition_id))
|
:import_editor_analysis_task_refs,
|
||||||
|
Map.put(socket.assigns.import_editor_analysis_task_refs, task.ref, definition_id)
|
||||||
|
)
|
||||||
|
|> Phoenix.Component.assign(
|
||||||
|
:import_editor_execution_states,
|
||||||
|
Map.delete(socket.assigns.import_editor_execution_states, definition_id)
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
:cancel ->
|
:cancel ->
|
||||||
@@ -87,32 +107,50 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec note_analysis_progress(term(), term(), term(), term(), term()) :: term()
|
||||||
def note_analysis_progress(socket, definition_id, step, detail, reload) do
|
def note_analysis_progress(socket, definition_id, step, detail, reload) do
|
||||||
socket
|
socket
|
||||||
|> Phoenix.Component.assign(
|
|> Phoenix.Component.assign(
|
||||||
:import_editor_analysis_states,
|
:import_editor_analysis_states,
|
||||||
Map.update(socket.assigns.import_editor_analysis_states, definition_id, default_analysis_state(), fn state ->
|
Map.update(
|
||||||
state
|
socket.assigns.import_editor_analysis_states,
|
||||||
|> Map.put(:loading, true)
|
definition_id,
|
||||||
|> Map.put(:step, step)
|
default_analysis_state(),
|
||||||
|> Map.put(:detail, detail)
|
fn state ->
|
||||||
end)
|
state
|
||||||
|
|> Map.put(:loading, true)
|
||||||
|
|> Map.put(:step, step)
|
||||||
|
|> Map.put(:detail, detail)
|
||||||
|
end
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec finish_analysis(term(), term(), term(), term(), term()) :: term()
|
||||||
def finish_analysis(socket, ref, result, reload, append_output) do
|
def finish_analysis(socket, ref, result, reload, append_output) do
|
||||||
case Map.get(socket.assigns.import_editor_analysis_task_refs, ref) do
|
case Map.get(socket.assigns.import_editor_analysis_task_refs, ref) do
|
||||||
nil ->
|
nil ->
|
||||||
socket
|
socket
|
||||||
|
|
||||||
definition_id ->
|
definition_id ->
|
||||||
analysis_state = Map.get(socket.assigns.import_editor_analysis_states, definition_id, default_analysis_state())
|
analysis_state =
|
||||||
|
Map.get(
|
||||||
|
socket.assigns.import_editor_analysis_states,
|
||||||
|
definition_id,
|
||||||
|
default_analysis_state()
|
||||||
|
)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> Phoenix.Component.assign(:import_editor_analysis_task_refs, Map.delete(socket.assigns.import_editor_analysis_task_refs, ref))
|
|> Phoenix.Component.assign(
|
||||||
|> Phoenix.Component.assign(:import_editor_analysis_states, Map.delete(socket.assigns.import_editor_analysis_states, definition_id))
|
:import_editor_analysis_task_refs,
|
||||||
|
Map.delete(socket.assigns.import_editor_analysis_task_refs, ref)
|
||||||
|
)
|
||||||
|
|> Phoenix.Component.assign(
|
||||||
|
:import_editor_analysis_states,
|
||||||
|
Map.delete(socket.assigns.import_editor_analysis_states, definition_id)
|
||||||
|
)
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:ok, report} ->
|
{:ok, report} ->
|
||||||
@@ -146,6 +184,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec handle_analysis_task_down(term(), term(), term(), term(), term()) :: term()
|
||||||
def handle_analysis_task_down(socket, ref, message, reload, append_output) do
|
def handle_analysis_task_down(socket, ref, message, reload, append_output) do
|
||||||
case Map.get(socket.assigns.import_editor_analysis_task_refs, ref) do
|
case Map.get(socket.assigns.import_editor_analysis_task_refs, ref) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -153,13 +192,20 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
|
|
||||||
definition_id ->
|
definition_id ->
|
||||||
socket
|
socket
|
||||||
|> Phoenix.Component.assign(:import_editor_analysis_task_refs, Map.delete(socket.assigns.import_editor_analysis_task_refs, ref))
|
|> Phoenix.Component.assign(
|
||||||
|> Phoenix.Component.assign(:import_editor_analysis_states, Map.delete(socket.assigns.import_editor_analysis_states, definition_id))
|
:import_editor_analysis_task_refs,
|
||||||
|
Map.delete(socket.assigns.import_editor_analysis_task_refs, ref)
|
||||||
|
)
|
||||||
|
|> Phoenix.Component.assign(
|
||||||
|
:import_editor_analysis_states,
|
||||||
|
Map.delete(socket.assigns.import_editor_analysis_states, definition_id)
|
||||||
|
)
|
||||||
|> append_output.(translated("activity.import"), message, nil, "error")
|
|> append_output.(translated("activity.import"), message, nil, "error")
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec importable_counts(term()) :: term()
|
||||||
def importable_counts(nil), do: %{total: 0, tags: 0, posts: 0, media: 0, pages: 0}
|
def importable_counts(nil), do: %{total: 0, tags: 0, posts: 0, media: 0, pages: 0}
|
||||||
|
|
||||||
def importable_counts(report) do
|
def importable_counts(report) do
|
||||||
@@ -171,25 +217,37 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
pages = importable_entity_count(Map.get(report.items, :pages, []))
|
pages = importable_entity_count(Map.get(report.items, :pages, []))
|
||||||
media = importable_entity_count(Map.get(report.items, :media, []))
|
media = importable_entity_count(Map.get(report.items, :media, []))
|
||||||
|
|
||||||
%{total: tag_count + posts + pages + media, tags: tag_count, posts: posts, media: media, pages: pages}
|
%{
|
||||||
|
total: tag_count + posts + pages + media,
|
||||||
|
tags: tag_count,
|
||||||
|
posts: posts,
|
||||||
|
media: media,
|
||||||
|
pages: pages
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec importable_entity_count(term()) :: term()
|
||||||
def importable_entity_count(items) do
|
def importable_entity_count(items) do
|
||||||
Enum.count(items || [], fn item ->
|
Enum.count(items || [], fn item ->
|
||||||
item.status == "new" or (item.status == "conflict" and Map.get(item, :resolution, "ignore") not in ["ignore", "skip"])
|
item.status == "new" or
|
||||||
|
(item.status == "conflict" and
|
||||||
|
Map.get(item, :resolution, "ignore") not in ["ignore", "skip"])
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec detail_items(term(), term()) :: term()
|
||||||
def detail_items(nil, _bucket), do: []
|
def detail_items(nil, _bucket), do: []
|
||||||
|
|
||||||
def detail_items(report, bucket) do
|
def detail_items(report, bucket) do
|
||||||
get_in(report, [:details, bucket]) || get_in(report, [:items, bucket]) || []
|
get_in(report, [:details, bucket]) || get_in(report, [:items, bucket]) || []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec default_analysis_state() :: term()
|
||||||
def default_analysis_state do
|
def default_analysis_state do
|
||||||
%{loading: false, step: nil, detail: nil, file_path: nil, ref: nil}
|
%{loading: false, step: nil, detail: nil, file_path: nil, ref: nil}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec default_sections() :: term()
|
||||||
def default_sections do
|
def default_sections do
|
||||||
%{
|
%{
|
||||||
post_conflicts: true,
|
post_conflicts: true,
|
||||||
@@ -203,18 +261,22 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec default_author(term()) :: term()
|
||||||
def default_author(project_id) do
|
def default_author(project_id) do
|
||||||
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
||||||
Map.get(metadata, :default_author)
|
Map.get(metadata, :default_author)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec suggested_definition_name(term()) :: term()
|
||||||
def suggested_definition_name(report) do
|
def suggested_definition_name(report) do
|
||||||
get_in(report, [:site_info, :url]) || get_in(report, [:site_info, :title])
|
get_in(report, [:site_info, :url]) || get_in(report, [:site_info, :title])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec maybe_put(term(), term(), term()) :: term()
|
||||||
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)
|
||||||
|
|
||||||
|
@spec allow_repo_sandbox(term()) :: term()
|
||||||
def allow_repo_sandbox(pid) when is_pid(pid) do
|
def allow_repo_sandbox(pid) when is_pid(pid) do
|
||||||
if Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) do
|
if Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) do
|
||||||
try do
|
try do
|
||||||
@@ -241,8 +303,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translate_phase(term()) :: term()
|
||||||
def translate_phase(other), do: other
|
def translate_phase(other), do: other
|
||||||
|
|
||||||
defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
defp present?(value), do: value not in [nil, ""]
|
defp present?(value), do: value not in [nil, ""]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,18 +3,27 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ConflictResolution do
|
|||||||
|
|
||||||
alias BDS.ImportDefinitions
|
alias BDS.ImportDefinitions
|
||||||
|
|
||||||
def change_conflict_resolution(socket, %{"item_type" => item_type, "item_name" => item_name, "resolution" => resolution}, reload) do
|
@spec change_conflict_resolution(term(), term(), term()) :: term()
|
||||||
|
def change_conflict_resolution(
|
||||||
|
socket,
|
||||||
|
%{"item_type" => item_type, "item_name" => item_name, "resolution" => resolution},
|
||||||
|
reload
|
||||||
|
) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab,
|
with %{id: definition_id} <- socket.assigns.current_tab,
|
||||||
%{} = definition <- ImportDefinitions.get_definition(definition_id),
|
%{} = definition <- ImportDefinitions.get_definition(definition_id),
|
||||||
%{} = report <- ImportDefinitions.decode_analysis_result(definition),
|
%{} = report <- ImportDefinitions.decode_analysis_result(definition),
|
||||||
updated_report <- update_conflict_resolution(report, item_type, item_name, resolution),
|
updated_report <- update_conflict_resolution(report, item_type, item_name, resolution),
|
||||||
{:ok, _definition} <- ImportDefinitions.update_definition(definition_id, %{last_analysis_result: updated_report}) do
|
{:ok, _definition} <-
|
||||||
|
ImportDefinitions.update_definition(definition_id, %{
|
||||||
|
last_analysis_result: updated_report
|
||||||
|
}) do
|
||||||
reload.(socket, socket.assigns.workbench)
|
reload.(socket, socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
_other -> reload.(socket, socket.assigns.workbench)
|
_other -> reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_conflict_resolution(term(), term(), term(), term()) :: term()
|
||||||
def update_conflict_resolution(report, item_type, item_name, resolution) do
|
def update_conflict_resolution(report, item_type, item_name, resolution) do
|
||||||
report
|
report
|
||||||
|> update_in([:conflicts], fn conflicts ->
|
|> update_in([:conflicts], fn conflicts ->
|
||||||
@@ -30,10 +39,15 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ConflictResolution do
|
|||||||
|> update_in([:details], &update_conflict_bucket(&1, item_type, item_name, resolution))
|
|> update_in([:details], &update_conflict_bucket(&1, item_type, item_name, resolution))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_conflict_bucket(term(), term(), term(), term()) :: term()
|
||||||
def update_conflict_bucket(nil, _item_type, _item_name, _resolution), do: nil
|
def update_conflict_bucket(nil, _item_type, _item_name, _resolution), do: nil
|
||||||
|
|
||||||
def update_conflict_bucket(buckets, item_type, item_name, resolution) do
|
def update_conflict_bucket(buckets, item_type, item_name, resolution) do
|
||||||
bucket_key = if(item_type == "page", do: :pages, else: if(item_type == "media", do: :media, else: :posts))
|
bucket_key =
|
||||||
|
if(item_type == "page",
|
||||||
|
do: :pages,
|
||||||
|
else: if(item_type == "media", do: :media, else: :posts)
|
||||||
|
)
|
||||||
|
|
||||||
update_in(buckets, [bucket_key], fn items ->
|
update_in(buckets, [bucket_key], fn items ->
|
||||||
Enum.map(items || [], fn item ->
|
Enum.map(items || [], fn item ->
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.Desktop.ShellLive.ImportEditor.AnalysisState
|
alias BDS.Desktop.ShellLive.ImportEditor.AnalysisState
|
||||||
|
|
||||||
|
@spec execute_import(term(), term(), term()) :: term()
|
||||||
def execute_import(socket, reload, _append_output) do
|
def execute_import(socket, reload, _append_output) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab,
|
with %{id: definition_id} <- socket.assigns.current_tab,
|
||||||
%{} = definition <- ImportDefinitions.get_definition(definition_id),
|
%{} = definition <- ImportDefinitions.get_definition(definition_id),
|
||||||
@@ -24,7 +25,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
uploads_folder_path: definition.uploads_folder_path,
|
uploads_folder_path: definition.uploads_folder_path,
|
||||||
default_author: default_author,
|
default_author: default_author,
|
||||||
on_progress: fn phase, current, total, detail ->
|
on_progress: fn phase, current, total, detail ->
|
||||||
send(live_view_pid, {:import_execution_progress, definition_id, phase, current, total, detail})
|
send(
|
||||||
|
live_view_pid,
|
||||||
|
{:import_execution_progress, definition_id, phase, current, total, detail}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
@@ -50,7 +54,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
ref: task.ref
|
ref: task.ref
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|> Phoenix.Component.assign(:import_editor_execution_task_refs, Map.put(socket.assigns.import_editor_execution_task_refs, task.ref, definition_id))
|
|> Phoenix.Component.assign(
|
||||||
|
:import_editor_execution_task_refs,
|
||||||
|
Map.put(socket.assigns.import_editor_execution_task_refs, task.ref, definition_id)
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -58,6 +65,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec note_execution_progress(term(), term(), term(), term(), term(), term(), term()) :: term()
|
||||||
def note_execution_progress(socket, definition_id, phase, current, total, detail, reload) do
|
def note_execution_progress(socket, definition_id, phase, current, total, detail, reload) do
|
||||||
{detail_text, eta} = decompose_progress_detail(detail)
|
{detail_text, eta} = decompose_progress_detail(detail)
|
||||||
translated_phase = translate_execution_phase(phase)
|
translated_phase = translate_execution_phase(phase)
|
||||||
@@ -65,30 +73,44 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
socket
|
socket
|
||||||
|> Phoenix.Component.assign(
|
|> Phoenix.Component.assign(
|
||||||
:import_editor_execution_states,
|
:import_editor_execution_states,
|
||||||
Map.update(socket.assigns.import_editor_execution_states, definition_id, default_execution_state(), fn state ->
|
Map.update(
|
||||||
state
|
socket.assigns.import_editor_execution_states,
|
||||||
|> Map.put(:is_executing, true)
|
definition_id,
|
||||||
|> Map.put(:phase, translated_phase)
|
default_execution_state(),
|
||||||
|> Map.put(:current, current)
|
fn state ->
|
||||||
|> Map.put(:total, total)
|
state
|
||||||
|> Map.put(:detail, detail_text)
|
|> Map.put(:is_executing, true)
|
||||||
|> Map.put(:eta, eta)
|
|> Map.put(:phase, translated_phase)
|
||||||
end)
|
|> Map.put(:current, current)
|
||||||
|
|> Map.put(:total, total)
|
||||||
|
|> Map.put(:detail, detail_text)
|
||||||
|
|> Map.put(:eta, eta)
|
||||||
|
end
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec finish_execution(term(), term(), term(), term(), term()) :: term()
|
||||||
def finish_execution(socket, ref, result, reload, append_output) do
|
def finish_execution(socket, ref, result, reload, append_output) do
|
||||||
case Map.get(socket.assigns.import_editor_execution_task_refs, ref) do
|
case Map.get(socket.assigns.import_editor_execution_task_refs, ref) do
|
||||||
nil ->
|
nil ->
|
||||||
socket
|
socket
|
||||||
|
|
||||||
definition_id ->
|
definition_id ->
|
||||||
previous_state = Map.get(socket.assigns.import_editor_execution_states, definition_id, default_execution_state())
|
previous_state =
|
||||||
|
Map.get(
|
||||||
|
socket.assigns.import_editor_execution_states,
|
||||||
|
definition_id,
|
||||||
|
default_execution_state()
|
||||||
|
)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> Phoenix.Component.assign(:import_editor_execution_task_refs, Map.delete(socket.assigns.import_editor_execution_task_refs, ref))
|
|> Phoenix.Component.assign(
|
||||||
|
:import_editor_execution_task_refs,
|
||||||
|
Map.delete(socket.assigns.import_editor_execution_task_refs, ref)
|
||||||
|
)
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:ok, execution_result} ->
|
{:ok, execution_result} ->
|
||||||
@@ -106,7 +128,12 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
ref: nil
|
ref: nil
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|> append_output.(translated("activity.import"), translated("importAnalysis.importComplete", %{count: previous_state.count}), nil, "info")
|
|> append_output.(
|
||||||
|
translated("activity.import"),
|
||||||
|
translated("importAnalysis.importComplete", %{count: previous_state.count}),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
{:error, %{message: message}} ->
|
{:error, %{message: message}} ->
|
||||||
@@ -144,7 +171,9 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_task_down(socket, kind, ref, reason, reload, append_output) when reason not in [:normal, :shutdown] do
|
@spec handle_task_down(term(), term(), term(), term(), term(), term()) :: term()
|
||||||
|
def handle_task_down(socket, kind, ref, reason, reload, append_output)
|
||||||
|
when reason not in [:normal, :shutdown] do
|
||||||
message = inspect(reason)
|
message = inspect(reason)
|
||||||
|
|
||||||
case kind do
|
case kind do
|
||||||
@@ -157,10 +186,18 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
socket
|
socket
|
||||||
|
|
||||||
definition_id ->
|
definition_id ->
|
||||||
previous_state = Map.get(socket.assigns.import_editor_execution_states, definition_id, default_execution_state())
|
previous_state =
|
||||||
|
Map.get(
|
||||||
|
socket.assigns.import_editor_execution_states,
|
||||||
|
definition_id,
|
||||||
|
default_execution_state()
|
||||||
|
)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> Phoenix.Component.assign(:import_editor_execution_task_refs, Map.delete(socket.assigns.import_editor_execution_task_refs, ref))
|
|> Phoenix.Component.assign(
|
||||||
|
:import_editor_execution_task_refs,
|
||||||
|
Map.delete(socket.assigns.import_editor_execution_task_refs, ref)
|
||||||
|
)
|
||||||
|> Phoenix.Component.assign(
|
|> Phoenix.Component.assign(
|
||||||
:import_editor_execution_states,
|
:import_editor_execution_states,
|
||||||
Map.put(socket.assigns.import_editor_execution_states, definition_id, %{
|
Map.put(socket.assigns.import_editor_execution_states, definition_id, %{
|
||||||
@@ -177,8 +214,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec handle_task_down(term(), term(), term(), term(), term(), term()) :: term()
|
||||||
def handle_task_down(socket, _kind, _ref, _reason, _reload, _append_output), do: socket
|
def handle_task_down(socket, _kind, _ref, _reason, _reload, _append_output), do: socket
|
||||||
|
|
||||||
|
@spec default_execution_state() :: term()
|
||||||
def default_execution_state do
|
def default_execution_state do
|
||||||
%{
|
%{
|
||||||
is_executing: false,
|
is_executing: false,
|
||||||
@@ -195,6 +234,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec execution_progress_width(term()) :: term()
|
||||||
def execution_progress_width(state) do
|
def execution_progress_width(state) do
|
||||||
current = Map.get(state, :current, 0)
|
current = Map.get(state, :current, 0)
|
||||||
total = Map.get(state, :total, 0)
|
total = Map.get(state, :total, 0)
|
||||||
@@ -205,25 +245,36 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec decompose_progress_detail(term()) :: term()
|
||||||
def decompose_progress_detail(%{detail: detail, eta: eta}), do: {to_string_or_nil(detail), eta}
|
def decompose_progress_detail(%{detail: detail, eta: eta}), do: {to_string_or_nil(detail), eta}
|
||||||
def decompose_progress_detail(detail) when is_binary(detail) or is_nil(detail), do: {detail, nil}
|
|
||||||
|
def decompose_progress_detail(detail) when is_binary(detail) or is_nil(detail),
|
||||||
|
do: {detail, nil}
|
||||||
|
|
||||||
def decompose_progress_detail(detail), do: {to_string_or_nil(detail), nil}
|
def decompose_progress_detail(detail), do: {to_string_or_nil(detail), nil}
|
||||||
|
|
||||||
|
@spec to_string_or_nil(term()) :: term()
|
||||||
def to_string_or_nil(nil), do: nil
|
def to_string_or_nil(nil), do: nil
|
||||||
def to_string_or_nil(value) when is_binary(value), do: value
|
def to_string_or_nil(value) when is_binary(value), do: value
|
||||||
def to_string_or_nil(value), do: inspect(value)
|
def to_string_or_nil(value), do: inspect(value)
|
||||||
|
|
||||||
|
@spec format_eta(term()) :: term()
|
||||||
def format_eta(nil), do: nil
|
def format_eta(nil), do: nil
|
||||||
|
|
||||||
def format_eta(ms) when is_integer(ms) and ms >= 0 do
|
def format_eta(ms) when is_integer(ms) and ms >= 0 do
|
||||||
seconds = div(ms, 1000)
|
seconds = div(ms, 1000)
|
||||||
|
|
||||||
if seconds < 60 do
|
if seconds < 60 do
|
||||||
translated("importAnalysis.eta", %{value: translated("importAnalysis.etaSeconds", %{count: seconds})})
|
translated("importAnalysis.eta", %{
|
||||||
|
value: translated("importAnalysis.etaSeconds", %{count: seconds})
|
||||||
|
})
|
||||||
else
|
else
|
||||||
m = div(seconds, 60)
|
m = div(seconds, 60)
|
||||||
s = rem(seconds, 60)
|
s = rem(seconds, 60)
|
||||||
translated("importAnalysis.eta", %{value: translated("importAnalysis.etaMinutes", %{minutes: m, seconds: s})})
|
|
||||||
|
translated("importAnalysis.eta", %{
|
||||||
|
value: translated("importAnalysis.etaMinutes", %{minutes: m, seconds: s})
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -240,7 +291,9 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translate_execution_phase(term()) :: term()
|
||||||
def translate_execution_phase(other), do: other
|
def translate_execution_phase(other), do: other
|
||||||
|
|
||||||
defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
alias BDS.{AI, ImportDefinitions, Metadata, Tags}
|
alias BDS.{AI, ImportDefinitions, Metadata, Tags}
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
@spec start_taxonomy_edit(term(), term(), term()) :: term()
|
||||||
def start_taxonomy_edit(
|
def start_taxonomy_edit(
|
||||||
socket,
|
socket,
|
||||||
%{"type" => type, "name" => name, "mapped_to" => mapped_to},
|
%{"type" => type, "name" => name, "mapped_to" => mapped_to},
|
||||||
@@ -25,6 +26,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec cancel_taxonomy_edit(term(), term()) :: term()
|
||||||
def cancel_taxonomy_edit(socket, reload) do
|
def cancel_taxonomy_edit(socket, reload) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab do
|
with %{id: definition_id} <- socket.assigns.current_tab do
|
||||||
socket
|
socket
|
||||||
@@ -38,6 +40,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_taxonomy_edit(term(), term(), term()) :: term()
|
||||||
def save_taxonomy_edit(
|
def save_taxonomy_edit(
|
||||||
socket,
|
socket,
|
||||||
%{"type" => type, "name" => name, "mapped_to" => mapped_to},
|
%{"type" => type, "name" => name, "mapped_to" => mapped_to},
|
||||||
@@ -68,10 +71,12 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec clear_taxonomy_mapping(term(), term(), term()) :: term()
|
||||||
def clear_taxonomy_mapping(socket, %{"type" => type, "name" => name}, reload) do
|
def clear_taxonomy_mapping(socket, %{"type" => type, "name" => name}, reload) do
|
||||||
save_taxonomy_edit(socket, %{"type" => type, "name" => name, "mapped_to" => ""}, reload)
|
save_taxonomy_edit(socket, %{"type" => type, "name" => name, "mapped_to" => ""}, reload)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec analyze_taxonomy_ai(term(), term(), term()) :: term()
|
||||||
def analyze_taxonomy_ai(socket, reload, append_output) do
|
def analyze_taxonomy_ai(socket, reload, append_output) do
|
||||||
with %{id: definition_id} <- socket.assigns.current_tab,
|
with %{id: definition_id} <- socket.assigns.current_tab,
|
||||||
%{} = definition <- ImportDefinitions.get_definition(definition_id),
|
%{} = definition <- ImportDefinitions.get_definition(definition_id),
|
||||||
@@ -142,6 +147,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_taxonomy_mapping(term(), term(), term(), term()) :: term()
|
||||||
def update_taxonomy_mapping(report, type, name, mapped_to) do
|
def update_taxonomy_mapping(report, type, name, mapped_to) do
|
||||||
bucket_key = if(type == "categories", do: :categories, else: :tags)
|
bucket_key = if(type == "categories", do: :categories, else: :tags)
|
||||||
normalized_value = mapped_to |> to_string() |> String.trim() |> blank_to_nil()
|
normalized_value = mapped_to |> to_string() |> String.trim() |> blank_to_nil()
|
||||||
@@ -164,6 +170,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec rebuild_taxonomy_stats(term()) :: term()
|
||||||
def rebuild_taxonomy_stats(items) do
|
def rebuild_taxonomy_stats(items) do
|
||||||
%{
|
%{
|
||||||
existing_count: Enum.count(items, & &1.exists_in_project),
|
existing_count: Enum.count(items, & &1.exists_in_project),
|
||||||
@@ -172,9 +179,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec stat_key(term()) :: term()
|
||||||
def stat_key(:categories), do: :category_stats
|
def stat_key(:categories), do: :category_stats
|
||||||
def stat_key(:tags), do: :tag_stats
|
def stat_key(:tags), do: :tag_stats
|
||||||
|
|
||||||
|
@spec apply_taxonomy_mappings(term(), term()) :: term()
|
||||||
def apply_taxonomy_mappings(report, analysis) do
|
def apply_taxonomy_mappings(report, analysis) do
|
||||||
report
|
report
|
||||||
|> update_in(
|
|> update_in(
|
||||||
@@ -198,6 +207,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec apply_taxonomy_mapping_bucket(term(), term()) :: term()
|
||||||
def apply_taxonomy_mapping_bucket(items, mappings) do
|
def apply_taxonomy_mapping_bucket(items, mappings) do
|
||||||
Enum.map(items || [], fn item ->
|
Enum.map(items || [], fn item ->
|
||||||
case Map.fetch(mappings, item.name) do
|
case Map.fetch(mappings, item.name) do
|
||||||
@@ -207,6 +217,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec existing_taxonomy_terms(term()) :: term()
|
||||||
def existing_taxonomy_terms(project_id) do
|
def existing_taxonomy_terms(project_id) do
|
||||||
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
||||||
|
|
||||||
@@ -216,6 +227,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec normalize_taxonomy_mapping_value(term(), term(), term()) :: term()
|
||||||
def normalize_taxonomy_mapping_value(project_id, type, mapped_to) do
|
def normalize_taxonomy_mapping_value(project_id, type, mapped_to) do
|
||||||
normalized_value = mapped_to |> to_string() |> String.trim() |> blank_to_nil()
|
normalized_value = mapped_to |> to_string() |> String.trim() |> blank_to_nil()
|
||||||
|
|
||||||
@@ -231,6 +243,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec auto_mapped_count(term(), term()) :: term()
|
||||||
def auto_mapped_count(previous_report, next_report) do
|
def auto_mapped_count(previous_report, next_report) do
|
||||||
previous_count =
|
previous_count =
|
||||||
(Map.get(previous_report.items, :categories, []) ++
|
(Map.get(previous_report.items, :categories, []) ++
|
||||||
@@ -244,6 +257,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
max(next_count - previous_count, 0)
|
max(next_count - previous_count, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec taxonomy_pill_class(term()) :: term()
|
||||||
def taxonomy_pill_class(item) do
|
def taxonomy_pill_class(item) do
|
||||||
cond do
|
cond do
|
||||||
item.exists_in_project -> "import-taxonomy-pill exists"
|
item.exists_in_project -> "import-taxonomy-pill exists"
|
||||||
@@ -252,9 +266,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec taxonomy_item_editing?(term(), term(), term()) :: term()
|
||||||
def taxonomy_item_editing?(%{type: type, name: name}, type, name), do: true
|
def taxonomy_item_editing?(%{type: type, name: name}, type, name), do: true
|
||||||
def taxonomy_item_editing?(_edit, _type, _name), do: false
|
def taxonomy_item_editing?(_edit, _type, _name), do: false
|
||||||
|
|
||||||
|
@spec taxonomy_mapping_tooltip(term()) :: term()
|
||||||
def taxonomy_mapping_tooltip(item) do
|
def taxonomy_mapping_tooltip(item) do
|
||||||
action =
|
action =
|
||||||
if present?(item.mapped_to),
|
if present?(item.mapped_to),
|
||||||
@@ -264,6 +280,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
|
|||||||
translated("importAnalysis.mappingTooltip", %{action: action})
|
translated("importAnalysis.mappingTooltip", %{action: action})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec maybe_put_option(term(), term(), term()) :: term()
|
||||||
def maybe_put_option(opts, _key, nil), do: opts
|
def maybe_put_option(opts, _key, nil), do: opts
|
||||||
def maybe_put_option(opts, key, value), do: Keyword.put(opts, key, value)
|
def maybe_put_option(opts, key, value), do: Keyword.put(opts, key, value)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ defmodule BDS.Desktop.ShellLive.Layout do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_set_sidebar_width(workbench, nil), do: workbench
|
defp maybe_set_sidebar_width(workbench, nil), do: workbench
|
||||||
|
|
||||||
defp maybe_set_sidebar_width(workbench, width),
|
defp maybe_set_sidebar_width(workbench, width),
|
||||||
do: Workbench.set_sidebar_width(workbench, parse_width(width))
|
do: Workbench.set_sidebar_width(workbench, parse_width(width))
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,16 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
alias BDS.UI.Workbench
|
alias BDS.UI.Workbench
|
||||||
|
|
||||||
embed_templates "media_editor_html/*"
|
embed_templates("media_editor_html/*")
|
||||||
|
|
||||||
@post_picker_limit 10
|
@post_picker_limit 10
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
assign(socket, :media_editor, build(socket.assigns))
|
assign(socket, :media_editor, build(socket.assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update(term(), term(), term()) :: term()
|
||||||
def update(socket, params, reload) do
|
def update(socket, params, reload) do
|
||||||
case socket.assigns.current_tab do
|
case socket.assigns.current_tab do
|
||||||
%{type: :media, id: media_id} ->
|
%{type: :media, id: media_id} ->
|
||||||
@@ -38,6 +40,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec persist_socket(term(), term(), term(), term()) :: term()
|
||||||
def persist_socket(socket, media_id, reload, append_output) do
|
def persist_socket(socket, media_id, reload, append_output) do
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -52,9 +55,18 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:media_editor_drafts, Map.delete(socket.assigns.media_editor_drafts, media_id))
|
|> assign(
|
||||||
|> assign(:media_editor_save_states, Map.put(socket.assigns.media_editor_save_states, media_id, :saved))
|
:media_editor_drafts,
|
||||||
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:media, media_id}, tab_meta(updated_media)))
|
Map.delete(socket.assigns.media_editor_drafts, media_id)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:media_editor_save_states,
|
||||||
|
Map.put(socket.assigns.media_editor_save_states, media_id, :saved)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:tab_meta,
|
||||||
|
Map.put(socket.assigns.tab_meta, {:media, media_id}, tab_meta(updated_media))
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -65,14 +77,19 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_quick_actions(term(), term(), term()) :: term()
|
||||||
def toggle_quick_actions(socket, media_id, reload) do
|
def toggle_quick_actions(socket, media_id, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_quick_actions_open, Map.update(socket.assigns.media_editor_quick_actions_open, media_id, true, &(!&1)))
|
|> assign(
|
||||||
|
:media_editor_quick_actions_open,
|
||||||
|
Map.update(socket.assigns.media_editor_quick_actions_open, media_id, true, &(!&1))
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec replace_file(term(), term(), term(), term()) :: term()
|
||||||
def replace_file(socket, media_id, reload, append_output) do
|
def replace_file(socket, media_id, reload, append_output) do
|
||||||
case FilePicker.choose_file(translated("Replace Media File")) do
|
case FilePicker.choose_file(translated("Replace Media File")) do
|
||||||
{:ok, source_path} ->
|
{:ok, source_path} ->
|
||||||
@@ -82,9 +99,18 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:media_editor_drafts, Map.delete(socket.assigns.media_editor_drafts, media_id))
|
|> assign(
|
||||||
|> assign(:media_editor_save_states, Map.put(socket.assigns.media_editor_save_states, media_id, :saved))
|
:media_editor_drafts,
|
||||||
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:media, media_id}, tab_meta(updated_media)))
|
Map.delete(socket.assigns.media_editor_drafts, media_id)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:media_editor_save_states,
|
||||||
|
Map.put(socket.assigns.media_editor_save_states, media_id, :saved)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:tab_meta,
|
||||||
|
Map.put(socket.assigns.tab_meta, {:media, media_id}, tab_meta(updated_media))
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
|
|
||||||
{:ok, nil} ->
|
{:ok, nil} ->
|
||||||
@@ -106,10 +132,16 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec detect_language(term(), term(), term(), term()) :: term()
|
||||||
def detect_language(socket, media_id, reload, append_output) do
|
def detect_language(socket, media_id, reload, append_output) do
|
||||||
if Map.get(socket.assigns, :offline_mode, true) do
|
if Map.get(socket.assigns, :offline_mode, true) do
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Detect Language"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|
|> append_output.(
|
||||||
|
translated("Detect Language"),
|
||||||
|
translated("Automatic AI actions stay gated by airplane mode."),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
@@ -118,15 +150,26 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
%MediaRecord{} = media ->
|
%MediaRecord{} = media ->
|
||||||
draft = current_draft(socket.assigns, media)
|
draft = current_draft(socket.assigns, media)
|
||||||
text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "alt", ""), Map.get(draft, "caption", "")], "\n\n")
|
|
||||||
|
text =
|
||||||
|
Enum.join(
|
||||||
|
[
|
||||||
|
Map.get(draft, "title", ""),
|
||||||
|
Map.get(draft, "alt", ""),
|
||||||
|
Map.get(draft, "caption", "")
|
||||||
|
],
|
||||||
|
"\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
case AI.detect_language(text) do
|
case AI.detect_language(text) do
|
||||||
{:ok, %{language_code: language_code}} when is_binary(language_code) and language_code != "" ->
|
{:ok, %{language_code: language_code}}
|
||||||
|
when is_binary(language_code) and language_code != "" ->
|
||||||
normalized = normalize_language(language_code)
|
normalized = normalize_language(language_code)
|
||||||
|
|
||||||
case Media.update_media(media.id, %{language: normalized}) do
|
case Media.update_media(media.id, %{language: normalized}) do
|
||||||
{:ok, updated_media} ->
|
{:ok, updated_media} ->
|
||||||
updated_draft = Map.put(current_draft(socket.assigns, media), "language", normalized)
|
updated_draft =
|
||||||
|
Map.put(current_draft(socket.assigns, media), "language", normalized)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> reconcile_draft(updated_media, updated_draft)
|
|> reconcile_draft(updated_media, updated_draft)
|
||||||
@@ -145,17 +188,28 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Detect Language"), translated("Language detection failed."), nil, "error")
|
|> append_output.(
|
||||||
|
translated("Detect Language"),
|
||||||
|
translated("Language detection failed."),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translate(term(), term(), term(), term(), term()) :: term()
|
||||||
def translate(socket, media_id, language, reload, append_output) do
|
def translate(socket, media_id, language, reload, append_output) do
|
||||||
if Map.get(socket.assigns, :offline_mode, true) do
|
if Map.get(socket.assigns, :offline_mode, true) do
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Translate"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|
|> append_output.(
|
||||||
|
translated("Translate"),
|
||||||
|
translated("Automatic AI actions stay gated by airplane mode."),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
normalized_language = normalize_language(language)
|
normalized_language = normalize_language(language)
|
||||||
@@ -165,8 +219,14 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
case Media.upsert_media_translation(media_id, normalized_language, translation) do
|
case Media.upsert_media_translation(media_id, normalized_language, translation) do
|
||||||
{:ok, _saved_translation} ->
|
{:ok, _saved_translation} ->
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_quick_actions_open, Map.put(socket.assigns.media_editor_quick_actions_open, media_id, false))
|
|> assign(
|
||||||
|> assign(:media_editor_translation_forms, Map.delete(socket.assigns.media_editor_translation_forms, media_id))
|
:media_editor_quick_actions_open,
|
||||||
|
Map.put(socket.assigns.media_editor_quick_actions_open, media_id, false)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:media_editor_translation_forms,
|
||||||
|
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -183,6 +243,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec apply_ai_suggestions(term(), term(), term(), term(), term()) :: term()
|
||||||
def apply_ai_suggestions(socket, media_id, fields, reload, append_output) do
|
def apply_ai_suggestions(socket, media_id, fields, reload, append_output) do
|
||||||
try do
|
try do
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
@@ -213,6 +274,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_socket(term(), term(), term(), term()) :: term()
|
||||||
def delete_socket(socket, media_id, reload, append_output) do
|
def delete_socket(socket, media_id, reload, append_output) do
|
||||||
case Media.delete_media(media_id) do
|
case Media.delete_media(media_id) do
|
||||||
{:ok, :deleted} ->
|
{:ok, :deleted} ->
|
||||||
@@ -223,11 +285,26 @@ defmodule BDS.Desktop.ShellLive.MediaEditor 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)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -237,28 +314,43 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_post_picker(term(), term(), term()) :: term()
|
||||||
def toggle_post_picker(socket, media_id, reload) do
|
def toggle_post_picker(socket, media_id, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_post_pickers_open, Map.update(socket.assigns.media_editor_post_pickers_open, media_id, true, &(!&1)))
|
|> assign(
|
||||||
|
:media_editor_post_pickers_open,
|
||||||
|
Map.update(socket.assigns.media_editor_post_pickers_open, media_id, true, &(!&1))
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec set_post_picker_query(term(), term(), term(), term()) :: term()
|
||||||
def set_post_picker_query(socket, media_id, query, reload) do
|
def set_post_picker_query(socket, media_id, query, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_post_picker_queries, Map.put(socket.assigns.media_editor_post_picker_queries, media_id, to_string(query || "")))
|
|> assign(
|
||||||
|
:media_editor_post_picker_queries,
|
||||||
|
Map.put(socket.assigns.media_editor_post_picker_queries, media_id, to_string(query || ""))
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec link_post(term(), term(), term(), term(), term()) :: term()
|
||||||
def link_post(socket, media_id, post_id, reload, append_output) do
|
def link_post(socket, media_id, post_id, reload, append_output) do
|
||||||
case Media.link_media_to_post(media_id, post_id) do
|
case Media.link_media_to_post(media_id, post_id) do
|
||||||
{:ok, _linked} ->
|
{:ok, _linked} ->
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_post_pickers_open, Map.put(socket.assigns.media_editor_post_pickers_open, media_id, false))
|
|> assign(
|
||||||
|> assign(:media_editor_post_picker_queries, Map.put(socket.assigns.media_editor_post_picker_queries, media_id, ""))
|
:media_editor_post_pickers_open,
|
||||||
|
Map.put(socket.assigns.media_editor_post_pickers_open, media_id, false)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:media_editor_post_picker_queries,
|
||||||
|
Map.put(socket.assigns.media_editor_post_picker_queries, media_id, "")
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -268,6 +360,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec unlink_post(term(), term(), term(), term(), term()) :: term()
|
||||||
def unlink_post(socket, media_id, post_id, reload, append_output) do
|
def unlink_post(socket, media_id, post_id, reload, append_output) do
|
||||||
case Media.unlink_media_from_post(media_id, post_id) do
|
case Media.unlink_media_from_post(media_id, post_id) do
|
||||||
{:ok, _unlinked} ->
|
{:ok, _unlinked} ->
|
||||||
@@ -280,6 +373,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec edit_translation(term(), term(), term(), term()) :: term()
|
||||||
def edit_translation(socket, media_id, language, reload) do
|
def edit_translation(socket, media_id, language, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
@@ -287,16 +381,20 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
form = %{
|
form = %{
|
||||||
"language" => language,
|
"language" => language,
|
||||||
"title" => translation && translation.title || "",
|
"title" => (translation && translation.title) || "",
|
||||||
"alt" => translation && translation.alt || "",
|
"alt" => (translation && translation.alt) || "",
|
||||||
"caption" => translation && translation.caption || ""
|
"caption" => (translation && translation.caption) || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_translation_forms, Map.put(socket.assigns.media_editor_translation_forms, media_id, form))
|
|> assign(
|
||||||
|
:media_editor_translation_forms,
|
||||||
|
Map.put(socket.assigns.media_editor_translation_forms, media_id, form)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_translation(term(), term(), term(), term()) :: term()
|
||||||
def update_translation(socket, media_id, params, reload) do
|
def update_translation(socket, media_id, params, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
@@ -308,10 +406,14 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_translation_forms, Map.put(socket.assigns.media_editor_translation_forms, media_id, form))
|
|> assign(
|
||||||
|
:media_editor_translation_forms,
|
||||||
|
Map.put(socket.assigns.media_editor_translation_forms, media_id, form)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_translation(term(), term(), term(), term()) :: term()
|
||||||
def save_translation(socket, media_id, reload, append_output) do
|
def save_translation(socket, media_id, reload, append_output) do
|
||||||
case Map.get(socket.assigns.media_editor_translation_forms, media_id) do
|
case Map.get(socket.assigns.media_editor_translation_forms, media_id) do
|
||||||
%{"language" => language} = form when language not in [nil, ""] ->
|
%{"language" => language} = form when language not in [nil, ""] ->
|
||||||
@@ -322,7 +424,10 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
}) do
|
}) do
|
||||||
{:ok, _translation} ->
|
{:ok, _translation} ->
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_translation_forms, Map.delete(socket.assigns.media_editor_translation_forms, media_id))
|
|> assign(
|
||||||
|
:media_editor_translation_forms,
|
||||||
|
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -336,16 +441,23 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec refresh_translation(term(), term(), term(), term(), term()) :: term()
|
||||||
def refresh_translation(socket, media_id, language, reload, append_output) do
|
def refresh_translation(socket, media_id, language, reload, append_output) do
|
||||||
if Map.get(socket.assigns, :offline_mode, true) do
|
if Map.get(socket.assigns, :offline_mode, true) do
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Translate"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|
|> append_output.(
|
||||||
|
translated("Translate"),
|
||||||
|
translated("Automatic AI actions stay gated by airplane mode."),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
case AI.translate_media(media_id, normalize_language(language)) do
|
case AI.translate_media(media_id, normalize_language(language)) do
|
||||||
{:ok, translation} ->
|
{:ok, translation} ->
|
||||||
case Media.upsert_media_translation(media_id, language, translation) do
|
case Media.upsert_media_translation(media_id, language, translation) do
|
||||||
{:ok, _saved_translation} -> socket |> reload.(socket.assigns.workbench)
|
{:ok, _saved_translation} ->
|
||||||
|
socket |> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
socket
|
socket
|
||||||
@@ -361,11 +473,15 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_translation(term(), term(), term(), term(), term()) :: term()
|
||||||
def delete_translation(socket, media_id, language, reload, append_output) do
|
def delete_translation(socket, media_id, language, reload, append_output) do
|
||||||
case Media.delete_media_translation(media_id, language) do
|
case Media.delete_media_translation(media_id, language) do
|
||||||
{:ok, _deleted?} ->
|
{:ok, _deleted?} ->
|
||||||
socket
|
socket
|
||||||
|> assign(:media_editor_translation_forms, Map.delete(socket.assigns.media_editor_translation_forms, media_id))
|
|> assign(
|
||||||
|
:media_editor_translation_forms,
|
||||||
|
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -375,6 +491,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build(term()) :: term()
|
||||||
def build(%{current_tab: %{type: :media, id: media_id}} = assigns) do
|
def build(%{current_tab: %{type: :media, id: media_id}} = assigns) do
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -385,7 +502,9 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
translations = Media.list_media_translations(media.id)
|
translations = Media.list_media_translations(media.id)
|
||||||
form = current_draft(assigns, media)
|
form = current_draft(assigns, media)
|
||||||
picker_query = Map.get(assigns.media_editor_post_picker_queries, media.id, "")
|
picker_query = Map.get(assigns.media_editor_post_picker_queries, media.id, "")
|
||||||
{picker_results, picker_overflow_count} = post_picker_results(media, linked_posts, picker_query)
|
|
||||||
|
{picker_results, picker_overflow_count} =
|
||||||
|
post_picker_results(media, linked_posts, picker_query)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: media.id,
|
id: media.id,
|
||||||
@@ -416,20 +535,26 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
def build(_assigns), do: nil
|
def build(_assigns), do: nil
|
||||||
|
|
||||||
def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
@spec translated(term(), term()) :: term()
|
||||||
|
def translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
|
@spec media_editor_save_state_label(term()) :: term()
|
||||||
def media_editor_save_state_label(:dirty), do: translated("Unsaved")
|
def media_editor_save_state_label(:dirty), do: translated("Unsaved")
|
||||||
def media_editor_save_state_label(:saved), do: translated("Saved")
|
def media_editor_save_state_label(:saved), do: translated("Saved")
|
||||||
def media_editor_save_state_label(_state), do: translated("Idle")
|
def media_editor_save_state_label(_state), do: translated("Idle")
|
||||||
|
|
||||||
|
@spec language_label(term()) :: term()
|
||||||
def language_label(code) do
|
def language_label(code) do
|
||||||
code
|
code
|
||||||
|> to_string()
|
|> to_string()
|
||||||
|> String.upcase()
|
|> String.upcase()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec normalize_language(term()) :: term()
|
||||||
def normalize_language(value), do: value |> to_string() |> String.trim() |> String.downcase()
|
def normalize_language(value), do: value |> to_string() |> String.trim() |> String.downcase()
|
||||||
|
|
||||||
|
@spec persist(term(), term()) :: term()
|
||||||
def persist(%MediaRecord{} = media, draft) do
|
def persist(%MediaRecord{} = media, draft) do
|
||||||
Media.update_media(media.id, %{
|
Media.update_media(media.id, %{
|
||||||
title: blank_to_nil(Map.get(draft, "title")),
|
title: blank_to_nil(Map.get(draft, "title")),
|
||||||
@@ -444,7 +569,11 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
defp reconcile_draft(socket, %MediaRecord{} = media, draft) do
|
defp reconcile_draft(socket, %MediaRecord{} = media, draft) do
|
||||||
persisted = persisted_form(media)
|
persisted = persisted_form(media)
|
||||||
dirty? = draft != persisted
|
dirty? = draft != persisted
|
||||||
workbench = if dirty?, do: Workbench.mark_dirty(socket.assigns.workbench, :media, media.id), else: Workbench.clear_dirty(socket.assigns.workbench, :media, media.id)
|
|
||||||
|
workbench =
|
||||||
|
if dirty?,
|
||||||
|
do: Workbench.mark_dirty(socket.assigns.workbench, :media, media.id),
|
||||||
|
else: Workbench.clear_dirty(socket.assigns.workbench, :media, media.id)
|
||||||
|
|
||||||
drafts =
|
drafts =
|
||||||
if dirty? do
|
if dirty? do
|
||||||
@@ -456,8 +585,21 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:media_editor_drafts, drafts)
|
|> assign(:media_editor_drafts, drafts)
|
||||||
|> assign(:media_editor_save_states, Map.put(socket.assigns.media_editor_save_states, media.id, if(dirty?, do: :dirty, else: :idle)))
|
|> assign(
|
||||||
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:media, media.id}, %{title: blank_to_nil(Map.get(draft, "title")) || display_title(media), subtitle: media.original_name || media.mime_type || ""}))
|
:media_editor_save_states,
|
||||||
|
Map.put(
|
||||||
|
socket.assigns.media_editor_save_states,
|
||||||
|
media.id,
|
||||||
|
if(dirty?, do: :dirty, else: :idle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:tab_meta,
|
||||||
|
Map.put(socket.assigns.tab_meta, {:media, media.id}, %{
|
||||||
|
title: blank_to_nil(Map.get(draft, "title")) || display_title(media),
|
||||||
|
subtitle: media.original_name || media.mime_type || ""
|
||||||
|
})
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp current_draft(assigns, %MediaRecord{} = media) do
|
defp current_draft(assigns, %MediaRecord{} = media) do
|
||||||
@@ -505,10 +647,15 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
from post in Post,
|
from post in Post,
|
||||||
where: post.project_id == ^media.project_id,
|
where: post.project_id == ^media.project_id,
|
||||||
order_by: [desc: post.updated_at, desc: post.created_at],
|
order_by: [desc: post.updated_at, desc: post.created_at],
|
||||||
select: %{post_id: post.id, title: fragment("COALESCE(?, ?, ?)", post.title, post.slug, post.id)}
|
select: %{
|
||||||
|
post_id: post.id,
|
||||||
|
title: fragment("COALESCE(?, ?, ?)", post.title, post.slug, post.id)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|> Enum.reject(&MapSet.member?(linked_ids, &1.post_id))
|
|> Enum.reject(&MapSet.member?(linked_ids, &1.post_id))
|
||||||
|> Enum.filter(fn post -> normalized_query == "" or String.contains?(String.downcase(post.title), normalized_query) end)
|
|> Enum.filter(fn post ->
|
||||||
|
normalized_query == "" or String.contains?(String.downcase(post.title), normalized_query)
|
||||||
|
end)
|
||||||
|
|
||||||
{Enum.take(posts, @post_picker_limit), max(length(posts) - @post_picker_limit, 0)}
|
{Enum.take(posts, @post_picker_limit), max(length(posts) - @post_picker_limit, 0)}
|
||||||
end
|
end
|
||||||
@@ -518,18 +665,28 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp preview_url(%MediaRecord{} = media) do
|
defp preview_url(%MediaRecord{} = media) do
|
||||||
if image?(media), do: "/media-thumbnail/#{media.id}?size=large&t=#{media.updated_at}", else: nil
|
if image?(media),
|
||||||
|
do: "/media-thumbnail/#{media.id}?size=large&t=#{media.updated_at}",
|
||||||
|
else: nil
|
||||||
end
|
end
|
||||||
|
|
||||||
defp image?(%MediaRecord{} = media), do: String.starts_with?(to_string(media.mime_type || ""), "image/")
|
defp image?(%MediaRecord{} = media),
|
||||||
|
do: String.starts_with?(to_string(media.mime_type || ""), "image/")
|
||||||
|
|
||||||
defp display_title(%MediaRecord{} = media), do: blank_to_nil(media.title) || blank_to_nil(media.original_name) || media.id
|
defp display_title(%MediaRecord{} = media),
|
||||||
|
do: blank_to_nil(media.title) || blank_to_nil(media.original_name) || media.id
|
||||||
|
|
||||||
|
defp dimensions_label(%MediaRecord{width: width, height: height})
|
||||||
|
when is_integer(width) and is_integer(height), do: "#{width} x #{height}"
|
||||||
|
|
||||||
defp dimensions_label(%MediaRecord{width: width, height: height}) when is_integer(width) and is_integer(height), do: "#{width} x #{height}"
|
|
||||||
defp dimensions_label(_media), do: nil
|
defp dimensions_label(_media), do: nil
|
||||||
|
|
||||||
defp format_file_size(size) when is_integer(size) and size >= 1_048_576, do: :erlang.float_to_binary(size / 1_048_576, decimals: 1) <> " MB"
|
defp format_file_size(size) when is_integer(size) and size >= 1_048_576,
|
||||||
defp format_file_size(size) when is_integer(size), do: :erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
|
do: :erlang.float_to_binary(size / 1_048_576, decimals: 1) <> " MB"
|
||||||
|
|
||||||
|
defp format_file_size(size) when is_integer(size),
|
||||||
|
do: :erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
|
||||||
|
|
||||||
defp format_file_size(_size), do: "0.0 KB"
|
defp format_file_size(_size), do: "0.0 KB"
|
||||||
|
|
||||||
defp detect_language_enabled?(form) do
|
defp detect_language_enabled?(form) do
|
||||||
@@ -567,5 +724,6 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp reload_with_assigned_workbench(socket, reload), do: reload.(socket, socket.assigns.workbench)
|
defp reload_with_assigned_workbench(socket, reload),
|
||||||
|
do: reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
alias BDS.Desktop.ShellLive.MenuEditor.{
|
alias BDS.Desktop.ShellLive.MenuEditor.{
|
||||||
DraftManagement,
|
DraftManagement,
|
||||||
PageCategory,
|
PageCategory,
|
||||||
@@ -12,8 +13,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
TreePredicates
|
TreePredicates
|
||||||
}
|
}
|
||||||
|
|
||||||
embed_templates "menu_editor_html/*"
|
embed_templates("menu_editor_html/*")
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
case socket.assigns[:current_tab] do
|
case socket.assigns[:current_tab] do
|
||||||
%{type: :menu_editor, id: tab_id} ->
|
%{type: :menu_editor, id: tab_id} ->
|
||||||
@@ -36,12 +38,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_item(term(), term(), term()) :: term()
|
||||||
def select_item(socket, item_id, reload) do
|
def select_item(socket, item_id, reload) do
|
||||||
socket
|
socket
|
||||||
|> State.update_state(fn state -> %{state | selected_id: item_id} end)
|
|> State.update_state(fn state -> %{state | selected_id: item_id} end)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec change_entry(term(), term(), term()) :: term()
|
||||||
def change_entry(socket, params, reload) do
|
def change_entry(socket, params, reload) do
|
||||||
query = Map.get(params, "query", "")
|
query = Map.get(params, "query", "")
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec submit_entry(term(), term()) :: term()
|
||||||
def submit_entry(socket, reload) do
|
def submit_entry(socket, reload) do
|
||||||
case DraftManagement.current_draft(socket.assigns) do
|
case DraftManagement.current_draft(socket.assigns) do
|
||||||
%{type: :page} ->
|
%{type: :page} ->
|
||||||
@@ -67,12 +72,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec cancel_entry(term(), term()) :: term()
|
||||||
def cancel_entry(socket, reload) do
|
def cancel_entry(socket, reload) do
|
||||||
socket
|
socket
|
||||||
|> State.update_state(&DraftManagement.cancel_draft/1)
|
|> State.update_state(&DraftManagement.cancel_draft/1)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_page(term(), term(), term()) :: term()
|
||||||
def select_page(socket, post_id, reload) do
|
def select_page(socket, post_id, reload) do
|
||||||
case PageCategory.page_post(socket.assigns.projects.active_project_id, post_id) do
|
case PageCategory.page_post(socket.assigns.projects.active_project_id, post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -85,6 +92,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_category(term(), term(), term()) :: term()
|
||||||
def select_category(socket, name, reload) do
|
def select_category(socket, name, reload) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
@@ -99,6 +107,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toolbar_action(term(), term(), term(), term()) :: term()
|
||||||
def toolbar_action(socket, action, reload, append_output) do
|
def toolbar_action(socket, action, reload, append_output) do
|
||||||
case action do
|
case action do
|
||||||
"add-entry" ->
|
"add-entry" ->
|
||||||
@@ -144,12 +153,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec drop_item(term(), term(), term(), term(), term()) :: term()
|
||||||
def drop_item(socket, drag_item_id, target_item_id, position, reload) do
|
def drop_item(socket, drag_item_id, target_item_id, position, reload) do
|
||||||
socket
|
socket
|
||||||
|> State.update_state(&TreeOps.drop_selected(&1, drag_item_id, target_item_id, position))
|
|> State.update_state(&TreeOps.drop_selected(&1, drag_item_id, target_item_id, position))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec handle_keydown(term(), term(), term()) :: term()
|
||||||
def handle_keydown(socket, "Escape", reload) do
|
def handle_keydown(socket, "Escape", reload) do
|
||||||
cancel_entry(socket, reload)
|
cancel_entry(socket, reload)
|
||||||
end
|
end
|
||||||
@@ -158,14 +169,16 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
reload.(socket, socket.assigns.workbench)
|
reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
attr :menu_editor, :map, required: true
|
attr(:menu_editor, :map, required: true)
|
||||||
|
|
||||||
|
@spec menu_editor(term()) :: term()
|
||||||
def menu_editor(assigns)
|
def menu_editor(assigns)
|
||||||
|
|
||||||
attr :items, :list, required: true
|
attr(:items, :list, required: true)
|
||||||
attr :menu_editor, :map, required: true
|
attr(:menu_editor, :map, required: true)
|
||||||
attr :depth, :integer, required: true
|
attr(:depth, :integer, required: true)
|
||||||
|
|
||||||
|
@spec menu_tree_level(term()) :: term()
|
||||||
def menu_tree_level(assigns) do
|
def menu_tree_level(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<%= for item <- @items do %>
|
<%= for item <- @items do %>
|
||||||
@@ -289,8 +302,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
attr :kind, :atom, required: true
|
attr(:kind, :atom, required: true)
|
||||||
|
|
||||||
|
@spec kind_icon(term()) :: term()
|
||||||
def kind_icon(assigns) do
|
def kind_icon(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<%= case @kind do %>
|
<%= case @kind do %>
|
||||||
@@ -306,9 +320,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translated(term(), term()) :: term()
|
||||||
def translated(text, bindings \\ %{}),
|
def translated(text, bindings \\ %{}),
|
||||||
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
|
@spec row_label(term(), term()) :: term()
|
||||||
def row_label(item, category_titles) do
|
def row_label(item, category_titles) do
|
||||||
if item.kind == :category_archive do
|
if item.kind == :category_archive do
|
||||||
Map.get(category_titles || %{}, item.slug, item.label)
|
Map.get(category_titles || %{}, item.slug, item.label)
|
||||||
@@ -317,6 +333,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec kind_label(term()) :: term()
|
||||||
def kind_label(:home), do: translated("menuEditor.type.home")
|
def kind_label(:home), do: translated("menuEditor.type.home")
|
||||||
def kind_label(:page), do: translated("menuEditor.type.page")
|
def kind_label(:page), do: translated("menuEditor.type.page")
|
||||||
def kind_label(:category_archive), do: translated("menuEditor.type.categoryArchive")
|
def kind_label(:category_archive), do: translated("menuEditor.type.categoryArchive")
|
||||||
@@ -324,12 +341,17 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
|||||||
|
|
||||||
defdelegate draft_item?(menu_editor, item_id), to: TreePredicates
|
defdelegate draft_item?(menu_editor, item_id), to: TreePredicates
|
||||||
|
|
||||||
|
@spec editing_title(term()) :: term()
|
||||||
def editing_title(%{draft: %{type: :category}}), do: translated("menuEditor.addCategoryArchive")
|
def editing_title(%{draft: %{type: :category}}), do: translated("menuEditor.addCategoryArchive")
|
||||||
def editing_title(_menu_editor), do: translated("menuEditor.pagePicker.title")
|
def editing_title(_menu_editor), do: translated("menuEditor.pagePicker.title")
|
||||||
|
|
||||||
|
@spec editing_hint(term()) :: term()
|
||||||
def editing_hint(%{draft: %{type: :category}}), do: translated("menuEditor.categoryPicker.hint")
|
def editing_hint(%{draft: %{type: :category}}), do: translated("menuEditor.categoryPicker.hint")
|
||||||
def editing_hint(_menu_editor), do: translated("menuEditor.createHint")
|
def editing_hint(_menu_editor), do: translated("menuEditor.createHint")
|
||||||
|
|
||||||
def editing_placeholder(%{draft: %{type: :category}}), do: translated("menuEditor.newCategoryPlaceholder")
|
@spec editing_placeholder(term()) :: term()
|
||||||
|
def editing_placeholder(%{draft: %{type: :category}}),
|
||||||
|
do: translated("menuEditor.newCategoryPlaceholder")
|
||||||
|
|
||||||
def editing_placeholder(_menu_editor), do: translated("menuEditor.newEntryPlaceholder")
|
def editing_placeholder(_menu_editor), do: translated("menuEditor.newEntryPlaceholder")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
alias BDS.Desktop.ShellLive.MenuEditor.PageCategory
|
alias BDS.Desktop.ShellLive.MenuEditor.PageCategory
|
||||||
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
|
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
|
||||||
|
|
||||||
|
@spec current_draft(term()) :: term()
|
||||||
def current_draft(assigns), do: Map.get(assigns.menu_editor_state || %{}, :draft)
|
def current_draft(assigns), do: Map.get(assigns.menu_editor_state || %{}, :draft)
|
||||||
|
|
||||||
|
@spec start_page_draft(term()) :: term()
|
||||||
def start_page_draft(state) do
|
def start_page_draft(state) do
|
||||||
item = %{
|
item = %{
|
||||||
item_id: Ecto.UUID.generate(),
|
item_id: Ecto.UUID.generate(),
|
||||||
@@ -29,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec start_category_draft(term()) :: term()
|
||||||
def start_category_draft(state) do
|
def start_category_draft(state) do
|
||||||
item = %{
|
item = %{
|
||||||
item_id: Ecto.UUID.generate(),
|
item_id: Ecto.UUID.generate(),
|
||||||
@@ -50,6 +53,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec finalize_submenu_draft(term()) :: term()
|
||||||
def finalize_submenu_draft(%{draft: %{item_id: item_id, query: query}} = state) do
|
def finalize_submenu_draft(%{draft: %{item_id: item_id, query: query}} = state) do
|
||||||
label =
|
label =
|
||||||
if(String.trim(query) == "",
|
if(String.trim(query) == "",
|
||||||
@@ -69,12 +73,19 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
|
|
||||||
def finalize_submenu_draft(state), do: state
|
def finalize_submenu_draft(state), do: state
|
||||||
|
|
||||||
|
@spec assign_page_to_draft(term(), term()) :: term()
|
||||||
def assign_page_to_draft(%{draft: %{item_id: item_id}} = state, post) do
|
def assign_page_to_draft(%{draft: %{item_id: item_id}} = state, post) do
|
||||||
%{
|
%{
|
||||||
state
|
state
|
||||||
| items:
|
| items:
|
||||||
TreeOps.update_item(state.items, item_id, fn item ->
|
TreeOps.update_item(state.items, item_id, fn item ->
|
||||||
%{item | kind: :page, label: post.title, slug: PageCategory.blank_to_nil(post.slug), children: []}
|
%{
|
||||||
|
item
|
||||||
|
| kind: :page,
|
||||||
|
label: post.title,
|
||||||
|
slug: PageCategory.blank_to_nil(post.slug),
|
||||||
|
children: []
|
||||||
|
}
|
||||||
end),
|
end),
|
||||||
draft: nil
|
draft: nil
|
||||||
}
|
}
|
||||||
@@ -82,6 +93,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
|
|
||||||
def assign_page_to_draft(state, _post), do: state
|
def assign_page_to_draft(state, _post), do: state
|
||||||
|
|
||||||
|
@spec assign_category_to_draft(term(), term()) :: term()
|
||||||
def assign_category_to_draft(%{draft: %{item_id: item_id}} = state, category) do
|
def assign_category_to_draft(%{draft: %{item_id: item_id}} = state, category) do
|
||||||
label = PageCategory.blank_to_nil(category.title) || category.name
|
label = PageCategory.blank_to_nil(category.title) || category.name
|
||||||
|
|
||||||
@@ -97,6 +109,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
|
|
||||||
def assign_category_to_draft(state, _category), do: state
|
def assign_category_to_draft(state, _category), do: state
|
||||||
|
|
||||||
|
@spec cancel_draft(term()) :: term()
|
||||||
def cancel_draft(%{draft: %{item_id: item_id}} = state) do
|
def cancel_draft(%{draft: %{item_id: item_id}} = state) do
|
||||||
items = TreeOps.remove_item(state.items, item_id)
|
items = TreeOps.remove_item(state.items, item_id)
|
||||||
%{state | items: items, selected_id: TreeOps.first_item_id(items), draft: nil}
|
%{state | items: items, selected_id: TreeOps.first_item_id(items), draft: nil}
|
||||||
@@ -104,6 +117,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
|
|
||||||
def cancel_draft(state), do: state
|
def cancel_draft(state), do: state
|
||||||
|
|
||||||
|
@spec confirm_category_draft(term(), term()) :: term()
|
||||||
def confirm_category_draft(socket, update_state_fun) do
|
def confirm_category_draft(socket, update_state_fun) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
draft = current_draft(socket.assigns)
|
draft = current_draft(socket.assigns)
|
||||||
@@ -117,8 +131,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
|
|||||||
|
|
||||||
category =
|
category =
|
||||||
cond do
|
cond do
|
||||||
category != nil -> category
|
category != nil ->
|
||||||
normalized == "" -> %{name: "", title: ""}
|
category
|
||||||
|
|
||||||
|
normalized == "" ->
|
||||||
|
%{name: "", title: ""}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{:ok, _metadata} = Metadata.add_category(project_id, normalized)
|
{:ok, _metadata} = Metadata.add_category(project_id, normalized)
|
||||||
%{name: normalized, title: normalized}
|
%{name: normalized, title: normalized}
|
||||||
|
|||||||
@@ -6,19 +6,26 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
|
|||||||
alias BDS.{Metadata, Repo}
|
alias BDS.{Metadata, Repo}
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
|
|
||||||
|
@spec page_posts(term()) :: term()
|
||||||
def page_posts(nil), do: []
|
def page_posts(nil), do: []
|
||||||
|
|
||||||
def page_posts(project_id) do
|
def page_posts(project_id) do
|
||||||
Repo.all(from post in Post, where: post.project_id == ^project_id, order_by: [asc: post.title, asc: post.slug])
|
Repo.all(
|
||||||
|
from post in Post,
|
||||||
|
where: post.project_id == ^project_id,
|
||||||
|
order_by: [asc: post.title, asc: post.slug]
|
||||||
|
)
|
||||||
|> Enum.filter(&("page" in (&1.categories || [])))
|
|> Enum.filter(&("page" in (&1.categories || [])))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec page_post(term(), term()) :: term()
|
||||||
def page_post(nil, _post_id), do: nil
|
def page_post(nil, _post_id), do: nil
|
||||||
|
|
||||||
def page_post(project_id, post_id) do
|
def page_post(project_id, post_id) do
|
||||||
Enum.find(page_posts(project_id), &(&1.id == post_id))
|
Enum.find(page_posts(project_id), &(&1.id == post_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filter_page_posts(term(), term()) :: term()
|
||||||
def filter_page_posts(posts, query) do
|
def filter_page_posts(posts, query) do
|
||||||
normalized = query |> to_string() |> String.trim() |> String.downcase()
|
normalized = query |> to_string() |> String.trim() |> String.downcase()
|
||||||
|
|
||||||
@@ -29,6 +36,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec category_options(term()) :: term()
|
||||||
def category_options(nil), do: []
|
def category_options(nil), do: []
|
||||||
|
|
||||||
def category_options(project_id) do
|
def category_options(project_id) do
|
||||||
@@ -40,6 +48,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filter_categories(term(), term()) :: term()
|
||||||
def filter_categories(categories, query) do
|
def filter_categories(categories, query) do
|
||||||
normalized = query |> to_string() |> String.trim() |> String.downcase()
|
normalized = query |> to_string() |> String.trim() |> String.downcase()
|
||||||
|
|
||||||
@@ -50,7 +59,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec blank_to_nil(term()) :: term()
|
||||||
def blank_to_nil(nil), do: nil
|
def blank_to_nil(nil), do: nil
|
||||||
|
|
||||||
def blank_to_nil(value) do
|
def blank_to_nil(value) do
|
||||||
trimmed = String.trim(to_string(value))
|
trimmed = String.trim(to_string(value))
|
||||||
if trimmed == "", do: nil, else: trimmed
|
if trimmed == "", do: nil, else: trimmed
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
|
|||||||
alias BDS.Menu
|
alias BDS.Menu
|
||||||
alias BDS.Desktop.ShellLive.MenuEditor.{PageCategory, TreeOps, TreePredicates}
|
alias BDS.Desktop.ShellLive.MenuEditor.{PageCategory, TreeOps, TreePredicates}
|
||||||
|
|
||||||
|
@spec ensure_state(term()) :: term()
|
||||||
def ensure_state(assigns) do
|
def ensure_state(assigns) do
|
||||||
project_id = assigns.projects.active_project_id
|
project_id = assigns.projects.active_project_id
|
||||||
|
|
||||||
@@ -16,11 +17,13 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_state(term(), term()) :: term()
|
||||||
def update_state(socket, updater) do
|
def update_state(socket, updater) do
|
||||||
state = ensure_state(socket.assigns)
|
state = ensure_state(socket.assigns)
|
||||||
assign(socket, :menu_editor_state, updater.(state))
|
assign(socket, :menu_editor_state, updater.(state))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build(term(), term()) :: term()
|
||||||
def build(_assigns, state) do
|
def build(_assigns, state) do
|
||||||
categories = PageCategory.category_options(state.project_id)
|
categories = PageCategory.category_options(state.project_id)
|
||||||
draft = state.draft
|
draft = state.draft
|
||||||
@@ -35,7 +38,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
|
|||||||
draft_query: draft_query,
|
draft_query: draft_query,
|
||||||
filtered_pages:
|
filtered_pages:
|
||||||
if(match?(%{type: :page}, draft),
|
if(match?(%{type: :page}, draft),
|
||||||
do: PageCategory.filter_page_posts(PageCategory.page_posts(state.project_id), draft_query),
|
do:
|
||||||
|
PageCategory.filter_page_posts(PageCategory.page_posts(state.project_id), draft_query),
|
||||||
else: []
|
else: []
|
||||||
),
|
),
|
||||||
filtered_categories:
|
filtered_categories:
|
||||||
@@ -53,6 +57,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save(term(), term(), term()) :: term()
|
||||||
def save(socket, reload, append_output) do
|
def save(socket, reload, append_output) do
|
||||||
state = socket.assigns.menu_editor_state
|
state = socket.assigns.menu_editor_state
|
||||||
|
|
||||||
@@ -60,12 +65,22 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
|
|||||||
Menu.update_menu(state.project_id, Enum.map(state.items, &TreeOps.persisted_item/1))
|
Menu.update_menu(state.project_id, Enum.map(state.items, &TreeOps.persisted_item/1))
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("menuEditor.tabTitle"), translated("menuEditor.saved"), nil, "info")
|
|> append_output.(
|
||||||
|
translated("menuEditor.tabTitle"),
|
||||||
|
translated("menuEditor.saved"),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_state(nil) do
|
defp load_state(nil) do
|
||||||
%{project_id: nil, items: [TreeOps.home_item()], selected_id: TreeOps.home_item_id(), draft: nil}
|
%{
|
||||||
|
project_id: nil,
|
||||||
|
items: [TreeOps.home_item()],
|
||||||
|
selected_id: TreeOps.home_item_id(),
|
||||||
|
draft: nil
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_state(project_id) do
|
defp load_state(project_id) do
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
|
|
||||||
@home_item_id "menu-home"
|
@home_item_id "menu-home"
|
||||||
|
|
||||||
|
@spec home_item_id() :: term()
|
||||||
def home_item_id, do: @home_item_id
|
def home_item_id, do: @home_item_id
|
||||||
|
|
||||||
|
@spec home_item() :: term()
|
||||||
def home_item do
|
def home_item do
|
||||||
%{item_id: @home_item_id, kind: :home, label: "Home", slug: nil, children: [], is_home: true}
|
%{item_id: @home_item_id, kind: :home, label: "Home", slug: nil, children: [], is_home: true}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec ui_item(term()) :: term()
|
||||||
def ui_item(%{kind: :home}), do: home_item()
|
def ui_item(%{kind: :home}), do: home_item()
|
||||||
|
|
||||||
def ui_item(item) do
|
def ui_item(item) do
|
||||||
@@ -24,25 +27,37 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec persisted_item(term()) :: term()
|
||||||
def persisted_item(%{kind: :home}), do: %{kind: :home, label: "Home", slug: nil}
|
def persisted_item(%{kind: :home}), do: %{kind: :home, label: "Home", slug: nil}
|
||||||
|
|
||||||
def persisted_item(%{kind: :submenu} = item) do
|
def persisted_item(%{kind: :submenu} = item) do
|
||||||
%{kind: :submenu, label: item.label, slug: nil, children: Enum.map(item.children || [], &persisted_item/1)}
|
%{
|
||||||
|
kind: :submenu,
|
||||||
|
label: item.label,
|
||||||
|
slug: nil,
|
||||||
|
children: Enum.map(item.children || [], &persisted_item/1)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def persisted_item(item) do
|
def persisted_item(item) do
|
||||||
%{kind: item.kind, label: item.label, slug: item.slug}
|
%{kind: item.kind, label: item.label, slug: item.slug}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec first_item_id(term()) :: term()
|
||||||
def first_item_id([item | _rest]), do: item.item_id
|
def first_item_id([item | _rest]), do: item.item_id
|
||||||
def first_item_id([]), do: nil
|
def first_item_id([]), do: nil
|
||||||
|
|
||||||
|
@spec insert_target(term(), term()) :: term()
|
||||||
def insert_target(items, nil), do: {[], length(items)}
|
def insert_target(items, nil), do: {[], length(items)}
|
||||||
|
|
||||||
def insert_target(items, selected_id) do
|
def insert_target(items, selected_id) do
|
||||||
case find_path(items, selected_id) do
|
case find_path(items, selected_id) do
|
||||||
nil -> {[], length(items)}
|
nil ->
|
||||||
[] -> {[], length(items)}
|
{[], length(items)}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
{[], length(items)}
|
||||||
|
|
||||||
path ->
|
path ->
|
||||||
case item_at_path(items, path) do
|
case item_at_path(items, path) do
|
||||||
%{kind: :submenu} -> {path, 0}
|
%{kind: :submenu} -> {path, 0}
|
||||||
@@ -51,9 +66,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec path_prefix?(term(), term()) :: term()
|
||||||
def path_prefix?(prefix, path) when length(prefix) > length(path), do: false
|
def path_prefix?(prefix, path) when length(prefix) > length(path), do: false
|
||||||
|
@spec path_prefix?(term(), term()) :: term()
|
||||||
def path_prefix?(prefix, path), do: Enum.take(path, length(prefix)) == prefix
|
def path_prefix?(prefix, path), do: Enum.take(path, length(prefix)) == prefix
|
||||||
|
|
||||||
|
@spec find_path(term(), term(), term()) :: term()
|
||||||
def find_path(items, item_id, path \\ []) do
|
def find_path(items, item_id, path \\ []) do
|
||||||
Enum.find_value(Enum.with_index(items), fn {item, index} ->
|
Enum.find_value(Enum.with_index(items), fn {item, index} ->
|
||||||
next_path = path ++ [index]
|
next_path = path ++ [index]
|
||||||
@@ -71,6 +89,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec item_at_path(term(), term()) :: term()
|
||||||
def item_at_path(_items, []), do: nil
|
def item_at_path(_items, []), do: nil
|
||||||
|
|
||||||
def item_at_path(items, [index]) do
|
def item_at_path(items, [index]) do
|
||||||
@@ -84,6 +103,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec items_at_path(term(), term()) :: term()
|
||||||
def items_at_path(items, []), do: items
|
def items_at_path(items, []), do: items
|
||||||
|
|
||||||
def items_at_path(items, [index | rest]) do
|
def items_at_path(items, [index | rest]) do
|
||||||
@@ -93,6 +113,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec replace_items_at_path(term(), term(), term()) :: term()
|
||||||
def replace_items_at_path(_items, [], replacement), do: replacement
|
def replace_items_at_path(_items, [], replacement), do: replacement
|
||||||
|
|
||||||
def replace_items_at_path(items, [index | rest], replacement) do
|
def replace_items_at_path(items, [index | rest], replacement) do
|
||||||
@@ -101,6 +122,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_item(term(), term(), term()) :: term()
|
||||||
def update_item(items, item_id, updater) do
|
def update_item(items, item_id, updater) do
|
||||||
Enum.map(items, fn item ->
|
Enum.map(items, fn item ->
|
||||||
cond do
|
cond do
|
||||||
@@ -111,6 +133,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec insert_item(term(), term(), term(), term()) :: term()
|
||||||
def insert_item(items, [], index, item) do
|
def insert_item(items, [], index, item) do
|
||||||
List.insert_at(items, index, item)
|
List.insert_at(items, index, item)
|
||||||
end
|
end
|
||||||
@@ -121,10 +144,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec remove_item(term(), term()) :: term()
|
||||||
def remove_item(items, item_id) do
|
def remove_item(items, item_id) do
|
||||||
remove_item_with_value(items, item_id) |> elem(0)
|
remove_item_with_value(items, item_id) |> elem(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec remove_item_with_value(term(), term()) :: term()
|
||||||
def remove_item_with_value(items, item_id) do
|
def remove_item_with_value(items, item_id) do
|
||||||
Enum.reduce_while(Enum.with_index(items), {items, nil}, fn {item, index}, _acc ->
|
Enum.reduce_while(Enum.with_index(items), {items, nil}, fn {item, index}, _acc ->
|
||||||
cond do
|
cond do
|
||||||
@@ -135,7 +160,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
{next_children, removed_item} = remove_item_with_value(item.children, item_id)
|
{next_children, removed_item} = remove_item_with_value(item.children, item_id)
|
||||||
|
|
||||||
if removed_item do
|
if removed_item do
|
||||||
{:halt, {List.replace_at(items, index, %{item | children: next_children}), removed_item}}
|
{:halt,
|
||||||
|
{List.replace_at(items, index, %{item | children: next_children}), removed_item}}
|
||||||
else
|
else
|
||||||
{:cont, {items, nil}}
|
{:cont, {items, nil}}
|
||||||
end
|
end
|
||||||
@@ -146,16 +172,23 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec append_child(term(), term(), term()) :: term()
|
||||||
def append_child(items, parent_item_id, child) do
|
def append_child(items, parent_item_id, child) do
|
||||||
update_item(items, parent_item_id, fn item ->
|
update_item(items, parent_item_id, fn item ->
|
||||||
%{item | children: (item.children || []) ++ [child]}
|
%{item | children: (item.children || []) ++ [child]}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def move_selected(%{selected_id: selected_id} = state, direction) when direction in [:up, :down] do
|
@spec move_selected(term(), term()) :: term()
|
||||||
|
def move_selected(%{selected_id: selected_id} = state, direction)
|
||||||
|
when direction in [:up, :down] do
|
||||||
case find_path(state.items, selected_id) do
|
case find_path(state.items, selected_id) do
|
||||||
nil -> state
|
nil ->
|
||||||
[] -> state
|
state
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
state
|
||||||
|
|
||||||
path ->
|
path ->
|
||||||
parent_path = Enum.drop(path, -1)
|
parent_path = Enum.drop(path, -1)
|
||||||
index = List.last(path)
|
index = List.last(path)
|
||||||
@@ -175,10 +208,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec indent_selected(term()) :: term()
|
||||||
def indent_selected(%{selected_id: selected_id} = state) do
|
def indent_selected(%{selected_id: selected_id} = state) do
|
||||||
case find_path(state.items, selected_id) do
|
case find_path(state.items, selected_id) do
|
||||||
nil -> state
|
nil ->
|
||||||
[] -> state
|
state
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
state
|
||||||
|
|
||||||
path ->
|
path ->
|
||||||
parent_path = Enum.drop(path, -1)
|
parent_path = Enum.drop(path, -1)
|
||||||
index = List.last(path)
|
index = List.last(path)
|
||||||
@@ -193,7 +231,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
case item_at_path(state.items, previous_sibling_path) do
|
case item_at_path(state.items, previous_sibling_path) do
|
||||||
%{kind: :submenu, item_id: sibling_id} ->
|
%{kind: :submenu, item_id: sibling_id} ->
|
||||||
case remove_item_with_value(state.items, selected_id) do
|
case remove_item_with_value(state.items, selected_id) do
|
||||||
{_next_items, nil} -> state
|
{_next_items, nil} ->
|
||||||
|
state
|
||||||
|
|
||||||
{next_items, removed_item} ->
|
{next_items, removed_item} ->
|
||||||
%{
|
%{
|
||||||
state
|
state
|
||||||
@@ -208,18 +248,27 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec unindent_selected(term()) :: term()
|
||||||
def unindent_selected(%{selected_id: selected_id} = state) do
|
def unindent_selected(%{selected_id: selected_id} = state) do
|
||||||
case find_path(state.items, selected_id) do
|
case find_path(state.items, selected_id) do
|
||||||
nil -> state
|
nil ->
|
||||||
[] -> state
|
state
|
||||||
[_root_index] -> state
|
|
||||||
|
[] ->
|
||||||
|
state
|
||||||
|
|
||||||
|
[_root_index] ->
|
||||||
|
state
|
||||||
|
|
||||||
path ->
|
path ->
|
||||||
parent_path = Enum.drop(path, -1)
|
parent_path = Enum.drop(path, -1)
|
||||||
parent_index = List.last(parent_path)
|
parent_index = List.last(parent_path)
|
||||||
grand_parent_path = Enum.drop(parent_path, -1)
|
grand_parent_path = Enum.drop(parent_path, -1)
|
||||||
|
|
||||||
case remove_item_with_value(state.items, selected_id) do
|
case remove_item_with_value(state.items, selected_id) do
|
||||||
{_next_items, nil} -> state
|
{_next_items, nil} ->
|
||||||
|
state
|
||||||
|
|
||||||
{next_items, removed_item} ->
|
{next_items, removed_item} ->
|
||||||
%{
|
%{
|
||||||
state
|
state
|
||||||
@@ -229,6 +278,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_selected(term()) :: term()
|
||||||
def delete_selected(%{selected_id: @home_item_id} = state), do: state
|
def delete_selected(%{selected_id: @home_item_id} = state), do: state
|
||||||
|
|
||||||
def delete_selected(%{selected_id: selected_id} = state) do
|
def delete_selected(%{selected_id: selected_id} = state) do
|
||||||
@@ -241,9 +291,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
def drop_selected(state, drag_item_id, target_item_id, _position) when drag_item_id == target_item_id,
|
def drop_selected(state, drag_item_id, target_item_id, _position)
|
||||||
do: state
|
when drag_item_id == target_item_id,
|
||||||
|
do: state
|
||||||
|
|
||||||
|
@spec drop_selected(term(), term(), term(), term()) :: term()
|
||||||
def drop_selected(state, drag_item_id, target_item_id, position) do
|
def drop_selected(state, drag_item_id, target_item_id, position) do
|
||||||
drag_path = find_path(state.items, drag_item_id)
|
drag_path = find_path(state.items, drag_item_id)
|
||||||
target_path = find_path(state.items, target_item_id)
|
target_path = find_path(state.items, target_item_id)
|
||||||
@@ -275,7 +327,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
defp insert_dropped_item(state, next_items, dragged_item, target_path, "inside") do
|
defp insert_dropped_item(state, next_items, dragged_item, target_path, "inside") do
|
||||||
case item_at_path(next_items, target_path) do
|
case item_at_path(next_items, target_path) do
|
||||||
%{kind: :submenu} ->
|
%{kind: :submenu} ->
|
||||||
%{state | items: insert_item(next_items, target_path, 0, dragged_item), selected_id: dragged_item.item_id}
|
%{
|
||||||
|
state
|
||||||
|
| items: insert_item(next_items, target_path, 0, dragged_item),
|
||||||
|
selected_id: dragged_item.item_id
|
||||||
|
}
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
state
|
state
|
||||||
@@ -285,12 +341,22 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
|
|||||||
defp insert_dropped_item(state, next_items, dragged_item, target_path, "before") do
|
defp insert_dropped_item(state, next_items, dragged_item, target_path, "before") do
|
||||||
parent_path = Enum.drop(target_path, -1)
|
parent_path = Enum.drop(target_path, -1)
|
||||||
index = List.last(target_path)
|
index = List.last(target_path)
|
||||||
%{state | items: insert_item(next_items, parent_path, index, dragged_item), selected_id: dragged_item.item_id}
|
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| items: insert_item(next_items, parent_path, index, dragged_item),
|
||||||
|
selected_id: dragged_item.item_id
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp insert_dropped_item(state, next_items, dragged_item, target_path, _position) do
|
defp insert_dropped_item(state, next_items, dragged_item, target_path, _position) do
|
||||||
parent_path = Enum.drop(target_path, -1)
|
parent_path = Enum.drop(target_path, -1)
|
||||||
index = List.last(target_path) + 1
|
index = List.last(target_path) + 1
|
||||||
%{state | items: insert_item(next_items, parent_path, index, dragged_item), selected_id: dragged_item.item_id}
|
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| items: insert_item(next_items, parent_path, index, dragged_item),
|
||||||
|
selected_id: dragged_item.item_id
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
|
|||||||
|
|
||||||
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
|
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
|
||||||
|
|
||||||
|
@spec can_move_up?(term(), term()) :: term()
|
||||||
def can_move_up?(items, selected_id) do
|
def can_move_up?(items, selected_id) do
|
||||||
case TreeOps.find_path(items, selected_id) do
|
case TreeOps.find_path(items, selected_id) do
|
||||||
[_parent, index] -> index > 0
|
[_parent, index] -> index > 0
|
||||||
@@ -12,9 +13,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_move_down?(term(), term()) :: term()
|
||||||
def can_move_down?(items, selected_id) do
|
def can_move_down?(items, selected_id) do
|
||||||
case TreeOps.find_path(items, selected_id) do
|
case TreeOps.find_path(items, selected_id) do
|
||||||
nil -> false
|
nil ->
|
||||||
|
false
|
||||||
|
|
||||||
path ->
|
path ->
|
||||||
parent_path = Enum.drop(path, -1)
|
parent_path = Enum.drop(path, -1)
|
||||||
index = List.last(path)
|
index = List.last(path)
|
||||||
@@ -22,10 +26,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_indent?(term(), term()) :: term()
|
||||||
def can_indent?(items, selected_id) do
|
def can_indent?(items, selected_id) do
|
||||||
case TreeOps.find_path(items, selected_id) do
|
case TreeOps.find_path(items, selected_id) do
|
||||||
nil -> false
|
nil ->
|
||||||
[] -> false
|
false
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
false
|
||||||
|
|
||||||
[_index] = path ->
|
[_index] = path ->
|
||||||
index = List.last(path)
|
index = List.last(path)
|
||||||
index > 0 and match?(%{kind: :submenu}, TreeOps.item_at_path(items, [index - 1]))
|
index > 0 and match?(%{kind: :submenu}, TreeOps.item_at_path(items, [index - 1]))
|
||||||
@@ -34,10 +43,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
|
|||||||
index = List.last(path)
|
index = List.last(path)
|
||||||
|
|
||||||
index > 0 and
|
index > 0 and
|
||||||
match?(%{kind: :submenu}, TreeOps.item_at_path(items, Enum.drop(path, -1) ++ [index - 1]))
|
match?(
|
||||||
|
%{kind: :submenu},
|
||||||
|
TreeOps.item_at_path(items, Enum.drop(path, -1) ++ [index - 1])
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_unindent?(term(), term()) :: term()
|
||||||
def can_unindent?(items, selected_id) do
|
def can_unindent?(items, selected_id) do
|
||||||
case TreeOps.find_path(items, selected_id) do
|
case TreeOps.find_path(items, selected_id) do
|
||||||
[_index] -> false
|
[_index] -> false
|
||||||
@@ -46,9 +59,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_delete?(term()) :: term()
|
||||||
def can_delete?(selected_id),
|
def can_delete?(selected_id),
|
||||||
do: is_binary(selected_id) and selected_id != TreeOps.home_item_id()
|
do: is_binary(selected_id) and selected_id != TreeOps.home_item_id()
|
||||||
|
|
||||||
|
@spec draft_item?(term(), term()) :: term()
|
||||||
def draft_item?(menu_editor, item_id) do
|
def draft_item?(menu_editor, item_id) do
|
||||||
match?(%{item_id: ^item_id}, menu_editor.draft)
|
match?(%{item_id: ^item_id}, menu_editor.draft)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
:git_diff
|
:git_diff
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
assign(socket, :misc_editor, build(socket.assigns))
|
assign(socket, :misc_editor, build(socket.assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec rerun(term()) :: term()
|
||||||
def rerun(socket) do
|
def rerun(socket) do
|
||||||
case meta(socket.assigns) do
|
case meta(socket.assigns) do
|
||||||
%{action: action} when is_binary(action) ->
|
%{action: action} when is_binary(action) ->
|
||||||
@@ -37,6 +39,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec apply_site_validation(term(), term()) :: term()
|
||||||
def apply_site_validation(socket, append_output) do
|
def apply_site_validation(socket, append_output) do
|
||||||
meta = meta(socket.assigns)
|
meta = meta(socket.assigns)
|
||||||
payload = Map.get(meta, :payload, %{})
|
payload = Map.get(meta, :payload, %{})
|
||||||
@@ -68,6 +71,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")}
|
append_output.(socket, translated("Site Validation"), inspect(error), nil, "error")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_duplicate(term(), term(), term()) :: term()
|
||||||
def toggle_duplicate(socket, pair_id, reload) do
|
def toggle_duplicate(socket, pair_id, reload) do
|
||||||
selected_by_tab = Map.get(socket.assigns, :misc_editor_selected_pairs, %{})
|
selected_by_tab = Map.get(socket.assigns, :misc_editor_selected_pairs, %{})
|
||||||
current = Map.get(selected_by_tab, socket.assigns.current_tab.id, MapSet.new())
|
current = Map.get(selected_by_tab, socket.assigns.current_tab.id, MapSet.new())
|
||||||
@@ -87,6 +91,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec dismiss_duplicate(term(), term(), term(), term(), term()) :: term()
|
||||||
def dismiss_duplicate(socket, post_id_a, post_id_b, reload, append_output) do
|
def dismiss_duplicate(socket, post_id_a, post_id_b, reload, append_output) do
|
||||||
case Embeddings.dismiss_duplicate_pair(post_id_a, post_id_b) do
|
case Embeddings.dismiss_duplicate_pair(post_id_a, post_id_b) do
|
||||||
{:ok, _saved_pair} ->
|
{:ok, _saved_pair} ->
|
||||||
@@ -109,6 +114,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec dismiss_selected(term(), term(), term()) :: term()
|
||||||
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
|
||||||
|
|
||||||
@@ -141,6 +147,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fix_translation_validation(term(), term()) :: term()
|
||||||
def fix_translation_validation(socket, append_output) do
|
def fix_translation_validation(socket, append_output) do
|
||||||
report =
|
report =
|
||||||
socket.assigns
|
socket.assigns
|
||||||
@@ -166,6 +173,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
append_output.(socket, translated("Translation Validation"), inspect(error), nil, "error")}
|
append_output.(socket, translated("Translation Validation"), inspect(error), nil, "error")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_git_diff_file(term(), term()) :: term()
|
||||||
def select_git_diff_file(socket, file_path) do
|
def select_git_diff_file(socket, file_path) do
|
||||||
assign(
|
assign(
|
||||||
socket,
|
socket,
|
||||||
@@ -178,6 +186,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec metadata_diff_repair_request(term(), term(), term()) :: term()
|
||||||
def metadata_diff_repair_request(socket, field, direction) do
|
def metadata_diff_repair_request(socket, field, direction) do
|
||||||
meta = meta(socket.assigns)
|
meta = meta(socket.assigns)
|
||||||
payload = Map.get(meta, :payload, %{})
|
payload = Map.get(meta, :payload, %{})
|
||||||
@@ -209,6 +218,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec metadata_diff_orphan_import_request(term()) :: term()
|
||||||
def metadata_diff_orphan_import_request(socket) do
|
def metadata_diff_orphan_import_request(socket) do
|
||||||
meta = meta(socket.assigns)
|
meta = meta(socket.assigns)
|
||||||
payload = Map.get(meta, :payload, %{})
|
payload = Map.get(meta, :payload, %{})
|
||||||
@@ -232,6 +242,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build(term()) :: term()
|
||||||
def build(%{current_tab: %{type: type}} = assigns) when type in @misc_routes do
|
def build(%{current_tab: %{type: type}} = assigns) when type in @misc_routes do
|
||||||
meta = meta(assigns)
|
meta = meta(assigns)
|
||||||
payload = Map.get(meta, :payload, %{})
|
payload = Map.get(meta, :payload, %{})
|
||||||
@@ -245,11 +256,14 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build(term()) :: term()
|
||||||
def build(_assigns), do: nil
|
def build(_assigns), do: nil
|
||||||
|
|
||||||
|
@spec translated(term(), term()) :: term()
|
||||||
def translated(text, bindings \\ %{}),
|
def translated(text, bindings \\ %{}),
|
||||||
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
|
@spec misc_class(term()) :: term()
|
||||||
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"
|
||||||
def misc_class(:translation_validation), do: "translation-validation-view"
|
def misc_class(:translation_validation), do: "translation-validation-view"
|
||||||
@@ -257,10 +271,13 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
def misc_class(:git_diff), do: "git-diff-view"
|
def misc_class(:git_diff), do: "git-diff-view"
|
||||||
|
|
||||||
def summary_items(%{summary: summary}) when is_map(summary), do: Enum.to_list(summary)
|
def summary_items(%{summary: summary}) when is_map(summary), do: Enum.to_list(summary)
|
||||||
|
@spec summary_items(term()) :: term()
|
||||||
def summary_items(_misc), do: []
|
def summary_items(_misc), do: []
|
||||||
|
|
||||||
|
@spec duplicate_checked?(term(), term()) :: term()
|
||||||
def duplicate_checked?(misc, pair_id), do: MapSet.member?(misc.selected_pairs, pair_id)
|
def duplicate_checked?(misc, pair_id), do: MapSet.member?(misc.selected_pairs, pair_id)
|
||||||
|
|
||||||
|
@spec pair_id_from_pair(term()) :: term()
|
||||||
def pair_id_from_pair(pair), do: pair_identity(pair)
|
def pair_id_from_pair(pair), do: pair_identity(pair)
|
||||||
|
|
||||||
defp build_site_validation(meta, payload) do
|
defp build_site_validation(meta, payload) do
|
||||||
@@ -410,6 +427,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translation_issue_label(term()) :: term()
|
||||||
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" ->
|
"same-language-as-canonical" ->
|
||||||
@@ -426,6 +444,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translation_issue_languages(term()) :: term()
|
||||||
def translation_issue_languages(issue) do
|
def translation_issue_languages(issue) do
|
||||||
canonical_language = issue_value(issue, :canonical_language)
|
canonical_language = issue_value(issue, :canonical_language)
|
||||||
translation_language = issue_value(issue, :translation_language)
|
translation_language = issue_value(issue, :translation_language)
|
||||||
@@ -440,8 +459,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translation_issue_value(term(), term()) :: term()
|
||||||
def translation_issue_value(issue, key), do: issue_value(issue, key)
|
def translation_issue_value(issue, key), do: issue_value(issue, key)
|
||||||
|
|
||||||
|
@spec git_diff_language(term()) :: term()
|
||||||
def git_diff_language(nil), do: "plaintext"
|
def git_diff_language(nil), do: "plaintext"
|
||||||
|
|
||||||
def git_diff_language(file_path) do
|
def git_diff_language(file_path) do
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
alias BDS.Posts.{Post, PostMedia, Translation}
|
alias BDS.Posts.{Post, PostMedia, Translation}
|
||||||
alias BDS.Tags.Tag
|
alias BDS.Tags.Tag
|
||||||
|
|
||||||
embed_templates "overlay_html/*"
|
embed_templates("overlay_html/*")
|
||||||
|
|
||||||
def context(assigns, tab_title, tab_subtitle) do
|
def context(assigns, tab_title, tab_subtitle) do
|
||||||
project_id = assigns.projects.active_project_id
|
project_id = assigns.projects.active_project_id
|
||||||
@@ -23,7 +23,12 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
media = media(project_id)
|
media = media(project_id)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
current_tab: %{type: current_tab.type, id: current_tab.id, title: tab_title, subtitle: tab_subtitle},
|
current_tab: %{
|
||||||
|
type: current_tab.type,
|
||||||
|
id: current_tab.id,
|
||||||
|
title: tab_title,
|
||||||
|
subtitle: tab_subtitle
|
||||||
|
},
|
||||||
current_post_language: source_language(current_tab, metadata),
|
current_post_language: source_language(current_tab, metadata),
|
||||||
current_media_language: source_language(current_tab, metadata),
|
current_media_language: source_language(current_tab, metadata),
|
||||||
posts: posts,
|
posts: posts,
|
||||||
@@ -59,7 +64,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
|
|
||||||
def markdown_link(text, url), do: "[#{text}](#{url})"
|
def markdown_link(text, url), do: "[#{text}](#{url})"
|
||||||
|
|
||||||
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 project_metadata(nil), do: %{main_language: "en", blog_languages: []}
|
def project_metadata(nil), do: %{main_language: "en", blog_languages: []}
|
||||||
|
|
||||||
@@ -77,7 +83,15 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
from post in Post,
|
from post in Post,
|
||||||
where: post.project_id == ^project_id,
|
where: post.project_id == ^project_id,
|
||||||
order_by: [desc: post.updated_at, desc: post.created_at],
|
order_by: [desc: post.updated_at, desc: post.created_at],
|
||||||
select: %{id: post.id, title: post.title, slug: post.slug, status: post.status, published_at: post.published_at, updated_at: post.updated_at, language: post.language}
|
select: %{
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
slug: post.slug,
|
||||||
|
status: post.status,
|
||||||
|
published_at: post.published_at,
|
||||||
|
updated_at: post.updated_at,
|
||||||
|
language: post.language
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|> Enum.map(fn post ->
|
|> Enum.map(fn post ->
|
||||||
%{
|
%{
|
||||||
@@ -96,7 +110,14 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
from media in MediaRecord,
|
from media in MediaRecord,
|
||||||
where: media.project_id == ^project_id,
|
where: media.project_id == ^project_id,
|
||||||
order_by: [desc: media.updated_at, desc: media.created_at],
|
order_by: [desc: media.updated_at, desc: media.created_at],
|
||||||
select: %{id: media.id, title: media.title, original_name: media.original_name, mime_type: media.mime_type, alt: media.alt, caption: media.caption}
|
select: %{
|
||||||
|
id: media.id,
|
||||||
|
title: media.title,
|
||||||
|
original_name: media.original_name,
|
||||||
|
mime_type: media.mime_type,
|
||||||
|
alt: media.alt,
|
||||||
|
caption: media.caption
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|> Enum.map(fn media ->
|
|> Enum.map(fn media ->
|
||||||
%{
|
%{
|
||||||
@@ -149,7 +170,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
defp existing_translations(_tab), do: %{}
|
defp existing_translations(_tab), do: %{}
|
||||||
|
|
||||||
defp blog_languages(metadata) do
|
defp blog_languages(metadata) do
|
||||||
([metadata.main_language || "en"] ++ (metadata.blog_languages || []) ++ Enum.map(I18n.supported_languages(), & &1.code))
|
([metadata.main_language || "en"] ++
|
||||||
|
(metadata.blog_languages || []) ++ Enum.map(I18n.supported_languages(), & &1.code))
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
end
|
end
|
||||||
@@ -193,9 +215,27 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
[
|
[
|
||||||
%{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: post.title || title, suggested_value: refine_title(post.title || title), locked: false},
|
%{
|
||||||
%{key: "excerpt", label: ShellData.translate("Excerpt", %{}, page_language), current_value: post.excerpt || subtitle, suggested_value: refine_excerpt(post.title || title, post.excerpt || subtitle), locked: false},
|
key: "title",
|
||||||
%{key: "slug", label: ShellData.translate("Slug", %{}, page_language), current_value: post.slug || slugify(post.title || title), suggested_value: refine_slug(post.slug || slugify(post.title || title)), locked: post.status == :published}
|
label: ShellData.translate("Title", %{}, page_language),
|
||||||
|
current_value: post.title || title,
|
||||||
|
suggested_value: refine_title(post.title || title),
|
||||||
|
locked: false
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: "excerpt",
|
||||||
|
label: ShellData.translate("Excerpt", %{}, page_language),
|
||||||
|
current_value: post.excerpt || subtitle,
|
||||||
|
suggested_value: refine_excerpt(post.title || title, post.excerpt || subtitle),
|
||||||
|
locked: false
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: "slug",
|
||||||
|
label: ShellData.translate("Slug", %{}, page_language),
|
||||||
|
current_value: post.slug || slugify(post.title || title),
|
||||||
|
suggested_value: refine_slug(post.slug || slugify(post.title || title)),
|
||||||
|
locked: post.status == :published
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
@@ -209,9 +249,27 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
%MediaRecord{} = media ->
|
%MediaRecord{} = media ->
|
||||||
[
|
[
|
||||||
%{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: media.title || title, suggested_value: refine_title(media.title || title), locked: false},
|
%{
|
||||||
%{key: "alt", label: ShellData.translate("Alt Text", %{}, page_language), current_value: media.alt || "", suggested_value: media.alt || title, locked: false},
|
key: "title",
|
||||||
%{key: "caption", label: ShellData.translate("Caption", %{}, page_language), current_value: media.caption || "", suggested_value: refine_excerpt(title, media.caption || title), locked: false}
|
label: ShellData.translate("Title", %{}, page_language),
|
||||||
|
current_value: media.title || title,
|
||||||
|
suggested_value: refine_title(media.title || title),
|
||||||
|
locked: false
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: "alt",
|
||||||
|
label: ShellData.translate("Alt Text", %{}, page_language),
|
||||||
|
current_value: media.alt || "",
|
||||||
|
suggested_value: media.alt || title,
|
||||||
|
locked: false
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: "caption",
|
||||||
|
label: ShellData.translate("Caption", %{}, page_language),
|
||||||
|
current_value: media.caption || "",
|
||||||
|
suggested_value: refine_excerpt(title, media.caption || title),
|
||||||
|
locked: false
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
@@ -248,7 +306,13 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
reference_list: reference_list
|
reference_list: reference_list
|
||||||
}
|
}
|
||||||
rescue
|
rescue
|
||||||
_error -> %{title: ShellData.translate("Delete Media", %{}, page_language), entity_name: media_id, entity_type: "media", reference_list: []}
|
_error ->
|
||||||
|
%{
|
||||||
|
title: ShellData.translate("Delete Media", %{}, page_language),
|
||||||
|
entity_name: media_id,
|
||||||
|
entity_type: "media",
|
||||||
|
reference_list: []
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_details(%{type: :tags}, page_language) do
|
defp delete_details(%{type: :tags}, page_language) do
|
||||||
@@ -263,16 +327,33 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
reference_list: []
|
reference_list: []
|
||||||
}
|
}
|
||||||
rescue
|
rescue
|
||||||
_error -> %{title: ShellData.translate("Delete Tag", %{}, page_language), entity_name: "tag", entity_type: "tag", reference_list: []}
|
_error ->
|
||||||
|
%{
|
||||||
|
title: ShellData.translate("Delete Tag", %{}, page_language),
|
||||||
|
entity_name: "tag",
|
||||||
|
entity_type: "tag",
|
||||||
|
reference_list: []
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_details(_tab, page_language) do
|
defp delete_details(_tab, page_language) do
|
||||||
%{title: ShellData.translate("Delete", %{}, page_language), entity_name: "", entity_type: "item", reference_list: []}
|
%{
|
||||||
|
title: ShellData.translate("Delete", %{}, page_language),
|
||||||
|
entity_name: "",
|
||||||
|
entity_type: "item",
|
||||||
|
reference_list: []
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_details(project_id, page_language) do
|
defp merge_details(project_id, page_language) do
|
||||||
tags =
|
tags =
|
||||||
Repo.all(from tag in Tag, where: tag.project_id == ^project_id, order_by: [asc: tag.name], limit: 3, select: tag.name)
|
Repo.all(
|
||||||
|
from tag in Tag,
|
||||||
|
where: tag.project_id == ^project_id,
|
||||||
|
order_by: [asc: tag.name],
|
||||||
|
limit: 3,
|
||||||
|
select: tag.name
|
||||||
|
)
|
||||||
|
|
||||||
target = List.first(tags) || "tag"
|
target = List.first(tags) || "tag"
|
||||||
|
|
||||||
@@ -283,7 +364,13 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
message: ShellData.translate("Cannot be undone.", %{}, page_language)
|
message: ShellData.translate("Cannot be undone.", %{}, page_language)
|
||||||
}
|
}
|
||||||
rescue
|
rescue
|
||||||
_error -> %{target: "tag", count: 1, title: ShellData.translate("Merge Tags", %{}, page_language), message: ShellData.translate("Cannot be undone.", %{}, page_language)}
|
_error ->
|
||||||
|
%{
|
||||||
|
target: "tag",
|
||||||
|
count: 1,
|
||||||
|
title: ShellData.translate("Merge Tags", %{}, page_language),
|
||||||
|
message: ShellData.translate("Cannot be undone.", %{}, page_language)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp canonical_post_url(post) do
|
defp canonical_post_url(post) do
|
||||||
@@ -302,7 +389,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|
|||||||
if base == "", do: "#{title} overview", else: base <> "."
|
if base == "", do: "#{title} overview", else: base <> "."
|
||||||
end
|
end
|
||||||
|
|
||||||
defp refine_slug(slug), do: slug |> to_string() |> String.trim_trailing("-") |> Kernel.<>("-updated")
|
defp refine_slug(slug),
|
||||||
|
do: slug |> to_string() |> String.trim_trailing("-") |> Kernel.<>("-updated")
|
||||||
|
|
||||||
defp slugify(value) do
|
defp slugify(value) do
|
||||||
value
|
value
|
||||||
|
|||||||
@@ -210,8 +210,15 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
|
|||||||
defp related_posts(links, key) do
|
defp related_posts(links, key) do
|
||||||
Enum.map(links, fn link ->
|
Enum.map(links, fn link ->
|
||||||
case Posts.get_post(Map.fetch!(link, key)) do
|
case Posts.get_post(Map.fetch!(link, key)) do
|
||||||
%Post{} = post -> %{id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id}
|
%Post{} = post ->
|
||||||
_other -> nil
|
%{
|
||||||
|
id: post.id,
|
||||||
|
title: post.title || post.slug || post.id,
|
||||||
|
text: link.link_text || post.slug || post.id
|
||||||
|
}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
@@ -232,15 +239,22 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
|
|||||||
|
|
||||||
defp git_history_target(%{type: :post, id: post_id}) do
|
defp git_history_target(%{type: :post, id: post_id}) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
%Post{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path}
|
%Post{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] ->
|
||||||
_other -> nil
|
{project_id, file_path}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp git_history_target(%{type: :media, id: media_id}) do
|
defp git_history_target(%{type: :media, id: media_id}) do
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
%MediaRecord{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path}
|
%MediaRecord{project_id: project_id, file_path: file_path}
|
||||||
_other -> nil
|
when file_path not in [nil, ""] ->
|
||||||
|
{project_id, file_path}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -287,5 +301,6 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
|
|||||||
|
|
||||||
defp present?(value), do: value not in [nil, ""]
|
defp present?(value), do: value not in [nil, ""]
|
||||||
|
|
||||||
defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
defp translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,13 +74,21 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|
|
||||||
defdelegate tag_chip_style(color), to: ListValues
|
defdelegate tag_chip_style(color), to: ListValues
|
||||||
|
|
||||||
embed_templates "post_editor_html/*"
|
embed_templates("post_editor_html/*")
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
assigns = Map.put(socket.assigns, :project_metadata, project_metadata(socket.assigns.projects.active_project_id))
|
assigns =
|
||||||
|
Map.put(
|
||||||
|
socket.assigns,
|
||||||
|
:project_metadata,
|
||||||
|
project_metadata(socket.assigns.projects.active_project_id)
|
||||||
|
)
|
||||||
|
|
||||||
assign(socket, :post_editor, build(assigns))
|
assign(socket, :post_editor, build(assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update(term(), term(), term()) :: term()
|
||||||
def update(socket, params, reload) do
|
def update(socket, params, reload) do
|
||||||
case socket.assigns.current_tab do
|
case socket.assigns.current_tab do
|
||||||
%{type: :post, id: post_id} ->
|
%{type: :post, id: post_id} ->
|
||||||
@@ -91,7 +99,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = project_metadata(post.project_id)
|
metadata = project_metadata(post.project_id)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
current_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
|
||||||
|
current_language =
|
||||||
|
Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
||||||
|
|
||||||
requested_language = normalize_language(Map.get(params, "language"), current_language)
|
requested_language = normalize_language(Map.get(params, "language"), current_language)
|
||||||
|
|
||||||
next_language =
|
next_language =
|
||||||
@@ -117,6 +128,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec persist_socket(term(), term(), term(), term(), term()) :: term()
|
||||||
def persist_socket(socket, post_id, action, reload, append_output) do
|
def persist_socket(socket, post_id, action, reload, append_output) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -125,7 +137,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = project_metadata(post.project_id)
|
metadata = project_metadata(post.project_id)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
|
||||||
|
active_language =
|
||||||
|
Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
||||||
|
|
||||||
draft = current_draft(socket.assigns, post, metadata, active_language)
|
draft = current_draft(socket.assigns, post, metadata, active_language)
|
||||||
|
|
||||||
case persist(post, draft, active_language, metadata, action) do
|
case persist(post, draft, active_language, metadata, action) do
|
||||||
@@ -135,9 +150,30 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, normalized_form))
|
|> assign(
|
||||||
|> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, save_state_for_action(action)))
|
:post_editor_drafts,
|
||||||
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: record_title(record, Posts.get_post!(post_id)), subtitle: Atom.to_string(record_status(record))}))
|
put_nested_map(
|
||||||
|
socket.assigns.post_editor_drafts,
|
||||||
|
post_id,
|
||||||
|
active_language,
|
||||||
|
normalized_form
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_save_states,
|
||||||
|
Map.put(
|
||||||
|
socket.assigns.post_editor_save_states,
|
||||||
|
post_id,
|
||||||
|
save_state_for_action(action)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:tab_meta,
|
||||||
|
Map.put(socket.assigns.tab_meta, {:post, post_id}, %{
|
||||||
|
title: record_title(record, Posts.get_post!(post_id)),
|
||||||
|
subtitle: Atom.to_string(record_status(record))
|
||||||
|
})
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -148,6 +184,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec discard_socket(term(), term(), term(), term()) :: term()
|
||||||
def discard_socket(socket, post_id, reload, append_output) do
|
def discard_socket(socket, post_id, reload, append_output) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -156,7 +193,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = project_metadata(post.project_id)
|
metadata = project_metadata(post.project_id)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
|
||||||
|
active_language =
|
||||||
|
Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
||||||
|
|
||||||
case discard(post, active_language, metadata) do
|
case discard(post, active_language, metadata) do
|
||||||
{:ok, restored_post} ->
|
{:ok, restored_post} ->
|
||||||
@@ -164,9 +203,21 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:post_editor_drafts, delete_nested_map(socket.assigns.post_editor_drafts, post_id, active_language))
|
|> assign(
|
||||||
|> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, :discarded))
|
:post_editor_drafts,
|
||||||
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: restored_post.title || restored_post.slug || restored_post.id, subtitle: Atom.to_string(restored_post.status || :draft)}))
|
delete_nested_map(socket.assigns.post_editor_drafts, post_id, active_language)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_save_states,
|
||||||
|
Map.put(socket.assigns.post_editor_save_states, post_id, :discarded)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:tab_meta,
|
||||||
|
Map.put(socket.assigns.tab_meta, {:post, post_id}, %{
|
||||||
|
title: restored_post.title || restored_post.slug || restored_post.id,
|
||||||
|
subtitle: Atom.to_string(restored_post.status || :draft)
|
||||||
|
})
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -177,6 +228,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_socket(term(), term(), term(), term()) :: term()
|
||||||
def delete_socket(socket, post_id, reload, append_output) do
|
def delete_socket(socket, post_id, reload, append_output) do
|
||||||
case Posts.delete_post(post_id) do
|
case Posts.delete_post(post_id) do
|
||||||
{:ok, :deleted} ->
|
{:ok, :deleted} ->
|
||||||
@@ -185,13 +237,28 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
socket
|
socket
|
||||||
|> 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)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -201,6 +268,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec set_mode(term(), term(), term(), term()) :: term()
|
||||||
def set_mode(socket, post_id, mode, reload) do
|
def set_mode(socket, post_id, mode, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
normalized_mode = normalize_mode(mode)
|
normalized_mode = normalize_mode(mode)
|
||||||
@@ -216,38 +284,67 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:post_editor_modes, Map.put(socket.assigns.post_editor_modes, post_id, normalized_mode))
|
|> assign(
|
||||||
|
:post_editor_modes,
|
||||||
|
Map.put(socket.assigns.post_editor_modes, post_id, normalized_mode)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_section(term(), term(), term(), term()) :: term()
|
||||||
def toggle_section(socket, post_id, section, reload) when section in [:metadata, :excerpt] do
|
def toggle_section(socket, post_id, section, reload) when section in [:metadata, :excerpt] do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:post_editor_expanded, Map.put(socket.assigns.post_editor_expanded, post_id, toggled_sections(socket.assigns.post_editor_expanded, post_id, section)))
|
|> assign(
|
||||||
|
:post_editor_expanded,
|
||||||
|
Map.put(
|
||||||
|
socket.assigns.post_editor_expanded,
|
||||||
|
post_id,
|
||||||
|
toggled_sections(socket.assigns.post_editor_expanded, post_id, section)
|
||||||
|
)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_language(term(), term(), term(), term()) :: term()
|
||||||
def select_language(socket, post_id, language, reload) do
|
def select_language(socket, post_id, language, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:post_editor_active_languages, Map.put(socket.assigns.post_editor_active_languages, post_id, normalize_language(language, language)))
|
|> assign(
|
||||||
|
:post_editor_active_languages,
|
||||||
|
Map.put(
|
||||||
|
socket.assigns.post_editor_active_languages,
|
||||||
|
post_id,
|
||||||
|
normalize_language(language, language)
|
||||||
|
)
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_quick_actions(term(), term(), term()) :: term()
|
||||||
def toggle_quick_actions(socket, post_id, reload) do
|
def toggle_quick_actions(socket, post_id, reload) do
|
||||||
workbench = socket.assigns.workbench
|
workbench = socket.assigns.workbench
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:post_editor_quick_actions_open, Map.update(socket.assigns.post_editor_quick_actions_open, post_id, true, &(!&1)))
|
|> assign(
|
||||||
|
:post_editor_quick_actions_open,
|
||||||
|
Map.update(socket.assigns.post_editor_quick_actions_open, post_id, true, &(!&1))
|
||||||
|
)
|
||||||
|> reload.(workbench)
|
|> reload.(workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec detect_language(term(), term(), term(), term()) :: term()
|
||||||
def detect_language(socket, post_id, reload, append_output) do
|
def detect_language(socket, post_id, reload, append_output) do
|
||||||
if Map.get(socket.assigns, :offline_mode, true) do
|
if Map.get(socket.assigns, :offline_mode, true) do
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Detect Language"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|
|> append_output.(
|
||||||
|
translated("Detect Language"),
|
||||||
|
translated("Automatic AI actions stay gated by airplane mode."),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
@@ -257,14 +354,24 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = project_metadata(post.project_id)
|
metadata = project_metadata(post.project_id)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
|
||||||
|
active_language =
|
||||||
|
Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
||||||
|
|
||||||
draft = current_draft(socket.assigns, post, metadata, active_language)
|
draft = current_draft(socket.assigns, post, metadata, active_language)
|
||||||
text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "content", "")], "\n\n")
|
text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "content", "")], "\n\n")
|
||||||
|
|
||||||
case AI.detect_language(text) do
|
case AI.detect_language(text) do
|
||||||
{:ok, %{language_code: language_code}} when is_binary(language_code) and language_code != "" ->
|
{:ok, %{language_code: language_code}}
|
||||||
|
when is_binary(language_code) and language_code != "" ->
|
||||||
socket
|
socket
|
||||||
|> put_draft_field(post_id, post, active_language, "language", normalize_language(language_code, canonical_language))
|
|> put_draft_field(
|
||||||
|
post_id,
|
||||||
|
post,
|
||||||
|
active_language,
|
||||||
|
"language",
|
||||||
|
normalize_language(language_code, canonical_language)
|
||||||
|
)
|
||||||
|> reload_with_assigned_workbench(reload)
|
|> reload_with_assigned_workbench(reload)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -274,17 +381,28 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Detect Language"), translated("Language detection failed."), nil, "error")
|
|> append_output.(
|
||||||
|
translated("Detect Language"),
|
||||||
|
translated("Language detection failed."),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translate(term(), term(), term(), term(), term()) :: term()
|
||||||
def translate(socket, post_id, language, reload, append_output) do
|
def translate(socket, post_id, language, reload, append_output) do
|
||||||
if Map.get(socket.assigns, :offline_mode, true) do
|
if Map.get(socket.assigns, :offline_mode, true) do
|
||||||
socket
|
socket
|
||||||
|> append_output.(translated("Translate"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|
|> append_output.(
|
||||||
|
translated("Translate"),
|
||||||
|
translated("Automatic AI actions stay gated by airplane mode."),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
normalized_language = normalize_language(language, "")
|
normalized_language = normalize_language(language, "")
|
||||||
@@ -298,9 +416,18 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
content: translation.content
|
content: translation.content
|
||||||
}) do
|
}) do
|
||||||
socket
|
socket
|
||||||
|> assign(:post_editor_active_languages, Map.put(socket.assigns.post_editor_active_languages, post_id, normalized_language))
|
|> assign(
|
||||||
|> assign(:post_editor_drafts, delete_nested_map(socket.assigns.post_editor_drafts, post_id, normalized_language))
|
:post_editor_active_languages,
|
||||||
|> assign(:post_editor_quick_actions_open, Map.put(socket.assigns.post_editor_quick_actions_open, post_id, false))
|
Map.put(socket.assigns.post_editor_active_languages, post_id, normalized_language)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_drafts,
|
||||||
|
delete_nested_map(socket.assigns.post_editor_drafts, post_id, normalized_language)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_quick_actions_open,
|
||||||
|
Map.put(socket.assigns.post_editor_quick_actions_open, post_id, false)
|
||||||
|
)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
else
|
else
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
@@ -317,6 +444,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec apply_ai_suggestions(term(), term(), term(), term(), term()) :: term()
|
||||||
def apply_ai_suggestions(socket, post_id, fields, reload, append_output) do
|
def apply_ai_suggestions(socket, post_id, fields, reload, append_output) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -340,12 +468,30 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
case Posts.update_post(post_id, attrs) do
|
case Posts.update_post(post_id, attrs) do
|
||||||
{:ok, updated_post} ->
|
{:ok, updated_post} ->
|
||||||
metadata = project_metadata(updated_post.project_id)
|
metadata = project_metadata(updated_post.project_id)
|
||||||
active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language(updated_post, metadata))
|
|
||||||
|
active_language =
|
||||||
|
Map.get(
|
||||||
|
socket.assigns.post_editor_active_languages,
|
||||||
|
post_id,
|
||||||
|
canonical_language(updated_post, metadata)
|
||||||
|
)
|
||||||
|
|
||||||
refreshed_form = persisted_form(updated_post, metadata, active_language)
|
refreshed_form = persisted_form(updated_post, metadata, active_language)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, refreshed_form))
|
|> assign(
|
||||||
|> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, :dirty))
|
:post_editor_drafts,
|
||||||
|
put_nested_map(
|
||||||
|
socket.assigns.post_editor_drafts,
|
||||||
|
post_id,
|
||||||
|
active_language,
|
||||||
|
refreshed_form
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_save_states,
|
||||||
|
Map.put(socket.assigns.post_editor_save_states, post_id, :dirty)
|
||||||
|
)
|
||||||
|> assign(:shell_overlay, nil)
|
|> assign(:shell_overlay, nil)
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
|
|
||||||
@@ -358,6 +504,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec insert_content(term(), term(), term(), term()) :: term()
|
||||||
def insert_content(socket, post_id, snippet, reload) do
|
def insert_content(socket, post_id, snippet, reload) do
|
||||||
socket
|
socket
|
||||||
|> Phoenix.LiveView.push_event("post-editor-insert-content", %{id: post_id, content: snippet})
|
|> Phoenix.LiveView.push_event("post-editor-insert-content", %{id: post_id, content: snippet})
|
||||||
@@ -365,6 +512,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec add_list_value(term(), term(), term(), term(), term()) :: term()
|
||||||
def add_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do
|
def add_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -373,7 +521,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = project_metadata(post.project_id)
|
metadata = project_metadata(post.project_id)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
|
||||||
|
active_language =
|
||||||
|
Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
||||||
|
|
||||||
draft = current_draft(socket.assigns, post, metadata, active_language)
|
draft = current_draft(socket.assigns, post, metadata, active_language)
|
||||||
normalized = normalize_list_entry(value)
|
normalized = normalize_list_entry(value)
|
||||||
|
|
||||||
@@ -398,6 +549,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec remove_list_value(term(), term(), term(), term(), term()) :: term()
|
||||||
def remove_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do
|
def remove_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -406,9 +558,18 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = project_metadata(post.project_id)
|
metadata = project_metadata(post.project_id)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
|
||||||
|
active_language =
|
||||||
|
Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
|
||||||
|
|
||||||
draft = current_draft(socket.assigns, post, metadata, active_language)
|
draft = current_draft(socket.assigns, post, metadata, active_language)
|
||||||
updated = draft |> Map.get(field_key(kind), "") |> csv_to_list() |> Enum.reject(&(&1 == value)) |> Enum.join(", ")
|
|
||||||
|
updated =
|
||||||
|
draft
|
||||||
|
|> Map.get(field_key(kind), "")
|
||||||
|
|> csv_to_list()
|
||||||
|
|> Enum.reject(&(&1 == value))
|
||||||
|
|> Enum.join(", ")
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> put_draft_field(post_id, post, active_language, field_key(kind), updated)
|
|> put_draft_field(post_id, post, active_language, field_key(kind), updated)
|
||||||
@@ -416,6 +577,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build(term()) :: term()
|
||||||
def build(%{current_tab: %{type: :post, id: post_id}} = assigns) do
|
def build(%{current_tab: %{type: :post, id: post_id}} = assigns) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -424,7 +586,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
metadata = assigned_project_metadata(assigns)
|
metadata = assigned_project_metadata(assigns)
|
||||||
canonical_language = canonical_language(post, metadata)
|
canonical_language = canonical_language(post, metadata)
|
||||||
active_language = Map.get(assigns.post_editor_active_languages, post.id, canonical_language)
|
|
||||||
|
active_language =
|
||||||
|
Map.get(assigns.post_editor_active_languages, post.id, canonical_language)
|
||||||
|
|
||||||
translations = translations(post.id)
|
translations = translations(post.id)
|
||||||
persisted = DraftManagement.persisted_form(post, metadata, active_language, translations)
|
persisted = DraftManagement.persisted_form(post, metadata, active_language, translations)
|
||||||
|
|
||||||
@@ -453,13 +618,15 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
metadata_expanded: Map.get(expanded, :metadata, false),
|
metadata_expanded: Map.get(expanded, :metadata, false),
|
||||||
excerpt_expanded: Map.get(expanded, :excerpt, false),
|
excerpt_expanded: Map.get(expanded, :excerpt, false),
|
||||||
mode: Map.get(assigns.post_editor_modes, post.id, :markdown),
|
mode: Map.get(assigns.post_editor_modes, post.id, :markdown),
|
||||||
editing_canonical?: editing_canonical_language?(translations, active_language, canonical_language),
|
editing_canonical?:
|
||||||
|
editing_canonical_language?(translations, active_language, canonical_language),
|
||||||
can_publish?: post.status == :draft,
|
can_publish?: post.status == :draft,
|
||||||
can_delete?: post.status == :published,
|
can_delete?: post.status == :published,
|
||||||
has_published_version?: has_published_version?(post),
|
has_published_version?: has_published_version?(post),
|
||||||
discard_label: discard_label(post),
|
discard_label: discard_label(post),
|
||||||
discard_title: discard_title(post),
|
discard_title: discard_title(post),
|
||||||
detect_language_enabled?: not blank?(Map.get(form, "title")) or not blank?(Map.get(form, "content")),
|
detect_language_enabled?:
|
||||||
|
not blank?(Map.get(form, "title")) or not blank?(Map.get(form, "content")),
|
||||||
can_translate?: Enum.any?(languages(metadata), &(&1 != canonical_language)),
|
can_translate?: Enum.any?(languages(metadata), &(&1 != canonical_language)),
|
||||||
languages: languages(metadata),
|
languages: languages(metadata),
|
||||||
form: form,
|
form: form,
|
||||||
@@ -469,16 +636,45 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
tag_values: tag_values(form),
|
tag_values: tag_values(form),
|
||||||
tag_chips: tag_chips(form, Tags.list_tags(post.project_id)),
|
tag_chips: tag_chips(form, Tags.list_tags(post.project_id)),
|
||||||
tag_query: query_value(assigns, :tags, post.id),
|
tag_query: query_value(assigns, :tags, post.id),
|
||||||
tag_query_addable?: query_addable?(query_value(assigns, :tags, post.id), tag_values(form), Tags.list_tags(post.project_id), fn option -> option.name end),
|
tag_query_addable?:
|
||||||
|
query_addable?(
|
||||||
|
query_value(assigns, :tags, post.id),
|
||||||
|
tag_values(form),
|
||||||
|
Tags.list_tags(post.project_id),
|
||||||
|
fn option -> option.name end
|
||||||
|
),
|
||||||
category_values: category_values(form),
|
category_values: category_values(form),
|
||||||
category_query: query_value(assigns, :categories, post.id),
|
category_query: query_value(assigns, :categories, post.id),
|
||||||
category_options: metadata.categories || [],
|
category_options: metadata.categories || [],
|
||||||
category_query_addable?: query_addable?(query_value(assigns, :categories, post.id), category_values(form), metadata.categories || [], & &1),
|
category_query_addable?:
|
||||||
tag_suggestions: tag_suggestions(form, Tags.list_tags(post.project_id), query_value(assigns, :tags, post.id)),
|
query_addable?(
|
||||||
category_suggestions: category_suggestions(form, metadata.categories || [], query_value(assigns, :categories, post.id)),
|
query_value(assigns, :categories, post.id),
|
||||||
|
category_values(form),
|
||||||
|
metadata.categories || [],
|
||||||
|
& &1
|
||||||
|
),
|
||||||
|
tag_suggestions:
|
||||||
|
tag_suggestions(
|
||||||
|
form,
|
||||||
|
Tags.list_tags(post.project_id),
|
||||||
|
query_value(assigns, :tags, post.id)
|
||||||
|
),
|
||||||
|
category_suggestions:
|
||||||
|
category_suggestions(
|
||||||
|
form,
|
||||||
|
metadata.categories || [],
|
||||||
|
query_value(assigns, :categories, post.id)
|
||||||
|
),
|
||||||
gallery_count: gallery_count(form),
|
gallery_count: gallery_count(form),
|
||||||
preview_url: preview_url(post, active_language, canonical_language, Map.get(assigns.post_editor_modes, post.id, :markdown)),
|
preview_url:
|
||||||
translation_flags: translation_flags(post, canonical_language, active_language, translations),
|
preview_url(
|
||||||
|
post,
|
||||||
|
active_language,
|
||||||
|
canonical_language,
|
||||||
|
Map.get(assigns.post_editor_modes, post.id, :markdown)
|
||||||
|
),
|
||||||
|
translation_flags:
|
||||||
|
translation_flags(post, canonical_language, active_language, translations),
|
||||||
linked_media: linked_media(post.id),
|
linked_media: linked_media(post.id),
|
||||||
post_links: post_links(post.id),
|
post_links: post_links(post.id),
|
||||||
footer: footer(post, current_translation, active_language, canonical_language)
|
footer: footer(post, current_translation, active_language, canonical_language)
|
||||||
@@ -488,17 +684,21 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|
|
||||||
def build(_assigns), do: nil
|
def build(_assigns), do: nil
|
||||||
|
|
||||||
|
@spec post_status_label(term()) :: term()
|
||||||
def post_status_label(status), do: ShellData.dashboard_status_label(status)
|
def post_status_label(status), do: ShellData.dashboard_status_label(status)
|
||||||
|
|
||||||
|
@spec post_editor_save_state_label(term()) :: term()
|
||||||
def post_editor_save_state_label(:dirty), do: translated("Unsaved")
|
def post_editor_save_state_label(:dirty), do: translated("Unsaved")
|
||||||
def post_editor_save_state_label(:saved), do: translated("Saved")
|
def post_editor_save_state_label(:saved), do: translated("Saved")
|
||||||
def post_editor_save_state_label(:published), do: translated("Published")
|
def post_editor_save_state_label(:published), do: translated("Published")
|
||||||
def post_editor_save_state_label(:discarded), do: translated("Reverted")
|
def post_editor_save_state_label(:discarded), do: translated("Reverted")
|
||||||
def post_editor_save_state_label(_state), do: translated("Idle")
|
def post_editor_save_state_label(_state), do: translated("Idle")
|
||||||
|
|
||||||
|
@spec post_editor_mode_label(term()) :: term()
|
||||||
def post_editor_mode_label(:markdown), do: translated("Markdown")
|
def post_editor_mode_label(:markdown), do: translated("Markdown")
|
||||||
def post_editor_mode_label(:preview), do: translated("Preview")
|
def post_editor_mode_label(:preview), do: translated("Preview")
|
||||||
|
|
||||||
|
@spec translated(term(), term()) :: term()
|
||||||
def translated(text, bindings \\ %{}),
|
def translated(text, bindings \\ %{}),
|
||||||
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
alias BDS.Desktop.ShellLive.PostEditor.PostMetadata
|
alias BDS.Desktop.ShellLive.PostEditor.PostMetadata
|
||||||
alias BDS.UI.Workbench
|
alias BDS.UI.Workbench
|
||||||
|
|
||||||
|
@spec normalize_mode(term()) :: term()
|
||||||
def normalize_mode(mode) when mode in [:markdown, :preview], do: mode
|
def normalize_mode(mode) when mode in [:markdown, :preview], do: mode
|
||||||
|
@spec normalize_mode(term()) :: term()
|
||||||
def normalize_mode("visual"), do: :markdown
|
def normalize_mode("visual"), do: :markdown
|
||||||
def normalize_mode("preview"), do: :preview
|
def normalize_mode("preview"), do: :preview
|
||||||
def normalize_mode(_mode), do: :markdown
|
def normalize_mode(_mode), do: :markdown
|
||||||
|
|
||||||
|
@spec normalize_language(term(), term()) :: term()
|
||||||
def normalize_language(value, fallback) do
|
def normalize_language(value, fallback) do
|
||||||
case value |> to_string() |> String.trim() do
|
case value |> to_string() |> String.trim() do
|
||||||
"" -> fallback
|
"" -> fallback
|
||||||
@@ -20,6 +23,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec normalize_params(term(), term(), term()) :: term()
|
||||||
def normalize_params(params, current_language, next_language) do
|
def normalize_params(params, current_language, next_language) do
|
||||||
%{
|
%{
|
||||||
"title" => Map.get(params, "title", ""),
|
"title" => Map.get(params, "title", ""),
|
||||||
@@ -28,12 +32,17 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
"tags" => Map.get(params, "tags", ""),
|
"tags" => Map.get(params, "tags", ""),
|
||||||
"categories" => Map.get(params, "categories", ""),
|
"categories" => Map.get(params, "categories", ""),
|
||||||
"author" => Map.get(params, "author", ""),
|
"author" => Map.get(params, "author", ""),
|
||||||
"language" => if(current_language == next_language, do: normalize_language(Map.get(params, "language"), current_language), else: next_language),
|
"language" =>
|
||||||
|
if(current_language == next_language,
|
||||||
|
do: normalize_language(Map.get(params, "language"), current_language),
|
||||||
|
else: next_language
|
||||||
|
),
|
||||||
"do_not_translate" => truthy?(Map.get(params, "do_not_translate")),
|
"do_not_translate" => truthy?(Map.get(params, "do_not_translate")),
|
||||||
"template_slug" => Map.get(params, "template_slug", "")
|
"template_slug" => Map.get(params, "template_slug", "")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec current_draft(term(), term(), term(), term()) :: term()
|
||||||
def current_draft(assigns, %Post{} = post, metadata, active_language) do
|
def current_draft(assigns, %Post{} = post, metadata, active_language) do
|
||||||
persisted = persisted_form(post, metadata, active_language)
|
persisted = persisted_form(post, metadata, active_language)
|
||||||
|
|
||||||
@@ -42,10 +51,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
|> Map.get(active_language, persisted)
|
|> Map.get(active_language, persisted)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec persisted_form(term(), term(), term()) :: term()
|
||||||
def persisted_form(%Post{} = post, metadata, active_language) do
|
def persisted_form(%Post{} = post, metadata, active_language) do
|
||||||
persisted_form(post, metadata, active_language, PostMetadata.translations(post.id))
|
persisted_form(post, metadata, active_language, PostMetadata.translations(post.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec persisted_form(term(), term(), term(), term()) :: term()
|
||||||
def persisted_form(post, metadata, active_language, translations) do
|
def persisted_form(post, metadata, active_language, translations) do
|
||||||
canonical_language = PostMetadata.canonical_language(post, metadata)
|
canonical_language = PostMetadata.canonical_language(post, metadata)
|
||||||
translation = Map.get(translations, active_language)
|
translation = Map.get(translations, active_language)
|
||||||
@@ -64,8 +75,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
%{
|
%{
|
||||||
"title" => translation && translation.title || "",
|
"title" => (translation && translation.title) || "",
|
||||||
"excerpt" => translation && translation.excerpt || "",
|
"excerpt" => (translation && translation.excerpt) || "",
|
||||||
"content" => if(translation, do: Posts.editor_body(translation), else: ""),
|
"content" => if(translation, do: Posts.editor_body(translation), else: ""),
|
||||||
"tags" => Enum.join(post.tags || [], ", "),
|
"tags" => Enum.join(post.tags || [], ", "),
|
||||||
"categories" => Enum.join(post.categories || [], ", "),
|
"categories" => Enum.join(post.categories || [], ", "),
|
||||||
@@ -77,22 +88,43 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec maybe_update_draft(term(), term(), term(), term(), term(), term(), term()) :: term()
|
||||||
def maybe_update_draft(socket, post_id, post, current_language, next_language, draft, true) do
|
def maybe_update_draft(socket, post_id, post, current_language, next_language, draft, true) do
|
||||||
workbench = Workbench.mark_dirty(socket.assigns.workbench, :post, post_id)
|
workbench = Workbench.mark_dirty(socket.assigns.workbench, :post, post_id)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, next_language, draft))
|
|> assign(
|
||||||
|> assign(:post_editor_active_languages, Map.put(socket.assigns.post_editor_active_languages, post_id, next_language))
|
:post_editor_drafts,
|
||||||
|> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, :dirty))
|
put_nested_map(socket.assigns.post_editor_drafts, post_id, next_language, draft)
|
||||||
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: draft["title"], subtitle: Atom.to_string(post.status || :draft)}))
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_active_languages,
|
||||||
|
Map.put(socket.assigns.post_editor_active_languages, post_id, next_language)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_save_states,
|
||||||
|
Map.put(socket.assigns.post_editor_save_states, post_id, :dirty)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:tab_meta,
|
||||||
|
Map.put(socket.assigns.tab_meta, {:post, post_id}, %{
|
||||||
|
title: draft["title"],
|
||||||
|
subtitle: Atom.to_string(post.status || :draft)
|
||||||
|
})
|
||||||
|
)
|
||||||
|> maybe_drop_old_language_draft(post_id, current_language, next_language)
|
|> maybe_drop_old_language_draft(post_id, current_language, next_language)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_update_draft(socket, post_id, _post, _current_language, next_language, _draft, false) do
|
def maybe_update_draft(socket, post_id, _post, _current_language, next_language, _draft, false) do
|
||||||
assign(socket, :post_editor_active_languages, Map.put(socket.assigns.post_editor_active_languages, post_id, next_language))
|
assign(
|
||||||
|
socket,
|
||||||
|
:post_editor_active_languages,
|
||||||
|
Map.put(socket.assigns.post_editor_active_languages, post_id, next_language)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec put_draft_field(term(), term(), term(), term(), term(), term()) :: term()
|
||||||
def put_draft_field(socket, post_id, post, active_language, field, value) do
|
def put_draft_field(socket, post_id, post, active_language, field, value) do
|
||||||
metadata = PostMetadata.project_metadata(post.project_id)
|
metadata = PostMetadata.project_metadata(post.project_id)
|
||||||
draft = Map.put(current_draft(socket.assigns, post, metadata, active_language), field, value)
|
draft = Map.put(current_draft(socket.assigns, post, metadata, active_language), field, value)
|
||||||
@@ -100,15 +132,28 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, draft))
|
|> assign(
|
||||||
|> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, :dirty))
|
:post_editor_drafts,
|
||||||
|
put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, draft)
|
||||||
|
)
|
||||||
|
|> assign(
|
||||||
|
:post_editor_save_states,
|
||||||
|
Map.put(socket.assigns.post_editor_save_states, post_id, :dirty)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec put_query_state(term(), term(), term(), term()) :: term()
|
||||||
def put_query_state(socket, post_id, kind, value) do
|
def put_query_state(socket, post_id, kind, value) do
|
||||||
key = query_key(kind)
|
key = query_key(kind)
|
||||||
assign(socket, key, Map.put(Map.get(socket.assigns, key, %{}), post_id, to_string(value || "")))
|
|
||||||
|
assign(
|
||||||
|
socket,
|
||||||
|
key,
|
||||||
|
Map.put(Map.get(socket.assigns, key, %{}), post_id, to_string(value || ""))
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec query_value(term(), term(), term()) :: term()
|
||||||
def query_value(assigns, kind, post_id) do
|
def query_value(assigns, kind, post_id) do
|
||||||
assigns
|
assigns
|
||||||
|> Map.get(query_key(kind), %{})
|
|> Map.get(query_key(kind), %{})
|
||||||
@@ -118,25 +163,33 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
defp query_key(:tags), do: :post_editor_tag_queries
|
defp query_key(:tags), do: :post_editor_tag_queries
|
||||||
defp query_key(:categories), do: :post_editor_category_queries
|
defp query_key(:categories), do: :post_editor_category_queries
|
||||||
|
|
||||||
defp maybe_drop_old_language_draft(socket, _post_id, current_language, next_language) when current_language == next_language,
|
defp maybe_drop_old_language_draft(socket, _post_id, current_language, next_language)
|
||||||
do: socket
|
when current_language == next_language,
|
||||||
|
do: socket
|
||||||
|
|
||||||
defp maybe_drop_old_language_draft(socket, post_id, current_language, _next_language) do
|
defp maybe_drop_old_language_draft(socket, post_id, current_language, _next_language) do
|
||||||
assign(socket, :post_editor_drafts, delete_nested_map(socket.assigns.post_editor_drafts, post_id, current_language))
|
assign(
|
||||||
|
socket,
|
||||||
|
:post_editor_drafts,
|
||||||
|
delete_nested_map(socket.assigns.post_editor_drafts, post_id, current_language)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggled_sections(term(), term(), term()) :: term()
|
||||||
def toggled_sections(expanded_by_post, post_id, section) do
|
def toggled_sections(expanded_by_post, post_id, section) do
|
||||||
expanded_by_post
|
expanded_by_post
|
||||||
|> Map.get(post_id, %{metadata: false, excerpt: false})
|
|> Map.get(post_id, %{metadata: false, excerpt: false})
|
||||||
|> Map.put_new(:metadata, false)
|
|> Map.put_new(:metadata, false)
|
||||||
|> Map.put_new(:excerpt, false)
|
|> Map.put_new(:excerpt, false)
|
||||||
|> Map.update!(section, ¬ &1)
|
|> Map.update!(section, &(not &1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec put_nested_map(term(), term(), term(), term()) :: term()
|
||||||
def put_nested_map(map, key, nested_key, value) do
|
def put_nested_map(map, key, nested_key, value) do
|
||||||
Map.update(map, key, %{nested_key => value}, &Map.put(&1, nested_key, value))
|
Map.update(map, key, %{nested_key => value}, &Map.put(&1, nested_key, value))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_nested_map(term(), term(), term()) :: term()
|
||||||
def delete_nested_map(map, key, nested_key) do
|
def delete_nested_map(map, key, nested_key) do
|
||||||
case Map.get(map, key) do
|
case Map.get(map, key) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -150,20 +203,26 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload_with_assigned_workbench(socket, reload), do: reload.(socket, socket.assigns.workbench)
|
@spec reload_with_assigned_workbench(term(), term()) :: term()
|
||||||
|
def reload_with_assigned_workbench(socket, reload),
|
||||||
|
do: reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
|
@spec save_state_for_action(term()) :: term()
|
||||||
def save_state_for_action(:publish), do: :published
|
def save_state_for_action(:publish), do: :published
|
||||||
def save_state_for_action(_action), do: :saved
|
def save_state_for_action(_action), do: :saved
|
||||||
|
|
||||||
|
@spec record_title(term(), term()) :: term()
|
||||||
def record_title(%Translation{title: title}, post),
|
def record_title(%Translation{title: title}, post),
|
||||||
do: blank_to_nil(title) || post.title || post.slug || post.id
|
do: blank_to_nil(title) || post.title || post.slug || post.id
|
||||||
|
|
||||||
def record_title(%Post{title: title, slug: slug, id: id}, _post),
|
def record_title(%Post{title: title, slug: slug, id: id}, _post),
|
||||||
do: blank_to_nil(title) || blank_to_nil(slug) || id
|
do: blank_to_nil(title) || blank_to_nil(slug) || id
|
||||||
|
|
||||||
|
@spec record_status(term()) :: term()
|
||||||
def record_status(%Translation{status: status}), do: status || :draft
|
def record_status(%Translation{status: status}), do: status || :draft
|
||||||
def record_status(%Post{status: status}), do: status || :draft
|
def record_status(%Post{status: status}), do: status || :draft
|
||||||
|
|
||||||
|
@spec editing_canonical_language?(term(), term(), term()) :: term()
|
||||||
def editing_canonical_language?(translations, active_language, canonical_language) do
|
def editing_canonical_language?(translations, active_language, canonical_language) do
|
||||||
active_language == canonical_language or not Map.has_key?(translations, active_language)
|
active_language == canonical_language or not Map.has_key?(translations, active_language)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,17 +3,22 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
|
|
||||||
alias BDS.{Metadata, Tags}
|
alias BDS.{Metadata, Tags}
|
||||||
|
|
||||||
|
@spec field_key(term()) :: term()
|
||||||
def field_key(:tags), do: "tags"
|
def field_key(:tags), do: "tags"
|
||||||
def field_key(:categories), do: "categories"
|
def field_key(:categories), do: "categories"
|
||||||
|
|
||||||
|
@spec tag_values(term()) :: term()
|
||||||
def tag_values(form), do: csv_to_list(Map.get(form, "tags", ""))
|
def tag_values(form), do: csv_to_list(Map.get(form, "tags", ""))
|
||||||
|
@spec category_values(term()) :: term()
|
||||||
def category_values(form), do: csv_to_list(Map.get(form, "categories", ""))
|
def category_values(form), do: csv_to_list(Map.get(form, "categories", ""))
|
||||||
|
|
||||||
|
@spec tag_suggestions(term(), term(), term()) :: term()
|
||||||
def tag_suggestions(form, options, query) do
|
def tag_suggestions(form, options, query) do
|
||||||
selected = MapSet.new(tag_values(form))
|
selected = MapSet.new(tag_values(form))
|
||||||
filter_suggestions(options, query, fn option -> option.name end, selected)
|
filter_suggestions(options, query, fn option -> option.name end, selected)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec tag_chips(term(), term()) :: term()
|
||||||
def tag_chips(form, options) do
|
def tag_chips(form, options) do
|
||||||
option_map = Map.new(options, fn option -> {option.name, option} end)
|
option_map = Map.new(options, fn option -> {option.name, option} end)
|
||||||
|
|
||||||
@@ -23,6 +28,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec category_suggestions(term(), term(), term()) :: term()
|
||||||
def category_suggestions(form, options, query) do
|
def category_suggestions(form, options, query) do
|
||||||
selected = MapSet.new(category_values(form))
|
selected = MapSet.new(category_values(form))
|
||||||
filter_suggestions(options, query, & &1, selected)
|
filter_suggestions(options, query, & &1, selected)
|
||||||
@@ -34,11 +40,14 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
options
|
options
|
||||||
|> Enum.filter(fn option ->
|
|> Enum.filter(fn option ->
|
||||||
label = labeler.(option)
|
label = labeler.(option)
|
||||||
not MapSet.member?(selected, label) and (query == "" or String.contains?(String.downcase(label), query))
|
|
||||||
|
not MapSet.member?(selected, label) and
|
||||||
|
(query == "" or String.contains?(String.downcase(label), query))
|
||||||
end)
|
end)
|
||||||
|> Enum.take(8)
|
|> Enum.take(8)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec query_addable?(term(), term(), term(), term()) :: term()
|
||||||
def query_addable?(query, selected_values, options, labeler) do
|
def query_addable?(query, selected_values, options, labeler) do
|
||||||
normalized = normalize_query(query)
|
normalized = normalize_query(query)
|
||||||
|
|
||||||
@@ -54,6 +63,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
|> String.downcase()
|
|> String.downcase()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec normalize_list_entry(term()) :: term()
|
||||||
def normalize_list_entry(value) do
|
def normalize_list_entry(value) do
|
||||||
value
|
value
|
||||||
|> to_string()
|
|> to_string()
|
||||||
@@ -61,6 +71,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
|> String.downcase()
|
|> String.downcase()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec ensure_list_value(term(), term(), term()) :: term()
|
||||||
def ensure_list_value(project_id, :tags, value) do
|
def ensure_list_value(project_id, :tags, value) do
|
||||||
if Enum.any?(Tags.list_tags(project_id), &(String.downcase(&1.name) == value)) do
|
if Enum.any?(Tags.list_tags(project_id), &(String.downcase(&1.name) == value)) do
|
||||||
:ok
|
:ok
|
||||||
@@ -83,6 +94,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
_error -> :ok
|
_error -> :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec csv_to_list(term()) :: term()
|
||||||
def csv_to_list(value) do
|
def csv_to_list(value) do
|
||||||
value
|
value
|
||||||
|> to_string()
|
|> to_string()
|
||||||
@@ -91,6 +103,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
|> Enum.reject(&(&1 == ""))
|
|> Enum.reject(&(&1 == ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec tag_chip_style(term()) :: term()
|
||||||
def tag_chip_style(nil), do: nil
|
def tag_chip_style(nil), do: nil
|
||||||
|
|
||||||
def tag_chip_style(color) do
|
def tag_chip_style(color) do
|
||||||
@@ -121,5 +134,6 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|
|||||||
|
|
||||||
defp contrast_color(_color), do: "#ffffff"
|
defp contrast_color(_color), do: "#ffffff"
|
||||||
|
|
||||||
|
@spec ai_overlay_fields(term()) :: term()
|
||||||
def ai_overlay_fields(selected), do: Enum.filter(selected, & &1.accepted)
|
def ai_overlay_fields(selected), do: Enum.filter(selected, & &1.accepted)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
|
|||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, PostMetadata}
|
alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, PostMetadata}
|
||||||
|
|
||||||
|
@spec persist(term(), term(), term(), term(), term()) :: term()
|
||||||
def persist(%Post{} = post, draft, active_language, metadata, action) do
|
def persist(%Post{} = post, draft, active_language, metadata, action) do
|
||||||
canonical_language = PostMetadata.canonical_language(post, metadata)
|
canonical_language = PostMetadata.canonical_language(post, metadata)
|
||||||
translations = PostMetadata.translations(post.id)
|
translations = PostMetadata.translations(post.id)
|
||||||
|
|
||||||
if DraftManagement.editing_canonical_language?(translations, active_language, canonical_language) do
|
if DraftManagement.editing_canonical_language?(
|
||||||
|
translations,
|
||||||
|
active_language,
|
||||||
|
canonical_language
|
||||||
|
) do
|
||||||
post
|
post
|
||||||
|> save_canonical_draft(draft)
|
|> save_canonical_draft(draft)
|
||||||
|> maybe_publish_post(post.id, action)
|
|> maybe_publish_post(post.id, action)
|
||||||
@@ -21,12 +26,17 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec discard(term(), term(), term()) :: term()
|
||||||
def discard(%Post{} = post, active_language, metadata) do
|
def discard(%Post{} = post, active_language, metadata) do
|
||||||
canonical_language = PostMetadata.canonical_language(post, metadata)
|
canonical_language = PostMetadata.canonical_language(post, metadata)
|
||||||
current_translations = PostMetadata.translations(post.id)
|
current_translations = PostMetadata.translations(post.id)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
not DraftManagement.editing_canonical_language?(current_translations, active_language, canonical_language) ->
|
not DraftManagement.editing_canonical_language?(
|
||||||
|
current_translations,
|
||||||
|
active_language,
|
||||||
|
canonical_language
|
||||||
|
) ->
|
||||||
{:ok, post}
|
{:ok, post}
|
||||||
|
|
||||||
post.file_path not in [nil, ""] and post.status == :draft ->
|
post.file_path not in [nil, ""] and post.status == :draft ->
|
||||||
@@ -37,15 +47,18 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec has_published_version?(term()) :: term()
|
||||||
def has_published_version?(%Post{} = post),
|
def has_published_version?(%Post{} = post),
|
||||||
do: not is_nil(post.published_at) or post.file_path not in [nil, ""]
|
do: not is_nil(post.published_at) or post.file_path not in [nil, ""]
|
||||||
|
|
||||||
|
@spec discard_label(term()) :: term()
|
||||||
def discard_label(%Post{} = post) do
|
def discard_label(%Post{} = post) do
|
||||||
if has_published_version?(post),
|
if has_published_version?(post),
|
||||||
do: translated("Discard Changes"),
|
do: translated("Discard Changes"),
|
||||||
else: translated("Discard Draft")
|
else: translated("Discard Draft")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec discard_title(term()) :: term()
|
||||||
def discard_title(%Post{} = post) do
|
def discard_title(%Post{} = post) do
|
||||||
if has_published_version?(post),
|
if has_published_version?(post),
|
||||||
do: translated("Discard changes and restore the published version"),
|
do: translated("Discard changes and restore the published version"),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
alias BDS.Media.Media, as: MediaRecord
|
alias BDS.Media.Media, as: MediaRecord
|
||||||
alias BDS.Posts.{Post, PostMedia}
|
alias BDS.Posts.{Post, PostMedia}
|
||||||
|
|
||||||
|
@spec project_metadata(term()) :: term()
|
||||||
def project_metadata(nil), do: %{main_language: "en", blog_languages: []}
|
def project_metadata(nil), do: %{main_language: "en", blog_languages: []}
|
||||||
|
|
||||||
def project_metadata(project_id) do
|
def project_metadata(project_id) do
|
||||||
@@ -17,6 +18,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
_error -> %{main_language: "en", blog_languages: []}
|
_error -> %{main_language: "en", blog_languages: []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec canonical_language(term(), term()) :: term()
|
||||||
def canonical_language(post, metadata) do
|
def canonical_language(post, metadata) do
|
||||||
BDS.Desktop.ShellLive.PostEditor.DraftManagement.normalize_language(
|
BDS.Desktop.ShellLive.PostEditor.DraftManagement.normalize_language(
|
||||||
post.language,
|
post.language,
|
||||||
@@ -24,28 +26,36 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translations(term()) :: term()
|
||||||
def translations(post_id) do
|
def translations(post_id) do
|
||||||
{:ok, translations} = Posts.list_post_translations(post_id)
|
{:ok, translations} = Posts.list_post_translations(post_id)
|
||||||
Map.new(translations, fn translation -> {translation.language, translation} end)
|
Map.new(translations, fn translation -> {translation.language, translation} end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec languages(term()) :: term()
|
||||||
def languages(metadata) do
|
def languages(metadata) do
|
||||||
(([metadata.main_language || "en"] ++ (metadata.blog_languages || [])) ++ Enum.map(I18n.supported_languages(), & &1.code))
|
(([metadata.main_language || "en"] ++ (metadata.blog_languages || [])) ++
|
||||||
|
Enum.map(I18n.supported_languages(), & &1.code))
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec template_options(term()) :: term()
|
||||||
def template_options(project_id) do
|
def template_options(project_id) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from template in Templates.Template,
|
from template in Templates.Template,
|
||||||
where: template.project_id == ^project_id,
|
where: template.project_id == ^project_id,
|
||||||
order_by: [asc: template.title, asc: template.slug],
|
order_by: [asc: template.title, asc: template.slug],
|
||||||
select: %{slug: template.slug, title: fragment("COALESCE(?, ?)", template.title, template.slug)}
|
select: %{
|
||||||
|
slug: template.slug,
|
||||||
|
title: fragment("COALESCE(?, ?)", template.title, template.slug)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
rescue
|
rescue
|
||||||
_error -> []
|
_error -> []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec linked_media(term()) :: term()
|
||||||
def linked_media(post_id) do
|
def linked_media(post_id) do
|
||||||
rows =
|
rows =
|
||||||
Repo.all(
|
Repo.all(
|
||||||
@@ -74,6 +84,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
_error -> []
|
_error -> []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec post_links(term()) :: term()
|
||||||
def post_links(post_id) do
|
def post_links(post_id) do
|
||||||
%{
|
%{
|
||||||
backlinks: related_posts(PostLinks.list_incoming_links(post_id), :source_post_id),
|
backlinks: related_posts(PostLinks.list_incoming_links(post_id), :source_post_id),
|
||||||
@@ -84,15 +95,29 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
defp related_posts(links, key) do
|
defp related_posts(links, key) do
|
||||||
Enum.map(links, fn link ->
|
Enum.map(links, fn link ->
|
||||||
case Posts.get_post(Map.fetch!(link, key)) do
|
case Posts.get_post(Map.fetch!(link, key)) do
|
||||||
%Post{} = post -> %{id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id}
|
%Post{} = post ->
|
||||||
_other -> nil
|
%{
|
||||||
|
id: post.id,
|
||||||
|
title: post.title || post.slug || post.id,
|
||||||
|
text: link.link_text || post.slug || post.id
|
||||||
|
}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translation_flags(term(), term(), term(), term()) :: term()
|
||||||
def translation_flags(post, canonical_language, active_language, translations) do
|
def translation_flags(post, canonical_language, active_language, translations) do
|
||||||
canonical = %{language: canonical_language, flag: I18n.flag(canonical_language), status: Atom.to_string(post.status || :draft), active: active_language == canonical_language, label: canonical_language}
|
canonical = %{
|
||||||
|
language: canonical_language,
|
||||||
|
flag: I18n.flag(canonical_language),
|
||||||
|
status: Atom.to_string(post.status || :draft),
|
||||||
|
active: active_language == canonical_language,
|
||||||
|
label: canonical_language
|
||||||
|
}
|
||||||
|
|
||||||
others =
|
others =
|
||||||
translations
|
translations
|
||||||
@@ -111,6 +136,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
[canonical | others]
|
[canonical | others]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec footer(term(), term(), term(), term()) :: term()
|
||||||
def footer(post, translation, active_language, canonical_language) do
|
def footer(post, translation, active_language, canonical_language) do
|
||||||
if active_language == canonical_language do
|
if active_language == canonical_language do
|
||||||
%{
|
%{
|
||||||
@@ -120,8 +146,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
%{
|
%{
|
||||||
created_at: format_timestamp(translation && translation.created_at || post.created_at),
|
created_at: format_timestamp((translation && translation.created_at) || post.created_at),
|
||||||
updated_at: format_timestamp(translation && translation.updated_at || post.updated_at),
|
updated_at: format_timestamp((translation && translation.updated_at) || post.updated_at),
|
||||||
published_at: format_timestamp(translation && translation.published_at)
|
published_at: format_timestamp(translation && translation.published_at)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -135,10 +161,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
|> Calendar.strftime("%x")
|
|> Calendar.strftime("%x")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec display_title(term(), term(), term()) :: term()
|
||||||
def display_title(title, slug, fallback_id) do
|
def display_title(title, slug, fallback_id) do
|
||||||
blank_to_nil(title) || blank_to_nil(slug) || fallback_id || translated("Untitled")
|
blank_to_nil(title) || blank_to_nil(slug) || fallback_id || translated("Untitled")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec gallery_count(term()) :: term()
|
||||||
def gallery_count(form) do
|
def gallery_count(form) do
|
||||||
form
|
form
|
||||||
|> Map.get("content", "")
|
|> Map.get("content", "")
|
||||||
@@ -147,8 +175,11 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
|> length()
|
|> length()
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_url(_post, _active_language, _canonical_language, mode) when mode != :preview, do: nil
|
@spec preview_url(term(), term(), term(), term()) :: term()
|
||||||
|
def preview_url(_post, _active_language, _canonical_language, mode) when mode != :preview,
|
||||||
|
do: nil
|
||||||
|
|
||||||
|
@spec preview_url(term(), term(), term(), term()) :: term()
|
||||||
def preview_url(%Post{} = post, active_language, canonical_language, :preview) do
|
def preview_url(%Post{} = post, active_language, canonical_language, :preview) do
|
||||||
query =
|
query =
|
||||||
%{}
|
%{}
|
||||||
@@ -156,7 +187,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
|> maybe_put_query("post_id", post.id)
|
|> maybe_put_query("post_id", post.id)
|
||||||
|> maybe_put_query("lang", active_language != canonical_language && active_language)
|
|> maybe_put_query("lang", active_language != canonical_language && active_language)
|
||||||
|
|
||||||
Preview.base_url() <> canonical_preview_path(post.created_at, post.slug) <> "?" <> URI.encode_query(query)
|
Preview.base_url() <>
|
||||||
|
canonical_preview_path(post.created_at, post.slug) <> "?" <> URI.encode_query(query)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp canonical_preview_path(created_at_ms, slug) do
|
defp canonical_preview_path(created_at_ms, slug) do
|
||||||
@@ -171,10 +203,13 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
defp maybe_put_query(query, key, value), do: Map.put(query, key, value)
|
defp maybe_put_query(query, key, value), do: Map.put(query, key, value)
|
||||||
|
|
||||||
def truthy?(value) when value in [true, "true", "on", 1, "1"], do: true
|
def truthy?(value) when value in [true, "true", "on", 1, "1"], do: true
|
||||||
|
@spec truthy?(term()) :: term()
|
||||||
def truthy?(_value), do: false
|
def truthy?(_value), do: false
|
||||||
|
|
||||||
|
@spec blank?(term()) :: term()
|
||||||
def blank?(value), do: blank_to_nil(value) == nil
|
def blank?(value), do: blank_to_nil(value) == nil
|
||||||
|
|
||||||
|
@spec blank_to_nil(term()) :: term()
|
||||||
def blank_to_nil(value) do
|
def blank_to_nil(value) do
|
||||||
value
|
value
|
||||||
|> to_string()
|
|> to_string()
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ defmodule BDS.Desktop.ShellLive.SessionUtil do
|
|||||||
Stream.iterate(1, &(&1 + 1))
|
Stream.iterate(1, &(&1 + 1))
|
||||||
|> Enum.find_value(fn index ->
|
|> Enum.find_value(fn index ->
|
||||||
candidate =
|
candidate =
|
||||||
if index == 1, do: @default_new_project_name, else: "#{@default_new_project_name} #{index}"
|
if index == 1,
|
||||||
|
do: @default_new_project_name,
|
||||||
|
else: "#{@default_new_project_name} #{index}"
|
||||||
|
|
||||||
if MapSet.member?(existing_names, candidate), do: nil, else: candidate
|
if MapSet.member?(existing_names, candidate), do: nil, else: candidate
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings
|
alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings
|
||||||
alias BDS.Desktop.ShellLive.SettingsEditor.StyleEditor
|
alias BDS.Desktop.ShellLive.SettingsEditor.StyleEditor
|
||||||
|
|
||||||
embed_templates "settings_editor_html/*"
|
embed_templates("settings_editor_html/*")
|
||||||
|
|
||||||
@settings_sections ~w(project editor content ai technology publishing data mcp)
|
@settings_sections ~w(project editor content ai technology publishing data mcp)
|
||||||
@supported_languages ["en", "de", "fr", "it", "es"]
|
@supported_languages ["en", "de", "fr", "it", "es"]
|
||||||
@@ -45,6 +45,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
defdelegate theme_display_name(theme), to: StyleEditor
|
defdelegate theme_display_name(theme), to: StyleEditor
|
||||||
defdelegate protected_category?(category), to: ManagedCategories
|
defdelegate protected_category?(category), to: ManagedCategories
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
case socket.assigns[:current_tab] do
|
case socket.assigns[:current_tab] do
|
||||||
%{type: :settings} ->
|
%{type: :settings} ->
|
||||||
@@ -64,12 +65,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_search(term(), term(), term()) :: term()
|
||||||
def update_search(socket, query, reload) do
|
def update_search(socket, query, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_search, to_string(query || ""))
|
|> assign(:settings_editor_search, to_string(query || ""))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build_settings(term()) :: term()
|
||||||
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
|
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
|
||||||
|
|
||||||
def build_settings(assigns) do
|
def build_settings(assigns) do
|
||||||
@@ -82,7 +85,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
)
|
)
|
||||||
|
|
||||||
editor_form =
|
editor_form =
|
||||||
Map.merge(EditorSettings.editor_form(), Map.get(assigns, :settings_editor_editor_draft, %{}))
|
Map.merge(
|
||||||
|
EditorSettings.editor_form(),
|
||||||
|
Map.get(assigns, :settings_editor_editor_draft, %{})
|
||||||
|
)
|
||||||
|
|
||||||
ai_form =
|
ai_form =
|
||||||
Map.merge(AISettings.ai_form(assigns), Map.get(assigns, :settings_editor_ai_draft, %{}))
|
Map.merge(AISettings.ai_form(assigns), Map.get(assigns, :settings_editor_ai_draft, %{}))
|
||||||
@@ -142,6 +148,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec translated(term(), term()) :: term()
|
||||||
def translated(text, bindings \\ %{}),
|
def translated(text, bindings \\ %{}),
|
||||||
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
@@ -171,7 +178,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
Enum.filter(@settings_sections, fn section ->
|
Enum.filter(@settings_sections, fn section ->
|
||||||
case section do
|
case section do
|
||||||
"project" ->
|
"project" ->
|
||||||
section_matches?(query, ~w(project name description data url language author bookmarklet))
|
section_matches?(
|
||||||
|
query,
|
||||||
|
~w(project name description data url language author bookmarklet)
|
||||||
|
)
|
||||||
|
|
||||||
"editor" ->
|
"editor" ->
|
||||||
section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged))
|
section_matches?(query, ~w(editor mode markdown preview diff wrap unchanged))
|
||||||
@@ -195,7 +205,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
|||||||
section_matches?(query, ~w(data rebuild maintenance links thumbnails filesystem))
|
section_matches?(query, ~w(data rebuild maintenance links thumbnails filesystem))
|
||||||
|
|
||||||
"mcp" ->
|
"mcp" ->
|
||||||
section_matches?(query, ~w(mcp claude copilot gemini opencode mistral codex agent server))
|
section_matches?(
|
||||||
|
query,
|
||||||
|
~w(mcp claude copilot gemini opencode mistral codex agent server)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
|
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
|
||||||
|
|
||||||
|
@spec ai_form(term()) :: term()
|
||||||
def ai_form(assigns) do
|
def ai_form(assigns) do
|
||||||
{:ok, online_endpoint} = AI.get_endpoint(:online)
|
{:ok, online_endpoint} = AI.get_endpoint(:online)
|
||||||
{:ok, airplane_endpoint} = AI.get_endpoint(:airplane)
|
{:ok, airplane_endpoint} = AI.get_endpoint(:airplane)
|
||||||
@@ -30,18 +31,21 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec endpoint_model_options(term(), term()) :: term()
|
||||||
def endpoint_model_options(assigns, endpoint_key) do
|
def endpoint_model_options(assigns, endpoint_key) do
|
||||||
assigns
|
assigns
|
||||||
|> Map.get(:settings_editor_endpoint_models, %{})
|
|> Map.get(:settings_editor_endpoint_models, %{})
|
||||||
|> Map.get(endpoint_key, [])
|
|> Map.get(endpoint_key, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_ai_draft(term(), term(), term()) :: term()
|
||||||
def update_ai_draft(socket, params, reload) do
|
def update_ai_draft(socket, params, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_ai_draft, normalize_ai_params(params))
|
|> assign(:settings_editor_ai_draft, normalize_ai_params(params))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec refresh_ai_models(term(), term(), term(), term()) :: term()
|
||||||
def refresh_ai_models(socket, endpoint_key, reload, append_output) do
|
def refresh_ai_models(socket, endpoint_key, reload, append_output) do
|
||||||
attrs = ai_attrs(socket.assigns)
|
attrs = ai_attrs(socket.assigns)
|
||||||
|
|
||||||
@@ -65,11 +69,17 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_ai(term(), term(), term()) :: term()
|
||||||
def save_ai(socket, reload, append_output) do
|
def save_ai(socket, reload, append_output) do
|
||||||
attrs = ai_attrs(socket.assigns)
|
attrs = ai_attrs(socket.assigns)
|
||||||
|
|
||||||
with :ok <-
|
with :ok <-
|
||||||
put_endpoint_preferences(:online, attrs.online_url, attrs.online_api_key, attrs.online_chat_model),
|
put_endpoint_preferences(
|
||||||
|
:online,
|
||||||
|
attrs.online_url,
|
||||||
|
attrs.online_api_key,
|
||||||
|
attrs.online_chat_model
|
||||||
|
),
|
||||||
:ok <-
|
:ok <-
|
||||||
put_endpoint_preferences(
|
put_endpoint_preferences(
|
||||||
:airplane,
|
:airplane,
|
||||||
@@ -85,7 +95,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
:ok <- maybe_put_model_preference(:airplane_chat, attrs.offline_chat_model),
|
:ok <- maybe_put_model_preference(:airplane_chat, attrs.offline_chat_model),
|
||||||
:ok <- maybe_put_model_preference(:airplane_title, attrs.offline_title_model),
|
:ok <- maybe_put_model_preference(:airplane_title, attrs.offline_title_model),
|
||||||
:ok <-
|
:ok <-
|
||||||
maybe_put_model_preference(:airplane_image_analysis, attrs.offline_image_analysis_model),
|
maybe_put_model_preference(
|
||||||
|
:airplane_image_analysis,
|
||||||
|
attrs.offline_image_analysis_model
|
||||||
|
),
|
||||||
:ok <- EditorSettings.put_global_setting("ai.system_prompt", attrs.system_prompt) do
|
:ok <- EditorSettings.put_global_setting("ai.system_prompt", attrs.system_prompt) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_ai_draft, %{})
|
|> assign(:settings_editor_ai_draft, %{})
|
||||||
@@ -99,6 +112,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec reset_ai_prompt(term(), term(), term()) :: term()
|
||||||
def reset_ai_prompt(socket, reload, append_output) do
|
def reset_ai_prompt(socket, reload, append_output) do
|
||||||
case EditorSettings.put_global_setting("ai.system_prompt", "") do
|
case EditorSettings.put_global_setting("ai.system_prompt", "") do
|
||||||
:ok ->
|
:ok ->
|
||||||
|
|||||||
@@ -6,21 +6,25 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
|
|||||||
alias BDS.Settings
|
alias BDS.Settings
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
@spec editor_form() :: term()
|
||||||
def editor_form do
|
def editor_form do
|
||||||
%{
|
%{
|
||||||
"default_mode" => get_global_setting("ui.preferred_editor_mode") || "markdown",
|
"default_mode" => get_global_setting("ui.preferred_editor_mode") || "markdown",
|
||||||
"diff_view_style" => get_global_setting("ui.git_diff_view_style") || "inline",
|
"diff_view_style" => get_global_setting("ui.git_diff_view_style") || "inline",
|
||||||
"wrap_long_lines" => get_global_setting("ui.git_diff_word_wrap") == "true",
|
"wrap_long_lines" => get_global_setting("ui.git_diff_word_wrap") == "true",
|
||||||
"hide_unchanged_regions" => get_global_setting("ui.git_diff_hide_unchanged_regions") == "true"
|
"hide_unchanged_regions" =>
|
||||||
|
get_global_setting("ui.git_diff_hide_unchanged_regions") == "true"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_editor_draft(term(), term(), term()) :: term()
|
||||||
def update_editor_draft(socket, params, reload) do
|
def update_editor_draft(socket, params, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_editor_draft, normalize_editor_params(params))
|
|> assign(:settings_editor_editor_draft, normalize_editor_params(params))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_editor(term(), term(), term()) :: term()
|
||||||
def save_editor(socket, reload, append_output) do
|
def save_editor(socket, reload, append_output) do
|
||||||
attrs = editor_attrs(socket.assigns)
|
attrs = editor_attrs(socket.assigns)
|
||||||
|
|
||||||
@@ -43,10 +47,12 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_global_setting(term()) :: term()
|
||||||
def get_global_setting(key) do
|
def get_global_setting(key) do
|
||||||
Settings.get_global_setting(key)
|
Settings.get_global_setting(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec put_global_setting(term(), term()) :: term()
|
||||||
def put_global_setting(key, value) do
|
def put_global_setting(key, value) do
|
||||||
Settings.put_global_setting(key, value)
|
Settings.put_global_setting(key, value)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
|
|||||||
"page" => %{title: "page", render_in_lists: false, show_title: true}
|
"page" => %{title: "page", render_in_lists: false, show_title: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@spec protected_categories() :: term()
|
||||||
def protected_categories, do: @protected_categories
|
def protected_categories, do: @protected_categories
|
||||||
|
|
||||||
|
@spec protected_category?(term()) :: term()
|
||||||
def protected_category?(category), do: MapSet.member?(@protected_categories, category)
|
def protected_category?(category), do: MapSet.member?(@protected_categories, category)
|
||||||
|
|
||||||
|
@spec category_rows(term()) :: term()
|
||||||
def category_rows(metadata) do
|
def category_rows(metadata) do
|
||||||
categories = Map.get(metadata, :categories, [])
|
categories = Map.get(metadata, :categories, [])
|
||||||
settings = Map.get(metadata, :category_settings, %{})
|
settings = Map.get(metadata, :category_settings, %{})
|
||||||
@@ -37,12 +40,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_new_category(term(), term(), term()) :: term()
|
||||||
def update_new_category(socket, name, reload) do
|
def update_new_category(socket, name, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_new_category, to_string(name || ""))
|
|> assign(:settings_editor_new_category, to_string(name || ""))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec add_category(term(), term(), term()) :: term()
|
||||||
def add_category(socket, reload, append_output) do
|
def add_category(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
name = socket.assigns[:settings_editor_new_category] |> to_string() |> String.trim()
|
name = socket.assigns[:settings_editor_new_category] |> to_string() |> String.trim()
|
||||||
@@ -73,11 +78,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec reset_categories(term(), term(), term()) :: term()
|
||||||
def reset_categories(socket, reload, append_output) do
|
def reset_categories(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
result =
|
result =
|
||||||
Enum.reduce_while(category_names(project_metadata(socket.assigns)), :ok, fn category, _acc ->
|
Enum.reduce_while(category_names(project_metadata(socket.assigns)), :ok, fn category,
|
||||||
|
_acc ->
|
||||||
if MapSet.member?(@protected_categories, category) do
|
if MapSet.member?(@protected_categories, category) do
|
||||||
{:cont, :ok}
|
{:cont, :ok}
|
||||||
else
|
else
|
||||||
@@ -102,6 +109,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_category(term(), term(), term(), term()) :: term()
|
||||||
def save_category(socket, params, reload, append_output) do
|
def save_category(socket, params, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
category = Map.get(params, "category", "")
|
category = Map.get(params, "category", "")
|
||||||
@@ -125,6 +133,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec remove_category(term(), term(), term(), term()) :: term()
|
||||||
def remove_category(socket, category, reload, append_output) do
|
def remove_category(socket, category, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
|
|||||||
%{id: :openai_codex, label: "OpenAI Codex", supported?: false}
|
%{id: :openai_codex, label: "OpenAI Codex", supported?: false}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@spec mcp_rows() :: term()
|
||||||
def mcp_rows do
|
def mcp_rows do
|
||||||
Enum.map(@mcp_agents, fn agent ->
|
Enum.map(@mcp_agents, fn agent ->
|
||||||
%{
|
%{
|
||||||
@@ -28,6 +29,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_mcp_agent(term(), term(), term(), term()) :: term()
|
||||||
def toggle_mcp_agent(socket, agent, reload, append_output) do
|
def toggle_mcp_agent(socket, agent, reload, append_output) do
|
||||||
case find_mcp_agent(agent) do
|
case find_mcp_agent(agent) do
|
||||||
%{id: agent_id, supported?: true} = config ->
|
%{id: agent_id, supported?: true} = config ->
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
|
|||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
@spec project_metadata(term()) :: term()
|
||||||
def project_metadata(assigns) do
|
def project_metadata(assigns) do
|
||||||
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
||||||
{:ok, metadata} -> metadata
|
{:ok, metadata} -> metadata
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec project_form(term()) :: term()
|
||||||
def project_form(metadata) do
|
def project_form(metadata) do
|
||||||
%{
|
%{
|
||||||
"name" => Map.get(metadata, :name, ""),
|
"name" => Map.get(metadata, :name, ""),
|
||||||
@@ -28,18 +30,21 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec technology_form(term()) :: term()
|
||||||
def technology_form(project_form) do
|
def technology_form(project_form) do
|
||||||
%{
|
%{
|
||||||
"semantic_similarity_enabled" => Map.get(project_form, "semantic_similarity_enabled", false)
|
"semantic_similarity_enabled" => Map.get(project_form, "semantic_similarity_enabled", false)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_project_draft(term(), term(), term()) :: term()
|
||||||
def update_project_draft(socket, params, reload) do
|
def update_project_draft(socket, params, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_project_draft, normalize_project_params(params))
|
|> assign(:settings_editor_project_draft, normalize_project_params(params))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_project(term(), term(), term()) :: term()
|
||||||
def save_project(socket, reload, append_output) do
|
def save_project(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
|
|||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
|
|
||||||
|
@spec publishing_form(term()) :: term()
|
||||||
def publishing_form(metadata) do
|
def publishing_form(metadata) do
|
||||||
prefs = Map.get(metadata, :publishing_preferences, %{})
|
prefs = Map.get(metadata, :publishing_preferences, %{})
|
||||||
|
|
||||||
@@ -17,12 +18,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_publishing_draft(term(), term(), term()) :: term()
|
||||||
def update_publishing_draft(socket, params, reload) do
|
def update_publishing_draft(socket, params, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:settings_editor_publishing_draft, normalize_publishing_params(params))
|
|> assign(:settings_editor_publishing_draft, normalize_publishing_params(params))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_publishing(term(), term(), term()) :: term()
|
||||||
def save_publishing(socket, reload, append_output) do
|
def save_publishing(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec clear_publishing(term(), term(), term()) :: term()
|
||||||
def clear_publishing(socket, reload, append_output) do
|
def clear_publishing(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
|
|||||||
"zinc"
|
"zinc"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@spec build_style(term()) :: term()
|
||||||
def build_style(%{projects: %{active_project_id: nil}}), do: nil
|
def build_style(%{projects: %{active_project_id: nil}}), do: nil
|
||||||
|
|
||||||
def build_style(assigns) do
|
def build_style(assigns) do
|
||||||
@@ -40,22 +41,26 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
|
|||||||
selected_theme: selected_theme,
|
selected_theme: selected_theme,
|
||||||
applied_theme: current_theme(assigns),
|
applied_theme: current_theme(assigns),
|
||||||
preview_mode: preview_mode,
|
preview_mode: preview_mode,
|
||||||
preview_url: "http://127.0.0.1:4123/__style-preview?theme=#{selected_theme}&mode=#{preview_mode}"
|
preview_url:
|
||||||
|
"http://127.0.0.1:4123/__style-preview?theme=#{selected_theme}&mode=#{preview_mode}"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec select_style_theme(term(), term(), term()) :: term()
|
||||||
def select_style_theme(socket, theme, reload) do
|
def select_style_theme(socket, theme, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:style_editor_theme, to_string(theme || "default"))
|
|> assign(:style_editor_theme, to_string(theme || "default"))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec change_style_preview_mode(term(), term(), term()) :: term()
|
||||||
def change_style_preview_mode(socket, mode, reload) do
|
def change_style_preview_mode(socket, mode, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:style_editor_preview_mode, to_string(mode || "auto"))
|
|> assign(:style_editor_preview_mode, to_string(mode || "auto"))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec apply_style_theme(term(), term(), term()) :: term()
|
||||||
def apply_style_theme(socket, reload, append_output) do
|
def apply_style_theme(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
theme = socket.assigns[:style_editor_theme] || current_theme(socket.assigns)
|
theme = socket.assigns[:style_editor_theme] || current_theme(socket.assigns)
|
||||||
@@ -71,6 +76,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec theme_display_name(term()) :: term()
|
||||||
def theme_display_name(theme) do
|
def theme_display_name(theme) do
|
||||||
theme
|
theme
|
||||||
|> to_string()
|
|> to_string()
|
||||||
@@ -78,6 +84,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
|
|||||||
|> String.capitalize()
|
|> String.capitalize()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec current_theme(term()) :: term()
|
||||||
def current_theme(assigns) do
|
def current_theme(assigns) do
|
||||||
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
|
||||||
{:ok, metadata} ->
|
{:ok, metadata} ->
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create(socket, project_id, "post", callbacks) do
|
def create(socket, project_id, "post", callbacks) do
|
||||||
case BDS.Posts.create_post(%{project_id: project_id, title: "", content: "", tags: [], categories: []}) do
|
case BDS.Posts.create_post(%{
|
||||||
|
project_id: project_id,
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
}) do
|
||||||
{:ok, _post} ->
|
{:ok, _post} ->
|
||||||
callbacks.reload.(socket, socket.assigns.workbench)
|
callbacks.reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
@@ -42,7 +48,12 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
|
|||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
socket
|
socket
|
||||||
|> callbacks.append_output.(translated("sidebar.importMedia"), inspect(reason), nil, "error")
|
|> callbacks.append_output.(
|
||||||
|
translated("sidebar.importMedia"),
|
||||||
|
inspect(reason),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|> callbacks.reload.(socket.assigns.workbench)
|
|> callbacks.reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -68,13 +79,23 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
|
|||||||
{:ok, script} ->
|
{:ok, script} ->
|
||||||
callbacks.open_sidebar.(
|
callbacks.open_sidebar.(
|
||||||
socket,
|
socket,
|
||||||
%{"route" => "scripts", "id" => script.id, "title" => script.title, "subtitle" => "Automation helpers"},
|
%{
|
||||||
|
"route" => "scripts",
|
||||||
|
"id" => script.id,
|
||||||
|
"title" => script.title,
|
||||||
|
"subtitle" => "Automation helpers"
|
||||||
|
},
|
||||||
:pin
|
:pin
|
||||||
)
|
)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
socket
|
socket
|
||||||
|> callbacks.append_output.(translated("sidebar.scripts.newScript"), inspect(reason), nil, "error")
|
|> callbacks.append_output.(
|
||||||
|
translated("sidebar.scripts.newScript"),
|
||||||
|
inspect(reason),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|> callbacks.reload.(socket.assigns.workbench)
|
|> callbacks.reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -90,29 +111,52 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
|
|||||||
{:ok, template} ->
|
{:ok, template} ->
|
||||||
callbacks.open_sidebar.(
|
callbacks.open_sidebar.(
|
||||||
socket,
|
socket,
|
||||||
%{"route" => "templates", "id" => template.id, "title" => template.title, "subtitle" => "Site rendering"},
|
%{
|
||||||
|
"route" => "templates",
|
||||||
|
"id" => template.id,
|
||||||
|
"title" => template.title,
|
||||||
|
"subtitle" => "Site rendering"
|
||||||
|
},
|
||||||
:pin
|
:pin
|
||||||
)
|
)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
socket
|
socket
|
||||||
|> callbacks.append_output.(translated("sidebar.templates.newTemplate"), inspect(reason), nil, "error")
|
|> callbacks.append_output.(
|
||||||
|
translated("sidebar.templates.newTemplate"),
|
||||||
|
inspect(reason),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|> callbacks.reload.(socket.assigns.workbench)
|
|> callbacks.reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(socket, project_id, "import", callbacks) do
|
def create(socket, project_id, "import", callbacks) do
|
||||||
case ImportDefinitions.create_definition(%{project_id: project_id, name: translated("sidebar.import.newDefinition")}) do
|
case ImportDefinitions.create_definition(%{
|
||||||
|
project_id: project_id,
|
||||||
|
name: translated("sidebar.import.newDefinition")
|
||||||
|
}) do
|
||||||
{:ok, definition} ->
|
{:ok, definition} ->
|
||||||
callbacks.open_sidebar.(
|
callbacks.open_sidebar.(
|
||||||
socket,
|
socket,
|
||||||
%{"route" => "import", "id" => definition.id, "title" => definition.name, "subtitle" => "Import definitions"},
|
%{
|
||||||
|
"route" => "import",
|
||||||
|
"id" => definition.id,
|
||||||
|
"title" => definition.name,
|
||||||
|
"subtitle" => "Import definitions"
|
||||||
|
},
|
||||||
:pin
|
:pin
|
||||||
)
|
)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
socket
|
socket
|
||||||
|> callbacks.append_output.(translated("sidebar.import.newDefinition"), inspect(reason), nil, "error")
|
|> callbacks.append_output.(
|
||||||
|
translated("sidebar.import.newDefinition"),
|
||||||
|
inspect(reason),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|> callbacks.reload.(socket.assigns.workbench)
|
|> callbacks.reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ defmodule BDS.Desktop.ShellLive.SidebarState do
|
|||||||
if is_map(filters) and Map.get(filters, :enabled) do
|
if is_map(filters) and Map.get(filters, :enabled) do
|
||||||
panel_state = filter_panel_state(socket, view_id)
|
panel_state = filter_panel_state(socket, view_id)
|
||||||
|
|
||||||
Map.put(sidebar_data, :filters, Map.merge(filters, %{
|
Map.put(
|
||||||
filter_panel_visible: panel_state.visible,
|
sidebar_data,
|
||||||
archive_collapsed: panel_state.archive_collapsed,
|
:filters,
|
||||||
tags_collapsed: panel_state.tags_collapsed,
|
Map.merge(filters, %{
|
||||||
categories_collapsed: panel_state.categories_collapsed,
|
filter_panel_visible: panel_state.visible,
|
||||||
expanded_year: panel_state.expanded_year
|
archive_collapsed: panel_state.archive_collapsed,
|
||||||
}))
|
tags_collapsed: panel_state.tags_collapsed,
|
||||||
|
categories_collapsed: panel_state.categories_collapsed,
|
||||||
|
expanded_year: panel_state.expanded_year
|
||||||
|
})
|
||||||
|
)
|
||||||
else
|
else
|
||||||
sidebar_data
|
sidebar_data
|
||||||
end
|
end
|
||||||
@@ -22,7 +26,12 @@ defmodule BDS.Desktop.ShellLive.SidebarState do
|
|||||||
def put_filter_panel_state(socket, updater) do
|
def put_filter_panel_state(socket, updater) do
|
||||||
view_id = Atom.to_string(socket.assigns.workbench.active_view)
|
view_id = Atom.to_string(socket.assigns.workbench.active_view)
|
||||||
state = socket |> filter_panel_state(view_id) |> updater.()
|
state = socket |> filter_panel_state(view_id) |> updater.()
|
||||||
Phoenix.Component.assign(socket, :sidebar_filter_panels, Map.put(socket.assigns.sidebar_filter_panels, view_id, state))
|
|
||||||
|
Phoenix.Component.assign(
|
||||||
|
socket,
|
||||||
|
:sidebar_filter_panels,
|
||||||
|
Map.put(socket.assigns.sidebar_filter_panels, view_id, state)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_filters(socket, view_id) do
|
def current_filters(socket, view_id) do
|
||||||
@@ -33,8 +42,17 @@ defmodule BDS.Desktop.ShellLive.SidebarState do
|
|||||||
|
|
||||||
def put_filters(socket, updater) do
|
def put_filters(socket, updater) do
|
||||||
view_id = Atom.to_string(socket.assigns.workbench.active_view)
|
view_id = Atom.to_string(socket.assigns.workbench.active_view)
|
||||||
filters = current_filters(socket, view_id) |> updater.() |> normalize_filters(socket.assigns.sidebar_data)
|
|
||||||
Phoenix.Component.assign(socket, :sidebar_filters_by_view, Map.put(socket.assigns.sidebar_filters_by_view, view_id, filters))
|
filters =
|
||||||
|
current_filters(socket, view_id)
|
||||||
|
|> updater.()
|
||||||
|
|> normalize_filters(socket.assigns.sidebar_data)
|
||||||
|
|
||||||
|
Phoenix.Component.assign(
|
||||||
|
socket,
|
||||||
|
:sidebar_filters_by_view,
|
||||||
|
Map.put(socket.assigns.sidebar_filters_by_view, view_id, filters)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_filter_value(filters, key, value) do
|
def toggle_filter_value(filters, key, value) do
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
alias BDS.Tags.Tag
|
alias BDS.Tags.Tag
|
||||||
alias BDS.Templates.Template
|
alias BDS.Templates.Template
|
||||||
|
|
||||||
embed_templates "tags_editor_html/*"
|
embed_templates("tags_editor_html/*")
|
||||||
|
|
||||||
|
@spec assign_socket(term()) :: term()
|
||||||
def assign_socket(socket) do
|
def assign_socket(socket) do
|
||||||
assign(socket, :tags_editor, build(socket.assigns))
|
assign(socket, :tags_editor, build(socket.assigns))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec toggle_selection(term(), term(), term()) :: term()
|
||||||
def toggle_selection(socket, tag_name, reload) do
|
def toggle_selection(socket, tag_name, reload) do
|
||||||
selected = Map.get(socket.assigns, :tags_editor_selected, [])
|
selected = Map.get(socket.assigns, :tags_editor_selected, [])
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_new_tag(term(), term(), term()) :: term()
|
||||||
def update_new_tag(socket, params, reload) do
|
def update_new_tag(socket, params, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:tags_editor_new_tag, %{
|
|> assign(:tags_editor_new_tag, %{
|
||||||
@@ -42,11 +45,16 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create_tag(term(), term(), term()) :: term()
|
||||||
def create_tag(socket, reload, append_output) do
|
def create_tag(socket, reload, append_output) do
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
draft = Map.get(socket.assigns, :tags_editor_new_tag, %{})
|
draft = Map.get(socket.assigns, :tags_editor_new_tag, %{})
|
||||||
|
|
||||||
case Tags.create_tag(%{project_id: project_id, name: Map.get(draft, "name"), color: blank_to_nil(Map.get(draft, "color"))}) do
|
case Tags.create_tag(%{
|
||||||
|
project_id: project_id,
|
||||||
|
name: Map.get(draft, "name"),
|
||||||
|
color: blank_to_nil(Map.get(draft, "color"))
|
||||||
|
}) do
|
||||||
{:ok, _tag} ->
|
{:ok, _tag} ->
|
||||||
socket
|
socket
|
||||||
|> assign(:tags_editor_new_tag, %{"name" => "", "color" => ""})
|
|> assign(:tags_editor_new_tag, %{"name" => "", "color" => ""})
|
||||||
@@ -59,6 +67,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_edit_tag(term(), term(), term()) :: term()
|
||||||
def update_edit_tag(socket, params, reload) do
|
def update_edit_tag(socket, params, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:tags_editor_edit_draft, %{
|
|> assign(:tags_editor_edit_draft, %{
|
||||||
@@ -69,16 +78,26 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec save_tag(term(), term(), term()) :: term()
|
||||||
def save_tag(socket, reload, append_output) do
|
def save_tag(socket, reload, append_output) do
|
||||||
selected = Map.get(socket.assigns, :tags_editor_selected, [])
|
selected = Map.get(socket.assigns, :tags_editor_selected, [])
|
||||||
draft = Map.get(socket.assigns, :tags_editor_edit_draft, %{})
|
draft = Map.get(socket.assigns, :tags_editor_edit_draft, %{})
|
||||||
|
|
||||||
case selected do
|
case selected do
|
||||||
[tag_name] ->
|
[tag_name] ->
|
||||||
case Repo.get_by(Tag, project_id: socket.assigns.projects.active_project_id, name: tag_name) do
|
case Repo.get_by(Tag,
|
||||||
nil -> reload.(socket, socket.assigns.workbench)
|
project_id: socket.assigns.projects.active_project_id,
|
||||||
|
name: tag_name
|
||||||
|
) do
|
||||||
|
nil ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
%Tag{} = tag ->
|
%Tag{} = tag ->
|
||||||
with {:ok, _updated_tag} <- Tags.update_tag(tag.id, %{color: blank_to_nil(Map.get(draft, "color")), post_template_slug: blank_to_nil(Map.get(draft, "post_template_slug"))}),
|
with {:ok, _updated_tag} <-
|
||||||
|
Tags.update_tag(tag.id, %{
|
||||||
|
color: blank_to_nil(Map.get(draft, "color")),
|
||||||
|
post_template_slug: blank_to_nil(Map.get(draft, "post_template_slug"))
|
||||||
|
}),
|
||||||
{:ok, renamed_tag} <- maybe_rename_tag(tag, Map.get(draft, "name", tag.name)) do
|
{:ok, renamed_tag} <- maybe_rename_tag(tag, Map.get(draft, "name", tag.name)) do
|
||||||
socket
|
socket
|
||||||
|> assign(:tags_editor_selected, [renamed_tag.name])
|
|> assign(:tags_editor_selected, [renamed_tag.name])
|
||||||
@@ -92,15 +111,22 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
_other -> reload.(socket, socket.assigns.workbench)
|
_other ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_selected(term(), term(), term()) :: term()
|
||||||
def delete_selected(socket, reload, append_output) do
|
def delete_selected(socket, reload, append_output) do
|
||||||
case Map.get(socket.assigns, :tags_editor_selected, []) do
|
case Map.get(socket.assigns, :tags_editor_selected, []) do
|
||||||
[tag_name] ->
|
[tag_name] ->
|
||||||
case Repo.get_by(Tag, project_id: socket.assigns.projects.active_project_id, name: tag_name) do
|
case Repo.get_by(Tag,
|
||||||
nil -> reload.(socket, socket.assigns.workbench)
|
project_id: socket.assigns.projects.active_project_id,
|
||||||
|
name: tag_name
|
||||||
|
) do
|
||||||
|
nil ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
%Tag{} = tag ->
|
%Tag{} = tag ->
|
||||||
case Tags.delete_tag(tag.id) do
|
case Tags.delete_tag(tag.id) do
|
||||||
{:ok, _deleted} ->
|
{:ok, _deleted} ->
|
||||||
@@ -116,16 +142,19 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
_other -> reload.(socket, socket.assigns.workbench)
|
_other ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_merge_target(term(), term(), term()) :: term()
|
||||||
def update_merge_target(socket, target, reload) do
|
def update_merge_target(socket, target, reload) do
|
||||||
socket
|
socket
|
||||||
|> assign(:tags_editor_merge_target, to_string(target || ""))
|
|> assign(:tags_editor_merge_target, to_string(target || ""))
|
||||||
|> reload.(socket.assigns.workbench)
|
|> reload.(socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec merge_selected(term(), term(), term()) :: term()
|
||||||
def merge_selected(socket, reload, append_output) do
|
def merge_selected(socket, reload, append_output) do
|
||||||
selected = Map.get(socket.assigns, :tags_editor_selected, [])
|
selected = Map.get(socket.assigns, :tags_editor_selected, [])
|
||||||
target_name = Map.get(socket.assigns, :tags_editor_merge_target, "")
|
target_name = Map.get(socket.assigns, :tags_editor_merge_target, "")
|
||||||
@@ -136,12 +165,19 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
|
|
||||||
true ->
|
true ->
|
||||||
project_id = socket.assigns.projects.active_project_id
|
project_id = socket.assigns.projects.active_project_id
|
||||||
tags = Repo.all(from tag in Tag, where: tag.project_id == ^project_id and tag.name in ^selected)
|
|
||||||
|
tags =
|
||||||
|
Repo.all(
|
||||||
|
from tag in Tag, where: tag.project_id == ^project_id and tag.name in ^selected
|
||||||
|
)
|
||||||
|
|
||||||
target = Enum.find(tags, &(&1.name == target_name))
|
target = Enum.find(tags, &(&1.name == target_name))
|
||||||
sources = Enum.reject(tags, &(&1.name == target_name))
|
sources = Enum.reject(tags, &(&1.name == target_name))
|
||||||
|
|
||||||
case target do
|
case target do
|
||||||
nil -> reload.(socket, socket.assigns.workbench)
|
nil ->
|
||||||
|
reload.(socket, socket.assigns.workbench)
|
||||||
|
|
||||||
_target ->
|
_target ->
|
||||||
case Tags.merge_tags(Enum.map(sources, & &1.id), target.id) do
|
case Tags.merge_tags(Enum.map(sources, & &1.id), target.id) do
|
||||||
{:ok, _merged} ->
|
{:ok, _merged} ->
|
||||||
@@ -160,23 +196,41 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec sync(term(), term(), term()) :: term()
|
||||||
def sync(socket, reload, append_output) do
|
def sync(socket, reload, append_output) do
|
||||||
_ = append_output
|
_ = append_output
|
||||||
:ok = Tags.sync_tags_json(socket.assigns.projects.active_project_id)
|
:ok = Tags.sync_tags_json(socket.assigns.projects.active_project_id)
|
||||||
reload.(socket, socket.assigns.workbench)
|
reload.(socket, socket.assigns.workbench)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build(term()) :: term()
|
||||||
def build(%{current_tab: %{type: :tags}} = assigns) do
|
def build(%{current_tab: %{type: :tags}} = assigns) do
|
||||||
project_id = assigns.projects.active_project_id
|
project_id = assigns.projects.active_project_id
|
||||||
tags = Repo.all(from tag in Tag, where: tag.project_id == ^project_id, order_by: [asc: tag.name])
|
|
||||||
|
tags =
|
||||||
|
Repo.all(from tag in Tag, where: tag.project_id == ^project_id, order_by: [asc: tag.name])
|
||||||
|
|
||||||
counts = tag_counts(project_id)
|
counts = tag_counts(project_id)
|
||||||
selected = Map.get(assigns, :tags_editor_selected, [])
|
selected = Map.get(assigns, :tags_editor_selected, [])
|
||||||
edit_tag = if length(selected) == 1, do: Enum.find(tags, &(&1.name == hd(selected))), else: nil
|
|
||||||
|
edit_tag =
|
||||||
|
if length(selected) == 1, do: Enum.find(tags, &(&1.name == hd(selected))), else: nil
|
||||||
|
|
||||||
edit_draft = Map.get(assigns, :tags_editor_edit_draft, edit_draft(edit_tag))
|
edit_draft = Map.get(assigns, :tags_editor_edit_draft, edit_draft(edit_tag))
|
||||||
templates = Repo.all(from template in Template, where: template.project_id == ^project_id, order_by: [asc: template.title], select: %{slug: template.slug, title: template.title})
|
|
||||||
|
templates =
|
||||||
|
Repo.all(
|
||||||
|
from template in Template,
|
||||||
|
where: template.project_id == ^project_id,
|
||||||
|
order_by: [asc: template.title],
|
||||||
|
select: %{slug: template.slug, title: template.title}
|
||||||
|
)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
tags: Enum.map(tags, fn tag -> %{name: tag.name, color: tag.color, count: Map.get(counts, tag.name, 0)} end),
|
tags:
|
||||||
|
Enum.map(tags, fn tag ->
|
||||||
|
%{name: tag.name, color: tag.color, count: Map.get(counts, tag.name, 0)}
|
||||||
|
end),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
new_tag: Map.get(assigns, :tags_editor_new_tag, %{"name" => "", "color" => ""}),
|
new_tag: Map.get(assigns, :tags_editor_new_tag, %{"name" => "", "color" => ""}),
|
||||||
edit_draft: edit_draft,
|
edit_draft: edit_draft,
|
||||||
@@ -187,14 +241,18 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
|
|
||||||
def build(_assigns), do: nil
|
def build(_assigns), do: nil
|
||||||
|
|
||||||
def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
@spec translated(term(), term()) :: term()
|
||||||
|
def translated(text, bindings \\ %{}),
|
||||||
|
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
|
||||||
|
|
||||||
|
@spec tag_font_size(term(), term()) :: term()
|
||||||
def tag_font_size(count, counts) do
|
def tag_font_size(count, counts) do
|
||||||
max_count = Enum.max([1 | Enum.map(counts, & &1.count)])
|
max_count = Enum.max([1 | Enum.map(counts, & &1.count)])
|
||||||
ratio = if max_count <= 1, do: 0.0, else: (count - 1) / max(max_count - 1, 1)
|
ratio = if max_count <= 1, do: 0.0, else: (count - 1) / max(max_count - 1, 1)
|
||||||
Float.round(0.85 + (1.8 - 0.85) * ratio, 2)
|
Float.round(0.85 + (1.8 - 0.85) * ratio, 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec tag_style(term(), term()) :: term()
|
||||||
def tag_style(tag, counts) do
|
def tag_style(tag, counts) do
|
||||||
size = tag_font_size(tag.count, counts)
|
size = tag_font_size(tag.count, counts)
|
||||||
|
|
||||||
@@ -217,7 +275,13 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
defp maybe_seed_edit_draft(socket, _selected), do: assign(socket, :tags_editor_edit_draft, %{})
|
defp maybe_seed_edit_draft(socket, _selected), do: assign(socket, :tags_editor_edit_draft, %{})
|
||||||
|
|
||||||
defp edit_draft(nil), do: %{}
|
defp edit_draft(nil), do: %{}
|
||||||
defp edit_draft(%Tag{} = tag), do: %{"name" => tag.name, "color" => tag.color || "", "post_template_slug" => tag.post_template_slug || ""}
|
|
||||||
|
defp edit_draft(%Tag{} = tag),
|
||||||
|
do: %{
|
||||||
|
"name" => tag.name,
|
||||||
|
"color" => tag.color || "",
|
||||||
|
"post_template_slug" => tag.post_template_slug || ""
|
||||||
|
}
|
||||||
|
|
||||||
defp maybe_rename_tag(%Tag{} = tag, next_name) do
|
defp maybe_rename_tag(%Tag{} = tag, next_name) do
|
||||||
normalized = String.trim(to_string(next_name || tag.name))
|
normalized = String.trim(to_string(next_name || tag.name))
|
||||||
@@ -237,6 +301,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp blank_to_nil(nil), do: nil
|
defp blank_to_nil(nil), do: nil
|
||||||
|
|
||||||
defp blank_to_nil(value) do
|
defp blank_to_nil(value) do
|
||||||
case String.trim(to_string(value)) do
|
case String.trim(to_string(value)) do
|
||||||
"" -> nil
|
"" -> nil
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ defmodule BDS.Desktop.ShellLive.TaskLocalization do
|
|||||||
|> Map.put(:message, localize_task_message(Map.get(task, :message), locale))
|
|> Map.put(:message, localize_task_message(Map.get(task, :message), locale))
|
||||||
|> Map.put(:group_name, localize_task_group(Map.get(task, :group_name), locale))
|
|> Map.put(:group_name, localize_task_group(Map.get(task, :group_name), locale))
|
||||||
|> Map.put(:status_label, localize_task_status_label(task.status, locale))
|
|> Map.put(:status_label, localize_task_status_label(task.status, locale))
|
||||||
|> Map.put(:progress_label, if(is_number(progress), do: progress_percent(progress), else: nil))
|
|> Map.put(
|
||||||
|
:progress_label,
|
||||||
|
if(is_number(progress), do: progress_percent(progress), else: nil)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp localize_task_message(nil, _locale), do: nil
|
defp localize_task_message(nil, _locale), do: nil
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ defmodule BDS.Desktop.ShellLive.TitlebarMenu do
|
|||||||
|
|
||||||
@spec active_group(map()) :: map() | nil
|
@spec active_group(map()) :: map() | nil
|
||||||
def active_group(assigns) do
|
def active_group(assigns) do
|
||||||
Enum.find(assigns.menu_groups || [], fn group -> Atom.to_string(group.id) == assigns.titlebar_menu_group end)
|
Enum.find(assigns.menu_groups || [], fn group ->
|
||||||
|
Atom.to_string(group.id) == assigns.titlebar_menu_group
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec active_items(map()) :: [map()]
|
@spec active_items(map()) :: [map()]
|
||||||
@@ -90,7 +92,9 @@ defmodule BDS.Desktop.ShellLive.TitlebarMenu do
|
|||||||
Handle a keydown event on an open titlebar menu. `invoke_fun` is called
|
Handle a keydown event on an open titlebar menu. `invoke_fun` is called
|
||||||
with the action id (string) when the user activates an item.
|
with the action id (string) when the user activates an item.
|
||||||
"""
|
"""
|
||||||
@spec handle_keydown(Phoenix.LiveView.Socket.t(), String.t(), (Phoenix.LiveView.Socket.t(), String.t() -> Phoenix.LiveView.Socket.t())) ::
|
@spec handle_keydown(Phoenix.LiveView.Socket.t(), String.t(), (Phoenix.LiveView.Socket.t(),
|
||||||
|
String.t() ->
|
||||||
|
Phoenix.LiveView.Socket.t())) ::
|
||||||
Phoenix.LiveView.Socket.t()
|
Phoenix.LiveView.Socket.t()
|
||||||
def handle_keydown(socket, key, invoke_fun) do
|
def handle_keydown(socket, key, invoke_fun) do
|
||||||
if socket.assigns.titlebar_menu_group do
|
if socket.assigns.titlebar_menu_group do
|
||||||
@@ -114,7 +118,9 @@ defmodule BDS.Desktop.ShellLive.TitlebarMenu do
|
|||||||
defp rotate_group(socket, offset) do
|
defp rotate_group(socket, offset) do
|
||||||
groups = socket.assigns.menu_groups || []
|
groups = socket.assigns.menu_groups || []
|
||||||
current_group = socket.assigns.titlebar_menu_group
|
current_group = socket.assigns.titlebar_menu_group
|
||||||
current_index = Enum.find_index(groups, fn group -> Atom.to_string(group.id) == current_group end)
|
|
||||||
|
current_index =
|
||||||
|
Enum.find_index(groups, fn group -> Atom.to_string(group.id) == current_group end)
|
||||||
|
|
||||||
if is_nil(current_index) or groups == [] do
|
if is_nil(current_index) or groups == [] do
|
||||||
socket
|
socket
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ defmodule BDS.Frontmatter do
|
|||||||
|
|
||||||
defp take_block_scalar_lines([line | rest], lines) do
|
defp take_block_scalar_lines([line | rest], lines) do
|
||||||
if String.starts_with?(line, @block_scalar_indent) do
|
if String.starts_with?(line, @block_scalar_indent) do
|
||||||
take_block_scalar_lines(rest, [String.replace_prefix(line, @block_scalar_indent, "") | lines])
|
take_block_scalar_lines(rest, [
|
||||||
|
String.replace_prefix(line, @block_scalar_indent, "") | lines
|
||||||
|
])
|
||||||
else
|
else
|
||||||
{Enum.reverse(lines), [line | rest]}
|
{Enum.reverse(lines), [line | rest]}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ defmodule BDS.Generation do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
import BDS.Generation.Paths,
|
import BDS.Generation.Paths,
|
||||||
except: [post_output_path: 1, post_output_path: 2]
|
except: [post_output_path: 1, post_output_path: 2]
|
||||||
|
|
||||||
import BDS.Generation.Sitemap,
|
import BDS.Generation.Sitemap,
|
||||||
only: [
|
only: [
|
||||||
render: 1,
|
render: 1,
|
||||||
render_multi_language: 6
|
render_multi_language: 6
|
||||||
]
|
]
|
||||||
|
|
||||||
import BDS.Generation.Progress
|
import BDS.Generation.Progress
|
||||||
import BDS.Generation.Outputs
|
import BDS.Generation.Outputs
|
||||||
import BDS.Generation.Data
|
import BDS.Generation.Data
|
||||||
@@ -89,7 +92,8 @@ defmodule BDS.Generation do
|
|||||||
{:ok, validation_report()} | {:error, term()}
|
{:ok, validation_report()} | {:error, term()}
|
||||||
def validate_site(project_id, sections \\ @core_sections, opts \\ [])
|
def validate_site(project_id, sections \\ @core_sections, opts \\ [])
|
||||||
|
|
||||||
def validate_site(project_id, sections, opts) when is_binary(project_id) and is_list(sections) and is_list(opts) do
|
def validate_site(project_id, sections, opts)
|
||||||
|
when is_binary(project_id) and is_list(sections) and is_list(opts) do
|
||||||
with {:ok, plan} <- plan_generation(project_id, sections) do
|
with {:ok, plan} <- plan_generation(project_id, sections) do
|
||||||
on_progress = callback(opts)
|
on_progress = callback(opts)
|
||||||
:ok = report_validation_progress(on_progress, 0.0, "Collecting sitemap URLs...")
|
:ok = report_validation_progress(on_progress, 0.0, "Collecting sitemap URLs...")
|
||||||
@@ -104,9 +108,12 @@ defmodule BDS.Generation do
|
|||||||
{:ok, generated_files_list} = list_generated_files(project_id)
|
{:ok, generated_files_list} = list_generated_files(project_id)
|
||||||
generated_file_updated_at = generated_file_updated_at_map(generated_files_list)
|
generated_file_updated_at = generated_file_updated_at_map(generated_files_list)
|
||||||
additional_languages = additional_languages(plan)
|
additional_languages = additional_languages(plan)
|
||||||
published_route_posts = suppress_subtree_translation_variants(data.published_route_posts, additional_languages)
|
|
||||||
|
|
||||||
{sitemap_content, sitemap_to_write, additional_expected_paths, additional_post_timestamp_checks} =
|
published_route_posts =
|
||||||
|
suppress_subtree_translation_variants(data.published_route_posts, additional_languages)
|
||||||
|
|
||||||
|
{sitemap_content, sitemap_to_write, additional_expected_paths,
|
||||||
|
additional_post_timestamp_checks} =
|
||||||
build_validation_sitemap_artifacts(
|
build_validation_sitemap_artifacts(
|
||||||
plan,
|
plan,
|
||||||
data,
|
data,
|
||||||
@@ -155,8 +162,8 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
@spec apply_validation(String.t(), [section()] | map()) :: {:ok, map()} | {:error, term()}
|
@spec apply_validation(String.t(), [section()] | map()) :: {:ok, map()} | {:error, term()}
|
||||||
def apply_validation(project_id, sections) when is_binary(project_id) and is_list(sections) do
|
def apply_validation(project_id, sections) when is_binary(project_id) and is_list(sections) do
|
||||||
with {:ok, plan} <- plan_generation(project_id, sections),
|
with {:ok, plan} <- plan_generation(project_id, sections),
|
||||||
{:ok, actual_files} <- disk_generated_files(project_id) do
|
{:ok, actual_files} <- disk_generated_files(project_id) do
|
||||||
expected_outputs = build_outputs(plan)
|
expected_outputs = build_outputs(plan)
|
||||||
expected_paths = MapSet.new(Enum.map(expected_outputs, &elem(&1, 0)))
|
expected_paths = MapSet.new(Enum.map(expected_outputs, &elem(&1, 0)))
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
@@ -190,7 +197,8 @@ defmodule BDS.Generation do
|
|||||||
generated_files_on_disk
|
generated_files_on_disk
|
||||||
|> Map.keys()
|
|> Map.keys()
|
||||||
|> Enum.filter(fn relative_path ->
|
|> Enum.filter(fn relative_path ->
|
||||||
path_section(relative_path) in plan.sections and not MapSet.member?(expected_paths, relative_path)
|
path_section(relative_path) in plan.sections and
|
||||||
|
not MapSet.member?(expected_paths, relative_path)
|
||||||
end)
|
end)
|
||||||
|> Enum.each(fn relative_path ->
|
|> Enum.each(fn relative_path ->
|
||||||
_ = File.rm(output_path(project, relative_path))
|
_ = File.rm(output_path(project, relative_path))
|
||||||
@@ -215,6 +223,7 @@ defmodule BDS.Generation do
|
|||||||
expected_output_map = Map.new(expected_outputs)
|
expected_output_map = Map.new(expected_outputs)
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
published_posts = list_published_posts(project_id)
|
published_posts = list_published_posts(project_id)
|
||||||
|
|
||||||
targeted_plan =
|
targeted_plan =
|
||||||
build_targeted_validation_plan(
|
build_targeted_validation_plan(
|
||||||
plan_validation_paths(report_paths(report), additional_languages(plan)),
|
plan_validation_paths(report_paths(report), additional_languages(plan)),
|
||||||
@@ -224,7 +233,12 @@ defmodule BDS.Generation do
|
|||||||
outputs_to_render =
|
outputs_to_render =
|
||||||
expected_outputs
|
expected_outputs
|
||||||
|> Enum.filter(fn {relative_path, _content} ->
|
|> Enum.filter(fn {relative_path, _content} ->
|
||||||
targeted_output?(relative_path, targeted_plan, plan.language, additional_languages(plan))
|
targeted_output?(
|
||||||
|
relative_path,
|
||||||
|
targeted_plan,
|
||||||
|
plan.language,
|
||||||
|
additional_languages(plan)
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Enum.each(outputs_to_render, fn {relative_path, content} ->
|
Enum.each(outputs_to_render, fn {relative_path, content} ->
|
||||||
@@ -243,7 +257,10 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
rendered_url_count: Enum.count(outputs_to_render, fn {relative_path, _content} -> route_html_path?(relative_path) end),
|
rendered_url_count:
|
||||||
|
Enum.count(outputs_to_render, fn {relative_path, _content} ->
|
||||||
|
route_html_path?(relative_path)
|
||||||
|
end),
|
||||||
deleted_url_count: deleted_url_count,
|
deleted_url_count: deleted_url_count,
|
||||||
removed_empty_dir_count: removed_empty_dir_count
|
removed_empty_dir_count: removed_empty_dir_count
|
||||||
}}
|
}}
|
||||||
@@ -257,15 +274,21 @@ defmodule BDS.Generation do
|
|||||||
defdelegate post_output_path(post, language), to: Paths
|
defdelegate post_output_path(post, language), to: Paths
|
||||||
|
|
||||||
@typedoc "Result returned by `write_generated_file/3,4`."
|
@typedoc "Result returned by `write_generated_file/3,4`."
|
||||||
@type write_result :: %{relative_path: String.t(), content_hash: String.t(), written?: boolean()}
|
@type write_result :: %{
|
||||||
|
relative_path: String.t(),
|
||||||
|
content_hash: String.t(),
|
||||||
|
written?: boolean()
|
||||||
|
}
|
||||||
|
|
||||||
@spec write_generated_file(String.t(), String.t(), String.t()) :: {:ok, write_result()}
|
@spec write_generated_file(String.t(), String.t(), String.t()) :: {:ok, write_result()}
|
||||||
def write_generated_file(project_id, relative_path, content),
|
def write_generated_file(project_id, relative_path, content),
|
||||||
do: write_generated_file(project_id, relative_path, content, [])
|
do: write_generated_file(project_id, relative_path, content, [])
|
||||||
|
|
||||||
@spec write_generated_file(String.t(), String.t(), String.t(), keyword()) :: {:ok, write_result()}
|
@spec write_generated_file(String.t(), String.t(), String.t(), keyword()) ::
|
||||||
|
{:ok, write_result()}
|
||||||
def write_generated_file(project_id, relative_path, content, opts)
|
def write_generated_file(project_id, relative_path, content, opts)
|
||||||
when is_binary(project_id) and is_binary(relative_path) and is_binary(content) and is_list(opts) do
|
when is_binary(project_id) and is_binary(relative_path) and is_binary(content) and
|
||||||
|
is_list(opts) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
content_hash = sha256(content)
|
content_hash = sha256(content)
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
@@ -331,8 +354,12 @@ defmodule BDS.Generation do
|
|||||||
data = generation_data(plan)
|
data = generation_data(plan)
|
||||||
published_translations = flattened_generation_translations(data.translations_by_post)
|
published_translations = flattened_generation_translations(data.translations_by_post)
|
||||||
translations_by_post_language = translation_lookup_map(published_translations)
|
translations_by_post_language = translation_lookup_map(published_translations)
|
||||||
translatable_published_posts = Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
|
||||||
translatable_published_list_posts = Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
translatable_published_posts =
|
||||||
|
Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
||||||
|
|
||||||
|
translatable_published_list_posts =
|
||||||
|
Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
||||||
|
|
||||||
localized_posts_by_language =
|
localized_posts_by_language =
|
||||||
additional_languages(plan)
|
additional_languages(plan)
|
||||||
@@ -421,7 +448,10 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
pagefind_outputs =
|
pagefind_outputs =
|
||||||
if :core in plan.sections do
|
if :core in plan.sections do
|
||||||
BDS.Generation.Pagefind.build_outputs(plan, core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs)
|
BDS.Generation.Pagefind.build_outputs(
|
||||||
|
plan,
|
||||||
|
core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs
|
||||||
|
)
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@@ -433,7 +463,9 @@ defmodule BDS.Generation do
|
|||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs ++ sitemap ++ pagefind_outputs ++ asset_outputs
|
core_outputs ++
|
||||||
|
page_outputs ++
|
||||||
|
single_outputs ++ archive_outputs ++ sitemap ++ pagefind_outputs ++ asset_outputs
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_validation_sitemap_artifacts(
|
defp build_validation_sitemap_artifacts(
|
||||||
@@ -454,17 +486,27 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
additional_language_sets =
|
additional_language_sets =
|
||||||
Enum.map(additional_languages(plan), fn language ->
|
Enum.map(additional_languages(plan), fn language ->
|
||||||
language_posts = Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
language_posts =
|
||||||
language_list_posts = Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
||||||
|
|
||||||
|
language_list_posts =
|
||||||
|
Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
|
||||||
|
|
||||||
language_post_index = build_generation_post_index(language_list_posts)
|
language_post_index = build_generation_post_index(language_list_posts)
|
||||||
|
|
||||||
{language,
|
{language, language_posts,
|
||||||
language_posts,
|
build_validation_route_paths(
|
||||||
build_validation_route_paths(plan, language_posts, language_list_posts, language_post_index, language)}
|
plan,
|
||||||
|
language_posts,
|
||||||
|
language_list_posts,
|
||||||
|
language_post_index,
|
||||||
|
language
|
||||||
|
)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
all_collection_paths =
|
all_collection_paths =
|
||||||
main_paths ++ Enum.flat_map(additional_language_sets, fn {_language, _posts, paths} -> paths end)
|
main_paths ++
|
||||||
|
Enum.flat_map(additional_language_sets, fn {_language, _posts, paths} -> paths end)
|
||||||
|
|
||||||
total_route_count = max(length(all_collection_paths), 1)
|
total_route_count = max(length(all_collection_paths), 1)
|
||||||
|
|
||||||
@@ -497,7 +539,8 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
sitemap_to_write =
|
sitemap_to_write =
|
||||||
case additional_languages(plan) do
|
case additional_languages(plan) do
|
||||||
[] -> sitemap_content
|
[] ->
|
||||||
|
sitemap_content
|
||||||
|
|
||||||
languages ->
|
languages ->
|
||||||
render_multi_language(
|
render_multi_language(
|
||||||
@@ -510,7 +553,8 @@ defmodule BDS.Generation do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
{sitemap_content, sitemap_to_write, additional_expected_paths, additional_post_timestamp_checks}
|
{sitemap_content, sitemap_to_write, additional_expected_paths,
|
||||||
|
additional_post_timestamp_checks}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp disk_generated_files(project_id) do
|
defp disk_generated_files(project_id) do
|
||||||
@@ -544,21 +588,52 @@ defmodule BDS.Generation do
|
|||||||
segments = String.split(relative_path, "/", trim: true)
|
segments = String.split(relative_path, "/", trim: true)
|
||||||
|
|
||||||
case strip_language_prefix(segments) do
|
case strip_language_prefix(segments) do
|
||||||
["404.html"] -> :core
|
["404.html"] ->
|
||||||
["index.html"] -> :core
|
:core
|
||||||
["page", _page, "index.html"] -> :core
|
|
||||||
["sitemap.xml"] -> :core
|
["index.html"] ->
|
||||||
["feed.xml"] -> :core
|
:core
|
||||||
["atom.xml"] -> :core
|
|
||||||
["calendar.json"] -> :core
|
["page", _page, "index.html"] ->
|
||||||
["pagefind" | _rest] -> :core
|
:core
|
||||||
[year, month, day, "index.html"] when byte_size(year) == 4 and byte_size(month) == 2 and byte_size(day) == 2 -> :date
|
|
||||||
[year, month, day, _slug, "index.html"] when byte_size(year) == 4 and byte_size(month) == 2 and byte_size(day) == 2 -> :single
|
["sitemap.xml"] ->
|
||||||
["category" | _rest] -> :category
|
:core
|
||||||
["tag" | _rest] -> :tag
|
|
||||||
[year, "index.html"] when byte_size(year) == 4 -> :date
|
["feed.xml"] ->
|
||||||
[year, month, "index.html"] when byte_size(year) == 4 and byte_size(month) == 2 -> :date
|
:core
|
||||||
_other -> :core
|
|
||||||
|
["atom.xml"] ->
|
||||||
|
:core
|
||||||
|
|
||||||
|
["calendar.json"] ->
|
||||||
|
:core
|
||||||
|
|
||||||
|
["pagefind" | _rest] ->
|
||||||
|
:core
|
||||||
|
|
||||||
|
[year, month, day, "index.html"]
|
||||||
|
when byte_size(year) == 4 and byte_size(month) == 2 and byte_size(day) == 2 ->
|
||||||
|
:date
|
||||||
|
|
||||||
|
[year, month, day, _slug, "index.html"]
|
||||||
|
when byte_size(year) == 4 and byte_size(month) == 2 and byte_size(day) == 2 ->
|
||||||
|
:single
|
||||||
|
|
||||||
|
["category" | _rest] ->
|
||||||
|
:category
|
||||||
|
|
||||||
|
["tag" | _rest] ->
|
||||||
|
:tag
|
||||||
|
|
||||||
|
[year, "index.html"] when byte_size(year) == 4 ->
|
||||||
|
:date
|
||||||
|
|
||||||
|
[year, month, "index.html"] when byte_size(year) == 4 and byte_size(month) == 2 ->
|
||||||
|
:date
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
:core
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -615,7 +690,9 @@ defmodule BDS.Generation do
|
|||||||
generated_file.relative_path == ^relative_path
|
generated_file.relative_path == ^relative_path
|
||||||
)
|
)
|
||||||
|
|
||||||
{pruned_count, _last_dir} = prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, ""))
|
{pruned_count, _last_dir} =
|
||||||
|
prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, ""))
|
||||||
|
|
||||||
{deleted_count + 1, removed_dir_count + pruned_count}
|
{deleted_count + 1, removed_dir_count + pruned_count}
|
||||||
|
|
||||||
{:error, :enoent} ->
|
{:error, :enoent} ->
|
||||||
@@ -634,7 +711,12 @@ defmodule BDS.Generation do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
Enum.each(ancillary_paths, fn relative_path ->
|
Enum.each(ancillary_paths, fn relative_path ->
|
||||||
_ = write_generated_file(project_id, relative_path, Map.fetch!(expected_output_map, relative_path))
|
_ =
|
||||||
|
write_generated_file(
|
||||||
|
project_id,
|
||||||
|
relative_path,
|
||||||
|
Map.fetch!(expected_output_map, relative_path)
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ defmodule BDS.Generation.Data do
|
|||||||
post_snapshot_candidates
|
post_snapshot_candidates
|
||||||
|> Enum.with_index(1)
|
|> Enum.with_index(1)
|
||||||
|> Enum.reduce(%{}, fn {post, index}, acc ->
|
|> Enum.reduce(%{}, fn {post, index}, acc ->
|
||||||
:ok = report_snapshot_stage_progress(on_snapshot_progress, :posts, index, length(post_snapshot_candidates))
|
:ok =
|
||||||
|
report_snapshot_stage_progress(
|
||||||
|
on_snapshot_progress,
|
||||||
|
:posts,
|
||||||
|
index,
|
||||||
|
length(post_snapshot_candidates)
|
||||||
|
)
|
||||||
|
|
||||||
case published_post_snapshot(project_data_dir, post) do
|
case published_post_snapshot(project_data_dir, post) do
|
||||||
nil -> acc
|
nil -> acc
|
||||||
@@ -54,7 +60,9 @@ defmodule BDS.Generation.Data do
|
|||||||
|> then(fn published ->
|
|> then(fn published ->
|
||||||
draft_candidates
|
draft_candidates
|
||||||
|> merge_generation_snapshots(snapshots_by_id)
|
|> merge_generation_snapshots(snapshots_by_id)
|
||||||
|> Enum.reduce(Map.new(published, &{&1.id, &1}), fn post, acc -> Map.put(acc, post.id, post) end)
|
|> Enum.reduce(Map.new(published, &{&1.id, &1}), fn post, acc ->
|
||||||
|
Map.put(acc, post.id, post)
|
||||||
|
end)
|
||||||
|> Map.values()
|
|> Map.values()
|
||||||
end)
|
end)
|
||||||
|> Enum.sort_by(&{-(&1.created_at || 0), -(&1.published_at || 0), to_string(&1.slug)})
|
|> Enum.sort_by(&{-(&1.created_at || 0), -(&1.published_at || 0), to_string(&1.slug)})
|
||||||
@@ -100,7 +108,12 @@ defmodule BDS.Generation.Data do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec resolve_posts_for_language([map()], String.t() | nil, map(), String.t() | nil) :: [map()]
|
@spec resolve_posts_for_language([map()], String.t() | nil, map(), String.t() | nil) :: [map()]
|
||||||
def resolve_posts_for_language(posts, target_language, translations_by_post_language, main_language) do
|
def resolve_posts_for_language(
|
||||||
|
posts,
|
||||||
|
target_language,
|
||||||
|
translations_by_post_language,
|
||||||
|
main_language
|
||||||
|
) do
|
||||||
target = String.downcase(to_string(target_language || ""))
|
target = String.downcase(to_string(target_language || ""))
|
||||||
main = String.downcase(to_string(main_language || ""))
|
main = String.downcase(to_string(main_language || ""))
|
||||||
|
|
||||||
@@ -126,22 +139,42 @@ defmodule BDS.Generation.Data do
|
|||||||
|
|
||||||
@spec build_generation_post_index([map()]) :: map()
|
@spec build_generation_post_index([map()]) :: map()
|
||||||
def build_generation_post_index(posts) do
|
def build_generation_post_index(posts) do
|
||||||
Enum.reduce(posts, %{posts_by_category: %{}, posts_by_tag: %{}, posts_by_year: %{}, posts_by_year_month: %{}, posts_by_year_month_day: %{}}, fn post, acc ->
|
Enum.reduce(
|
||||||
{year, month_value, day_value} = local_date_parts!(post.created_at)
|
posts,
|
||||||
month = String.pad_leading(Integer.to_string(month_value), 2, "0")
|
%{
|
||||||
day = String.pad_leading(Integer.to_string(day_value), 2, "0")
|
posts_by_category: %{},
|
||||||
year_month = "#{year}/#{month}"
|
posts_by_tag: %{},
|
||||||
year_month_day = "#{year}/#{month}/#{day}"
|
posts_by_year: %{},
|
||||||
|
posts_by_year_month: %{},
|
||||||
|
posts_by_year_month_day: %{}
|
||||||
|
},
|
||||||
|
fn post, acc ->
|
||||||
|
{year, month_value, day_value} = local_date_parts!(post.created_at)
|
||||||
|
month = String.pad_leading(Integer.to_string(month_value), 2, "0")
|
||||||
|
day = String.pad_leading(Integer.to_string(day_value), 2, "0")
|
||||||
|
year_month = "#{year}/#{month}"
|
||||||
|
year_month_day = "#{year}/#{month}/#{day}"
|
||||||
|
|
||||||
acc
|
acc
|
||||||
|> append_generation_index(:posts_by_year, year, post)
|
|> append_generation_index(:posts_by_year, year, post)
|
||||||
|> append_generation_index(:posts_by_year_month, year_month, post)
|
|> append_generation_index(:posts_by_year_month, year_month, post)
|
||||||
|> append_generation_index(:posts_by_year_month_day, year_month_day, post)
|
|> append_generation_index(:posts_by_year_month_day, year_month_day, post)
|
||||||
|> then(fn indexed ->
|
|> then(fn indexed ->
|
||||||
indexed = Enum.reduce(post.categories || [], indexed, &append_generation_index(&2, :posts_by_category, &1, post))
|
indexed =
|
||||||
Enum.reduce(post.tags || [], indexed, &append_generation_index(&2, :posts_by_tag, &1, post))
|
Enum.reduce(
|
||||||
end)
|
post.categories || [],
|
||||||
end)
|
indexed,
|
||||||
|
&append_generation_index(&2, :posts_by_category, &1, post)
|
||||||
|
)
|
||||||
|
|
||||||
|
Enum.reduce(
|
||||||
|
post.tags || [],
|
||||||
|
indexed,
|
||||||
|
&append_generation_index(&2, :posts_by_tag, &1, post)
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
## --- internals -----------------------------------------------------------
|
## --- internals -----------------------------------------------------------
|
||||||
@@ -168,9 +201,11 @@ defmodule BDS.Generation.Data do
|
|||||||
"page" => %{render_in_lists: false, show_title: true}
|
"page" => %{render_in_lists: false, show_title: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
Enum.reduce(Map.get(plan, :category_settings, %{}) || %{}, defaults, fn {category, settings}, acc ->
|
Enum.reduce(Map.get(plan, :category_settings, %{}) || %{}, defaults, fn {category, settings},
|
||||||
|
acc ->
|
||||||
Map.put(acc, category, %{
|
Map.put(acc, category, %{
|
||||||
render_in_lists: category_setting_flag(settings, :render_in_lists, "render_in_lists", true),
|
render_in_lists:
|
||||||
|
category_setting_flag(settings, :render_in_lists, "render_in_lists", true),
|
||||||
show_title: category_setting_flag(settings, :show_title, "show_title", true)
|
show_title: category_setting_flag(settings, :show_title, "show_title", true)
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
@@ -207,23 +242,30 @@ defmodule BDS.Generation.Data do
|
|||||||
{:ok, contents} ->
|
{:ok, contents} ->
|
||||||
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
||||||
|
|
||||||
%Post{fallback_post |
|
%Post{
|
||||||
id: DocumentFields.get(fields, "id", fallback_post.id),
|
fallback_post
|
||||||
title: DocumentFields.get(fields, "title", fallback_post.title) || "",
|
| id: DocumentFields.get(fields, "id", fallback_post.id),
|
||||||
slug: DocumentFields.fetch!(fields, "slug"),
|
title: DocumentFields.get(fields, "title", fallback_post.title) || "",
|
||||||
excerpt: Map.get(fields, "excerpt"),
|
slug: DocumentFields.fetch!(fields, "slug"),
|
||||||
content: nil,
|
excerpt: Map.get(fields, "excerpt"),
|
||||||
status: :published,
|
content: nil,
|
||||||
author: Map.get(fields, "author"),
|
status: :published,
|
||||||
language: Map.get(fields, "language", fallback_post.language),
|
author: Map.get(fields, "author"),
|
||||||
do_not_translate: DocumentFields.get(fields, "doNotTranslate", fallback_post.do_not_translate || false),
|
language: Map.get(fields, "language", fallback_post.language),
|
||||||
template_slug: DocumentFields.get(fields, "templateSlug", fallback_post.template_slug),
|
do_not_translate:
|
||||||
created_at: DocumentFields.get(fields, "createdAt", fallback_post.created_at),
|
DocumentFields.get(
|
||||||
updated_at: DocumentFields.get(fields, "updatedAt", fallback_post.updated_at),
|
fields,
|
||||||
published_at: DocumentFields.get(fields, "publishedAt", fallback_post.published_at),
|
"doNotTranslate",
|
||||||
file_path: fallback_post.file_path,
|
fallback_post.do_not_translate || false
|
||||||
tags: Map.get(fields, "tags", fallback_post.tags || []),
|
),
|
||||||
categories: Map.get(fields, "categories", fallback_post.categories || [])
|
template_slug:
|
||||||
|
DocumentFields.get(fields, "templateSlug", fallback_post.template_slug),
|
||||||
|
created_at: DocumentFields.get(fields, "createdAt", fallback_post.created_at),
|
||||||
|
updated_at: DocumentFields.get(fields, "updatedAt", fallback_post.updated_at),
|
||||||
|
published_at: DocumentFields.get(fields, "publishedAt", fallback_post.published_at),
|
||||||
|
file_path: fallback_post.file_path,
|
||||||
|
tags: Map.get(fields, "tags", fallback_post.tags || []),
|
||||||
|
categories: Map.get(fields, "categories", fallback_post.categories || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
@@ -231,13 +273,20 @@ defmodule BDS.Generation.Data do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_generation_route_posts(project_id, project_data_dir, published_posts, on_snapshot_progress) do
|
defp build_generation_route_posts(
|
||||||
|
project_id,
|
||||||
|
project_data_dir,
|
||||||
|
published_posts,
|
||||||
|
on_snapshot_progress
|
||||||
|
) do
|
||||||
source_post_ids = Enum.map(published_posts, & &1.id)
|
source_post_ids = Enum.map(published_posts, & &1.id)
|
||||||
|
|
||||||
translation_candidates =
|
translation_candidates =
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from translation in Translation,
|
from translation in Translation,
|
||||||
where: translation.project_id == ^project_id and translation.translation_for in ^source_post_ids,
|
where:
|
||||||
|
translation.project_id == ^project_id and
|
||||||
|
translation.translation_for in ^source_post_ids,
|
||||||
where: translation.status in [:published, :draft],
|
where: translation.status in [:published, :draft],
|
||||||
order_by: [asc: translation.translation_for, asc: translation.language]
|
order_by: [asc: translation.translation_for, asc: translation.language]
|
||||||
)
|
)
|
||||||
@@ -246,7 +295,13 @@ defmodule BDS.Generation.Data do
|
|||||||
translation_candidates
|
translation_candidates
|
||||||
|> Enum.with_index(1)
|
|> Enum.with_index(1)
|
||||||
|> Enum.reduce(%{}, fn {translation, index}, acc ->
|
|> Enum.reduce(%{}, fn {translation, index}, acc ->
|
||||||
:ok = report_snapshot_stage_progress(on_snapshot_progress, :translations, index, length(translation_candidates))
|
:ok =
|
||||||
|
report_snapshot_stage_progress(
|
||||||
|
on_snapshot_progress,
|
||||||
|
:translations,
|
||||||
|
index,
|
||||||
|
length(translation_candidates)
|
||||||
|
)
|
||||||
|
|
||||||
case published_translation_snapshot(project_data_dir, translation) do
|
case published_translation_snapshot(project_data_dir, translation) do
|
||||||
nil -> acc
|
nil -> acc
|
||||||
@@ -288,18 +343,20 @@ defmodule BDS.Generation.Data do
|
|||||||
{:ok, contents} ->
|
{:ok, contents} ->
|
||||||
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
||||||
|
|
||||||
%Translation{fallback_translation |
|
%Translation{
|
||||||
id: DocumentFields.get(fields, "id", fallback_translation.id),
|
fallback_translation
|
||||||
translation_for: DocumentFields.fetch!(fields, "translationFor"),
|
| id: DocumentFields.get(fields, "id", fallback_translation.id),
|
||||||
language: DocumentFields.fetch!(fields, "language"),
|
translation_for: DocumentFields.fetch!(fields, "translationFor"),
|
||||||
title: DocumentFields.get(fields, "title", fallback_translation.title) || "",
|
language: DocumentFields.fetch!(fields, "language"),
|
||||||
excerpt: Map.get(fields, "excerpt", fallback_translation.excerpt),
|
title: DocumentFields.get(fields, "title", fallback_translation.title) || "",
|
||||||
content: nil,
|
excerpt: Map.get(fields, "excerpt", fallback_translation.excerpt),
|
||||||
status: :published,
|
content: nil,
|
||||||
created_at: DocumentFields.get(fields, "createdAt", fallback_translation.created_at),
|
status: :published,
|
||||||
updated_at: DocumentFields.get(fields, "updatedAt", fallback_translation.updated_at),
|
created_at: DocumentFields.get(fields, "createdAt", fallback_translation.created_at),
|
||||||
published_at: DocumentFields.get(fields, "publishedAt", fallback_translation.published_at),
|
updated_at: DocumentFields.get(fields, "updatedAt", fallback_translation.updated_at),
|
||||||
file_path: fallback_translation.file_path
|
published_at:
|
||||||
|
DocumentFields.get(fields, "publishedAt", fallback_translation.published_at),
|
||||||
|
file_path: fallback_translation.file_path
|
||||||
}
|
}
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
|
|||||||
@@ -25,8 +25,16 @@ defmodule BDS.Generation.Outputs do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec build_validation_route_paths(map(), [map()], [map()], map(), String.t() | nil) :: [String.t()]
|
@spec build_validation_route_paths(map(), [map()], [map()], map(), String.t() | nil) :: [
|
||||||
def build_validation_route_paths(plan, route_posts, published_list_posts, post_index, route_language) do
|
String.t()
|
||||||
|
]
|
||||||
|
def build_validation_route_paths(
|
||||||
|
plan,
|
||||||
|
route_posts,
|
||||||
|
published_list_posts,
|
||||||
|
post_index,
|
||||||
|
route_language
|
||||||
|
) do
|
||||||
[
|
[
|
||||||
core_route_paths(plan, published_list_posts, route_language),
|
core_route_paths(plan, published_list_posts, route_language),
|
||||||
page_route_paths(plan, route_posts, route_language),
|
page_route_paths(plan, route_posts, route_language),
|
||||||
@@ -250,7 +258,9 @@ defmodule BDS.Generation.Outputs do
|
|||||||
Enum.flat_map(posts_by_tag, fn {tag, posts} ->
|
Enum.flat_map(posts_by_tag, fn {tag, posts} ->
|
||||||
tag_slug = archive_route_segment(tag)
|
tag_slug = archive_route_segment(tag)
|
||||||
|
|
||||||
build_paginated_archive_outputs(plan, languages, ["tag", tag_slug], posts, fn page_posts, language, pagination ->
|
build_paginated_archive_outputs(plan, languages, ["tag", tag_slug], posts, fn page_posts,
|
||||||
|
language,
|
||||||
|
pagination ->
|
||||||
render_archive_page(plan, tag, page_posts, language, "tag", pagination)
|
render_archive_page(plan, tag, page_posts, language, "tag", pagination)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -260,23 +270,31 @@ defmodule BDS.Generation.Outputs do
|
|||||||
def build_date_outputs(plan, post_index, languages) do
|
def build_date_outputs(plan, post_index, languages) do
|
||||||
year_outputs =
|
year_outputs =
|
||||||
Enum.flat_map(post_index.posts_by_year, fn {year, posts} ->
|
Enum.flat_map(post_index.posts_by_year, fn {year, posts} ->
|
||||||
build_paginated_archive_outputs(plan, languages, [Integer.to_string(year)], posts, fn page_posts, language, pagination ->
|
build_paginated_archive_outputs(
|
||||||
render_date_archive_page(
|
plan,
|
||||||
plan,
|
languages,
|
||||||
Integer.to_string(year),
|
[Integer.to_string(year)],
|
||||||
%{kind: "year", year: year},
|
posts,
|
||||||
page_posts,
|
fn page_posts, language, pagination ->
|
||||||
language,
|
render_date_archive_page(
|
||||||
pagination
|
plan,
|
||||||
)
|
Integer.to_string(year),
|
||||||
end)
|
%{kind: "year", year: year},
|
||||||
|
page_posts,
|
||||||
|
language,
|
||||||
|
pagination
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
month_outputs =
|
month_outputs =
|
||||||
Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
|
Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
|
||||||
[year, month] = String.split(year_month, "/", parts: 2)
|
[year, month] = String.split(year_month, "/", parts: 2)
|
||||||
|
|
||||||
build_paginated_archive_outputs(plan, languages, [year, month], posts, fn page_posts, language, pagination ->
|
build_paginated_archive_outputs(plan, languages, [year, month], posts, fn page_posts,
|
||||||
|
language,
|
||||||
|
pagination ->
|
||||||
render_date_archive_page(
|
render_date_archive_page(
|
||||||
plan,
|
plan,
|
||||||
"#{year}-#{month}",
|
"#{year}-#{month}",
|
||||||
@@ -292,11 +310,18 @@ defmodule BDS.Generation.Outputs do
|
|||||||
Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} ->
|
Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} ->
|
||||||
[year, month, day] = String.split(year_month_day, "/", parts: 3)
|
[year, month, day] = String.split(year_month_day, "/", parts: 3)
|
||||||
|
|
||||||
build_paginated_archive_outputs(plan, languages, [year, month, day], posts, fn page_posts, language, pagination ->
|
build_paginated_archive_outputs(plan, languages, [year, month, day], posts, fn page_posts,
|
||||||
|
language,
|
||||||
|
pagination ->
|
||||||
render_date_archive_page(
|
render_date_archive_page(
|
||||||
plan,
|
plan,
|
||||||
"#{year}-#{month}-#{day}",
|
"#{year}-#{month}-#{day}",
|
||||||
%{kind: "day", year: String.to_integer(year), month: String.to_integer(month), day: String.to_integer(day)},
|
%{
|
||||||
|
kind: "day",
|
||||||
|
year: String.to_integer(year),
|
||||||
|
month: String.to_integer(month),
|
||||||
|
day: String.to_integer(day)
|
||||||
|
},
|
||||||
page_posts,
|
page_posts,
|
||||||
language,
|
language,
|
||||||
pagination
|
pagination
|
||||||
@@ -323,19 +348,32 @@ defmodule BDS.Generation.Outputs do
|
|||||||
Enum.flat_map(additional_languages, fn localized_language ->
|
Enum.flat_map(additional_languages, fn localized_language ->
|
||||||
localized_prefix = route_language(plan.language, localized_language)
|
localized_prefix = route_language(plan.language, localized_language)
|
||||||
localized_source_posts = Map.get(localized_posts_by_language, localized_language, [])
|
localized_source_posts = Map.get(localized_posts_by_language, localized_language, [])
|
||||||
localized_posts = build_list_posts(plan.base_url, localized_source_posts, localized_prefix)
|
|
||||||
|
localized_posts =
|
||||||
|
build_list_posts(plan.base_url, localized_source_posts, localized_prefix)
|
||||||
|
|
||||||
build_root_outputs(plan, localized_language, localized_posts) ++
|
build_root_outputs(plan, localized_language, localized_posts) ++
|
||||||
[
|
[
|
||||||
{Path.join(localized_language, "404.html"), render_not_found_output(plan, localized_language)},
|
{Path.join(localized_language, "404.html"),
|
||||||
{Path.join(localized_language, "feed.xml"), render_feed(plan, localized_language, localized_source_posts)},
|
render_not_found_output(plan, localized_language)},
|
||||||
{Path.join(localized_language, "atom.xml"), render_atom(plan, localized_language, localized_source_posts)}
|
{Path.join(localized_language, "feed.xml"),
|
||||||
|
render_feed(plan, localized_language, localized_source_posts)},
|
||||||
|
{Path.join(localized_language, "atom.xml"),
|
||||||
|
render_atom(plan, localized_language, localized_source_posts)}
|
||||||
]
|
]
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map()) :: [{String.t(), iodata()}]
|
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map()) :: [
|
||||||
def build_page_outputs(project_id, main_language, published_posts, translations_by_post_language, localized_posts_by_language) do
|
{String.t(), iodata()}
|
||||||
|
]
|
||||||
|
def build_page_outputs(
|
||||||
|
project_id,
|
||||||
|
main_language,
|
||||||
|
published_posts,
|
||||||
|
translations_by_post_language,
|
||||||
|
localized_posts_by_language
|
||||||
|
) do
|
||||||
page_outputs =
|
page_outputs =
|
||||||
published_posts
|
published_posts
|
||||||
|> Enum.filter(&("page" in (&1.categories || [])))
|
|> Enum.filter(&("page" in (&1.categories || [])))
|
||||||
@@ -355,7 +393,14 @@ defmodule BDS.Generation.Outputs do
|
|||||||
language: canonical_variant.language,
|
language: canonical_variant.language,
|
||||||
excerpt: canonical_variant.excerpt
|
excerpt: canonical_variant.excerpt
|
||||||
},
|
},
|
||||||
fn -> render_post_page(canonical_variant.title, body, post.slug, canonical_variant.language) end
|
fn ->
|
||||||
|
render_post_page(
|
||||||
|
canonical_variant.title,
|
||||||
|
body,
|
||||||
|
post.slug,
|
||||||
|
canonical_variant.language
|
||||||
|
)
|
||||||
|
end
|
||||||
)}
|
)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -404,13 +449,22 @@ defmodule BDS.Generation.Outputs do
|
|||||||
plan.project_name,
|
plan.project_name,
|
||||||
page_posts,
|
page_posts,
|
||||||
%{kind: "core"},
|
%{kind: "core"},
|
||||||
pagination_for_page(page_number, total_pages, length(posts), plan.max_posts_per_page, route_language, []),
|
pagination_for_page(
|
||||||
|
page_number,
|
||||||
|
total_pages,
|
||||||
|
length(posts),
|
||||||
|
plan.max_posts_per_page,
|
||||||
|
route_language,
|
||||||
|
[]
|
||||||
|
),
|
||||||
fn -> render_home(plan, language) end
|
fn -> render_home(plan, language) end
|
||||||
)}
|
)}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec build_paginated_archive_outputs(map(), [String.t()], [String.t()], [map()], (... -> iodata())) :: [{String.t(), iodata()}]
|
@spec build_paginated_archive_outputs(map(), [String.t()], [String.t()], [map()], (... ->
|
||||||
|
iodata())) ::
|
||||||
|
[{String.t(), iodata()}]
|
||||||
def build_paginated_archive_outputs(plan, languages, segments, posts, render_fun) do
|
def build_paginated_archive_outputs(plan, languages, segments, posts, render_fun) do
|
||||||
total_pages = page_count(length(posts), plan.max_posts_per_page)
|
total_pages = page_count(length(posts), plan.max_posts_per_page)
|
||||||
|
|
||||||
@@ -425,13 +479,22 @@ defmodule BDS.Generation.Outputs do
|
|||||||
render_fun.(
|
render_fun.(
|
||||||
page_posts,
|
page_posts,
|
||||||
language,
|
language,
|
||||||
pagination_for_page(page_number, total_pages, length(posts), plan.max_posts_per_page, route_language, segments)
|
pagination_for_page(
|
||||||
|
page_number,
|
||||||
|
total_pages,
|
||||||
|
length(posts),
|
||||||
|
plan.max_posts_per_page,
|
||||||
|
route_language,
|
||||||
|
segments
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map()) :: [{String.t(), iodata()}]
|
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map()) :: [
|
||||||
|
{String.t(), iodata()}
|
||||||
|
]
|
||||||
def build_single_outputs(
|
def build_single_outputs(
|
||||||
project_id,
|
project_id,
|
||||||
main_language,
|
main_language,
|
||||||
@@ -457,7 +520,12 @@ defmodule BDS.Generation.Outputs do
|
|||||||
excerpt: canonical_variant.excerpt
|
excerpt: canonical_variant.excerpt
|
||||||
},
|
},
|
||||||
fn ->
|
fn ->
|
||||||
render_post_page(canonical_variant.title, body, post.slug, canonical_variant.language)
|
render_post_page(
|
||||||
|
canonical_variant.title,
|
||||||
|
body,
|
||||||
|
post.slug,
|
||||||
|
canonical_variant.language
|
||||||
|
)
|
||||||
end
|
end
|
||||||
)}
|
)}
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ defmodule BDS.Generation.Pagefind do
|
|||||||
|> Enum.flat_map(fn language ->
|
|> Enum.flat_map(fn language ->
|
||||||
route_language = route_language(plan.language, language)
|
route_language = route_language(plan.language, language)
|
||||||
pages = pages_for_language(html_outputs, route_language)
|
pages = pages_for_language(html_outputs, route_language)
|
||||||
prefix = if route_language in [nil, ""], do: ["pagefind"], else: [route_language, "pagefind"]
|
|
||||||
|
prefix =
|
||||||
|
if route_language in [nil, ""], do: ["pagefind"], else: [route_language, "pagefind"]
|
||||||
|
|
||||||
[
|
[
|
||||||
{Path.join(prefix ++ ["index.json"]), Jason.encode!(%{"language" => language, "pages" => pages})},
|
{Path.join(prefix ++ ["index.json"]),
|
||||||
|
Jason.encode!(%{"language" => language, "pages" => pages})},
|
||||||
{Path.join(prefix ++ ["pagefind-ui.js"]), ui_js(language)},
|
{Path.join(prefix ++ ["pagefind-ui.js"]), ui_js(language)},
|
||||||
{Path.join(prefix ++ ["pagefind-ui.css"]), ui_css()}
|
{Path.join(prefix ++ ["pagefind-ui.css"]), ui_css()}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -49,18 +49,37 @@ defmodule BDS.Generation.Paths do
|
|||||||
def root_output_path(nil, 1), do: "index.html"
|
def root_output_path(nil, 1), do: "index.html"
|
||||||
def root_output_path("", 1), do: "index.html"
|
def root_output_path("", 1), do: "index.html"
|
||||||
def root_output_path(route_language, 1), do: Path.join(route_language, "index.html")
|
def root_output_path(route_language, 1), do: Path.join(route_language, "index.html")
|
||||||
def root_output_path(nil, page_number), do: Path.join(["page", Integer.to_string(page_number), "index.html"])
|
|
||||||
|
def root_output_path(nil, page_number),
|
||||||
|
do: Path.join(["page", Integer.to_string(page_number), "index.html"])
|
||||||
|
|
||||||
def root_output_path("", page_number), do: root_output_path(nil, page_number)
|
def root_output_path("", page_number), do: root_output_path(nil, page_number)
|
||||||
def root_output_path(route_language, page_number), do: Path.join([route_language, "page", Integer.to_string(page_number), "index.html"])
|
|
||||||
|
def root_output_path(route_language, page_number),
|
||||||
|
do: Path.join([route_language, "page", Integer.to_string(page_number), "index.html"])
|
||||||
|
|
||||||
@spec page_output_path(String.t(), language()) :: String.t()
|
@spec page_output_path(String.t(), language()) :: String.t()
|
||||||
def page_output_path(slug, nil), do: Path.join([slug, "index.html"])
|
def page_output_path(slug, nil), do: Path.join([slug, "index.html"])
|
||||||
def page_output_path(slug, ""), do: page_output_path(slug, nil)
|
def page_output_path(slug, ""), do: page_output_path(slug, nil)
|
||||||
def page_output_path(slug, language), do: Path.join([language, slug, "index.html"])
|
def page_output_path(slug, language), do: Path.join([language, slug, "index.html"])
|
||||||
|
|
||||||
@spec pagination_for_page(pos_integer(), pos_integer(), non_neg_integer(), pos_integer(), language(), [String.t()]) ::
|
@spec pagination_for_page(
|
||||||
|
pos_integer(),
|
||||||
|
pos_integer(),
|
||||||
|
non_neg_integer(),
|
||||||
|
pos_integer(),
|
||||||
|
language(),
|
||||||
|
[String.t()]
|
||||||
|
) ::
|
||||||
map()
|
map()
|
||||||
def pagination_for_page(page_number, total_pages, total_items, items_per_page, route_language, segments) do
|
def pagination_for_page(
|
||||||
|
page_number,
|
||||||
|
total_pages,
|
||||||
|
total_items,
|
||||||
|
items_per_page,
|
||||||
|
route_language,
|
||||||
|
segments
|
||||||
|
) do
|
||||||
%{
|
%{
|
||||||
current_page: page_number,
|
current_page: page_number,
|
||||||
total_pages: total_pages,
|
total_pages: total_pages,
|
||||||
@@ -75,8 +94,12 @@ defmodule BDS.Generation.Paths do
|
|||||||
|
|
||||||
@spec archive_or_root_href(language(), [String.t()], integer()) :: String.t()
|
@spec archive_or_root_href(language(), [String.t()], integer()) :: String.t()
|
||||||
def archive_or_root_href(_route_language, _segments, page_number) when page_number < 1, do: ""
|
def archive_or_root_href(_route_language, _segments, page_number) when page_number < 1, do: ""
|
||||||
def archive_or_root_href(route_language, [], page_number), do: root_page_href(route_language, page_number)
|
|
||||||
def archive_or_root_href(route_language, segments, page_number), do: archive_href(route_language, segments, page_number)
|
def archive_or_root_href(route_language, [], page_number),
|
||||||
|
do: root_page_href(route_language, page_number)
|
||||||
|
|
||||||
|
def archive_or_root_href(route_language, segments, page_number),
|
||||||
|
do: archive_href(route_language, segments, page_number)
|
||||||
|
|
||||||
@spec root_page_href(language(), integer()) :: String.t()
|
@spec root_page_href(language(), integer()) :: String.t()
|
||||||
def root_page_href(route_language, page_number) when page_number <= 1 do
|
def root_page_href(route_language, page_number) when page_number <= 1 do
|
||||||
@@ -147,7 +170,9 @@ defmodule BDS.Generation.Paths do
|
|||||||
|
|
||||||
@spec archive_route_segment(any()) :: String.t()
|
@spec archive_route_segment(any()) :: String.t()
|
||||||
def archive_route_segment(nil), do: ""
|
def archive_route_segment(nil), do: ""
|
||||||
def archive_route_segment(value), do: value |> to_string() |> URI.encode(&URI.char_unreserved?/1)
|
|
||||||
|
def archive_route_segment(value),
|
||||||
|
do: value |> to_string() |> URI.encode(&URI.char_unreserved?/1)
|
||||||
|
|
||||||
@spec normalize_base_url(String.t() | nil) :: String.t() | nil
|
@spec normalize_base_url(String.t() | nil) :: String.t() | nil
|
||||||
def normalize_base_url(nil), do: nil
|
def normalize_base_url(nil), do: nil
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ defmodule BDS.Generation.Renderers do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc "Render an archive page (category, tag, year) with pagination."
|
@doc "Render an archive page (category, tag, year) with pagination."
|
||||||
@spec render_archive_page(map(), String.t(), [map()], String.t() | nil, String.t(), map()) :: String.t()
|
@spec render_archive_page(map(), String.t(), [map()], String.t() | nil, String.t(), map()) ::
|
||||||
|
String.t()
|
||||||
def render_archive_page(plan, title, posts, language, kind, pagination) do
|
def render_archive_page(plan, title, posts, language, kind, pagination) do
|
||||||
fallback = fn ->
|
fallback = fn ->
|
||||||
items =
|
items =
|
||||||
@@ -130,7 +131,15 @@ defmodule BDS.Generation.Renderers do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc "Render a list/archive page through the project template, falling back to inline."
|
@doc "Render a list/archive page through the project template, falling back to inline."
|
||||||
@spec render_list_output(map(), String.t() | nil, String.t(), [map()], map(), map(), (-> String.t())) ::
|
@spec render_list_output(
|
||||||
|
map(),
|
||||||
|
String.t() | nil,
|
||||||
|
String.t(),
|
||||||
|
[map()],
|
||||||
|
map(),
|
||||||
|
map(),
|
||||||
|
(-> String.t())
|
||||||
|
) ::
|
||||||
String.t()
|
String.t()
|
||||||
def render_list_output(
|
def render_list_output(
|
||||||
%{project_id: project_id, language: main_language},
|
%{project_id: project_id, language: main_language},
|
||||||
|
|||||||
@@ -34,17 +34,20 @@ defmodule BDS.Generation.Sitemap do
|
|||||||
build_hreflang_links(plan.base_url, "/", plan.language, all_languages)
|
build_hreflang_links(plan.base_url, "/", plan.language, all_languages)
|
||||||
)
|
)
|
||||||
] ++
|
] ++
|
||||||
Enum.map(Paths.root_pagination_pages(length(published_list_posts), plan.max_posts_per_page), fn page_number ->
|
Enum.map(
|
||||||
page_path = "/page/#{page_number}"
|
Paths.root_pagination_pages(length(published_list_posts), plan.max_posts_per_page),
|
||||||
|
fn page_number ->
|
||||||
|
page_path = "/page/#{page_number}"
|
||||||
|
|
||||||
url_entry(
|
url_entry(
|
||||||
Paths.url_for_path(plan.base_url, page_path),
|
Paths.url_for_path(plan.base_url, page_path),
|
||||||
latest_post_updated_at,
|
latest_post_updated_at,
|
||||||
"daily",
|
"daily",
|
||||||
"0.9",
|
"0.9",
|
||||||
build_hreflang_links(plan.base_url, page_path, plan.language, all_languages)
|
build_hreflang_links(plan.base_url, page_path, plan.language, all_languages)
|
||||||
)
|
)
|
||||||
end) ++
|
end
|
||||||
|
) ++
|
||||||
Enum.map(translatable_posts, fn post ->
|
Enum.map(translatable_posts, fn post ->
|
||||||
post_path = Paths.relative_path_to_url_path(Paths.post_output_path(post))
|
post_path = Paths.relative_path_to_url_path(Paths.post_output_path(post))
|
||||||
|
|
||||||
@@ -100,28 +103,34 @@ defmodule BDS.Generation.Sitemap do
|
|||||||
build_hreflang_links(plan.base_url, year_path, plan.language, all_languages)
|
build_hreflang_links(plan.base_url, year_path, plan.language, all_languages)
|
||||||
)
|
)
|
||||||
end) ++
|
end) ++
|
||||||
Enum.map(Enum.sort_by(post_index.posts_by_year_month, &elem(&1, 0), :desc), fn {year_month, _posts} ->
|
Enum.map(
|
||||||
month_path = "/#{year_month}"
|
Enum.sort_by(post_index.posts_by_year_month, &elem(&1, 0), :desc),
|
||||||
|
fn {year_month, _posts} ->
|
||||||
|
month_path = "/#{year_month}"
|
||||||
|
|
||||||
url_entry(
|
url_entry(
|
||||||
Paths.url_for_path(plan.base_url, month_path),
|
Paths.url_for_path(plan.base_url, month_path),
|
||||||
latest_post_updated_at,
|
latest_post_updated_at,
|
||||||
"monthly",
|
"monthly",
|
||||||
"0.5",
|
"0.5",
|
||||||
build_hreflang_links(plan.base_url, month_path, plan.language, all_languages)
|
build_hreflang_links(plan.base_url, month_path, plan.language, all_languages)
|
||||||
)
|
)
|
||||||
end) ++
|
end
|
||||||
Enum.map(Enum.sort_by(post_index.posts_by_year_month_day, &elem(&1, 0), :desc), fn {year_month_day, _posts} ->
|
) ++
|
||||||
day_path = "/#{year_month_day}"
|
Enum.map(
|
||||||
|
Enum.sort_by(post_index.posts_by_year_month_day, &elem(&1, 0), :desc),
|
||||||
|
fn {year_month_day, _posts} ->
|
||||||
|
day_path = "/#{year_month_day}"
|
||||||
|
|
||||||
url_entry(
|
url_entry(
|
||||||
Paths.url_for_path(plan.base_url, day_path),
|
Paths.url_for_path(plan.base_url, day_path),
|
||||||
latest_post_updated_at,
|
latest_post_updated_at,
|
||||||
"monthly",
|
"monthly",
|
||||||
"0.4",
|
"0.4",
|
||||||
build_hreflang_links(plan.base_url, day_path, plan.language, all_languages)
|
build_hreflang_links(plan.base_url, day_path, plan.language, all_languages)
|
||||||
)
|
)
|
||||||
end) ++
|
end
|
||||||
|
) ++
|
||||||
Enum.map(Enum.sort_by(post_index.posts_by_category, &elem(&1, 0)), fn {category, _posts} ->
|
Enum.map(Enum.sort_by(post_index.posts_by_category, &elem(&1, 0)), fn {category, _posts} ->
|
||||||
category_path = "/category/#{Paths.archive_route_segment(category)}"
|
category_path = "/category/#{Paths.archive_route_segment(category)}"
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defmodule BDS.Generation.Validation do
|
|||||||
relative_path_to_url_path: 1,
|
relative_path_to_url_path: 1,
|
||||||
url_path_to_relative_index_path: 1
|
url_path_to_relative_index_path: 1
|
||||||
]
|
]
|
||||||
|
|
||||||
import BDS.Generation.Progress, only: [report_validation_compare_progress: 3]
|
import BDS.Generation.Progress, only: [report_validation_compare_progress: 3]
|
||||||
import BDS.Generation.Sitemap, only: [extract_locs: 1, loc_to_project_path: 2]
|
import BDS.Generation.Sitemap, only: [extract_locs: 1, loc_to_project_path: 2]
|
||||||
|
|
||||||
@@ -20,7 +21,11 @@ defmodule BDS.Generation.Validation do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec build_post_timestamp_checks(String.t(), [map()], map()) :: [map()]
|
@spec build_post_timestamp_checks(String.t(), [map()], map()) :: [map()]
|
||||||
def build_post_timestamp_checks(project_data_dir, published_route_posts, generated_file_updated_at) do
|
def build_post_timestamp_checks(
|
||||||
|
project_data_dir,
|
||||||
|
published_route_posts,
|
||||||
|
generated_file_updated_at
|
||||||
|
) do
|
||||||
Enum.map(published_route_posts, fn post ->
|
Enum.map(published_route_posts, fn post ->
|
||||||
relative_path = BDS.Generation.Paths.post_output_path(post)
|
relative_path = BDS.Generation.Paths.post_output_path(post)
|
||||||
|
|
||||||
@@ -69,13 +74,19 @@ defmodule BDS.Generation.Validation do
|
|||||||
|> Enum.map(&loc_to_project_path(&1, params.base_url))
|
|> Enum.map(&loc_to_project_path(&1, params.base_url))
|
||||||
|> Enum.reduce(MapSet.new(), &MapSet.put(&2, normalize_url_path(&1)))
|
|> Enum.reduce(MapSet.new(), &MapSet.put(&2, normalize_url_path(&1)))
|
||||||
|> then(fn expected_paths ->
|
|> then(fn expected_paths ->
|
||||||
Enum.reduce(Map.get(params, :additional_expected_paths, []), expected_paths, fn path, acc ->
|
Enum.reduce(Map.get(params, :additional_expected_paths, []), expected_paths, fn path,
|
||||||
|
acc ->
|
||||||
MapSet.put(acc, normalize_url_path(path))
|
MapSet.put(acc, normalize_url_path(path))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{existing_html_path_set, zero_byte_html_path_set} =
|
{existing_html_path_set, zero_byte_html_path_set} =
|
||||||
collect_html_index_paths(index_paths, params.html_dir, params.on_progress, total_compare_steps)
|
collect_html_index_paths(
|
||||||
|
index_paths,
|
||||||
|
params.html_dir,
|
||||||
|
params.on_progress,
|
||||||
|
total_compare_steps
|
||||||
|
)
|
||||||
|
|
||||||
missing_url_paths =
|
missing_url_paths =
|
||||||
expected_path_set
|
expected_path_set
|
||||||
@@ -119,11 +130,14 @@ defmodule BDS.Generation.Validation do
|
|||||||
acc
|
acc
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
html_path = Path.join(params.html_dir, url_path_to_relative_index_path(normalized_url_path))
|
html_path =
|
||||||
|
Path.join(params.html_dir, url_path_to_relative_index_path(normalized_url_path))
|
||||||
|
|
||||||
case {File.stat(html_path, time: :posix), File.stat(check.post_file_path, time: :posix)} do
|
case {File.stat(html_path, time: :posix),
|
||||||
|
File.stat(check.post_file_path, time: :posix)} do
|
||||||
{{:ok, html_stat}, {:ok, post_stat}} ->
|
{{:ok, html_stat}, {:ok, post_stat}} ->
|
||||||
effective_generated_at_ms = max(mtime_ms(html_stat), check.generated_updated_at_ms || 0)
|
effective_generated_at_ms =
|
||||||
|
max(mtime_ms(html_stat), check.generated_updated_at_ms || 0)
|
||||||
|
|
||||||
if mtime_ms(post_stat) > effective_generated_at_ms do
|
if mtime_ms(post_stat) > effective_generated_at_ms do
|
||||||
MapSet.put(acc, normalized_url_path)
|
MapSet.put(acc, normalized_url_path)
|
||||||
@@ -233,7 +247,18 @@ defmodule BDS.Generation.Validation do
|
|||||||
nil ->
|
nil ->
|
||||||
case Regex.run(~r|^/(\d{4})/(\d{2})/(\d{2})/([^/]+)$|, path) do
|
case Regex.run(~r|^/(\d{4})/(\d{2})/(\d{2})/([^/]+)$|, path) do
|
||||||
[_, year, month, day, slug] ->
|
[_, year, month, day, slug] ->
|
||||||
update_in(plan.requested_post_routes, &[ %{year: String.to_integer(year), month: String.to_integer(month), day: String.to_integer(day), slug: slug} | &1 ])
|
update_in(
|
||||||
|
plan.requested_post_routes,
|
||||||
|
&[
|
||||||
|
%{
|
||||||
|
year: String.to_integer(year),
|
||||||
|
month: String.to_integer(month),
|
||||||
|
day: String.to_integer(day),
|
||||||
|
slug: slug
|
||||||
|
}
|
||||||
|
| &1
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
case Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) do
|
case Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) do
|
||||||
@@ -281,29 +306,43 @@ defmodule BDS.Generation.Validation do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
enriched =
|
enriched =
|
||||||
Enum.reduce(initial_plan.requested_post_routes, %{initial_plan | requested_post_routes: targeted_post_routes}, fn route, acc ->
|
Enum.reduce(
|
||||||
case Enum.find(published_posts, &post_matches_route?(&1, route)) do
|
initial_plan.requested_post_routes,
|
||||||
nil ->
|
%{initial_plan | requested_post_routes: targeted_post_routes},
|
||||||
acc
|
fn route, acc ->
|
||||||
|> update_in([:requested_years], &MapSet.put(&1, route.year))
|
case Enum.find(published_posts, &post_matches_route?(&1, route)) do
|
||||||
|> update_in([:requested_year_months], &MapSet.put(&1, route_month_key(route.year, route.month)))
|
nil ->
|
||||||
|> Map.put(:request_root_routes, true)
|
acc
|
||||||
|
|> update_in([:requested_years], &MapSet.put(&1, route.year))
|
||||||
|
|> update_in(
|
||||||
|
[:requested_year_months],
|
||||||
|
&MapSet.put(&1, route_month_key(route.year, route.month))
|
||||||
|
)
|
||||||
|
|> Map.put(:request_root_routes, true)
|
||||||
|
|
||||||
post ->
|
post ->
|
||||||
{year, month, _day} = local_date_parts!(post.created_at)
|
{year, month, _day} = local_date_parts!(post.created_at)
|
||||||
|
|
||||||
acc
|
acc
|
||||||
|> update_in([:requested_category_slugs], fn set ->
|
|> update_in([:requested_category_slugs], fn set ->
|
||||||
Enum.reduce(post.categories || [], set, &MapSet.put(&2, archive_route_segment(&1)))
|
Enum.reduce(
|
||||||
end)
|
post.categories || [],
|
||||||
|> update_in([:requested_tag_slugs], fn set ->
|
set,
|
||||||
Enum.reduce(post.tags || [], set, &MapSet.put(&2, archive_route_segment(&1)))
|
&MapSet.put(&2, archive_route_segment(&1))
|
||||||
end)
|
)
|
||||||
|> update_in([:requested_years], &MapSet.put(&1, year))
|
end)
|
||||||
|> update_in([:requested_year_months], &MapSet.put(&1, route_month_key(year, month)))
|
|> update_in([:requested_tag_slugs], fn set ->
|
||||||
|> Map.put(:request_root_routes, true)
|
Enum.reduce(post.tags || [], set, &MapSet.put(&2, archive_route_segment(&1)))
|
||||||
|
end)
|
||||||
|
|> update_in([:requested_years], &MapSet.put(&1, year))
|
||||||
|
|> update_in(
|
||||||
|
[:requested_year_months],
|
||||||
|
&MapSet.put(&1, route_month_key(year, month))
|
||||||
|
)
|
||||||
|
|> Map.put(:request_root_routes, true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
)
|
||||||
|
|
||||||
language_plans =
|
language_plans =
|
||||||
initial_plan.language_plans
|
initial_plan.language_plans
|
||||||
@@ -314,8 +353,10 @@ defmodule BDS.Generation.Validation do
|
|||||||
|
|
||||||
%{
|
%{
|
||||||
enriched
|
enriched
|
||||||
| requested_category_slugs: MapSet.intersection(enriched.requested_category_slugs, available_category_slugs),
|
| requested_category_slugs:
|
||||||
requested_tag_slugs: MapSet.intersection(enriched.requested_tag_slugs, available_tag_slugs),
|
MapSet.intersection(enriched.requested_category_slugs, available_category_slugs),
|
||||||
|
requested_tag_slugs:
|
||||||
|
MapSet.intersection(enriched.requested_tag_slugs, available_tag_slugs),
|
||||||
language_plans: language_plans
|
language_plans: language_plans
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -351,13 +392,15 @@ defmodule BDS.Generation.Validation do
|
|||||||
{nil, path}
|
{nil, path}
|
||||||
end
|
end
|
||||||
|
|
||||||
_other -> {nil, path}
|
_other ->
|
||||||
|
{nil, path}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec targeted_output?(String.t(), map(), String.t() | nil, [String.t()]) :: boolean()
|
@spec targeted_output?(String.t(), map(), String.t() | nil, [String.t()]) :: boolean()
|
||||||
def targeted_output?(relative_path, targeted_plan, main_language, additional_languages) do
|
def targeted_output?(relative_path, targeted_plan, main_language, additional_languages) do
|
||||||
{language, stripped_path} = extract_relative_output_language(relative_path, additional_languages)
|
{language, stripped_path} =
|
||||||
|
extract_relative_output_language(relative_path, additional_languages)
|
||||||
|
|
||||||
plan =
|
plan =
|
||||||
case language do
|
case language do
|
||||||
@@ -384,7 +427,11 @@ defmodule BDS.Generation.Validation do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp targeted_output_for_plan?(_relative_path, %{requires_fallback_section_render: true}, _main?), do: true
|
defp targeted_output_for_plan?(
|
||||||
|
_relative_path,
|
||||||
|
%{requires_fallback_section_render: true},
|
||||||
|
_main?
|
||||||
|
), do: true
|
||||||
|
|
||||||
defp targeted_output_for_plan?(relative_path, plan, _main?) do
|
defp targeted_output_for_plan?(relative_path, plan, _main?) do
|
||||||
cond do
|
cond do
|
||||||
@@ -400,8 +447,18 @@ defmodule BDS.Generation.Validation do
|
|||||||
MapSet.member?(plan.requested_tag_slugs, slug)
|
MapSet.member?(plan.requested_tag_slugs, slug)
|
||||||
|
|
||||||
Regex.match?(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path) ->
|
Regex.match?(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path) ->
|
||||||
[_, year, month, day, slug] = Regex.run(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path)
|
[_, year, month, day, slug] =
|
||||||
MapSet.member?(plan.requested_post_routes, route_key(String.to_integer(year), String.to_integer(month), String.to_integer(day), slug))
|
Regex.run(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path)
|
||||||
|
|
||||||
|
MapSet.member?(
|
||||||
|
plan.requested_post_routes,
|
||||||
|
route_key(
|
||||||
|
String.to_integer(year),
|
||||||
|
String.to_integer(month),
|
||||||
|
String.to_integer(day),
|
||||||
|
slug
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Regex.match?(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path) ->
|
Regex.match?(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path) ->
|
||||||
[_, year, month] = Regex.run(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path)
|
[_, year, month] = Regex.run(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path)
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ defmodule BDS.Git do
|
|||||||
has_lfs: has_lfs_configured?(project_dir)
|
has_lfs: has_lfs_configured?(project_dir)
|
||||||
}}
|
}}
|
||||||
else
|
else
|
||||||
{:error, :not_found} = error -> error
|
{:error, :not_found} = error ->
|
||||||
|
error
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
@@ -74,7 +76,8 @@ defmodule BDS.Git do
|
|||||||
|
|
||||||
def status(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do
|
def status(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do
|
||||||
with {:ok, project_dir} <- project_dir(project_id),
|
with {:ok, project_dir} <- project_dir(project_id),
|
||||||
{:ok, output} <- run_git(project_dir, ["status", "--porcelain=v1", "--untracked-files=all"], opts) do
|
{:ok, output} <-
|
||||||
|
run_git(project_dir, ["status", "--porcelain=v1", "--untracked-files=all"], opts) do
|
||||||
{:ok, %{files: parse_status(output)}}
|
{:ok, %{files: parse_status(output)}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -112,7 +115,8 @@ defmodule BDS.Git do
|
|||||||
when is_binary(project_id) and is_binary(branch) and is_list(opts) do
|
when is_binary(project_id) and is_binary(branch) and is_list(opts) do
|
||||||
with {:ok, project_dir} <- project_dir(project_id),
|
with {:ok, project_dir} <- project_dir(project_id),
|
||||||
{:ok, local_log} <- run_git(project_dir, ["log", "--format=%H%x09%s", branch], opts),
|
{:ok, local_log} <- run_git(project_dir, ["log", "--format=%H%x09%s", branch], opts),
|
||||||
{:ok, remote_log} <- run_git(project_dir, ["log", "--format=%H", "origin/#{branch}"], opts) do
|
{:ok, remote_log} <-
|
||||||
|
run_git(project_dir, ["log", "--format=%H", "origin/#{branch}"], opts) do
|
||||||
local_commits = parse_local_history(local_log)
|
local_commits = parse_local_history(local_log)
|
||||||
remote_hashes = MapSet.new(parse_remote_history(remote_log))
|
remote_hashes = MapSet.new(parse_remote_history(remote_log))
|
||||||
local_hashes = MapSet.new(Enum.map(local_commits, & &1.hash))
|
local_hashes = MapSet.new(Enum.map(local_commits, & &1.hash))
|
||||||
@@ -121,7 +125,9 @@ defmodule BDS.Git do
|
|||||||
remote_hashes
|
remote_hashes
|
||||||
|> MapSet.difference(local_hashes)
|
|> MapSet.difference(local_hashes)
|
||||||
|> MapSet.to_list()
|
|> MapSet.to_list()
|
||||||
|> Enum.map(fn hash -> %{hash: hash, subject: nil, sync_status: %{kind: :remote_only}} end)
|
|> Enum.map(fn hash ->
|
||||||
|
%{hash: hash, subject: nil, sync_status: %{kind: :remote_only}}
|
||||||
|
end)
|
||||||
|
|
||||||
commits =
|
commits =
|
||||||
Enum.map(local_commits, fn commit ->
|
Enum.map(local_commits, fn commit ->
|
||||||
@@ -136,7 +142,8 @@ defmodule BDS.Git do
|
|||||||
def file_history(project_id, file_path, opts \\ [])
|
def file_history(project_id, file_path, opts \\ [])
|
||||||
when is_binary(project_id) and is_binary(file_path) and is_list(opts) do
|
when is_binary(project_id) and is_binary(file_path) and is_list(opts) do
|
||||||
with {:ok, project_dir} <- project_dir(project_id),
|
with {:ok, project_dir} <- project_dir(project_id),
|
||||||
{:ok, output} <- run_git(project_dir, ["log", "--follow", "--format=%H%x09%s", "--", file_path], opts) do
|
{:ok, output} <-
|
||||||
|
run_git(project_dir, ["log", "--follow", "--format=%H%x09%s", "--", file_path], opts) do
|
||||||
{:ok, %{commits: parse_local_history(output) |> Enum.take(50)}}
|
{:ok, %{commits: parse_local_history(output) |> Enum.take(50)}}
|
||||||
else
|
else
|
||||||
{:error, {:git_failed, _message}} -> {:ok, %{commits: []}}
|
{:error, {:git_failed, _message}} -> {:ok, %{commits: []}}
|
||||||
@@ -147,8 +154,11 @@ defmodule BDS.Git do
|
|||||||
def fetch(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do
|
def fetch(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do
|
||||||
with {:ok, project_dir} <- project_dir(project_id) do
|
with {:ok, project_dir} <- project_dir(project_id) do
|
||||||
case run_git(project_dir, ["fetch", "--all", "--prune"], opts) do
|
case run_git(project_dir, ["fetch", "--all", "--prune"], opts) do
|
||||||
{:ok, output} -> {:ok, %{updated: true, output: output}}
|
{:ok, output} ->
|
||||||
{:error, {:git_failed, message}} -> structured_git_error(project_dir, :fetch, message, opts)
|
{:ok, %{updated: true, output: output}}
|
||||||
|
|
||||||
|
{:error, {:git_failed, message}} ->
|
||||||
|
structured_git_error(project_dir, :fetch, message, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -177,9 +187,11 @@ defmodule BDS.Git do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reconcile(project_id, old_commit, new_commit, opts \\ [])
|
def reconcile(project_id, old_commit, new_commit, opts \\ [])
|
||||||
when is_binary(project_id) and is_binary(old_commit) and is_binary(new_commit) and is_list(opts) do
|
when is_binary(project_id) and is_binary(old_commit) and is_binary(new_commit) and
|
||||||
|
is_list(opts) do
|
||||||
with {:ok, project_dir} <- project_dir(project_id),
|
with {:ok, project_dir} <- project_dir(project_id),
|
||||||
{:ok, output} <- run_git(project_dir, ["diff", "--name-status", old_commit, new_commit], opts) do
|
{:ok, output} <-
|
||||||
|
run_git(project_dir, ["diff", "--name-status", old_commit, new_commit], opts) do
|
||||||
{:ok, %{changed: parse_changed_files(output)}}
|
{:ok, %{changed: parse_changed_files(output)}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -197,7 +209,14 @@ defmodule BDS.Git do
|
|||||||
{:ok, local_branch} <- current_branch(project_dir, opts) do
|
{:ok, local_branch} <- current_branch(project_dir, opts) do
|
||||||
case upstream_branch(project_dir, opts) do
|
case upstream_branch(project_dir, opts) do
|
||||||
{:ok, nil} ->
|
{:ok, nil} ->
|
||||||
{:ok, %{local_branch: local_branch, upstream_branch: nil, has_upstream: false, ahead: 0, behind: 0}}
|
{:ok,
|
||||||
|
%{
|
||||||
|
local_branch: local_branch,
|
||||||
|
upstream_branch: nil,
|
||||||
|
has_upstream: false,
|
||||||
|
ahead: 0,
|
||||||
|
behind: 0
|
||||||
|
}}
|
||||||
|
|
||||||
{:ok, upstream_branch} ->
|
{:ok, upstream_branch} ->
|
||||||
{:ok,
|
{:ok,
|
||||||
@@ -316,7 +335,11 @@ defmodule BDS.Git do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp upstream_branch(project_dir, opts) do
|
defp upstream_branch(project_dir, opts) do
|
||||||
case run_git(project_dir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], opts) do
|
case run_git(
|
||||||
|
project_dir,
|
||||||
|
["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
||||||
|
opts
|
||||||
|
) do
|
||||||
{:ok, output} -> {:ok, blank_to_nil(output)}
|
{:ok, output} -> {:ok, blank_to_nil(output)}
|
||||||
{:error, {:git_failed, _message}} -> {:ok, nil}
|
{:error, {:git_failed, _message}} -> {:ok, nil}
|
||||||
end
|
end
|
||||||
@@ -364,21 +387,37 @@ defmodule BDS.Git do
|
|||||||
defp parse_changed_files(output) do
|
defp parse_changed_files(output) do
|
||||||
base = fn -> %{added: [], modified: [], deleted: [], renamed: []} end
|
base = fn -> %{added: [], modified: [], deleted: [], renamed: []} end
|
||||||
|
|
||||||
Enum.reduce(String.split(output, "\n", trim: true), %{posts: base.(), scripts: base.(), templates: base.()}, fn line, acc ->
|
Enum.reduce(
|
||||||
case String.split(line, "\t", trim: true) do
|
String.split(output, "\n", trim: true),
|
||||||
["A", path] -> update_changed(acc, path, :added, path)
|
%{posts: base.(), scripts: base.(), templates: base.()},
|
||||||
["M", path] -> update_changed(acc, path, :modified, path)
|
fn line, acc ->
|
||||||
["D", path] -> update_changed(acc, path, :deleted, path)
|
case String.split(line, "\t", trim: true) do
|
||||||
["R" <> _score, old_path, new_path] -> update_changed(acc, new_path, :renamed, %{old: old_path, new: new_path})
|
["A", path] ->
|
||||||
_other -> acc
|
update_changed(acc, path, :added, path)
|
||||||
|
|
||||||
|
["M", path] ->
|
||||||
|
update_changed(acc, path, :modified, path)
|
||||||
|
|
||||||
|
["D", path] ->
|
||||||
|
update_changed(acc, path, :deleted, path)
|
||||||
|
|
||||||
|
["R" <> _score, old_path, new_path] ->
|
||||||
|
update_changed(acc, new_path, :renamed, %{old: old_path, new: new_path})
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
acc
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_changed(acc, path, key, value) do
|
defp update_changed(acc, path, key, value) do
|
||||||
case category_for_path(path) do
|
case category_for_path(path) do
|
||||||
nil -> acc
|
nil ->
|
||||||
category -> Map.update!(acc, category, &Map.update!(&1, key, fn items -> items ++ [value] end))
|
acc
|
||||||
|
|
||||||
|
category ->
|
||||||
|
Map.update!(acc, category, &Map.update!(&1, key, fn items -> items ++ [value] end))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -427,6 +466,7 @@ defmodule BDS.Git do
|
|||||||
|
|
||||||
defp auth_guidance(provider, platform) do
|
defp auth_guidance(provider, platform) do
|
||||||
provider_label = provider || :git
|
provider_label = provider || :git
|
||||||
|
|
||||||
"Authentication failed for #{provider_label} on #{platform}. Configure SSH keys or a credential helper that works non-interactively."
|
"Authentication failed for #{provider_label} on #{platform}. Configure SSH keys or a credential helper that works non-interactively."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,23 @@ defmodule BDS.ImportAnalysis do
|
|||||||
notify_progress(on_progress, "Loading existing posts...")
|
notify_progress(on_progress, "Loading existing posts...")
|
||||||
existing_posts = Repo.all(from post in Post, where: post.project_id == ^project_id)
|
existing_posts = Repo.all(from post in Post, where: post.project_id == ^project_id)
|
||||||
|
|
||||||
notify_progress(on_progress, "Loading existing media...", "#{length(existing_posts)} posts in project")
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"Loading existing media...",
|
||||||
|
"#{length(existing_posts)} posts in project"
|
||||||
|
)
|
||||||
|
|
||||||
existing_media = Repo.all(from media in Media, where: media.project_id == ^project_id)
|
existing_media = Repo.all(from media in Media, where: media.project_id == ^project_id)
|
||||||
|
|
||||||
notify_progress(on_progress, "Loading existing tags...", "#{length(existing_media)} media in project")
|
notify_progress(
|
||||||
existing_tag_names = Repo.all(from tag in Tag, where: tag.project_id == ^project_id, select: tag.name)
|
on_progress,
|
||||||
|
"Loading existing tags...",
|
||||||
|
"#{length(existing_media)} media in project"
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_tag_names =
|
||||||
|
Repo.all(from tag in Tag, where: tag.project_id == ^project_id, select: tag.name)
|
||||||
|
|
||||||
existing_tag_set = existing_tag_names |> Enum.map(&String.downcase/1) |> MapSet.new()
|
existing_tag_set = existing_tag_names |> Enum.map(&String.downcase/1) |> MapSet.new()
|
||||||
|
|
||||||
posts_by_slug = Map.new(existing_posts, &{&1.slug, &1})
|
posts_by_slug = Map.new(existing_posts, &{&1.slug, &1})
|
||||||
@@ -53,15 +65,35 @@ defmodule BDS.ImportAnalysis do
|
|||||||
|> Enum.reject(&is_nil(&1.checksum))
|
|> Enum.reject(&is_nil(&1.checksum))
|
||||||
|> Map.new(&{&1.checksum, &1})
|
|> Map.new(&{&1.checksum, &1})
|
||||||
|
|
||||||
notify_progress(on_progress, "Analyzing posts...", "#{length(wxr_data.posts)} posts to analyze")
|
notify_progress(
|
||||||
analyzed_posts = Enum.map(wxr_data.posts, &analyze_post_item(&1, posts_by_slug, posts_by_checksum, "post"))
|
on_progress,
|
||||||
|
"Analyzing posts...",
|
||||||
|
"#{length(wxr_data.posts)} posts to analyze"
|
||||||
|
)
|
||||||
|
|
||||||
notify_progress(on_progress, "Analyzing pages...", "#{length(wxr_data.pages)} pages to analyze")
|
analyzed_posts =
|
||||||
analyzed_pages = Enum.map(wxr_data.pages, &analyze_post_item(&1, posts_by_slug, posts_by_checksum, "page"))
|
Enum.map(wxr_data.posts, &analyze_post_item(&1, posts_by_slug, posts_by_checksum, "post"))
|
||||||
|
|
||||||
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"Analyzing pages...",
|
||||||
|
"#{length(wxr_data.pages)} pages to analyze"
|
||||||
|
)
|
||||||
|
|
||||||
|
analyzed_pages =
|
||||||
|
Enum.map(wxr_data.pages, &analyze_post_item(&1, posts_by_slug, posts_by_checksum, "page"))
|
||||||
|
|
||||||
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"Analyzing media files...",
|
||||||
|
"#{length(wxr_data.media)} media files to analyze"
|
||||||
|
)
|
||||||
|
|
||||||
notify_progress(on_progress, "Analyzing media files...", "#{length(wxr_data.media)} media files to analyze")
|
|
||||||
analyzed_media =
|
analyzed_media =
|
||||||
Enum.map(wxr_data.media, &analyze_media_item(&1, uploads_folder_path, media_by_name, media_by_checksum))
|
Enum.map(
|
||||||
|
wxr_data.media,
|
||||||
|
&analyze_media_item(&1, uploads_folder_path, media_by_name, media_by_checksum)
|
||||||
|
)
|
||||||
|
|
||||||
notify_progress(on_progress, "Processing categories and tags...")
|
notify_progress(on_progress, "Processing categories and tags...")
|
||||||
category_items = Enum.map(wxr_data.categories, &analyze_taxonomy_item(&1, existing_tag_set))
|
category_items = Enum.map(wxr_data.categories, &analyze_taxonomy_item(&1, existing_tag_set))
|
||||||
@@ -113,10 +145,18 @@ defmodule BDS.ImportAnalysis do
|
|||||||
|
|
||||||
{status, existing} =
|
{status, existing} =
|
||||||
cond do
|
cond do
|
||||||
existing_by_slug && existing_by_slug.checksum == content_checksum && not is_nil(existing_by_slug.checksum) -> {"update", existing_by_slug}
|
existing_by_slug && existing_by_slug.checksum == content_checksum &&
|
||||||
existing_by_slug -> {"conflict", existing_by_slug}
|
not is_nil(existing_by_slug.checksum) ->
|
||||||
existing_by_checksum -> {"content-duplicate", existing_by_checksum}
|
{"update", existing_by_slug}
|
||||||
true -> {"new", nil}
|
|
||||||
|
existing_by_slug ->
|
||||||
|
{"conflict", existing_by_slug}
|
||||||
|
|
||||||
|
existing_by_checksum ->
|
||||||
|
{"content-duplicate", existing_by_checksum}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{"new", nil}
|
||||||
end
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
@@ -163,10 +203,18 @@ defmodule BDS.ImportAnalysis do
|
|||||||
existing_by_checksum = Map.get(media_by_checksum, file_checksum)
|
existing_by_checksum = Map.get(media_by_checksum, file_checksum)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
existing_by_name && existing_by_name.checksum == file_checksum && not is_nil(existing_by_name.checksum) -> {"update", file_checksum, existing_by_name}
|
existing_by_name && existing_by_name.checksum == file_checksum &&
|
||||||
existing_by_name -> {"conflict", file_checksum, existing_by_name}
|
not is_nil(existing_by_name.checksum) ->
|
||||||
existing_by_checksum -> {"content-duplicate", file_checksum, existing_by_checksum}
|
{"update", file_checksum, existing_by_name}
|
||||||
true -> {"new", file_checksum, nil}
|
|
||||||
|
existing_by_name ->
|
||||||
|
{"conflict", file_checksum, existing_by_name}
|
||||||
|
|
||||||
|
existing_by_checksum ->
|
||||||
|
{"content-duplicate", file_checksum, existing_by_checksum}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{"new", file_checksum, nil}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -265,7 +313,9 @@ defmodule BDS.ImportAnalysis do
|
|||||||
defp date_distribution(posts, pages, media) do
|
defp date_distribution(posts, pages, media) do
|
||||||
combined_posts = posts ++ pages
|
combined_posts = posts ++ pages
|
||||||
|
|
||||||
post_counts = Enum.reduce(combined_posts, %{}, &increment_year(&1.created_at || &1.published_at, &2))
|
post_counts =
|
||||||
|
Enum.reduce(combined_posts, %{}, &increment_year(&1.created_at || &1.published_at, &2))
|
||||||
|
|
||||||
media_counts = Enum.reduce(media, %{}, &increment_year(&1.created_at, &2))
|
media_counts = Enum.reduce(media, %{}, &increment_year(&1.created_at, &2))
|
||||||
|
|
||||||
post_counts
|
post_counts
|
||||||
@@ -325,7 +375,10 @@ defmodule BDS.ImportAnalysis do
|
|||||||
| total_count: existing.total_count + 1,
|
| total_count: existing.total_count + 1,
|
||||||
usages: Map.put(existing.usages, params_key, usage),
|
usages: Map.put(existing.usages, params_key, usage),
|
||||||
post_slugs:
|
post_slugs:
|
||||||
if(is_binary(slug), do: MapSet.put(existing.post_slugs, slug), else: existing.post_slugs)
|
if(is_binary(slug),
|
||||||
|
do: MapSet.put(existing.post_slugs, slug),
|
||||||
|
else: existing.post_slugs
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Map.put(inner_acc, name, updated)
|
Map.put(inner_acc, name, updated)
|
||||||
@@ -393,9 +446,17 @@ defmodule BDS.ImportAnalysis do
|
|||||||
|
|
||||||
defp year_from(value) when is_integer(value) do
|
defp year_from(value) when is_integer(value) do
|
||||||
cond do
|
cond do
|
||||||
value > 100_000_000_000 -> value |> DateTime.from_unix!(:millisecond) |> DateTime.shift_zone!("Etc/UTC") |> Map.get(:year)
|
value > 100_000_000_000 ->
|
||||||
value > 1_000_000_000 -> value |> DateTime.from_unix!(:second) |> Map.get(:year)
|
value
|
||||||
true -> value
|
|> DateTime.from_unix!(:millisecond)
|
||||||
|
|> DateTime.shift_zone!("Etc/UTC")
|
||||||
|
|> Map.get(:year)
|
||||||
|
|
||||||
|
value > 1_000_000_000 ->
|
||||||
|
value |> DateTime.from_unix!(:second) |> Map.get(:year)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
value
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
_error -> nil
|
_error -> nil
|
||||||
@@ -405,10 +466,14 @@ defmodule BDS.ImportAnalysis do
|
|||||||
normalized = String.replace(value, " ", "T")
|
normalized = String.replace(value, " ", "T")
|
||||||
|
|
||||||
case NaiveDateTime.from_iso8601(normalized) do
|
case NaiveDateTime.from_iso8601(normalized) do
|
||||||
{:ok, naive} -> naive.year
|
{:ok, naive} ->
|
||||||
|
naive.year
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
case DateTime.from_iso8601(value) do
|
case DateTime.from_iso8601(value) do
|
||||||
{:ok, datetime, _offset} -> datetime.year
|
{:ok, datetime, _offset} ->
|
||||||
|
datetime.year
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
case Regex.run(~r/(\d{4})/, value) do
|
case Regex.run(~r/(\d{4})/, value) do
|
||||||
[_, year] -> String.to_integer(year)
|
[_, year] -> String.to_integer(year)
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ defmodule BDS.ImportDefinitions do
|
|||||||
|> maybe_put(:name, attr(attrs, :name))
|
|> maybe_put(:name, attr(attrs, :name))
|
||||||
|> maybe_put(:wxr_file_path, attr(attrs, :wxr_file_path))
|
|> maybe_put(:wxr_file_path, attr(attrs, :wxr_file_path))
|
||||||
|> maybe_put(:uploads_folder_path, attr(attrs, :uploads_folder_path))
|
|> maybe_put(:uploads_folder_path, attr(attrs, :uploads_folder_path))
|
||||||
|> maybe_put(:last_analysis_result, normalize_analysis_result(attr(attrs, :last_analysis_result)))
|
|> maybe_put(
|
||||||
|
:last_analysis_result,
|
||||||
|
normalize_analysis_result(attr(attrs, :last_analysis_result))
|
||||||
|
)
|
||||||
|> Map.put(:updated_at, Persistence.now_ms())
|
|> Map.put(:updated_at, Persistence.now_ms())
|
||||||
|
|
||||||
definition
|
definition
|
||||||
@@ -50,7 +53,9 @@ defmodule BDS.ImportDefinitions do
|
|||||||
|
|
||||||
def delete_definition(definition_id) when is_binary(definition_id) do
|
def delete_definition(definition_id) when is_binary(definition_id) do
|
||||||
case Repo.get(ImportDefinition, definition_id) do
|
case Repo.get(ImportDefinition, definition_id) do
|
||||||
nil -> {:error, :not_found}
|
nil ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
%ImportDefinition{} = definition ->
|
%ImportDefinition{} = definition ->
|
||||||
Repo.delete(definition)
|
Repo.delete(definition)
|
||||||
|> case do
|
|> case do
|
||||||
@@ -60,7 +65,8 @@ defmodule BDS.ImportDefinitions do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def decode_analysis_result(%ImportDefinition{last_analysis_result: result}), do: decode_analysis_result(result)
|
def decode_analysis_result(%ImportDefinition{last_analysis_result: result}),
|
||||||
|
do: decode_analysis_result(result)
|
||||||
|
|
||||||
def decode_analysis_result(result) when is_binary(result) do
|
def decode_analysis_result(result) when is_binary(result) do
|
||||||
case Jason.decode(result) do
|
case Jason.decode(result) do
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ defmodule BDS.ImportDefinitions.ImportDefinition do
|
|||||||
|
|
||||||
def changeset(definition, attrs) do
|
def changeset(definition, attrs) do
|
||||||
definition
|
definition
|
||||||
|> cast(attrs, [:id, :project_id, :name, :wxr_file_path, :uploads_folder_path, :last_analysis_result, :created_at, :updated_at])
|
|> cast(attrs, [
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:name,
|
||||||
|
:wxr_file_path,
|
||||||
|
:uploads_folder_path,
|
||||||
|
:last_analysis_result,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
])
|
||||||
|> validate_required([:id, :project_id, :name, :created_at, :updated_at])
|
|> validate_required([:id, :project_id, :name, :created_at, :updated_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ defmodule BDS.ImportExecution do
|
|||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
alias BDS.Tags
|
alias BDS.Tags
|
||||||
|
|
||||||
def execute_import(project_id, report, opts \\ []) when is_binary(project_id) and is_map(report) do
|
def execute_import(project_id, report, opts \\ [])
|
||||||
|
when is_binary(project_id) and is_map(report) do
|
||||||
normalized_report = normalize_report(report)
|
normalized_report = normalize_report(report)
|
||||||
default_author = Keyword.get(opts, :default_author) || project_default_author(project_id)
|
default_author = Keyword.get(opts, :default_author) || project_default_author(project_id)
|
||||||
uploads_folder_path = Keyword.get(opts, :uploads_folder_path)
|
uploads_folder_path = Keyword.get(opts, :uploads_folder_path)
|
||||||
@@ -42,16 +43,52 @@ defmodule BDS.ImportExecution do
|
|||||||
started_at = System.monotonic_time(:millisecond)
|
started_at = System.monotonic_time(:millisecond)
|
||||||
|
|
||||||
notify_progress(on_progress, "tags", 0, taxonomy_total, "creating_tags", started_at)
|
notify_progress(on_progress, "tags", 0, taxonomy_total, "creating_tags", started_at)
|
||||||
result = execute_taxonomies(category_items, tag_items, project_id, result, on_progress, started_at)
|
|
||||||
|
result =
|
||||||
|
execute_taxonomies(category_items, tag_items, project_id, result, on_progress, started_at)
|
||||||
|
|
||||||
notify_progress(on_progress, "posts", 0, length(post_items), "importing_posts", started_at)
|
notify_progress(on_progress, "posts", 0, length(post_items), "importing_posts", started_at)
|
||||||
result = execute_posts(post_items, project_id, default_author, tag_mapping, category_mapping, result, on_progress, :posts, started_at)
|
|
||||||
|
result =
|
||||||
|
execute_posts(
|
||||||
|
post_items,
|
||||||
|
project_id,
|
||||||
|
default_author,
|
||||||
|
tag_mapping,
|
||||||
|
category_mapping,
|
||||||
|
result,
|
||||||
|
on_progress,
|
||||||
|
:posts,
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
notify_progress(on_progress, "media", 0, length(media_items), "importing_media", started_at)
|
notify_progress(on_progress, "media", 0, length(media_items), "importing_media", started_at)
|
||||||
result = execute_media(media_items, project_id, default_author, result, on_progress, uploads_folder_path, started_at)
|
|
||||||
|
result =
|
||||||
|
execute_media(
|
||||||
|
media_items,
|
||||||
|
project_id,
|
||||||
|
default_author,
|
||||||
|
result,
|
||||||
|
on_progress,
|
||||||
|
uploads_folder_path,
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
notify_progress(on_progress, "pages", 0, length(page_items), "importing_pages", started_at)
|
notify_progress(on_progress, "pages", 0, length(page_items), "importing_pages", started_at)
|
||||||
result = execute_posts(page_items, project_id, default_author, tag_mapping, category_mapping, result, on_progress, :pages, started_at)
|
|
||||||
|
result =
|
||||||
|
execute_posts(
|
||||||
|
page_items,
|
||||||
|
project_id,
|
||||||
|
default_author,
|
||||||
|
tag_mapping,
|
||||||
|
category_mapping,
|
||||||
|
result,
|
||||||
|
on_progress,
|
||||||
|
:pages,
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
notify_progress(on_progress, "complete", 1, 1, "import_complete", started_at)
|
notify_progress(on_progress, "complete", 1, 1, "import_complete", started_at)
|
||||||
{:ok, result}
|
{:ok, result}
|
||||||
@@ -68,41 +105,99 @@ defmodule BDS.ImportExecution do
|
|||||||
|> Enum.reduce(result, fn {item, index}, acc ->
|
|> Enum.reduce(result, fn {item, index}, acc ->
|
||||||
cond do
|
cond do
|
||||||
Map.get(item, :exists_in_project) || not is_nil(Map.get(item, :mapped_to)) ->
|
Map.get(item, :exists_in_project) || not is_nil(Map.get(item, :mapped_to)) ->
|
||||||
notify_progress(on_progress, "tags", index, total, "skipped_tag:#{item.name}", started_at)
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"tags",
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
"skipped_tag:#{item.name}",
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
put_in(acc, [:tags, :skipped], acc.tags.skipped + 1)
|
put_in(acc, [:tags, :skipped], acc.tags.skipped + 1)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
case Tags.create_tag(%{project_id: project_id, name: item.name}) do
|
case Tags.create_tag(%{project_id: project_id, name: item.name}) do
|
||||||
{:ok, _tag} ->
|
{:ok, _tag} ->
|
||||||
notify_progress(on_progress, "tags", index, total, "created_tag:#{item.name}", started_at)
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"tags",
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
"created_tag:#{item.name}",
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
put_in(acc, [:tags, :created], acc.tags.created + 1)
|
put_in(acc, [:tags, :created], acc.tags.created + 1)
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
notify_progress(on_progress, "tags", index, total, "skipped_tag:#{item.name}", started_at)
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"tags",
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
"skipped_tag:#{item.name}",
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
put_in(acc, [:tags, :skipped], acc.tags.skipped + 1)
|
put_in(acc, [:tags, :skipped], acc.tags.skipped + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp execute_posts(items, project_id, default_author, tag_mapping, category_mapping, result, on_progress, bucket, started_at) do
|
defp execute_posts(
|
||||||
|
items,
|
||||||
|
project_id,
|
||||||
|
default_author,
|
||||||
|
tag_mapping,
|
||||||
|
category_mapping,
|
||||||
|
result,
|
||||||
|
on_progress,
|
||||||
|
bucket,
|
||||||
|
started_at
|
||||||
|
) do
|
||||||
total = length(items)
|
total = length(items)
|
||||||
phase = Atom.to_string(bucket)
|
phase = Atom.to_string(bucket)
|
||||||
|
|
||||||
Enum.with_index(items, 1)
|
Enum.with_index(items, 1)
|
||||||
|> Enum.reduce(result, fn {item, index}, acc ->
|
|> Enum.reduce(result, fn {item, index}, acc ->
|
||||||
notify_progress(on_progress, phase, index, total, "processing:#{item.title}", started_at)
|
notify_progress(on_progress, phase, index, total, "processing:#{item.title}", started_at)
|
||||||
execute_post_item(project_id, maybe_apply_page_category(item, bucket), acc, bucket, default_author, tag_mapping, category_mapping)
|
|
||||||
|
execute_post_item(
|
||||||
|
project_id,
|
||||||
|
maybe_apply_page_category(item, bucket),
|
||||||
|
acc,
|
||||||
|
bucket,
|
||||||
|
default_author,
|
||||||
|
tag_mapping,
|
||||||
|
category_mapping
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp execute_media(items, project_id, default_author, result, on_progress, uploads_folder_path, started_at) do
|
defp execute_media(
|
||||||
|
items,
|
||||||
|
project_id,
|
||||||
|
default_author,
|
||||||
|
result,
|
||||||
|
on_progress,
|
||||||
|
uploads_folder_path,
|
||||||
|
started_at
|
||||||
|
) do
|
||||||
total = length(items)
|
total = length(items)
|
||||||
|
|
||||||
items
|
items
|
||||||
|> Enum.with_index(1)
|
|> Enum.with_index(1)
|
||||||
|> Enum.reduce(result, fn {item, index}, acc ->
|
|> Enum.reduce(result, fn {item, index}, acc ->
|
||||||
notify_progress(on_progress, "media", index, total, "processing:#{item.filename}", started_at)
|
notify_progress(
|
||||||
|
on_progress,
|
||||||
|
"media",
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
"processing:#{item.filename}",
|
||||||
|
started_at
|
||||||
|
)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
item.status == "missing" ->
|
item.status == "missing" ->
|
||||||
@@ -116,7 +211,9 @@ defmodule BDS.ImportExecution do
|
|||||||
|
|
||||||
true ->
|
true ->
|
||||||
case import_media_item(project_id, item, default_author, uploads_folder_path, acc) do
|
case import_media_item(project_id, item, default_author, uploads_folder_path, acc) do
|
||||||
{:ok, _media} -> put_in(acc, [:media, :imported], acc.media.imported + 1)
|
{:ok, _media} ->
|
||||||
|
put_in(acc, [:media, :imported], acc.media.imported + 1)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
acc
|
acc
|
||||||
|> put_in([:media, :errors], acc.media.errors + 1)
|
|> put_in([:media, :errors], acc.media.errors + 1)
|
||||||
@@ -127,7 +224,15 @@ defmodule BDS.ImportExecution do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp execute_post_item(project_id, item, result, bucket, default_author, tag_mapping, category_mapping) do
|
defp execute_post_item(
|
||||||
|
project_id,
|
||||||
|
item,
|
||||||
|
result,
|
||||||
|
bucket,
|
||||||
|
default_author,
|
||||||
|
tag_mapping,
|
||||||
|
category_mapping
|
||||||
|
) do
|
||||||
cond do
|
cond do
|
||||||
item.status in ["update", "content-duplicate", "duplicate"] ->
|
item.status in ["update", "content-duplicate", "duplicate"] ->
|
||||||
put_in(result, [bucket, :skipped], get_in(result, [bucket, :skipped]) + 1)
|
put_in(result, [bucket, :skipped], get_in(result, [bucket, :skipped]) + 1)
|
||||||
@@ -177,7 +282,8 @@ defmodule BDS.ImportExecution do
|
|||||||
|
|
||||||
defp overwrite_post_item(item, default_author, tag_mapping, category_mapping) do
|
defp overwrite_post_item(item, default_author, tag_mapping, category_mapping) do
|
||||||
case Repo.get(Post, item.existing_id) do
|
case Repo.get(Post, item.existing_id) do
|
||||||
nil -> {:error, :not_found}
|
nil ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
Posts.update_post(post.id, %{
|
Posts.update_post(post.id, %{
|
||||||
@@ -194,7 +300,13 @@ defmodule BDS.ImportExecution do
|
|||||||
|
|
||||||
defp import_media_item(project_id, item, default_author, uploads_folder_path, result) do
|
defp import_media_item(project_id, item, default_author, uploads_folder_path, result) do
|
||||||
source_path = item.source_file || uploads_source_path(item.relative_path, uploads_folder_path)
|
source_path = item.source_file || uploads_source_path(item.relative_path, uploads_folder_path)
|
||||||
checksum = if(source_path != nil and File.exists?(source_path), do: md5(File.read!(source_path)), else: nil)
|
|
||||||
|
checksum =
|
||||||
|
if(source_path != nil and File.exists?(source_path),
|
||||||
|
do: md5(File.read!(source_path)),
|
||||||
|
else: nil
|
||||||
|
)
|
||||||
|
|
||||||
linked_post_ids = parent_post_ids(item, result)
|
linked_post_ids = parent_post_ids(item, result)
|
||||||
|
|
||||||
if source_path && File.exists?(source_path) do
|
if source_path && File.exists?(source_path) do
|
||||||
@@ -221,7 +333,10 @@ defmodule BDS.ImportExecution do
|
|||||||
checksum: checksum
|
checksum: checksum
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = if linked_post_ids == [], do: attrs, else: Map.put(attrs, :linked_post_ids, linked_post_ids)
|
attrs =
|
||||||
|
if linked_post_ids == [],
|
||||||
|
do: attrs,
|
||||||
|
else: Map.put(attrs, :linked_post_ids, linked_post_ids)
|
||||||
|
|
||||||
case Media.import_media(attrs) do
|
case Media.import_media(attrs) do
|
||||||
{:ok, %{id: media_id} = media} ->
|
{:ok, %{id: media_id} = media} ->
|
||||||
@@ -255,8 +370,12 @@ defmodule BDS.ImportExecution do
|
|||||||
|
|
||||||
defp parent_post_ids(item, result) do
|
defp parent_post_ids(item, result) do
|
||||||
case Map.get(item, :parent_wp_id) do
|
case Map.get(item, :parent_wp_id) do
|
||||||
nil -> []
|
nil ->
|
||||||
0 -> []
|
[]
|
||||||
|
|
||||||
|
0 ->
|
||||||
|
[]
|
||||||
|
|
||||||
wp_id ->
|
wp_id ->
|
||||||
case Map.get(result.wp_id_to_post_id, wp_id) do
|
case Map.get(result.wp_id_to_post_id, wp_id) do
|
||||||
nil -> []
|
nil -> []
|
||||||
@@ -265,7 +384,8 @@ defmodule BDS.ImportExecution do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp track_wp_id(result, %{wp_id: wp_id}, %{id: post_id}) when is_integer(wp_id) and not is_nil(post_id) do
|
defp track_wp_id(result, %{wp_id: wp_id}, %{id: post_id})
|
||||||
|
when is_integer(wp_id) and not is_nil(post_id) do
|
||||||
update_in(result, [:wp_id_to_post_id], &Map.put(&1, wp_id, post_id))
|
update_in(result, [:wp_id_to_post_id], &Map.put(&1, wp_id, post_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -333,7 +453,9 @@ defmodule BDS.ImportExecution do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_apply_page_category(item, :pages) do
|
defp maybe_apply_page_category(item, :pages) do
|
||||||
categories = (Map.get(item, :categories) || []) |> Enum.uniq() |> Enum.concat(["page"]) |> Enum.uniq()
|
categories =
|
||||||
|
(Map.get(item, :categories) || []) |> Enum.uniq() |> Enum.concat(["page"]) |> Enum.uniq()
|
||||||
|
|
||||||
%{item | categories: categories}
|
%{item | categories: categories}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -349,7 +471,11 @@ defmodule BDS.ImportExecution do
|
|||||||
true -> key
|
true -> key
|
||||||
end
|
end
|
||||||
|
|
||||||
Map.put(acc, key, %{resolved: resolved, needs_creation: not item.exists_in_project and not present_string?(Map.get(item, :mapped_to))})
|
Map.put(acc, key, %{
|
||||||
|
resolved: resolved,
|
||||||
|
needs_creation:
|
||||||
|
not item.exists_in_project and not present_string?(Map.get(item, :mapped_to))
|
||||||
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -443,13 +569,15 @@ defmodule BDS.ImportExecution do
|
|||||||
defp uploads_source_path(relative_path, uploads_folder_path)
|
defp uploads_source_path(relative_path, uploads_folder_path)
|
||||||
|
|
||||||
defp uploads_source_path(relative_path, uploads_folder_path)
|
defp uploads_source_path(relative_path, uploads_folder_path)
|
||||||
when is_binary(relative_path) and is_binary(uploads_folder_path) and uploads_folder_path != "" do
|
when is_binary(relative_path) and is_binary(uploads_folder_path) and
|
||||||
|
uploads_folder_path != "" do
|
||||||
Path.join(uploads_folder_path, relative_path)
|
Path.join(uploads_folder_path, relative_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp uploads_source_path(_relative_path, _uploads_folder_path), do: nil
|
defp uploads_source_path(_relative_path, _uploads_folder_path), do: nil
|
||||||
|
|
||||||
defp notify_progress(callback, phase, current, total, detail, started_at) when is_function(callback, 4) do
|
defp notify_progress(callback, phase, current, total, detail, started_at)
|
||||||
|
when is_function(callback, 4) do
|
||||||
eta = compute_eta(current, total, started_at)
|
eta = compute_eta(current, total, started_at)
|
||||||
|
|
||||||
try do
|
try do
|
||||||
@@ -466,7 +594,9 @@ defmodule BDS.ImportExecution do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compute_eta(current, total, started_at) when is_integer(current) and is_integer(total) and current > 0 and total > 0 and current <= total do
|
defp compute_eta(current, total, started_at)
|
||||||
|
when is_integer(current) and is_integer(total) and current > 0 and total > 0 and
|
||||||
|
current <= total do
|
||||||
elapsed = System.monotonic_time(:millisecond) - started_at
|
elapsed = System.monotonic_time(:millisecond) - started_at
|
||||||
if current >= total, do: 0, else: trunc(elapsed / current * (total - current))
|
if current >= total, do: 0, else: trunc(elapsed / current * (total - current))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -114,9 +114,11 @@ defmodule BDS.Maintenance do
|
|||||||
phases = [
|
phases = [
|
||||||
{"Comparing project metadata", fn -> project_metadata_diff_reports(project_id) end},
|
{"Comparing project metadata", fn -> project_metadata_diff_reports(project_id) end},
|
||||||
{"Comparing post metadata", fn -> post_diff_reports(project_id, project) end},
|
{"Comparing post metadata", fn -> post_diff_reports(project_id, project) end},
|
||||||
{"Comparing post translations", fn -> post_translation_diff_reports(project_id, project) end},
|
{"Comparing post translations",
|
||||||
|
fn -> post_translation_diff_reports(project_id, project) end},
|
||||||
{"Comparing media metadata", fn -> media_diff_reports(project_id, project) end},
|
{"Comparing media metadata", fn -> media_diff_reports(project_id, project) end},
|
||||||
{"Comparing media translations", fn -> media_translation_diff_reports(project_id, project) end},
|
{"Comparing media translations",
|
||||||
|
fn -> media_translation_diff_reports(project_id, project) end},
|
||||||
{"Comparing script metadata", fn -> script_diff_reports(project_id, project) end},
|
{"Comparing script metadata", fn -> script_diff_reports(project_id, project) end},
|
||||||
{"Comparing template metadata", fn -> template_diff_reports(project_id, project) end},
|
{"Comparing template metadata", fn -> template_diff_reports(project_id, project) end},
|
||||||
{"Comparing embeddings", fn -> Embeddings.diff_reports(project_id) end}
|
{"Comparing embeddings", fn -> Embeddings.diff_reports(project_id) end}
|
||||||
@@ -132,7 +134,9 @@ defmodule BDS.Maintenance do
|
|||||||
fun.()
|
fun.()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
:ok = report_metadata_diff_phase(on_progress, total_phases, total_phases, "Scanning orphan files")
|
:ok =
|
||||||
|
report_metadata_diff_phase(on_progress, total_phases, total_phases, "Scanning orphan files")
|
||||||
|
|
||||||
orphan_reports = orphan_reports(project_id, project)
|
orphan_reports = orphan_reports(project_id, project)
|
||||||
:ok = report_metadata_diff_complete(on_progress)
|
:ok = report_metadata_diff_complete(on_progress)
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ defmodule BDS.Maintenance.DiffComputation do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def normalize_nested_diff_value(value) when is_map(value), do: normalize_map_diff_values(value)
|
def normalize_nested_diff_value(value) when is_map(value), do: normalize_map_diff_values(value)
|
||||||
def normalize_nested_diff_value(value) when is_list(value), do: Enum.map(value, &normalize_nested_diff_value/1)
|
|
||||||
|
def normalize_nested_diff_value(value) when is_list(value),
|
||||||
|
do: Enum.map(value, &normalize_nested_diff_value/1)
|
||||||
|
|
||||||
def normalize_nested_diff_value(value) when is_atom(value), do: Atom.to_string(value)
|
def normalize_nested_diff_value(value) when is_atom(value), do: Atom.to_string(value)
|
||||||
def normalize_nested_diff_value(value), do: value
|
def normalize_nested_diff_value(value), do: value
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -110,10 +110,18 @@ defmodule BDS.Maintenance.DiffReports do
|
|||||||
diff_field("author", post.author, Map.get(fields, "author")),
|
diff_field("author", post.author, Map.get(fields, "author")),
|
||||||
diff_field("language", post.language, Map.get(fields, "language")),
|
diff_field("language", post.language, Map.get(fields, "language")),
|
||||||
diff_field("status", post.status, DocumentFields.get(fields, "status")),
|
diff_field("status", post.status, DocumentFields.get(fields, "status")),
|
||||||
diff_field("template_slug", post.template_slug, DocumentFields.get(fields, "templateSlug")),
|
diff_field(
|
||||||
|
"template_slug",
|
||||||
|
post.template_slug,
|
||||||
|
DocumentFields.get(fields, "templateSlug")
|
||||||
|
),
|
||||||
diff_field("created_at", post.created_at, DocumentFields.get(fields, "createdAt")),
|
diff_field("created_at", post.created_at, DocumentFields.get(fields, "createdAt")),
|
||||||
diff_field("updated_at", post.updated_at, DocumentFields.get(fields, "updatedAt")),
|
diff_field("updated_at", post.updated_at, DocumentFields.get(fields, "updatedAt")),
|
||||||
diff_field("published_at", post.published_at, DocumentFields.get(fields, "publishedAt")),
|
diff_field(
|
||||||
|
"published_at",
|
||||||
|
post.published_at,
|
||||||
|
DocumentFields.get(fields, "publishedAt")
|
||||||
|
),
|
||||||
diff_field("tags", post.tags, Map.get(fields, "tags", [])),
|
diff_field("tags", post.tags, Map.get(fields, "tags", [])),
|
||||||
diff_field("categories", post.categories, Map.get(fields, "categories", []))
|
diff_field("categories", post.categories, Map.get(fields, "categories", []))
|
||||||
]
|
]
|
||||||
@@ -265,7 +273,11 @@ defmodule BDS.Maintenance.DiffReports do
|
|||||||
diff_field("title", script.title, Map.get(fields, "title")),
|
diff_field("title", script.title, Map.get(fields, "title")),
|
||||||
diff_field("entrypoint", script.entrypoint, Map.get(fields, "entrypoint")),
|
diff_field("entrypoint", script.entrypoint, Map.get(fields, "entrypoint")),
|
||||||
diff_field("enabled", script.enabled, Map.get(fields, "enabled")),
|
diff_field("enabled", script.enabled, Map.get(fields, "enabled")),
|
||||||
diff_field("created_at", script.created_at, DocumentFields.get(fields, "createdAt")),
|
diff_field(
|
||||||
|
"created_at",
|
||||||
|
script.created_at,
|
||||||
|
DocumentFields.get(fields, "createdAt")
|
||||||
|
),
|
||||||
diff_field("updated_at", script.updated_at, DocumentFields.get(fields, "updatedAt"))
|
diff_field("updated_at", script.updated_at, DocumentFields.get(fields, "updatedAt"))
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
@@ -296,8 +308,16 @@ defmodule BDS.Maintenance.DiffReports do
|
|||||||
[
|
[
|
||||||
diff_field("title", template.title, Map.get(fields, "title")),
|
diff_field("title", template.title, Map.get(fields, "title")),
|
||||||
diff_field("enabled", template.enabled, Map.get(fields, "enabled")),
|
diff_field("enabled", template.enabled, Map.get(fields, "enabled")),
|
||||||
diff_field("created_at", template.created_at, DocumentFields.get(fields, "createdAt")),
|
diff_field(
|
||||||
diff_field("updated_at", template.updated_at, DocumentFields.get(fields, "updatedAt"))
|
"created_at",
|
||||||
|
template.created_at,
|
||||||
|
DocumentFields.get(fields, "createdAt")
|
||||||
|
),
|
||||||
|
diff_field(
|
||||||
|
"updated_at",
|
||||||
|
template.updated_at,
|
||||||
|
DocumentFields.get(fields, "updatedAt")
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ defmodule BDS.MCP.AgentConfig do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def config_path(:claude_code, home_dir), do: Path.join(home_dir, ".claude.json")
|
def config_path(:claude_code, home_dir), do: Path.join(home_dir, ".claude.json")
|
||||||
def config_path(:github_copilot, home_dir), do: Path.join([home_dir, "Library", "Application Support", "Code", "User", "mcp.json"])
|
|
||||||
|
def config_path(:github_copilot, home_dir),
|
||||||
|
do: Path.join([home_dir, "Library", "Application Support", "Code", "User", "mcp.json"])
|
||||||
|
|
||||||
def packaged_executable_path(install_root, platform) when is_binary(install_root) do
|
def packaged_executable_path(install_root, platform) when is_binary(install_root) do
|
||||||
executable_name =
|
executable_name =
|
||||||
@@ -90,12 +92,21 @@ defmodule BDS.MCP.AgentConfig do
|
|||||||
defp merge_config(:github_copilot, config, command, args) do
|
defp merge_config(:github_copilot, config, command, args) do
|
||||||
servers = Map.get(config, "servers", %{})
|
servers = Map.get(config, "servers", %{})
|
||||||
|
|
||||||
Map.put(config, "servers", Map.put(servers, @server_name, %{"type" => "stdio", "command" => command, "args" => args}))
|
Map.put(
|
||||||
|
config,
|
||||||
|
"servers",
|
||||||
|
Map.put(servers, @server_name, %{"type" => "stdio", "command" => command, "args" => args})
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_config(:claude_code, config, command, args) do
|
defp merge_config(:claude_code, config, command, args) do
|
||||||
servers = Map.get(config, "mcpServers", %{})
|
servers = Map.get(config, "mcpServers", %{})
|
||||||
Map.put(config, "mcpServers", Map.put(servers, @server_name, %{"command" => command, "args" => args}))
|
|
||||||
|
Map.put(
|
||||||
|
config,
|
||||||
|
"mcpServers",
|
||||||
|
Map.put(servers, @server_name, %{"command" => command, "args" => args})
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp remove_server_entry(:github_copilot, config) do
|
defp remove_server_entry(:github_copilot, config) do
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ defmodule BDS.MCP.Proposal do
|
|||||||
|
|
||||||
schema "mcp_proposals" do
|
schema "mcp_proposals" do
|
||||||
field :kind, :string
|
field :kind, :string
|
||||||
field :status, Ecto.Enum, values: [:pending, :accepted, :discarded, :expired], default: :pending
|
|
||||||
|
field :status, Ecto.Enum,
|
||||||
|
values: [:pending, :accepted, :discarded, :expired],
|
||||||
|
default: :pending
|
||||||
|
|
||||||
field :entity_id, :string
|
field :entity_id, :string
|
||||||
field :data, :map
|
field :data, :map
|
||||||
field :created_at, :integer
|
field :created_at, :integer
|
||||||
@@ -17,7 +21,9 @@ defmodule BDS.MCP.Proposal do
|
|||||||
|
|
||||||
def changeset(proposal, attrs) do
|
def changeset(proposal, attrs) do
|
||||||
proposal
|
proposal
|
||||||
|> cast(attrs, [:id, :kind, :status, :entity_id, :data, :created_at, :expires_at], empty_values: [nil])
|
|> cast(attrs, [:id, :kind, :status, :entity_id, :data, :created_at, :expires_at],
|
||||||
|
empty_values: [nil]
|
||||||
|
)
|
||||||
|> validate_required([:id, :kind, :status, :entity_id, :data, :created_at, :expires_at])
|
|> validate_required([:id, :kind, :status, :entity_id, :data, :created_at, :expires_at])
|
||||||
|> unique_constraint(:status, name: :mcp_proposals_entity_idx)
|
|> unique_constraint(:status, name: :mcp_proposals_entity_idx)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,12 +74,15 @@ defmodule BDS.MCP.ProposalStore do
|
|||||||
|
|
||||||
defp mark_status(id, status) do
|
defp mark_status(id, status) do
|
||||||
case Repo.get(Proposal, id) do
|
case Repo.get(Proposal, id) do
|
||||||
nil -> nil
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
proposal ->
|
proposal ->
|
||||||
Repo.delete_all(
|
Repo.delete_all(
|
||||||
from other in Proposal,
|
from other in Proposal,
|
||||||
where:
|
where:
|
||||||
other.id != ^id and other.kind == ^proposal.kind and other.entity_id == ^proposal.entity_id and
|
other.id != ^id and other.kind == ^proposal.kind and
|
||||||
|
other.entity_id == ^proposal.entity_id and
|
||||||
other.status == ^status
|
other.status == ^status
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ defmodule BDS.MCP.ProposalStore do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp derive_entity_id(data) do
|
defp derive_entity_id(data) do
|
||||||
data["post_id"] || data["script_id"] || data["template_id"] || data["media_id"] || Ecto.UUID.generate()
|
data["post_id"] || data["script_id"] || data["template_id"] || data["media_id"] ||
|
||||||
|
Ecto.UUID.generate()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -138,8 +138,11 @@ defmodule BDS.MCP.Server do
|
|||||||
case URI.parse(target) do
|
case URI.parse(target) do
|
||||||
%URI{path: "/mcp"} ->
|
%URI{path: "/mcp"} ->
|
||||||
case GenServer.call(__MODULE__, {:http_request, request}, 5_000) do
|
case GenServer.call(__MODULE__, {:http_request, request}, 5_000) do
|
||||||
{:ok, status, body} -> http_response(status, Jason.encode!(body), "application/json", request.headers)
|
{:ok, status, body} ->
|
||||||
{:error, status, body} -> http_response(status, body, "text/plain", request.headers)
|
http_response(status, Jason.encode!(body), "application/json", request.headers)
|
||||||
|
|
||||||
|
{:error, status, body} ->
|
||||||
|
http_response(status, body, "text/plain", request.headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
@@ -170,7 +173,10 @@ defmodule BDS.MCP.Server do
|
|||||||
success_response(id, %{
|
success_response(id, %{
|
||||||
"protocolVersion" => Map.get(params, "protocolVersion", "2025-03-26"),
|
"protocolVersion" => Map.get(params, "protocolVersion", "2025-03-26"),
|
||||||
"capabilities" => %{"tools" => %{}, "resources" => %{}},
|
"capabilities" => %{"tools" => %{}, "resources" => %{}},
|
||||||
"serverInfo" => %{"name" => @server_name, "version" => Application.spec(:bds, :vsn) |> to_string()}
|
"serverInfo" => %{
|
||||||
|
"name" => @server_name,
|
||||||
|
"version" => Application.spec(:bds, :vsn) |> to_string()
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
"tools/list" ->
|
"tools/list" ->
|
||||||
@@ -196,10 +202,17 @@ defmodule BDS.MCP.Server do
|
|||||||
arguments = Map.get(params, "arguments", %{})
|
arguments = Map.get(params, "arguments", %{})
|
||||||
|
|
||||||
case BDS.MCP.call_tool(name, arguments) do
|
case BDS.MCP.call_tool(name, arguments) do
|
||||||
{:ok, result} -> {:ok, success_response(id, %{"content" => [%{"type" => "json", "json" => result}]})}
|
{:ok, result} ->
|
||||||
{:error, :unknown_tool} -> {:error, error_response(id, -32601, "Unknown tool")}
|
{:ok, success_response(id, %{"content" => [%{"type" => "json", "json" => result}]})}
|
||||||
{:error, :not_found} -> {:error, error_response(id, -32004, "Not found")}
|
|
||||||
{:error, reason} -> {:error, error_response(id, -32000, inspect(reason))}
|
{:error, :unknown_tool} ->
|
||||||
|
{:error, error_response(id, -32601, "Unknown tool")}
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
{:error, error_response(id, -32004, "Not found")}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, error_response(id, -32000, inspect(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -286,7 +299,8 @@ defmodule BDS.MCP.Server do
|
|||||||
|> IO.iodata_to_binary()
|
|> IO.iodata_to_binary()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp http_error_response(status, headers \\ %{}), do: http_response(status, reason_body(status), "text/plain", headers)
|
defp http_error_response(status, headers \\ %{}),
|
||||||
|
do: http_response(status, reason_body(status), "text/plain", headers)
|
||||||
|
|
||||||
defp reason_body(400), do: "Bad Request"
|
defp reason_body(400), do: "Bad Request"
|
||||||
defp reason_body(404), do: "Not Found"
|
defp reason_body(404), do: "Not Found"
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ defmodule BDS.MCP.Stdio do
|
|||||||
if line != "" do
|
if line != "" do
|
||||||
response =
|
response =
|
||||||
case Jason.decode(line) do
|
case Jason.decode(line) do
|
||||||
{:ok, payload} -> handle_payload(payload)
|
{:ok, payload} ->
|
||||||
{:error, _reason} -> %{"jsonrpc" => "2.0", "id" => nil, "error" => %{"code" => -32700, "message" => "Parse error"}}
|
handle_payload(payload)
|
||||||
|
|
||||||
|
{:error, _reason} ->
|
||||||
|
%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => nil,
|
||||||
|
"error" => %{"code" => -32700, "message" => "Parse error"}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
IO.write(Jason.encode!(response) <> "\n")
|
IO.write(Jason.encode!(response) <> "\n")
|
||||||
@@ -18,14 +25,22 @@ defmodule BDS.MCP.Stdio do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_payload(%{"jsonrpc" => "2.0", "id" => id, "method" => "initialize", "params" => params}) do
|
defp handle_payload(%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"method" => "initialize",
|
||||||
|
"params" => params
|
||||||
|
}) do
|
||||||
%{
|
%{
|
||||||
"jsonrpc" => "2.0",
|
"jsonrpc" => "2.0",
|
||||||
"id" => id,
|
"id" => id,
|
||||||
"result" => %{
|
"result" => %{
|
||||||
"protocolVersion" => Map.get(params, "protocolVersion", "2025-03-26"),
|
"protocolVersion" => Map.get(params, "protocolVersion", "2025-03-26"),
|
||||||
"capabilities" => %{"tools" => %{}, "resources" => %{}},
|
"capabilities" => %{"tools" => %{}, "resources" => %{}},
|
||||||
"serverInfo" => %{"name" => "Blogging Desktop Server", "version" => Application.spec(:bds, :vsn) |> to_string()}
|
"serverInfo" => %{
|
||||||
|
"name" => "Blogging Desktop Server",
|
||||||
|
"version" => Application.spec(:bds, :vsn) |> to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -34,10 +49,26 @@ defmodule BDS.MCP.Stdio do
|
|||||||
%{"jsonrpc" => "2.0", "id" => id, "result" => %{"tools" => BDS.MCP.list_tools()}}
|
%{"jsonrpc" => "2.0", "id" => id, "result" => %{"tools" => BDS.MCP.list_tools()}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_payload(%{"jsonrpc" => "2.0", "id" => id, "method" => "tools/call", "params" => %{"name" => name} = params}) do
|
defp handle_payload(%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"method" => "tools/call",
|
||||||
|
"params" => %{"name" => name} = params
|
||||||
|
}) do
|
||||||
case BDS.MCP.call_tool(name, Map.get(params, "arguments", %{})) do
|
case BDS.MCP.call_tool(name, Map.get(params, "arguments", %{})) do
|
||||||
{:ok, result} -> %{"jsonrpc" => "2.0", "id" => id, "result" => %{"content" => [%{"type" => "json", "json" => result}]}}
|
{:ok, result} ->
|
||||||
{:error, reason} -> %{"jsonrpc" => "2.0", "id" => id, "error" => %{"code" => -32000, "message" => inspect(reason)}}
|
%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"result" => %{"content" => [%{"type" => "json", "json" => result}]}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"error" => %{"code" => -32000, "message" => inspect(reason)}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,17 +76,38 @@ defmodule BDS.MCP.Stdio do
|
|||||||
%{"jsonrpc" => "2.0", "id" => id, "result" => %{"resources" => BDS.MCP.list_resources()}}
|
%{"jsonrpc" => "2.0", "id" => id, "result" => %{"resources" => BDS.MCP.list_resources()}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_payload(%{"jsonrpc" => "2.0", "id" => id, "method" => "resources/read", "params" => %{"uri" => uri}}) do
|
defp handle_payload(%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"method" => "resources/read",
|
||||||
|
"params" => %{"uri" => uri}
|
||||||
|
}) do
|
||||||
case BDS.MCP.read_resource(uri) do
|
case BDS.MCP.read_resource(uri) do
|
||||||
{:ok, result} ->
|
{:ok, result} ->
|
||||||
%{"jsonrpc" => "2.0", "id" => id, "result" => %{"contents" => [%{"uri" => uri, "mimeType" => "application/json", "text" => Jason.encode!(result)}]}}
|
%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"result" => %{
|
||||||
|
"contents" => [
|
||||||
|
%{"uri" => uri, "mimeType" => "application/json", "text" => Jason.encode!(result)}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
%{"jsonrpc" => "2.0", "id" => id, "error" => %{"code" => -32000, "message" => inspect(reason)}}
|
%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"error" => %{"code" => -32000, "message" => inspect(reason)}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_payload(%{"jsonrpc" => "2.0", "id" => id}) do
|
defp handle_payload(%{"jsonrpc" => "2.0", "id" => id}) do
|
||||||
%{"jsonrpc" => "2.0", "id" => id, "error" => %{"code" => -32601, "message" => "Method not found"}}
|
%{
|
||||||
|
"jsonrpc" => "2.0",
|
||||||
|
"id" => id,
|
||||||
|
"error" => %{"code" => -32601, "message" => "Method not found"}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ defmodule BDS.MCP.Tools do
|
|||||||
@spec validate_template(String.t()) :: {:ok, %{valid: boolean(), errors: [String.t()]}}
|
@spec validate_template(String.t()) :: {:ok, %{valid: boolean(), errors: [String.t()]}}
|
||||||
def validate_template(source) when is_binary(source) do
|
def validate_template(source) when is_binary(source) do
|
||||||
case Liquex.parse(source) do
|
case Liquex.parse(source) do
|
||||||
{:ok, _ast} -> {:ok, %{valid: true, errors: []}}
|
{:ok, _ast} ->
|
||||||
{:error, reason, line} -> {:ok, %{valid: false, errors: ["#{inspect(reason)} at line #{line}"]}}
|
{:ok, %{valid: true, errors: []}}
|
||||||
|
|
||||||
|
{:error, reason, line} ->
|
||||||
|
{:ok, %{valid: false, errors: ["#{inspect(reason)} at line #{line}"]}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -276,7 +279,8 @@ defmodule BDS.MCP.Tools do
|
|||||||
ttl_ms: @proposal_ttl_app_ms
|
ttl_ms: @proposal_ttl_app_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, %{"proposal_id" => proposal.id, "current" => sanitize(media), "proposed" => changes}}
|
{:ok,
|
||||||
|
%{"proposal_id" => proposal.id, "current" => sanitize(media), "proposed" => changes}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ defmodule BDS.Media.Rebuilder do
|
|||||||
|
|
||||||
@type rebuild_opts :: keyword()
|
@type rebuild_opts :: keyword()
|
||||||
|
|
||||||
@spec rebuild_media_from_files(String.t(), rebuild_opts()) :: {:ok, [Media.t()]} | {:error, term()}
|
@spec rebuild_media_from_files(String.t(), rebuild_opts()) ::
|
||||||
|
{:ok, [Media.t()]} | {:error, term()}
|
||||||
def rebuild_media_from_files(project_id, opts \\ []) do
|
def rebuild_media_from_files(project_id, opts \\ []) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
on_progress = progress_callback(opts)
|
on_progress = progress_callback(opts)
|
||||||
@@ -61,9 +62,10 @@ defmodule BDS.Media.Rebuilder do
|
|||||||
translation_sidecars
|
translation_sidecars
|
||||||
|> Enum.with_index(length(canonical_sidecars) + 1)
|
|> Enum.with_index(length(canonical_sidecars) + 1)
|
||||||
|> Enum.each(fn {sidecar, index} ->
|
|> Enum.each(fn {sidecar, index} ->
|
||||||
Sidecars.upsert_translation_from_sidecar(project, canonical_media_by_binary_path, sidecar,
|
Sidecars.upsert_translation_from_sidecar(
|
||||||
sync_search: false
|
project,
|
||||||
)
|
canonical_media_by_binary_path,
|
||||||
|
sidecar, sync_search: false)
|
||||||
|
|
||||||
:ok = report_rebuild_progress(on_progress, index, total_files, "media files")
|
:ok = report_rebuild_progress(on_progress, index, total_files, "media files")
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -141,7 +141,12 @@ defmodule BDS.Media.Sidecars do
|
|||||||
media
|
media
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec upsert_translation_from_sidecar(BDS.Projects.Project.t(), %{required(Path.t()) => Media.t()}, map(), keyword()) ::
|
@spec upsert_translation_from_sidecar(
|
||||||
|
BDS.Projects.Project.t(),
|
||||||
|
%{required(Path.t()) => Media.t()},
|
||||||
|
map(),
|
||||||
|
keyword()
|
||||||
|
) ::
|
||||||
Translation.t() | :skip | :ok
|
Translation.t() | :skip | :ok
|
||||||
def upsert_translation_from_sidecar(project, canonical_media_by_binary_path, sidecar, opts) do
|
def upsert_translation_from_sidecar(project, canonical_media_by_binary_path, sidecar, opts) do
|
||||||
case Map.get(canonical_media_by_binary_path, sidecar.binary_path) do
|
case Map.get(canonical_media_by_binary_path, sidecar.binary_path) do
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ defmodule BDS.Media.Thumbnails do
|
|||||||
missing_paths =
|
missing_paths =
|
||||||
media
|
media
|
||||||
|> thumbnail_paths()
|
|> thumbnail_paths()
|
||||||
|> Enum.map(fn {_size, relative_path} -> Path.join(Projects.project_data_dir(project), relative_path) end)
|
|> Enum.map(fn {_size, relative_path} ->
|
||||||
|
Path.join(Projects.project_data_dir(project), relative_path)
|
||||||
|
end)
|
||||||
|> Enum.reject(&File.exists?/1)
|
|> Enum.reject(&File.exists?/1)
|
||||||
|
|
||||||
next_acc =
|
next_acc =
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ defmodule BDS.Persistence do
|
|||||||
value
|
value
|
||||||
|> String.trim()
|
|> String.trim()
|
||||||
|> case do
|
|> case do
|
||||||
"" -> nil
|
"" ->
|
||||||
|
nil
|
||||||
|
|
||||||
trimmed ->
|
trimmed ->
|
||||||
case Integer.parse(trimmed) do
|
case Integer.parse(trimmed) do
|
||||||
{integer, ""} -> normalize_unix_timestamp(integer)
|
{integer, ""} -> normalize_unix_timestamp(integer)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defmodule BDS.PostLinks do
|
|||||||
alias BDS.Projects
|
alias BDS.Projects
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
|
|
||||||
|
@spec sync_post_links(Post.t()) :: :ok
|
||||||
def sync_post_links(%Post{} = post) do
|
def sync_post_links(%Post{} = post) do
|
||||||
links =
|
links =
|
||||||
post
|
post
|
||||||
@@ -41,6 +42,7 @@ defmodule BDS.PostLinks do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_post_links(String.t()) :: :ok
|
||||||
def delete_post_links(post_id) when is_binary(post_id) do
|
def delete_post_links(post_id) when is_binary(post_id) do
|
||||||
Repo.delete_all(
|
Repo.delete_all(
|
||||||
from link in Link,
|
from link in Link,
|
||||||
@@ -50,12 +52,18 @@ defmodule BDS.PostLinks do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec list_outgoing_links(String.t()) :: [Link.t()]
|
||||||
def list_outgoing_links(post_id) when is_binary(post_id) do
|
def list_outgoing_links(post_id) when is_binary(post_id) do
|
||||||
Repo.all(from link in Link, where: link.source_post_id == ^post_id, order_by: [asc: link.created_at])
|
Repo.all(
|
||||||
|
from link in Link, where: link.source_post_id == ^post_id, order_by: [asc: link.created_at]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec list_incoming_links(String.t()) :: [Link.t()]
|
||||||
def list_incoming_links(post_id) when is_binary(post_id) do
|
def list_incoming_links(post_id) when is_binary(post_id) do
|
||||||
Repo.all(from link in Link, where: link.target_post_id == ^post_id, order_by: [asc: link.created_at])
|
Repo.all(
|
||||||
|
from link in Link, where: link.target_post_id == ^post_id, order_by: [asc: link.created_at]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp post_body(%Post{content: content}) when is_binary(content), do: content
|
defp post_body(%Post{content: content}) when is_binary(content), do: content
|
||||||
@@ -83,11 +91,15 @@ defmodule BDS.PostLinks do
|
|||||||
defp extract_links(body) when is_binary(body) do
|
defp extract_links(body) when is_binary(body) do
|
||||||
markdown_links =
|
markdown_links =
|
||||||
Regex.scan(~r/\[([^\]]+)\]\(([^)]+)\)/, body)
|
Regex.scan(~r/\[([^\]]+)\]\(([^)]+)\)/, body)
|
||||||
|> Enum.map(fn [_full, link_text, href] -> %{link_text: normalize_link_text(link_text), href: href} end)
|
|> Enum.map(fn [_full, link_text, href] ->
|
||||||
|
%{link_text: normalize_link_text(link_text), href: href}
|
||||||
|
end)
|
||||||
|
|
||||||
html_links =
|
html_links =
|
||||||
Regex.scan(~r/<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/is, body)
|
Regex.scan(~r/<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/is, body)
|
||||||
|> Enum.map(fn [_full, href, link_text] -> %{link_text: normalize_link_text(link_text), href: href} end)
|
|> Enum.map(fn [_full, href, link_text] ->
|
||||||
|
%{link_text: normalize_link_text(link_text), href: href}
|
||||||
|
end)
|
||||||
|
|
||||||
markdown_links ++ html_links
|
markdown_links ++ html_links
|
||||||
end
|
end
|
||||||
@@ -121,12 +133,17 @@ defmodule BDS.PostLinks do
|
|||||||
[language, year, month, day, slug] ->
|
[language, year, month, day, slug] ->
|
||||||
if language_code?(language) and numeric_year?(year) and numeric_month_or_day?(month) and
|
if language_code?(language) and numeric_year?(year) and numeric_month_or_day?(month) and
|
||||||
numeric_month_or_day?(day),
|
numeric_month_or_day?(day),
|
||||||
do: slug,
|
do: slug,
|
||||||
else: nil
|
else: nil
|
||||||
|
|
||||||
[slug] -> slug
|
[slug] ->
|
||||||
[language, slug] -> if(language_code?(language), do: slug, else: nil)
|
slug
|
||||||
_other -> nil
|
|
||||||
|
[language, slug] ->
|
||||||
|
if(language_code?(language), do: slug, else: nil)
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ defmodule BDS.Posts.AutoTranslation do
|
|||||||
|
|
||||||
defp media_needed?(media_id, language) do
|
defp media_needed?(media_id, language) do
|
||||||
case Repo.get(Media.Media, media_id) do
|
case Repo.get(Media.Media, media_id) do
|
||||||
%Media.Media{language: source_language} when source_language not in [nil, ""] and source_language != language ->
|
%Media.Media{language: source_language}
|
||||||
|
when source_language not in [nil, ""] and source_language != language ->
|
||||||
not Repo.exists?(
|
not Repo.exists?(
|
||||||
from translation in Media.Translation,
|
from translation in Media.Translation,
|
||||||
where: translation.translation_for == ^media_id and translation.language == ^language
|
where: translation.translation_for == ^media_id and translation.language == ^language
|
||||||
|
|||||||
@@ -18,8 +18,15 @@ defmodule BDS.Posts.Link do
|
|||||||
}
|
}
|
||||||
|
|
||||||
schema "post_links" do
|
schema "post_links" do
|
||||||
belongs_to :source_post, BDS.Posts.Post, foreign_key: :source_post_id, references: :id, type: :string
|
belongs_to :source_post, BDS.Posts.Post,
|
||||||
belongs_to :target_post, BDS.Posts.Post, foreign_key: :target_post_id, references: :id, type: :string
|
foreign_key: :source_post_id,
|
||||||
|
references: :id,
|
||||||
|
type: :string
|
||||||
|
|
||||||
|
belongs_to :target_post, BDS.Posts.Post,
|
||||||
|
foreign_key: :target_post_id,
|
||||||
|
references: :id,
|
||||||
|
type: :string
|
||||||
|
|
||||||
field :link_text, :string
|
field :link_text, :string
|
||||||
field :created_at, :integer
|
field :created_at, :integer
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ defmodule BDS.Posts.TranslationValidation do
|
|||||||
Repo.all(
|
Repo.all(
|
||||||
from translation in Translation,
|
from translation in Translation,
|
||||||
where: translation.project_id == ^project_id,
|
where: translation.project_id == ^project_id,
|
||||||
order_by: [asc: translation.translation_for, asc: translation.language, asc: translation.id]
|
order_by: [
|
||||||
|
asc: translation.translation_for,
|
||||||
|
asc: translation.language,
|
||||||
|
asc: translation.id
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
project_data_dir = Projects.project_data_dir(project)
|
project_data_dir = Projects.project_data_dir(project)
|
||||||
@@ -67,7 +71,13 @@ defmodule BDS.Posts.TranslationValidation do
|
|||||||
translation_rows
|
translation_rows
|
||||||
|> Enum.with_index(1)
|
|> Enum.with_index(1)
|
||||||
|> Enum.flat_map(fn {translation, index} ->
|
|> Enum.flat_map(fn {translation, index} ->
|
||||||
:ok = RebuildFromFiles.report_rebuild_progress(on_progress, index, total_items, "translations")
|
:ok =
|
||||||
|
RebuildFromFiles.report_rebuild_progress(
|
||||||
|
on_progress,
|
||||||
|
index,
|
||||||
|
total_items,
|
||||||
|
"translations"
|
||||||
|
)
|
||||||
|
|
||||||
case invalid_database_translation_issue(translation, source_post_map, metadata) do
|
case invalid_database_translation_issue(translation, source_post_map, metadata) do
|
||||||
nil -> []
|
nil -> []
|
||||||
@@ -80,7 +90,13 @@ defmodule BDS.Posts.TranslationValidation do
|
|||||||
markdown_files
|
markdown_files
|
||||||
|> Enum.with_index(length(translation_rows) + 1)
|
|> Enum.with_index(length(translation_rows) + 1)
|
||||||
|> Enum.reduce({0, []}, fn {file_path, index}, {count, issues} ->
|
|> Enum.reduce({0, []}, fn {file_path, index}, {count, issues} ->
|
||||||
:ok = RebuildFromFiles.report_rebuild_progress(on_progress, index, total_items, "translations")
|
:ok =
|
||||||
|
RebuildFromFiles.report_rebuild_progress(
|
||||||
|
on_progress,
|
||||||
|
index,
|
||||||
|
total_items,
|
||||||
|
"translations"
|
||||||
|
)
|
||||||
|
|
||||||
case invalid_filesystem_translation_issue(file_path, source_post_map, metadata) do
|
case invalid_filesystem_translation_issue(file_path, source_post_map, metadata) do
|
||||||
{:ok, nil} -> {count + 1, issues}
|
{:ok, nil} -> {count + 1, issues}
|
||||||
@@ -118,11 +134,19 @@ defmodule BDS.Posts.TranslationValidation do
|
|||||||
normalized_report = normalize_report(report)
|
normalized_report = normalize_report(report)
|
||||||
|
|
||||||
{deleted_database_rows, flushed_translations, synced_post_ids} =
|
{deleted_database_rows, flushed_translations, synced_post_ids} =
|
||||||
Enum.reduce(normalized_report.invalid_database_rows, {0, 0, MapSet.new()}, fn issue, {deleted, flushed, synced_ids} ->
|
Enum.reduce(normalized_report.invalid_database_rows, {0, 0, MapSet.new()}, fn issue,
|
||||||
|
{deleted,
|
||||||
|
flushed,
|
||||||
|
synced_ids} ->
|
||||||
case fix_invalid_database_row(issue) do
|
case fix_invalid_database_row(issue) do
|
||||||
{:deleted, post_id} -> {deleted + 1, flushed, maybe_put_synced_post(synced_ids, post_id)}
|
{:deleted, post_id} ->
|
||||||
{:flushed, post_id} -> {deleted, flushed + 1, maybe_put_synced_post(synced_ids, post_id)}
|
{deleted + 1, flushed, maybe_put_synced_post(synced_ids, post_id)}
|
||||||
:noop -> {deleted, flushed, synced_ids}
|
|
||||||
|
{:flushed, post_id} ->
|
||||||
|
{deleted, flushed + 1, maybe_put_synced_post(synced_ids, post_id)}
|
||||||
|
|
||||||
|
:noop ->
|
||||||
|
{deleted, flushed, synced_ids}
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -365,7 +389,10 @@ defmodule BDS.Posts.TranslationValidation do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fix_invalid_database_row(%{translation_id: translation_id, translation_for: translation_for})
|
defp fix_invalid_database_row(%{
|
||||||
|
translation_id: translation_id,
|
||||||
|
translation_for: translation_for
|
||||||
|
})
|
||||||
when is_binary(translation_id) do
|
when is_binary(translation_id) do
|
||||||
case Repo.get(Translation, translation_id) do
|
case Repo.get(Translation, translation_id) do
|
||||||
%Translation{} = translation ->
|
%Translation{} = translation ->
|
||||||
@@ -402,7 +429,11 @@ defmodule BDS.Posts.TranslationValidation do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp issue_sort_key(issue) do
|
defp issue_sort_key(issue) do
|
||||||
[Map.get(issue, :translation_for), Map.get(issue, :translation_id), Map.get(issue, :file_path)]
|
[
|
||||||
|
Map.get(issue, :translation_for),
|
||||||
|
Map.get(issue, :translation_id),
|
||||||
|
Map.get(issue, :file_path)
|
||||||
|
]
|
||||||
|> Enum.map(&to_string(&1 || ""))
|
|> Enum.map(&to_string(&1 || ""))
|
||||||
|> Enum.join(":")
|
|> Enum.join(":")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -64,7 +64,11 @@ defmodule BDS.Preview do
|
|||||||
{:reply, reply, next_state}
|
{:reply, reply, next_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:ensure_preview, project_id, _data_dir, _owner_pid}, _from, %{current: %{project_id: project_id, is_running: true}} = state) do
|
def handle_call(
|
||||||
|
{:ensure_preview, project_id, _data_dir, _owner_pid},
|
||||||
|
_from,
|
||||||
|
%{current: %{project_id: project_id, is_running: true}} = state
|
||||||
|
) do
|
||||||
{:reply, {:ok, public_server(state.current)}, state}
|
{:reply, {:ok, public_server(state.current)}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -224,7 +228,9 @@ defmodule BDS.Preview do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp draft_preview_translation(_post_id, nil, _post_language), do: nil
|
defp draft_preview_translation(_post_id, nil, _post_language), do: nil
|
||||||
defp draft_preview_translation(_post_id, requested_language, post_language) when requested_language == post_language, do: nil
|
|
||||||
|
defp draft_preview_translation(_post_id, requested_language, post_language)
|
||||||
|
when requested_language == post_language, do: nil
|
||||||
|
|
||||||
defp draft_preview_translation(post_id, requested_language, _post_language) do
|
defp draft_preview_translation(post_id, requested_language, _post_language) do
|
||||||
Repo.get_by(Translation, translation_for: post_id, language: requested_language)
|
Repo.get_by(Translation, translation_for: post_id, language: requested_language)
|
||||||
@@ -456,7 +462,10 @@ defmodule BDS.Preview do
|
|||||||
{uri.path || "/", URI.decode_query(uri.query || "")}
|
{uri.path || "/", URI.decode_query(uri.query || "")}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_response_overrides(%{content_type: content_type, body: body} = response, query_params)
|
defp apply_response_overrides(
|
||||||
|
%{content_type: content_type, body: body} = response,
|
||||||
|
query_params
|
||||||
|
)
|
||||||
when is_binary(content_type) and is_binary(body) do
|
when is_binary(content_type) and is_binary(body) do
|
||||||
if String.starts_with?(content_type, "text/html") do
|
if String.starts_with?(content_type, "text/html") do
|
||||||
%{response | body: apply_preview_overrides(body, query_params)}
|
%{response | body: apply_preview_overrides(body, query_params)}
|
||||||
@@ -465,7 +474,8 @@ defmodule BDS.Preview do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_preview_overrides(body, query_params) when is_binary(body) and is_map(query_params) do
|
defp apply_preview_overrides(body, query_params)
|
||||||
|
when is_binary(body) and is_map(query_params) do
|
||||||
theme_override = normalize_pico_theme_override(query_params["theme"])
|
theme_override = normalize_pico_theme_override(query_params["theme"])
|
||||||
mode_override = normalize_mode_override(query_params["mode"])
|
mode_override = normalize_mode_override(query_params["mode"])
|
||||||
|
|
||||||
@@ -506,7 +516,9 @@ defmodule BDS.Preview do
|
|||||||
[html_tag] ->
|
[html_tag] ->
|
||||||
replacement =
|
replacement =
|
||||||
if String.contains?(html_tag, attribute <> "=") do
|
if String.contains?(html_tag, attribute <> "=") do
|
||||||
Regex.replace(~r/\s#{attribute}="[^"]*"/, html_tag, ~s( #{attribute}="#{value}"), global: false)
|
Regex.replace(~r/\s#{attribute}="[^"]*"/, html_tag, ~s( #{attribute}="#{value}"),
|
||||||
|
global: false
|
||||||
|
)
|
||||||
else
|
else
|
||||||
String.replace_suffix(html_tag, ">", ~s( #{attribute}="#{value}">))
|
String.replace_suffix(html_tag, ">", ~s( #{attribute}="#{value}">))
|
||||||
end
|
end
|
||||||
@@ -520,7 +532,11 @@ defmodule BDS.Preview do
|
|||||||
|
|
||||||
defp not_found_assigns(query_params) do
|
defp not_found_assigns(query_params) do
|
||||||
%{}
|
%{}
|
||||||
|> maybe_put_assign("pico_stylesheet_href", normalize_pico_theme_override(query_params["theme"]), &PreviewAssets.stylesheet_href/1)
|
|> maybe_put_assign(
|
||||||
|
"pico_stylesheet_href",
|
||||||
|
normalize_pico_theme_override(query_params["theme"]),
|
||||||
|
&PreviewAssets.stylesheet_href/1
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_assign(assigns, _key, nil, _mapper), do: assigns
|
defp maybe_put_assign(assigns, _key, nil, _mapper), do: assigns
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ defmodule BDS.Projects do
|
|||||||
sync_filesystem_metadata(project)
|
sync_filesystem_metadata(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -166,7 +167,8 @@ defmodule BDS.Projects do
|
|||||||
|
|
||||||
@spec delete_project(String.t()) ::
|
@spec delete_project(String.t()) ::
|
||||||
{:ok, Project.t()}
|
{:ok, Project.t()}
|
||||||
| {:error, :not_found | :cannot_delete_default_project | :cannot_delete_active_project | term()}
|
| {:error,
|
||||||
|
:not_found | :cannot_delete_default_project | :cannot_delete_active_project | term()}
|
||||||
def delete_project(project_id) when is_binary(project_id) do
|
def delete_project(project_id) when is_binary(project_id) do
|
||||||
case Repo.get(Project, project_id) do
|
case Repo.get(Project, project_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -180,7 +182,9 @@ defmodule BDS.Projects do
|
|||||||
|
|
||||||
%Project{} = project ->
|
%Project{} = project ->
|
||||||
internal_dir = if is_nil(project.data_path), do: project_data_dir(project), else: nil
|
internal_dir = if is_nil(project.data_path), do: project_data_dir(project), else: nil
|
||||||
cleanup_dirs = [internal_dir, project_cache_dir(project)] |> Enum.filter(&is_binary/1) |> Enum.uniq()
|
|
||||||
|
cleanup_dirs =
|
||||||
|
[internal_dir, project_cache_dir(project)] |> Enum.filter(&is_binary/1) |> Enum.uniq()
|
||||||
|
|
||||||
Repo.transaction(fn ->
|
Repo.transaction(fn ->
|
||||||
Repo.delete!(project)
|
Repo.delete!(project)
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ defmodule BDS.Rebuild do
|
|||||||
timeout = Keyword.get(opts, :timeout, :infinity)
|
timeout = Keyword.get(opts, :timeout, :infinity)
|
||||||
|
|
||||||
items
|
items
|
||||||
|> Task.async_stream(mapper, max_concurrency: max_concurrency, ordered: ordered, timeout: timeout)
|
|> Task.async_stream(mapper,
|
||||||
|
max_concurrency: max_concurrency,
|
||||||
|
ordered: ordered,
|
||||||
|
timeout: timeout
|
||||||
|
)
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
{:ok, item} -> item
|
{:ok, item} -> item
|
||||||
{:exit, reason} -> exit(reason)
|
{:exit, reason} -> exit(reason)
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ defmodule BDS.ReleasePackaging do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_metadata(platform, version, output_dir) when is_binary(version) and is_binary(output_dir) do
|
def build_metadata(platform, version, output_dir)
|
||||||
|
when is_binary(version) and is_binary(output_dir) do
|
||||||
normalized_platform = normalize_platform(platform)
|
normalized_platform = normalize_platform(platform)
|
||||||
payload_name = "bds2-#{normalized_platform}-#{version}"
|
payload_name = "bds2-#{normalized_platform}-#{version}"
|
||||||
payload_root = Path.join(output_dir, payload_name)
|
payload_root = Path.join(output_dir, payload_name)
|
||||||
@@ -66,7 +67,9 @@ defmodule BDS.ReleasePackaging do
|
|||||||
|
|
||||||
defp normalize_platform(platform) when platform in [:macos, :linux, :windows], do: platform
|
defp normalize_platform(platform) when platform in [:macos, :linux, :windows], do: platform
|
||||||
defp normalize_platform(:darwin), do: :macos
|
defp normalize_platform(:darwin), do: :macos
|
||||||
defp normalize_platform(platform) when is_binary(platform), do: platform |> String.downcase() |> String.to_atom()
|
|
||||||
|
defp normalize_platform(platform) when is_binary(platform),
|
||||||
|
do: platform |> String.downcase() |> String.to_atom()
|
||||||
|
|
||||||
defp archive_extension(:windows), do: ".zip"
|
defp archive_extension(:windows), do: ".zip"
|
||||||
defp archive_extension(_platform), do: ".tar.gz"
|
defp archive_extension(_platform), do: ".tar.gz"
|
||||||
@@ -107,7 +110,9 @@ defmodule BDS.ReleasePackaging do
|
|||||||
relative_entries = collect_entries(metadata.payload_root)
|
relative_entries = collect_entries(metadata.payload_root)
|
||||||
cwd = metadata.output_dir |> String.to_charlist()
|
cwd = metadata.output_dir |> String.to_charlist()
|
||||||
archive = metadata.archive_path |> String.to_charlist()
|
archive = metadata.archive_path |> String.to_charlist()
|
||||||
entries = Enum.map(relative_entries, &String.to_charlist(Path.join(metadata.payload_name, &1)))
|
|
||||||
|
entries =
|
||||||
|
Enum.map(relative_entries, &String.to_charlist(Path.join(metadata.payload_name, &1)))
|
||||||
|
|
||||||
case :zip.create(archive, entries, cwd: cwd) do
|
case :zip.create(archive, entries, cwd: cwd) do
|
||||||
{:ok, _archive_path} -> :ok
|
{:ok, _archive_path} -> :ok
|
||||||
@@ -116,7 +121,13 @@ defmodule BDS.ReleasePackaging do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp create_archive(metadata) do
|
defp create_archive(metadata) do
|
||||||
case System.cmd("tar", ["-czf", metadata.archive_path, "-C", metadata.output_dir, metadata.payload_name]) do
|
case System.cmd("tar", [
|
||||||
|
"-czf",
|
||||||
|
metadata.archive_path,
|
||||||
|
"-C",
|
||||||
|
metadata.output_dir,
|
||||||
|
metadata.payload_name
|
||||||
|
]) do
|
||||||
{_output, 0} -> :ok
|
{_output, 0} -> :ok
|
||||||
{output, status} -> {:error, {:tar_failed, status, output}}
|
{output, status} -> {:error, {:tar_failed, status, output}}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ defmodule BDS.Rendering do
|
|||||||
|
|
||||||
def render_post_page(project_id, template_slug, assigns)
|
def render_post_page(project_id, template_slug, assigns)
|
||||||
when is_binary(project_id) and is_map(assigns) do
|
when is_binary(project_id) and is_map(assigns) do
|
||||||
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :post, template_slug),
|
with {:ok, template_source} <-
|
||||||
|
TemplateSelection.load_template_source(project_id, :post, template_slug),
|
||||||
{:ok, rendered} <-
|
{:ok, rendered} <-
|
||||||
TemplateSelection.render_template(project_id, template_source, PostRendering.post_assigns(project_id, assigns)) do
|
TemplateSelection.render_template(
|
||||||
|
project_id,
|
||||||
|
template_source,
|
||||||
|
PostRendering.post_assigns(project_id, assigns)
|
||||||
|
) do
|
||||||
{:ok, rendered}
|
{:ok, rendered}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -17,16 +22,25 @@ defmodule BDS.Rendering do
|
|||||||
def render_list_page(project_id, assigns) when is_binary(project_id) and is_map(assigns) do
|
def render_list_page(project_id, assigns) when is_binary(project_id) and is_map(assigns) do
|
||||||
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :list, nil),
|
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :list, nil),
|
||||||
{:ok, rendered} <-
|
{:ok, rendered} <-
|
||||||
TemplateSelection.render_template(project_id, template_source, ListArchive.list_assigns(project_id, assigns)) do
|
TemplateSelection.render_template(
|
||||||
|
project_id,
|
||||||
|
template_source,
|
||||||
|
ListArchive.list_assigns(project_id, assigns)
|
||||||
|
) do
|
||||||
{:ok, rendered}
|
{:ok, rendered}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_not_found_page(project_id, assigns \\ %{})
|
def render_not_found_page(project_id, assigns \\ %{})
|
||||||
when is_binary(project_id) and is_map(assigns) do
|
when is_binary(project_id) and is_map(assigns) do
|
||||||
with {:ok, template_source} <- TemplateSelection.load_template_source(project_id, :not_found, nil),
|
with {:ok, template_source} <-
|
||||||
|
TemplateSelection.load_template_source(project_id, :not_found, nil),
|
||||||
{:ok, rendered} <-
|
{:ok, rendered} <-
|
||||||
TemplateSelection.render_template(project_id, template_source, ListArchive.not_found_assigns(project_id, assigns)) do
|
TemplateSelection.render_template(
|
||||||
|
project_id,
|
||||||
|
template_source,
|
||||||
|
ListArchive.not_found_assigns(project_id, assigns)
|
||||||
|
) do
|
||||||
{:ok, rendered}
|
{:ok, rendered}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ defmodule BDS.Rendering.Metadata do
|
|||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|> Enum.map(fn language ->
|
|> Enum.map(fn language ->
|
||||||
normalized = I18n.normalize_language(language)
|
normalized = I18n.normalize_language(language)
|
||||||
href_prefix = LinksAndLanguages.language_prefix(normalized, metadata.main_language || current_language)
|
|
||||||
|
href_prefix =
|
||||||
|
LinksAndLanguages.language_prefix(normalized, metadata.main_language || current_language)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
code: normalized,
|
code: normalized,
|
||||||
@@ -84,9 +86,17 @@ defmodule BDS.Rendering.Metadata do
|
|||||||
order_by: [asc: translation.language]
|
order_by: [asc: translation.language]
|
||||||
)
|
)
|
||||||
|
|
||||||
[%{href: LinksAndLanguages.post_path(post, nil), hreflang: LinksAndLanguages.normalize_language(post.language, main_language)}] ++
|
[
|
||||||
|
%{
|
||||||
|
href: LinksAndLanguages.post_path(post, nil),
|
||||||
|
hreflang: LinksAndLanguages.normalize_language(post.language, main_language)
|
||||||
|
}
|
||||||
|
] ++
|
||||||
Enum.map(translations, fn translation ->
|
Enum.map(translations, fn translation ->
|
||||||
%{href: LinksAndLanguages.post_path(post, translation.language, main_language), hreflang: translation.language}
|
%{
|
||||||
|
href: LinksAndLanguages.post_path(post, translation.language, main_language),
|
||||||
|
hreflang: translation.language
|
||||||
|
}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ defmodule BDS.Scripting do
|
|||||||
runtime().execute(source, entrypoint, args, opts)
|
runtime().execute(source, entrypoint, args, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec execute_project_script(String.t(), String.t(), String.t(), [term()], [Runtime.execution_option()]) ::
|
@spec execute_project_script(String.t(), String.t(), String.t(), [term()], [
|
||||||
|
Runtime.execution_option()
|
||||||
|
]) ::
|
||||||
{:ok, term()} | {:error, term()}
|
{:ok, term()} | {:error, term()}
|
||||||
def execute_project_script(project_id, source, entrypoint, args \\ [], opts \\ [])
|
def execute_project_script(project_id, source, entrypoint, args \\ [], opts \\ [])
|
||||||
when is_binary(project_id) and is_binary(source) and is_binary(entrypoint) and
|
when is_binary(project_id) and is_binary(source) and is_binary(entrypoint) and
|
||||||
@@ -45,13 +47,20 @@ defmodule BDS.Scripting do
|
|||||||
execute(source, entrypoint, args, Keyword.put(opts, :capabilities, capabilities))
|
execute(source, entrypoint, args, Keyword.put(opts, :capabilities, capabilities))
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec execute_macro(String.t(), String.t(), [term()], keyword()) :: {:ok, String.t()} | {:error, term()}
|
@spec execute_macro(String.t(), String.t(), [term()], keyword()) ::
|
||||||
|
{:ok, String.t()} | {:error, term()}
|
||||||
def execute_macro(project_id, source, args, opts \\ [])
|
def execute_macro(project_id, source, args, opts \\ [])
|
||||||
when is_binary(project_id) and is_binary(source) and is_list(args) and is_list(opts) do
|
when is_binary(project_id) and is_binary(source) and is_list(args) and is_list(opts) do
|
||||||
config = Application.fetch_env!(:bds, :scripting)
|
config = Application.fetch_env!(:bds, :scripting)
|
||||||
timeout = Keyword.get(opts, :timeout, Keyword.fetch!(config, :timeout))
|
timeout = Keyword.get(opts, :timeout, Keyword.fetch!(config, :timeout))
|
||||||
|
|
||||||
case execute_project_script(project_id, source, "render", args, Keyword.put(opts, :timeout, timeout)) do
|
case execute_project_script(
|
||||||
|
project_id,
|
||||||
|
source,
|
||||||
|
"render",
|
||||||
|
args,
|
||||||
|
Keyword.put(opts, :timeout, timeout)
|
||||||
|
) do
|
||||||
{:ok, nil} -> {:ok, ""}
|
{:ok, nil} -> {:ok, ""}
|
||||||
{:ok, value} -> {:ok, to_string(value)}
|
{:ok, value} -> {:ok, to_string(value)}
|
||||||
{:error, _reason} -> {:ok, ""}
|
{:error, _reason} -> {:ok, ""}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user