Add first step of creating an Article
This commit is contained in:
@ -6,7 +6,7 @@ defmodule Outlook.Articles do
|
|||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Outlook.Repo
|
alias Outlook.Repo
|
||||||
|
|
||||||
alias Outlook.Articles.Article
|
alias Outlook.Articles.{Article,RawHtmlInput}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of articles.
|
Returns the list of articles.
|
||||||
@ -101,4 +101,8 @@ defmodule Outlook.Articles do
|
|||||||
def change_article(%Article{} = article, attrs \\ %{}) do
|
def change_article(%Article{} = article, attrs \\ %{}) do
|
||||||
Article.changeset(article, attrs)
|
Article.changeset(article, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def change_raw_html_input(%RawHtmlInput{} = raw_html_input, attrs \\ %{}) do
|
||||||
|
RawHtmlInput.changeset(raw_html_input, attrs)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
16
lib/outlook/articles/raw_html_input.ex
Normal file
16
lib/outlook/articles/raw_html_input.ex
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
defmodule Outlook.Articles.RawHtmlInput do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field :content, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(html_input, attrs) do
|
||||||
|
html_input
|
||||||
|
|> cast(attrs, [:content])
|
||||||
|
|> validate_required([:content])
|
||||||
|
|> validate_length(:content, min: 200)
|
||||||
|
end
|
||||||
|
end
|
||||||
14
lib/outlook/html_preparations.ex
Normal file
14
lib/outlook/html_preparations.ex
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
defmodule Outlook.HtmlPreparations do
|
||||||
|
@moduledoc """
|
||||||
|
The HtmlPreparations context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Outlook.HtmlPreparations.HtmlPreparation
|
||||||
|
|
||||||
|
def convert_raw_html_input(html) do
|
||||||
|
html
|
||||||
|
|> Floki.parse_fragment!
|
||||||
|
|> HtmlPreparation.floki_to_internal
|
||||||
|
|> HtmlPreparation.set_sibling_with
|
||||||
|
end
|
||||||
|
end
|
||||||
65
lib/outlook/html_preparations/html_preparation.ex
Normal file
65
lib/outlook/html_preparations/html_preparation.ex
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
defmodule Outlook.HtmlPreparations.HtmlPreparation do
|
||||||
|
import Ecto.UUID, only: [generate: 0]
|
||||||
|
|
||||||
|
alias Outlook.InternalTree.InternalNode
|
||||||
|
|
||||||
|
@block_elements ["address","article","aside","blockquote","canvas","dd","div","dl","dt","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hr","li","main","nav","noscript","ol","p","pre","section","table","tfoot","ul","video"]
|
||||||
|
# @inline_elements ["a","abbr","acronym","b","bdo","big","br","button","cite","code","dfn","em","i","img","input","kbd","label","map","object","output","q","samp","script","select","small","span","strong","sub","sup","textarea","time","tt","u","var"]
|
||||||
|
|
||||||
|
defp clean_atts_to_map(atts) do
|
||||||
|
atts_to_keep = ~w(href src)
|
||||||
|
atts_to_rename = ~w(class style src-set)
|
||||||
|
atts
|
||||||
|
|> Enum.reject(fn {k,_} -> k not in (atts_to_keep ++ atts_to_rename) end)
|
||||||
|
|> Enum.reject(fn {_,v} -> v == "" end)
|
||||||
|
|> Enum.map(fn {k,v} -> {k in atts_to_rename && "#{k}-old" || k, v} end)
|
||||||
|
|> Enum.map(fn {k,v} -> {String.to_atom(k),v} end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
end
|
||||||
|
|
||||||
|
def floki_to_internal [ { tag, attributes, content } | rest ] do
|
||||||
|
[ %InternalNode{
|
||||||
|
name: tag,
|
||||||
|
attributes: clean_atts_to_map(attributes),
|
||||||
|
type: :element,
|
||||||
|
uuid: generate(),
|
||||||
|
content: floki_to_internal(content)
|
||||||
|
} | floki_to_internal(rest) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def floki_to_internal [ "" <> textnode | rest ] do
|
||||||
|
[ %InternalNode{
|
||||||
|
type: :text,
|
||||||
|
uuid: generate(),
|
||||||
|
content: textnode
|
||||||
|
} | floki_to_internal(rest) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def floki_to_internal [ {:comment, comment} | rest ] do
|
||||||
|
[ %InternalNode{
|
||||||
|
type: :comment,
|
||||||
|
uuid: generate(),
|
||||||
|
content: comment
|
||||||
|
} | floki_to_internal(rest) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def floki_to_internal([ ]), do: ( [ ] )
|
||||||
|
|
||||||
|
|
||||||
|
def set_sibling_with([ %{type: :element} = node | rest ]) do
|
||||||
|
[ %InternalNode{ node |
|
||||||
|
sibling_with: node.name in @block_elements && :block || :inline,
|
||||||
|
content: set_sibling_with(node.content)
|
||||||
|
} | set_sibling_with(rest) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_sibling_with([ node | rest ]) do
|
||||||
|
sib_with = case node.type do
|
||||||
|
:text -> Regex.match?(~r/^\s*$/, node.content) && :both || :inline
|
||||||
|
:comment -> :both
|
||||||
|
end
|
||||||
|
[ %InternalNode{ node | sibling_with: sib_with } | set_sibling_with(rest) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_sibling_with([ ]), do: ( [ ] )
|
||||||
|
end
|
||||||
4
lib/outlook/internal_tree/internal_node.ex
Normal file
4
lib/outlook/internal_tree/internal_node.ex
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
defmodule Outlook.InternalTree.InternalNode do
|
||||||
|
@derive Jason.Encoder
|
||||||
|
defstruct name: "", attributes: %{}, type: :atom, uuid: "", content: [], sibling_with: nil
|
||||||
|
end
|
||||||
@ -1,9 +1,6 @@
|
|||||||
<.header>
|
<.header>
|
||||||
Listing Articles
|
Listing Articles
|
||||||
<:actions>
|
<:actions>
|
||||||
<.link patch={~p"/articles/new"}>
|
|
||||||
<.button>New Article</.button>
|
|
||||||
</.link>
|
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
|||||||
55
lib/outlook_web/live/article_live/new.ex
Normal file
55
lib/outlook_web/live/article_live/new.ex
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
defmodule OutlookWeb.ArticleLive.New do
|
||||||
|
use OutlookWeb, :live_view
|
||||||
|
|
||||||
|
import OutlookWeb.ArticleLive.NewComponents
|
||||||
|
|
||||||
|
alias Outlook.{Articles,Authors,HtmlPreparations}
|
||||||
|
alias Articles.{Article,RawHtmlInput}
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "New Article")
|
||||||
|
|> assign(:article, %Article{})
|
||||||
|
|> assign(:raw_html_input, %RawHtmlInput{})
|
||||||
|
|> assign(:changeset, Articles.change_raw_html_input(%RawHtmlInput{}))
|
||||||
|
|> assign(:selected_els, [])
|
||||||
|
|> assign(:step, :import_raw_html)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(%{"author_id" => author_id}, _, socket) do
|
||||||
|
author = Authors.get_author!(author_id)
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:author, author)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("validate_raw_html_input", %{"raw_html_input" => raw_html_input_params}, socket) do
|
||||||
|
changeset = validate_raw_html_input(raw_html_input_params, socket)
|
||||||
|
{:noreply, assign(socket, :changeset, changeset)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("convert_raw_html_input", %{"raw_html_input" => raw_html_input_params}, socket) do
|
||||||
|
changeset = validate_raw_html_input(raw_html_input_params, socket)
|
||||||
|
case changeset.valid? do
|
||||||
|
true ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:raw_internal_tree, HtmlPreparations.convert_raw_html_input(raw_html_input_params["content"]))
|
||||||
|
|> assign(:step, :review_raw_internaltree)}
|
||||||
|
false ->
|
||||||
|
{:noreply, assign(socket, :changeset, changeset)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_raw_html_input(raw_html_input_params, socket) do
|
||||||
|
socket.assigns.raw_html_input
|
||||||
|
|> Articles.change_raw_html_input(raw_html_input_params)
|
||||||
|
|> Map.put(:action, :validate)
|
||||||
|
end
|
||||||
|
end
|
||||||
9
lib/outlook_web/live/article_live/new.html.heex
Normal file
9
lib/outlook_web/live/article_live/new.html.heex
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<.header>
|
||||||
|
New article for <%= @author.name %>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.import_raw_html :if={@step == :import_raw_html} changeset={@changeset}></.import_raw_html>
|
||||||
|
<.review_raw_internaltree :if={@step == :review_raw_internaltree}></.review_raw_internaltree>
|
||||||
|
<.review_translation_units :if={@step == :review_translation_units}></.review_translation_units>
|
||||||
|
|
||||||
|
<.back navigate={~p"/authors/#{@author}"}>Back to author</.back>
|
||||||
37
lib/outlook_web/live/article_live/new_components.ex
Normal file
37
lib/outlook_web/live/article_live/new_components.ex
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
defmodule OutlookWeb.ArticleLive.NewComponents do
|
||||||
|
use OutlookWeb, :html
|
||||||
|
|
||||||
|
def import_raw_html(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<div>Import article</div>
|
||||||
|
|
||||||
|
<.simple_form
|
||||||
|
:let={f}
|
||||||
|
for={@changeset}
|
||||||
|
id="raw-html-input-form"
|
||||||
|
phx-change="validate_raw_html_input"
|
||||||
|
phx-submit="convert_raw_html_input"
|
||||||
|
>
|
||||||
|
<.input field={{f, :content}} type="textarea" label="text to import" phx-debounce="500" />
|
||||||
|
<:actions>
|
||||||
|
<.button phx-disable-with="Importing...">HTML importieren</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def review_raw_internaltree(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>Review Raw InternalTree</div>
|
||||||
|
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def review_translation_units(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>Review Translation Units</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -5,6 +5,9 @@
|
|||||||
<.link patch={~p"/authors/#{@author}/show/edit"} phx-click={JS.push_focus()}>
|
<.link patch={~p"/authors/#{@author}/show/edit"} phx-click={JS.push_focus()}>
|
||||||
<.button>Edit author</.button>
|
<.button>Edit author</.button>
|
||||||
</.link>
|
</.link>
|
||||||
|
<.link patch={~p"/articles/new?author_id=#{@author}"} phx-click={JS.push_focus()}>
|
||||||
|
<.button>New article</.button>
|
||||||
|
</.link>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
|||||||
@ -78,9 +78,10 @@ defmodule OutlookWeb.Router do
|
|||||||
live "/authors/:id/show/edit", AuthorLive.Show, :edit
|
live "/authors/:id/show/edit", AuthorLive.Show, :edit
|
||||||
|
|
||||||
live "/articles", ArticleLive.Index, :index
|
live "/articles", ArticleLive.Index, :index
|
||||||
live "/articles/new", ArticleLive.Index, :new
|
|
||||||
live "/articles/:id/edit", ArticleLive.Index, :edit
|
live "/articles/:id/edit", ArticleLive.Index, :edit
|
||||||
|
|
||||||
|
live "/articles/new", ArticleLive.New, :new
|
||||||
|
|
||||||
live "/articles/:id", ArticleLive.Show, :show
|
live "/articles/:id", ArticleLive.Show, :show
|
||||||
live "/articles/:id/show/edit", ArticleLive.Show, :edit
|
live "/articles/:id/show/edit", ArticleLive.Show, :edit
|
||||||
|
|
||||||
|
|||||||
2
mix.exs
2
mix.exs
@ -41,7 +41,7 @@ defmodule Outlook.MixProject do
|
|||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:phoenix_live_view, "~> 0.18.3"},
|
{:phoenix_live_view, "~> 0.18.3"},
|
||||||
{:heroicons, "~> 0.5"},
|
{:heroicons, "~> 0.5"},
|
||||||
{:floki, ">= 0.30.0", only: :test},
|
{:floki, ">= 0.30.0"},
|
||||||
{:phoenix_live_dashboard, "~> 0.7.2"},
|
{:phoenix_live_dashboard, "~> 0.7.2"},
|
||||||
{:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
|
{:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
|
||||||
{:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
|
{:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
|
||||||
|
|||||||
Reference in New Issue
Block a user