Merge branch 'develop' into rc

* develop: (39 commits)
  ammend changes
  split jotai atom into selectedCell and anchorEle atom
  amend requested changes
  create table
  useBasicSearch: switch to match-sorter
  ammend fix recommendations
  remove contextMenuRef and replace with jotai
  fix issue requested in pr
  remove unused dependencies
  fix ContextMenu close animation (#630)
  use mui cut, paste icons (#630)
  comment out console.logs (#630)
  TableRow: remove unnecessary wrapping component ContextMenu (#630)
  fix add column to left/right showing stale data in new column settings
  enable context menu on all string field and only number field
  fix remove bug
  add context menu feature with cut, copy, and paste
  add cut and paste icons
  fix filters override clear button copy
  clean up status number logic
  ...
This commit is contained in:
Sidney Alcantara
2022-02-04 15:31:48 +11:00
42 changed files with 991 additions and 486 deletions

View File

@@ -39,6 +39,7 @@
"jszip": "^3.6.0",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"match-sorter": "^6.3.1",
"notistack": "^2.0.2",
"pb-util": "^1.0.1",
"query-string": "^6.8.3",
@@ -56,7 +57,6 @@
"react-helmet": "^6.1.0",
"react-hook-form": "^7.21.2",
"react-image": "^4.0.3",
"react-joyride": "^2.3.0",
"react-json-view": "^1.19.1",
"react-markdown": "^8.0.0",
"react-router-dom": "^5.0.1",

40
src/atoms/ContextMenu.ts Normal file
View File

@@ -0,0 +1,40 @@
import { useAtom } from "jotai";
import { atomWithReset, useResetAtom, useUpdateAtom } from "jotai/utils";
export type SelectedCell = {
rowIndex: number;
colIndex: number;
};
export type anchorEl = HTMLElement;
const selectedCellAtom = atomWithReset<SelectedCell | null>(null);
const anchorEleAtom = atomWithReset<HTMLElement | null>(null);
export function useSetAnchorEle() {
const setAnchorEle = useUpdateAtom(anchorEleAtom);
return { setAnchorEle };
}
export function useSetSelectedCell() {
const setSelectedCell = useUpdateAtom(selectedCellAtom);
return { setSelectedCell };
}
export function useContextMenuAtom() {
const [anchorEle] = useAtom(anchorEleAtom);
const [selectedCell] = useAtom(selectedCellAtom);
const resetAnchorEle = useResetAtom(anchorEleAtom);
const resetSelectedCell = useResetAtom(selectedCellAtom);
const resetContextMenu = async () => {
await resetAnchorEle();
await resetSelectedCell();
};
return {
anchorEle,
selectedCell,
resetContextMenu,
};
}

View File

@@ -27,10 +27,8 @@ export default function NewColumn({
data,
openSettings,
handleClose,
handleSave,
}: INewColumnProps) {
const { table, settingsActions } = useProjectContext();
const { settingsActions, table, tableActions } = useProjectContext();
const [columnLabel, setColumnLabel] = useState("");
const [fieldKey, setFieldKey] = useState("");
const [type, setType] = useState(FieldType.shortText);
@@ -139,14 +137,19 @@ export default function NewColumn({
actions={{
primary: {
onClick: () => {
handleSave(fieldKey, {
type,
name: columnLabel,
fieldName: fieldKey,
key: fieldKey,
config: {},
...data.initializeColumn,
});
tableActions?.column.insert(
{
type,
name: columnLabel,
fieldName: fieldKey,
key: fieldKey,
config: {},
},
{
insert: data.insert,
index: data.sourceIndex,
}
);
if (requireConfiguration) {
openSettings({
type,
@@ -154,7 +157,6 @@ export default function NewColumn({
fieldName: fieldKey,
key: fieldKey,
config: {},
...data.initializeColumn,
});
} else handleClose();
analytics.logEvent("create_column", {

View File

@@ -51,6 +51,7 @@ type SelectedColumnHeader = {
column: Column<any> & { [key: string]: any };
anchorEl: PopoverProps["anchorEl"];
};
export type ColumnMenuRef = {
selectedColumnHeader: SelectedColumnHeader | null;
setSelectedColumnHeader: React.Dispatch<
@@ -92,9 +93,7 @@ export default function ColumnMenu() {
if (column && column.type === FieldType.last) {
setModal({
type: ModalStates.new,
data: {
initializeColumn: { index: column.index ? column.index + 1 : 0 },
},
data: {},
});
}
}, [column]);
@@ -209,7 +208,8 @@ export default function ColumnMenu() {
setModal({
type: ModalStates.new,
data: {
initializeColumn: { index: column.index ? column.index - 1 : 0 },
insert: "left",
sourceIndex: column.index,
},
}),
},
@@ -220,7 +220,8 @@ export default function ColumnMenu() {
setModal({
type: ModalStates.new,
data: {
initializeColumn: { index: column.index ? column.index + 1 : 0 },
insert: "right",
sourceIndex: column.index,
},
}),
},
@@ -351,6 +352,7 @@ export default function ColumnMenu() {
open={modal.type === ModalStates.typeChange}
/>
<FieldSettings
key={column.key}
{...menuModalProps}
open={modal.type === ModalStates.settings}
/>

View File

@@ -0,0 +1,49 @@
import { Menu } from "@mui/material";
import { default as MenuItem } from "./MenuItem";
import { IContextMenuItem } from "./MenuItem";
interface IMenuContents {
anchorEl: HTMLElement;
open: boolean;
handleClose: () => void;
items: IContextMenuItem[];
}
export function MenuContents({
anchorEl,
open,
handleClose,
items,
}: IMenuContents) {
const handleContext = (e: React.MouseEvent) => e.preventDefault();
return (
<Menu
id="cell-context-menu"
aria-labelledby="cell-context-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
sx={{
"& .MuiMenu-paper": {
backgroundColor: "background.default",
width: 200,
maxWidth: "100%",
},
}}
onContextMenu={handleContext}
>
{items.map((item, indx: number) => (
<MenuItem key={indx} {...item} />
))}
</Menu>
);
}

View File

@@ -0,0 +1,34 @@
import {
ListItemIcon,
ListItemText,
MenuItem,
Typography,
} from "@mui/material";
export interface IContextMenuItem {
onClick: () => void;
icon: JSX.Element;
label: string;
disabled?: boolean;
hotkeyLabel?: string;
}
export default function ContextMenuItem({
onClick,
icon,
label,
disabled,
hotkeyLabel,
}: IContextMenuItem) {
return (
<MenuItem disabled={disabled} onClick={onClick}>
<ListItemIcon>{icon} </ListItemIcon>
<ListItemText> {label} </ListItemText>
{hotkeyLabel && (
<Typography variant="body2" color="text.secondary">
{hotkeyLabel}
</Typography>
)}
</MenuItem>
);
}

View File

@@ -0,0 +1,27 @@
import _find from "lodash/find";
import { getFieldProp } from "@src/components/fields";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { MenuContents } from "./MenuContent";
import { useContextMenuAtom, useSetSelectedCell } from "@src/atoms/ContextMenu";
export default function ContextMenu() {
const { tableState } = useProjectContext();
const { anchorEle, selectedCell, resetContextMenu } = useContextMenuAtom();
const columns = tableState?.columns;
const selectedColIndex = selectedCell?.colIndex;
const selectedCol = _find(columns, { index: selectedColIndex });
const configActions =
getFieldProp("contextMenuActions", selectedCol?.type) ||
function empty() {};
const actions = configActions(selectedCell, resetContextMenu) || [];
if (!anchorEle || actions.length === 0) return <></>;
return (
<MenuContents
anchorEl={anchorEle}
open={Boolean(anchorEle)}
handleClose={resetContextMenu}
items={actions}
/>
);
}

View File

@@ -1,16 +1,24 @@
import { useSetAnchorEle } from "@src/atoms/ContextMenu";
import { Fragment } from "react";
import { Row, RowRendererProps } from "react-data-grid";
import OutOfOrderIndicator from "./OutOfOrderIndicator";
export default function TableRow(props: RowRendererProps<any>) {
const { setAnchorEle } = useSetAnchorEle();
const handleContextMenu = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
e.preventDefault();
setAnchorEle?.(e?.target as HTMLElement);
};
if (props.row._rowy_outOfOrder)
return (
<Fragment key={props.row.id}>
<OutOfOrderIndicator top={props.top} height={props.height} />
<Row {...props} />
<Row onContextMenu={handleContextMenu} {...props} />
</Fragment>
);
return <Row {...props} />;
return <Row onContextMenu={handleContextMenu} {...props} />;
}

View File

@@ -19,6 +19,7 @@ import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer";
import TableHeader from "../TableHeader";
import ColumnHeader from "./ColumnHeader";
import ColumnMenu from "./ColumnMenu";
import ContextMenu from "./ContextMenu";
import FinalColumnHeader from "./FinalColumnHeader";
import FinalColumn from "./formatters/FinalColumn";
import TableRow from "./TableRow";
@@ -31,6 +32,7 @@ import { formatSubTableName } from "@src/utils/fns";
import { useAppContext } from "@src/contexts/AppContext";
import { useProjectContext } from "@src/contexts/ProjectContext";
import useWindowSize from "@src/hooks/useWindowSize";
import { useSetSelectedCell } from "@src/atoms/ContextMenu";
export type TableColumn = Column<any> & {
isNew?: boolean;
@@ -52,6 +54,7 @@ export default function Table() {
updateCell,
} = useProjectContext();
const { userDoc, userClaims } = useAppContext();
const { setSelectedCell } = useSetSelectedCell();
const userDocHiddenFields =
userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id)]
@@ -262,6 +265,12 @@ export default function Table() {
});
}
}}
onSelectedCellChange={({ rowIdx, idx }) =>
setSelectedCell({
rowIndex: rowIdx,
colIndex: idx,
})
}
/>
</DndProvider>
) : (
@@ -270,6 +279,7 @@ export default function Table() {
</TableContainer>
<ColumnMenu />
<ContextMenu />
<BulkActions
selectedRows={selectedRows}
columns={columns}

View File

@@ -229,9 +229,10 @@ export default function Filters() {
}}
>
Clear
{overrideTableFilters
? " (ignore table filter)"
: " (use table filter)"}
{hasTableFilters &&
(overrideTableFilters
? " (ignore table filter)"
: " (use table filter)")}
</Button>
<Button
@@ -337,20 +338,16 @@ export default function Filters() {
<div className="content">
<FilterInputs {...userFilterInputs} />
{hasTableFilters && (
<FormControlLabel
control={
<Checkbox
checked={overrideTableFilters}
onChange={(e) =>
setOverrideTableFilters(e.target.checked)
}
/>
}
label="Override table filters"
sx={{ justifyContent: "center", mb: 1, mr: 0 }}
/>
)}
<FormControlLabel
control={
<Checkbox
checked={overrideTableFilters}
onChange={(e) => setOverrideTableFilters(e.target.checked)}
/>
}
label="Override table filters"
sx={{ justifyContent: "center", mb: 1, mr: 0 }}
/>
<Stack
direction="row"

View File

@@ -134,8 +134,10 @@ export default function TableSettings({
await settingsActions?.updateTable(data);
deployExtensionsWebhooks();
clearDialog();
analytics.logEvent("update_table", { type: values.tableType });
} else {
await settingsActions?.createTable(data);
await analytics.logEvent("create_table", { type: values.tableType });
deployExtensionsWebhooks(() => {
if (router.location.pathname === "/") {
router.history.push(
@@ -149,10 +151,6 @@ export default function TableSettings({
clearDialog();
});
}
analytics.logEvent(
TableSettingsDialogModes.update ? "update_table" : "create_table",
{ type: values.tableType }
);
};
const fields = tableSettings(

View File

@@ -6,6 +6,8 @@ import EmailIcon from "@mui/icons-material/MailOutlined";
import BasicCell from "../_BasicCell/BasicCellValue";
import TextEditor from "@src/components/Table/editors/TextEditor";
import { filterOperators } from "../ShortText/Filter";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Email" */)
@@ -20,6 +22,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <EmailIcon />,
description: "Email address. Not validated.",
contextMenuActions: BasicContextMenuActions,
TableCell: withBasicCell(BasicCell),
TableEditor: TextEditor,
SideDrawerField,

View File

@@ -6,6 +6,7 @@ import LongTextIcon from "@mui/icons-material/Notes";
import BasicCell from "./BasicCell";
import TextEditor from "@src/components/Table/editors/TextEditor";
import { filterOperators } from "../ShortText/Filter";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
@@ -23,6 +24,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <LongTextIcon />,
description: "Text displayed on multiple lines.",
contextMenuActions: BasicContextMenuActions,
TableCell: withBasicCell(BasicCell),
TableEditor: TextEditor,
SideDrawerField,

View File

@@ -6,6 +6,7 @@ import NumberIcon from "@src/assets/icons/Number";
import BasicCell from "./BasicCell";
import TextEditor from "@src/components/Table/editors/TextEditor";
import { filterOperators } from "./Filter";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Number" */)
@@ -20,6 +21,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <NumberIcon />,
description: "Numeric value.",
contextMenuActions: BasicContextMenuActions,
TableCell: withBasicCell(BasicCell),
TableEditor: TextEditor,
SideDrawerField,

View File

@@ -6,6 +6,7 @@ import PhoneIcon from "@mui/icons-material/PhoneOutlined";
import BasicCell from "../_BasicCell/BasicCellValue";
import TextEditor from "@src/components/Table/editors/TextEditor";
import { filterOperators } from "../ShortText/Filter";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
@@ -21,6 +22,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <PhoneIcon />,
description: "Phone number stored as text. Not validated.",
contextMenuActions: BasicContextMenuActions,
TableCell: withBasicCell(BasicCell),
TableEditor: TextEditor,
SideDrawerField,

View File

@@ -5,6 +5,7 @@ import withHeavyCell from "../_withTableCell/withHeavyCell";
import RichTextIcon from "@mui/icons-material/TextFormat";
import BasicCell from "../_BasicCell/BasicCellNull";
import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-RichText" */)
@@ -25,6 +26,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <RichTextIcon />,
description: "HTML edited with a rich text editor.",
contextMenuActions: BasicContextMenuActions,
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: withSideDrawerEditor(TableCell),
SideDrawerField,

View File

@@ -7,6 +7,7 @@ import BasicCell from "../_BasicCell/BasicCellValue";
import TextEditor from "@src/components/Table/editors/TextEditor";
import { filterOperators } from "./Filter";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
import(
@@ -26,6 +27,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <ShortTextIcon />,
description: "Text displayed on a single line.",
contextMenuActions: BasicContextMenuActions,
TableCell: withBasicCell(BasicCell),
TableEditor: TextEditor,
SideDrawerField,

View File

@@ -0,0 +1,68 @@
import Subheading from "@src/components/Table/ColumnMenu/Subheading";
import EditIcon from "@mui/icons-material/Edit";
import IconButton from "@mui/material/IconButton";
import Grid from "@mui/material/Grid";
import Divider from "@mui/material/Divider";
import { IConditionModal } from "./Settings";
import { createValueLabel } from "./utils/conditionListHelper";
interface I_ConditionList {
config: Record<string, any>;
setModal: React.Dispatch<React.SetStateAction<IConditionModal>>;
}
export default function ConditionList({ config, setModal }: I_ConditionList) {
const conditions = config?.conditions ?? [];
const noConditions = Boolean(conditions?.length < 1); // Double check this
if (noConditions) {
return (
<>
No conditions set yet
<br />
</>
);
}
return (
<>
<Subheading>Conditions</Subheading>
{conditions.map((condition, index) => {
return (
<>
<Grid
container
justifyContent="space-between"
alignItems={"center"}
>
<GridItem
index={index}
condition={condition}
setModal={setModal}
/>
</Grid>
<Divider />
</>
);
})}
</>
);
}
const GridItem = ({ condition, setModal, index }: any) => {
const noCondition = Boolean(!condition);
if (noCondition) return <></>;
return (
<>
{condition?.label}
<Grid item>
{createValueLabel(condition)}
<IconButton
onClick={() => setModal({ isOpen: true, condition, index })}
>
<EditIcon />
</IconButton>
</Grid>
</>
);
};

View File

@@ -0,0 +1,113 @@
import { useEffect } from "react";
import _find from "lodash/find";
import Modal from "@src/components/Modal";
import DeleteIcon from "@mui/icons-material/Delete";
import { default as Content } from "./ConditionModalContent";
import { EMPTY_STATE } from "./Settings";
import { isElement, isEmpty } from "lodash";
export default function ConditionModal({
modal,
setModal,
conditions,
setConditions,
}) {
const handleClose = () => setModal(EMPTY_STATE);
const handleSave = () => {
let _conditions = [...conditions];
_conditions[modal.index] = modal.condition;
setConditions(_conditions);
setModal(EMPTY_STATE);
};
const handleAdd = () => {
const labelIsEmpty = Boolean(modal.condition.label.length < 4);
const stringValueIsEmpty = Boolean(
modal.condition.type === "string" && modal.condition.value.length === 0
);
const hasDuplicate = Boolean(_find(conditions, modal.condition));
const validation = Boolean(
labelIsEmpty || stringValueIsEmpty || hasDuplicate
);
if (validation) return;
function setConditionHack(type, condition) {
let rCondition = condition;
if (type === "undefined") rCondition = { ...condition, value: undefined };
if (type === "boolean" && typeof condition.value === "object")
rCondition = { ...condition, value: false }; //Again 'rowy's multiselect does not accept default value'
return rCondition;
}
const modalCondition = setConditionHack(
modal.condition.type,
modal.condition
);
const noConditions = Boolean(conditions?.length === 0 || !conditions);
const arr = noConditions
? [modalCondition]
: [...conditions, modalCondition];
setConditions(arr);
setModal(EMPTY_STATE);
};
const handleRemove = () => {
const _newConditions = conditions.filter(
(c, index) => index !== modal.index
);
setConditions(_newConditions);
setModal(EMPTY_STATE);
};
const handleUpdate = (key: string) => (value) => {
const newState = {
...modal,
condition: { ...modal.condition, [key]: value },
};
setModal(newState);
};
const primaryAction = (index) => {
return index === null
? {
children: "Add condition",
onClick: () => handleAdd(),
disabled: false,
}
: {
children: "Save changes",
onClick: () => handleSave(),
disabled: false,
};
};
const secondaryAction = (index) => {
return index === null
? {
children: "Cancel",
onClick: () => setModal(EMPTY_STATE),
}
: {
startIcon: <DeleteIcon />,
children: "Remove condition",
onClick: () => handleRemove(),
};
};
useEffect(() => {
handleUpdate("operator")(modal.condition.operator ?? "==");
}, [modal.condition.type]);
return (
<Modal
open={modal.isOpen}
title={`${modal.index ? "Edit" : "Add"} condition`}
maxWidth={"xs"}
onClose={handleClose}
actions={{
primary: primaryAction(modal.index),
secondary: secondaryAction(modal.index),
}}
children={
<Content
condition={modal.condition}
conditions={conditions}
handleUpdate={handleUpdate}
/>
}
/>
);
}

View File

@@ -0,0 +1,104 @@
import _find from "lodash/find";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import MultiSelect from "@rowy/multiselect";
interface I_ConditionModalContent {
handleUpdate: () => void;
modal: any;
}
const multiSelectOption = [
{ label: "Boolean", value: "boolean" },
{ label: "Number", value: "number" },
{ label: "String", value: "string" },
{ label: "Undefined", value: "undefined" },
{ label: "Null", value: "null" },
];
const booleanOptions = [
{ label: "True", value: "true" },
{ label: "False", value: "false" },
];
const operatorOptions = [
{ label: "Less than", value: "<" },
{ label: "Less than or equal", value: "<=" },
{ label: "Equal", value: "==" },
{ label: "Equal or more than", value: ">=" },
{ label: "More than", value: ">" },
];
export default function ConditionModalContent({
condition,
conditions,
handleUpdate,
}: any) {
const { label, operator, type, value } = condition;
const duplicateCond = Boolean(_find(conditions, condition));
const labelReqLen = Boolean(condition.label.length < 4);
return (
<>
<Typography variant="overline">DATA TYPE (input)</Typography>
<MultiSelect
options={multiSelectOption}
onChange={(v) => handleUpdate("type")(v)}
value={type}
multiple={false}
label="Select data type"
/>
{/** This is the issue where false is causing a problem */}
{/** To add defaultValue into MultiSelect?*/}
{type === "boolean" && (
<MultiSelect
options={booleanOptions}
onChange={(v) => handleUpdate("value")(v === "true")}
value={value ? "true" : "false"}
multiple={false}
label="Select condition value"
/>
)}
{type === "number" && (
<Grid container direction="row" justifyContent="space-between">
<div style={{ width: "45%" }}>
<MultiSelect
options={operatorOptions}
onChange={(v) => handleUpdate("operator")(v)}
value={operator}
multiple={false}
label="Select operator"
/>
</div>
<TextField
error={duplicateCond}
type="number"
label="Value"
value={value}
onChange={(e) => handleUpdate("value")(Number(e.target.value))}
helperText={
duplicateCond ? "Numeric Conditional already exists" : ""
}
/>
</Grid>
)}
{type === "string" && (
<TextField
error={duplicateCond}
fullWidth
label="Value"
value={value}
onChange={(e) => handleUpdate("value")(e.target.value)}
helperText={duplicateCond ? "string value already exists" : ""}
/>
)}
<TextField
error={labelReqLen}
value={label}
label="Label"
fullWidth
onChange={(e) => handleUpdate("label")(e.target.value)}
/>
</>
);
}

View File

@@ -0,0 +1,12 @@
import { IFilterOperator } from "../types";
export const filterOperators: IFilterOperator[] = [
{
label: "equals",
value: "==",
},
{
label: "not equals",
value: "!=",
},
];

View File

@@ -0,0 +1,71 @@
import { forwardRef, useMemo } from "react";
import { IPopoverInlineCellProps } from "../types";
import { ButtonBase } from "@mui/material";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import _find from "lodash/find";
import getLabel from "./utils/getLabelHelper";
import { LowPriority } from "@mui/icons-material";
export const StatusSingleSelect = forwardRef(function StatusSingleSelect(
{ column, value, showPopoverCell, disabled }: IPopoverInlineCellProps,
ref: React.Ref<any>
) {
const conditions = column.config?.conditions ?? [];
const lowPriorityOperator = ["<", "<=", ">=", ">"];
const otherOperator = conditions.filter(
(c) => !lowPriorityOperator.includes(c.operator)
);
/**Revisit this */
const sortLowPriorityList = conditions
.filter((c) => {
return lowPriorityOperator.includes(c.operator);
})
.sort((a, b) => {
const aDistFromValue = Math.abs(value - a.value);
const bDistFromValue = Math.abs(value - b.value);
//return the smallest distance
return aDistFromValue - bDistFromValue;
});
const sortedConditions = [...otherOperator, ...sortLowPriorityList];
const label = useMemo(
() => getLabel(value, sortedConditions),
[value, sortedConditions]
);
return (
<ButtonBase
onClick={() => showPopoverCell(true)}
ref={ref}
disabled={disabled}
className="cell-collapse-padding"
style={{
padding: "var(--cell-padding)",
paddingRight: 0,
height: "100%",
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
}}
>
<div style={{ flexGrow: 1, overflow: "hidden" }}>{label}</div>
{!disabled && (
<ArrowDropDownIcon
className="row-hover-iconButton"
sx={{
flexShrink: 0,
mr: 0.5,
borderRadius: 1,
p: (32 - 24) / 2 / 8,
boxSizing: "content-box",
}}
/>
)}
</ButtonBase>
);
});
export default StatusSingleSelect;

View File

@@ -0,0 +1,48 @@
import _find from "lodash/find";
import { IPopoverCellProps } from "../types";
import MultiSelect_ from "@rowy/multiselect";
export default function StatusSingleSelect({
value,
onSubmit,
column,
parentRef,
showPopoverCell,
disabled,
}: IPopoverCellProps) {
const config = column.config ?? {};
const conditions = config.conditions ?? [];
/**Revisit eventually, can we abstract or use a helper function to clean this? */
const reMappedConditions = conditions.map((c) => {
let rValue = { ...c };
if (c.type === "number") {
if (c.operator === "<") rValue = { ...c, value: c.value - 1 };
if (c.operator === ">") rValue = { ...c, value: c.value + 1 };
}
return rValue;
});
return (
<MultiSelect_
value={value}
onChange={(v) => onSubmit(v)}
options={conditions.length >= 1 ? reMappedConditions : []} // this handles when conditions are deleted
multiple={false}
freeText={config.freeText}
disabled={disabled}
label={column.name as string}
labelPlural={column.name as string}
TextFieldProps={{
style: { display: "none" },
SelectProps: {
open: true,
MenuProps: {
anchorEl: parentRef,
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
},
},
}}
onClose={() => showPopoverCell(false)}
/>
);
}

View File

@@ -1,21 +1,12 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { ISettingsProps } from "../types";
import Subheading from "@src/components/Table/ColumnMenu/Subheading";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Grid from "@mui/material/Grid";
import Divider from "@mui/material/Divider";
import EditIcon from "@mui/icons-material/Edit";
import AddIcon from "@mui/icons-material/Add";
import Modal from "@src/components/Modal";
import DeleteIcon from "@mui/icons-material/Delete";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import MultiSelect from "@rowy/multiselect";
import Button from "@mui/material/Button";
import ConditionModal from "./ConditionModal";
import ConditionList from "./ConditionList";
const EMPTY_STATE: {
export interface IConditionModal {
isOpen: boolean;
index: number | null;
condition: {
@@ -24,7 +15,9 @@ const EMPTY_STATE: {
label: string;
operator: string | undefined;
};
} = {
}
export const EMPTY_STATE: IConditionModal = {
index: null,
isOpen: false,
condition: {
@@ -34,190 +27,12 @@ const EMPTY_STATE: {
operator: "==",
},
};
const ConditionModal = ({ modal, setModal, conditions, setConditions }) => {
const handleClose = () => {
setModal(EMPTY_STATE);
};
const handleSave = () => {
let _conditions = [...conditions];
_conditions[modal.index] = modal.condition;
setConditions(_conditions);
setModal(EMPTY_STATE);
};
const handleAdd = () => {
setConditions(
conditions ? [...conditions, modal.condition] : [modal.condition]
);
setModal(EMPTY_STATE);
};
const handleRemove = () => {
let _conditions = [...conditions];
delete _conditions[modal.index];
setConditions(_conditions);
setModal(EMPTY_STATE);
};
const handleUpdate = (key: string) => (value) => {
setModal({ ...modal, condition: { ...modal.condition, [key]: value } });
};
useEffect(() => {
handleUpdate("operator")(modal.condition.operator ?? "==");
}, [modal.condition.type]);
return (
<Modal
open={modal.isOpen}
title={`${modal.index ? "Edit" : "Add"} condition`}
maxWidth={"xs"}
onClose={handleClose}
actions={{
primary:
modal.index === null
? {
children: "Add condition",
onClick: handleAdd,
disabled: false,
}
: {
children: "Save changes",
onClick: handleSave,
disabled: false,
},
secondary:
modal.index === null
? {
children: "Cancel",
onClick: () => {
setModal(EMPTY_STATE);
},
}
: {
startIcon: <DeleteIcon />,
children: "Remove condition",
onClick: handleRemove,
},
}}
children={
<>
<Typography variant="overline">DATA TYPE (input)</Typography>
<MultiSelect
options={[
{ label: "Boolean", value: "boolean" },
{ label: "Number", value: "number" },
{ label: "String", value: "string" },
{ label: "Undefined", value: "undefined" },
{ label: "Null", value: "null" },
]}
onChange={handleUpdate("type")}
value={modal.condition.type}
multiple={false}
label="Select data type"
/>
<Typography variant="overline">Condition </Typography>
{modal.condition.type === "boolean" && (
<MultiSelect
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
onChange={(v) => handleUpdate("value")(v === "true")}
value={modal.condition.value ? "true" : "false"}
multiple={false}
label="Select condition value"
/>
)}
{modal.condition.type === "number" && (
<Grid container direction="row" justifyContent="space-between">
<div style={{ width: "45%" }}>
<MultiSelect
options={[
{ label: "Less than", value: "<" },
{ label: "Less than or equal", value: "<=" },
{ label: "Equal", value: "==" },
{ label: "Equal or more than", value: ">=" },
{ label: "More than", value: ">" },
]}
onChange={handleUpdate("operator")}
value={modal.condition.operator}
multiple={false}
label="Select operator"
/>
</div>
<TextField
type="number"
label="Value"
value={modal.condition.value}
onChange={(e) => handleUpdate("value")(e.target.value)}
/>
</Grid>
)}
{modal.condition.type === "string" && (
<TextField
fullWidth
label="Value"
value={modal.condition.value}
onChange={(e) => handleUpdate("value")(e.target.value)}
/>
)}
<Typography variant="overline">Assigned label (output)</Typography>
<TextField
value={modal.condition.label}
label="Type the cell output"
fullWidth
onChange={(e) => handleUpdate("label")(e.target.value)}
/>
</>
}
/>
);
};
export default function Settings({ onChange, config }: ISettingsProps) {
const [modal, setModal] = useState(EMPTY_STATE);
const { conditions } = config;
return (
<>
<Subheading>Conditions</Subheading>
{conditions ? (
conditions.map((condition, index) => {
return (
<>
<Grid
container
justifyContent="space-between"
alignItems={"center"}
>
{condition.label}
<Grid item>
{["undefined", "null"].includes(condition.type)
? condition.type
: `${condition.type}:${
condition.type === "number" ? condition.operator : ""
}${
condition.type === "boolean"
? JSON.stringify(condition.value)
: condition.value
}`}
<IconButton
onClick={() => {
setModal({ isOpen: true, condition, index });
}}
>
<EditIcon />
</IconButton>
</Grid>
</Grid>
<Divider />
</>
);
})
) : (
<>
No conditions set yet
<br />
</>
)}
<ConditionList config={config} setModal={setModal} />
<Button
onClick={() => setModal({ ...EMPTY_STATE, isOpen: true })}
startIcon={<AddIcon />}

View File

@@ -1,25 +1,36 @@
import { Controller } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { Grid } from "@mui/material";
import "@mui/lab";
import { useFieldStyles } from "@src/components/SideDrawer/Form/utils";
import { useStatusStyles } from "./styles";
export default function Rating({ control, column }: ISideDrawerFieldProps) {
const fieldClasses = useFieldStyles();
const ratingClasses = useStatusStyles();
import MultiSelect from "@rowy/multiselect";
import getLabel from "./utils/getLabelHelper";
export default function Status({
control,
column,
disabled,
}: ISideDrawerFieldProps) {
const config = column.config ?? {};
return (
<Controller
control={control}
name={column.key}
render={({ field: { value } }) => (
<Grid container alignItems="center" className={fieldClasses.root}>
<>{value}</>
</Grid>
render={({ field: { onChange, onBlur, value } }) => (
<>
<MultiSelect
value={getLabel(value, config?.conditions)}
onChange={onChange}
options={config?.conditions ?? []}
multiple={false}
freeText={config?.freeText}
disabled={disabled}
TextFieldProps={{
label: "",
hiddenLabel: true,
onBlur,
id: `sidedrawer-field-${column.key}`,
}}
/>
</>
)}
/>
);

View File

@@ -1,48 +0,0 @@
import { useMemo } from "react";
import { IHeavyCellProps } from "../types";
import { useStatusStyles } from "./styles";
import _find from "lodash/find";
export default function Status({ column, value }: IHeavyCellProps) {
const statusClasses = useStatusStyles();
const conditions = column.config?.conditions ?? [];
const label = useMemo(() => {
if (["null", "undefined"].includes(typeof value)) {
const condition = _find(conditions, (c) => c.type === typeof value);
return condition?.label;
} else if (typeof value === "number") {
const numberConditions = conditions.filter((c) => c.type === "number");
for (let i = 0; i < numberConditions.length; i++) {
const condition = numberConditions[i];
switch (condition.operator) {
case "<":
if (value < condition.value) return condition.label;
break;
case "<=":
if (value <= condition.value) return condition.label;
break;
case ">=":
if (value >= condition.value) return condition.label;
break;
case ">":
if (value > condition.value) return condition.label;
break;
case "==":
default:
if (value == condition.value) return condition.label;
break;
}
}
} else {
for (let i = 0; i < conditions.length; i++) {
const condition = conditions[i];
if (value == condition.value) return condition.label;
}
}
return JSON.stringify(value);
}, [value, conditions]);
return <>{label}</>;
}

View File

@@ -1,14 +1,14 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withHeavyCell from "../_withTableCell/withHeavyCell";
import StatusIcon from "@src/assets/icons/Status";
import BasicCell from "../_BasicCell/BasicCellNull";
import NullEditor from "@src/components/Table/editors/NullEditor";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-Status" */)
);
import { filterOperators } from "./Filter";
import BasicCell from "../_BasicCell/BasicCellNull";
import PopoverCell from "./PopoverCell";
import InlineCell from "./InlineCell";
import withPopoverCell from "../_withTableCell/withPopoverCell";
const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Status" */)
@@ -26,10 +26,16 @@ export const config: IFieldConfig = {
initializable: true,
icon: <StatusIcon />,
description: "Displays field value as custom status text. Read-only. ",
TableCell: withHeavyCell(BasicCell, TableCell),
TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, {
anchorOrigin: { horizontal: "left", vertical: "bottom" },
transparent: true,
}),
TableEditor: NullEditor as any,
settings: Settings,
SideDrawerField,
requireConfiguration: true,
filter: {
operators: filterOperators,
},
};
export default config;

View File

@@ -0,0 +1,12 @@
export function createValueLabel(condition) {
const { operator, type, value } = condition || {};
const typeLabelMap = new Map([
["undefined", `${type}`],
["null", `${type}`],
["number", ` ${type}:${operator}${value}`],
["boolean", `${type}:${value}`],
]);
const string = typeLabelMap.get(type);
const validString = Boolean(typeof string === "string");
return validString ? string : JSON.stringify(value);
}

View File

@@ -0,0 +1,72 @@
import _find from "lodash/find";
type value = number | "string" | undefined | null;
interface condition {
type: string;
operator: string;
label: string;
value: value;
}
//TODO ADD TYPES
const getFalseyLabelFrom = (arr: condition[], value: string) => {
const falseyType = (value) =>
typeof value === "object" ? "null" : "undefined";
const conditions = _find(arr, (c) => c.type === falseyType(value));
return conditions?.label;
};
const getBooleanLabelFrom = (arr: condition[], value: string) => {
const boolConditions = arr.filter((c) => c.type === "boolean");
for (let c of boolConditions) {
if (value === c.value) return c.label;
}
};
/**
* @param arr conditional array
* @param value if value is not detected, conditional value becomes the default value
* @returns conditional's label || undefined
*/
const getNumericLabelFrom = (arr: condition[], value: number) => {
const numLabelFind = (v, c) => {
const condVal = c.value;
const operatorMap = new Map([
["<", v < condVal],
[">", v > condVal],
["<=", v <= condVal],
[">=", v >= condVal],
["==", v === condVal],
]);
return operatorMap.get(c.operator) ? c.label : undefined;
};
const numConditions = arr.filter((c) => c?.type === "number");
for (let c of numConditions) {
const label = numLabelFind(value, c);
if (typeof label === "string") return label;
}
};
const getLabelFrom = (arr, value) => {
const validVal = Boolean(value);
if (!validVal) return;
for (let c of arr) {
if (value === c.value) return c.label;
}
};
export default function getLabel(value, conditions) {
let _label: any = undefined;
const isBoolean = Boolean(typeof value === "boolean");
const notBoolean = Boolean(typeof value !== "boolean");
const isNullOrUndefined = Boolean(!value && notBoolean);
const isNumeric = Boolean(typeof value === "number");
if (isNullOrUndefined) _label = getFalseyLabelFrom(conditions, value);
else if (isBoolean) _label = getBooleanLabelFrom(conditions, value);
else if (isNumeric) _label = getNumericLabelFrom(conditions, value);
else _label = getLabelFrom(conditions, value);
return _label ?? value;
}

View File

@@ -6,6 +6,7 @@ import UrlIcon from "@mui/icons-material/Link";
import TableCell from "./TableCell";
import TextEditor from "@src/components/Table/editors/TextEditor";
import { filterOperators } from "../ShortText/Filter";
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
@@ -21,6 +22,7 @@ export const config: IFieldConfig = {
initializable: true,
icon: <UrlIcon />,
description: "Web address. Not validated.",
contextMenuActions: BasicContextMenuActions,
TableCell: withBasicCell(TableCell),
TableEditor: TextEditor,
SideDrawerField,

View File

@@ -0,0 +1,74 @@
import _find from "lodash/find";
import Cut from "@mui/icons-material/ContentCut";
import CopyCells from "@src/assets/icons/CopyCells";
import Paste from "@mui/icons-material/ContentPaste";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { useSnackbar } from "notistack";
import { SelectedCell } from "@src/atoms/ContextMenu";
export interface IContextMenuActions {
label: string;
icon: React.ReactNode;
onClick: () => void;
}
export default function BasicContextMenuActions(
selectedCell: SelectedCell,
reset: () => void | Promise<void>
): IContextMenuActions[] {
const { tableState, deleteCell, updateCell } = useProjectContext();
const { enqueueSnackbar } = useSnackbar();
const columns = tableState?.columns;
const rows = tableState?.rows;
const selectedRowIndex = selectedCell.rowIndex as number;
const selectedColIndex = selectedCell?.colIndex;
const selectedCol = _find(columns, { index: selectedColIndex });
const selectedRow = rows?.[selectedRowIndex];
const cell = selectedRow?.[selectedCol.key];
const handleClose = async () => await reset?.();
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(cell));
enqueueSnackbar("Copied to clipboard", { variant: "success" });
} catch (error) {
enqueueSnackbar(`Failed to copy:${error}`, { variant: "error" });
}
handleClose();
};
const handleCut = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(cell));
if (typeof cell !== "undefined")
deleteCell?.(selectedRow?.ref, selectedCol?.key);
} catch (error) {
enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" });
}
handleClose();
};
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
const paste = await JSON.parse(text);
updateCell?.(selectedRow?.ref, selectedCol.key, paste);
} catch (error) {
enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" });
}
handleClose();
};
const contextMenuActions = [
{ label: "Cut", icon: <Cut />, onClick: handleCut },
{ label: "Copy", icon: <CopyCells />, onClick: handleCopy },
{ label: "Paste", icon: <Paste />, onClick: handlePaste },
];
return contextMenuActions;
}

View File

@@ -6,6 +6,7 @@ import {
IPopoverCellProps,
} from "../types";
import _find from "lodash/find";
import { makeStyles, createStyles } from "@mui/styles";
import { Popover, PopoverProps } from "@mui/material";
@@ -50,7 +51,7 @@ export default function withPopoverCell(
return function PopoverCell(props: FormatterProps<any>) {
const classes = useStyles();
const { transparent, ...popoverProps } = options ?? {};
const { updateCell } = useProjectContext();
const { deleteCell, updateCell, tableState } = useProjectContext();
const { validationRegex, required } = (props.column as any).config;
@@ -98,8 +99,18 @@ export default function withPopoverCell(
</ErrorBoundary>
);
//This is where we update the documents
const handleSubmit = (value: any) => {
if (updateCell && !options?.readOnly) {
const targetRow = _find(tableState?.rows, { id: props.row.ref.id });
const targetCell = targetRow?.[props.column.key];
const canDelete = Boolean(
typeof value === "undefined" && targetCell !== value
);
if (deleteCell && !options?.readOnly && canDelete) {
deleteCell(props.row.ref, props.column.key);
setLocalValue(value);
} else if (updateCell && !options?.readOnly) {
updateCell(props.row.ref, props.column.key, value);
setLocalValue(value);
}

View File

@@ -1,8 +1,9 @@
import { FieldType } from "@src/constants/fields";
import { FormatterProps, EditorProps } from "react-data-grid";
import { Control, UseFormReturn } from "react-hook-form";
import { PopoverProps } from "@mui/material";
import { SelectedCell } from "@src/atoms/ContextMenu";
import { IContextMenuActions } from "./_BasicCell/BasicCellContextMenuActions";
export { FieldType };
@@ -17,6 +18,10 @@ export interface IFieldConfig {
icon?: React.ReactNode;
description?: string;
setupGuideLink?: string;
contextMenuActions?: (
selectedCell: SelectedCell,
reset: () => Promise<void>
) => IContextMenuActions[];
TableCell: React.ComponentType<FormatterProps<any>>;
TableEditor: React.ComponentType<EditorProps<any, any>>;
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;

View File

@@ -61,6 +61,10 @@ export interface IProjectContext {
ignoreRequiredFields?: boolean
) => void;
deleteRow: (rowId) => void;
deleteCell: (
rowRef: firebase.firestore.DocumentReference,
fieldValue: string
) => void;
updateCell: (
ref: firebase.firestore.DocumentReference,
fieldName: string,
@@ -290,6 +294,17 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
return;
};
const deleteCell: IProjectContext["deleteCell"] = (rowRef, fieldValue) => {
rowRef
.update({
[fieldValue]: firebase.firestore.FieldValue.delete(),
})
.then(
() => console.log("Field Value deleted"),
(error) => console.error("Failed to delete", error)
);
};
const updateCell: IProjectContext["updateCell"] = (
ref,
fieldName,
@@ -297,9 +312,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
onSuccess
) => {
if (value === undefined) return;
const update = { [fieldName]: value };
if (table?.audit !== false) {
update[table?.auditFieldUpdatedBy || "_updatedBy"] = rowyUser(
currentUser!,
@@ -395,6 +408,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
tableActions,
addRow,
addRows,
deleteCell,
updateCell,
deleteRow,
settingsActions,

View File

@@ -1,17 +1,16 @@
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { matchSorter } from "match-sorter";
export default function useBasicSearch<T>(
list: T[],
predicate: (item: T, query: string) => boolean,
keys: string[],
debounce: number = 400
) {
const [query, setQuery] = useState("");
const [handleQuery] = useDebouncedCallback(setQuery, debounce);
const results = query
? list.filter((user) => predicate(user, query.toLowerCase()))
: list;
const results = query ? matchSorter(list, query, { keys }) : list;
return [results, query, handleQuery] as const;
}

View File

@@ -4,6 +4,7 @@ export type TableActions = {
// TODO: Stricter types here
column: {
add: Function;
insert: Function;
resize: (index: number, width: number) => void;
rename: Function;
remove: Function;
@@ -89,6 +90,7 @@ export default function useTable() {
const actions: TableActions = {
column: {
add: configActions.addColumn,
insert: configActions.insert,
resize: configActions.resize,
rename: configActions.rename,
update: configActions.updateColumn,

View File

@@ -2,11 +2,12 @@ import { useEffect } from "react";
import { useDebouncedCallback } from "use-debounce";
import _camelCase from "lodash/camelCase";
import _orderBy from "lodash/orderBy";
import _sortBy from "lodash/sortBy";
import useDoc, { DocActions } from "../useDoc";
import { FieldType } from "@src/constants/fields";
import { arrayMover, formatPath } from "../../utils/fns";
import { formatPath } from "../../utils/fns";
import { db, deleteField } from "../../firebase";
export type ColumnConfig = {
@@ -85,6 +86,7 @@ const useTableConfig = (tableId?: string) => {
_columnValues.filter((col: any) => !col.hidden && !col.fixed),
"index"
);
const targetColumn: any = columnsArray[index - numberOfFixedColumns];
const updatedColumns = {
...columns,
@@ -116,13 +118,53 @@ const useTableConfig = (tableId?: string) => {
callback: onSuccess,
});
};
/** insert column by index
* @param col properties of new column
* @param source source object { index: selected source index, insert: left | right }
*/
const insert = (col, source) => {
const { columns } = tableConfigState;
const orderedCol = _orderBy(Object.values(columns), "index");
//offset index is necessary for splice insert
const offset = source.insert === "left" ? 0 : 1;
//insert poistion, is source index + offset
//if source.index is undefined, set target index to end of row
const targetIndx = Boolean(typeof source.index === "undefined")
? orderedCol.length
: source.index + offset;
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
orderedCol.splice(targetIndx, 0, col);
const updatedColumns = orderedCol.reduce(
(acc: any, col: any, indx: number) => {
acc[col.key] = { ...col, index: indx };
return acc;
},
{}
);
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
});
};
/** remove column by index
* @param index of column.
*/
const remove = (key: string) => {
const { columns } = tableConfigState;
let updatedColumns = columns;
updatedColumns[key] = deleteField();
let updatedColumns: any = Object.values(columns)
.filter((c: any) => c.key !== key)
.sort((c: any) => c.index)
.reduce((acc: any, curr: any, index: any) => {
acc[curr.key] = { ...curr, index };
return acc;
}, {});
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
@@ -136,15 +178,21 @@ const useTableConfig = (tableId?: string) => {
const { columns } = tableConfigState;
const oldIndex = columns[draggedColumnKey].index;
const newIndex = columns[droppedColumnKey].index;
const columnsArray = _sortBy(Object.values(columns), "index");
arrayMover(columnsArray, oldIndex, newIndex);
let updatedColumns = columns;
columnsArray
.filter((c) => c) // arrayMover has a bug creating undefined items
.forEach((column: any, index) => {
updatedColumns[column.key] = { ...column, index };
});
//sort columns by index, remove drag col, insert it in drop position
const sortedColumns = _sortBy(Object.values(columns), "index");
const removeCol = sortedColumns.splice(oldIndex, 1);
sortedColumns.splice(newIndex, 0, removeCol[0]);
//itereate and update index to proper value
const updatedColumns = sortedColumns.reduce(
(acc: any, curr: any, index) => {
acc[curr.key] = { ...curr, index };
return acc;
},
{}
);
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
@@ -162,6 +210,7 @@ const useTableConfig = (tableId?: string) => {
});
};
const actions = {
insert,
updateColumn,
updateConfig,
addColumn,

View File

@@ -46,6 +46,7 @@ import { SETTINGS } from "@src/config/dbPaths";
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
const useHomeViewState = createPersistedState("__ROWY__HOME_VIEW");
const SEARCH_KEYS = ["id", "name", "section", "description"];
export default function HomePage() {
const { userDoc, userRoles } = useAppContext();
@@ -53,11 +54,7 @@ export default function HomePage() {
const [results, query, handleQuery] = useBasicSearch(
tables ?? [],
(table, query) =>
table.id.toLowerCase().includes(query) ||
table.name.toLowerCase().includes(query) ||
table.section.toLowerCase().includes(query) ||
table.description.toLowerCase().includes(query)
SEARCH_KEYS
);
const [view, setView] = useHomeViewState("grid");

View File

@@ -20,6 +20,8 @@ import useCollection from "@src/hooks/useCollection";
import useBasicSearch from "@src/hooks/useBasicSearch";
import { USERS } from "@src/config/dbPaths";
const SEARCH_KEYS = ["id", "user.displayName", "user.email"];
export interface User {
id: string;
user: {
@@ -35,13 +37,7 @@ export default function UserManagementPage() {
const users: User[] = usersState.documents ?? [];
const loading = usersState.loading || !Array.isArray(usersState.documents);
const [results, query, handleQuery] = useBasicSearch(
users,
(user, query) =>
user.id.toLowerCase() === query ||
user.user.displayName.toLowerCase().includes(query) ||
user.user.email.toLowerCase().includes(query)
);
const [results, query, handleQuery] = useBasicSearch(users, SEARCH_KEYS);
return (
<Container maxWidth="sm" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>

View File

@@ -5,33 +5,6 @@ import _isPlainObject from "lodash/isPlainObject";
import { TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "@src/config/dbPaths";
/**
* reposition an element in an array
* @param arr array
* @param old_index index of element to be moved
* @param new_index new position of the moved element
*/
export const arrayMover = (
arr: any[],
old_index: number,
new_index: number
) => {
while (old_index < 0) {
old_index += arr.length;
}
while (new_index < 0) {
new_index += arr.length;
}
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) {
arr.push(undefined);
}
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr; // for testing purposes
};
export const missingFieldsReducer =
(data: any) => (acc: string[], curr: string) => {
if (data[curr] === undefined) {

4
test.js Normal file
View File

@@ -0,0 +1,4 @@
const values = [true, false, undefined, null, "string"];
const randIndx = Math.floor(Math.random() * 6);
values.push(Math.random() * 100);
console.log(values, values[randIndx], randIndx);

161
yarn.lock
View File

@@ -1481,6 +1481,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.0.tgz#b8d142fc0f7664fb3d9b5833fd40dcbab89276c0"
integrity sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.13.10", "@babel/runtime@^7.8.4":
version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
@@ -6285,38 +6292,24 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
debug@4.3.1, debug@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
debug@^3.0.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
debug@^4.0.0:
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
debug@^4.0.1, debug@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
debug@4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
@@ -6354,11 +6347,6 @@ dedent@^0.7.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
deep-diff@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26"
integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==
deep-equal@^1.0.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
@@ -7467,11 +7455,6 @@ exegesis@^4.1.0:
raw-body "^2.3.3"
semver "^7.0.0"
exenv@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exit-code@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/exit-code/-/exit-code-1.0.2.tgz#ce165811c9f117af6a5f882940b96ae7f9aecc34"
@@ -8028,11 +8011,9 @@ fn.name@1.x.x:
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.0.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb"
integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==
dependencies:
debug "^3.0.0"
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
for-in@^1.0.2:
version "1.0.2"
@@ -9463,11 +9444,6 @@ is-lambda@^1.0.1:
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=
is-lite@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/is-lite/-/is-lite-0.8.1.tgz#a9bd03c90ea723d450c78c991b84f78e7e3126f9"
integrity sha512-ekSwuewzOmwFnzzAOWuA5fRFPqOeTrLIL3GWT7hdVVi+oLuD+Rau8gCmkb94vH5hjXc1Q/CfIW/y/td1RrNQIg==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@@ -11134,6 +11110,14 @@ marked@^0.7.0:
resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e"
integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==
match-sorter@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
dependencies:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
material-colors@^1.2.1:
version "1.2.6"
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
@@ -12009,9 +11993,9 @@ nanoclone@^0.2.1:
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.1.23:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
nanomatch@^1.2.9:
version "1.2.13"
@@ -12067,16 +12051,6 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
nested-property@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-1.0.1.tgz#2001105b5c69413411b876bba9b86f4316af613f"
integrity sha512-BnBBoo/8bBNRdAnJc7+m79oWk7dXwW1+vCesaEQhfDGVwXGLMvmI4NwYgLTW94R/x+R2s/yr2g/hB/4w/YSAvA==
nested-property@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==
netmask@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
@@ -13030,11 +13004,6 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
popper.js@^1.16.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
portfinder@^1.0.23, portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@@ -14365,18 +14334,6 @@ react-firebaseui@^5.0.2:
dependencies:
firebaseui "^4.8.0"
react-floater@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.7.3.tgz#f57947960682586866ec21540e73c9049ca9f787"
integrity sha512-d1wAEph+xRxQ0RJ3woMmYLlZHTaCIsja7Bv6JNo2ezsVUgdMan4CxOR4Do4/xgpmRFfsQMdlygexLAZZypWirw==
dependencies:
deepmerge "^4.2.2"
exenv "^1.2.2"
is-lite "^0.8.1"
popper.js "^1.16.0"
react-proptype-conditional-require "^1.0.4"
tree-changes "^0.5.1"
react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
@@ -14407,22 +14364,6 @@ react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-joyride@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.3.1.tgz#de3c8e8bfd6b58f62e7c6e8b38467e807ef8f90f"
integrity sha512-MmyhECU3V+4kZAJrcDPPXcXxaoTpwc7g+E7Cq6QZ5IqJZrWYSVvpVCfudQcdcf6BsNbgawRhvCvbQyeWoPtNig==
dependencies:
deep-diff "^1.0.2"
deepmerge "^4.2.2"
exenv "^1.2.2"
is-lite "^0.8.1"
nested-property "^4.0.0"
react-floater "^0.7.3"
react-is "^16.13.1"
scroll "^3.0.1"
scrollparent "^2.0.1"
tree-changes "^0.7.1"
react-json-view@^1.19.1:
version "1.21.3"
resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.21.3.tgz#f184209ee8f1bf374fb0c41b0813cff54549c475"
@@ -14458,11 +14399,6 @@ react-markdown@^8.0.0:
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-proptype-conditional-require@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz#69c2d5741e6df5e08f230f36bbc2944ee1222555"
integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU=
react-redux@^7.2.0:
version "7.2.4"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
@@ -14918,6 +14854,11 @@ remark-rehype@^10.0.0:
mdast-util-to-hast "^12.1.0"
unified "^10.0.0"
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -15355,16 +15296,6 @@ schema-utils@^3.0.0:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
scroll@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/scroll/-/scroll-3.0.1.tgz#d5afb59fb3592ee3df31c89743e78b39e4cd8a26"
integrity sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==
scrollparent@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/scrollparent/-/scrollparent-2.0.1.tgz#715d5b9cc57760fb22bdccc3befb5bfe06b1a317"
integrity sha1-cV1bnMV3YPsivczDvvtb/gaxoxc=
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -16698,22 +16629,6 @@ tr46@^2.1.0:
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=
tree-changes@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.5.1.tgz#e31cc8a0f56c8c401f0a88243d9165dbea4f570c"
integrity sha512-O873xzV2xRZ6N059Mn06QzmGKEE21LlvIPbsk2G+GS9ZX5OCur6PIwuuh0rWpAPvLWQZPj0XObyG27zZyLHUzw==
dependencies:
deep-diff "^1.0.2"
nested-property "1.0.1"
tree-changes@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.7.1.tgz#fa8810cbe417e80b9a42c4b018f934c7ad8fa156"
integrity sha512-sPIt8PKDi0OQTglr7lsetcB9DU19Ls/ZgFSjFvK6DWJGisAn4sOxtjpmQfuqjexQE4UU9U53LNmataL1kRJ3Uw==
dependencies:
fast-deep-equal "^3.1.3"
is-lite "^0.8.1"
triple-beam@^1.2.0, triple-beam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"