Skip to content

Commit e692315

Browse files
refactor(SearchBlock): Remove chip filters, used dropdown instead
And use toggle to enable find and replace mode
1 parent 74a21eb commit e692315

1 file changed

Lines changed: 133 additions & 42 deletions

File tree

frontend/src/components/Controls/SearchBlock.vue

Lines changed: 133 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,102 @@
11
<template>
2-
<div ref="searchBlock">
2+
<div ref="searchBlock" class="focus-within:outline-none" @keydown="handleKeydown">
3+
<div class="mb-4">
4+
<OptionToggle
5+
v-model="searchMode"
6+
:options="[
7+
{ label: 'Search', value: 'search', icon: 'search' },
8+
{ label: 'Find & Replace', value: 'replace', icon: 'edit-3' },
9+
]" />
10+
</div>
11+
312
<div class="mb-4 flex gap-2">
413
<BuilderInput
14+
ref="searchInput"
515
class="flex-1"
616
type="text"
7-
:placeholder="searchMode === 'replace' ? 'Find...' : 'Search'"
17+
:placeholder="searchMode === 'replace' ? 'Find...' : 'Search blocks...'"
818
v-model="query"
9-
@input="setQuery" />
10-
<BuilderButton
11-
@click="toggleSearchMode"
12-
:variant="searchMode === 'replace' ? 'solid' : 'outline'"
13-
icon="repeat"
14-
:title="
15-
searchMode === 'replace' ? 'Switch to Search mode' : 'Switch to Find & Replace mode'
16-
"></BuilderButton>
19+
@input="setQuery"
20+
@keydown.enter="handlePrimaryAction" />
21+
22+
<Popover class="relative inline-block text-left">
23+
<template #target="{ isOpen, togglePopover }">
24+
<BuilderButton
25+
@click="togglePopover"
26+
variant="outline"
27+
icon="filter"
28+
label="Filters"
29+
:class="[
30+
'flex items-center gap-2 text-sm',
31+
selectedFiltersCount > 0 ? 'border-ink-gray-6 bg-ink-gray-1' : '',
32+
]">
33+
<span
34+
v-if="selectedFiltersCount > 0"
35+
class="bg-ink-gray-7 ml-1 rounded-full px-2 py-0.5 text-xs text-white">
36+
{{ selectedFiltersCount }}
37+
</span>
38+
<FeatherIcon :name="isOpen ? 'chevron-up' : 'chevron-down'" class="size-4" />
39+
</BuilderButton>
40+
</template>
41+
<template #body>
42+
<div class="w-48 rounded-lg bg-white py-2 shadow-lg ring-1 ring-black ring-opacity-5">
43+
<div class="px-3 py-2 text-xs font-medium text-ink-gray-5">Filter search results by:</div>
44+
<div class="space-y-1 px-2">
45+
<label
46+
v-for="filter in filters"
47+
:key="filter.name"
48+
class="flex cursor-pointer items-center rounded px-2 py-1.5 text-sm hover:bg-surface-gray-1">
49+
<Input
50+
type="checkbox"
51+
:checked="filter.selected"
52+
@change="toggleFilter(filter)"
53+
class="focus:ring-ink-gray-5 mr-3 size-4 rounded border-gray-300 text-ink-gray-7" />
54+
<span>{{ filter.name }}</span>
55+
</label>
56+
</div>
57+
<div class="border-surface-gray-3 border-t px-2 pt-2">
58+
<button
59+
@click="clearAllFilters"
60+
class="w-full rounded px-2 py-1.5 text-left text-sm text-ink-gray-5 hover:bg-surface-gray-1">
61+
Clear all filters
62+
</button>
63+
</div>
64+
</div>
65+
</template>
66+
</Popover>
1767
</div>
1868

19-
<BuilderInput
20-
v-if="searchMode === 'replace'"
21-
class="mb-4"
22-
type="text"
23-
placeholder="Replace with..."
24-
v-model="replaceQuery" />
25-
26-
<div class="mb-4 flex flex-wrap gap-2 text-sm">
27-
<Badge
28-
v-for="filter in filters"
29-
:key="filter.name"
30-
:label="filter.name"
31-
theme="gray"
32-
:variant="filter.selected ? 'solid' : 'outline'"
33-
size="sm"
34-
class="cursor-pointer"
35-
@click="toggleFilter(filter)" />
69+
<div v-if="searchMode === 'replace'" class="mb-4">
70+
<BuilderInput
71+
class="w-full"
72+
type="text"
73+
placeholder="Replace with..."
74+
v-model="replaceQuery"
75+
@keydown.enter="handlePrimaryAction" />
3676
</div>
3777

