Compare commits
4 commits
3b3ef796a4
...
9b64e35cce
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b64e35cce | |||
| f1e64d7986 | |||
| 953a351cd9 | |||
| 1ac89d7c28 |
8 changed files with 0 additions and 407 deletions
|
|
@ -166,11 +166,6 @@ defmodule Mv.Membership.Member do
|
||||||
attribute :postal_code, :string do
|
attribute :postal_code, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :search_vector, AshPostgres.Tsvector,
|
|
||||||
writable?: false,
|
|
||||||
public?: false,
|
|
||||||
select_by_default?: false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
defmodule MvWeb.Components.SearchBarComponent do
|
|
||||||
@moduledoc """
|
|
||||||
Provides the SearchBar Live-Component.
|
|
||||||
|
|
||||||
- uses the DaisyUI search input field
|
|
||||||
- sends search_changed event to parent live view with a query
|
|
||||||
"""
|
|
||||||
use MvWeb, :live_component
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def update(_assigns, socket) do
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> assign_new(:query, fn -> "" end)
|
|
||||||
|> assign_new(:placeholder, fn -> gettext("Search...") end)
|
|
||||||
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<form phx-change="search" phx-target={@myself} class="flex" role="search" aria-label="Search">
|
|
||||||
<label class="input">
|
|
||||||
<svg
|
|
||||||
class="h-[1em] opacity-50"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width="2.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
|
||||||
<path d="m21 21-4.3-4.3"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
placeholder={@placeholder}
|
|
||||||
value={@query}
|
|
||||||
name="query"
|
|
||||||
phx-debounce="300"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
# Function to handle the search
|
|
||||||
def handle_event("search", %{"query" => q}, socket) do
|
|
||||||
# Forward a high level message to the parent
|
|
||||||
send(self(), {:search_changed, q})
|
|
||||||
{:noreply, assign(socket, :query, q)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
defmodule MvWeb.MemberLive.Index do
|
defmodule MvWeb.MemberLive.Index do
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
import Ash.Expr
|
|
||||||
import Ash.Query
|
|
||||||
import MvWeb.TableComponents
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(params, _session, socket) do
|
def mount(params, _session, socket) do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, gettext("Members"))
|
|> assign(:page_title, gettext("Members"))
|
||||||
|> assign(:query, "")
|
|
||||||
|> assign_new(:sort_field, fn -> :first_name end)
|
|> assign_new(:sort_field, fn -> :first_name end)
|
||||||
|> assign_new(:sort_order, fn -> :asc end)
|
|> assign_new(:sort_order, fn -> :asc end)
|
||||||
|> assign(:selected_members, [])
|
|> assign(:selected_members, [])
|
||||||
|
|
@ -19,32 +15,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
# Receive messages from any toolbar component
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
|
|
||||||
# Function to handle search
|
|
||||||
@impl true
|
|
||||||
def handle_info({:search_changed, q}, socket) do
|
|
||||||
members =
|
|
||||||
if String.trim(q) == "" do
|
|
||||||
Ash.read!(Mv.Membership.Member)
|
|
||||||
else
|
|
||||||
Mv.Membership.Member
|
|
||||||
|> filter(expr(fragment("search_vector @@ plainto_tsquery('simple', ?)", ^q)))
|
|
||||||
|> Ash.read!()
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> assign(:query, q)
|
|
||||||
|> assign(:members, members)}
|
|
||||||
end
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
# Handle Events
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Handle Events
|
# Handle Events
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,6 @@
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<.live_component
|
|
||||||
module={MvWeb.Components.SearchBarComponent}
|
|
||||||
id="search-bar"
|
|
||||||
query={@query}
|
|
||||||
placeholder={gettext("Search...")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<.table
|
<.table
|
||||||
id="members"
|
id="members"
|
||||||
rows={@members}
|
rows={@members}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
defmodule Mv.Repo.Migrations.AddSearchVectorToMembers do
|
|
||||||
@moduledoc """
|
|
||||||
Updates resources based on their most recent snapshots.
|
|
||||||
|
|
||||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
def up do
|
|
||||||
alter table(:members) do
|
|
||||||
add :search_vector, :tsvector
|
|
||||||
end
|
|
||||||
|
|
||||||
execute("""
|
|
||||||
CREATE INDEX members_search_vector_idx
|
|
||||||
ON members
|
|
||||||
USING GIN (search_vector)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Eigene Trigger-Funktion mit Gewichtung
|
|
||||||
execute("""
|
|
||||||
CREATE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.search_vector :=
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.birth_date::text, '')), 'C') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
|
|
||||||
RETURN NEW;
|
|
||||||
END
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
""")
|
|
||||||
|
|
||||||
execute("""
|
|
||||||
CREATE TRIGGER update_search_vector
|
|
||||||
BEFORE INSERT OR UPDATE ON members
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION members_search_vector_trigger()
|
|
||||||
""")
|
|
||||||
end
|
|
||||||
|
|
||||||
def down do
|
|
||||||
execute("DROP TRIGGER IF EXISTS update_search_vector ON members")
|
|
||||||
execute("DROP FUNCTION IF EXISTS members_search_vector_trigger()")
|
|
||||||
execute("DROP INDEX IF EXISTS members_search_vector_idx")
|
|
||||||
|
|
||||||
alter table(:members) do
|
|
||||||
remove :search_vector
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
{
|
|
||||||
"attributes": [
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "fragment(\"uuid_generate_v7()\")",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": true,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "id",
|
|
||||||
"type": "uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "first_name",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "last_name",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": false,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "email",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "birth_date",
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "paid",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "phone_number",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "join_date",
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "exit_date",
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "notes",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "city",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "street",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "house_number",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "postal_code",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_nil?": true,
|
|
||||||
"default": "nil",
|
|
||||||
"generated?": false,
|
|
||||||
"precision": null,
|
|
||||||
"primary_key?": false,
|
|
||||||
"references": null,
|
|
||||||
"scale": null,
|
|
||||||
"size": null,
|
|
||||||
"source": "search_vectors",
|
|
||||||
"type": "tsvector"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"base_filter": null,
|
|
||||||
"check_constraints": [],
|
|
||||||
"custom_indexes": [],
|
|
||||||
"custom_statements": [],
|
|
||||||
"has_create_action": true,
|
|
||||||
"hash": "3B162FD69B92BF8258DB56BA0CBB6108FBE996B1F7231C5F2D9EC53D956EFC75",
|
|
||||||
"identities": [],
|
|
||||||
"multitenancy": {
|
|
||||||
"attribute": null,
|
|
||||||
"global": null,
|
|
||||||
"strategy": null
|
|
||||||
},
|
|
||||||
"repo": "Elixir.Mv.Repo",
|
|
||||||
"schema": null,
|
|
||||||
"table": "members"
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
defmodule MvWeb.Components.SearchBarComponentTest do
|
|
||||||
use MvWeb.ConnCase, async: true
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
|
|
||||||
describe "SearchBarComponent" do
|
|
||||||
test "renders with placeholder", %{conn: conn} do
|
|
||||||
conn = conn_with_oidc_user(conn)
|
|
||||||
{:ok, view, _html} = live(conn, "/members")
|
|
||||||
|
|
||||||
assert has_element?(view, "input[placeholder='Search...']")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates query when user types", %{conn: conn} do
|
|
||||||
conn = conn_with_oidc_user(conn)
|
|
||||||
{:ok, view, _html} = live(conn, "/members")
|
|
||||||
|
|
||||||
# simulate search input and check that other members are not listed
|
|
||||||
html =
|
|
||||||
view
|
|
||||||
|> element("form[role=search]")
|
|
||||||
|> render_change(%{"query" => "Friedrich"})
|
|
||||||
|
|
||||||
refute html =~ "Greta"
|
|
||||||
|
|
||||||
html =
|
|
||||||
view
|
|
||||||
|> element("form[role=search]")
|
|
||||||
|> render_change(%{"query" => "Greta"})
|
|
||||||
|
|
||||||
refute html =~ "Friedrich"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -108,16 +108,4 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
assert conn.assigns.sort_order == :desc
|
assert conn.assigns.sort_order == :desc
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handle_info(:search_changed) updates assigns with search results", %{conn: conn} do
|
|
||||||
conn = conn_with_oidc_user(conn)
|
|
||||||
{:ok, view, _html} = live(conn, "/members")
|
|
||||||
|
|
||||||
send(view.pid, {:search_changed, "Friedrich"})
|
|
||||||
|
|
||||||
state = :sys.get_state(view.pid)
|
|
||||||
|
|
||||||
assert state.socket.assigns.query == "Friedrich"
|
|
||||||
assert is_list(state.socket.assigns.members)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue