Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add filter to BuilderSidebarToolbar. WF-42 #516

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/ui/src/builder/BuilderSidebarTitleSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<div v-if="isSearchActive" class="BuilderSidebarTitleSearch">
<input
ref="searchInput"
v-model="searchQuery"
type="text"
placeholder="Search..."
/>
<slot />
<i
:class="{ disabled: disabled }"
class="searchIcon material-symbols-outlined"
title="Close"
tabindex="0"
@keydown.enter="toggleSearch"
@click="toggleSearch"
>
close
</i>
</div>
<div v-else class="BuilderSidebarTitleSearch">
<i class="material-symbols-outlined">{{ icon }}</i>
<h3>{{ title }}</h3>
<i
title="Search"
class="searchIcon material-symbols-outlined"
tabindex="0"
@keydown.enter="toggleSearch"
@click="toggleSearch"
>
search
</i>
</div>
</template>

<script setup lang="ts">
import { nextTick, ref, Ref } from "vue";

defineProps({
icon: { type: String, required: true },
title: { type: String, required: true },
disabled: { type: Boolean, required: false },
});

const searchQuery = defineModel({ type: String, default: "" });

defineEmits({});

const searchInput: Ref<HTMLInputElement> = ref(null);
const isSearchActive: Ref<boolean> = ref(false);

async function toggleSearch() {
isSearchActive.value = !isSearchActive.value;
if (isSearchActive.value) {
await nextTick();
searchInput.value.focus();
} else {
searchQuery.value = "";
}
}
</script>

<style scoped>
@import "./sharedStyles.css";

.BuilderSidebarTitleSearch {
display: flex;
gap: 8px;
align-items: center;
font-size: 1rem;

background: var(--builderBackgroundColor);
padding: 16px;
top: 0;
position: sticky;
font-size: 1rem;
}

.BuilderSidebarTitleSearch h3 {
font-weight: 500;
font-size: 0.875rem;
flex-grow: 1;
}

.BuilderSidebarTitleSearch .searchIcon {
cursor: pointer;
}

.BuilderSidebarTitleSearch .searchIcon.disabled {
color: var(--builderDisabledColor);
}

.BuilderSidebarTitleSearch input {
outline: 0;
border: 0;
flex-grow: 1;
width: 50%;
}
</style>
74 changes: 48 additions & 26 deletions src/ui/src/builder/BuilderSidebarToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
<template>
<div class="BuilderSidebarToolbar">
<div class="sectionTitle">
<i class="material-symbols-outlined"> handyman </i>
<h3>Toolkit</h3>
</div>
<BuilderSidebarTitleSearch
v-model="searchQuery"
title="Toolkit"
icon="handyman"
/>
<div class="categories">
<template
v-for="(categoryData, category) in categoriesData"
:key="category"
>
<div v-if="categoryData.isVisible !== false" class="category">
<div v-if="shouldDisplayCategory(category)" class="category">
<div class="title">
<i class="material-symbols-outlined">{{
categoryData.icon ?? "question_mark"
}}</i>
<h4>{{ category }}</h4>

<div
<button
class="drop-arrow"
@click="toggleCollapseCategory(category)"
>
Expand All @@ -27,10 +28,14 @@
: "expand_less"
}}
</i>
</div>
</button>
</div>

<div v-show="!categoryData.isCollapsed" class="components">
<div
v-show="!categoryData.isCollapsed"
class="components"
:aria-expanded="!categoryData.isCollapsed"
>
<div
v-for="(
definition, type
Expand Down Expand Up @@ -63,18 +68,23 @@ import { Ref, computed, inject, ref } from "vue";
import { useDragDropComponent } from "./useDragDropComponent";
import injectionKeys from "../injectionKeys";
import { Component, WriterComponentDefinition } from "../writerTypes";
import BuilderSidebarTitleSearch from "./BuilderSidebarTitleSearch.vue";

const wf = inject(injectionKeys.core);
const ssbm = inject(injectionKeys.builderManager);
const { removeInsertionCandidacy } = useDragDropComponent(wf);

type CategoryId = "Root" | "Layout" | "Content" | "Input" | "Embed" | "Other";

type CategoryData = {
isVisible?: boolean;
isCollapsed?: boolean;
icon?: string;
};

const categoriesData: Ref<Record<string, CategoryData>> = ref({
const searchQuery = ref("");

const categoriesData: Ref<Record<CategoryId, CategoryData>> = ref({
Root: {
isVisible: false,
},
Expand All @@ -100,20 +110,37 @@ const categoriesData: Ref<Record<string, CategoryData>> = ref({
},
});

function shouldDisplayCategory(categoryId: CategoryId): boolean {
if (categoriesData.value[categoryId]?.isVisible === false) return false;

return (
Object.keys(definitionsByDisplayCategory.value[categoryId] ?? {})
.length > 0
);
}

function toggleCollapseCategory(categoryId: string) {
const categoryData = categoriesData.value[categoryId];
categoryData.isCollapsed = !categoryData.isCollapsed;
}

const definitionsByDisplayCategory = computed(() => {
const types = wf.getSupportedComponentTypes();
const result: Record<
string,
Record<string, WriterComponentDefinition>
const result: Partial<
Record<CategoryId, Record<string, WriterComponentDefinition>>
> = {};

types.map((type) => {
const definition = wf.getComponentDefinition(type);

const matchingSearch =
searchQuery.value === "" ||
definition.name
.toLocaleLowerCase()
.includes(searchQuery.value.toLocaleLowerCase());

if (!matchingSearch) return;

const isMatch = Object.keys(categoriesData.value).includes(
definition.category,
);
Expand Down Expand Up @@ -149,20 +176,6 @@ const handleDragEnd = (ev: DragEvent) => {
font-size: 0.7rem;
}

.BuilderSidebarToolbar > .sectionTitle {
padding: 16px;
position: sticky;
top: 0;
background: var(--builderBackgroundColor);
font-size: 0.875rem;
height: 40px;
}

h3 {
font-weight: 500;
font-size: 0.875rem;
}

.categories {
padding: 0 12px 12px 12px;
flex: 1 1 auto;
Expand Down Expand Up @@ -201,6 +214,14 @@ h3 {
}

.drop-arrow {
/* reset button default */
border: none;
background-color: unset;
padding: 0;
margin: 0;
height: auto;
width: auto;

border-radius: 50%;
min-width: 24px;
min-height: 24px;
Expand All @@ -212,5 +233,6 @@ h3 {

.drop-arrow:hover {
background: var(--builderSubtleSeparatorColor);
color: unset;
}
</style>
79 changes: 13 additions & 66 deletions src/ui/src/builder/BuilderSidebarTree.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
<template>
<div class="BuilderSidebarTree">
<div v-if="!isSearchActive" class="sectionTitle">
<i class="material-symbols-outlined"> account_tree </i>
<h3>Component Tree</h3>
<i
title="Search"
class="searchIcon material-symbols-outlined"
@click="toggleSearch"
>
search
</i>
</div>
<div v-if="isSearchActive" class="sectionTitle">
<input
ref="searchInput"
v-model="searchQuery"
type="text"
placeholder="Search..."
/>
<BuilderSidebarTitleSearch
v-model="searchQuery"
title="Component Tree"
icon="account_tree"
:disabled="!matchAvailable"
>
<i
:class="{ disabled: !matchAvailable }"
class="searchIcon material-symbols-outlined"
Expand All @@ -26,6 +14,8 @@
? `Go to match ${previousMatchIndex + 1} of ${matchingComponents.length}`
: `Previous match`
"
tabindex="0"
@keydown.enter="goToPreviousMatch"
@click="goToPreviousMatch"
>
navigate_before
Expand All @@ -38,19 +28,13 @@
? `Go to match ${nextMatchIndex + 1} of ${matchingComponents.length}`
: `Next match`
"
tabindex="0"
@keydown.enter="goToNextMatch"
@click="goToNextMatch"
>
navigate_next
</i>
<i
:class="{ disabled: !matchAvailable }"
class="searchIcon material-symbols-outlined"
title="Close"
@click="toggleSearch"
>
close
</i>
</div>
</BuilderSidebarTitleSearch>
<div ref="componentTree" class="components">
<div
v-for="component in rootComponents"
Expand Down Expand Up @@ -79,31 +63,20 @@ import BuilderTreeBranch from "./BuilderTreeBranch.vue";
import injectionKeys from "../injectionKeys";
import { Component } from "../writerTypes";
import { watch } from "vue";
import BuilderSidebarTitleSearch from "./BuilderSidebarTitleSearch.vue";

const wf = inject(injectionKeys.core);
const ssbm = inject(injectionKeys.builderManager);

const { createAndInsertComponent, goToComponentParentPage } =
useComponentActions(wf, ssbm);

const searchInput: Ref<HTMLInputElement> = ref(null);
const isSearchActive: Ref<boolean> = ref(false);
const searchQuery: Ref<string> = ref(null);
const matchIndex: Ref<number> = ref(-1);
const rootComponents = computed(() => {
return wf.getComponents(null, { sortedByPosition: true });
});

async function toggleSearch() {
isSearchActive.value = !isSearchActive.value;
if (isSearchActive.value) {
await nextTick();
searchInput.value.focus();
} else {
searchQuery.value = null;
}
}

function determineMatch(component: Component, query: string): boolean {
if (component.id.toLocaleLowerCase().includes(query)) return true;
if (component.type.toLocaleLowerCase().includes(query)) return true;
Expand Down Expand Up @@ -158,7 +131,6 @@ function goToNextMatch() {
}

const matchingComponents: ComputedRef<Component[]> = computed(() => {
if (!isSearchActive.value) return;
if (!searchQuery.value) return;
const query = searchQuery.value.toLocaleLowerCase();
const components = wf.getComponents();
Expand Down Expand Up @@ -192,35 +164,10 @@ async function addPage() {
flex-direction: column;
}

.sectionTitle {
background: var(--builderBackgroundColor);
padding: 16px;
top: 0;
position: sticky;
font-size: 1rem;
}

.sectionTitle h3 {
font-weight: 500;
font-size: 0.875rem;
flex-grow: 1;
}

.sectionTitle .searchIcon {
cursor: pointer;
}

.sectionTitle .searchIcon.disabled {
.searchIcon.disabled {
color: var(--builderDisabledColor);
}

.sectionTitle input {
outline: 0;
border: 0;
flex-grow: 1;
width: 50%;
}

.components {
padding: 0 12px 12px 12px;
}
Expand Down
Loading
Loading