ash_hq/lib/ash_hq_web/components/search.ex

377 lines
12 KiB
Elixir
Raw Normal View History

2022-03-26 10:17:01 +13:00
defmodule AshHqWeb.Components.Search do
@moduledoc "The search overlay modal"
2022-03-26 10:17:01 +13:00
use Surface.LiveComponent
require Ash.Query
2022-09-28 16:18:05 +13:00
alias AshHqWeb.Components.{CalloutText, Catalogue}
alias AshHqWeb.DocRoutes
2022-09-28 16:18:05 +13:00
alias Phoenix.LiveView.JS
alias Surface.Components.{Form, LivePatch}
2022-11-16 07:47:25 +13:00
alias Surface.Components.Form.Checkbox
2022-03-26 10:17:01 +13:00
2023-01-31 03:21:42 +13:00
prop(close, :event, required: true)
prop(libraries, :list, required: true)
prop(selected_versions, :map, required: true)
prop(selected_types, :list, required: true)
prop(change_types, :event, required: true)
prop(change_versions, :event, required: true)
prop(remove_version, :event, required: true)
prop(uri, :string, required: true)
2022-03-26 10:17:01 +13:00
2023-01-31 03:21:42 +13:00
data(search, :string, default: "")
data(item_list, :list, default: [])
data(selected_item, :string)
data(selecting_packages, :boolean, default: false)
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;"
class="fixed flex justify-center align-middle w-screen h-full backdrop-blur-sm bg-white bg-opacity-10 z-50"
>
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-base-dark-850 border-2 dark:border-base-dark-900"
:on-window-keydown="select-previous"
phx-key="ArrowUp"
>
2022-09-28 16:18:05 +13:00
<div class="h-full w-full" id="search-versions" style="display: none;">
<Catalogue
id="search-versions-contents"
toggle={toggle_libraries()}
libraries={@libraries}
selected_versions={@selected_versions}
change_versions={@change_versions}
/>
</div>
2023-01-31 03:21:42 +13:00
<div id="search-body" class="h-full" :on-window-keydown="select-next" phx-key="ArrowDown">
<div class="p-6 h-full grid gap-6 grid-rows-[max-content_auto_max-content]">
<button
id="close-search"
class="absolute top-6 right-6 h-6 w-6 cursor-pointer z-10 hover:text-base-light-400"
:on-click={@close}
>
<Heroicons.Outline.XIcon class="h-6 w-6" />
</button>
<div class="flex flex-col w-full sticky">
<div class="w-full flex flex-row justify-start top-0">
<Heroicons.Outline.SearchIcon class="h-6 w-6 mr-4" />
<div class="flex flex-row justify-between w-full mr-10 border-b border-base-light-600">
<Form for={:search} change="search" submit="go-to-doc" class="w-full">
<input
id="search-input"
name="search"
value={@search}
phx-debounce={300}
class="text-lg dark:bg-base-dark-850 grow ring-0 outline-none w-full"
/>
</Form>
</div>
</div>
<div class="ml-10">
<Form for={:types} change={@change_types}>
<div class="flex flex-row space-x-8 flex-wrap mt-2 text-sm text-base-light-500 dark:text-base-dark-300">
<div>Search For:</div>
{#for type <- AshHq.Docs.Extensions.Search.Types.types()}
<div class="flex flex-row items-center">
<Checkbox
class="mr-2"
id={"#{type}-selected"}
value={type in @selected_types}
name={"types[#{type}]"}
/>
<label for={"#{type}-selected"}>
{type}
</label>
</div>
{/for}
</div>
2022-09-01 02:48:26 +12:00
</Form>
</div>
2022-03-26 10:17:01 +13:00
</div>
2023-01-31 03:21:42 +13:00
<div class="grid overflow-auto">
2022-09-01 02:48:26 +12:00
{render_items(assigns, @item_list)}
2022-08-31 20:11:15 +12:00
</div>
2023-01-31 03:21:42 +13:00
<div class="flex flex-row justify-start items-center relative bottom-0">
<Heroicons.Outline.CollectionIcon class="w-6 h-6 mr-2" />
<AshHqWeb.Components.VersionPills
id="search-version-pills"
selected_versions={@selected_versions}
remove_version={@remove_version}
libraries={@libraries}
toggle={toggle_libraries()}
/>
2022-08-31 20:11:15 +12:00
</div>
2022-03-26 10:17:01 +13:00
</div>
</div>
</div>
</div>
"""
end
2022-07-22 09:11:21 +12:00
defp render_items(assigns, items) do
2022-03-26 10:17:01 +13:00
~F"""
2023-01-31 03:21:42 +13:00
<div class="divide-y">
{#for item <- items}
<LivePatch
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
to={DocRoutes.doc_link(item, @selected_versions)}
opts={id: "result-#{item.id}", "phx-click": @close}
>
<div class={
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-4",
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
}>
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
<div>
{render_item_type(assigns, item)}
</div>
<div class="flex flex-row flex-wrap items-center">
{#for {path_item, index} <- Enum.with_index(item_path(item))}
{#if index != 0}
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
{/if}
<div>
{path_item}
</div>
{/for}
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
<div class="font-bold">
{#if Map.get(item, :name_matches)}
<CalloutText text={item_name(item)} />
{#else}
{item_name(item)}
{/if}
2022-07-22 09:11:21 +12:00
</div>
</div>
</div>
2023-01-31 03:21:42 +13:00
<div class="text-base-light-700 dark:text-base-dark-400 ml-10">
{raw(item.search_headline)}
</div>
</div>
2023-01-31 03:21:42 +13:00
</LivePatch>
{/for}
</div>
"""
end
2022-08-31 20:11:15 +12:00
defp render_item_type(assigns, item) do
2023-01-31 03:21:42 +13:00
icon_classes = "h-4 w-4 flex-none mt-1 mx-1"
2022-08-31 20:11:15 +12:00
case item_type(item) do
2023-01-25 19:52:13 +13:00
"Forum" ->
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.UserGroupIcon class={icon_classes} />
2023-01-25 19:52:13 +13:00
"""
"Mix Task" ->
~F"""
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
2023-01-31 03:21:42 +13:00
class={icon_classes}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
/>
</svg>
"""
2022-09-16 10:35:33 +12:00
"Function" ->
case item.type do
type when type in [:function, :macro] ->
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.CodeIcon class={icon_classes} />
2022-09-16 10:35:33 +12:00
"""
:callback ->
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.AtSymbolIcon class={icon_classes} />
2022-09-16 10:35:33 +12:00
"""
:type ->
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.InformationCircleIcon class={icon_classes} />
2022-09-16 10:35:33 +12:00
"""
end
"Module" ->
2022-08-31 20:11:15 +12:00
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.CodeIcon class={icon_classes} />
2022-08-31 20:11:15 +12:00
"""
type when type in ["Dsl", "Option"] ->
AshHqWeb.Components.DocSidebar.render_icon(assigns, item.extension_type)
"Guide" ->
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.BookOpenIcon class={icon_classes} />
2022-08-31 20:11:15 +12:00
"""
_ ->
~F"""
2023-01-31 03:21:42 +13:00
<Heroicons.Outline.PuzzleIcon class={icon_classes} />
2022-08-31 20:11:15 +12:00
"""
end
end
2022-09-28 16:18:05 +13:00
defp toggle_libraries(js \\ %JS{}) do
js
|> JS.toggle(to: "#search-body")
|> JS.toggle(to: "#search-versions")
end
2023-01-25 19:52:13 +13:00
defp item_name(%{thread_name: thread_name, channel_name: channel_name}),
do: "#{String.capitalize(channel_name)} Forum: #{inspect(thread_name)}"
2022-07-22 09:11:21 +12:00
defp item_name(%{name: name}), do: name
defp item_name(%{version: version}), do: version
defp item_path(%{
library_name: library_name,
extension_name: extension_name,
path: path
}) do
[library_name, extension_name, path] |> List.flatten()
end
defp item_path(%{
library_name: library_name,
module_name: module_name
}) do
[library_name, module_name]
end
defp item_path(%{library_name: library_name}) do
[library_name]
end
defp item_path(%{library_version: %{library_name: library_name}}) do
[library_name]
end
defp item_path(_) do
[]
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
2023-01-05 05:18:04 +13:00
if assigns[:uri] != socket.assigns[:uri] do
{:ok, socket |> assign(:search, nil) |> assign(assigns) |> search()}
else
{:ok, socket |> assign(assigns) |> search()}
end
2022-03-30 17:40:17 +13:00
end
2022-09-28 16:18:05 +13:00
def handle_event("toggle_versions", _, socket) do
{:noreply, socket |> assign(:selecting_packages, !socket.assigns.selecting_packages)}
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
def handle_event("go-to-doc", _data, socket) do
2022-03-28 10:26:35 +13:00
case Enum.find(socket.assigns.item_list, fn item ->
item.id == socket.assigns.selected_item.id
end) do
nil ->
{:noreply, socket}
item ->
{:noreply,
socket
|> push_event("click-on-item", %{"id" => "result-#{item.id}"})}
2022-03-28 10:26:35 +13:00
end
end
defp item_type(%resource{}) do
AshHq.Docs.Extensions.Search.item_type(resource)
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, %{}] ||
socket.assigns[:selected_types] == [] do
2022-06-05 08:58:50 +12:00
assign(socket, results: %{}, item_list: [])
2022-03-26 10:17:01 +13:00
else
2022-04-08 18:59:39 +12:00
versions =
Enum.map(socket.assigns.selected_versions, fn
{library_id, "latest"} ->
Enum.find_value(socket.assigns.libraries, fn library ->
if library.id == library_id do
case AshHqWeb.Helpers.latest_version(library) do
2022-04-08 18:59:39 +12:00
nil ->
nil
version ->
version.id
end
end
end)
{_, version_id} ->
version_id
end)
2022-07-26 07:16:51 +12:00
|> Enum.reject(&(&1 == "" || is_nil(&1)))
2022-04-08 18:59:39 +12:00
%{result: item_list} =
2022-04-05 18:20:36 +12:00
AshHq.Docs.Search.run!(
socket.assigns.search,
versions,
%{types: socket.assigns[:selected_types]}
2022-03-29 08:47:43 +13:00
)
2022-03-28 10:26:35 +13:00
selected_item = Enum.at(item_list, 0)
socket
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
defp set_selected_item(socket, nil), do: socket
defp set_selected_item(socket, selected_item) do
socket
|> assign(:selected_item, selected_item)
|> push_event("js:scroll-to", %{id: "result-#{selected_item.id}"})
end
2022-03-26 10:17:01 +13:00
end