Merge pull request #216 from AntlerVC/develop

Develop
This commit is contained in:
Shams
2020-09-30 22:34:24 +10:00
committed by GitHub
30 changed files with 843 additions and 321 deletions

2
.gitignore vendored
View File

@@ -44,3 +44,5 @@ package-lock.json
node_modules/
cloud_functions/functions/src/functionConfig.json
*.iml
.idea

View File

@@ -3,7 +3,19 @@ import * as _ from "lodash";
export const serverTimestamp = admin.firestore.FieldValue.serverTimestamp;
import { sendEmail } from "./email";
import { hasAnyRole } from "./auth";
export default { sendEmail, serverTimestamp, hasAnyRole };
var characters =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
export function generateId(length) {
var result = "";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export default { generateId, sendEmail, serverTimestamp, hasAnyRole };
export const replacer = (data: any) => (m: string, key: string) => {
const objKey = key.split(":")[0];
const defaultValue = key.split(":")[1] || "";

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@antlerengineering/multiselect": "^0.5.0",
"@antlerengineering/multiselect": "^0.6.0",
"@date-io/date-fns": "1.x",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -45,7 +45,7 @@
"serve": "^11.3.2",
"tinymce": "^5.2.0",
"typescript": "^3.7.2",
"use-algolia": "^1.3.0",
"use-algolia": "^1.4.1",
"use-debounce": "^3.3.0",
"use-persisted-state": "^0.3.0",
"yarn": "^1.19.0",

View File

@@ -1,14 +1,18 @@
import React from "react";
import _merge from "lodash/merge";
import { createMuiTheme, ThemeOptions, fade } from "@material-ui/core/styles";
import {
createMuiTheme,
Theme as ThemeType,
ThemeOptions,
fade,
} from "@material-ui/core/styles";
import ClearIcon from "@material-ui/icons/Clear";
export const HEADING_FONT = "Europa, sans-serif";
export const BODY_FONT = '"Open Sans", sans-serif';
export const ANTLER_RED = "#ed4747";
export const ANTLER_RED_ACCESSIBLE = "#e22729";
export const SECONDARY_GREY = "#282829";
export const SECONDARY_TEXT = "rgba(0, 0, 0, 0.6)";
export const ERROR = "#b00020";
@@ -98,7 +102,7 @@ export const themeBase = createMuiTheme({
},
});
export const defaultOverrides: ThemeOptions = {
export const defaultOverrides = (theme: ThemeType): ThemeOptions => ({
transitions: {
easing: { custom: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
},
@@ -106,18 +110,16 @@ export const defaultOverrides: ThemeOptions = {
MuiContainer: {
root: {
"@supports (padding: max(0px))": {
paddingLeft: `max(${themeBase.spacing(
2
)}px, env(safe-area-inset-left))`,
paddingRight: `max(${themeBase.spacing(
paddingLeft: `max(${theme.spacing(2)}px, env(safe-area-inset-left))`,
paddingRight: `max(${theme.spacing(
2
)}px, env(safe-area-inset-right))`,
"@media (min-width: 640px)": {
paddingLeft: `max(${themeBase.spacing(
paddingLeft: `max(${theme.spacing(
3
)}px, env(safe-area-inset-left))`,
paddingRight: `max(${themeBase.spacing(
paddingRight: `max(${theme.spacing(
3
)}px, env(safe-area-inset-right))`,
},
@@ -125,7 +127,7 @@ export const defaultOverrides: ThemeOptions = {
},
},
MuiTooltip: {
tooltip: themeBase.typography.caption,
tooltip: theme.typography.caption,
},
MuiButton: {
root: { minHeight: 36 },
@@ -137,7 +139,7 @@ export const defaultOverrides: ThemeOptions = {
boxShadow: "none",
},
containedSizeLarge: {
padding: themeBase.spacing(1, 4),
padding: theme.spacing(1, 4),
},
outlinedPrimary: {
@@ -145,10 +147,10 @@ export const defaultOverrides: ThemeOptions = {
borderColor: "rgba(0, 0, 0, 0.23)",
},
outlinedSizeLarge: {
padding: themeBase.spacing(1, 4),
padding: theme.spacing(1, 4),
borderRadius: 500,
"&$outlinedPrimary": { borderColor: ANTLER_RED },
"&$outlinedPrimary": { borderColor: theme.palette.primary.main },
},
},
MuiSvgIcon: {
@@ -157,14 +159,14 @@ export const defaultOverrides: ThemeOptions = {
// Override text field label
MuiFormLabel: {
root: {
...themeBase.typography.subtitle2,
...theme.typography.subtitle2,
lineHeight: 1,
},
},
// Override radio & checkbox labels
MuiFormControlLabel: {
root: { display: "flex" },
label: themeBase.typography.body1,
label: theme.typography.body1,
},
MuiChip: {
root: {
@@ -174,32 +176,32 @@ export const defaultOverrides: ThemeOptions = {
height: "auto",
minHeight: 32,
color: themeBase.palette.text.secondary,
color: theme.palette.text.secondary,
},
label: {
...themeBase.typography.caption,
...theme.typography.caption,
color: "inherit",
padding: themeBase.spacing(1, 1.5),
padding: theme.spacing(1, 1.5),
// whiteSpace: "normal",
"$outlined &": {
paddingTop: themeBase.spacing(0.875),
paddingBottom: themeBase.spacing(0.875),
paddingTop: theme.spacing(0.875),
paddingBottom: theme.spacing(0.875),
},
},
sizeSmall: { minHeight: 24 },
labelSmall: {
padding: themeBase.spacing(0.5, 1.5),
padding: theme.spacing(0.5, 1.5),
},
outlined: {
backgroundColor: themeBase.palette.action.selected,
borderColor: themeBase.palette.action.selected,
backgroundColor: theme.palette.action.selected,
borderColor: theme.palette.action.selected,
},
outlinedPrimary: {
backgroundColor: fade(
ANTLER_RED,
themeBase.palette.action.selectedOpacity
theme.palette.primary.main,
theme.palette.action.selectedOpacity
),
},
@@ -207,7 +209,7 @@ export const defaultOverrides: ThemeOptions = {
},
MuiBadge: {
badge: {
...themeBase.typography.caption,
...theme.typography.caption,
fontFeatureSettings: '"tnum"',
},
},
@@ -252,8 +254,8 @@ export const defaultOverrides: ThemeOptions = {
valueLabel: {
top: -22,
...themeBase.typography.caption,
color: themeBase.palette.primary.main,
...theme.typography.caption,
color: theme.palette.primary.main,
"& > *": {
width: "auto",
@@ -263,12 +265,12 @@ export const defaultOverrides: ThemeOptions = {
whiteSpace: "nowrap",
borderRadius: 500,
padding: themeBase.spacing(0, 1),
paddingRight: themeBase.spacing(0.875),
padding: theme.spacing(0, 1),
paddingRight: theme.spacing(0.875),
},
"& *": { transform: "none" },
},
markLabel: themeBase.typography.caption,
markLabel: theme.typography.caption,
},
MuiLinearProgress: {
colorPrimary: { backgroundColor: "#e7e7e7" },
@@ -308,8 +310,12 @@ export const defaultOverrides: ThemeOptions = {
},
MuiTextField: { variant: "filled" },
},
};
});
export const Theme = (customization: ThemeOptions) =>
createMuiTheme(_merge(_merge(themeBase, defaultOverrides), customization));
createMuiTheme(
_merge(themeBase, defaultOverrides(_merge(themeBase, customization))),
customization
);
export default Theme;

View File

@@ -1,73 +1,79 @@
import React, { useState, useMemo, useEffect } from "react";
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import { useDebouncedCallback } from "use-debounce";
import _get from "lodash/get";
import {
Grid,
TextField,
List,
MenuItem,
ListItemIcon,
Checkbox,
ListItemText,
Divider,
Button,
Typography,
Checkbox,
Divider,
Grid,
InputAdornment,
List,
ListItemIcon,
ListItemText,
MenuItem,
TextField,
Typography,
Radio,
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import { IConnectTableSelectProps } from ".";
import { IConnectServiceSelectProps } from ".";
import useStyles from "./styles";
import Loading from "components/Loading";
import algoliasearch from "algoliasearch/lite";
import { useFiretableContext } from "../../contexts/firetableContext";
const searchClient = algoliasearch(
process.env.REACT_APP_ALGOLIA_APP_ID ?? "",
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY ?? ""
);
export interface IPopupContentsProps
extends Omit<IConnectTableSelectProps, "className" | "TextFieldProps"> {}
extends Omit<IConnectServiceSelectProps, "className" | "TextFieldProps"> {}
// TODO: Implement infinite scroll here
export default function PopupContents({
value = [],
onChange,
collectionPath,
config,
multiple = true,
row,
docRef,
}: IPopupContentsProps) {
const index = collectionPath ?? config.index; //temporary for pre column restructure migration
const url = config.url;
const titleKey = config.titleKey ?? config.primaryKey;
const subtitleKey = config.subtitleKey;
const resultsKey = config.resultsKey;
const primaryKey = config.primaryKey;
const multiple = Boolean(config.multiple);
const classes = useStyles();
const { userClaims } = useFiretableContext();
const algoliaIndex = useMemo(() => {
return searchClient.initIndex(index);
}, [index]);
// Algolia search query
// Webservice search query
const [query, setQuery] = useState("");
// Algolia query response
// Webservice response
const [response, setResponse] = useState<any | null>(null);
const hits: any["hits"] = response?.hits ?? [];
const [docData, setDocData] = useState<any | null>(null);
useEffect(() => {
docRef.get().then((d) => setDocData(d.data()));
}, []);
const hits: any["hits"] = _get(response, resultsKey) ?? [];
const [search] = useDebouncedCallback(
async (query: string) => {
if (!algoliaIndex) return;
const data = { ...userClaims, ...row };
const filters = config?.filters
? config?.filters.replace(/\{\{(.*?)\}\}/g, (m, k) => data[k])
: "";
if (!docData) return;
if (!url) return;
const uri = new URL(url),
params = { q: query };
Object.keys(params).forEach((key) =>
uri.searchParams.append(key, params[key])
);
const resp = await algoliaIndex.search(query, {
filters,
const resp = await fetch(uri.toString(), {
method: "POST",
body: JSON.stringify(docData),
headers: { "content-type": "application/json" },
});
setResponse(resp);
const jsonBody = await resp.json();
setResponse(jsonBody);
},
1000,
{ leading: true }
@@ -75,27 +81,21 @@ export default function PopupContents({
useEffect(() => {
search(query);
}, [query]);
}, [query, docData]);
if (!response) return <Loading />;
const select = (hit: any) => () => {
const { _highlightResult, ...snapshot } = hit;
const output = {
snapshot,
docPath: `${index}/${snapshot.objectID}`,
};
if (multiple) onChange([...value, output]);
else onChange([output]);
if (multiple) onChange([...value, hit]);
else onChange([hit]);
};
const deselect = (hit: any) => () => {
if (multiple)
onChange(value.filter((v) => v.snapshot.objectID !== hit.objectID));
onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey]));
else onChange([]);
};
const selectedValues = value?.map((item) => item.snapshot.objectID);
const selectedValues = value?.map((item) => _get(item, primaryKey));
const clearSelection = () => onChange([]);
@@ -125,35 +125,46 @@ export default function PopupContents({
<Grid item xs className={classes.listRow}>
<List className={classes.list}>
{hits.map((hit) => {
const isSelected = selectedValues.indexOf(hit.objectID) !== -1;
const isSelected =
selectedValues.indexOf(_get(hit, primaryKey)) !== -1;
console.log(`Selected Values: ${selectedValues}`);
return (
<React.Fragment key={hit.objectID}>
<React.Fragment key={_get(hit, primaryKey)}>
<MenuItem
dense
onClick={isSelected ? deselect(hit) : select(hit)}
>
<ListItemIcon className={classes.checkboxContainer}>
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${hit.objectID}`,
}}
/>
{multiple ? (
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
}}
/>
) : (
<Radio
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
}}
/>
)}
</ListItemIcon>
<ListItemText
id={`label-${hit.objectID}`}
primary={config?.primaryKeys
?.map((key: string) => hit[key])
.join(" ")}
secondary={config?.secondaryKeys
?.map((key: string) => hit[key])
.join(" ")}
id={`label-${_get(hit, primaryKey)}`}
primary={_get(hit, titleKey)}
secondary={!subtitleKey ? "" : _get(hit, subtitleKey)}
/>
</MenuItem>
<Divider className={classes.divider} />
@@ -176,7 +187,7 @@ export default function PopupContents({
color="textSecondary"
className={classes.selectedNum}
>
{value?.length} of {response?.nbHits}
{value?.length} of {hits?.length}
</Typography>
<Button

View File

@@ -0,0 +1,75 @@
import React, { lazy, Suspense } from "react";
import clsx from "clsx";
import { TextField, TextFieldProps } from "@material-ui/core";
import useStyles from "./styles";
import Loading from "components/Loading";
import ErrorBoundary from "components/ErrorBoundary";
const PopupContents = lazy(
() => import("./PopupContents" /* webpackChunkName: "PopupContents" */)
);
export type ServiceValue = { value: string; [prop: string]: any };
export interface IConnectServiceSelectProps {
value: ServiceValue[];
onChange: (value: ServiceValue[]) => void;
row: any;
config: {
displayKey: string;
[key: string]: any;
};
editable?: boolean;
/** Optional style overrides for root MUI `TextField` component */
className?: string;
/** Override any props of the root MUI `TextField` component */
TextFieldProps?: Partial<TextFieldProps>;
docRef: firebase.firestore.DocumentReference;
}
export default function ConnectServiceSelect({
value = [],
className,
TextFieldProps = {},
...props
}: IConnectServiceSelectProps) {
const classes = useStyles();
const sanitisedValue = Array.isArray(value) ? value : [];
return (
<TextField
label=""
hiddenLabel
variant={"filled" as any}
select
value={sanitisedValue}
className={clsx(classes.root, className)}
{...TextFieldProps}
SelectProps={{
renderValue: (value) => `${(value as any[]).length} selected`,
displayEmpty: true,
classes: { root: classes.selectRoot },
...TextFieldProps.SelectProps,
// Must have this set to prevent MUI transforming `value`
// prop for this component to a comma-separated string
MenuProps: {
classes: { paper: classes.paper, list: classes.menuChild },
MenuListProps: { disablePadding: true },
getContentAnchorEl: null,
anchorOrigin: { vertical: "bottom", horizontal: "center" },
transformOrigin: { vertical: "top", horizontal: "center" },
...TextFieldProps.SelectProps?.MenuProps,
},
}}
>
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<PopupContents value={sanitisedValue} {...props} />
</Suspense>
</ErrorBoundary>
</TextField>
);
}

View File

@@ -1,85 +1,146 @@
import React, { lazy, Suspense } from "react";
import clsx from "clsx";
import React, { useState, useEffect } from "react";
import useAlgolia from "use-algolia";
import _find from "lodash/find";
import { useDebounce } from "use-debounce";
import { TextField, TextFieldProps } from "@material-ui/core";
import useStyles from "./styles";
import MultiSelect, { MultiSelectProps } from "@antlerengineering/multiselect";
import Loading from "components/Loading";
import ErrorBoundary from "components/ErrorBoundary";
const PopupContents = lazy(
() => import("./PopupContents" /* webpackChunkName: "PopupContents" */)
);
export type ConnectTableValue = { snapshot: any; docPath: string };
export type ConnectTableValue = {
snapshot: any;
docPath: string;
};
export interface IConnectTableSelectProps {
value: ConnectTableValue[];
onChange: (value: ConnectTableValue[]) => void;
row: any;
column: any;
collectionPath: string;
config: {
filters: string;
primaryKeys: string[];
secondaryKeys: string[];
multiple?: boolean;
[key: string]: any;
};
editable?: boolean;
/** Optionally set this prop to `false` to only allow one option */
multiple?: boolean;
/** Optional style overrides for root MUI `TextField` component */
className?: string;
/** Override any props of the root MUI `TextField` component */
TextFieldProps?: Partial<TextFieldProps>;
TextFieldProps?: MultiSelectProps<ConnectTableValue[]>["TextFieldProps"];
}
/**
* TODO: Update this to use @antlerengineering/multiselect
* This is a copy-paste of the old MultiSelect
*/
export default function ConnectTableSelect({
value = [],
onChange,
row,
column,
collectionPath,
config,
editable,
className,
TextFieldProps = {},
...props
}: IConnectTableSelectProps) {
const classes = useStyles();
// Store a local copy of the value so the dropdown doesnt automatically close
// when the user selects a new item and we allow for multiple selections
const [localValue, setLocalValue] = useState(
Array.isArray(value) ? value : []
);
const sanitisedValue = Array.isArray(value) ? value : [];
const [algoliaState, requestDispatch, , setAlgoliaConfig] = useAlgolia(
process.env.REACT_APP_ALGOLIA_APP_ID!,
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY!,
"" // Dont choose the index until the user opens the dropdown
);
const algoliaIndex = collectionPath ?? config.index;
const options = algoliaState.hits.map((hit) => ({
label: config?.primaryKeys?.map((key: string) => hit[key]).join(" "),
value: hit.objectID,
}));
// Pass a list of objectIDs to MultiSelect
const sanitisedValue = localValue.map(
(item) => item.docPath.split("/")[item.docPath.split("/").length - 1]
);
const handleChange = (_newValue) => {
// Ensure we return an array
const newValue = Array.isArray(_newValue)
? _newValue
: _newValue !== null
? [_newValue]
: [];
// Calculate new value
const newLocalValue = newValue.map((objectID) => {
// If this objectID is already in the previous value, use that previous
// values snapshot (in case it points to an object not in the current
// Algolia query)
const existingMatch = _find(localValue, {
docPath: `${algoliaIndex}/${objectID}`,
});
if (existingMatch) return existingMatch;
// If this is a completely new selection, grab the snapshot from the
// current Algolia query
const match = _find(algoliaState.hits, { objectID });
const { _highlightResult, ...snapshot } = match;
return {
snapshot,
docPath: `${algoliaIndex}/${snapshot.objectID}`,
};
});
// If !multiple, we MUST change the value (bypassing localValue),
// otherwise `setLocalValue` wont be called in time for the new
// `localValue` to be read by `handleSave`
if (config.multiple === false) onChange(newLocalValue);
// Otherwise, `setLocalValue` until user closes dropdown
else setLocalValue(newLocalValue);
};
// Save when user closes dropdown
const handleSave = () => {
if (config.multiple !== false) onChange(localValue);
};
// Change MultiSelect input field to search Algolia directly
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search, 1000);
useEffect(() => {
requestDispatch({ query: debouncedSearch });
}, [debouncedSearch]);
return (
<TextField
label=""
hiddenLabel
variant={"filled" as any}
select
value={sanitisedValue}
className={clsx(classes.root, className)}
{...TextFieldProps}
SelectProps={{
renderValue: (value) => `${(value as any[]).length} selected`,
displayEmpty: true,
classes: { root: classes.selectRoot },
...TextFieldProps.SelectProps,
// Must have this set to prevent MUI transforming `value`
// prop for this component to a comma-separated string
multiple: true,
MenuProps: {
classes: { paper: classes.paper, list: classes.menuChild },
MenuListProps: { disablePadding: true },
getContentAnchorEl: null,
anchorOrigin: { vertical: "bottom", horizontal: "center" },
transformOrigin: { vertical: "top", horizontal: "center" },
...TextFieldProps.SelectProps?.MenuProps,
},
<MultiSelect
value={config.multiple === false ? sanitisedValue[0] : sanitisedValue}
onChange={handleChange}
onOpen={() => setAlgoliaConfig({ indexName: algoliaIndex })}
onClose={handleSave}
options={options}
TextFieldProps={{
className,
hiddenLabel: true,
...TextFieldProps,
}}
>
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<PopupContents value={sanitisedValue} {...props} />
</Suspense>
</ErrorBoundary>
</TextField>
label={column?.name}
multiple={(config?.multiple ?? true) as any}
AutocompleteProps={{
loading: algoliaState.loading,
loadingText: <Loading />,
inputValue: search,
onInputChange: (_, value, reason) => {
if (reason === "input") setSearch(value);
},
filterOptions: () => options,
}}
countText={`${localValue.length} of ${
algoliaState.response?.nbHits ?? "?"
}`}
disabled={editable === false}
/>
);
}

View File

@@ -0,0 +1,84 @@
import React from "react";
import { Controller, useWatch } from "react-hook-form";
import { IFieldProps } from "../utils";
import { Chip, Grid, useTheme } from "@material-ui/core";
import { get } from "lodash";
import ConnectServiceSelect, {
IConnectServiceSelectProps,
} from "../../../ConnectServiceSelect";
export interface IConnectServiceProps
extends IFieldProps,
Partial<Omit<IConnectServiceSelectProps, "docRef">> {}
export default function ConnectService({
control,
name,
editable,
...props
}: IConnectServiceProps) {
const theme = useTheme();
const disabled = editable === false;
const { config, docRef } = props;
if (!config) {
return <></>;
}
const displayKey = config.titleKey ?? config.primaryKey;
return (
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleDelete = (hit: any) => () => {
// if (multiple)
onChange(
value.filter(
(v) => get(v, config.primaryKey) !== get(hit, config.primaryKey)
)
);
// else form.setFieldValue(field.name, []);
};
return (
<>
{!disabled && (
<ConnectServiceSelect
{...(props as any)}
value={value}
multiple={false}
onChange={onChange}
docRef={docRef}
TextFieldProps={{
fullWidth: true,
onBlur,
}}
/>
)}
{Array.isArray(value) && (
<Grid
container
spacing={1}
style={{ marginTop: theme.spacing(1) }}
>
{value.map((snapshot) => (
<Grid item key={get(snapshot, config.primaryKey)}>
<Chip
component="li"
size="medium"
label={get(snapshot, displayKey)}
onDelete={disabled ? undefined : handleDelete(snapshot)}
/>
</Grid>
))}
</Grid>
)}
</>
);
}}
/>
);
}

View File

@@ -44,6 +44,9 @@ export default function ConnectTable({
TextFieldProps={{
fullWidth: true,
onBlur,
SelectProps: {
renderValue: () => `${value?.length ?? 0} selected`,
},
}}
/>
)}

View File

@@ -9,7 +9,8 @@ import {
} from "@material-ui/pickers";
import { DATE_TIME_FORMAT } from "constants/dates";
import AccessTimeIcon from "@material-ui/icons/AccessTime";
import DateRangeIcon from "@material-ui/icons/DateRange";
import TimeIcon from "@material-ui/icons/Schedule";
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
@@ -52,7 +53,7 @@ export default function DateTimePicker({
InputAdornmentProps={{
style: { marginRight: theme.spacing(-1) },
}}
keyboardIcon={<AccessTimeIcon />}
keyboardIcon={<TimeIcon />}
{...props}
value={transformedValue}
onChange={handleChange}
@@ -60,6 +61,16 @@ export default function DateTimePicker({
label=""
hiddenLabel
id={`sidedrawer-field-${name}`}
dateRangeIcon={
<DateRangeIcon
style={{ color: theme.palette.primary.contrastText }}
/>
}
timeIcon={
<TimeIcon
style={{ color: theme.palette.primary.contrastText }}
/>
}
/>
);
}}

View File

@@ -12,7 +12,7 @@ export type IMultiSelectProps = IFieldProps &
MultiSelectProps<string>,
"name" | "multiple" | "value" | "onChange" | "options"
> & {
config?: { options: string[] };
config?: { options: string[]; freeText: boolean };
};
export default function MultiSelect({
@@ -51,7 +51,7 @@ export default function MultiSelect({
onBlur,
}}
searchable
freeText={false}
freeText={config?.freeText}
/>
{value && Array.isArray(value) && (

View File

@@ -12,7 +12,10 @@ export type ISingleSelectProps = IFieldProps &
MultiSelectProps<string>,
"name" | "multiple" | "value" | "onChange" | "options"
> & {
config?: { options: string[] };
config?: {
options: string[];
freeText?: boolean;
};
};
/**
@@ -39,7 +42,7 @@ export default function SingleSelect({
{...props}
options={config?.options ?? []}
multiple={false}
value={value}
value={Array.isArray(value) ? value[0] : value}
onChange={onChange}
disabled={editable === false}
TextFieldProps={{
@@ -48,7 +51,7 @@ export default function SingleSelect({
onBlur,
}}
searchable
freeText={false}
freeText={config?.freeText}
/>
{value?.length > 0 && (

View File

@@ -8,7 +8,8 @@ import { Fields, Values, getInitialValues, Field } from "./utils";
import { FieldType } from "constants/fields";
import Autosave from "./Autosave";
import FieldWrapper from "./FieldWrapper";
import { useAppContext } from "contexts/appContext";
import { useFiretableContext } from "contexts/firetableContext";
import Text from "./Fields/Text";
const Url = lazy(
() => import("./Fields/Url" /* webpackChunkName: "SideDrawer-Url" */)
@@ -84,6 +85,13 @@ const ConnectTable = lazy(
"./Fields/ConnectTable" /* webpackChunkName: "SideDrawer-ConnectTable" */
)
);
const ConnectService = lazy(
() =>
import(
"./Fields/ConnectService" /* webpackChunkName: "SideDrawer-ConnectTable" */
)
);
const Code = lazy(
() => import("./Fields/Code" /* webpackChunkName: "SideDrawer-Code" */)
);
@@ -105,6 +113,11 @@ export default function Form({ fields, values }: IFormProps) {
const { ref: docRef, ...rowValues } = values;
const defaultValues = { ...initialValues, ...rowValues };
const { tableState } = useFiretableContext();
const { userDoc } = useAppContext();
const userDocHiddenFields =
userDoc.state.doc?.tables?.[`${tableState?.tablePath}`]?.hiddenFields ?? [];
const { register, control } = useForm({
mode: "onBlur",
defaultValues,
@@ -138,130 +151,136 @@ export default function Form({ fields, values }: IFormProps) {
/>
<Grid container spacing={4} direction="column" wrap="nowrap">
{fields.map((_field, i) => {
// Call the field function with values if necessary
// Otherwise, just use the field object
const field: Field = _isFunction(_field) ? _field(values) : _field;
const { type, ...fieldProps } = field;
let _type = type;
{fields
.filter((f) => !userDocHiddenFields.includes(f.name))
.map((_field, i) => {
// Call the field function with values if necessary
// Otherwise, just use the field object
const field: Field = _isFunction(_field) ? _field(values) : _field;
const { type, ...fieldProps } = field;
let _type = type;
// Derivative/aggregate field support
if (field.config && field.config.renderFieldType) {
_type = field.config.renderFieldType;
}
// Derivative/aggregate field support
if (field.config && field.config.renderFieldType) {
_type = field.config.renderFieldType;
}
let fieldComponent: React.ComponentType<any> | null = null;
let fieldComponent: React.ComponentType<any> | null = null;
switch (_type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.number:
fieldComponent = Text;
break;
switch (_type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.number:
fieldComponent = Text;
break;
case FieldType.url:
fieldComponent = Url;
break;
case FieldType.url:
fieldComponent = Url;
break;
case FieldType.singleSelect:
fieldComponent = SingleSelect;
break;
case FieldType.singleSelect:
fieldComponent = SingleSelect;
break;
case FieldType.multiSelect:
fieldComponent = MultiSelect;
break;
case FieldType.multiSelect:
fieldComponent = MultiSelect;
break;
case FieldType.date:
fieldComponent = DatePicker;
break;
case FieldType.date:
fieldComponent = DatePicker;
break;
case FieldType.dateTime:
fieldComponent = DateTimePicker;
break;
case FieldType.dateTime:
fieldComponent = DateTimePicker;
break;
case FieldType.checkbox:
fieldComponent = Checkbox;
break;
case FieldType.checkbox:
fieldComponent = Checkbox;
break;
case FieldType.color:
fieldComponent = Color;
break;
case FieldType.color:
fieldComponent = Color;
break;
case FieldType.slider:
fieldComponent = Slider;
break;
case FieldType.slider:
fieldComponent = Slider;
break;
case FieldType.richText:
fieldComponent = RichText;
break;
case FieldType.richText:
fieldComponent = RichText;
break;
case FieldType.image:
fieldComponent = ImageUploader;
break;
case FieldType.image:
fieldComponent = ImageUploader;
break;
case FieldType.file:
fieldComponent = FileUploader;
break;
case FieldType.file:
fieldComponent = FileUploader;
break;
case FieldType.rating:
fieldComponent = Rating;
break;
case FieldType.rating:
fieldComponent = Rating;
break;
case FieldType.percentage:
fieldComponent = Percentage;
break;
case FieldType.percentage:
fieldComponent = Percentage;
break;
case FieldType.connectTable:
fieldComponent = ConnectTable;
break;
case FieldType.connectTable:
fieldComponent = ConnectTable;
break;
case FieldType.subTable:
fieldComponent = SubTable;
break;
case FieldType.connectService:
fieldComponent = ConnectService;
break;
case FieldType.action:
fieldComponent = Action;
break;
case FieldType.subTable:
fieldComponent = SubTable;
break;
case FieldType.json:
fieldComponent = JsonEditor;
break;
case FieldType.action:
fieldComponent = Action;
break;
case FieldType.code:
fieldComponent = Code;
break;
case FieldType.json:
fieldComponent = JsonEditor;
break;
case undefined:
// default:
case FieldType.code:
fieldComponent = Code;
break;
case undefined:
// default:
return null;
default:
break;
}
// Should not reach this state
if (fieldComponent === null) {
console.error("`fieldComponent` is null");
return null;
}
default:
break;
}
// Should not reach this state
if (fieldComponent === null) {
console.error("`fieldComponent` is null");
return null;
}
return (
<FieldWrapper
key={fieldProps.name ?? i}
type={_type}
name={field.name}
label={field.label}
>
{React.createElement(fieldComponent, {
...fieldProps,
control,
docRef,
})}
</FieldWrapper>
);
})}
return (
<FieldWrapper
key={fieldProps.name ?? i}
type={_type}
name={field.name}
label={field.label}
>
{React.createElement(fieldComponent, {
...fieldProps,
control,
docRef,
})}
</FieldWrapper>
);
})}
<FieldWrapper
type="debug"

View File

@@ -21,11 +21,12 @@ export type Fields = (Field | ((values: Values) => Field))[];
export const initializeValue = (type) => {
switch (type) {
case FieldType.singleSelect:
case FieldType.multiSelect:
case FieldType.image:
case FieldType.file:
return [];
case FieldType.singleSelect:
case FieldType.date:
case FieldType.dateTime:
return null;

View File

@@ -68,13 +68,13 @@ export default function SideDrawer() {
useEffect(() => {
if (cell) {
window.history.pushState(
"",
`${tableState?.tablePath}`,
`${window.location.pathname}?rowRef=${encodeURIComponent(
tableState?.rows[cell.row].ref.path
)}`
);
// window.history.pushState(
// "",
// `${tableState?.tablePath}`,
// `${window.location.pathname}?rowRef=${encodeURIComponent(
// tableState?.rows[cell.row].ref.path
// )}`
// );
console.log(tableState?.tablePath, tableState?.rows[cell.row].id);
if (urlDocState.doc) {
urlDocState.unsubscribe();

View File

@@ -66,6 +66,70 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
</Grid>
</>
);
case FieldType.connectService:
return (
<>
<TextField
label="Webservice Url"
name="url"
value={config.url}
fullWidth
onChange={(e) => {
handleChange("url")(e.target.value);
}}
/>
<TextField
label="Results key Path"
name="resultsKey"
helperText="Can be specified as a key path"
placeholder="data.results"
value={config.resultsKey}
fullWidth
onChange={(e) => {
handleChange("resultsKey")(e.target.value);
}}
/>
<TextField
label="Primary Key"
name="primaryKey"
value={config.primaryKey}
fullWidth
onChange={(e) => {
handleChange("primaryKey")(e.target.value);
}}
/>
<TextField
label="Title Key (optional)"
name="titleKey"
value={config.titleKey}
fullWidth
onChange={(e) => {
handleChange("titleKey")(e.target.value);
}}
/>
<TextField
label="SubTitle Key (optional)"
name="subtitleKey"
value={config.subtitleKey}
fullWidth
onChange={(e) => {
handleChange("subtitleKey")(e.target.value);
}}
/>
<FormControlLabel
control={
<Switch
checked={config.multiple}
onChange={() =>
handleChange("multiple")(!Boolean(config.multiple))
}
name="select-multiple"
/>
}
label="Enable multiple item selection"
/>
</>
);
case FieldType.connectTable:
const tableOptions = _sortBy(
tables?.map((t) => ({

View File

@@ -106,7 +106,13 @@ export default function Action({
className={classes.fab}
onClick={handleRun}
disabled={
isRunning || !!(hasRan && !value.redo && !value.undo) || disabled
isRunning ||
!!(
hasRan &&
(config["redo.enabled"] ? false : !value.redo) &&
(config["undo.enabled"] ? false : !value.undo)
) ||
disabled
}
>
{isRunning ? (

View File

@@ -0,0 +1,121 @@
import React from "react";
import clsx from "clsx";
import { CustomCellProps } from "./withCustomCell";
import _get from "lodash/get";
import { createStyles, makeStyles, Grid, Chip } from "@material-ui/core";
import ConnectServiceSelect from "components/ConnectServiceSelect";
import { useFiretableContext } from "contexts/firetableContext";
const useStyles = makeStyles((theme) =>
createStyles({
root: {
minWidth: 0,
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
},
disabled: {},
fullHeight: {
height: "100%",
font: "inherit",
color: "inherit",
letterSpacing: "inherit",
},
select: {
padding: theme.spacing(0, 3, 0, 1.5),
display: "flex",
alignItems: "center",
"&&": { paddingRight: theme.spacing(4) },
"$disabled &": { paddingRight: theme.spacing(0) },
},
icon: {
marginRight: theme.spacing(1),
"$disabled &": { display: "none" },
},
chipList: {
overflowX: "hidden",
width: "100%",
},
chip: { cursor: "inherit" },
})
);
export default function ConnectService({
rowIdx,
column,
value,
onSubmit,
row,
docRef,
}: CustomCellProps) {
const classes = useStyles();
const { config } = column as any;
const { dataGridRef } = useFiretableContext();
if (!config) return <></>;
const disabled = !column.editable || config?.isLocked;
const titleKey = config.titleKey ?? config.primaryKey;
// Render chips
const renderValue = (value) => (
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
{value?.map((doc: any) => (
<Grid item key={_get(doc, config.primaryKey)}>
<Chip label={_get(doc, titleKey)} className={classes.chip} />
</Grid>
))}
</Grid>
);
const onClick = (e) => e.stopPropagation();
const onClose = () => {
if (dataGridRef?.current?.selectCell)
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
};
return (
<ConnectServiceSelect
row={row}
value={value}
onChange={onSubmit}
config={config}
docRef={docRef}
TextFieldProps={{
fullWidth: true,
label: "",
hiddenLabel: true,
variant: "standard" as "filled",
InputProps: {
disableUnderline: true,
classes: { root: classes.fullHeight },
},
SelectProps: {
onClose,
classes: {
root: clsx(classes.fullHeight, classes.select),
icon: clsx(classes.icon),
},
renderValue,
MenuProps: {
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
},
},
onClick,
disabled,
}}
className={clsx(
classes.fullHeight,
classes.root,
disabled && classes.disabled
)}
/>
);
}

View File

@@ -61,21 +61,22 @@ export default function ConnectTable({
const { collectionPath, config } = column as any;
const { dataGridRef } = useFiretableContext();
if (!config || !config.primaryKeys) return <></>;
const disabled = !column.editable || config?.isLocked;
const disabled = column.editable === false || config?.isLocked;
// Render chips
const renderValue = (value) => (
const renderValue = () => (
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
{value?.map((doc: any) => (
<Grid item key={doc.docPath}>
<Chip
label={config.primaryKeys
.map((key: string) => doc.snapshot[key])
.join(" ")}
className={classes.chip}
/>
</Grid>
))}
{Array.isArray(value) &&
value.map((doc: any) => (
<Grid item key={doc.docPath}>
<Chip
label={config.primaryKeys
.map((key: string) => doc.snapshot[key])
.join(" ")}
className={classes.chip}
/>
</Grid>
))}
</Grid>
);
@@ -88,10 +89,12 @@ export default function ConnectTable({
return (
<ConnectTableSelect
row={row}
column={column}
value={value}
onChange={onSubmit}
collectionPath={collectionPath}
config={config}
editable={column.editable as boolean}
TextFieldProps={{
fullWidth: true,
label: "",
@@ -114,7 +117,6 @@ export default function ConnectTable({
},
},
onClick,
disabled,
}}
className={clsx(
classes.fullHeight,

View File

@@ -4,6 +4,9 @@ import { CustomCellProps } from "./withCustomCell";
import { useDebouncedCallback } from "use-debounce";
import { makeStyles, createStyles } from "@material-ui/core";
import DateRangeIcon from "@material-ui/icons/DateRange";
import TimeIcon from "@material-ui/icons/Schedule";
import { FieldType, DateIcon, DateTimeIcon } from "constants/fields";
import { DATE_FORMAT, DATE_TIME_FORMAT } from "constants/dates";
@@ -37,6 +40,10 @@ const useStyles = makeStyles((theme) =>
height: "100%",
padding: theme.spacing(1.5, 0),
},
dateTabIcon: {
color: theme.palette.primary.contrastText,
},
})
);
@@ -92,6 +99,8 @@ export default function Date({
classes: { root: "row-hover-iconButton" },
}}
DialogProps={{ onClick: (e) => e.stopPropagation() }}
dateRangeIcon={<DateRangeIcon className={classes.dateTabIcon} />}
timeIcon={<TimeIcon className={classes.dateTabIcon} />}
/>
</MuiPickersUtilsProvider>
);

View File

@@ -63,6 +63,16 @@ export default function MultiSelect({
// Support SingleSelect field
const isSingle = (column as any).type === FieldType.singleSelect;
let sanitisedValue: any;
if (isSingle) {
if (value === undefined || value === null) sanitisedValue = null;
else if (Array.isArray(value)) sanitisedValue = value[0];
else sanitisedValue = value;
} else {
if (value === undefined || value === null) sanitisedValue = [];
else sanitisedValue = value;
}
// Render chips or basic string
const renderValue = isSingle
? () =>
@@ -110,15 +120,9 @@ export default function MultiSelect({
>
<div>
<MultiSelect_
value={
value === undefined || value === null
? isSingle
? null
: []
: value
}
value={sanitisedValue}
onChange={onSubmit}
freeText={!isSingle && config.freeText}
freeText={config.freeText}
multiple={!isSingle as any}
label={column.name}
labelPlural={column.name}

View File

@@ -19,7 +19,7 @@ export default function Rating({
onSubmit,
}: CustomCellProps) {
const classes = useStyles();
const { max, precision } = (column as any).config as {
const { max, precision } = ((column as any).config ?? {}) as {
max: number;
precision: number;
};

View File

@@ -31,6 +31,7 @@ const Action = lazy(() => import("./Action" /* webpackChunkName: "Action" */));
const ConnectTable = lazy(
() => import("./ConnectTable" /* webpackChunkName: "ConnectTable" */)
);
const ConnectService = lazy(() => import("./ConnectService"));
const SubTable = lazy(
() => import("./SubTable" /* webpackChunkName: "SubTable" */)
);
@@ -106,6 +107,9 @@ export const getFormatter = (column: any, readOnly: boolean = false) => {
case FieldType.connectTable:
return withCustomCell(ConnectTable, readOnly);
case FieldType.connectService:
return withCustomCell(ConnectService, readOnly);
case FieldType.subTable:
return withCustomCell(SubTable, readOnly);

View File

@@ -21,6 +21,7 @@ const useStyles = makeStyles((theme) =>
export type CustomCellProps = FormatterProps<any> & {
value: any;
onSubmit: (value: any) => void;
docRef: firebase.firestore.DocumentReference;
};
const getCellValue = (row, key) => {
@@ -49,6 +50,7 @@ const withCustomCell = (
<Suspense fallback={<div />}>
<Component
{...props}
docRef={props.row.ref}
value={getCellValue(props.row, props.column.key as string)}
onSubmit={handleSubmit}
/>

View File

@@ -27,8 +27,8 @@ const useStyles = makeStyles((theme) =>
margin: 0,
padding: theme.spacing(1.5, 0, 3),
height: 200,
overflowY: "auto",
height: 400,
overflowY: "overlay" as any,
"& li": { margin: theme.spacing(0.5, 0) },
},

View File

@@ -50,7 +50,7 @@ const useStyles = makeStyles((theme) =>
flexShrink: 0,
},
header: { overflowX: "scroll" },
header: { overflowX: "hidden" },
data: {
overflow: "scroll",
height: 300,

View File

@@ -34,11 +34,21 @@ export default function ImportWizard() {
const { tableState, tableActions } = useFiretableContext();
useEffect(() => {
if (!tableState) return;
if (
tableState?.config.tableConfig.doc &&
!tableState?.config.tableConfig.doc?.columns
)
tableState.config.tableConfig.doc &&
!tableState.config.tableConfig.doc?.columns
) {
setOpen(true);
return;
}
if (Array.isArray(tableState.filters) && tableState.filters?.length > 0)
tableActions!.table.filter([]);
if (Array.isArray(tableState.orderBy) && tableState.orderBy?.length > 0)
tableActions!.table.orderBy([]);
}, [tableState]);
if (tableState?.rows.length === 0) return null;

View File

@@ -23,6 +23,8 @@ import ImageIcon from "@material-ui/icons/PhotoSizeSelectActual";
import FileIcon from "@material-ui/icons/AttachFile";
import SingleSelectIcon from "@material-ui/icons/FormatListBulleted";
import WebServiceIcon from "@material-ui/icons/Http";
import MultiSelectIcon from "assets/icons/MultiSelect";
import ConnectTableIcon from "assets/icons/ConnectTable";
@@ -67,6 +69,7 @@ export {
ColorIcon,
SliderIcon,
UserIcon,
WebServiceIcon,
};
export enum FieldType {
@@ -92,6 +95,7 @@ export enum FieldType {
singleSelect = "SINGLE_SELECT",
multiSelect = "MULTI_SELECT",
connectService = "SERVICE_SELECT",
connectTable = "DOCUMENT_SELECT",
subTable = "SUB_TABLE",
@@ -128,7 +132,6 @@ export const FIELDS = [
{ icon: <ImageIcon />, name: "Image", type: FieldType.image },
{ icon: <FileIcon />, name: "File", type: FieldType.file },
{
icon: <SingleSelectIcon />,
name: "Single Select",
@@ -139,16 +142,20 @@ export const FIELDS = [
name: "Multi Select",
type: FieldType.multiSelect,
},
{
icon: <SubTableIcon />,
name: "Sub-table",
type: FieldType.subTable,
},
{
icon: <ConnectTableIcon />,
name: "Connect Table",
type: FieldType.connectTable,
},
{
icon: <SubTableIcon />,
name: "Sub-table",
type: FieldType.subTable,
icon: <WebServiceIcon />,
name: "Webservice Select",
type: FieldType.connectService,
},
{ icon: <JsonIcon />, name: "JSON", type: FieldType.json },
@@ -198,6 +205,7 @@ export const FIELD_TYPE_DESCRIPTIONS = {
[FieldType.connectTable]:
"Connects to an existing table to fetch a snapshot of values from a row. Requires Algolia integration.",
[FieldType.connectService]: "Select a value from a list of websevice results",
[FieldType.subTable]:
"Creates a sub-table. Also displays number of rows inside the sub-table. Max sub-table levels: 100.",

View File

@@ -106,10 +106,10 @@
"@algolia/logger-common" "4.1.0"
"@algolia/requester-common" "4.1.0"
"@antlerengineering/multiselect@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@antlerengineering/multiselect/-/multiselect-0.5.0.tgz#26e9790a9050f45986d4530df01cbedf54d310e9"
integrity sha512-rPLgC6Qy/WF2DDlhIjgEaDEmULRRnxC3DKjt11oT3iQiDkRJj7Mx8cCL4bViRLEkg9W4/AW5EngneHpa8aZybA==
"@antlerengineering/multiselect@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@antlerengineering/multiselect/-/multiselect-0.6.0.tgz#aed7aef5f2c4a9316d00328d85eb41d149694b47"
integrity sha512-fvW26wQsaghW9KjDnUWP3WLaktx/DgzT1M2quPIMrQ5e+gPT5psNEmWGEqG4jN0hYfsnl+5m5cHMwCD/wR/Mmg==
"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
version "7.8.3"
@@ -9964,11 +9964,15 @@ node-fetch@^2.3.0, node-fetch@^2.6.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.1.0.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
node-forge@^0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a"
integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==
node-int64@^0.4.0:
version "0.4.0"
@@ -14370,10 +14374,10 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
use-algolia@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/use-algolia/-/use-algolia-1.3.0.tgz#a53623b9c248bad14ca1cba551658c625613cb72"
integrity sha512-t6rY8apz0CmeNUbVchsi0+7UIeutJ8FPNfGTbx/f2vCw1LFYAo9ZUrG81vIsKMAhSLP2FKEXeztDWYr976MEpw==
use-algolia@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/use-algolia/-/use-algolia-1.4.1.tgz#a39747a3c39c69a99b19815ffebf2426526e8287"
integrity sha512-V1XlPYDQK45Tws2Zk//SKFjHiBmlwiGFESygFWWtNrVwlZRJrbF/pPLMquv1Hkq9jW2npX8y7GA/pdWBAfAeuQ==
use-debounce@^3.3.0:
version "3.4.0"