From d589d84b4018c6c9d8a319cad95cc9a3ad109545 Mon Sep 17 00:00:00 2001 From: Thelonius Kort Date: Wed, 4 Jan 2023 14:42:13 +0100 Subject: [PATCH] Add creating and basic editing of translation --- lib/outlook/internal_tree.ex | 4 +- lib/outlook/translations.ex | 5 +- lib/outlook/translations/basic.ex | 25 +++++ lib/outlook/translations/translation.ex | 2 +- lib/outlook_web.ex | 1 + .../components/tunit_editor_component.ex | 51 +++++++++++ .../live/article_live/index.html.heex | 1 + .../live/article_live/new_components.ex | 8 ++ .../live/article_live/show.html.heex | 2 + lib/outlook_web/live/translation_live/edit.ex | 28 ++++++ .../live/translation_live/form_component.ex | 91 ++++++++++++++----- .../live/translation_live/index.html.heex | 4 +- lib/outlook_web/live/translation_live/new.ex | 38 ++++++++ .../live/translation_live/show.html.heex | 15 +-- lib/outlook_web/router.ex | 7 +- 15 files changed, 235 insertions(+), 47 deletions(-) create mode 100644 lib/outlook/translations/basic.ex create mode 100644 lib/outlook_web/components/tunit_editor_component.ex create mode 100644 lib/outlook_web/live/translation_live/edit.ex create mode 100644 lib/outlook_web/live/translation_live/new.ex diff --git a/lib/outlook/internal_tree.ex b/lib/outlook/internal_tree.ex index b70485f..88cf60a 100644 --- a/lib/outlook/internal_tree.ex +++ b/lib/outlook/internal_tree.ex @@ -9,9 +9,9 @@ defmodule Outlook.InternalTree do |> Html.to_html() end - def render_html_preview(tree) do + def render_html_preview(tree, target \\ "1") do tree - |> Html.to_html_preview("1") + |> Html.to_html_preview(target) end require Logger diff --git a/lib/outlook/translations.ex b/lib/outlook/translations.ex index b94a777..08f773d 100644 --- a/lib/outlook/translations.ex +++ b/lib/outlook/translations.ex @@ -35,7 +35,10 @@ defmodule Outlook.Translations do ** (Ecto.NoResultsError) """ - def get_translation!(id), do: Repo.get!(Translation, id) + def get_translation!(id) do + Repo.get!(Translation, id) + |> Repo.preload([:article]) + end @doc """ Creates a translation. diff --git a/lib/outlook/translations/basic.ex b/lib/outlook/translations/basic.ex new file mode 100644 index 0000000..68fec6c --- /dev/null +++ b/lib/outlook/translations/basic.ex @@ -0,0 +1,25 @@ +defmodule Outlook.Translations.Basic do + + alias Outlook.InternalTree.InternalNode + alias Outlook.InternalTree.TranslationUnit + + def internal_tree_to_tunit_map(tree) do + collect_translation_units(tree) + |> Enum.map(fn tunit -> {tunit.uuid, tunit} end) + |> Enum.into(%{}) + end + + defp collect_translation_units([%InternalNode{type: :element} = node | rest]) do + collect_translation_units(node.content) ++ collect_translation_units(rest) + end + + defp collect_translation_units([%TranslationUnit{} = tunit | rest]) do + [tunit | collect_translation_units(rest)] + end + + defp collect_translation_units([_|rest]) do + [] ++ collect_translation_units(rest) + end + + defp collect_translation_units([]), do: [] +end diff --git a/lib/outlook/translations/translation.ex b/lib/outlook/translations/translation.ex index 3d9fd16..c3e2b33 100644 --- a/lib/outlook/translations/translation.ex +++ b/lib/outlook/translations/translation.ex @@ -24,7 +24,7 @@ defmodule Outlook.Translations.Translation do def changeset(translation, attrs) do translation |> cast(attrs, [:lang, :title, :teaser, :date, :public, :unauthorized, :article_id]) - |> cast(attrs, [:content], force_changes: true) + |> cast(attrs, [:content]) |> validate_required([:lang, :title, :content, :date, :public, :unauthorized, :article_id]) |> unique_constraint([:lang, :article_id], message: "translation for this language already exists", diff --git a/lib/outlook_web.ex b/lib/outlook_web.ex index 1c7a24a..24df2b1 100644 --- a/lib/outlook_web.ex +++ b/lib/outlook_web.ex @@ -87,6 +87,7 @@ defmodule OutlookWeb do # Core UI components and translation import OutlookWeb.CoreComponents import OutlookWeb.HtmlTreeComponent + import OutlookWeb.TunitEditorComponent import OutlookWeb.Gettext # Shortcut for generating JS commands diff --git a/lib/outlook_web/components/tunit_editor_component.ex b/lib/outlook_web/components/tunit_editor_component.ex new file mode 100644 index 0000000..0270b2e --- /dev/null +++ b/lib/outlook_web/components/tunit_editor_component.ex @@ -0,0 +1,51 @@ +defmodule OutlookWeb.TunitEditorComponent do + + use Phoenix.Component + + import OutlookWeb.CoreComponents + # alias Phoenix.LiveView.JS + + defp statuses do + [ {:untranslated, "bg-red-800 col-span-3 disabled:border-gray-600"}, + {:passable, "bg-amber-500 col-span-2 disabled:border-gray-600"}, + {:done, "bg-green-700 disabled:border-gray-600"} ] + end + + attr :current_tunit, :any + attr :target, :string + + def tunit_editor(assigns) do + assigns = unless assigns.current_tunit do + assigns + |> assign( + current_tunit: %Outlook.InternalTree.TranslationUnit{}, + disabled: true + ) + else + assigns |> assign(disabled: false) + end + ~H""" +
+ <%!--
+ <%= @current_tunit.content |> raw %> +
--%> +
+ +
+
+
+
+ <.status_button class={class} status={status} target={@target} disabled={@current_tunit.status == status}> +
+
+ """ + end + + defp status_button(assigns) do + ~H""" + <.link phx-click="tunit_status" phx-value-status={@status} phx-target={@target}> + <.button class={@class} title="select translation status" disabled={@disabled}><%= @status |> to_string %> + + """ + end +end diff --git a/lib/outlook_web/live/article_live/index.html.heex b/lib/outlook_web/live/article_live/index.html.heex index e3b4d21..530619b 100644 --- a/lib/outlook_web/live/article_live/index.html.heex +++ b/lib/outlook_web/live/article_live/index.html.heex @@ -15,6 +15,7 @@ <.link navigate={~p"/articles/#{article}"}>Show <.link patch={~p"/articles/#{article}/edit"}>Edit + <.link navigate={~p"/translations/new?article_id=#{article}"}>New Translation <:action :let={article}> <.link phx-click={JS.push("delete", value: %{id: article.id})} data-confirm="Are you sure?"> diff --git a/lib/outlook_web/live/article_live/new_components.ex b/lib/outlook_web/live/article_live/new_components.ex index c67a420..635a339 100644 --- a/lib/outlook_web/live/article_live/new_components.ex +++ b/lib/outlook_web/live/article_live/new_components.ex @@ -47,4 +47,12 @@ defmodule OutlookWeb.ArticleLive.NewComponents do <.button phx-click="approve_translation_units">Continue """ end + + def final_form(assigns) do + ~H""" +
Final Form
+ + <.button phx-click="save">Save + """ + end end diff --git a/lib/outlook_web/live/article_live/show.html.heex b/lib/outlook_web/live/article_live/show.html.heex index e3e0d07..dc5a745 100644 --- a/lib/outlook_web/live/article_live/show.html.heex +++ b/lib/outlook_web/live/article_live/show.html.heex @@ -18,6 +18,8 @@ <%= InternalTree.render_html(@article.content) |> raw %> +<.link navigate={~p"/translations/new?article_id=#{@article.id}"}>New Translation + <.back navigate={~p"/articles"}>Back to articles <.modal :if={@live_action == :edit} id="article-modal" show on_cancel={JS.patch(~p"/articles/#{@article}")}> diff --git a/lib/outlook_web/live/translation_live/edit.ex b/lib/outlook_web/live/translation_live/edit.ex new file mode 100644 index 0000000..e6fe131 --- /dev/null +++ b/lib/outlook_web/live/translation_live/edit.ex @@ -0,0 +1,28 @@ +defmodule OutlookWeb.TranslationLive.Edit do + use OutlookWeb, :live_view + + alias Outlook.Articles + alias Outlook.Translations + + @impl true + def render(assigns) do + ~H""" + <.live_component + module={OutlookWeb.TranslationLive.FormComponent} + id={:new} + title="New Translation" + action={@live_action} + translation={@translation} + translation_content={@translation_content} + navigate={~p"/translations"} + /> + """ + end + + @impl true + def mount(%{"id" => id} = _params, _session, socket) do + socket = socket + |> assign_new(:translation, fn -> Translations.get_translation!(id) end) + {:ok, assign_new(socket, :translation_content, fn -> socket.assigns.translation.content end)} + end +end diff --git a/lib/outlook_web/live/translation_live/form_component.ex b/lib/outlook_web/live/translation_live/form_component.ex index a08bbe7..b8e914e 100644 --- a/lib/outlook_web/live/translation_live/form_component.ex +++ b/lib/outlook_web/live/translation_live/form_component.ex @@ -1,36 +1,45 @@ defmodule OutlookWeb.TranslationLive.FormComponent do use OutlookWeb, :live_component - alias Outlook.Translations + alias Outlook.{Translations,InternalTree} + alias Outlook.InternalTree.TranslationUnit @impl true def render(assigns) do ~H""" -
- <.header> - <%= @title %> - <:subtitle>Use this form to manage translation records in your database. - +
+
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage translation records in your database. + - <.simple_form - :let={f} - for={@changeset} - id="translation-form" - phx-target={@myself} - phx-change="validate" - phx-submit="save" - > - <.input field={{f, :lang}} type="text" label="lang" /> - <.input field={{f, :title}} type="text" label="title" /> - <.input field={{f, :teaser}} type="text" label="teaser" /> - <.input field={{f, :content}} type="text" label="content" /> - <.input field={{f, :date}} type="datetime-local" label="date" /> - <.input field={{f, :public}} type="checkbox" label="public" /> - <.input field={{f, :unauthorized}} type="checkbox" label="unauthorized" /> - <:actions> - <.button phx-disable-with="Saving...">Save Translation - - + <.simple_form + :let={f} + for={@changeset} + id="translation-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={{f, :article_id}} type="hidden" /> + <.input field={{f, :lang}} type="select" label="lang" + options={Application.get_env(:outlook,:deepl)[:target_langs]} /> + <.input field={{f, :title}} type="text" label="title" /> + <.input field={{f, :teaser}} type="textarea" label="teaser" class="h-28" /> + <%!-- <.input field={{f, :content}} type="text" label="content" /> --%> + <.input field={{f, :date}} type="datetime-local" label="date" /> + <.input field={{f, :public}} type="checkbox" label="public" /> + <.input field={{f, :unauthorized}} type="checkbox" label="unauthorized" /> + <:actions> + <.button phx-disable-with="Saving...">Save Translation + + + <.tunit_editor current_tunit={@current_tunit} target={@myself} /> +
+
+ <%= InternalTree.render_html_preview(@translation.article.content, @myself) |> raw %> +
""" end @@ -56,9 +65,41 @@ defmodule OutlookWeb.TranslationLive.FormComponent do end def handle_event("save", %{"translation" => translation_params}, socket) do + socket = socket + |> update_translation_with_current_tunit() + translation_params = translation_params + |> Map.put("content", socket.assigns.translation_content) save_translation(socket, socket.assigns.action, translation_params) end + def handle_event("tunit_status", %{"status" => status}, socket) do + tunit = %TranslationUnit{socket.assigns.current_tunit | status: String.to_atom(status)} + {:noreply, socket |> assign(:current_tunit, tunit)} + end + + def handle_event("select_current_tunit", %{"uuid" => uuid}, socket) do + {:noreply, + socket + |> update_translation_with_current_tunit + |> assign(:current_tunit, socket.assigns.translation_content[uuid])} + end + + def handle_event("update_current_tunit", %{"content" => content}, socket) do + tunit = %TranslationUnit{socket.assigns.current_tunit | content: content} + {:noreply, socket |> assign(:current_tunit, tunit)} + end + + defp update_translation_with_current_tunit(socket) do + translation_content = if socket.assigns.current_tunit do + socket.assigns.translation_content + |> Map.put(socket.assigns.current_tunit.uuid, socket.assigns.current_tunit) + else + socket.assigns.translation_content + end + socket + |> assign(:translation_content, translation_content) + end + defp save_translation(socket, :edit, translation_params) do case Translations.update_translation(socket.assigns.translation, translation_params) do {:ok, _translation} -> diff --git a/lib/outlook_web/live/translation_live/index.html.heex b/lib/outlook_web/live/translation_live/index.html.heex index e0379a1..edcf9f0 100644 --- a/lib/outlook_web/live/translation_live/index.html.heex +++ b/lib/outlook_web/live/translation_live/index.html.heex @@ -11,7 +11,7 @@ <:col :let={translation} label="Lang"><%= translation.lang %> <:col :let={translation} label="Title"><%= translation.title %> <:col :let={translation} label="Teaser"><%= translation.teaser %> - <:col :let={translation} label="Content"><%= translation.content %> + <%!-- <:col :let={translation} label="Content"><%= translation.content %> --%> <:col :let={translation} label="Date"><%= translation.date %> <:col :let={translation} label="Public"><%= translation.public %> <:col :let={translation} label="Unauthorized"><%= translation.unauthorized %> @@ -19,7 +19,7 @@
<.link navigate={~p"/translations/#{translation}"}>Show
- <.link patch={~p"/translations/#{translation}/edit"}>Edit + <.link navigate={~p"/translations/#{translation}/edit"}>Edit <:action :let={translation}> <.link phx-click={JS.push("delete", value: %{id: translation.id})} data-confirm="Are you sure?"> diff --git a/lib/outlook_web/live/translation_live/new.ex b/lib/outlook_web/live/translation_live/new.ex new file mode 100644 index 0000000..600b6d4 --- /dev/null +++ b/lib/outlook_web/live/translation_live/new.ex @@ -0,0 +1,38 @@ +defmodule OutlookWeb.TranslationLive.New do + use OutlookWeb, :live_view + + alias Outlook.Articles + alias Outlook.Translations.{Translation,Basic} + + @impl true + def render(assigns) do + ~H""" + <.live_component + module={OutlookWeb.TranslationLive.FormComponent} + id={:new} + title="New Translation" + action={@live_action} + translation={@translation} + translation_content={@translation_content} + navigate={~p"/translations"} + /> + """ + end + + @impl true + def mount(%{"article_id" => article_id} = _params, _session, socket) do + socket = socket + |> assign_new(:translation, fn -> + %Translation{ + article_id: article_id, + article: get_article(article_id) + } + end) + {:ok, assign_new(socket, :translation_content, fn -> + Basic.internal_tree_to_tunit_map(socket.assigns.translation.article.content) end)} + end + + defp get_article(article_id) do + Articles.get_article!(article_id) + end +end diff --git a/lib/outlook_web/live/translation_live/show.html.heex b/lib/outlook_web/live/translation_live/show.html.heex index 8703f90..4efb2ab 100644 --- a/lib/outlook_web/live/translation_live/show.html.heex +++ b/lib/outlook_web/live/translation_live/show.html.heex @@ -2,7 +2,7 @@ Translation <%= @translation.id %> <:subtitle>This is a translation record from your database. <:actions> - <.link patch={~p"/translations/#{@translation}/show/edit"} phx-click={JS.push_focus()}> + <.link navigate={~p"/translations/#{@translation}/edit"} phx-click={JS.push_focus()}> <.button>Edit translation @@ -12,21 +12,10 @@ <:item title="Lang"><%= @translation.lang %> <:item title="Title"><%= @translation.title %> <:item title="Teaser"><%= @translation.teaser %> - <:item title="Content"><%= @translation.content %> + <%!-- <:item title="Content"><%= @translation.content %> --%> <:item title="Date"><%= @translation.date %> <:item title="Public"><%= @translation.public %> <:item title="Unauthorized"><%= @translation.unauthorized %> <.back navigate={~p"/translations"}>Back to translations - -<.modal :if={@live_action == :edit} id="translation-modal" show on_cancel={JS.patch(~p"/translations/#{@translation}")}> - <.live_component - module={OutlookWeb.TranslationLive.FormComponent} - id={@translation.id} - title={@page_title} - action={@live_action} - translation={@translation} - navigate={~p"/translations/#{@translation}"} - /> - diff --git a/lib/outlook_web/router.ex b/lib/outlook_web/router.ex index c37de82..297abaa 100644 --- a/lib/outlook_web/router.ex +++ b/lib/outlook_web/router.ex @@ -86,11 +86,12 @@ defmodule OutlookWeb.Router do live "/articles/:id/show/edit", ArticleLive.Show, :edit live "/translations", TranslationLive.Index, :index - live "/translations/new", TranslationLive.Index, :new - live "/translations/:id/edit", TranslationLive.Index, :edit + + live "/translations/new", TranslationLive.New, :new + + live "/translations/:id/edit", TranslationLive.Edit, :edit live "/translations/:id", TranslationLive.Show, :show - live "/translations/:id/show/edit", TranslationLive.Show, :edit live "/deepl_accounts", DeeplAccountLive.Index, :index live "/deepl_accounts/new", DeeplAccountLive.Index, :new