From f03037a943b3f2098096dac954b5082dd8722321 Mon Sep 17 00:00:00 2001 From: shibao Date: Fri, 18 Nov 2022 23:45:15 -0500 Subject: [PATCH] add search to notes --- lib/memex/notes.ex | 52 ++++++++++++++++++- lib/memex_web/live/note_live/index.ex | 37 ++++++++++--- lib/memex_web/live/note_live/index.html.heex | 11 ++++ lib/memex_web/router.ex | 13 ++--- .../20220726000552_create_notes.exs | 28 +++++++++- 5 files changed, 124 insertions(+), 17 deletions(-) diff --git a/lib/memex/notes.ex b/lib/memex/notes.ex index 8a3602d..a44bea7 100644 --- a/lib/memex/notes.ex +++ b/lib/memex/notes.ex @@ -17,10 +17,34 @@ defmodule Memex.Notes do """ @spec list_notes(User.t() | nil) :: [Note.t()] - def list_notes(%{id: user_id}) do + @spec list_notes(search :: String.t() | nil, User.t() | nil) :: [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 @@ -30,10 +54,34 @@ defmodule Memex.Notes do [%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..4209771 100644 --- a/lib/memex_web/live/note_live/index.ex +++ b/lib/memex_web/live/note_live/index.ex @@ -3,13 +3,8 @@ 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))} - end - - def mount(_params, _session, socket) do - {:ok, socket |> assign(notes: Notes.list_public_notes())} + def mount(params, _session, socket) do + {:ok, socket |> assign(search: nil) |> display_notes()} end @impl true @@ -34,7 +29,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 +54,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..66aca4f 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") %> + + <%= 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..e461d0d 100644 --- a/lib/memex_web/router.ex +++ b/lib/memex_web/router.ex @@ -59,15 +59,15 @@ 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 - live "/contexts/:id/show/edit", ContextLive.Show, :edit + live "/context/:id/show/edit", ContextLive.Show, :edit live "/pipelines/new", PipelineLive.Index, :new live "/pipelines/:id/edit", PipelineLive.Index, :edit - live "/pipelines/:id/show/edit", PipelineLive.Show, :edit + live "/pipeline/:id/edit", PipelineLive.Show, :edit get "/users/settings", UserSettingsController, :edit put "/users/settings", UserSettingsController, :update @@ -79,13 +79,14 @@ 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 + live "/context/:id", ContextLive.Show, :show live "/pipelines", PipelineLive.Index, :index - live "/pipelines/:id", PipelineLive.Show, :show + live "/pipeline/:id", PipelineLive.Show, :show end end 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