Skip to content

Commit

Permalink
feat: improve template search functionality (#142)
Browse files Browse the repository at this point in the history
* feat: improve template search functionality

* fix: correctly open node from search

* fix: eslint error
  • Loading branch information
ReinderVosDeWael authored Jul 24, 2024
1 parent d5b82be commit 7246de8
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 35 deletions.
4 changes: 3 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
type ModalComponent
} from "@skeletonlabs/skeleton"
import "../app.postcss"
import ModalSearchDecisionTree from "./templates/TemplatesDirectory/ModalSearchDecisionTree.svelte"
initializeStores()
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow })
const modalRegistry: Record<string, ModalComponent> = {
markdown: { ref: ModalMarkdown }
markdown: { ref: ModalMarkdown },
searchDecisionTree: { ref: ModalSearchDecisionTree }
}
$modeCurrent = true
Expand Down
10 changes: 1 addition & 9 deletions src/routes/templates/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,7 @@

<svelte:fragment slot="panel">
<div hidden={tabSet !== 0}>
{#if data.user?.is_admin}
<div class="flex space-x-3 pb-2">
<div class="right-0 content-center">
<SlideToggle name="slider-editable" size="sm" bind:checked={editable}>Editable</SlideToggle>
</div>
<ExportTemplates bind:nodes />
</div>
{/if}
<TemplatesDirectory {nodes} bind:selectedNodes {editable} />
<TemplatesDirectory {nodes} bind:selectedNodes isAdmin={data.user?.is_admin} />
</div>
<div hidden={tabSet !== 1}>
<SelectedNodes bind:nodes={selectedNodes} />
Expand Down
13 changes: 2 additions & 11 deletions src/routes/templates/DecisionTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,8 @@ export class DecisionTree {
return children
}

filterChildrenByIds(ids: number[]): DecisionTree {
const nodes = [this, ...this.getChildrenRecursive().filter(child => ids.includes(child.id))]
return new DecisionTree(
nodes.map(node => ({
id: node.id,
text: node.text,
parent_id: node.parent?.id ?? null
})),
this.id,
this.parent
)
filterChildrenByIds(ids: number[]): DecisionTree[] {
return this.getChildrenRecursive().filter(child => ids.includes(child.id))
}

getPath(): string[] {
Expand Down
10 changes: 7 additions & 3 deletions src/routes/templates/TemplatesDirectory/AdminButtons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
const modal: ModalSettings = {
type: "component",
component: "markdown",
title: `New template inside "${shortenText(node.text)}"`,
title: `New template inside "${shortenText([...node.getPath(), node.text].join(" | "))}"`,
meta: { instructions: instructions, value: "" },
response: async response => {
if (!response.value) return
Expand Down Expand Up @@ -132,9 +132,13 @@
}
</script>

<span class={"space-x-2"}>
<span class="grid grid-rows-1 grid-flow-col gap-0">
{#each adminButtons as adminButton}
<button on:click={adminButton.onClick} hidden={!adminButton.show} class="hover-highlight">
<button
on:click={adminButton.onClick}
hidden={!adminButton.show}
class="btn hover:variant-ghost-primary w-[1rem] h-[1.5rem]"
>
<svelte:component this={adminButton.icon} class={adminButton.class} />
</button>
{/each}
Expand Down
128 changes: 128 additions & 0 deletions src/routes/templates/TemplatesDirectory/ModalSearchDecisionTree.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<script lang="ts">
import { getModalStore } from "@skeletonlabs/skeleton"
import type { DecisionTree } from "../DecisionTree"
import Fuse from "fuse.js"
import AdminButtons from "./AdminButtons.svelte"
import { openNodeIds } from "./store"
const modalStore = getModalStore()
let root: DecisionTree = $modalStore[0].meta.root
let editable: boolean = $modalStore[0].meta.editable ?? false
let searchTerm = ""
let debounceSearchTerm = ""
let elemDocSearch: HTMLElement
let results: DecisionTree[] = []
const allChildNodes = root.getChildrenRecursive()
const pathsTextsAndIds = allChildNodes.map(node => ({
path: node.getPath().join(" "),
id: node.id
}))
const searcher = new Fuse(pathsTextsAndIds, {
keys: ["path"],
threshold: 0,
isCaseSensitive: false,
useExtendedSearch: true,
ignoreLocation: true
})
let debounceTimer: ReturnType<typeof setTimeout>
function debounceSearch(event: Event) {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
searchTemplates()
debounceSearchTerm = searchTerm
}, 150)
}
function searchTemplates() {
if (!searchTerm) {
results = []
return
}
const searchedPaths = searcher.search(searchTerm).map(result => result.item)
const searchedNodes = allChildNodes.filter(node => searchedPaths.some(path => path.id === node.id))
const searchedIds = [
...searchedNodes.map(node => [...node.getParents(), node]).flat(),
...searchedNodes.map(node => node.getChildrenRecursive()).flat()
]
.filter((value, index, self) => self.indexOf(value) === index)
.filter(node => node.children.length === 0)
.map(node => node.id)
results = root.filterChildrenByIds(searchedIds)
}
function onKeyDown(event: KeyboardEvent) {
if (["Enter", "ArrowDown"].includes(event.code)) {
const queryFirstAnchorElement = elemDocSearch.querySelector("a")
if (queryFirstAnchorElement) queryFirstAnchorElement.focus()
}
}
function onSearchClick(node: DecisionTree) {
const parents = node.getParents()
const ids = [...parents.map(parent => parent.id), node.id]
openNodeIds.set(new Set(ids))
modalStore.close()
}
function onSaveClick(node: DecisionTree) {
if ($modalStore[0].response) {
$modalStore[0].response({ value: node })
modalStore.close()
}
}
</script>

<div bind:this={elemDocSearch} class="card p-4 w-modal-wide shadow-xl space-y-4 h-[36rem]">
<header class="input-group input-group-divider grid-cols-[auto_1fr_auto] flex items-center">
<i class="fa-solid fa-magnifying-glass text-xl ml-4"></i>
<input
class="input"
bind:value={searchTerm}
type="search"
placeholder="Search..."
on:input={debounceSearch}
on:keydown={onKeyDown}
/>
</header>

{#if results.length > 0}
<nav class="list-nav" tabindex="-1">
<ul class="overflow-y-auto h-[32rem]">
{#each results as node}
<li class="text-lg">
<div class="flex items-center">
{#if editable}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click={() => modalStore.close()}>
<AdminButtons {node} />
</div>
{/if}
<button
class="btn hover:variant-ghost-primary w-[1rem] h-[1.5rem]"
on:click={() => onSaveClick(node)}
>
<i class="fa-solid fa-plus"></i>
</button>
<button on:click={() => onSearchClick(node)} class="btn hover:variant-ghost-primary">
<span class="text-wrap text-left">{node.getPath().slice(1).join(" | ")}</span>
</button>
</div>
</li>
{/each}
</ul>
</nav>
{:else if !debounceSearchTerm}
<div class="p-4">
<p>Start typing to search for templates.</p>
</div>
{:else}
<div class="p-4">
<p>No Results found for <code class="code">{debounceSearchTerm}</code>.</p>
</div>
{/if}
</div>
15 changes: 12 additions & 3 deletions src/routes/templates/TemplatesDirectory/SortableNestedNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,31 @@
import { slide } from "svelte/transition"
import type { DecisionTree } from "../DecisionTree"
import AdminButtons from "./AdminButtons.svelte"
import { openNodeIds } from "./store"
export let node: DecisionTree
export let editable = false
export let isRoot = true
let isFolded = !isRoot
let sorter: Sortable
const dispatch = createEventDispatcher()
function fold() {
if (isRoot) return
isFolded = !isFolded
if ($openNodeIds.has(node.id)) {
openNodeIds.set(new Set([...$openNodeIds].filter(id => id !== node.id)))
} else {
openNodeIds.set(new Set([...$openNodeIds, node.id]))
}
}
let isFolded = isRoot ? false : !$openNodeIds.has(node.id)
const unsubscribe = openNodeIds.subscribe(value => {
isFolded = isRoot ? false : !value.has(node.id)
})
function onSave() {
dispatch("save", { id: node.id })
dispatch("save", { node: node })
}
onMount(() => {
Expand All @@ -49,6 +57,7 @@
})
}
})
return unsubscribe
})
$: sorter?.option("disabled", !editable)
Expand Down
39 changes: 31 additions & 8 deletions src/routes/templates/TemplatesDirectory/TemplatesDirectory.svelte
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
<script lang="ts">
import LoadingBar from "$lib/components/LoadingBar.svelte"
import { getToastStore } from "@skeletonlabs/skeleton"
import { getModalStore, getToastStore, SlideToggle, type ModalSettings } from "@skeletonlabs/skeleton"
import type { DecisionTree } from "../DecisionTree"
import SortableNested from "./SortableNested.svelte"
import SearchTemplates from "./SearchTemplates.svelte"
import ExportTemplates from "../ExportTemplates.svelte"
export let nodes: DecisionTree
export let selectedNodes: DecisionTree[] = []
export let editable: boolean = false
export let isAdmin: boolean = false
let filteredNodes: DecisionTree = nodes
let editable: boolean = false
const toastStore = getToastStore()
const modalStore = getModalStore()
function onSave(event: CustomEvent) {
const nodeId = event.detail.id
const node = nodes.getNodeById(nodeId)
function openSearchModal() {
const modal: ModalSettings = {
type: "component",
component: "searchDecisionTree",
meta: { root: nodes, editable: editable },
response: (response: { value: DecisionTree }) => {
onSave(response.value)
}
}
modalStore.trigger(modal)
}
function onSave(node: DecisionTree) {
if (!node) return
if (selectedNodes.find(savedNode => savedNode.id === node.id)) {
toastStore.trigger({
Expand All @@ -36,6 +48,17 @@
{#if !nodes}
<LoadingBar label="Processing templates..." />
{:else}
<SearchTemplates tree={nodes} bind:filteredNodes />
<SortableNested node={filteredNodes} on:save={onSave} {editable} />
<div class="flex space-x-3 pb-2">
<button class="btn variant-filled-primary hover:variant-soft-primary" on:click={openSearchModal}>
<i class="fas fa-search mr-2"></i>
Search
</button>
{#if isAdmin}
<ExportTemplates bind:nodes />
<div class="right-0 content-center">
<SlideToggle name="slider-editable" size="sm" bind:checked={editable}>Editable</SlideToggle>
</div>
{/if}
</div>
<SortableNested node={filteredNodes} on:save={e => onSave(e.detail.node)} {editable} />
{/if}
3 changes: 3 additions & 0 deletions src/routes/templates/TemplatesDirectory/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { writable } from "svelte/store"

export const openNodeIds = writable<Set<number>>(new Set())

0 comments on commit 7246de8

Please sign in to comment.