From f9be5229e78d81e0496ad34e2f0ecd29463f1498 Mon Sep 17 00:00:00 2001 From: shibao Date: Sat, 19 Nov 2022 00:21:14 -0500 Subject: [PATCH] add search to notes --- lib/memex/notes.ex | 52 ++++++++++++++++++- lib/memex_web/live/note_live/index.ex | 35 +++++++++++-- lib/memex_web/live/note_live/index.html.heex | 11 ++++ lib/memex_web/router.ex | 5 +- priv/gettext/actions.pot | 6 +-- priv/gettext/default.pot | 6 +-- priv/gettext/prompts.pot | 2 +- .../20220726000552_create_notes.exs | 28 +++++++++- 8 files changed, 128 insertions(+), 17 deletions(-) diff --git a/lib/memex/notes.ex b/lib/memex/notes.ex index ecd0b21..ef04f80 100644 --- a/lib/memex/notes.ex +++ b/lib/memex/notes.ex @@ -20,10 +20,34 @@ defmodule Memex.Notes do """ @spec list_notes(User.t()) :: [Note.t()] - def list_notes(%{id: user_id}) do + @spec list_notes(search :: String.t() | nil, User.t()) :: [Note.t()] + def list_notes(search \\ nil, user) + + def list_notes(search, %{id: user_id}) when search |> is_nil() or search == "" do Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.title) end + def list_notes(search, %{id: user_id}) when search |> is_binary() do + trimmed_search = String.trim(search) + + Repo.all( + from n in Note, + where: n.user_id == ^user_id, + where: + fragment( + "search @@ to_tsquery(websearch_to_tsquery(?)::text || ':*')", + ^trimmed_search + ), + order_by: { + :desc, + fragment( + "ts_rank_cd(search, to_tsquery(websearch_to_tsquery(?)::text || ':*'), 4)", + ^trimmed_search + ) + } + ) + end + @doc """ Returns the list of public notes for viewing @@ -36,10 +60,34 @@ defmodule Memex.Notes do [%Note{title: "my note"}, ...] """ @spec list_public_notes() :: [Note.t()] - def list_public_notes do + @spec list_public_notes(search :: String.t() | nil) :: [Note.t()] + def list_public_notes(search \\ nil) + + def list_public_notes(search) when search |> is_nil() or search == "" do Repo.all(from n in Note, where: n.visibility == :public, order_by: n.title) end + def list_public_notes(search) when search |> is_binary() do + trimmed_search = String.trim(search) + + Repo.all( + from n in Note, + where: n.visibility == :public, + where: + fragment( + "search @@ to_tsquery(websearch_to_tsquery(?)::text || ':*')", + ^trimmed_search + ), + order_by: { + :desc, + fragment( + "ts_rank_cd(search, to_tsquery(websearch_to_tsquery(?)::text || ':*'), 4)", + ^trimmed_search + ) + } + ) + end + @doc """ Gets a single note. diff --git a/lib/memex_web/live/note_live/index.ex b/lib/memex_web/live/note_live/index.ex index 706f804..d2ff82f 100644 --- a/lib/memex_web/live/note_live/index.ex +++ b/lib/memex_web/live/note_live/index.ex @@ -3,13 +3,12 @@ defmodule MemexWeb.NoteLive.Index do 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))} + def mount(%{"search" => search}, _session, socket) do + {:ok, socket |> assign(search: search) |> display_notes()} end def mount(_params, _session, socket) do - {:ok, socket |> assign(notes: Notes.list_public_notes())} + {:ok, socket |> assign(search: nil) |> display_notes()} end @impl true @@ -34,7 +33,17 @@ defmodule MemexWeb.NoteLive.Index do defp apply_action(socket, :index, _params) do socket |> assign(page_title: "notes") + |> assign(search: nil) |> assign(note: nil) + |> display_notes() + end + + defp apply_action(socket, :search, %{"search" => search}) do + socket + |> assign(page_title: "notes") + |> assign(search: search) + |> assign(note: nil) + |> display_notes() end @impl true @@ -49,4 +58,22 @@ defmodule MemexWeb.NoteLive.Index do {:noreply, socket} end + + @impl true + def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do + {:noreply, socket |> push_patch(to: Routes.note_index_path(Endpoint, :index))} + end + + def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do + {:noreply, socket |> push_patch(to: Routes.note_index_path(Endpoint, :search, search_term))} + end + + defp display_notes(%{assigns: %{current_user: current_user, search: search}} = socket) + when not (current_user |> is_nil()) do + socket |> assign(notes: Notes.list_notes(search, current_user)) + end + + defp display_notes(%{assigns: %{search: search}} = socket) do + socket |> assign(notes: Notes.list_public_notes(search)) + 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 ceb95e2..233414d 100644 --- a/lib/memex_web/live/note_live/index.html.heex +++ b/lib/memex_web/live/note_live/index.html.heex @@ -3,6 +3,17 @@ <%= gettext("notes") %> + <.form + :let={f} + for={:search} + phx-change="search" + phx-submit="search" + phx-debounce="500" + class="self-stretch flex flex-col items-stretch" + > + <%= text_input(f, :search_term, class: "input input-primary", value: @search) %> + + <%= if @notes |> Enum.empty?() do %>

