feat: widget fuzzy search (#14464)

* widget fuzzy search POC

* fix threshold

* Makes fuse to initialize only once

* add proper dependancy to useEffect

* Adds more search terms

* Adds search term for other widgets

* reduce distance for better search results
This commit is contained in:
Aswath K 2022-06-17 08:42:47 +05:30 committed by GitHub
parent 15559d0048
commit bd33b76c1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 55 additions and 12 deletions

View File

@ -4,28 +4,35 @@ import WidgetCard from "./WidgetCard";
import { getWidgetCards } from "selectors/editorSelectors";
import ExplorerSearch from "./Explorer/ExplorerSearch";
import { debounce } from "lodash";
import produce from "immer";
import {
createMessage,
WIDGET_SIDEBAR_CAPTION,
} from "@appsmith/constants/messages";
import Fuse from "fuse.js";
import { WidgetCardProps } from "widgets/BaseWidget";
function WidgetSidebar({ isActive }: { isActive: boolean }) {
const cards = useSelector(getWidgetCards);
const [filteredCards, setFilteredCards] = useState(cards);
const searchInputRef = useRef<HTMLInputElement | null>(null);
let fuse: Fuse<WidgetCardProps, Fuse.FuseOptions<WidgetCardProps>>;
useEffect(() => {
fuse = new Fuse(cards, {
keys: ["displayName", "searchTags"],
threshold: 0.5,
distance: 20,
});
}, [cards]);
const filterCards = (keyword: string) => {
let filteredCards = cards;
if (keyword.trim().length > 0) {
filteredCards = produce(cards, (draft) => {
cards.forEach((card, index) => {
if (card.displayName.toLowerCase().indexOf(keyword) === -1) {
delete draft[index];
}
});
});
const searchResult = fuse.search(keyword);
setFilteredCards(searchResult as WidgetCardProps[]);
} else {
setFilteredCards(cards);
}
setFilteredCards(filteredCards);
};
useEffect(() => {

View File

@ -2,7 +2,7 @@ import { createSelector } from "reselect";
import { AppState } from "reducers";
import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
import { WidgetProps } from "widgets/BaseWidget";
import { WidgetCardProps, WidgetProps } from "widgets/BaseWidget";
import {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
@ -206,7 +206,7 @@ export const getWidgetCards = createSelector(
(config) => !config.hideCard,
);
const _cards = cards.map((config) => {
const _cards: WidgetCardProps[] = cards.map((config) => {
const {
columns,
detachFromLayout = false,
@ -214,6 +214,7 @@ export const getWidgetCards = createSelector(
iconSVG,
key,
rows,
searchTags,
type,
} = config;
return {
@ -224,6 +225,7 @@ export const getWidgetCards = createSelector(
detachFromLayout,
displayName,
icon: iconSVG,
searchTags,
};
});
const sortedCards = sortBy(_cards, ["displayName"]);

View File

@ -52,6 +52,7 @@ export const configureWidget = (config: WidgetConfiguration) => {
const _config = {
...features,
...config.defaults,
searchTags: config.searchTags,
type: config.type,
hideCard: !!config.hideCard || !config.iconSVG,
isDeprecated: !!config.isDeprecated,

View File

@ -6,6 +6,7 @@ export const CONFIG = {
name: "Audio Recorder",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["sound recorder", "voice recorder"],
defaults: {
iconColor: "white",
isDisabled: false,

View File

@ -6,6 +6,7 @@ export const CONFIG = {
name: "Audio",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["mp3", "sound", "wave", "player"],
defaults: {
rows: 4,
columns: 28,

View File

@ -12,6 +12,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: false, // Defines if this widget adds any meta properties
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
searchTags: ["click", "submit"],
defaults: {
rows: 4,
columns: 24,

View File

@ -11,6 +11,7 @@ export const CONFIG = {
name: "Button",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["click", "submit"],
defaults: {
animateLoading: true,
text: "Submit",

View File

@ -8,6 +8,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: true, // Defines if this widget adds any meta properties
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
searchTags: ["photo", "video recorder"],
defaults: {
widgetName: "Camera",
rows: 33,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Chart",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["graph", "visuals", "visualisations"],
defaults: {
rows: 32,
columns: 24,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Checkbox",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["boolean"],
defaults: {
rows: 4,
columns: 7,

View File

@ -7,6 +7,7 @@ export const CONFIG = {
name: "Container",
iconSVG: IconSVG,
isCanvas: true,
searchTags: ["div", "parent", "group"],
defaults: {
backgroundColor: "#FFFFFF",
rows: 40,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Currency Input",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["amount", "total"],
defaults: {
...BaseConfig.defaults,
widgetName: "CurrencyInput",

View File

@ -10,6 +10,7 @@ export const CONFIG = {
name: "DatePicker",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["calendar"],
defaults: {
isDisabled: false,
datePickerType: "DATE_PICKER",

View File

@ -6,6 +6,7 @@ export const CONFIG = {
type: Widget.getWidgetType(),
name: "Divider",
iconSVG: IconSVG,
searchTags: ["line"],
defaults: {
rows: 4,
columns: 20,

View File

@ -7,6 +7,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: false, // Defines if this widget adds any meta properties
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
searchTags: ["pdf"],
defaults: {
widgetName: "DocumentViewer",
docUrl:

View File

@ -7,6 +7,7 @@ export const CONFIG = {
name: "FilePicker",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["upload"],
defaults: {
rows: 4,
files: [],

View File

@ -9,6 +9,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: true,
isCanvas: true,
searchTags: ["group"],
defaults: {
rows: 40,
columns: 24,

View File

@ -7,6 +7,7 @@ export const CONFIG = {
type: Widget.getWidgetType(),
name: "Icon Button",
iconSVG: IconSVG,
searchTags: ["click", "submit"],
defaults: {
iconName: IconNames.PLUS,
buttonVariant: ButtonVariantTypes.PRIMARY,

View File

@ -6,6 +6,7 @@ export const CONFIG = {
name: "Iframe",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["embed"],
defaults: {
source: "https://www.example.com",
borderOpacity: 100,

View File

@ -7,6 +7,7 @@ export const CONFIG = {
name: "Input",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["form", "text input", "number", "textarea"],
defaults: {
...BaseConfig.defaults,
inputType: "TEXT",

View File

@ -8,6 +8,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: true, // Defines if this widget adds any meta properties
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
searchTags: ["graph", "visuals", "visualisations"],
defaults: {
rows: 32,
columns: 24,

View File

@ -20,6 +20,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: true,
isCanvas: true,
searchTags: ["dialog", "popup", "notification"],
defaults: {
rows: 24,
columns: 24,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Multi TreeSelect",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["dropdown"],
defaults: {
rows: 4,
columns: 20,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "MultiSelect",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["dropdown", "tags"],
defaults: {
rows: 4,
columns: 20,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Phone Input",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["call"],
defaults: {
...BaseConfig.defaults,
widgetName: "PhoneInput",

View File

@ -9,6 +9,7 @@ export const CONFIG = {
iconSVG: IconSVG,
needsMeta: false, // Defines if this widget adds any meta properties
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
searchTags: ["percent"],
defaults: {
widgetName: "Progress",
rows: 4,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Radio Group",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["choice"],
defaults: {
rows: 8,
columns: 20,

View File

@ -7,6 +7,7 @@ export const CONFIG = {
name: "Rating",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["stars"],
defaults: {
rows: 4,
columns: 10,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Rich Text Editor",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["input", "rte"],
defaults: {
defaultText: "This is the initial <b>content</b> of the editor",
rows: 20,

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Select",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["dropdown"],
defaults: {
rows: 4,
columns: 20,

View File

@ -6,6 +6,7 @@ import Widget from "./widget";
export const CONFIG = {
type: Widget.getWidgetType(),
name: "TreeSelect",
searchTags: ["dropdown"],
iconSVG: IconSVG,
needsMeta: true,
defaults: {

View File

@ -8,6 +8,7 @@ export const CONFIG = {
name: "Switch",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["boolean"],
defaults: {
label: "Label",
rows: 4,

View File

@ -14,6 +14,7 @@ export const CONFIG = {
name: "Table",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["datagrid"],
defaults: {
rows: 28,
columns: 34,

View File

@ -7,6 +7,7 @@ export const CONFIG = {
type: Widget.getWidgetType(),
name: "Text",
iconSVG: IconSVG,
searchTags: ["typography", "paragraph"],
defaults: {
text: "Label",
fontSize: DEFAULT_FONT_SIZE,

View File

@ -6,6 +6,7 @@ export const CONFIG = {
name: "Video",
iconSVG: IconSVG,
needsMeta: true,
searchTags: ["youtube"],
defaults: {
rows: 28,
columns: 24,

View File

@ -14,6 +14,7 @@ export interface WidgetConfiguration {
isCanvas?: boolean;
needsMeta?: boolean;
features?: WidgetFeatures;
searchTags?: string[];
properties: {
config: PropertyPaneConfig[];
default: Record<string, string>;