ash_hq/lib/ash_hq_web/components/search.ex

333 lines
10 KiB
Elixir
Raw Normal View History

2022-03-26 10:17:01 +13:00
defmodule AshHqWeb.Components.Search do
use Surface.LiveComponent
require Ash.Query
2022-03-28 10:26:35 +13:00
alias AshHqWeb.Routes
alias Surface.Components.{Form, LiveRedirect}
2022-03-30 17:40:17 +13:00
alias Surface.Components.Form.{Checkbox, Label, Select}
2022-03-26 10:17:01 +13:00
prop open, :boolean, default: false
prop close, :event, required: true
2022-03-28 10:26:35 +13:00
prop libraries, :list, required: true
prop selected_versions, :map, required: true
prop change_versions, :event, required: true
2022-03-30 17:40:17 +13:00
prop selected_types, :list, required: true
prop change_types, :event, required: true
prop uri, :string, required: true
2022-03-26 10:17:01 +13:00
data versions, :map, default: %{}
data search, :string, default: ""
data results, :map, default: %{}
data selected_item, :string
2022-03-26 10:17:01 +13:00
def render(assigns) do
~F"""
<div
id={@id}
2022-03-28 10:26:35 +13:00
style="display: none;"
2022-03-30 05:12:28 +13:00
class="absolute flex justify-center align-middle w-screen h-full backdrop-blur-sm pb-8 bg-white bg-opacity-10"
>
2022-03-26 10:17:01 +13:00
<div
:on-click-away={AshHqWeb.AppViewLive.toggle_search()}
class="dark:text-white absolute rounded-xl left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3/4 h-3/4 bg-white dark:bg-primary-black border-2 dark:border-gray-900"
:on-window-keydown="select-previous"
phx-key="ArrowUp"
>
<div class="h-full px-6 my-6" :on-window-keydown="select-next" phx-key="ArrowDown">
2022-03-30 17:40:17 +13:00
<div class="w-full flex flex-row justify-start sticky top-0 pb-3 border-b border-gray-600">
2022-03-26 10:17:01 +13:00
<Heroicons.Outline.SearchIcon class="h-6 w-6 mr-4 ml-4" />
<div class="flex flex-row justify-between w-full">
2022-03-30 17:40:17 +13:00
<Form for={:search} change="search" submit="go-to-doc" class="w-full">
<input
id="search-input"
name="search"
class="text-lg bg-primary-black grow ring-0 outline-none w-full"
/>
2022-03-26 10:17:01 +13:00
</Form>
<button id="close-search" class="mr-4 ml-4 h-6 w-6 hover:text-gray-400" :on-click={@close}>
<Heroicons.Outline.XIcon class="h-6 w-6" />
</button>
</div>
</div>
<div class="grid grid-cols-9 h-[85%] mt-3">
2022-03-28 10:26:35 +13:00
<div class="col-span-3 md:col-span-2 xl:col-span-1 border-r border-gray-600">
2022-03-30 17:40:17 +13:00
<Form for={:types} change={@change_types}>
{#for type <- AshHq.Docs.Extensions.Search.Types.types()}
<Label field={type}>
{type}
</Label>
<Checkbox id={"#{type}-selected"} value={type in @selected_types} name={"types[#{type}]"}/>
{/for}
</Form>
2022-03-28 10:26:35 +13:00
<Form for={:versions} change={@change_versions}>
2022-03-26 10:17:01 +13:00
{#for library <- @libraries}
<Label field={library.id}>
2022-03-28 10:26:35 +13:00
{library.display_name}
2022-03-26 10:17:01 +13:00
</Label>
2022-03-30 05:12:28 +13:00
<div class="pb-2">
<Select
2022-03-30 17:40:17 +13:00
id={"versions-select-#{library.id}"}
2022-03-30 05:12:28 +13:00
class="text-black form-select rounded-md pt-1 py-2 w-3/4"
name={"versions[#{library.id}]"}
2022-03-28 10:26:35 +13:00
selected={Map.get(@selected_versions, library.id)}
options={Enum.map(library.versions, &{&1.version, &1.id})}
/>
2022-03-26 10:17:01 +13:00
</div>
{/for}
</Form>
</div>
2022-03-28 10:26:35 +13:00
<div class="pl-4 overflow-y-auto col-span-6 md:col-span-7 xl:col-span-8">
{render_groups(assigns, @results)}
2022-03-26 10:17:01 +13:00
</div>
</div>
</div>
</div>
</div>
"""
end
defp render_groups(assigns, results, first? \\ true) do
2022-03-26 10:17:01 +13:00
~F"""
{#for {group, results} <- results}
<div class={"ml-4": !first?}>
{#if first?}
<div class="font-medium text-lg">
{group}
</div>
{/if}
2022-03-29 08:47:43 +13:00
{#if Enum.empty?(results.items)}
{render_results(assigns, results)}
2022-03-29 08:47:43 +13:00
{#else}
<div class={"mt-4", "border-l border-gray-700 pl-2": !first?}>
{render_results(assigns, results)}
</div>
{/if}
2022-03-26 10:17:01 +13:00
</div>
{/for}
"""
end
defp render_results(assigns, results) do
~F"""
<div>
<div class="font-medium mb-1">
2022-03-28 10:26:35 +13:00
{#if Map.get(results, :path, []) != []}
<div class="flex flex-row justify-start align-middle items-center text-center">
2022-03-28 10:26:35 +13:00
{#for path_item <- Map.get(results, :path, [])}
<Heroicons.Solid.ChevronRightIcon class="h-6 w-6" />
<div>
{path_item}
</div>
{/for}
</div>
{/if}
</div>
{#for item <- results.items}
2022-03-30 17:40:17 +13:00
<LiveRedirect to={Routes.doc_link(item)} opts={id: item.id}>
2022-03-28 10:26:35 +13:00
<div
class={
"rounded-lg mb-4 py-4 px-2 hover:bg-gray-600",
"bg-gray-600": @selected_item.id == item.id,
"bg-gray-800": @selected_item.id != item.id
}
>
2022-03-29 11:05:19 +13:00
{#if item.__struct__ != AshHq.Docs.LibraryVersion &&
item.name != List.last(Map.get(results, :path, []))}
2022-03-28 10:26:35 +13:00
{item.name}
{/if}
{#if item.__struct__ == AshHq.Docs.LibraryVersion}
{item.version}
{/if}
<div class="text-gray-400">
{raw(item.search_headline)}
</div>
</div>
2022-03-28 10:26:35 +13:00
</LiveRedirect>
{/for}
{render_groups(assigns, results.further, false)}
</div>
"""
end
2022-03-26 10:17:01 +13:00
def mount(socket) do
{:ok, socket}
end
2022-03-30 17:40:17 +13:00
def update(assigns, socket) do
{:ok, socket |> assign(assigns) |> search()}
end
2022-03-26 10:17:01 +13:00
def handle_event("search", %{"search" => search}, socket) do
{:noreply, socket |> assign(:search, search) |> search()}
end
def handle_event("select-next", _, socket) do
2022-03-28 10:26:35 +13:00
if socket.assigns[:selected_item] && socket.assigns[:item_list] do
next =
2022-03-28 10:26:35 +13:00
socket.assigns.item_list
|> Enum.drop_while(&(&1.id != socket.assigns.selected_item.id))
|> Enum.at(1)
{:noreply, set_selected_item(socket, next)}
else
{:noreply, socket}
end
end
def handle_event("select-previous", _, socket) do
2022-03-28 10:26:35 +13:00
if socket.assigns[:selected_item] && socket.assigns[:item_list] do
next =
2022-03-28 10:26:35 +13:00
socket.assigns.item_list
|> Enum.reverse()
2022-03-28 10:26:35 +13:00
|> Enum.drop_while(&(&1.id != socket.assigns.selected_item.id))
|> Enum.at(1)
{:noreply, set_selected_item(socket, next)}
else
{:noreply, socket}
end
end
2022-03-28 10:26:35 +13:00
def handle_event("go-to-doc", _, socket) do
case Enum.find(socket.assigns.item_list, fn item ->
item.id == socket.assigns.selected_item.id
end) do
nil ->
{:noreply, socket}
item ->
2022-03-30 17:40:17 +13:00
{:noreply, push_redirect(socket, to: Routes.doc_link(item))}
2022-03-28 10:26:35 +13:00
end
end
2022-03-26 10:17:01 +13:00
defp search(socket) do
if socket.assigns[:search] in [nil, ""] || socket.assigns[:selected_versions] in [nil, %{}] do
assign(socket, :results, %{})
else
search_results =
2022-03-28 10:26:35 +13:00
AshHq.Docs
|> Ash.Api.resources()
2022-03-30 17:40:17 +13:00
|> Enum.filter(fn resource ->
AshHq.Docs.Extensions.Search in Ash.Resource.Info.extensions(resource) &&
AshHq.Docs.Extensions.Search.type(resource) in socket.assigns.selected_types
end)
2022-03-28 10:26:35 +13:00
|> Enum.flat_map(fn resource ->
to_load = AshHq.Docs.Extensions.Search.load_for_search(resource)
resource.search!(socket.assigns.search, Map.values(socket.assigns.selected_versions),
2022-03-29 08:47:43 +13:00
query: Ash.Query.limit(resource, 25),
2022-03-28 10:26:35 +13:00
load: to_load
)
end)
2022-03-29 08:47:43 +13:00
|> Enum.sort_by(
&{-&1.match_rank, Map.get(&1, :extension_order, -1), Enum.count(Map.get(&1, :path, []))}
)
sort_rank =
search_results
|> Enum.with_index()
|> Map.new(fn {item, i} ->
{item.id, i}
end)
results =
search_results
2022-03-28 10:26:35 +13:00
|> Enum.group_by(fn
2022-03-29 08:47:43 +13:00
%{extension_type: type} ->
2022-03-28 10:26:35 +13:00
type
2022-03-30 17:40:17 +13:00
%AshHq.Docs.Function{module_name: module_name} ->
module_name
%AshHq.Docs.Module{name: name} ->
name
2022-03-29 08:47:43 +13:00
%AshHq.Docs.Guide{
library_version: %{version: version, library_display_name: library_display_name}
2022-03-30 05:12:28 +13:00
} ->
"#{library_display_name} #{version}"
%AshHq.Docs.Extension{
library_version: %{version: version, library_display_name: library_display_name}
2022-03-29 08:47:43 +13:00
} ->
"#{library_display_name} #{version}"
2022-03-28 10:26:35 +13:00
%AshHq.Docs.LibraryVersion{library_display_name: library_display_name, version: version} ->
"#{library_display_name} #{version}"
end)
2022-03-26 10:17:01 +13:00
|> Enum.sort_by(fn {_type, items} ->
items
2022-03-29 08:47:43 +13:00
|> Enum.map(&Map.get(sort_rank, &1.id))
|> Enum.min()
2022-03-26 10:17:01 +13:00
end)
|> Enum.map(fn {type, items} ->
{type, group_by_paths(items)}
end)
2022-03-28 10:26:35 +13:00
item_list = item_list(results)
selected_item = Enum.at(item_list, 0)
socket
|> assign(:results, results)
2022-03-28 10:26:35 +13:00
|> assign(:item_list, item_list)
|> set_selected_item(selected_item)
2022-03-26 10:17:01 +13:00
end
end
2022-03-28 10:26:35 +13:00
defp item_list(results) do
List.flatten(do_item_list(results))
end
2022-03-28 10:26:35 +13:00
defp do_item_list({_key, %{items: items, further: further}}) do
do_item_list(items) ++ do_item_list(further)
end
2022-03-28 10:26:35 +13:00
defp do_item_list(items) when is_list(items) do
Enum.map(items, &do_item_list/1)
end
2022-03-28 10:26:35 +13:00
defp do_item_list(item), do: item
defp set_selected_item(socket, nil), do: socket
defp set_selected_item(socket, selected_item) do
socket
|> assign(:selected_item, selected_item)
2022-03-28 10:26:35 +13:00
|> push_event("js:scroll-to", %{id: selected_item.id, boundary_id: socket.assigns[:id]})
end
2022-03-26 10:17:01 +13:00
defp group_by_paths(items) do
items
2022-03-28 10:26:35 +13:00
|> Enum.map(&{Map.get(&1, :path, []), &1})
2022-03-26 10:17:01 +13:00
|> do_group_by_paths()
end
defp do_group_by_paths(items, path_acc \\ []) do
{items_for_group, further} =
Enum.split_with(items, fn
{[], _} ->
true
_ ->
false
end)
further_items =
further
|> Enum.group_by(
fn {[next | _rest], _item} ->
next
end,
fn {[_next | rest], item} ->
{rest, item}
end
)
|> Enum.map(fn {nested, items} ->
{nested, do_group_by_paths(items, path_acc ++ [nested])}
end)
%{path: path_acc, items: Enum.map(items_for_group, &elem(&1, 1)), further: further_items}
2022-03-26 10:17:01 +13:00
end
end