38-
<div v-if="searchMode === 'replace' && results.length > 0" class="mb-4 space-y-3">
39-
<BuilderButton @click="replaceAll" variant="solid" class="w-full" :disabled="!replaceQuery">
78+
<div
79+
v-if="query && (searchMode === 'search' || (searchMode === 'replace' && results.length > 0))"
80+
class="mb-4">
81+
<BuilderButton
82+
v-if="searchMode === 'replace'"
83+
@click="handlePrimaryAction"
84+
variant="solid"
85+
class="w-full"
86+
:disabled="!replaceQuery">
4087
Replace All ({{ results.length }} matches)
4188
</BuilderButton>
42-
<div class="text-xs text-ink-gray-5">
43-
{{ replacedCount > 0 ? `${replacedCount} replacements made` : "" }}
89+
<div v-if="searchMode === 'replace' && replacedCount > 0" class="mt-2 text-xs text-ink-gray-5">
90+
{{ replacedCount }} replacements made
4491
</div>
4592
</div>
4693

4794
<div v-if="!query" class="mt-6 text-center">
48-
<!-- Empty State -->
4995
<div class="flex flex-col items-center justify-center py-8">
50-
<FeatherIcon name="search" class="mb-4 size-8 text-ink-gray-4" />
51-
<p class="mb-4 text-p-xs text-ink-gray-5">
52-
Find blocks by content, styles, attributes, or element type.
53-
</p>
96+
<div class="mb-4 flex size-16 items-center justify-center rounded-full bg-surface-gray-2">
97+
<FeatherIcon name="search" class="size-8 text-ink-gray-4" />
98+
</div>
99+
<h3 class="mb-2 text-sm font-medium text-ink-gray-6">Search your blocks</h3>
54100
</div>
55101
</div>
56102

@@ -93,14 +139,17 @@
93139
import type Block from "@/block";
94140
import useCanvasStore from "@/stores/canvasStore";
95141
import { watchDebounced } from "@vueuse/core";
96-
import { Badge, FeatherIcon } from "frappe-ui";
97-
import { nextTick, onMounted, Ref, ref } from "vue";
142+
import { FeatherIcon, Popover } from "frappe-ui";
143+
import Input from "frappe-ui/src/components/Input.vue";
144+
import { computed, nextTick, onMounted, Ref, ref } from "vue";
98145
import { toast } from "vue-sonner";
99146
import BuilderButton from "./BuilderButton.vue";
147+
import OptionToggle from "./OptionToggle.vue";
100148
101149
const canvasStore = useCanvasStore();
102150
103151
const searchBlock = ref(null) as Ref<HTMLInputElement | null>;
152+
const searchInput = ref(null) as Ref<HTMLInputElement | null>;
104153
const query = ref("");
105154
const replaceQuery = ref("");
106155
const searchMode = ref<"search" | "replace">("search");
@@ -210,20 +259,51 @@ const filters = ref(
210259
})),
211260
);
212261
262+
const selectedFiltersCount = computed(() => {
263+
return filters.value.filter((f) => f.selected).length;
264+
});
265+
213266
const setQuery = (value: string) => {
214267
query.value = value;
215268
replacedCount.value = 0;
216269
};
217270
218-
const toggleSearchMode = () => {
219-
searchMode.value = searchMode.value === "search" ? "replace" : "search";
271+
const handlePrimaryAction = () => {
272+
if (searchMode.value === "search") {
273+
performSearch();
274+
} else if (searchMode.value === "replace" && results.value.length > 0) {
275+
replaceAll();
276+
}
277+
};
278+
279+
const handleKeydown = (event: KeyboardEvent) => {
280+
// Cmd+F or Ctrl+F for quick search
281+
if ((event.metaKey || event.ctrlKey) && event.key === "f") {
282+
event.preventDefault();
283+
const input = searchInput.value?.querySelector?.("input") || searchInput.value;
284+
if (input && "focus" in input && typeof input.focus === "function") {
285+
input.focus();
286+
}
287+
}
288+
// Escape to clear search
289+
if (event.key === "Escape") {
290+
query.value = "";
291+
replaceQuery.value = "";
292+
}
220293
};
221294
222295
const toggleFilter = (filter: any) => {
223296
filter.selected = !filter.selected;
224297
performSearch();
225298
};
226299
300+
const clearAllFilters = () => {
301+
filters.value.forEach((filter) => {
302+
filter.selected = false;
303+
});
304+
performSearch();
305+
};
306+
227307
const getMatchDetails = (block: Block) => {
228308
if (!query.value) return "";
229309
@@ -351,8 +431,10 @@ const searchWithFilters = (searchTerm: string): Block[] => {
351431
352432
onMounted(async () => {
353433
await nextTick();
354-
const input = searchBlock.value?.querySelector("input");
355-
input?.focus();
434+
const input = searchInput.value?.querySelector?.("input") || searchInput.value;
435+
if (input && "focus" in input && typeof input.focus === "function") {
436+
input.focus();
437+
}
356438
});
357439
358440
watchDebounced(query, performSearch, {
@@ -362,4 +444,13 @@ watchDebounced(query, performSearch, {
362444
watchDebounced(() => filters.value.map((f) => f.selected).join(","), performSearch, {
363445
debounce: 300,
364446
});
447+
448+
// Reset replaced count when switching modes
449+
watchDebounced(
450+
searchMode,
451+
() => {
452+
replacedCount.value = 0;
453+
},
454+
{ debounce: 100 },
455+
);
365456
</script>

0 commit comments

Comments
 (0)