From 8fcbb7aced4fe0707692a4a15d56ffd7ff7aba6c Mon Sep 17 00:00:00 2001 From: shibao Date: Sat, 26 Nov 2022 21:26:21 -0500 Subject: [PATCH] add steps --- lib/memex/pipelines/pipeline.ex | 2 +- lib/memex/pipelines/step.ex | 15 +- lib/memex/pipelines/steps.ex | 103 ++++++++++-- lib/memex_web/components/step_content.ex | 44 +++++ lib/memex_web/live/pipeline_live/show.ex | 91 +++++++++- .../live/pipeline_live/show.html.heex | 157 +++++++++++++++--- .../live/step_live/form_component.ex | 74 +++++++++ .../live/step_live/form_component.html.heex | 34 ++++ lib/memex_web/router.ex | 2 + priv/gettext/actions.pot | 14 +- priv/gettext/default.pot | 54 +++++- priv/gettext/prompts.pot | 3 +- test/memex/steps_test.exs | 80 +++++++-- test/memex_web/live/pipeline_live_test.exs | 120 ++++++++++++- 14 files changed, 727 insertions(+), 66 deletions(-) create mode 100644 lib/memex_web/components/step_content.ex create mode 100644 lib/memex_web/live/step_live/form_component.ex create mode 100644 lib/memex_web/live/step_live/form_component.html.heex diff --git a/lib/memex/pipelines/pipeline.ex b/lib/memex/pipelines/pipeline.ex index 17b0d71..1a86d74 100644 --- a/lib/memex/pipelines/pipeline.ex +++ b/lib/memex/pipelines/pipeline.ex @@ -6,7 +6,7 @@ defmodule Memex.Pipelines.Pipeline do import Ecto.Changeset import MemexWeb.Gettext alias Ecto.{Changeset, UUID} - alias Memex.{Accounts.User, Pipelines.Step} + alias Memex.{Accounts.User, Pipelines.Steps.Step} @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id diff --git a/lib/memex/pipelines/step.ex b/lib/memex/pipelines/step.ex index f7df400..18727de 100644 --- a/lib/memex/pipelines/step.ex +++ b/lib/memex/pipelines/step.ex @@ -1,4 +1,4 @@ -defmodule Memex.Pipelines.Step do +defmodule Memex.Pipelines.Steps.Step do @moduledoc """ Represents a step taken while executing a pipeline """ @@ -46,16 +46,25 @@ defmodule Memex.Pipelines.Step do |> validate_required([:title, :content, :user_id, :position]) end - @spec update_changeset(t(), attrs :: map(), position :: non_neg_integer(), User.t()) :: + @spec update_changeset(t(), attrs :: map(), User.t()) :: changeset() def update_changeset( %{user_id: user_id} = step, attrs, - position, %User{id: user_id} ) do step |> cast(attrs, [:title, :content]) + |> validate_required([:title, :content, :user_id, :position]) + end + + @spec position_changeset(t(), position :: non_neg_integer(), User.t()) :: changeset() + def position_changeset( + %{user_id: user_id} = step, + position, + %User{id: user_id} + ) do + step |> change(position: position) |> validate_required([:title, :content, :user_id, :position]) end diff --git a/lib/memex/pipelines/steps.ex b/lib/memex/pipelines/steps.ex index 0967a44..d33de67 100644 --- a/lib/memex/pipelines/steps.ex +++ b/lib/memex/pipelines/steps.ex @@ -4,8 +4,9 @@ defmodule Memex.Pipelines.Steps do """ import Ecto.Query, warn: false + alias Ecto.Multi alias Memex.{Accounts.User, Repo} - alias Memex.Pipelines.{Pipeline, Step} + alias Memex.Pipelines.{Pipeline, Steps.Step} @doc """ Returns the list of steps. @@ -105,11 +106,11 @@ defmodule Memex.Pipelines.Steps do {:error, %Ecto.Changeset{}} """ - @spec update_step(Step.t(), attrs :: map(), position :: non_neg_integer(), User.t()) :: + @spec update_step(Step.t(), attrs :: map(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()} - def update_step(%Step{} = step, attrs, position, user) do + def update_step(%Step{} = step, attrs, user) do step - |> Step.update_changeset(attrs, position, user) + |> Step.update_changeset(attrs, user) |> Repo.update() end @@ -130,11 +131,31 @@ defmodule Memex.Pipelines.Steps do """ @spec delete_step(Step.t(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()} def delete_step(%Step{user_id: user_id} = step, %{id: user_id}) do - step |> Repo.delete() + delete_step(step) end def delete_step(%Step{} = step, %{role: :admin}) do - step |> Repo.delete() + delete_step(step) + end + + defp delete_step(step) do + Multi.new() + |> Multi.delete(:delete_step, step) + |> Multi.update_all( + :reorder_steps, + fn %{delete_step: %{position: position, pipeline_id: pipeline_id}} -> + from s in Step, + where: s.pipeline_id == ^pipeline_id, + where: s.position > ^position, + update: [set: [position: s.position - 1]] + end, + [] + ) + |> Repo.transaction() + |> case do + {:ok, %{delete_step: step}} -> {:ok, step} + {:error, :delete_step, changeset, _changes_so_far} -> {:error, changeset} + end end @doc """ @@ -149,11 +170,69 @@ defmodule Memex.Pipelines.Steps do %Ecto.Changeset{data: %Step{}} """ - @spec change_step(Step.t(), position :: non_neg_integer(), Pipeline.t(), User.t()) :: - Step.changeset() - @spec change_step(Step.t(), attrs :: map(), position :: non_neg_integer(), User.t()) :: - Step.changeset() - def change_step(%Step{} = step, attrs \\ %{}, position, user) do - step |> Step.update_changeset(attrs, position, user) + @spec change_step(Step.t(), User.t()) :: Step.changeset() + @spec change_step(Step.t(), attrs :: map(), User.t()) :: Step.changeset() + def change_step(%Step{} = step, attrs \\ %{}, user) do + step |> Step.update_changeset(attrs, user) + end + + @spec reorder_step(Step.t(), :up | :down, User.t()) :: + {:ok, Step.t()} | {:error, Step.changeset()} + def reorder_step(%Step{position: 0} = step, :up, _user), do: {:error, step} + + def reorder_step( + %Step{position: position, pipeline_id: pipeline_id, user_id: user_id} = step, + :up, + %{id: user_id} = user + ) do + Multi.new() + |> Multi.update_all( + :reorder_steps, + from(s in Step, + where: s.pipeline_id == ^pipeline_id, + where: s.position == ^position - 1, + update: [set: [position: ^position]] + ), + [] + ) + |> Multi.update( + :update_step, + step |> Step.position_changeset(position - 1, user) + ) + |> Repo.transaction() + |> case do + {:ok, %{update_step: step}} -> {:ok, step} + {:error, :update_step, changeset, _changes_so_far} -> {:error, changeset} + end + end + + def reorder_step( + %Step{pipeline_id: pipeline_id, position: position, user_id: user_id} = step, + :down, + %{id: user_id} = user + ) do + Multi.new() + |> Multi.one( + :step_count, + from(s in Step, where: s.pipeline_id == ^pipeline_id, distinct: true, select: count(s.id)) + ) + |> Multi.update_all( + :reorder_steps, + from(s in Step, + where: s.pipeline_id == ^pipeline_id, + where: s.position == ^position + 1, + update: [set: [position: ^position]] + ), + [] + ) + |> Multi.update(:update_step, fn %{step_count: step_count} -> + new_position = if position >= step_count - 1, do: position, else: position + 1 + step |> Step.position_changeset(new_position, user) + end) + |> Repo.transaction() + |> case do + {:ok, %{update_step: step}} -> {:ok, step} + {:error, :update_step, changeset, _changes_so_far} -> {:error, changeset} + end end end diff --git a/lib/memex_web/components/step_content.ex b/lib/memex_web/components/step_content.ex new file mode 100644 index 0000000..75a8595 --- /dev/null +++ b/lib/memex_web/components/step_content.ex @@ -0,0 +1,44 @@ +defmodule MemexWeb.Components.StepContent do + @moduledoc """ + Display the content for a step + """ + use MemexWeb, :component + alias Memex.Pipelines.Steps.Step + alias Phoenix.HTML + + attr :step, Step, required: true + + def step_content(assigns) do + ~H""" +

<%= add_links_to_content(@step.content) %>

+ """ + end + + defp add_links_to_content(content) do + Regex.replace( + ~r/\[\[([\p{L}\p{N}\-]+)\]\]/, + content, + fn _whole_match, slug -> + link = + HTML.Link.link( + "[[#{slug}]]", + to: Routes.context_show_path(Endpoint, :show, slug), + class: "link inline", + data: [qa: "step-context-#{slug}"] + ) + |> HTML.Safe.to_iodata() + |> IO.iodata_to_binary() + + "

#{link}

" + end + ) + |> HTML.raw() + end +end diff --git a/lib/memex_web/live/pipeline_live/show.ex b/lib/memex_web/live/pipeline_live/show.ex index bc2a609..8fe07a7 100644 --- a/lib/memex_web/live/pipeline_live/show.ex +++ b/lib/memex_web/live/pipeline_live/show.ex @@ -1,7 +1,8 @@ defmodule MemexWeb.PipelineLive.Show do use MemexWeb, :live_view - - alias Memex.{Accounts.User, Pipelines, Pipelines.Pipeline} + import MemexWeb.Components.StepContent + alias Memex.{Accounts.User, Pipelines} + alias Memex.Pipelines.{Pipeline, Steps, Steps.Step} @impl true def mount(_params, _session, socket) do @@ -10,9 +11,9 @@ defmodule MemexWeb.PipelineLive.Show do @impl true def handle_params( - %{"slug" => slug}, - _, - %{assigns: %{live_action: live_action, current_user: current_user}} = socket + %{"slug" => slug} = params, + _url, + %{assigns: %{current_user: current_user, live_action: live_action}} = socket ) do pipeline = case Pipelines.get_pipeline_by_slug(slug, current_user) do @@ -24,10 +25,45 @@ defmodule MemexWeb.PipelineLive.Show do socket |> assign(:page_title, page_title(live_action, pipeline)) |> assign(:pipeline, pipeline) + |> assign(:steps, pipeline |> Steps.list_steps(current_user)) + |> apply_action(live_action, params) {:noreply, socket} end + defp apply_action(socket, live_action, _params) when live_action in [:show, :edit] do + socket + end + + defp apply_action( + %{ + assigns: %{ + steps: steps, + pipeline: %{id: pipeline_id}, + current_user: %{id: current_user_id} + } + } = socket, + :add_step, + _params + ) do + socket + |> assign( + step: %Step{ + position: steps |> Enum.count(), + pipeline_id: pipeline_id, + user_id: current_user_id + } + ) + end + + defp apply_action( + %{assigns: %{current_user: current_user}} = socket, + :edit_step, + %{"step_id" => step_id} + ) do + socket |> assign(step: step_id |> Steps.get_step!(current_user)) + end + @impl true def handle_event( "delete", @@ -44,8 +80,51 @@ defmodule MemexWeb.PipelineLive.Show do {:noreply, socket} end + @impl true + def handle_event( + "delete_step", + %{"step-id" => step_id}, + %{assigns: %{pipeline: %{slug: pipeline_slug}, current_user: current_user}} = socket + ) do + {:ok, %{title: title}} = + step_id + |> Steps.get_step!(current_user) + |> Steps.delete_step(current_user) + + socket = + socket + |> put_flash(:info, gettext("%{title} deleted", title: title)) + |> push_patch(to: Routes.pipeline_show_path(Endpoint, :show, pipeline_slug)) + + {:noreply, socket} + end + + @impl true + def handle_event( + "reorder_step", + %{"step-id" => step_id, "direction" => direction}, + %{assigns: %{pipeline: %{slug: pipeline_slug}, current_user: current_user}} = socket + ) do + direction = if direction == "up", do: :up, else: :down + + {:ok, _step} = + step_id + |> Steps.get_step!(current_user) + |> Steps.reorder_step(direction, current_user) + + socket = + socket + |> push_patch(to: Routes.pipeline_show_path(Endpoint, :show, pipeline_slug)) + + {:noreply, socket} + end + defp page_title(:show, %{slug: slug}), do: slug - defp page_title(:edit, %{slug: slug}), do: gettext("edit %{slug}", slug: slug) + + defp page_title(live_action, %{slug: slug}) when live_action in [:edit, :edit_step], + do: gettext("edit %{slug}", slug: slug) + + defp page_title(:add_step, %{slug: slug}), do: gettext("add step to %{slug}", slug: slug) @spec is_owner_or_admin?(Pipeline.t(), User.t()) :: boolean() defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true diff --git a/lib/memex_web/live/pipeline_live/show.html.heex b/lib/memex_web/live/pipeline_live/show.html.heex index 102e4b4..4072ba6 100644 --- a/lib/memex_web/live/pipeline_live/show.html.heex +++ b/lib/memex_web/live/pipeline_live/show.html.heex @@ -5,20 +5,22 @@

<%= if @pipeline.tags, do: @pipeline.tags |> Enum.join(", ") %>

- + <%= if @pipeline.description do %> + + <% end %>

<%= gettext("Visibility: %{visibility}", visibility: @pipeline.visibility) %>

-
+
<.link class="btn btn-primary" navigate={Routes.pipeline_index_path(@socket, :index)}> <%= dgettext("actions", "back") %> @@ -42,18 +44,131 @@ <% end %>
+ +
+ +

+ <%= gettext("steps:") %> +

+ + <%= if @steps |> Enum.empty?() do %> +

+ <%= gettext("no steps") %> +

+ <% else %> + <%= for %{id: step_id, position: position, title: title} = step <- @steps do %> +
+

+ <%= gettext("%{position}. %{title}", position: position + 1, title: title) %> +

+ + <%= if is_owner?(@pipeline, @current_user) do %> +
+ <%= if position <= 0 do %> + + <% else %> + + <% end %> + + <%= if position >= length(@steps) - 1 do %> + + <% else %> + + <% end %> + + <.link + class="self-end btn btn-primary" + patch={Routes.pipeline_show_path(@socket, :edit_step, @pipeline.slug, step_id)} + data-qa={"edit-step-#{step_id}"} + > + <%= dgettext("actions", "edit") %> + + + +
+ <% end %> +
+ + <.step_content step={step} /> + <% end %> + <% end %> + + <%= if is_owner?(@pipeline, @current_user) do %> + <.link + class="self-end btn btn-primary" + patch={Routes.pipeline_show_path(@socket, :add_step, @pipeline.slug)} + data-qa={"add-step-#{@pipeline.id}"} + > + <%= dgettext("actions", "add step") %> + + <% end %>
-<%= if @live_action in [:edit] do %> - <.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}> - <.live_component - module={MemexWeb.PipelineLive.FormComponent} - id={@pipeline.id} - current_user={@current_user} - title={@page_title} - action={@live_action} - pipeline={@pipeline} - return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)} - /> - +<%= case @live_action do %> + <% :edit -> %> + <.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}> + <.live_component + module={MemexWeb.PipelineLive.FormComponent} + id={@pipeline.id} + current_user={@current_user} + title={@page_title} + action={@live_action} + pipeline={@pipeline} + return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)} + /> + + <% :add_step -> %> + <.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}> + <.live_component + module={MemexWeb.StepLive.FormComponent} + id={@pipeline.id || :new} + current_user={@current_user} + title={@page_title} + action={@live_action} + pipeline={@pipeline} + step={@step} + return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)} + /> + + <% :edit_step -> %> + <.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}> + <.live_component + module={MemexWeb.StepLive.FormComponent} + id={@pipeline.id || :new} + current_user={@current_user} + title={@page_title} + action={@live_action} + pipeline={@pipeline} + step={@step} + return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)} + /> + + <% _ -> %> <% end %> diff --git a/lib/memex_web/live/step_live/form_component.ex b/lib/memex_web/live/step_live/form_component.ex new file mode 100644 index 0000000..32ab236 --- /dev/null +++ b/lib/memex_web/live/step_live/form_component.ex @@ -0,0 +1,74 @@ +defmodule MemexWeb.StepLive.FormComponent do + use MemexWeb, :live_component + + alias Memex.Pipelines.Steps + + @impl true + def update(%{step: step, current_user: current_user, pipeline: _pipeline} = assigns, socket) do + changeset = Steps.change_step(step, current_user) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event( + "validate", + %{"step" => step_params}, + %{assigns: %{step: step, current_user: current_user}} = socket + ) do + changeset = + step + |> Steps.change_step(step_params, current_user) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"step" => step_params}, %{assigns: %{action: action}} = socket) do + save_step(socket, action, step_params) + end + + defp save_step( + %{assigns: %{step: step, return_to: return_to, current_user: current_user}} = socket, + :edit_step, + step_params + ) do + case Steps.update_step(step, step_params, current_user) do + {:ok, %{title: title}} -> + {:noreply, + socket + |> put_flash(:info, gettext("%{title} saved", title: title)) + |> push_navigate(to: return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_step( + %{ + assigns: %{ + step: %{position: position}, + return_to: return_to, + current_user: current_user, + pipeline: pipeline + } + } = socket, + :add_step, + step_params + ) do + case Steps.create_step(step_params, position, pipeline, current_user) do + {:ok, %{title: title}} -> + {:noreply, + socket + |> put_flash(:info, gettext("%{title} created", title: title)) + |> push_navigate(to: return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/lib/memex_web/live/step_live/form_component.html.heex b/lib/memex_web/live/step_live/form_component.html.heex new file mode 100644 index 0000000..19623df --- /dev/null +++ b/lib/memex_web/live/step_live/form_component.html.heex @@ -0,0 +1,34 @@ +
+ <.form + :let={f} + for={@changeset} + id="step-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + phx-debounce="300" + class="flex flex-col justify-start items-stretch space-y-4" + > + <%= text_input(f, :title, + class: "input input-primary", + placeholder: gettext("title") + ) %> + <%= error_tag(f, :title) %> + + <%= textarea(f, :content, + id: "step-form-content", + class: "input input-primary h-64 min-h-64", + phx_hook: "MaintainAttrs", + phx_update: "ignore", + placeholder: gettext("use [[context-slug]] to link to a context") + ) %> + <%= error_tag(f, :content) %> + +
+ <%= submit(dgettext("actions", "save"), + phx_disable_with: gettext("saving..."), + class: "mx-auto btn btn-primary" + ) %> +
+ +
diff --git a/lib/memex_web/router.ex b/lib/memex_web/router.ex index e8e795c..f20f3c5 100644 --- a/lib/memex_web/router.ex +++ b/lib/memex_web/router.ex @@ -69,6 +69,8 @@ defmodule MemexWeb.Router do live "/pipelines/new", PipelineLive.Index, :new live "/pipelines/:slug/edit", PipelineLive.Index, :edit live "/pipeline/:slug/edit", PipelineLive.Show, :edit + live "/pipeline/:slug/add_step", PipelineLive.Show, :add_step + live "/pipeline/:slug/:step_id", PipelineLive.Show, :edit_step get "/users/settings", UserSettingsController, :edit put "/users/settings", UserSettingsController, :update diff --git a/priv/gettext/actions.pot b/priv/gettext/actions.pot index 4500be4..01cee7f 100644 --- a/priv/gettext/actions.pot +++ b/priv/gettext/actions.pot @@ -72,7 +72,8 @@ msgstr "" #: lib/memex_web/live/note_live/index.html.heex:49 #: lib/memex_web/live/note_live/show.html.heex:38 #: lib/memex_web/live/pipeline_live/index.html.heex:49 -#: lib/memex_web/live/pipeline_live/show.html.heex:41 +#: lib/memex_web/live/pipeline_live/show.html.heex:43 +#: lib/memex_web/live/pipeline_live/show.html.heex:113 #, elixir-autogen, elixir-format msgid "delete" msgstr "" @@ -87,7 +88,8 @@ msgstr "" #: lib/memex_web/live/note_live/index.html.heex:38 #: lib/memex_web/live/note_live/show.html.heex:27 #: lib/memex_web/live/pipeline_live/index.html.heex:38 -#: lib/memex_web/live/pipeline_live/show.html.heex:30 +#: lib/memex_web/live/pipeline_live/show.html.heex:32 +#: lib/memex_web/live/pipeline_live/show.html.heex:102 #, elixir-autogen, elixir-format msgid "edit" msgstr "" @@ -137,13 +139,19 @@ msgstr "" #: lib/memex_web/live/context_live/form_component.html.heex:42 #: lib/memex_web/live/note_live/form_component.html.heex:42 #: lib/memex_web/live/pipeline_live/form_component.html.heex:42 +#: lib/memex_web/live/step_live/form_component.html.heex:28 #, elixir-autogen, elixir-format msgid "save" msgstr "" #: lib/memex_web/live/context_live/show.html.heex:16 #: lib/memex_web/live/note_live/show.html.heex:23 -#: lib/memex_web/live/pipeline_live/show.html.heex:23 +#: lib/memex_web/live/pipeline_live/show.html.heex:25 #, elixir-autogen, elixir-format msgid "back" msgstr "" + +#: lib/memex_web/live/pipeline_live/show.html.heex:129 +#, elixir-autogen, elixir-format +msgid "add step" +msgstr "" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 219a105..3cb050d 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -82,7 +82,7 @@ msgstr "" #: lib/memex_web/live/context_live/show.html.heex:11 #: lib/memex_web/live/note_live/show.html.heex:18 -#: lib/memex_web/live/pipeline_live/show.html.heex:18 +#: lib/memex_web/live/pipeline_live/show.html.heex:20 #, elixir-autogen, elixir-format msgid "Visibility: %{visibility}" msgstr "" @@ -309,6 +309,7 @@ msgstr "" #: lib/memex_web/live/context_live/form_component.html.heex:43 #: lib/memex_web/live/note_live/form_component.html.heex:43 #: lib/memex_web/live/pipeline_live/form_component.html.heex:43 +#: lib/memex_web/live/step_live/form_component.html.heex:29 #, elixir-autogen, elixir-format msgid "saving..." msgstr "" @@ -426,7 +427,7 @@ msgstr "" #: lib/memex_web/live/note_live/index.ex:57 #: lib/memex_web/live/note_live/show.ex:41 #: lib/memex_web/live/pipeline_live/index.ex:57 -#: lib/memex_web/live/pipeline_live/show.ex:41 +#: lib/memex_web/live/pipeline_live/show.ex:77 #, elixir-autogen, elixir-format msgid "%{slug} deleted" msgstr "" @@ -443,7 +444,7 @@ msgstr "" #: lib/memex_web/live/note_live/index.ex:23 #: lib/memex_web/live/note_live/show.ex:48 #: lib/memex_web/live/pipeline_live/index.ex:23 -#: lib/memex_web/live/pipeline_live/show.ex:48 +#: lib/memex_web/live/pipeline_live/show.ex:125 #, elixir-autogen, elixir-format msgid "edit %{slug}" msgstr "" @@ -460,7 +461,7 @@ msgstr "" #: lib/memex_web/live/context_live/show.ex:19 #: lib/memex_web/live/note_live/show.ex:19 -#: lib/memex_web/live/pipeline_live/show.ex:19 +#: lib/memex_web/live/pipeline_live/show.ex:20 #, elixir-autogen, elixir-format msgid "%{slug} could not be found" msgstr "" @@ -503,3 +504,48 @@ msgstr "" #, elixir-autogen, elixir-format msgid "what is this?" msgstr "" + +#: lib/memex_web/live/pipeline_live/show.html.heex:62 +#, elixir-autogen, elixir-format +msgid "%{position}. %{title}" +msgstr "" + +#: lib/memex_web/live/step_live/form_component.ex:67 +#, elixir-autogen, elixir-format +msgid "%{title} created" +msgstr "" + +#: lib/memex_web/live/pipeline_live/show.ex:96 +#, elixir-autogen, elixir-format +msgid "%{title} deleted" +msgstr "" + +#: lib/memex_web/live/step_live/form_component.ex:43 +#, elixir-autogen, elixir-format +msgid "%{title} saved" +msgstr "" + +#: lib/memex_web/live/pipeline_live/show.ex:127 +#, elixir-autogen, elixir-format +msgid "add step to %{slug}" +msgstr "" + +#: lib/memex_web/live/pipeline_live/show.html.heex:56 +#, elixir-autogen, elixir-format +msgid "no steps" +msgstr "" + +#: lib/memex_web/live/pipeline_live/show.html.heex:51 +#, elixir-autogen, elixir-format +msgid "steps:" +msgstr "" + +#: lib/memex_web/live/step_live/form_component.html.heex:14 +#, elixir-autogen, elixir-format +msgid "title" +msgstr "" + +#: lib/memex_web/live/step_live/form_component.html.heex:23 +#, elixir-autogen, elixir-format +msgid "use [[context-slug]] to link to a context" +msgstr "" diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot index 3217299..355ae8f 100644 --- a/priv/gettext/prompts.pot +++ b/priv/gettext/prompts.pot @@ -146,7 +146,8 @@ msgstr "" #: lib/memex_web/live/note_live/index.html.heex:46 #: lib/memex_web/live/note_live/show.html.heex:35 #: lib/memex_web/live/pipeline_live/index.html.heex:46 -#: lib/memex_web/live/pipeline_live/show.html.heex:38 +#: lib/memex_web/live/pipeline_live/show.html.heex:40 +#: lib/memex_web/live/pipeline_live/show.html.heex:110 #, elixir-autogen, elixir-format msgid "are you sure?" msgstr "" diff --git a/test/memex/steps_test.exs b/test/memex/steps_test.exs index ab8467c..486cfef 100644 --- a/test/memex/steps_test.exs +++ b/test/memex/steps_test.exs @@ -1,7 +1,7 @@ defmodule Memex.StepsTest do use Memex.DataCase import Memex.{PipelinesFixtures, StepsFixtures} - alias Memex.Pipelines.{Step, Steps} + alias Memex.Pipelines.{Steps, Steps.Step} @moduletag :steps_test @invalid_attrs %{content: nil, title: nil} @@ -13,19 +13,19 @@ defmodule Memex.StepsTest do [user: user, pipeline: pipeline] end - test "list_steps/1 returns all steps for a user", %{pipeline: pipeline, user: user} do + test "list_steps/2 returns all steps for a user", %{pipeline: pipeline, user: user} do step_a = step_fixture(0, pipeline, user) step_b = step_fixture(1, pipeline, user) step_c = step_fixture(2, pipeline, user) assert Steps.list_steps(pipeline, user) == [step_a, step_b, step_c] end - test "get_step!/1 returns the step with given id", %{pipeline: pipeline, user: user} do + test "get_step!/2 returns the step with given id", %{pipeline: pipeline, user: user} do step = step_fixture(0, pipeline, user) assert Steps.get_step!(step.id, user) == step end - test "get_step!/1 only returns unlisted or public steps for other users", %{user: user} do + test "get_step!/2 only returns unlisted or public steps for other users", %{user: user} do another_user = user_fixture() another_pipeline = pipeline_fixture(another_user) step = step_fixture(0, another_pipeline, another_user) @@ -35,7 +35,7 @@ defmodule Memex.StepsTest do end end - test "create_step/1 with valid data creates a step", %{pipeline: pipeline, user: user} do + test "create_step/4 with valid data creates a step", %{pipeline: pipeline, user: user} do valid_attrs = %{ "content" => "some content", "title" => "some title" @@ -46,12 +46,12 @@ defmodule Memex.StepsTest do assert step.title == "some title" end - test "create_step/1 with invalid data returns error changeset", + test "create_step/4 with invalid data returns error changeset", %{pipeline: pipeline, user: user} do assert {:error, %Ecto.Changeset{}} = Steps.create_step(@invalid_attrs, 0, pipeline, user) end - test "update_step/2 with valid data updates the step", %{pipeline: pipeline, user: user} do + test "update_step/3 with valid data updates the step", %{pipeline: pipeline, user: user} do step = step_fixture(0, pipeline, user) update_attrs = %{ @@ -59,36 +59,90 @@ defmodule Memex.StepsTest do "title" => "some updated title" } - assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs, 0, user) + assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs, user) assert step.content == "some updated content" assert step.title == "some updated title" end - test "update_step/2 with invalid data returns error changeset", %{ + test "update_step/3 with invalid data returns error changeset", %{ pipeline: pipeline, user: user } do step = step_fixture(0, pipeline, user) - assert {:error, %Ecto.Changeset{}} = Steps.update_step(step, @invalid_attrs, 0, user) + assert {:error, %Ecto.Changeset{}} = Steps.update_step(step, @invalid_attrs, user) assert step == Steps.get_step!(step.id, user) end - test "delete_step/1 deletes the step", %{pipeline: pipeline, user: user} do + test "delete_step/2 deletes the step", %{pipeline: pipeline, user: user} do step = step_fixture(0, pipeline, user) assert {:ok, %Step{}} = Steps.delete_step(step, user) assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id, user) end end - test "delete_step/1 deletes the step for an admin user", %{pipeline: pipeline, user: user} do + test "delete_step/2 moves past steps up", %{pipeline: pipeline, user: user} do + first_step = step_fixture(0, pipeline, user) + second_step = step_fixture(1, pipeline, user) + assert {:ok, %Step{}} = Steps.delete_step(first_step, user) + assert %{position: 0} = second_step |> Repo.reload!() + end + + test "delete_step/2 deletes the step for an admin user", %{pipeline: pipeline, user: user} do admin_user = admin_fixture() step = step_fixture(0, pipeline, user) assert {:ok, %Step{}} = Steps.delete_step(step, admin_user) assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id, user) end end + test "change_step/2 returns a step changeset", %{pipeline: pipeline, user: user} do + step = step_fixture(0, pipeline, user) + assert %Ecto.Changeset{} = Steps.change_step(step, user) + end + test "change_step/1 returns a step changeset", %{pipeline: pipeline, user: user} do step = step_fixture(0, pipeline, user) - assert %Ecto.Changeset{} = Steps.change_step(step, 0, user) + assert %Ecto.Changeset{} = Steps.change_step(step, user) + end + + test "reorder_step/1 reorders steps properly", %{pipeline: pipeline, user: user} do + [ + %{id: first_step_id} = first_step, + %{id: second_step_id} = second_step, + %{id: third_step_id} = third_step + ] = Enum.map(0..2, fn index -> step_fixture(index, pipeline, user) end) + + Steps.reorder_step(third_step, :up, user) + + assert [ + %{id: ^first_step_id, position: 0}, + %{id: ^third_step_id, position: 1}, + %{id: ^second_step_id, position: 2} + ] = Steps.list_steps(pipeline, user) + + Steps.reorder_step(first_step, :up, user) + + assert [ + %{id: ^first_step_id, position: 0}, + %{id: ^third_step_id, position: 1}, + %{id: ^second_step_id, position: 2} + ] = Steps.list_steps(pipeline, user) + + second_step + |> Repo.reload!() + |> Steps.reorder_step(:down, user) + + assert [ + %{id: ^first_step_id, position: 0}, + %{id: ^third_step_id, position: 1}, + %{id: ^second_step_id, position: 2} + ] = Steps.list_steps(pipeline, user) + + Steps.reorder_step(first_step, :down, user) + + assert [ + %{id: ^third_step_id, position: 0}, + %{id: ^first_step_id, position: 1}, + %{id: ^second_step_id, position: 2} + ] = Steps.list_steps(pipeline, user) end end end diff --git a/test/memex_web/live/pipeline_live_test.exs b/test/memex_web/live/pipeline_live_test.exs index afa41c3..08dd23b 100644 --- a/test/memex_web/live/pipeline_live_test.exs +++ b/test/memex_web/live/pipeline_live_test.exs @@ -1,8 +1,7 @@ defmodule MemexWeb.PipelineLiveTest do use MemexWeb.ConnCase - import Phoenix.LiveViewTest - import Memex.PipelinesFixtures + import Memex.{PipelinesFixtures, StepsFixtures} @create_attrs %{ "description" => "some description", @@ -22,6 +21,18 @@ defmodule MemexWeb.PipelineLiveTest do "slug" => nil, "visibility" => nil } + @step_create_attrs %{ + "content" => "some content", + "title" => "some title" + } + @step_update_attrs %{ + "content" => "some updated content", + "title" => "some updated title" + } + @step_invalid_attrs %{ + "content" => nil, + "title" => nil + } defp create_pipeline(%{user: user}) do [pipeline: pipeline_fixture(user)] @@ -134,5 +145,110 @@ defmodule MemexWeb.PipelineLiveTest do refute has_element?(index_live, "#pipeline-#{pipeline.id}") end + + test "creates a step", %{conn: conn, pipeline: pipeline} do + {:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + show_live + |> element("[data-qa=\"add-step-#{pipeline.id}\"]") + |> render_click() + + assert_patch(show_live, Routes.pipeline_show_path(conn, :add_step, pipeline.slug)) + + {:ok, _show_live, html} = + show_live + |> form("#step-form", step: @step_create_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + assert html =~ "some title created" + assert html =~ "some description" + end + end + + describe "show with a step" do + setup [:register_and_log_in_user, :create_pipeline] + + setup %{pipeline: pipeline, user: user} do + [ + step: step_fixture(0, pipeline, user) + ] + end + + test "updates a step", %{conn: conn, pipeline: pipeline, step: step} do + {:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + show_live + |> element("[data-qa=\"edit-step-#{step.id}\"]") + |> render_click() + + assert_patch(show_live, Routes.pipeline_show_path(conn, :edit_step, pipeline.slug, step.id)) + + assert show_live + |> form("#step-form", step: @step_invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _show_live, html} = + show_live + |> form("#step-form", step: @step_update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + assert html =~ "some updated title saved" + assert html =~ "some updated content" + end + + test "deletes a step", %{conn: conn, pipeline: pipeline, step: step} do + {:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + html = + show_live + |> element("[data-qa=\"delete-step-#{step.id}\"]") + |> render_click() + + assert_patch(show_live, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + assert html =~ "some title deleted" + refute html =~ "some updated content" + end + end + + describe "show with multiple steps" do + setup [:register_and_log_in_user, :create_pipeline] + + setup %{pipeline: pipeline, user: user} do + [ + first_step: step_fixture(%{title: "first step"}, 0, pipeline, user), + second_step: step_fixture(%{title: "second step"}, 1, pipeline, user), + third_step: step_fixture(%{title: "third step"}, 2, pipeline, user) + ] + end + + test "reorders a step", + %{conn: conn, pipeline: pipeline, first_step: first_step, second_step: second_step} do + {:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug)) + + html = + show_live + |> element("[data-qa=\"move-step-up-#{second_step.id}\"]") + |> render_click() + + assert html =~ "1. second step" + assert html =~ "2. first step" + assert html =~ "3. third step" + + refute has_element?(show_live, "[data-qa=\"move-step-up-#{second_step.id}\"]") + + html = + show_live + |> element("[data-qa=\"move-step-down-#{first_step.id}\"]") + |> render_click() + + assert html =~ "1. second step" + assert html =~ "2. third step" + assert html =~ "3. first step" + + refute has_element?(show_live, "[data-qa=\"move-step-down-#{first_step.id}\"]") + end end end