chore: added more @spec

This commit is contained in:
2026-05-01 17:49:50 +02:00
parent abcae1dad7
commit 881056eb61
157 changed files with 6223 additions and 1647 deletions

View File

@@ -8,10 +8,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking}
embed_templates "chat_editor_html/*"
embed_templates("chat_editor_html/*")
# ── Public API: state assignment ───────────────────────────────────────────
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
assign(socket, :chat_editor, MessageBuild.build(socket.assigns))
end
@@ -25,6 +26,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
# ── Public API: input + surface state ──────────────────────────────────────
@spec update_input(term(), term(), term()) :: term()
def update_input(socket, value, reload) do
%{id: conversation_id} = socket.assigns.current_tab
@@ -36,6 +38,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|> reload.(socket.assigns.workbench)
end
@spec update_surface_form(term(), term(), term(), term()) :: term()
def update_surface_form(socket, surface_id, fields, reload)
when is_binary(surface_id) and is_map(fields) do
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)
end
@spec select_surface_tab(term(), term(), term(), term()) :: term()
def select_surface_tab(socket, surface_id, index, reload)
when is_binary(surface_id) and is_integer(index) and index >= 0 do
socket
@@ -55,10 +59,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|> reload.(socket.assigns.workbench)
end
@spec current_surface_data(term(), term()) :: term()
def current_surface_data(socket, surface_id) when is_binary(surface_id) do
Map.get(socket.assigns.chat_editor_surface_data, surface_id, %{})
end
@spec set_action_error(term(), term(), term(), term()) :: term()
def set_action_error(socket, conversation_id, message, reload)
when is_binary(conversation_id) and is_binary(message) do
socket
@@ -69,6 +75,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|> reload.(socket.assigns.workbench)
end
@spec clear_action_error(term(), term(), term()) :: term()
def clear_action_error(socket, conversation_id, reload) when is_binary(conversation_id) do
socket
|> assign(
@@ -80,6 +87,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
# ── Public API: messaging ──────────────────────────────────────────────────
@spec send_message(term(), term(), term()) :: term()
def send_message(socket, reload, append_output) do
%{id: conversation_id} = socket.assigns.current_tab
message = socket.assigns.chat_editor_inputs |> Map.get(conversation_id, "") |> String.trim()
@@ -144,6 +152,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
end
end
@spec abort_message(term(), term()) :: term()
def abort_message(socket, reload) do
%{id: conversation_id} = socket.assigns.current_tab
@@ -167,6 +176,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
end
end
@spec note_tool_call(term(), term(), term(), term()) :: term()
def note_tool_call(socket, conversation_id, tool_call, reload)
when is_binary(conversation_id) and is_map(tool_call) do
update_request(
@@ -189,6 +199,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
)
end
@spec note_tool_result(term(), term(), term(), term()) :: term()
def note_tool_result(socket, conversation_id, name, reload)
when is_binary(conversation_id) and is_binary(name) do
update_request(
@@ -201,6 +212,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
)
end
@spec note_streaming_content(term(), term(), term(), term()) :: term()
def note_streaming_content(socket, conversation_id, content, reload)
when is_binary(conversation_id) and is_binary(content) do
update_request(
@@ -211,6 +223,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
)
end
@spec finish_request(term(), term(), term(), term(), term()) :: term()
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
{nil, _remaining_refs} ->
@@ -245,12 +258,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
# ── HEEx-callable helpers ─────────────────────────────────────────────────
@spec message_role_label(term()) :: term()
def message_role_label(:user), do: translated("chat.role.you")
def message_role_label(_role), do: translated("chat.role.assistant")
defdelegate tool_call_name(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 markdown_html(content) when is_binary(content) do
@@ -264,8 +279,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
raw(html)
end
@spec markdown_html(term()) :: term()
def markdown_html(_content), do: ""
@spec payload_json(term()) :: term()
def payload_json(nil), do: "{}"
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)
end
@spec chart_width(term(), term()) :: term()
def chart_width(_max_value, _value), do: 0
def truthy?(value) when value in [true, "true", 1, "1", "on"], do: true
@spec truthy?(term()) :: term()
def truthy?(_value), do: false
# ── HEEx components ───────────────────────────────────────────────────────
attr :markers, :list, required: true
attr(:markers, :list, required: true)
@spec chat_tool_markers(term()) :: term()
def chat_tool_markers(assigns) do
~H"""
<%= if @markers != [] do %>
@@ -307,8 +327,9 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
"""
end
attr :surface, :map, required: true
attr(:surface, :map, required: true)
@spec chat_surface(term()) :: term()
def chat_surface(assigns) do
~H"""
<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
)
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)
end)
end
@@ -571,6 +593,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
defp format_error(reason), do: inspect(reason)
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
end

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.ChatEditor.{ModelSelection, ToolSurfaces, ToolTracking}
@spec build(term()) :: term()
def build(%{current_tab: %{type: :chat, id: conversation_id}} = assigns) do
case AI.get_chat_conversation(conversation_id) do
nil ->

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do
import Phoenix.Component, only: [assign: 3]
@spec toggle_model_selector(term(), term()) :: term()
def toggle_model_selector(socket, reload) do
%{id: conversation_id} = socket.assigns.current_tab
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)
end
@spec set_model(term(), term(), term(), term()) :: term()
def set_model(socket, model_id, reload, append_output) do
%{id: conversation_id} = socket.assigns.current_tab
@@ -37,6 +39,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do
end
end
@spec group_available_models(term()) :: term()
def group_available_models(models) when is_list(models) do
models
|> 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)))
end
@spec needs_api_key?(term()) :: term()
def needs_api_key?(true), do: false
def needs_api_key?(false) do

View File

@@ -14,9 +14,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
"render_tabs"
])
@spec render_tool?(term()) :: term()
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
@spec build_render_surfaces(term(), term(), term()) :: term()
def build_render_surfaces(tool_calls, message_id, assigns) do
tool_calls
|> Enum.with_index()
@@ -28,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
end)
end
@spec build_render_surface(term(), term(), term()) :: term()
def build_render_surface(%{name: name, arguments: arguments}, surface_id, assigns) do
if MapSet.member?(@render_tool_names, name) do
do_build_render_surface(name, arguments || %{}, surface_id, assigns)
@@ -51,6 +55,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
end
end
@spec normalize_tool_surface(term()) :: term()
def normalize_tool_surface(_content), do: nil
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),
input_type: map_value(field, "inputType") || map_value(field, "input_type", "text"),
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", [])),
required?: truthy?(map_value(field, "required", false))
}
@@ -161,8 +171,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
type: "form",
title: map_value(arguments, "title"),
fields: fields,
submit_label: map_value(arguments, "submitLabel") || map_value(arguments, "submit_label", translated("chat.stop")),
submit_action: map_value(arguments, "submitAction") || map_value(arguments, "submit_action", "submitForm")
submit_label:
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
@@ -181,7 +195,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
|> List.wrap()
|> Enum.with_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)
@@ -203,11 +221,21 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do
type = map_value(content, "type", "text")
case type do
render_type 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)
render_type
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" ->
%{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 ->
%{id: surface_id, type: "json", raw: content}

View File

@@ -3,10 +3,12 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
@tool_args_max_length 30
@spec tool_call_name(term()) :: term()
def tool_call_name(tool_call) when is_map(tool_call) do
BDS.MapUtils.attr(tool_call, :name) || "tool"
end
@spec tool_call_arguments(term()) :: term()
def tool_call_arguments(tool_call) when is_map(tool_call) do
BDS.MapUtils.attr(tool_call, :arguments) || BDS.MapUtils.attr(tool_call, :args) || %{}
end
@@ -25,6 +27,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
end)
end
@spec normalize_tool_calls(term()) :: term()
def normalize_tool_calls(_tool_calls), do: []
def tool_arguments_preview(arguments) when is_map(arguments) do
@@ -33,6 +36,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
|> Enum.join(", ")
end
@spec tool_arguments_preview(term()) :: term()
def tool_arguments_preview(_arguments), 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
@spec mark_tool_call_completed(term(), term()) :: term()
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(%{tool_events: tool_events}) do

View File

@@ -10,12 +10,14 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
embed_templates("code_entity_editor_html/*")
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
socket
|> assign(:script_editor, build_script(socket.assigns))
|> assign(:template_editor, build_template(socket.assigns))
end
@spec update_script(term(), term(), term()) :: term()
def update_script(socket, params, reload) do
%{id: script_id} = socket.assigns.current_tab
@@ -27,6 +29,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|> reload.(socket.assigns.workbench)
end
@spec save_script(term(), term(), term()) :: term()
def save_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab
@@ -62,6 +65,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec check_script(term(), term(), term()) :: term()
def check_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab
@@ -82,6 +86,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec run_script(term(), term(), term()) :: term()
def run_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab
@@ -111,6 +116,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec delete_script(term(), term(), term()) :: term()
def delete_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab
@@ -124,6 +130,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec update_template(term(), term(), term()) :: term()
def update_template(socket, params, reload) do
%{id: template_id} = socket.assigns.current_tab
@@ -139,6 +146,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
|> reload.(socket.assigns.workbench)
end
@spec save_template(term(), term(), term()) :: term()
def save_template(socket, reload, append_output) do
%{id: template_id} = socket.assigns.current_tab
@@ -169,6 +177,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec validate_template(term(), term(), term()) :: term()
def validate_template(socket, reload, append_output) do
%{id: template_id} = socket.assigns.current_tab
@@ -195,6 +204,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec delete_template(term(), term(), term()) :: term()
def delete_template(socket, reload, append_output) do
%{id: template_id} = socket.assigns.current_tab
@@ -211,6 +221,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end
end
@spec build_script(term()) :: term()
def build_script(%{current_tab: %{type: :scripts, id: script_id}} = assigns) do
case Scripts.get_script(script_id) do
nil ->
@@ -236,6 +247,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def build_script(_assigns), do: nil
@spec build_template(term()) :: term()
def build_template(%{current_tab: %{type: :templates, id: template_id}} = assigns) do
case Templates.get_template(template_id) do
nil ->
@@ -259,9 +271,11 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def build_template(_assigns), do: nil
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
@spec format_timestamp(term()) :: term()
def format_timestamp(nil), do: ""
def format_timestamp(timestamp), do: BDS.Persistence.timestamp_to_iso8601(timestamp)

View File

@@ -57,7 +57,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
total,
detail,
reload
), to: ProgressTracking
),
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 analyze_taxonomy_ai(socket, reload, append_output), to: TaxonomyEditing
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
case socket.assigns[:current_tab] do
%{type: :import, id: definition_id} ->
@@ -140,6 +142,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
end
end
@spec toggle_section(term(), term(), term()) :: term()
def toggle_section(socket, section, reload) do
with %{id: definition_id} <- socket.assigns.current_tab,
section_key
@@ -171,6 +174,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
end
end
@spec toggle_model_selector(term(), term()) :: term()
def toggle_model_selector(socket, reload) do
with %{id: definition_id} <- socket.assigns.current_tab do
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
@spec select_ai_model(term(), term(), term()) :: term()
def select_ai_model(socket, model_id, reload) do
with %{id: definition_id} <- socket.assigns.current_tab do
socket
@@ -205,6 +210,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:import_editor, :map, required: true)
@spec import_editor(term()) :: term()
def import_editor(assigns) do
assigns =
assigns
@@ -547,6 +553,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:expanded, :boolean, required: true)
attr(:section, :string, required: true)
@spec conflict_section(term()) :: term()
def conflict_section(assigns) do
~H"""
<section class="import-detail-section conflicts-section">
@@ -597,6 +604,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:section, :string, required: true)
attr(:show_type, :boolean, default: false)
@spec post_detail_section(term()) :: term()
def post_detail_section(assigns) do
~H"""
<section class="import-detail-section">
@@ -646,6 +654,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:expanded, :boolean, required: true)
attr(:section, :string, required: true)
@spec media_detail_section(term()) :: term()
def media_detail_section(assigns) do
~H"""
<section class="import-detail-section">
@@ -685,6 +694,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:label, :string, required: true)
attr(:stats, :map, required: true)
@spec stat_card(term()) :: term()
def stat_card(assigns) do
~H"""
<div class="import-stat-card">
@@ -703,6 +713,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:label, :string, required: true)
attr(:stats, :map, required: true)
@spec other_stat_card(term()) :: term()
def other_stat_card(assigns) do
~H"""
<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(:stats, :map, required: true)
@spec media_stat_card(term()) :: term()
def media_stat_card(assigns) do
~H"""
<div class="import-stat-card">
@@ -739,6 +751,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:label, :string, required: true)
attr(:stats, :map, required: true)
@spec taxonomy_stat_card(term()) :: term()
def taxonomy_stat_card(assigns) do
~H"""
<div class="import-stat-card">
@@ -759,6 +772,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
attr(:edit, :map, default: nil)
attr(:type, :string, required: true)
@spec taxonomy_group(term()) :: term()
def taxonomy_group(assigns) do
~H"""
<div class="taxonomy-group">

View File

@@ -4,20 +4,27 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
alias BDS.{ImportAnalysis, ImportDefinitions, Metadata}
alias BDS.Desktop.{FilePicker, FolderPicker, ShellData}
@spec change_definition(term(), term(), term()) :: term()
def change_definition(socket, params, reload) do
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)
else
_other -> reload.(socket, socket.assigns.workbench)
end
end
@spec select_uploads_folder(term(), term(), term()) :: term()
def select_uploads_folder(socket, reload, append_output) do
with %{id: definition_id} <- socket.assigns.current_tab do
case FolderPicker.choose_directory(translated("importAnalysis.uploadsFolder")) do
{: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)
:cancel ->
@@ -33,6 +40,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
end
end
@spec select_and_analyze(term(), term(), term()) :: term()
def select_and_analyze(socket, reload, append_output) do
with %{id: definition_id} <- socket.assigns.current_tab,
%{} = definition <- ImportDefinitions.get_definition(definition_id) do
@@ -50,9 +58,15 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
task =
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 ->
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)
@@ -70,8 +84,14 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
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(:import_editor_execution_states, Map.delete(socket.assigns.import_editor_execution_states, definition_id))
|> Phoenix.Component.assign(
: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)
:cancel ->
@@ -87,32 +107,50 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
end
end
@spec note_analysis_progress(term(), term(), term(), term(), term()) :: term()
def note_analysis_progress(socket, definition_id, step, detail, reload) do
socket
|> Phoenix.Component.assign(
:import_editor_analysis_states,
Map.update(socket.assigns.import_editor_analysis_states, definition_id, default_analysis_state(), fn state ->
state
|> Map.put(:loading, true)
|> Map.put(:step, step)
|> Map.put(:detail, detail)
end)
Map.update(
socket.assigns.import_editor_analysis_states,
definition_id,
default_analysis_state(),
fn state ->
state
|> Map.put(:loading, true)
|> Map.put(:step, step)
|> Map.put(:detail, detail)
end
)
)
|> reload.(socket.assigns.workbench)
end
@spec finish_analysis(term(), term(), term(), term(), term()) :: term()
def finish_analysis(socket, ref, result, reload, append_output) do
case Map.get(socket.assigns.import_editor_analysis_task_refs, ref) do
nil ->
socket
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
|> Phoenix.Component.assign(: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))
|> Phoenix.Component.assign(
: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
{:ok, report} ->
@@ -146,6 +184,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
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
case Map.get(socket.assigns.import_editor_analysis_task_refs, ref) do
nil ->
@@ -153,13 +192,20 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
definition_id ->
socket
|> Phoenix.Component.assign(: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))
|> Phoenix.Component.assign(
: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")
|> reload.(socket.assigns.workbench)
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(report) do
@@ -171,25 +217,37 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
pages = importable_entity_count(Map.get(report.items, :pages, []))
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
@spec importable_entity_count(term()) :: term()
def importable_entity_count(items) do
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
@spec detail_items(term(), term()) :: term()
def detail_items(nil, _bucket), do: []
def detail_items(report, bucket) do
get_in(report, [:details, bucket]) || get_in(report, [:items, bucket]) || []
end
@spec default_analysis_state() :: term()
def default_analysis_state do
%{loading: false, step: nil, detail: nil, file_path: nil, ref: nil}
end
@spec default_sections() :: term()
def default_sections do
%{
post_conflicts: true,
@@ -203,18 +261,22 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
}
end
@spec default_author(term()) :: term()
def default_author(project_id) do
{:ok, metadata} = Metadata.get_project_metadata(project_id)
Map.get(metadata, :default_author)
end
@spec suggested_definition_name(term()) :: term()
def suggested_definition_name(report) do
get_in(report, [:site_info, :url]) || get_in(report, [:site_info, :title])
end
@spec maybe_put(term(), term(), term()) :: term()
def maybe_put(map, _key, nil), do: map
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
if Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) do
try do
@@ -241,8 +303,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do
end
end
@spec translate_phase(term()) :: term()
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, ""]
end

View File

@@ -3,18 +3,27 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ConflictResolution do
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,
%{} = definition <- ImportDefinitions.get_definition(definition_id),
%{} = report <- ImportDefinitions.decode_analysis_result(definition),
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)
else
_other -> reload.(socket, socket.assigns.workbench)
end
end
@spec update_conflict_resolution(term(), term(), term(), term()) :: term()
def update_conflict_resolution(report, item_type, item_name, resolution) do
report
|> 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))
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(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 ->
Enum.map(items || [], fn item ->

View File

@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.ImportEditor.AnalysisState
@spec execute_import(term(), term(), term()) :: term()
def execute_import(socket, reload, _append_output) do
with %{id: definition_id} <- socket.assigns.current_tab,
%{} = definition <- ImportDefinitions.get_definition(definition_id),
@@ -24,7 +25,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
uploads_folder_path: definition.uploads_folder_path,
default_author: default_author,
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)
@@ -50,7 +54,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
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)
end
else
@@ -58,6 +65,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
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
{detail_text, eta} = decompose_progress_detail(detail)
translated_phase = translate_execution_phase(phase)
@@ -65,30 +73,44 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
socket
|> Phoenix.Component.assign(
:import_editor_execution_states,
Map.update(socket.assigns.import_editor_execution_states, definition_id, default_execution_state(), fn state ->
state
|> Map.put(:is_executing, true)
|> Map.put(:phase, translated_phase)
|> Map.put(:current, current)
|> Map.put(:total, total)
|> Map.put(:detail, detail_text)
|> Map.put(:eta, eta)
end)
Map.update(
socket.assigns.import_editor_execution_states,
definition_id,
default_execution_state(),
fn state ->
state
|> Map.put(:is_executing, true)
|> Map.put(:phase, translated_phase)
|> Map.put(:current, current)
|> Map.put(:total, total)
|> Map.put(:detail, detail_text)
|> Map.put(:eta, eta)
end
)
)
|> reload.(socket.assigns.workbench)
end
@spec finish_execution(term(), term(), term(), term(), term()) :: term()
def finish_execution(socket, ref, result, reload, append_output) do
case Map.get(socket.assigns.import_editor_execution_task_refs, ref) do
nil ->
socket
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)
)
case result do
{:ok, execution_result} ->
@@ -106,7 +128,12 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
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)
{:error, %{message: message}} ->
@@ -144,7 +171,9 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
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)
case kind do
@@ -157,10 +186,18 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
socket
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
|> 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(
:import_editor_execution_states,
Map.put(socket.assigns.import_editor_execution_states, definition_id, %{
@@ -177,8 +214,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
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
@spec default_execution_state() :: term()
def default_execution_state do
%{
is_executing: false,
@@ -195,6 +234,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
}
end
@spec execution_progress_width(term()) :: term()
def execution_progress_width(state) do
current = Map.get(state, :current, 0)
total = Map.get(state, :total, 0)
@@ -205,25 +245,36 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
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) 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}
@spec to_string_or_nil(term()) :: term()
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), do: inspect(value)
@spec format_eta(term()) :: term()
def format_eta(nil), do: nil
def format_eta(ms) when is_integer(ms) and ms >= 0 do
seconds = div(ms, 1000)
if seconds < 60 do
translated("importAnalysis.eta", %{value: translated("importAnalysis.etaSeconds", %{count: seconds})})
translated("importAnalysis.eta", %{
value: translated("importAnalysis.etaSeconds", %{count: seconds})
})
else
m = div(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
@@ -240,7 +291,9 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
end
end
@spec translate_execution_phase(term()) :: term()
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

View File

@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
alias BDS.{AI, ImportDefinitions, Metadata, Tags}
alias BDS.Desktop.ShellData
@spec start_taxonomy_edit(term(), term(), term()) :: term()
def start_taxonomy_edit(
socket,
%{"type" => type, "name" => name, "mapped_to" => mapped_to},
@@ -25,6 +26,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end
end
@spec cancel_taxonomy_edit(term(), term()) :: term()
def cancel_taxonomy_edit(socket, reload) do
with %{id: definition_id} <- socket.assigns.current_tab do
socket
@@ -38,6 +40,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end
end
@spec save_taxonomy_edit(term(), term(), term()) :: term()
def save_taxonomy_edit(
socket,
%{"type" => type, "name" => name, "mapped_to" => mapped_to},
@@ -68,10 +71,12 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end
end
@spec clear_taxonomy_mapping(term(), term(), term()) :: term()
def clear_taxonomy_mapping(socket, %{"type" => type, "name" => name}, reload) do
save_taxonomy_edit(socket, %{"type" => type, "name" => name, "mapped_to" => ""}, reload)
end
@spec analyze_taxonomy_ai(term(), term(), term()) :: term()
def analyze_taxonomy_ai(socket, reload, append_output) do
with %{id: definition_id} <- socket.assigns.current_tab,
%{} = definition <- ImportDefinitions.get_definition(definition_id),
@@ -142,6 +147,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end
end
@spec update_taxonomy_mapping(term(), term(), term(), term()) :: term()
def update_taxonomy_mapping(report, type, name, mapped_to) do
bucket_key = if(type == "categories", do: :categories, else: :tags)
normalized_value = mapped_to |> to_string() |> String.trim() |> blank_to_nil()
@@ -164,6 +170,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
)
end
@spec rebuild_taxonomy_stats(term()) :: term()
def rebuild_taxonomy_stats(items) do
%{
existing_count: Enum.count(items, & &1.exists_in_project),
@@ -172,9 +179,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
}
end
@spec stat_key(term()) :: term()
def stat_key(:categories), do: :category_stats
def stat_key(:tags), do: :tag_stats
@spec apply_taxonomy_mappings(term(), term()) :: term()
def apply_taxonomy_mappings(report, analysis) do
report
|> update_in(
@@ -198,6 +207,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end)
end
@spec apply_taxonomy_mapping_bucket(term(), term()) :: term()
def apply_taxonomy_mapping_bucket(items, mappings) do
Enum.map(items || [], fn item ->
case Map.fetch(mappings, item.name) do
@@ -207,6 +217,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end)
end
@spec existing_taxonomy_terms(term()) :: term()
def existing_taxonomy_terms(project_id) do
{:ok, metadata} = Metadata.get_project_metadata(project_id)
@@ -216,6 +227,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
}
end
@spec normalize_taxonomy_mapping_value(term(), term(), term()) :: term()
def normalize_taxonomy_mapping_value(project_id, type, mapped_to) do
normalized_value = mapped_to |> to_string() |> String.trim() |> blank_to_nil()
@@ -231,6 +243,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
end
end
@spec auto_mapped_count(term(), term()) :: term()
def auto_mapped_count(previous_report, next_report) do
previous_count =
(Map.get(previous_report.items, :categories, []) ++
@@ -244,6 +257,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
max(next_count - previous_count, 0)
end
@spec taxonomy_pill_class(term()) :: term()
def taxonomy_pill_class(item) do
cond do
item.exists_in_project -> "import-taxonomy-pill exists"
@@ -252,9 +266,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
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?(_edit, _type, _name), do: false
@spec taxonomy_mapping_tooltip(term()) :: term()
def taxonomy_mapping_tooltip(item) do
action =
if present?(item.mapped_to),
@@ -264,6 +280,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
translated("importAnalysis.mappingTooltip", %{action: action})
end
@spec maybe_put_option(term(), term(), term()) :: term()
def maybe_put_option(opts, _key, nil), do: opts
def maybe_put_option(opts, key, value), do: Keyword.put(opts, key, value)

View File

@@ -32,6 +32,7 @@ defmodule BDS.Desktop.ShellLive.Layout do
end
defp maybe_set_sidebar_width(workbench, nil), do: workbench
defp maybe_set_sidebar_width(workbench, width),
do: Workbench.set_sidebar_width(workbench, parse_width(width))

View File

@@ -13,14 +13,16 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
alias BDS.Repo
alias BDS.UI.Workbench
embed_templates "media_editor_html/*"
embed_templates("media_editor_html/*")
@post_picker_limit 10
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
assign(socket, :media_editor, build(socket.assigns))
end
@spec update(term(), term(), term()) :: term()
def update(socket, params, reload) do
case socket.assigns.current_tab do
%{type: :media, id: media_id} ->
@@ -38,6 +40,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec persist_socket(term(), term(), term(), term()) :: term()
def persist_socket(socket, media_id, reload, append_output) do
case Media.get_media(media_id) do
nil ->
@@ -52,9 +55,18 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
socket
|> assign(:workbench, workbench)
|> assign(:media_editor_drafts, 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)))
|> assign(
:media_editor_drafts,
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)
{:error, reason} ->
@@ -65,14 +77,19 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec toggle_quick_actions(term(), term(), term()) :: term()
def toggle_quick_actions(socket, media_id, reload) do
workbench = socket.assigns.workbench
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)
end
@spec replace_file(term(), term(), term(), term()) :: term()
def replace_file(socket, media_id, reload, append_output) do
case FilePicker.choose_file(translated("Replace Media File")) do
{:ok, source_path} ->
@@ -82,9 +99,18 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
socket
|> assign(:workbench, workbench)
|> assign(:media_editor_drafts, 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)))
|> assign(
:media_editor_drafts,
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)
{:ok, nil} ->
@@ -106,10 +132,16 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec detect_language(term(), term(), term(), term()) :: term()
def detect_language(socket, media_id, reload, append_output) do
if Map.get(socket.assigns, :offline_mode, true) do
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)
else
case Media.get_media(media_id) do
@@ -118,15 +150,26 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
%MediaRecord{} = 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
{: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)
case Media.update_media(media.id, %{language: normalized}) do
{: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
|> reconcile_draft(updated_media, updated_draft)
@@ -145,17 +188,28 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
_other ->
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)
end
end
end
end
@spec translate(term(), term(), term(), term(), term()) :: term()
def translate(socket, media_id, language, reload, append_output) do
if Map.get(socket.assigns, :offline_mode, true) do
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)
else
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
{:ok, _saved_translation} ->
socket
|> assign(: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))
|> assign(
: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)
{:error, reason} ->
@@ -183,6 +243,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec apply_ai_suggestions(term(), term(), term(), term(), term()) :: term()
def apply_ai_suggestions(socket, media_id, fields, reload, append_output) do
try do
case Media.get_media(media_id) do
@@ -213,6 +274,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec delete_socket(term(), term(), term(), term()) :: term()
def delete_socket(socket, media_id, reload, append_output) do
case Media.delete_media(media_id) do
{:ok, :deleted} ->
@@ -223,11 +285,26 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:media, media_id}))
|> assign(:media_editor_drafts, Map.delete(socket.assigns.media_editor_drafts, media_id))
|> assign(:media_editor_quick_actions_open, Map.delete(socket.assigns.media_editor_quick_actions_open, media_id))
|> assign(:media_editor_post_pickers_open, Map.delete(socket.assigns.media_editor_post_pickers_open, media_id))
|> assign(:media_editor_post_picker_queries, Map.delete(socket.assigns.media_editor_post_picker_queries, media_id))
|> assign(:media_editor_save_states, Map.delete(socket.assigns.media_editor_save_states, media_id))
|> assign(:media_editor_translation_forms, Map.delete(socket.assigns.media_editor_translation_forms, media_id))
|> assign(
:media_editor_quick_actions_open,
Map.delete(socket.assigns.media_editor_quick_actions_open, media_id)
)
|> assign(
:media_editor_post_pickers_open,
Map.delete(socket.assigns.media_editor_post_pickers_open, media_id)
)
|> assign(
:media_editor_post_picker_queries,
Map.delete(socket.assigns.media_editor_post_picker_queries, media_id)
)
|> assign(
:media_editor_save_states,
Map.delete(socket.assigns.media_editor_save_states, media_id)
)
|> assign(
:media_editor_translation_forms,
Map.delete(socket.assigns.media_editor_translation_forms, media_id)
)
|> reload.(workbench)
{:error, reason} ->
@@ -237,28 +314,43 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec toggle_post_picker(term(), term(), term()) :: term()
def toggle_post_picker(socket, media_id, reload) do
workbench = socket.assigns.workbench
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)
end
@spec set_post_picker_query(term(), term(), term(), term()) :: term()
def set_post_picker_query(socket, media_id, query, reload) do
workbench = socket.assigns.workbench
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)
end
@spec link_post(term(), term(), term(), term(), term()) :: term()
def link_post(socket, media_id, post_id, reload, append_output) do
case Media.link_media_to_post(media_id, post_id) do
{:ok, _linked} ->
socket
|> assign(: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, ""))
|> assign(
: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)
{:error, reason} ->
@@ -268,6 +360,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec unlink_post(term(), term(), term(), term(), term()) :: term()
def unlink_post(socket, media_id, post_id, reload, append_output) do
case Media.unlink_media_from_post(media_id, post_id) do
{:ok, _unlinked} ->
@@ -280,6 +373,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec edit_translation(term(), term(), term(), term()) :: term()
def edit_translation(socket, media_id, language, reload) do
workbench = socket.assigns.workbench
@@ -287,16 +381,20 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
form = %{
"language" => language,
"title" => translation && translation.title || "",
"alt" => translation && translation.alt || "",
"caption" => translation && translation.caption || ""
"title" => (translation && translation.title) || "",
"alt" => (translation && translation.alt) || "",
"caption" => (translation && translation.caption) || ""
}
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)
end
@spec update_translation(term(), term(), term(), term()) :: term()
def update_translation(socket, media_id, params, reload) do
workbench = socket.assigns.workbench
@@ -308,10 +406,14 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
}
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)
end
@spec save_translation(term(), term(), term(), term()) :: term()
def save_translation(socket, media_id, reload, append_output) do
case Map.get(socket.assigns.media_editor_translation_forms, media_id) do
%{"language" => language} = form when language not in [nil, ""] ->
@@ -322,7 +424,10 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
}) do
{:ok, _translation} ->
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)
{:error, reason} ->
@@ -336,16 +441,23 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec refresh_translation(term(), term(), term(), term(), term()) :: term()
def refresh_translation(socket, media_id, language, reload, append_output) do
if Map.get(socket.assigns, :offline_mode, true) do
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)
else
case AI.translate_media(media_id, normalize_language(language)) do
{:ok, translation} ->
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} ->
socket
@@ -361,11 +473,15 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec delete_translation(term(), term(), term(), term(), term()) :: term()
def delete_translation(socket, media_id, language, reload, append_output) do
case Media.delete_media_translation(media_id, language) do
{:ok, _deleted?} ->
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)
{:error, reason} ->
@@ -375,6 +491,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
end
@spec build(term()) :: term()
def build(%{current_tab: %{type: :media, id: media_id}} = assigns) do
case Media.get_media(media_id) do
nil ->
@@ -385,7 +502,9 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
translations = Media.list_media_translations(media.id)
form = current_draft(assigns, media)
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,
@@ -416,20 +535,26 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
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(:saved), do: translated("Saved")
def media_editor_save_state_label(_state), do: translated("Idle")
@spec language_label(term()) :: term()
def language_label(code) do
code
|> to_string()
|> String.upcase()
end
@spec normalize_language(term()) :: term()
def normalize_language(value), do: value |> to_string() |> String.trim() |> String.downcase()
@spec persist(term(), term()) :: term()
def persist(%MediaRecord{} = media, draft) do
Media.update_media(media.id, %{
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
persisted = persisted_form(media)
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 =
if dirty? do
@@ -456,8 +585,21 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
socket
|> assign(:workbench, workbench)
|> 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(: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 || ""}))
|> assign(
: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
defp current_draft(assigns, %MediaRecord{} = media) do
@@ -505,10 +647,15 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
from post in Post,
where: post.project_id == ^media.project_id,
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.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)}
end
@@ -518,18 +665,28 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end
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
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 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), do: :erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
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),
do: :erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
defp format_file_size(_size), do: "0.0 KB"
defp detect_language_enabled?(form) do
@@ -567,5 +724,6 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
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

View File

@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
use Phoenix.Component
alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.MenuEditor.{
DraftManagement,
PageCategory,
@@ -12,8 +13,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
TreePredicates
}
embed_templates "menu_editor_html/*"
embed_templates("menu_editor_html/*")
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
case socket.assigns[:current_tab] do
%{type: :menu_editor, id: tab_id} ->
@@ -36,12 +38,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end
end
@spec select_item(term(), term(), term()) :: term()
def select_item(socket, item_id, reload) do
socket
|> State.update_state(fn state -> %{state | selected_id: item_id} end)
|> reload.(socket.assigns.workbench)
end
@spec change_entry(term(), term(), term()) :: term()
def change_entry(socket, params, reload) do
query = Map.get(params, "query", "")
@@ -50,6 +54,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|> reload.(socket.assigns.workbench)
end
@spec submit_entry(term(), term()) :: term()
def submit_entry(socket, reload) do
case DraftManagement.current_draft(socket.assigns) do
%{type: :page} ->
@@ -67,12 +72,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end
end
@spec cancel_entry(term(), term()) :: term()
def cancel_entry(socket, reload) do
socket
|> State.update_state(&DraftManagement.cancel_draft/1)
|> reload.(socket.assigns.workbench)
end
@spec select_page(term(), term(), term()) :: term()
def select_page(socket, post_id, reload) do
case PageCategory.page_post(socket.assigns.projects.active_project_id, post_id) do
nil ->
@@ -85,6 +92,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end
end
@spec select_category(term(), term(), term()) :: term()
def select_category(socket, name, reload) do
project_id = socket.assigns.projects.active_project_id
@@ -99,6 +107,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end
end
@spec toolbar_action(term(), term(), term(), term()) :: term()
def toolbar_action(socket, action, reload, append_output) do
case action do
"add-entry" ->
@@ -144,12 +153,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end
end
@spec drop_item(term(), term(), term(), term(), term()) :: term()
def drop_item(socket, drag_item_id, target_item_id, position, reload) do
socket
|> State.update_state(&TreeOps.drop_selected(&1, drag_item_id, target_item_id, position))
|> reload.(socket.assigns.workbench)
end
@spec handle_keydown(term(), term(), term()) :: term()
def handle_keydown(socket, "Escape", reload) do
cancel_entry(socket, reload)
end
@@ -158,14 +169,16 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
reload.(socket, socket.assigns.workbench)
end
attr :menu_editor, :map, required: true
attr(:menu_editor, :map, required: true)
@spec menu_editor(term()) :: term()
def menu_editor(assigns)
attr :items, :list, required: true
attr :menu_editor, :map, required: true
attr :depth, :integer, required: true
attr(:items, :list, required: true)
attr(:menu_editor, :map, required: true)
attr(:depth, :integer, required: true)
@spec menu_tree_level(term()) :: term()
def menu_tree_level(assigns) do
~H"""
<%= for item <- @items do %>
@@ -289,8 +302,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
"""
end
attr :kind, :atom, required: true
attr(:kind, :atom, required: true)
@spec kind_icon(term()) :: term()
def kind_icon(assigns) do
~H"""
<%= case @kind do %>
@@ -306,9 +320,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
"""
end
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
@spec row_label(term(), term()) :: term()
def row_label(item, category_titles) do
if item.kind == :category_archive do
Map.get(category_titles || %{}, item.slug, item.label)
@@ -317,6 +333,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end
end
@spec kind_label(term()) :: term()
def kind_label(:home), do: translated("menuEditor.type.home")
def kind_label(:page), do: translated("menuEditor.type.page")
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
@spec editing_title(term()) :: term()
def editing_title(%{draft: %{type: :category}}), do: translated("menuEditor.addCategoryArchive")
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(_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")
end

View File

@@ -6,8 +6,10 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
alias BDS.Desktop.ShellLive.MenuEditor.PageCategory
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
@spec current_draft(term()) :: term()
def current_draft(assigns), do: Map.get(assigns.menu_editor_state || %{}, :draft)
@spec start_page_draft(term()) :: term()
def start_page_draft(state) do
item = %{
item_id: Ecto.UUID.generate(),
@@ -29,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
}
end
@spec start_category_draft(term()) :: term()
def start_category_draft(state) do
item = %{
item_id: Ecto.UUID.generate(),
@@ -50,6 +53,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
}
end
@spec finalize_submenu_draft(term()) :: term()
def finalize_submenu_draft(%{draft: %{item_id: item_id, query: query}} = state) do
label =
if(String.trim(query) == "",
@@ -69,12 +73,19 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
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
%{
state
| items:
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),
draft: nil
}
@@ -82,6 +93,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
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
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
@spec cancel_draft(term()) :: term()
def cancel_draft(%{draft: %{item_id: item_id}} = state) do
items = TreeOps.remove_item(state.items, item_id)
%{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
@spec confirm_category_draft(term(), term()) :: term()
def confirm_category_draft(socket, update_state_fun) do
project_id = socket.assigns.projects.active_project_id
draft = current_draft(socket.assigns)
@@ -117,8 +131,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
category =
cond do
category != nil -> category
normalized == "" -> %{name: "", title: ""}
category != nil ->
category
normalized == "" ->
%{name: "", title: ""}
true ->
{:ok, _metadata} = Metadata.add_category(project_id, normalized)
%{name: normalized, title: normalized}

View File

@@ -6,19 +6,26 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
alias BDS.{Metadata, Repo}
alias BDS.Posts.Post
@spec page_posts(term()) :: term()
def page_posts(nil), 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 || [])))
end
@spec page_post(term(), term()) :: term()
def page_post(nil, _post_id), do: nil
def page_post(project_id, post_id) do
Enum.find(page_posts(project_id), &(&1.id == post_id))
end
@spec filter_page_posts(term(), term()) :: term()
def filter_page_posts(posts, query) do
normalized = query |> to_string() |> String.trim() |> String.downcase()
@@ -29,6 +36,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
end)
end
@spec category_options(term()) :: term()
def category_options(nil), do: []
def category_options(project_id) do
@@ -40,6 +48,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
end)
end
@spec filter_categories(term(), term()) :: term()
def filter_categories(categories, query) do
normalized = query |> to_string() |> String.trim() |> String.downcase()
@@ -50,7 +59,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
end)
end
@spec blank_to_nil(term()) :: term()
def blank_to_nil(nil), do: nil
def blank_to_nil(value) do
trimmed = String.trim(to_string(value))
if trimmed == "", do: nil, else: trimmed

View File

@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
alias BDS.Menu
alias BDS.Desktop.ShellLive.MenuEditor.{PageCategory, TreeOps, TreePredicates}
@spec ensure_state(term()) :: term()
def ensure_state(assigns) do
project_id = assigns.projects.active_project_id
@@ -16,11 +17,13 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
end
end
@spec update_state(term(), term()) :: term()
def update_state(socket, updater) do
state = ensure_state(socket.assigns)
assign(socket, :menu_editor_state, updater.(state))
end
@spec build(term(), term()) :: term()
def build(_assigns, state) do
categories = PageCategory.category_options(state.project_id)
draft = state.draft
@@ -35,7 +38,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
draft_query: draft_query,
filtered_pages:
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: []
),
filtered_categories:
@@ -53,6 +57,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
}
end
@spec save(term(), term(), term()) :: term()
def save(socket, reload, append_output) do
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))
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)
end
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
defp load_state(project_id) do

View File

@@ -3,12 +3,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
@home_item_id "menu-home"
@spec home_item_id() :: term()
def home_item_id, do: @home_item_id
@spec home_item() :: term()
def home_item do
%{item_id: @home_item_id, kind: :home, label: "Home", slug: nil, children: [], is_home: true}
end
@spec ui_item(term()) :: term()
def ui_item(%{kind: :home}), do: home_item()
def ui_item(item) do
@@ -24,25 +27,37 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
}
end
@spec persisted_item(term()) :: term()
def persisted_item(%{kind: :home}), do: %{kind: :home, label: "Home", slug: nil}
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
def persisted_item(item) do
%{kind: item.kind, label: item.label, slug: item.slug}
end
@spec first_item_id(term()) :: term()
def first_item_id([item | _rest]), do: item.item_id
def first_item_id([]), do: nil
@spec insert_target(term(), term()) :: term()
def insert_target(items, nil), do: {[], length(items)}
def insert_target(items, selected_id) do
case find_path(items, selected_id) do
nil -> {[], length(items)}
[] -> {[], length(items)}
nil ->
{[], length(items)}
[] ->
{[], length(items)}
path ->
case item_at_path(items, path) do
%{kind: :submenu} -> {path, 0}
@@ -51,9 +66,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec path_prefix?(term(), term()) :: term()
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
@spec find_path(term(), term(), term()) :: term()
def find_path(items, item_id, path \\ []) do
Enum.find_value(Enum.with_index(items), fn {item, index} ->
next_path = path ++ [index]
@@ -71,6 +89,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec item_at_path(term(), term()) :: term()
def item_at_path(_items, []), do: nil
def item_at_path(items, [index]) do
@@ -84,6 +103,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec items_at_path(term(), term()) :: term()
def items_at_path(items, []), do: items
def items_at_path(items, [index | rest]) do
@@ -93,6 +113,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
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, [index | rest], replacement) do
@@ -101,6 +122,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec update_item(term(), term(), term()) :: term()
def update_item(items, item_id, updater) do
Enum.map(items, fn item ->
cond do
@@ -111,6 +133,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec insert_item(term(), term(), term(), term()) :: term()
def insert_item(items, [], index, item) do
List.insert_at(items, index, item)
end
@@ -121,10 +144,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec remove_item(term(), term()) :: term()
def remove_item(items, item_id) do
remove_item_with_value(items, item_id) |> elem(0)
end
@spec remove_item_with_value(term(), term()) :: term()
def remove_item_with_value(items, item_id) do
Enum.reduce_while(Enum.with_index(items), {items, nil}, fn {item, index}, _acc ->
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)
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
{:cont, {items, nil}}
end
@@ -146,16 +172,23 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec append_child(term(), term(), term()) :: term()
def append_child(items, parent_item_id, child) do
update_item(items, parent_item_id, fn item ->
%{item | children: (item.children || []) ++ [child]}
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
nil -> state
[] -> state
nil ->
state
[] ->
state
path ->
parent_path = Enum.drop(path, -1)
index = List.last(path)
@@ -175,10 +208,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec indent_selected(term()) :: term()
def indent_selected(%{selected_id: selected_id} = state) do
case find_path(state.items, selected_id) do
nil -> state
[] -> state
nil ->
state
[] ->
state
path ->
parent_path = Enum.drop(path, -1)
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
%{kind: :submenu, item_id: sibling_id} ->
case remove_item_with_value(state.items, selected_id) do
{_next_items, nil} -> state
{_next_items, nil} ->
state
{next_items, removed_item} ->
%{
state
@@ -208,18 +248,27 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec unindent_selected(term()) :: term()
def unindent_selected(%{selected_id: selected_id} = state) do
case find_path(state.items, selected_id) do
nil -> state
[] -> state
[_root_index] -> state
nil ->
state
[] ->
state
[_root_index] ->
state
path ->
parent_path = Enum.drop(path, -1)
parent_index = List.last(parent_path)
grand_parent_path = Enum.drop(parent_path, -1)
case remove_item_with_value(state.items, selected_id) do
{_next_items, nil} -> state
{_next_items, nil} ->
state
{next_items, removed_item} ->
%{
state
@@ -229,6 +278,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec delete_selected(term()) :: term()
def delete_selected(%{selected_id: @home_item_id} = state), do: state
def delete_selected(%{selected_id: selected_id} = state) do
@@ -241,9 +291,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
state
end
def drop_selected(state, drag_item_id, target_item_id, _position) when drag_item_id == target_item_id,
do: state
def drop_selected(state, drag_item_id, target_item_id, _position)
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
drag_path = find_path(state.items, drag_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
case item_at_path(next_items, target_path) do
%{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 ->
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
parent_path = Enum.drop(target_path, -1)
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
defp insert_dropped_item(state, next_items, dragged_item, target_path, _position) do
parent_path = Enum.drop(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

View File

@@ -3,6 +3,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
@spec can_move_up?(term(), term()) :: term()
def can_move_up?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
[_parent, index] -> index > 0
@@ -12,9 +13,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
end
end
@spec can_move_down?(term(), term()) :: term()
def can_move_down?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
nil -> false
nil ->
false
path ->
parent_path = Enum.drop(path, -1)
index = List.last(path)
@@ -22,10 +26,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
end
end
@spec can_indent?(term(), term()) :: term()
def can_indent?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
nil -> false
[] -> false
nil ->
false
[] ->
false
[_index] = path ->
index = List.last(path)
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 > 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
@spec can_unindent?(term(), term()) :: term()
def can_unindent?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
[_index] -> false
@@ -46,9 +59,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
end
end
@spec can_delete?(term()) :: term()
def can_delete?(selected_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
match?(%{item_id: ^item_id}, menu_editor.draft)
end

View File

@@ -20,10 +20,12 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
:git_diff
]
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
assign(socket, :misc_editor, build(socket.assigns))
end
@spec rerun(term()) :: term()
def rerun(socket) do
case meta(socket.assigns) do
%{action: action} when is_binary(action) ->
@@ -37,6 +39,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec apply_site_validation(term(), term()) :: term()
def apply_site_validation(socket, append_output) do
meta = meta(socket.assigns)
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")}
end
@spec toggle_duplicate(term(), term(), term()) :: term()
def toggle_duplicate(socket, pair_id, reload) do
selected_by_tab = Map.get(socket.assigns, :misc_editor_selected_pairs, %{})
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)
end
@spec dismiss_duplicate(term(), term(), term(), term(), term()) :: term()
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
{:ok, _saved_pair} ->
@@ -109,6 +114,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec dismiss_selected(term(), term(), term()) :: term()
def dismiss_selected(socket, reload, append_output) do
tab_id = socket.assigns.current_tab.id
@@ -141,6 +147,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec fix_translation_validation(term(), term()) :: term()
def fix_translation_validation(socket, append_output) do
report =
socket.assigns
@@ -166,6 +173,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
append_output.(socket, translated("Translation Validation"), inspect(error), nil, "error")}
end
@spec select_git_diff_file(term(), term()) :: term()
def select_git_diff_file(socket, file_path) do
assign(
socket,
@@ -178,6 +186,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
)
end
@spec metadata_diff_repair_request(term(), term(), term()) :: term()
def metadata_diff_repair_request(socket, field, direction) do
meta = meta(socket.assigns)
payload = Map.get(meta, :payload, %{})
@@ -209,6 +218,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec metadata_diff_orphan_import_request(term()) :: term()
def metadata_diff_orphan_import_request(socket) do
meta = meta(socket.assigns)
payload = Map.get(meta, :payload, %{})
@@ -232,6 +242,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec build(term()) :: term()
def build(%{current_tab: %{type: type}} = assigns) when type in @misc_routes do
meta = meta(assigns)
payload = Map.get(meta, :payload, %{})
@@ -245,11 +256,14 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec build(term()) :: term()
def build(_assigns), do: nil
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
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(:metadata_diff), do: "metadata-diff-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 summary_items(%{summary: summary}) when is_map(summary), do: Enum.to_list(summary)
@spec summary_items(term()) :: term()
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)
@spec pair_id_from_pair(term()) :: term()
def pair_id_from_pair(pair), do: pair_identity(pair)
defp build_site_validation(meta, payload) do
@@ -410,6 +427,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
}
end
@spec translation_issue_label(term()) :: term()
def translation_issue_label(issue) do
case issue_value(issue, :issue) do
"same-language-as-canonical" ->
@@ -426,6 +444,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec translation_issue_languages(term()) :: term()
def translation_issue_languages(issue) do
canonical_language = issue_value(issue, :canonical_language)
translation_language = issue_value(issue, :translation_language)
@@ -440,8 +459,10 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end
end
@spec translation_issue_value(term(), term()) :: term()
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(file_path) do

View File

@@ -12,7 +12,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
alias BDS.Posts.{Post, PostMedia, Translation}
alias BDS.Tags.Tag
embed_templates "overlay_html/*"
embed_templates("overlay_html/*")
def context(assigns, tab_title, tab_subtitle) do
project_id = assigns.projects.active_project_id
@@ -23,7 +23,12 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
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_media_language: source_language(current_tab, metadata),
posts: posts,
@@ -59,7 +64,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
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: []}
@@ -77,7 +83,15 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
from post in Post,
where: post.project_id == ^project_id,
order_by: [desc: post.updated_at, desc: post.created_at],
select: %{id: post.id, title: post.title, slug: post.slug, status: post.status, 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 ->
%{
@@ -96,7 +110,14 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
from media in MediaRecord,
where: media.project_id == ^project_id,
order_by: [desc: media.updated_at, desc: media.created_at],
select: %{id: media.id, title: media.title, original_name: media.original_name, mime_type: media.mime_type, 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 ->
%{
@@ -149,7 +170,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
defp existing_translations(_tab), 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.uniq()
end
@@ -193,9 +215,27 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
case Posts.get_post(post_id) do
%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: "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}
%{
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: "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 ->
@@ -209,9 +249,27 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
case Media.get_media(media_id) do
%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: "caption", label: ShellData.translate("Caption", %{}, page_language), current_value: media.caption || "", suggested_value: refine_excerpt(title, media.caption || title), locked: false}
%{
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: "caption",
label: ShellData.translate("Caption", %{}, page_language),
current_value: media.caption || "",
suggested_value: refine_excerpt(title, media.caption || title),
locked: false
}
]
_other ->
@@ -248,7 +306,13 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
reference_list: reference_list
}
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
defp delete_details(%{type: :tags}, page_language) do
@@ -263,16 +327,33 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
reference_list: []
}
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
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
defp merge_details(project_id, page_language) do
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"
@@ -283,7 +364,13 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
message: ShellData.translate("Cannot be undone.", %{}, page_language)
}
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
defp canonical_post_url(post) do
@@ -302,7 +389,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
if base == "", do: "#{title} overview", else: base <> "."
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
value

View File

@@ -210,8 +210,15 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
defp related_posts(links, key) do
Enum.map(links, fn link ->
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}
_other -> nil
%Post{} = post ->
%{
id: post.id,
title: post.title || post.slug || post.id,
text: link.link_text || post.slug || post.id
}
_other ->
nil
end
end)
|> 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
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}
_other -> nil
%Post{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] ->
{project_id, file_path}
_other ->
nil
end
end
defp git_history_target(%{type: :media, id: 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}
_other -> nil
%MediaRecord{project_id: project_id, file_path: file_path}
when file_path not in [nil, ""] ->
{project_id, file_path}
_other ->
nil
end
end
@@ -287,5 +301,6 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
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

View File

@@ -74,13 +74,21 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
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
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))
end
@spec update(term(), term(), term()) :: term()
def update(socket, params, reload) do
case socket.assigns.current_tab do
%{type: :post, id: post_id} ->
@@ -91,7 +99,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = project_metadata(post.project_id)
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)
next_language =
@@ -117,6 +128,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec persist_socket(term(), term(), term(), term(), term()) :: term()
def persist_socket(socket, post_id, action, reload, append_output) do
case Posts.get_post(post_id) do
nil ->
@@ -125,7 +137,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = project_metadata(post.project_id)
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)
case persist(post, draft, active_language, metadata, action) do
@@ -135,9 +150,30 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
socket
|> assign(:workbench, workbench)
|> assign(:post_editor_drafts, 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))}))
|> assign(
:post_editor_drafts,
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)
{:error, reason} ->
@@ -148,6 +184,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec discard_socket(term(), term(), term(), term()) :: term()
def discard_socket(socket, post_id, reload, append_output) do
case Posts.get_post(post_id) do
nil ->
@@ -156,7 +193,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = project_metadata(post.project_id)
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
{:ok, restored_post} ->
@@ -164,9 +203,21 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
socket
|> assign(:workbench, workbench)
|> assign(:post_editor_drafts, 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)}))
|> assign(
:post_editor_drafts,
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)
{:error, reason} ->
@@ -177,6 +228,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec delete_socket(term(), term(), term(), term()) :: term()
def delete_socket(socket, post_id, reload, append_output) do
case Posts.delete_post(post_id) do
{:ok, :deleted} ->
@@ -185,13 +237,28 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
socket
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, post_id}))
|> assign(:post_editor_drafts, Map.delete(socket.assigns.post_editor_drafts, post_id))
|> assign(:post_editor_active_languages, Map.delete(socket.assigns.post_editor_active_languages, post_id))
|> assign(:post_editor_tag_queries, Map.delete(socket.assigns.post_editor_tag_queries, post_id))
|> assign(:post_editor_category_queries, Map.delete(socket.assigns.post_editor_category_queries, post_id))
|> assign(:post_editor_quick_actions_open, Map.delete(socket.assigns.post_editor_quick_actions_open, post_id))
|> assign(
:post_editor_active_languages,
Map.delete(socket.assigns.post_editor_active_languages, post_id)
)
|> assign(
:post_editor_tag_queries,
Map.delete(socket.assigns.post_editor_tag_queries, post_id)
)
|> assign(
:post_editor_category_queries,
Map.delete(socket.assigns.post_editor_category_queries, post_id)
)
|> assign(
:post_editor_quick_actions_open,
Map.delete(socket.assigns.post_editor_quick_actions_open, post_id)
)
|> assign(:post_editor_modes, Map.delete(socket.assigns.post_editor_modes, post_id))
|> assign(:post_editor_expanded, Map.delete(socket.assigns.post_editor_expanded, post_id))
|> assign(:post_editor_save_states, Map.delete(socket.assigns.post_editor_save_states, post_id))
|> assign(
:post_editor_save_states,
Map.delete(socket.assigns.post_editor_save_states, post_id)
)
|> reload.(workbench)
{:error, reason} ->
@@ -201,6 +268,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec set_mode(term(), term(), term(), term()) :: term()
def set_mode(socket, post_id, mode, reload) do
workbench = socket.assigns.workbench
normalized_mode = normalize_mode(mode)
@@ -216,38 +284,67 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
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)
end
@spec toggle_section(term(), term(), term(), term()) :: term()
def toggle_section(socket, post_id, section, reload) when section in [:metadata, :excerpt] do
workbench = socket.assigns.workbench
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)
end
@spec select_language(term(), term(), term(), term()) :: term()
def select_language(socket, post_id, language, reload) do
workbench = socket.assigns.workbench
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)
end
@spec toggle_quick_actions(term(), term(), term()) :: term()
def toggle_quick_actions(socket, post_id, reload) do
workbench = socket.assigns.workbench
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)
end
@spec detect_language(term(), term(), term(), term()) :: term()
def detect_language(socket, post_id, reload, append_output) do
if Map.get(socket.assigns, :offline_mode, true) do
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)
else
case Posts.get_post(post_id) do
@@ -257,14 +354,24 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = project_metadata(post.project_id)
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)
text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "content", "")], "\n\n")
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
|> 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)
{:error, reason} ->
@@ -274,17 +381,28 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
_other ->
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)
end
end
end
end
@spec translate(term(), term(), term(), term(), term()) :: term()
def translate(socket, post_id, language, reload, append_output) do
if Map.get(socket.assigns, :offline_mode, true) do
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)
else
normalized_language = normalize_language(language, "")
@@ -298,9 +416,18 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
content: translation.content
}) do
socket
|> assign(:post_editor_active_languages, 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))
|> assign(
:post_editor_active_languages,
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)
else
{:error, reason} ->
@@ -317,6 +444,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec apply_ai_suggestions(term(), term(), term(), term(), term()) :: term()
def apply_ai_suggestions(socket, post_id, fields, reload, append_output) do
case Posts.get_post(post_id) do
nil ->
@@ -340,12 +468,30 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
case Posts.update_post(post_id, attrs) do
{:ok, updated_post} ->
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)
socket
|> assign(: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(
: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)
|> reload.(socket.assigns.workbench)
@@ -358,6 +504,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec insert_content(term(), term(), term(), term()) :: term()
def insert_content(socket, post_id, snippet, reload) do
socket
|> 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)
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
case Posts.get_post(post_id) do
nil ->
@@ -373,7 +521,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = project_metadata(post.project_id)
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)
normalized = normalize_list_entry(value)
@@ -398,6 +549,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
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
case Posts.get_post(post_id) do
nil ->
@@ -406,9 +558,18 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = project_metadata(post.project_id)
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)
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
|> put_draft_field(post_id, post, active_language, field_key(kind), updated)
@@ -416,6 +577,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end
end
@spec build(term()) :: term()
def build(%{current_tab: %{type: :post, id: post_id}} = assigns) do
case Posts.get_post(post_id) do
nil ->
@@ -424,7 +586,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
%Post{} = post ->
metadata = assigned_project_metadata(assigns)
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)
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),
excerpt_expanded: Map.get(expanded, :excerpt, false),
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_delete?: post.status == :published,
has_published_version?: has_published_version?(post),
discard_label: discard_label(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)),
languages: languages(metadata),
form: form,
@@ -469,16 +636,45 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
tag_values: tag_values(form),
tag_chips: tag_chips(form, Tags.list_tags(post.project_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_query: query_value(assigns, :categories, post.id),
category_options: metadata.categories || [],
category_query_addable?: query_addable?(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)),
category_query_addable?:
query_addable?(
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),
preview_url: 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),
preview_url:
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),
post_links: post_links(post.id),
footer: footer(post, current_translation, active_language, canonical_language)
@@ -488,17 +684,21 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
def build(_assigns), do: nil
@spec post_status_label(term()) :: term()
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(:saved), do: translated("Saved")
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(_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(:preview), do: translated("Preview")
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())

View File

@@ -8,11 +8,14 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
alias BDS.Desktop.ShellLive.PostEditor.PostMetadata
alias BDS.UI.Workbench
@spec normalize_mode(term()) :: term()
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("preview"), do: :preview
def normalize_mode(_mode), do: :markdown
@spec normalize_language(term(), term()) :: term()
def normalize_language(value, fallback) do
case value |> to_string() |> String.trim() do
"" -> fallback
@@ -20,6 +23,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
end
end
@spec normalize_params(term(), term(), term()) :: term()
def normalize_params(params, current_language, next_language) do
%{
"title" => Map.get(params, "title", ""),
@@ -28,12 +32,17 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
"tags" => Map.get(params, "tags", ""),
"categories" => Map.get(params, "categories", ""),
"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")),
"template_slug" => Map.get(params, "template_slug", "")
}
end
@spec current_draft(term(), term(), term(), term()) :: term()
def current_draft(assigns, %Post{} = post, metadata, active_language) do
persisted = persisted_form(post, metadata, active_language)
@@ -42,10 +51,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
|> Map.get(active_language, persisted)
end
@spec persisted_form(term(), term(), term()) :: term()
def persisted_form(%Post{} = post, metadata, active_language) do
persisted_form(post, metadata, active_language, PostMetadata.translations(post.id))
end
@spec persisted_form(term(), term(), term(), term()) :: term()
def persisted_form(post, metadata, active_language, translations) do
canonical_language = PostMetadata.canonical_language(post, metadata)
translation = Map.get(translations, active_language)
@@ -64,8 +75,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
}
else
%{
"title" => translation && translation.title || "",
"excerpt" => translation && translation.excerpt || "",
"title" => (translation && translation.title) || "",
"excerpt" => (translation && translation.excerpt) || "",
"content" => if(translation, do: Posts.editor_body(translation), else: ""),
"tags" => Enum.join(post.tags || [], ", "),
"categories" => Enum.join(post.categories || [], ", "),
@@ -77,22 +88,43 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
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
workbench = Workbench.mark_dirty(socket.assigns.workbench, :post, post_id)
socket
|> assign(:workbench, workbench)
|> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, next_language, 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)}))
|> assign(
:post_editor_drafts,
put_nested_map(socket.assigns.post_editor_drafts, post_id, next_language, 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)
end
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
@spec put_draft_field(term(), term(), term(), term(), term(), term()) :: term()
def put_draft_field(socket, post_id, post, active_language, field, value) do
metadata = PostMetadata.project_metadata(post.project_id)
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
|> assign(:workbench, workbench)
|> assign(: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))
|> assign(
: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
@spec put_query_state(term(), term(), term(), term()) :: term()
def put_query_state(socket, post_id, kind, value) do
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
@spec query_value(term(), term(), term()) :: term()
def query_value(assigns, kind, post_id) do
assigns
|> 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(:categories), do: :post_editor_category_queries
defp maybe_drop_old_language_draft(socket, _post_id, current_language, next_language) when current_language == next_language,
do: socket
defp maybe_drop_old_language_draft(socket, _post_id, current_language, next_language)
when current_language == next_language,
do: socket
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
@spec toggled_sections(term(), term(), term()) :: term()
def toggled_sections(expanded_by_post, post_id, section) do
expanded_by_post
|> Map.get(post_id, %{metadata: false, excerpt: false})
|> Map.put_new(:metadata, false)
|> Map.put_new(:excerpt, false)
|> Map.update!(section, &not &1)
|> Map.update!(section, &(not &1))
end
@spec put_nested_map(term(), term(), term(), term()) :: term()
def put_nested_map(map, key, nested_key, value) do
Map.update(map, key, %{nested_key => value}, &Map.put(&1, nested_key, value))
end
@spec delete_nested_map(term(), term(), term()) :: term()
def delete_nested_map(map, key, nested_key) do
case Map.get(map, key) do
nil ->
@@ -150,20 +203,26 @@ defmodule BDS.Desktop.ShellLive.PostEditor.DraftManagement do
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(_action), do: :saved
@spec record_title(term(), term()) :: term()
def record_title(%Translation{title: title}, post),
do: blank_to_nil(title) || post.title || post.slug || post.id
def record_title(%Post{title: title, slug: slug, id: id}, _post),
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(%Post{status: status}), do: status || :draft
@spec editing_canonical_language?(term(), term(), term()) :: term()
def editing_canonical_language?(translations, active_language, canonical_language) do
active_language == canonical_language or not Map.has_key?(translations, active_language)
end

View File

@@ -3,17 +3,22 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
alias BDS.{Metadata, Tags}
@spec field_key(term()) :: term()
def field_key(:tags), do: "tags"
def field_key(:categories), do: "categories"
@spec tag_values(term()) :: term()
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", ""))
@spec tag_suggestions(term(), term(), term()) :: term()
def tag_suggestions(form, options, query) do
selected = MapSet.new(tag_values(form))
filter_suggestions(options, query, fn option -> option.name end, selected)
end
@spec tag_chips(term(), term()) :: term()
def tag_chips(form, options) do
option_map = Map.new(options, fn option -> {option.name, option} end)
@@ -23,6 +28,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
end)
end
@spec category_suggestions(term(), term(), term()) :: term()
def category_suggestions(form, options, query) do
selected = MapSet.new(category_values(form))
filter_suggestions(options, query, & &1, selected)
@@ -34,11 +40,14 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
options
|> Enum.filter(fn 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)
|> Enum.take(8)
end
@spec query_addable?(term(), term(), term(), term()) :: term()
def query_addable?(query, selected_values, options, labeler) do
normalized = normalize_query(query)
@@ -54,6 +63,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|> String.downcase()
end
@spec normalize_list_entry(term()) :: term()
def normalize_list_entry(value) do
value
|> to_string()
@@ -61,6 +71,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|> String.downcase()
end
@spec ensure_list_value(term(), term(), term()) :: term()
def ensure_list_value(project_id, :tags, value) do
if Enum.any?(Tags.list_tags(project_id), &(String.downcase(&1.name) == value)) do
:ok
@@ -83,6 +94,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
_error -> :ok
end
@spec csv_to_list(term()) :: term()
def csv_to_list(value) do
value
|> to_string()
@@ -91,6 +103,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
|> Enum.reject(&(&1 == ""))
end
@spec tag_chip_style(term()) :: term()
def tag_chip_style(nil), do: nil
def tag_chip_style(color) do
@@ -121,5 +134,6 @@ defmodule BDS.Desktop.ShellLive.PostEditor.ListValues do
defp contrast_color(_color), do: "#ffffff"
@spec ai_overlay_fields(term()) :: term()
def ai_overlay_fields(selected), do: Enum.filter(selected, & &1.accepted)
end

View File

@@ -6,11 +6,16 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
alias BDS.Desktop.ShellData
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
canonical_language = PostMetadata.canonical_language(post, metadata)
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
|> save_canonical_draft(draft)
|> maybe_publish_post(post.id, action)
@@ -21,12 +26,17 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
end
end
@spec discard(term(), term(), term()) :: term()
def discard(%Post{} = post, active_language, metadata) do
canonical_language = PostMetadata.canonical_language(post, metadata)
current_translations = PostMetadata.translations(post.id)
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}
post.file_path not in [nil, ""] and post.status == :draft ->
@@ -37,15 +47,18 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
end
end
@spec has_published_version?(term()) :: term()
def has_published_version?(%Post{} = post),
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
if has_published_version?(post),
do: translated("Discard Changes"),
else: translated("Discard Draft")
end
@spec discard_title(term()) :: term()
def discard_title(%Post{} = post) do
if has_published_version?(post),
do: translated("Discard changes and restore the published version"),

View File

@@ -8,6 +8,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
alias BDS.Media.Media, as: MediaRecord
alias BDS.Posts.{Post, PostMedia}
@spec project_metadata(term()) :: term()
def project_metadata(nil), do: %{main_language: "en", blog_languages: []}
def project_metadata(project_id) do
@@ -17,6 +18,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
_error -> %{main_language: "en", blog_languages: []}
end
@spec canonical_language(term(), term()) :: term()
def canonical_language(post, metadata) do
BDS.Desktop.ShellLive.PostEditor.DraftManagement.normalize_language(
post.language,
@@ -24,28 +26,36 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
)
end
@spec translations(term()) :: term()
def translations(post_id) do
{:ok, translations} = Posts.list_post_translations(post_id)
Map.new(translations, fn translation -> {translation.language, translation} end)
end
@spec languages(term()) :: term()
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.uniq()
end
@spec template_options(term()) :: term()
def template_options(project_id) do
Repo.all(
from template in Templates.Template,
where: template.project_id == ^project_id,
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
_error -> []
end
@spec linked_media(term()) :: term()
def linked_media(post_id) do
rows =
Repo.all(
@@ -74,6 +84,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
_error -> []
end
@spec post_links(term()) :: term()
def post_links(post_id) do
%{
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
Enum.map(links, fn link ->
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}
_other -> nil
%Post{} = post ->
%{
id: post.id,
title: post.title || post.slug || post.id,
text: link.link_text || post.slug || post.id
}
_other ->
nil
end
end)
|> Enum.reject(&is_nil/1)
end
@spec translation_flags(term(), term(), term(), term()) :: term()
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 =
translations
@@ -111,6 +136,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
[canonical | others]
end
@spec footer(term(), term(), term(), term()) :: term()
def footer(post, translation, active_language, canonical_language) do
if active_language == canonical_language do
%{
@@ -120,8 +146,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
}
else
%{
created_at: format_timestamp(translation && translation.created_at || post.created_at),
updated_at: format_timestamp(translation && translation.updated_at || post.updated_at),
created_at: format_timestamp((translation && translation.created_at) || post.created_at),
updated_at: format_timestamp((translation && translation.updated_at) || post.updated_at),
published_at: format_timestamp(translation && translation.published_at)
}
end
@@ -135,10 +161,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|> Calendar.strftime("%x")
end
@spec display_title(term(), term(), term()) :: term()
def display_title(title, slug, fallback_id) do
blank_to_nil(title) || blank_to_nil(slug) || fallback_id || translated("Untitled")
end
@spec gallery_count(term()) :: term()
def gallery_count(form) do
form
|> Map.get("content", "")
@@ -147,8 +175,11 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|> length()
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
query =
%{}
@@ -156,7 +187,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|> maybe_put_query("post_id", post.id)
|> 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
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)
def truthy?(value) when value in [true, "true", "on", 1, "1"], do: true
@spec truthy?(term()) :: term()
def truthy?(_value), do: false
@spec blank?(term()) :: term()
def blank?(value), do: blank_to_nil(value) == nil
@spec blank_to_nil(term()) :: term()
def blank_to_nil(value) do
value
|> to_string()

View File

@@ -19,7 +19,9 @@ defmodule BDS.Desktop.ShellLive.SessionUtil do
Stream.iterate(1, &(&1 + 1))
|> Enum.find_value(fn index ->
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
end)

View File

@@ -17,7 +17,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings
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)
@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 protected_category?(category), to: ManagedCategories
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
case socket.assigns[:current_tab] do
%{type: :settings} ->
@@ -64,12 +65,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
end
end
@spec update_search(term(), term(), term()) :: term()
def update_search(socket, query, reload) do
socket
|> assign(:settings_editor_search, to_string(query || ""))
|> reload.(socket.assigns.workbench)
end
@spec build_settings(term()) :: term()
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
def build_settings(assigns) do
@@ -82,7 +85,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
)
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 =
Map.merge(AISettings.ai_form(assigns), Map.get(assigns, :settings_editor_ai_draft, %{}))
@@ -142,6 +148,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
}
end
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
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 ->
case section do
"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" ->
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))
"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

View File

@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
@spec ai_form(term()) :: term()
def ai_form(assigns) do
{:ok, online_endpoint} = AI.get_endpoint(:online)
{:ok, airplane_endpoint} = AI.get_endpoint(:airplane)
@@ -30,18 +31,21 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
}
end
@spec endpoint_model_options(term(), term()) :: term()
def endpoint_model_options(assigns, endpoint_key) do
assigns
|> Map.get(:settings_editor_endpoint_models, %{})
|> Map.get(endpoint_key, [])
end
@spec update_ai_draft(term(), term(), term()) :: term()
def update_ai_draft(socket, params, reload) do
socket
|> assign(:settings_editor_ai_draft, normalize_ai_params(params))
|> reload.(socket.assigns.workbench)
end
@spec refresh_ai_models(term(), term(), term(), term()) :: term()
def refresh_ai_models(socket, endpoint_key, reload, append_output) do
attrs = ai_attrs(socket.assigns)
@@ -65,11 +69,17 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
end
end
@spec save_ai(term(), term(), term()) :: term()
def save_ai(socket, reload, append_output) do
attrs = ai_attrs(socket.assigns)
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 <-
put_endpoint_preferences(
: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_title, attrs.offline_title_model),
: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
socket
|> assign(:settings_editor_ai_draft, %{})
@@ -99,6 +112,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
end
end
@spec reset_ai_prompt(term(), term(), term()) :: term()
def reset_ai_prompt(socket, reload, append_output) do
case EditorSettings.put_global_setting("ai.system_prompt", "") do
:ok ->

View File

@@ -6,21 +6,25 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
alias BDS.Settings
alias BDS.Desktop.ShellData
@spec editor_form() :: term()
def editor_form do
%{
"default_mode" => get_global_setting("ui.preferred_editor_mode") || "markdown",
"diff_view_style" => get_global_setting("ui.git_diff_view_style") || "inline",
"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
@spec update_editor_draft(term(), term(), term()) :: term()
def update_editor_draft(socket, params, reload) do
socket
|> assign(:settings_editor_editor_draft, normalize_editor_params(params))
|> reload.(socket.assigns.workbench)
end
@spec save_editor(term(), term(), term()) :: term()
def save_editor(socket, reload, append_output) do
attrs = editor_attrs(socket.assigns)
@@ -43,10 +47,12 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
end
end
@spec get_global_setting(term()) :: term()
def get_global_setting(key) do
Settings.get_global_setting(key)
end
@spec put_global_setting(term(), term()) :: term()
def put_global_setting(key, value) do
Settings.put_global_setting(key, value)
end

View File

@@ -14,10 +14,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
"page" => %{title: "page", render_in_lists: false, show_title: true}
}
@spec protected_categories() :: term()
def protected_categories, do: @protected_categories
@spec protected_category?(term()) :: term()
def protected_category?(category), do: MapSet.member?(@protected_categories, category)
@spec category_rows(term()) :: term()
def category_rows(metadata) do
categories = Map.get(metadata, :categories, [])
settings = Map.get(metadata, :category_settings, %{})
@@ -37,12 +40,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
end)
end
@spec update_new_category(term(), term(), term()) :: term()
def update_new_category(socket, name, reload) do
socket
|> assign(:settings_editor_new_category, to_string(name || ""))
|> reload.(socket.assigns.workbench)
end
@spec add_category(term(), term(), term()) :: term()
def add_category(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id
name = socket.assigns[:settings_editor_new_category] |> to_string() |> String.trim()
@@ -73,11 +78,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
end
end
@spec reset_categories(term(), term(), term()) :: term()
def reset_categories(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id
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
{:cont, :ok}
else
@@ -102,6 +109,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
end
end
@spec save_category(term(), term(), term(), term()) :: term()
def save_category(socket, params, reload, append_output) do
project_id = socket.assigns.projects.active_project_id
category = Map.get(params, "category", "")
@@ -125,6 +133,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do
end
end
@spec remove_category(term(), term(), term(), term()) :: term()
def remove_category(socket, category, reload, append_output) do
project_id = socket.assigns.projects.active_project_id

View File

@@ -16,6 +16,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
%{id: :openai_codex, label: "OpenAI Codex", supported?: false}
]
@spec mcp_rows() :: term()
def mcp_rows do
Enum.map(@mcp_agents, fn agent ->
%{
@@ -28,6 +29,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
end)
end
@spec toggle_mcp_agent(term(), term(), term(), term()) :: term()
def toggle_mcp_agent(socket, agent, reload, append_output) do
case find_mcp_agent(agent) do
%{id: agent_id, supported?: true} = config ->

View File

@@ -6,12 +6,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
alias BDS.Metadata
alias BDS.Desktop.ShellData
@spec project_metadata(term()) :: term()
def project_metadata(assigns) do
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
{:ok, metadata} -> metadata
end
end
@spec project_form(term()) :: term()
def project_form(metadata) do
%{
"name" => Map.get(metadata, :name, ""),
@@ -28,18 +30,21 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
}
end
@spec technology_form(term()) :: term()
def technology_form(project_form) do
%{
"semantic_similarity_enabled" => Map.get(project_form, "semantic_similarity_enabled", false)
}
end
@spec update_project_draft(term(), term(), term()) :: term()
def update_project_draft(socket, params, reload) do
socket
|> assign(:settings_editor_project_draft, normalize_project_params(params))
|> reload.(socket.assigns.workbench)
end
@spec save_project(term(), term(), term()) :: term()
def save_project(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
alias BDS.Metadata
alias BDS.Desktop.ShellData
@spec publishing_form(term()) :: term()
def publishing_form(metadata) do
prefs = Map.get(metadata, :publishing_preferences, %{})
@@ -17,12 +18,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
}
end
@spec update_publishing_draft(term(), term(), term()) :: term()
def update_publishing_draft(socket, params, reload) do
socket
|> assign(:settings_editor_publishing_draft, normalize_publishing_params(params))
|> reload.(socket.assigns.workbench)
end
@spec save_publishing(term(), term(), term()) :: term()
def save_publishing(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id
@@ -39,6 +42,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do
end
end
@spec clear_publishing(term(), term(), term()) :: term()
def clear_publishing(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id

View File

@@ -29,6 +29,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
"zinc"
]
@spec build_style(term()) :: term()
def build_style(%{projects: %{active_project_id: nil}}), do: nil
def build_style(assigns) do
@@ -40,22 +41,26 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
selected_theme: selected_theme,
applied_theme: current_theme(assigns),
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
@spec select_style_theme(term(), term(), term()) :: term()
def select_style_theme(socket, theme, reload) do
socket
|> assign(:style_editor_theme, to_string(theme || "default"))
|> reload.(socket.assigns.workbench)
end
@spec change_style_preview_mode(term(), term(), term()) :: term()
def change_style_preview_mode(socket, mode, reload) do
socket
|> assign(:style_editor_preview_mode, to_string(mode || "auto"))
|> reload.(socket.assigns.workbench)
end
@spec apply_style_theme(term(), term(), term()) :: term()
def apply_style_theme(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id
theme = socket.assigns[:style_editor_theme] || current_theme(socket.assigns)
@@ -71,6 +76,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
end
end
@spec theme_display_name(term()) :: term()
def theme_display_name(theme) do
theme
|> to_string()
@@ -78,6 +84,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do
|> String.capitalize()
end
@spec current_theme(term()) :: term()
def current_theme(assigns) do
case Metadata.get_project_metadata(assigns.projects.active_project_id) do
{:ok, metadata} ->

View File

@@ -22,7 +22,13 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
end
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} ->
callbacks.reload.(socket, socket.assigns.workbench)
@@ -42,7 +48,12 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
{:error, reason} ->
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)
end
@@ -68,13 +79,23 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
{:ok, script} ->
callbacks.open_sidebar.(
socket,
%{"route" => "scripts", "id" => script.id, "title" => script.title, "subtitle" => "Automation helpers"},
%{
"route" => "scripts",
"id" => script.id,
"title" => script.title,
"subtitle" => "Automation helpers"
},
:pin
)
{:error, reason} ->
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)
end
end
@@ -90,29 +111,52 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
{:ok, template} ->
callbacks.open_sidebar.(
socket,
%{"route" => "templates", "id" => template.id, "title" => template.title, "subtitle" => "Site rendering"},
%{
"route" => "templates",
"id" => template.id,
"title" => template.title,
"subtitle" => "Site rendering"
},
:pin
)
{:error, reason} ->
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)
end
end
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} ->
callbacks.open_sidebar.(
socket,
%{"route" => "import", "id" => definition.id, "title" => definition.name, "subtitle" => "Import definitions"},
%{
"route" => "import",
"id" => definition.id,
"title" => definition.name,
"subtitle" => "Import definitions"
},
:pin
)
{:error, reason} ->
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)
end
end

View File

@@ -7,13 +7,17 @@ defmodule BDS.Desktop.ShellLive.SidebarState do
if is_map(filters) and Map.get(filters, :enabled) do
panel_state = filter_panel_state(socket, view_id)
Map.put(sidebar_data, :filters, Map.merge(filters, %{
filter_panel_visible: panel_state.visible,
archive_collapsed: panel_state.archive_collapsed,
tags_collapsed: panel_state.tags_collapsed,
categories_collapsed: panel_state.categories_collapsed,
expanded_year: panel_state.expanded_year
}))
Map.put(
sidebar_data,
:filters,
Map.merge(filters, %{
filter_panel_visible: panel_state.visible,
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
sidebar_data
end
@@ -22,7 +26,12 @@ defmodule BDS.Desktop.ShellLive.SidebarState do
def put_filter_panel_state(socket, updater) do
view_id = Atom.to_string(socket.assigns.workbench.active_view)
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
def current_filters(socket, view_id) do
@@ -33,8 +42,17 @@ defmodule BDS.Desktop.ShellLive.SidebarState do
def put_filters(socket, updater) do
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
def toggle_filter_value(filters, key, value) do

View File

@@ -11,12 +11,14 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
alias BDS.Tags.Tag
alias BDS.Templates.Template
embed_templates "tags_editor_html/*"
embed_templates("tags_editor_html/*")
@spec assign_socket(term()) :: term()
def assign_socket(socket) do
assign(socket, :tags_editor, build(socket.assigns))
end
@spec toggle_selection(term(), term(), term()) :: term()
def toggle_selection(socket, tag_name, reload) do
selected = Map.get(socket.assigns, :tags_editor_selected, [])
@@ -33,6 +35,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|> reload.(socket.assigns.workbench)
end
@spec update_new_tag(term(), term(), term()) :: term()
def update_new_tag(socket, params, reload) do
socket
|> assign(:tags_editor_new_tag, %{
@@ -42,11 +45,16 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|> reload.(socket.assigns.workbench)
end
@spec create_tag(term(), term(), term()) :: term()
def create_tag(socket, reload, append_output) do
project_id = socket.assigns.projects.active_project_id
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} ->
socket
|> assign(:tags_editor_new_tag, %{"name" => "", "color" => ""})
@@ -59,6 +67,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end
end
@spec update_edit_tag(term(), term(), term()) :: term()
def update_edit_tag(socket, params, reload) do
socket
|> assign(:tags_editor_edit_draft, %{
@@ -69,16 +78,26 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|> reload.(socket.assigns.workbench)
end
@spec save_tag(term(), term(), term()) :: term()
def save_tag(socket, reload, append_output) do
selected = Map.get(socket.assigns, :tags_editor_selected, [])
draft = Map.get(socket.assigns, :tags_editor_edit_draft, %{})
case selected do
[tag_name] ->
case Repo.get_by(Tag, project_id: socket.assigns.projects.active_project_id, name: tag_name) do
nil -> reload.(socket, socket.assigns.workbench)
case Repo.get_by(Tag,
project_id: socket.assigns.projects.active_project_id,
name: tag_name
) do
nil ->
reload.(socket, socket.assigns.workbench)
%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
socket
|> assign(:tags_editor_selected, [renamed_tag.name])
@@ -92,15 +111,22 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end
end
_other -> reload.(socket, socket.assigns.workbench)
_other ->
reload.(socket, socket.assigns.workbench)
end
end
@spec delete_selected(term(), term(), term()) :: term()
def delete_selected(socket, reload, append_output) do
case Map.get(socket.assigns, :tags_editor_selected, []) do
[tag_name] ->
case Repo.get_by(Tag, project_id: socket.assigns.projects.active_project_id, name: tag_name) do
nil -> reload.(socket, socket.assigns.workbench)
case Repo.get_by(Tag,
project_id: socket.assigns.projects.active_project_id,
name: tag_name
) do
nil ->
reload.(socket, socket.assigns.workbench)
%Tag{} = tag ->
case Tags.delete_tag(tag.id) do
{:ok, _deleted} ->
@@ -116,16 +142,19 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end
end
_other -> reload.(socket, socket.assigns.workbench)
_other ->
reload.(socket, socket.assigns.workbench)
end
end
@spec update_merge_target(term(), term(), term()) :: term()
def update_merge_target(socket, target, reload) do
socket
|> assign(:tags_editor_merge_target, to_string(target || ""))
|> reload.(socket.assigns.workbench)
end
@spec merge_selected(term(), term(), term()) :: term()
def merge_selected(socket, reload, append_output) do
selected = Map.get(socket.assigns, :tags_editor_selected, [])
target_name = Map.get(socket.assigns, :tags_editor_merge_target, "")
@@ -136,12 +165,19 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
true ->
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))
sources = Enum.reject(tags, &(&1.name == target_name))
case target do
nil -> reload.(socket, socket.assigns.workbench)
nil ->
reload.(socket, socket.assigns.workbench)
_target ->
case Tags.merge_tags(Enum.map(sources, & &1.id), target.id) do
{:ok, _merged} ->
@@ -160,23 +196,41 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end
end
@spec sync(term(), term(), term()) :: term()
def sync(socket, reload, append_output) do
_ = append_output
:ok = Tags.sync_tags_json(socket.assigns.projects.active_project_id)
reload.(socket, socket.assigns.workbench)
end
@spec build(term()) :: term()
def build(%{current_tab: %{type: :tags}} = assigns) do
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)
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))
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,
new_tag: Map.get(assigns, :tags_editor_new_tag, %{"name" => "", "color" => ""}),
edit_draft: edit_draft,
@@ -187,14 +241,18 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
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
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)
Float.round(0.85 + (1.8 - 0.85) * ratio, 2)
end
@spec tag_style(term(), term()) :: term()
def tag_style(tag, counts) do
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 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
normalized = String.trim(to_string(next_name || tag.name))
@@ -237,6 +301,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end
defp blank_to_nil(nil), do: nil
defp blank_to_nil(value) do
case String.trim(to_string(value)) do
"" -> nil

View File

@@ -46,7 +46,10 @@ defmodule BDS.Desktop.ShellLive.TaskLocalization do
|> 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(: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
defp localize_task_message(nil, _locale), do: nil

View File

@@ -41,7 +41,9 @@ defmodule BDS.Desktop.ShellLive.TitlebarMenu do
@spec active_group(map()) :: map() | nil
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
@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
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()
def handle_keydown(socket, key, invoke_fun) do
if socket.assigns.titlebar_menu_group do
@@ -114,7 +118,9 @@ defmodule BDS.Desktop.ShellLive.TitlebarMenu do
defp rotate_group(socket, offset) do
groups = socket.assigns.menu_groups || []
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
socket