<%= gettext("no notes found") %> diff --git a/lib/memex_web/router.ex b/lib/memex_web/router.ex index e92c1b0..0b15cc6 100644 --- a/lib/memex_web/router.ex +++ b/lib/memex_web/router.ex @@ -59,7 +59,7 @@ defmodule MemexWeb.Router do live "/notes/new", NoteLive.Index, :new live "/notes/:id/edit", NoteLive.Index, :edit - live "/notes/:id/show/edit", NoteLive.Show, :edit + live "/note/:id/edit", NoteLive.Show, :edit live "/contexts/new", ContextLive.Index, :new live "/contexts/:id/edit", ContextLive.Index, :edit @@ -79,7 +79,8 @@ defmodule MemexWeb.Router do pipe_through [:browser] live "/notes", NoteLive.Index, :index - live "/notes/:id", NoteLive.Show, :show + live "/notes/:search", NoteLive.Index, :search + live "/note/:id", NoteLive.Show, :show live "/contexts", ContextLive.Index, :index live "/contexts/:id", ContextLive.Show, :show diff --git a/priv/gettext/actions.pot b/priv/gettext/actions.pot index 8ffe295..e2851dc 100644 --- a/priv/gettext/actions.pot +++ b/priv/gettext/actions.pot @@ -75,7 +75,7 @@ msgid "create invite" msgstr "" #: lib/memex_web/live/context_live/index.html.heex:53 -#: lib/memex_web/live/note_live/index.html.heex:32 +#: lib/memex_web/live/note_live/index.html.heex:43 #: lib/memex_web/live/pipeline_live/index.html.heex:51 #, elixir-autogen, elixir-format msgid "delete" @@ -88,7 +88,7 @@ msgstr "" #: lib/memex_web/live/context_live/index.html.heex:43 #: lib/memex_web/live/context_live/show.html.heex:40 -#: lib/memex_web/live/note_live/index.html.heex:23 +#: lib/memex_web/live/note_live/index.html.heex:34 #: lib/memex_web/live/note_live/show.html.heex:27 #: lib/memex_web/live/pipeline_live/index.html.heex:41 #: lib/memex_web/live/pipeline_live/show.html.heex:35 @@ -117,7 +117,7 @@ msgstr "" msgid "new context" msgstr "" -#: lib/memex_web/live/note_live/index.html.heex:41 +#: lib/memex_web/live/note_live/index.html.heex:52 #, elixir-autogen, elixir-format msgid "new note" msgstr "" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index d50b6d8..a36bc00 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -15,7 +15,7 @@ msgstr "" msgid "%{title} created" msgstr "" -#: lib/memex_web/live/note_live/index.ex:48 +#: lib/memex_web/live/note_live/index.ex:57 #, elixir-autogen, elixir-format msgid "%{title} deleted" msgstr "" @@ -167,7 +167,7 @@ msgstr "" msgid "document your processes, attaching contexts to each step" msgstr "" -#: lib/memex_web/live/note_live/index.ex:24 +#: lib/memex_web/live/note_live/index.ex:23 #, elixir-autogen, elixir-format msgid "edit %{title}" msgstr "" @@ -267,7 +267,7 @@ msgstr "" msgid "no invites 😔" msgstr "" -#: lib/memex_web/live/note_live/index.html.heex:8 +#: lib/memex_web/live/note_live/index.html.heex:19 #, elixir-autogen, elixir-format msgid "no notes found" msgstr "" diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot index f3b1669..0659604 100644 --- a/priv/gettext/prompts.pot +++ b/priv/gettext/prompts.pot @@ -142,7 +142,7 @@ msgid "are you sure you want to make %{invite_name} unlimited?" msgstr "" #: lib/memex_web/live/context_live/index.html.heex:51 -#: lib/memex_web/live/note_live/index.html.heex:29 +#: lib/memex_web/live/note_live/index.html.heex:40 #: lib/memex_web/live/pipeline_live/index.html.heex:49 #, elixir-autogen, elixir-format msgid "are you sure?" diff --git a/priv/repo/migrations/20220726000552_create_notes.exs b/priv/repo/migrations/20220726000552_create_notes.exs index 1e4e0c6..13d8da4 100644 --- a/priv/repo/migrations/20220726000552_create_notes.exs +++ b/priv/repo/migrations/20220726000552_create_notes.exs @@ -1,17 +1,41 @@ defmodule Memex.Repo.Migrations.CreateNotes do use Ecto.Migration - def change do + def up do create table(:notes, primary_key: false) do add :id, :binary_id, primary_key: true add :title, :string add :content, :text - add :tags, {:array, :string} + add :tags, {:array, :citext} add :visibility, :string add :user_id, references(:users, on_delete: :delete_all, type: :binary_id) timestamps() end + + flush() + + execute """ + CREATE FUNCTION immutable_array_to_string(text[], text) + RETURNS text LANGUAGE sql IMMUTABLE as $$SELECT array_to_string($1, $2)$$ + """ + + execute """ + ALTER TABLE notes + ADD COLUMN search tsvector + GENERATED ALWAYS AS ( + setweight(to_tsvector('english', coalesce(title, '')), 'A') || + setweight(to_tsvector('english', coalesce(immutable_array_to_string(tags, ' '), '')), 'B') || + setweight(to_tsvector('english', coalesce(content, '')), 'C') + ) STORED + """ + + execute("CREATE INDEX notes_trgm_idx ON notes USING GIN (search)") + end + + def down do + drop table(:notes) + execute("DROP FUNCTION immutable_array_to_string(text[], text)") end end