Merge branch 'fields-refactor' of https://github.com/AntlerVC/firetable into fields-refactor

This commit is contained in:
Shams mosowi
2021-01-12 18:28:08 +08:00
15 changed files with 411 additions and 22 deletions

View File

@@ -73,13 +73,14 @@ export default function withPopoverCell(
const basicCell = (
<BasicCell
{...props}
column={props.column}
value={localValue}
name={(props.column as any).name}
type={(props.column as any).type as FieldType}
onSubmit={handleSubmit}
disabled={props.column.editable === false}
docRef={props.row.ref}
setShowComplexCell={setShowComplexCell}
ref={basicCellRef}
disabled={props.column.editable === false}
onSubmit={handleSubmit}
/>
);

View File

@@ -1,4 +1,5 @@
import React from "react";
import clsx from "clsx";
import { IPopoverBasicCellProps } from "../types";
import { makeStyles, createStyles, Grid, ButtonBase } from "@material-ui/core";
@@ -6,17 +7,12 @@ import { makeStyles, createStyles, Grid, ButtonBase } from "@material-ui/core";
const useStyles = makeStyles((theme) =>
createStyles({
root: {
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
paddingLeft: theme.spacing(1.5),
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
padding: theme.spacing(0, 1),
},
colorIndicator: {
@@ -40,7 +36,7 @@ export const Color = React.forwardRef(function Color(
container
alignItems="center"
spacing={1}
className={classes.root}
className={clsx("cell-collapse-padding", classes.root)}
component={ButtonBase}
onClick={() => setShowComplexCell(true)}
ref={ref}

View File

@@ -0,0 +1,151 @@
import React, { useState, useEffect } from "react";
import useAlgolia from "use-algolia";
import _find from "lodash/find";
import { useDebounce } from "use-debounce";
import MultiSelect, { MultiSelectProps } from "@antlerengineering/multiselect";
import Loading from "components/Loading";
export type ConnectTableValue = {
docPath: string;
snapshot: Record<string, any>;
};
export interface IConnectTableSelectProps {
value: ConnectTableValue[];
onChange: (value: ConnectTableValue[]) => void;
column: any;
config: {
filters: string;
primaryKeys: string[];
secondaryKeys?: string[];
multiple?: boolean;
searchLabel?: string;
[key: string]: any;
};
disabled?: boolean;
/** Optional style overrides for root MUI `TextField` component */
className?: string;
/** Override any props of the root MUI `TextField` component */
TextFieldProps?: MultiSelectProps<ConnectTableValue[]>["TextFieldProps"];
onClose?: MultiSelectProps<ConnectTableValue[]>["onClose"];
/** Load the Algolia index before the MultiSelect onOpen function is triggered */
loadBeforeOpen?: boolean;
}
export default function ConnectTableSelect({
value = [],
onChange,
column,
config,
disabled,
className,
TextFieldProps = {},
onClose,
loadBeforeOpen,
}: IConnectTableSelectProps) {
// 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 algoliaIndex = config.index;
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 if !loadBeforeOpen
loadBeforeOpen ? algoliaIndex : ""
);
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);
if (onClose) onClose();
};
// Change MultiSelect input field to search Algolia directly
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search, 1000);
useEffect(() => {
requestDispatch({ query: debouncedSearch });
}, [debouncedSearch]);
return (
<MultiSelect
value={config.multiple === false ? sanitisedValue[0] : sanitisedValue}
onChange={handleChange}
onOpen={() => setAlgoliaConfig({ indexName: algoliaIndex })}
onClose={handleSave}
options={options}
TextFieldProps={{
className,
hiddenLabel: true,
...TextFieldProps,
}}
label={column?.name}
labelPlural={config.searchLabel}
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={disabled}
/>
);
}

View File

@@ -0,0 +1,89 @@
import React from "react";
import clsx from "clsx";
import { IPopoverBasicCellProps } from "../types";
import {
makeStyles,
createStyles,
ButtonBase,
Grid,
Chip,
} from "@material-ui/core";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
const useStyles = makeStyles((theme) =>
createStyles({
root: {
height: "100%",
padding: theme.spacing(0, 1, 0, 1.5),
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
},
value: {
flex: 1,
maxWidth: `calc(100% - 24px + 4px)`,
overflow: "hidden",
marginRight: 0,
},
chip: {
display: "flex",
cursor: "inherit",
},
chipLabel: { whiteSpace: "nowrap" },
icon: {
display: "block",
color: theme.palette.action.active,
},
disabled: {
color: theme.palette.action.disabled,
},
})
);
export const ConnectTable = React.forwardRef(function ConnectTable(
{ value, setShowComplexCell, disabled, column }: IPopoverBasicCellProps,
ref: React.Ref<any>
) {
const classes = useStyles();
const config = column.config ?? {};
return (
<ButtonBase
className={clsx("cell-collapse-padding", classes.root)}
onClick={() => setShowComplexCell(true)}
ref={ref}
disabled={disabled}
>
<Grid
container
wrap="nowrap"
alignItems="center"
spacing={1}
className={classes.value}
>
{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>
<ArrowDropDownIcon
className={clsx(classes.icon, disabled && classes.disabled)}
/>
</ButtonBase>
);
});
export default ConnectTable;

View File

@@ -0,0 +1,40 @@
import React from "react";
import { IPopoverCellProps } from "../types";
import _get from "lodash/get";
import ConnectTableSelect from "./ConnectTableSelect";
export default function ConnectTable({
value,
onSubmit,
column,
parentRef,
setShowComplexCell,
disabled,
}: IPopoverCellProps) {
const config = column.config ?? {};
if (!config || !config.primaryKeys) return null;
return (
<ConnectTableSelect
column={column}
value={value}
onChange={onSubmit}
config={(config as any) ?? {}}
disabled={disabled}
TextFieldProps={{
style: { display: "none" },
SelectProps: {
open: true,
MenuProps: {
anchorEl: parentRef,
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
},
},
}}
onClose={() => setShowComplexCell(false)}
loadBeforeOpen
/>
);
}

View File

@@ -0,0 +1,74 @@
import React from "react";
import { Controller } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { useTheme, Grid, Chip } from "@material-ui/core";
import ConnectTableSelect from "./ConnectTableSelect";
export default function ConnectTable({
column,
control,
disabled,
}: ISideDrawerFieldProps) {
const theme = useTheme();
const config = column.config ?? {};
return (
<Controller
control={control}
name={column.key}
render={({ onChange, onBlur, value }) => {
const handleDelete = (hit: any) => () => {
// if (multiple)
onChange(value.filter((v) => v.snapshot.objectID !== hit.objectID));
// else form.setFieldValue(field.name, []);
};
return (
<>
{!disabled && (
<ConnectTableSelect
column={column}
config={(config as any) ?? {}}
value={value}
onChange={onChange}
TextFieldProps={{
label: "",
hiddenLabel: true,
fullWidth: true,
onBlur,
SelectProps: {
renderValue: () => `${value?.length ?? 0} selected`,
},
}}
/>
)}
{Array.isArray(value) && (
<Grid
container
spacing={1}
style={{ marginTop: theme.spacing(1) }}
>
{value.map(({ snapshot }) => (
<Grid item key={snapshot.objectID}>
<Chip
component="li"
size="medium"
label={column.config?.primaryKeys
?.map((key: string) => snapshot[key])
.join(" ")}
onDelete={disabled ? undefined : handleDelete(snapshot)}
/>
</Grid>
))}
</Grid>
)}
</>
);
}}
/>
);
}

View File

@@ -0,0 +1,35 @@
import React, { lazy } from "react";
import { IFieldConfig, FieldType } from "components/fields/types";
import withPopoverCell from "components/Table/withPopoverCell";
import ConnectTableIcon from "assets/icons/ConnectTable";
import PopoverBasicCell from "./PopoverBasicCell";
import NullEditor from "components/Table/editors/NullEditor";
const PopoverCell = lazy(
() =>
import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectTable" */)
);
const SideDrawerField = lazy(
() =>
import(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-ConnectTable" */
)
);
export const config: IFieldConfig = {
type: FieldType.connectTable,
name: "Connect Table",
dataType: "{ docPath: string; snapshot: Record<string, any>; }",
initialValue: [],
icon: <ConnectTableIcon />,
description:
"Connects to an existing table to fetch a snapshot of values from a row. Requires Algolia integration.",
TableCell: withPopoverCell(PopoverCell, PopoverBasicCell, {
anchorOrigin: { horizontal: "left", vertical: "bottom" },
transparent: true,
}),
TableEditor: NullEditor,
SideDrawerField,
};
export default config;

View File

@@ -0,0 +1,4 @@
export const sanitiseValue = (value: any) => {
if (value === undefined || value === null || value === "") return [];
else return value as string[];
};

View File

@@ -44,7 +44,7 @@ const useStyles = makeStyles((theme) =>
})
);
export const SingleSelect = React.forwardRef(function SingleSelect(
export const MultiSelect = React.forwardRef(function MultiSelect(
{ value, setShowComplexCell, disabled, onSubmit }: IPopoverBasicCellProps,
ref: React.Ref<any>
) {
@@ -86,4 +86,4 @@ export const SingleSelect = React.forwardRef(function SingleSelect(
</ButtonBase>
);
});
export default SingleSelect;
export default MultiSelect;

View File

@@ -6,7 +6,7 @@ import MultiSelect_ from "@antlerengineering/multiselect";
import { sanitiseValue } from "./utils";
export default function SingleSelect({
export default function MultiSelect({
value,
onSubmit,
column,

View File

@@ -9,7 +9,7 @@ import FormattedChip from "components/FormattedChip";
import { sanitiseValue } from "./utils";
import { ConvertStringToArray } from "./ConvertStringToArray";
export default function SingleSelect({
export default function MultiSelect({
column,
control,
disabled,

View File

@@ -19,7 +19,7 @@ const SideDrawerField = lazy(
export const config: IFieldConfig = {
type: FieldType.multiSelect,
name: "MultiSelect",
name: "Multi Select",
dataType: "string[]",
initialValue: [],
icon: <MultiSelectIcon />,

View File

@@ -19,7 +19,7 @@ const SideDrawerField = lazy(
export const config: IFieldConfig = {
type: FieldType.singleSelect,
name: "SingleSelect",
name: "Single Select",
dataType: "string | null",
initialValue: null,
icon: <SingleSelectIcon />,

View File

@@ -24,6 +24,7 @@ import File_ from "./File";
import SingleSelect from "./SingleSelect";
import MultiSelect from "./MultiSelect";
import SubTable from "./SubTable";
import ConnectTable from "./ConnectTable";
import Json from "./Json";
import Code from "./Code";
import RichText from "./RichText";
@@ -60,7 +61,7 @@ export const FIELDS: IFieldConfig[] = [
MultiSelect,
// CONNECTION
SubTable,
// TODO: connectTable,
ConnectTable,
// TODO: connectService,
// CODE
Json,

View File

@@ -39,10 +39,8 @@ export interface IPopoverCellProps extends ICustomCellProps {
setShowComplexCell: React.Dispatch<React.SetStateAction<boolean>>;
parentRef: PopoverProps["anchorEl"];
}
export interface IPopoverBasicCellProps extends IBasicCellProps {
export interface IPopoverBasicCellProps extends ICustomCellProps {
setShowComplexCell: React.Dispatch<React.SetStateAction<boolean>>;
disabled: boolean;
onSubmit: (value: any) => void;
}
export interface ISideDrawerFieldProps {