From e0f0e393263ae5d31003cf9a1cf5bc7ee727cc0d Mon Sep 17 00:00:00 2001 From: shibao Date: Thu, 17 Nov 2022 22:38:52 -0500 Subject: [PATCH] work on notes --- assets/js/maintain_attrs.js | 20 ++- lib/memex/notes.ex | 102 +++++++++---- lib/memex/notes/note.ex | 42 +++++- lib/memex_web/components/note_card.ex | 29 ++++ .../components/notes_table_component.ex | 136 ++++++++++++++++++ .../live/note_live/form_component.ex | 42 ++++-- .../live/note_live/form_component.html.heex | 49 ++++--- lib/memex_web/live/note_live/index.ex | 39 ++--- lib/memex_web/live/note_live/index.html.heex | 98 ++++++------- lib/memex_web/live/note_live/show.ex | 10 +- lib/memex_web/live/note_live/show.html.heex | 67 +++++---- .../20220726000552_create_notes.exs | 4 +- test/memex/notes_test.exs | 90 +++++++----- test/memex_web/live/context_live_test.exs | 24 +++- test/memex_web/live/note_live_test.exs | 35 +++-- test/support/fixtures/notes_fixtures.ex | 9 +- 16 files changed, 561 insertions(+), 235 deletions(-) create mode 100644 lib/memex_web/components/note_card.ex create mode 100644 lib/memex_web/components/notes_table_component.ex diff --git a/assets/js/maintain_attrs.js b/assets/js/maintain_attrs.js index f4ff238..ae1a194 100644 --- a/assets/js/maintain_attrs.js +++ b/assets/js/maintain_attrs.js @@ -2,7 +2,21 @@ // update. https://github.com/phoenixframework/phoenix_live_view/issues/1011 export default { - attrs () { return this.el.getAttribute('data-attrs').split(', ') }, - beforeUpdate () { this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) }, - updated () { this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) } + attrs () { + if (this.el && this.el.getAttribute('data-attrs')) { + return this.el.getAttribute('data-attrs').split(', ') + } else { + return [] + } + }, + beforeUpdate () { + if (this.el) { + this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) + } + }, + updated () { + if (this.el) { + this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) + } + } } diff --git a/lib/memex/notes.ex b/lib/memex/notes.ex index 6670bb1..8a3602d 100644 --- a/lib/memex/notes.ex +++ b/lib/memex/notes.ex @@ -4,21 +4,34 @@ defmodule Memex.Notes do """ import Ecto.Query, warn: false - alias Memex.Repo - - alias Memex.Notes.Note + alias Ecto.Changeset + alias Memex.{Accounts.User, Notes.Note, Repo} @doc """ Returns the list of notes. ## Examples - iex> list_notes() + iex> list_notes(%User{id: 123}) [%Note{}, ...] """ - def list_notes do - Repo.all(Note) + @spec list_notes(User.t() | nil) :: [Note.t()] + def list_notes(%{id: user_id}) do + Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.title) + end + + @doc """ + Returns the list of public notes for viewing + + ## Examples + + iex> list_public_notes() + [%Note{}, ...] + """ + @spec list_public_notes() :: [Note.t()] + def list_public_notes do + Repo.all(from n in Note, where: n.visibility == :public, order_by: n.title) end @doc """ @@ -28,31 +41,46 @@ defmodule Memex.Notes do ## Examples - iex> get_note!(123) + iex> get_note!(123, %User{id: 123}) %Note{} - iex> get_note!(456) + iex> get_note!(456, %User{id: 123}) ** (Ecto.NoResultsError) """ - def get_note!(id), do: Repo.get!(Note, id) + @spec get_note!(Note.id(), User.t()) :: Note.t() + def get_note!(id, %{id: user_id}) do + Repo.one!( + from n in Note, + where: n.id == ^id, + where: n.user_id == ^user_id or n.visibility in [:public, :unlisted] + ) + end + + def get_note!(id, _invalid_user) do + Repo.one!( + from n in Note, + where: n.id == ^id, + where: n.visibility in [:public, :unlisted] + ) + end @doc """ Creates a note. ## Examples - iex> create_note(%{field: value}) + iex> create_note(%{field: value}, %User{id: 123}) {:ok, %Note{}} - iex> create_note(%{field: bad_value}) + iex> create_note(%{field: bad_value}, %User{id: 123}) {:error, %Ecto.Changeset{}} """ - def create_note(attrs \\ %{}) do - %Note{} - |> Note.changeset(attrs) - |> Repo.insert() + @spec create_note(User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()} + @spec create_note(attrs :: map(), User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()} + def create_note(attrs \\ %{}, user) do + Note.create_changeset(attrs, user) |> Repo.insert() end @doc """ @@ -60,16 +88,18 @@ defmodule Memex.Notes do ## Examples - iex> update_note(note, %{field: new_value}) + iex> update_note(note, %{field: new_value}, %User{id: 123}) {:ok, %Note{}} - iex> update_note(note, %{field: bad_value}) + iex> update_note(note, %{field: bad_value}, %User{id: 123}) {:error, %Ecto.Changeset{}} """ - def update_note(%Note{} = note, attrs) do + @spec update_note(Note.t(), attrs :: map(), User.t()) :: + {:ok, Note.t()} | {:error, Changeset.t()} + def update_note(%Note{} = note, attrs, user) do note - |> Note.changeset(attrs) + |> Note.update_changeset(attrs, user) |> Repo.update() end @@ -78,15 +108,16 @@ defmodule Memex.Notes do ## Examples - iex> delete_note(note) + iex> delete_note(note, %User{id: 123}) {:ok, %Note{}} - iex> delete_note(note) + iex> delete_note(note, %User{id: 123}) {:error, %Ecto.Changeset{}} """ - def delete_note(%Note{} = note) do - Repo.delete(note) + @spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()} + def delete_note(%Note{user_id: user_id} = note, %{id: user_id}) do + note |> Repo.delete() end @doc """ @@ -94,11 +125,30 @@ defmodule Memex.Notes do ## Examples - iex> change_note(note) + iex> change_note(note, %User{id: 123}) + %Ecto.Changeset{data: %Note{}} + + iex> change_note(note, %{title: "new title"}, %User{id: 123}) %Ecto.Changeset{data: %Note{}} """ - def change_note(%Note{} = note, attrs \\ %{}) do - Note.changeset(note, attrs) + @spec change_note(Note.t(), User.t()) :: Changeset.t(Note.t()) + @spec change_note(Note.t(), attrs :: map(), User.t()) :: Changeset.t(Note.t()) + def change_note(%Note{} = note, attrs \\ %{}, user) do + note |> Note.update_changeset(attrs, user) + end + + @doc """ + Gets a canonical string representation of the `:tags` field for a Note + """ + @spec get_tags_string(Note.t() | Changeset.t() | [String.t()] | nil) :: String.t() + def get_tags_string(nil), do: "" + def get_tags_string(tags) when tags |> is_list(), do: tags |> Enum.join(",") + def get_tags_string(%Note{tags: tags}), do: tags |> get_tags_string() + + def get_tags_string(%Changeset{} = changeset) do + changeset + |> Changeset.get_field(:tags) + |> get_tags_string() end end diff --git a/lib/memex/notes/note.ex b/lib/memex/notes/note.ex index 1175d5a..6df99ab 100644 --- a/lib/memex/notes/note.ex +++ b/lib/memex/notes/note.ex @@ -1,22 +1,54 @@ defmodule Memex.Notes.Note do + @moduledoc """ + Schema for a user-written note + """ use Ecto.Schema import Ecto.Changeset + alias Ecto.UUID + alias Memex.{Accounts.User, Notes.Note} @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "notes" do field :content, :string - field :tag, {:array, :string} + field :tags, {:array, :string} + field :tags_string, :string, virtual: true field :title, :string field :visibility, Ecto.Enum, values: [:public, :private, :unlisted] + belongs_to :user, User + timestamps() end + @type t :: %Note{} + @type id :: UUID.t() + @doc false - def changeset(note, attrs) do - note - |> cast(attrs, [:title, :content, :tag, :visibility]) - |> validate_required([:title, :content, :tag, :visibility]) + def create_changeset(attrs, %User{id: user_id}) do + %Note{} + |> cast(attrs, [:title, :content, :tags, :visibility]) + |> change(user_id: user_id) + |> cast_tags_string(attrs) + |> validate_required([:title, :content, :user_id, :visibility]) end + + def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do + note + |> cast(attrs, [:title, :content, :tags, :visibility]) + |> cast_tags_string(attrs) + |> validate_required([:title, :content, :visibility]) + end + + defp cast_tags_string(changeset, %{"tags_string" => tags_string}) when is_binary(tags_string) do + tags = + tags_string + |> String.split(",", trim: true) + |> Enum.map(fn str -> str |> String.trim() end) + |> Enum.sort() + + changeset |> change(tags: tags) + end + + defp cast_tags_string(changeset, _attrs), do: changeset end diff --git a/lib/memex_web/components/note_card.ex b/lib/memex_web/components/note_card.ex new file mode 100644 index 0000000..3f306e0 --- /dev/null +++ b/lib/memex_web/components/note_card.ex @@ -0,0 +1,29 @@ +defmodule MemexWeb.Components.NoteCard do + @moduledoc """ + Display card for an note + """ + + use MemexWeb, :component + + def note_card(assigns) do + ~H""" +
+

