Skip to content

Commit

Permalink
feat: add search to external fields
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Nov 27, 2023
1 parent 28f24f9 commit fe3b439
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 23 deletions.
31 changes: 25 additions & 6 deletions apps/demo/config/blocks/Hero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,31 @@ export const Hero: ComponentConfig<HeroProps> = {
quote: {
type: "external",
placeholder: "Select a quote",
fetchList: async () =>
quotes.map((quote, idx) => ({
index: idx,
title: quote.author,
description: quote.content,
})),
showSearch: true,
fetchList: async ({ query }) => {
// Simulate delay
await new Promise((res) => setTimeout(res, 500));

return quotes
.map((quote, idx) => ({
index: idx,
title: quote.author,
description: quote.content,
}))
.filter((item) => {
if (!query) return item;

const queryLowercase = query.toLowerCase();

if (item.title.toLowerCase().indexOf(queryLowercase) > -1) {
return item;
}

if (item.description.toLowerCase().indexOf(queryLowercase) > -1) {
return item;
}
});
},
mapProp: (result) => {
return { index: result.index, label: result.description };
},
Expand Down
8 changes: 6 additions & 2 deletions packages/core/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import styles from "./Button.module.css";
import getClassNameFactory from "../../lib/get-class-name-factory";
import { ClipLoader } from "react-spinners";
Expand All @@ -17,6 +17,7 @@ export const Button = ({
fullWidth,
icon,
size = "medium",
loading: loadingProp = false,
}: {
children: ReactNode;
href?: string;
Expand All @@ -29,8 +30,11 @@ export const Button = ({
fullWidth?: boolean;
icon?: ReactNode;
size?: "medium" | "large";
loading?: boolean;
}) => {
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(loadingProp);

useEffect(() => setLoading(loadingProp), [loadingProp]);

const ElementType = href ? "a" : "button";

Expand Down
73 changes: 61 additions & 12 deletions packages/core/components/ExternalInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useMemo, useEffect, useState } from "react";
import { useMemo, useEffect, useState, useCallback } from "react";
import styles from "./styles.module.css";
import getClassNameFactory from "../../lib/get-class-name-factory";
import { ExternalField } from "../../types/Config";
import { Link, Unlock } from "react-feather";
import { Link, Search, Unlock } from "react-feather";
import { Modal } from "../Modal";
import { Heading } from "../Heading";
import { ClipLoader } from "react-spinners";
import { Button } from "../Button";

const getClassName = getClassNameFactory("ExternalInput", styles);
const getClassNameModal = getClassNameFactory("ExternalInputModal", styles);
Expand Down Expand Up @@ -44,17 +45,29 @@ export const ExternalInput = ({
return Array.from(validKeys);
}, [data]);

useEffect(() => {
(async () => {
const listData = dataCache[name] || (await field.fetchList());
const [searchQuery, setSearchQuery] = useState(field.initialQuery || "");

const search = useCallback(
async (query) => {
setIsLoading(true);

const cacheKey = `${id}-${name}-${query}`;

const listData =
dataCache[cacheKey] || (await field.fetchList({ query }));

if (listData) {
setData(listData);
setIsLoading(false);

dataCache[name] = listData;
dataCache[cacheKey] = listData;
}
})();
},
[name, field]
);

useEffect(() => {
search(searchQuery);
}, []);

return (
Expand Down Expand Up @@ -100,13 +113,47 @@ export const ExternalInput = ({
className={getClassNameModal({
isLoading,
loaded: !isLoading,
hasData: !!data,
hasData: data.length > 0,
})}
>
<div className={getClassNameModal("masthead")}>
<Heading rank={2} size="xxl">
Select content
</Heading>

{field.showSearch && (
<form
className={getClassNameModal("searchForm")}
onSubmit={(e) => {
e.preventDefault();

search(searchQuery);
}}
>
<label className={getClassNameModal("search")}>
<span className={getClassNameModal("searchIconText")}>
Search
</span>
<div className={getClassNameModal("searchIcon")}>
<Search size="18" />
</div>
<input
className={getClassNameModal("searchInput")}
name="q"
type="search"
placeholder="Search"
onChange={(e) => {
setSearchQuery(e.currentTarget.value);
}}
autoComplete="off"
value={searchQuery}
></input>
</label>
<Button type="submit" loading={isLoading} disabled={isLoading}>
Search
</Button>
</form>
)}
</div>

<div className={getClassNameModal("tableWrapper")}>
Expand Down Expand Up @@ -147,12 +194,14 @@ export const ExternalInput = ({
})}
</tbody>
</table>
</div>

<div className={getClassNameModal("noContentBanner")}>No content</div>
<div className={getClassNameModal("loadingBanner")}>
<ClipLoader size={24} aria-label="Loading" />
</div>
</div>

<div className={getClassNameModal("loadingBanner")}>
<ClipLoader size={24} />
<div className={getClassNameModal("noContentBanner")}>
No results.
</div>
</div>
</Modal>
Expand Down
77 changes: 75 additions & 2 deletions packages/core/components/ExternalInput/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@

.ExternalInputModal-masthead {
background-color: white;
display: flex;
flex-wrap: wrap;
gap: 24px;
padding: 32px 24px;
}

.ExternalInputModal-tableWrapper {
position: relative;
overflow-x: auto;
overflow-y: auto;
flex-grow: 1;
Expand Down Expand Up @@ -136,10 +140,15 @@

.ExternalInputModal-loadingBanner {
display: none;
background-color: white;
background-color: #ffffff90;
padding: 64px;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

.ExternalInputModal--isLoading .ExternalInputModal-loadingBanner {
Expand All @@ -148,10 +157,74 @@

.ExternalInputModal-noContentBanner {
display: none;
border-top: 1px solid var(--puck-color-grey-8);
padding: 24px;
text-align: center;
}

.ExternalInputModal--loaded:not(.ExternalInputModal--hasData)
.ExternalInputModal-noContentBanner {
display: block;
padding: 24px;
}

.ExternalInputModal-searchForm {
display: flex;
margin-left: auto;
height: 43px;
gap: 12px;
}

.ExternalInputModal-search {
display: flex;
background: white;
border-width: 1px;
border-style: solid;
border-color: var(--puck-color-grey-8);
border-radius: 4px;
width: 100%;
}

.ExternalInputModal-search:focus-within {
border-color: var(--puck-color-azure-4);
outline: var(--puck-color-azure-8) 4px solid;
outline-offset: 0;
}

.ExternalInputModal-searchIcon {
align-items: center;
background: var(--puck-color-grey-11);
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
border-right: 1px solid var(--puck-color-grey-8);
color: var(--puck-color-grey-6);
display: flex;
justify-content: center;
padding: 12px 15px;
}

.ExternalInputModal-searchIconText {
clip: rect(0 0 0 0);
clip-path: inset(100%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

.ExternalInputModal-searchInput {
border: none;
border-radius: 4px;
background: white;
color: black;
font-family: inherit;
font-size: 14px;
padding: 12px 15px;
width: 100%;
}

.ExternalInputModal-searchInput:focus {
border: none;
outline: none;
box-shadow: none;
}
4 changes: 3 additions & 1 deletion packages/core/types/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ export type ExternalField<
> = BaseField & {
type: "external";
placeholder?: string;
fetchList: () => Promise<any[] | null>;
fetchList: (params: { query: string }) => Promise<any[] | null>;
mapProp?: (value: any) => Props;
getItemSummary: (item: Props, index?: number) => string;
showSearch?: boolean;
initialQuery?: string;
};

export type CustomField<
Expand Down

0 comments on commit fe3b439

Please sign in to comment.