+ <%= @note.name %> +

+ +

+ <%= gettext("visibility: %{visibility}", visibility: @note.visibility) %> +

+ + <%= if @inner_block do %> +
+ <%= render_slot(@inner_block) %> +
+ <% end %> +
+ """ + end +end diff --git a/lib/memex_web/components/notes_table_component.ex b/lib/memex_web/components/notes_table_component.ex new file mode 100644 index 0000000..731f588 --- /dev/null +++ b/lib/memex_web/components/notes_table_component.ex @@ -0,0 +1,136 @@ +defmodule MemexWeb.Components.NotesTableComponent do + @moduledoc """ + A component that displays a list of notes + """ + use MemexWeb, :live_component + alias Ecto.UUID + alias Memex.{Accounts.User, Notes, Notes.Note} + alias MemexWeb.Endpoint + alias Phoenix.LiveView.{Rendered, Socket} + + @impl true + @spec update( + %{ + required(:id) => UUID.t(), + required(:current_user) => User.t(), + required(:notes) => [Note.t()], + optional(any()) => any() + }, + Socket.t() + ) :: {:ok, Socket.t()} + def update(%{id: _id, notes: _notes, current_user: _current_user} = assigns, socket) do + socket = + socket + |> assign(assigns) + |> assign_new(:actions, fn -> [] end) + |> display_notes() + + {:ok, socket} + end + + defp display_notes( + %{ + assigns: %{ + notes: notes, + current_user: current_user, + actions: actions + } + } = socket + ) do + columns = + if actions == [] or current_user |> is_nil() do + [] + else + [%{label: nil, key: :actions, sortable: false}] + end + + columns = [ + %{label: gettext("title"), key: :title}, + %{label: gettext("content"), key: :content}, + %{label: gettext("tags"), key: :tags}, + %{label: gettext("visibility"), key: :visibility} + | columns + ] + + rows = + notes + |> Enum.map(fn note -> + note + |> get_row_data_for_note(%{ + columns: columns, + current_user: current_user, + actions: actions + }) + end) + + socket |> assign(columns: columns, rows: rows) + end + + @impl true + def render(assigns) do + ~H""" +
+ <.live_component + module={MemexWeb.Components.TableComponent} + id={@id} + columns={@columns} + rows={@rows} + /> +
+ """ + end + + @spec get_row_data_for_note(Note.t(), additional_data :: map()) :: map() + defp get_row_data_for_note(note, %{columns: columns} = additional_data) do + columns + |> Map.new(fn %{key: key} -> + {key, get_value_for_key(key, note, additional_data)} + end) + end + + @spec get_value_for_key(atom(), Note.t(), additional_data :: map()) :: + any() | {any(), Rendered.t()} + defp get_value_for_key(:title, %{id: id, title: title}, _additional_data) do + assigns = %{id: id, title: title} + + title_block = ~H""" + <.link + navigate={Routes.note_show_path(Endpoint, :show, @id)} + class="link" + data-qa={"note-show-#{@id}"} + > + <%= @title %> + + """ + + {title, title_block} + end + + defp get_value_for_key(:content, %{content: content}, _additional_data) do + assigns = %{content: content} + + content_block = ~H""" +
+ <%= @content %> +
+ """ + + {content, content_block} + end + + defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do + tags |> Notes.get_tags_string() + end + + defp get_value_for_key(:actions, note, %{actions: actions}) do + assigns = %{actions: actions, note: note} + + ~H""" +
+ <%= render_slot(@actions, @note) %> +
+ """ + end + + defp get_value_for_key(key, note, _additional_data), do: note |> Map.get(key) +end diff --git a/lib/memex_web/live/note_live/form_component.ex b/lib/memex_web/live/note_live/form_component.ex index 8a0f640..01833f5 100644 --- a/lib/memex_web/live/note_live/form_component.ex +++ b/lib/memex_web/live/note_live/form_component.ex @@ -4,8 +4,8 @@ defmodule MemexWeb.NoteLive.FormComponent do alias Memex.Notes @impl true - def update(%{note: note} = assigns, socket) do - changeset = Notes.change_note(note) + def update(%{note: note, current_user: current_user} = assigns, socket) do + changeset = Notes.change_note(note, current_user) {:ok, socket @@ -14,39 +14,51 @@ defmodule MemexWeb.NoteLive.FormComponent do end @impl true - def handle_event("validate", %{"note" => note_params}, socket) do + def handle_event( + "validate", + %{"note" => note_params}, + %{assigns: %{note: note, current_user: current_user}} = socket + ) do changeset = - socket.assigns.note - |> Notes.change_note(note_params) + note + |> Notes.change_note(note_params, current_user) |> Map.put(:action, :validate) {:noreply, assign(socket, :changeset, changeset)} end - def handle_event("save", %{"note" => note_params}, socket) do - save_note(socket, socket.assigns.action, note_params) + def handle_event("save", %{"note" => note_params}, %{assigns: %{action: action}} = socket) do + save_note(socket, action, note_params) end - defp save_note(socket, :edit, note_params) do - case Notes.update_note(socket.assigns.note, note_params) do - {:ok, _note} -> + defp save_note( + %{assigns: %{note: note, return_to: return_to, current_user: current_user}} = socket, + :edit, + note_params + ) do + case Notes.update_note(note, note_params, current_user) do + {:ok, %{title: title}} -> {:noreply, socket |> put_flash(:info, gettext("%{title} saved", title: title)) - |> push_navigate(to: socket.assigns.return_to)} + |> push_navigate(to: return_to)} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, :changeset, changeset)} end end - defp save_note(socket, :new, note_params) do - case Notes.create_note(note_params) do - {:ok, _note} -> + defp save_note( + %{assigns: %{return_to: return_to, current_user: current_user}} = socket, + :new, + note_params + ) do + case Notes.create_note(note_params, current_user) do + {:ok, %{title: title}} -> {:noreply, socket |> put_flash(:info, gettext("%{title} created", title: title)) - |> push_navigate(to: socket.assigns.return_to)} + |> push_navigate(to: return_to)} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, changeset: changeset)} diff --git a/lib/memex_web/live/note_live/form_component.html.heex b/lib/memex_web/live/note_live/form_component.html.heex index f3d3cb0..75b588d 100644 --- a/lib/memex_web/live/note_live/form_component.html.heex +++ b/lib/memex_web/live/note_live/form_component.html.heex @@ -1,6 +1,4 @@ -
-

<%= @title %>

- +
<.form :let={f} for={@changeset} @@ -8,27 +6,44 @@ phx-target={@myself} phx-change="validate" phx-submit="save" + phx-debounce="300" + class="flex flex-col justify-start items-stretch space-y-4" > - <%= label(f, :title) %> - <%= text_input(f, :title) %> + <%= text_input(f, :title, + class: "input input-primary", + placeholder: gettext("title") + ) %> <%= error_tag(f, :title) %> - <%= label(f, :content) %> - <%= textarea(f, :content) %> + <%= textarea(f, :content, + id: "note-form-content", + class: "input input-primary h-64 min-h-64", + phx_hook: "MaintainAttrs", + phx_update: "ignore", + placeholder: gettext("content") + ) %> <%= error_tag(f, :content) %> - <%= label(f, :tag) %> - <%= multiple_select(f, :tag, "Option 1": "option1", "Option 2": "option2") %> - <%= error_tag(f, :tag) %> - - <%= label(f, :visibility) %> - <%= select(f, :visibility, Ecto.Enum.values(Memex.Notes.Note, :visibility), - prompt: "Choose a value" + <%= text_input(f, :tags_string, + id: "tags-input", + class: "input input-primary", + placeholder: gettext("tag1,tag2"), + phx_update: "ignore", + value: Notes.get_tags_string(@changeset) ) %> - <%= error_tag(f, :visibility) %> + <%= error_tag(f, :tags_string) %> -
- <%= submit("Save", phx_disable_with: "Saving...") %> +
+ <%= select(f, :visibility, Ecto.Enum.values(Memex.Notes.Note, :visibility), + class: "grow input input-primary", + prompt: gettext("select privacy") + ) %> + + <%= submit(dgettext("actions", "save"), + phx_disable_with: gettext("saving..."), + class: "mx-auto btn btn-primary" + ) %>
+ <%= error_tag(f, :visibility) %>
diff --git a/lib/memex_web/live/note_live/index.ex b/lib/memex_web/live/note_live/index.ex index 548905e..706f804 100644 --- a/lib/memex_web/live/note_live/index.ex +++ b/lib/memex_web/live/note_live/index.ex @@ -1,45 +1,52 @@ defmodule MemexWeb.NoteLive.Index do use MemexWeb, :live_view - - alias Memex.Notes - alias Memex.Notes.Note + alias Memex.{Notes, Notes.Note} @impl true + def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) + when not (current_user |> is_nil()) do + {:ok, socket |> assign(notes: Notes.list_notes(current_user))} + end + def mount(_params, _session, socket) do - {:ok, assign(socket, :notes, list_notes())} + {:ok, socket |> assign(notes: Notes.list_public_notes())} end @impl true - def handle_params(params, _url, socket) do - {:noreply, apply_action(socket, socket.assigns.live_action, params)} + def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do + {:noreply, apply_action(socket, live_action, params)} end - defp apply_action(socket, :edit, %{"id" => id}) do + defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do + %{title: title} = note = Notes.get_note!(id, current_user) + socket |> assign(page_title: gettext("edit %{title}", title: title)) - |> assign(:note, Notes.get_note!(id)) + |> assign(note: note) end - defp apply_action(socket, :new, _params) do + defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do socket |> assign(page_title: "new note") - |> assign(:note, %Note{}) + |> assign(note: %Note{user_id: current_user_id}) end defp apply_action(socket, :index, _params) do socket |> assign(page_title: "notes") - |> assign(:note, nil) + |> assign(note: nil) end @impl true - def handle_event("delete", %{"id" => id}, socket) do - note = Notes.get_note!(id) - {:ok, _} = Notes.delete_note(note) + def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do + %{title: title} = note = Notes.get_note!(id, current_user) + {:ok, _} = Notes.delete_note(note, current_user) + socket = + socket + |> assign(notes: Notes.list_notes(current_user)) |> put_flash(:info, gettext("%{title} deleted", title: title)) - defp list_notes do - Notes.list_notes() + {:noreply, socket} end end diff --git a/lib/memex_web/live/note_live/index.html.heex b/lib/memex_web/live/note_live/index.html.heex index 19a6eab..ceb95e2 100644 --- a/lib/memex_web/live/note_live/index.html.heex +++ b/lib/memex_web/live/note_live/index.html.heex @@ -1,10 +1,54 @@ -

Listing Notes

+
+

+ <%= gettext("notes") %> +

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

+ <%= gettext("no notes found") %> +

+ <% else %> + <.live_component + module={MemexWeb.Components.NotesTableComponent} + id="notes-index-table" + current_user={@current_user} + notes={@notes} + > + <:actions :let={note}> + <%= if @current_user do %> + <.link + patch={Routes.note_index_path(@socket, :edit, note)} + data-qa={"note-edit-#{note.id}"} + > + <%= dgettext("actions", "edit") %> + + <.link + href="#" + phx-click="delete" + phx-value-id={note.id} + data-confirm={dgettext("prompts", "are you sure?")} + data-qa={"delete-note-#{note.id}"} + > + <%= dgettext("actions", "delete") %> + + <% end %> + + + <% end %> + + <%= if @current_user do %> + <.link patch={Routes.note_index_path(@socket, :new)} class="self-end btn btn-primary"> + <%= dgettext("actions", "new note") %> + + <% end %> +
<%= if @live_action in [:new, :edit] do %> <.modal return_to={Routes.note_index_path(@socket, :index)}> <.live_component module={MemexWeb.NoteLive.FormComponent} id={@note.id || :new} + current_user={@current_user} title={@page_title} action={@live_action} note={@note} @@ -12,55 +56,3 @@ /> <% end %> - - - - - - - - - - - - - - <%= for note <- @notes do %> - - - - - - - - - <% end %> - -
TitleContentTagVisibility
<%= note.title %><%= note.content %><%= note.tag %><%= note.visibility %> - - <.link navigate={Routes.note_show_path(@socket, :show, note)}> - <%= dgettext("actions", "Show") %> - - - - <.link patch={Routes.note_index_path(@socket, :edit, note)}> - <%= dgettext("actions", "Edit") %> - - - - <.link - href="#" - phx-click="delete" - phx-value-id={note.id} - data-confirm={dgettext("prompts", "Are you sure?")} - > - <%= dgettext("actions", "Delete") %> - - -
- - - <.link patch={Routes.note_index_path(@socket, :new)}> - <%= dgettext("actions", "New Note") %> - - diff --git a/lib/memex_web/live/note_live/show.ex b/lib/memex_web/live/note_live/show.ex index 0854b1a..44d6f4b 100644 --- a/lib/memex_web/live/note_live/show.ex +++ b/lib/memex_web/live/note_live/show.ex @@ -9,11 +9,15 @@ defmodule MemexWeb.NoteLive.Show do end @impl true - def handle_params(%{"id" => id}, _, socket) do + def handle_params( + %{"id" => id}, + _, + %{assigns: %{live_action: live_action, current_user: current_user}} = socket + ) do {:noreply, socket - |> assign(:page_title, page_title(socket.assigns.live_action)) - |> assign(:note, Notes.get_note!(id))} + |> assign(:page_title, page_title(live_action)) + |> assign(:note, Notes.get_note!(id, current_user))} end defp page_title(:show), do: "show note" diff --git a/lib/memex_web/live/note_live/show.html.heex b/lib/memex_web/live/note_live/show.html.heex index fc5239b..8de70cf 100644 --- a/lib/memex_web/live/note_live/show.html.heex +++ b/lib/memex_web/live/note_live/show.html.heex @@ -1,10 +1,41 @@ -

Show Note

+
+

+ <%= @note.title %> +

+ +

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

+ + + +

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

+ +
+ <.link class="btn btn-primary" patch={Routes.note_index_path(@socket, :index)}> + <%= dgettext("actions", "Back") %> + + <%= if @current_user do %> + <.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note)}> + <%= dgettext("actions", "edit") %> + + <% end %> +
+
<%= if @live_action in [:edit] do %> <.modal return_to={Routes.note_show_path(@socket, :show, @note)}> <.live_component module={MemexWeb.NoteLive.FormComponent} id={@note.id} + current_user={@current_user} title={@page_title} action={@live_action} note={@note} @@ -12,37 +43,3 @@ /> <% end %> - -
    -
  • - Title: - <%= @note.title %> -
  • - -
  • - Content: - <%= @note.content %> -
  • - -
  • - Tag: - <%= @note.tag %> -
  • - -
  • - Visibility: - <%= @note.visibility %> -
  • -
- - - <.link patch={Routes.note_show_path(@socket, :edit, @note)} class="button"> - <%= dgettext("actions", "Edit") %> - - -| - - <.link patch={Routes.note_index_path(@socket, :index)}> - <%= dgettext("actions", "Back") %> - - diff --git a/priv/repo/migrations/20220726000552_create_notes.exs b/priv/repo/migrations/20220726000552_create_notes.exs index 8c6495a..1e4e0c6 100644 --- a/priv/repo/migrations/20220726000552_create_notes.exs +++ b/priv/repo/migrations/20220726000552_create_notes.exs @@ -6,9 +6,11 @@ defmodule Memex.Repo.Migrations.CreateNotes do add :id, :binary_id, primary_key: true add :title, :string add :content, :text - add :tag, {:array, :string} + add :tags, {:array, :string} add :visibility, :string + add :user_id, references(:users, on_delete: :delete_all, type: :binary_id) + timestamps() end end diff --git a/test/memex/notes_test.exs b/test/memex/notes_test.exs index 4b9ea29..4462278 100644 --- a/test/memex/notes_test.exs +++ b/test/memex/notes_test.exs @@ -1,71 +1,85 @@ defmodule Memex.NotesTest do use Memex.DataCase + import Memex.NotesFixtures + alias Memex.{Notes, Notes.Note} - alias Memex.Notes + @invalid_attrs %{content: nil, tag: nil, title: nil, visibility: nil} describe "notes" do - alias Memex.Notes.Note - - import Memex.NotesFixtures - - @invalid_attrs %{content: nil, tag: nil, title: nil, visibility: nil} - - test "list_notes/0 returns all notes" do - note = note_fixture() - assert Notes.list_notes() == [note] + setup do + [user: user_fixture()] end - test "get_note!/1 returns the note with given id" do - note = note_fixture() - assert Notes.get_note!(note.id) == note + test "list_notes/1 returns all notes for a user", %{user: user} do + note_a = note_fixture(%{title: "a", visibility: :public}, user) + note_b = note_fixture(%{title: "b", visibility: :unlisted}, user) + note_c = note_fixture(%{title: "c", visibility: :private}, user) + assert Notes.list_notes(user) == [note_a, note_b, note_c] end - test "create_note/1 with valid data creates a note" do - valid_attrs = %{content: "some content", tag: [], title: "some title", visibility: :public} + test "list_public_notes/0 returns public notes", %{user: user} do + public_note = note_fixture(%{visibility: :public}, user) + note_fixture(%{visibility: :unlisted}, user) + note_fixture(%{visibility: :private}, user) + assert Notes.list_public_notes() == [public_note] + end - assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs) + test "get_note!/1 returns the note with given id", %{user: user} do + note = note_fixture(user) + assert Notes.get_note!(note.id, user) == note + end + + test "create_note/1 with valid data creates a note", %{user: user} do + valid_attrs = %{ + "content" => "some content", + "tags_string" => "tag1,tag2", + "title" => "some title", + "visibility" => :public + } + + assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs, user) assert note.content == "some content" - assert note.tag == [] + assert note.tags == ["tag1", "tag2"] assert note.title == "some title" assert note.visibility == :public end - test "create_note/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Notes.create_note(@invalid_attrs) + test "create_note/1 with invalid data returns error changeset", %{user: user} do + assert {:error, %Ecto.Changeset{}} = Notes.create_note(@invalid_attrs, user) end - test "update_note/2 with valid data updates the note" do - note = note_fixture() + test "update_note/2 with valid data updates the note", %{user: user} do + note = note_fixture(user) update_attrs = %{ - content: "some updated content", - tag: [], - title: "some updated title", - visibility: :private + "content" => "some updated content", + "tags_string" => "tag1,tag2", + "title" => "some updated title", + "visibility" => :private } - assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs) + assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs, user) assert note.content == "some updated content" - assert note.tag == [] + assert note.tags == ["tag1", "tag2"] assert note.title == "some updated title" assert note.visibility == :private end - test "update_note/2 with invalid data returns error changeset" do - note = note_fixture() - assert {:error, %Ecto.Changeset{}} = Notes.update_note(note, @invalid_attrs) - assert note == Notes.get_note!(note.id) + test "update_note/2 with invalid data returns error changeset", %{user: user} do + note = note_fixture(user) + assert {:error, %Ecto.Changeset{}} = Notes.update_note(note, @invalid_attrs, user) + assert note == Notes.get_note!(note.id, user) end - test "delete_note/1 deletes the note" do - note = note_fixture() - assert {:ok, %Note{}} = Notes.delete_note(note) - assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id) end + test "delete_note/1 deletes the note", %{user: user} do + note = note_fixture(user) + assert {:ok, %Note{}} = Notes.delete_note(note, user) + assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end end - test "change_note/1 returns a note changeset" do - note = note_fixture() - assert %Ecto.Changeset{} = Notes.change_note(note) + test "change_note/1 returns a note changeset", %{user: user} do + note = note_fixture(user) + assert %Ecto.Changeset{} = Notes.change_note(note, user) end end end diff --git a/test/memex_web/live/context_live_test.exs b/test/memex_web/live/context_live_test.exs index 3529d75..2522147 100644 --- a/test/memex_web/live/context_live_test.exs +++ b/test/memex_web/live/context_live_test.exs @@ -4,14 +4,24 @@ defmodule MemexWeb.ContextLiveTest do import Phoenix.LiveViewTest import Memex.ContextsFixtures - @create_attrs %{content: "some content", tag: [], title: "some title", visibility: :public} - @update_attrs %{ - content: "some updated content", - tag: [], - title: "some updated title", - visibility: :private + @create_attrs %{ + "content" => "some content", + "tags_string" => "tag1", + "title" => "some title", + "visibility" => :public + } + @update_attrs %{ + "content" => "some updated content", + "tags_string" => "tag1,tag2", + "title" => "some updated title", + "visibility" => :private + } + @invalid_attrs %{ + "content" => nil, + "tags_string" => "", + "title" => nil, + "visibility" => nil } - @invalid_attrs %{content: nil, tag: [], title: nil, visibility: nil} defp create_context(_) do context = context_fixture() diff --git a/test/memex_web/live/note_live_test.exs b/test/memex_web/live/note_live_test.exs index 63bf0ee..7b70a57 100644 --- a/test/memex_web/live/note_live_test.exs +++ b/test/memex_web/live/note_live_test.exs @@ -4,22 +4,31 @@ defmodule MemexWeb.NoteLiveTest do import Phoenix.LiveViewTest import Memex.NotesFixtures - @create_attrs %{content: "some content", tag: [], title: "some title", visibility: :public} - @update_attrs %{ - content: "some updated content", - tag: [], - title: "some updated title", - visibility: :private + @create_attrs %{ + "content" => "some content", + "tags_string" => "tag1", + "title" => "some title", + "visibility" => :public + } + @update_attrs %{ + "content" => "some updated content", + "tags_string" => "tag1,tag2", + "title" => "some updated title", + "visibility" => :private + } + @invalid_attrs %{ + "content" => nil, + "tags_string" => "", + "title" => nil, + "visibility" => nil } - @invalid_attrs %{content: nil, tag: [], title: nil, visibility: nil} - defp create_note(_) do - note = note_fixture() - %{note: note} + defp create_note(%{user: user}) do + [note: note_fixture(user)] end describe "Index" do - setup [:create_note] + setup [:register_and_log_in_user, :create_note] test "lists all notes", %{conn: conn, note: note} do {:ok, _index_live, html} = live(conn, Routes.note_index_path(conn, :index)) @@ -53,7 +62,7 @@ defmodule MemexWeb.NoteLiveTest do test "updates note in listing", %{conn: conn, note: note} do {:ok, index_live, _html} = live(conn, Routes.note_index_path(conn, :index)) - assert index_live |> element("#note-#{note.id} a", "Edit") |> render_click() =~ + assert index_live |> element("[data-qa=\"note-edit-#{note.id}\"]") |> render_click() =~ "edit" assert_patch(index_live, Routes.note_index_path(conn, :edit, note)) @@ -75,7 +84,7 @@ defmodule MemexWeb.NoteLiveTest do test "deletes note in listing", %{conn: conn, note: note} do {:ok, index_live, _html} = live(conn, Routes.note_index_path(conn, :index)) - assert index_live |> element("#note-#{note.id} a", "Delete") |> render_click() + assert index_live |> element("[data-qa=\"delete-note-#{note.id}\"]") |> render_click() refute has_element?(index_live, "#note-#{note.id}") end end diff --git a/test/support/fixtures/notes_fixtures.ex b/test/support/fixtures/notes_fixtures.ex index 6548cc3..ee3266b 100644 --- a/test/support/fixtures/notes_fixtures.ex +++ b/test/support/fixtures/notes_fixtures.ex @@ -3,20 +3,23 @@ defmodule Memex.NotesFixtures do This module defines test helpers for creating entities via the `Memex.Notes` context. """ + alias Memex.{Accounts.User, Notes, Notes.Note} @doc """ Generate a note. """ - def note_fixture(attrs \\ %{}) do + @spec note_fixture(User.t()) :: Note.t() + @spec note_fixture(attrs :: map(), User.t()) :: Note.t() + def note_fixture(attrs \\ %{}, user) do {:ok, note} = attrs |> Enum.into(%{ content: "some content", tag: [], title: "some title", - visibility: :public + visibility: :private }) - |> Memex.Notes.create_note() + |> Notes.create_note(user) note end