diff --git a/README.md b/README.md
index f2d5077c2..afd3e770c 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,7 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
Set up Rowy on your Google Cloud Platform project with this easy deploy button.
Your data and cloud functions stay on your own Firestore/GCP and is managed via
-a cloud run instance that operates exclusively on your GCP project. So we do do
+a cloud run instance that operates exclusively on your GCP project. So we do
not access or store any of your data on Rowy.
[](https://rowy.app/)
diff --git a/package.json b/package.json
index b2f8c5320..573fc881d 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"file-saver": "^2.0.5",
"firebase": "^9.12.1",
"firebaseui": "^6.0.1",
+ "fuse.js": "^7.0.0",
"jotai": "^1.8.4",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.0",
diff --git a/src/atoms/tableScope/rowActions.ts b/src/atoms/tableScope/rowActions.ts
index af5caddf8..3baeaf0d1 100644
--- a/src/atoms/tableScope/rowActions.ts
+++ b/src/atoms/tableScope/rowActions.ts
@@ -386,9 +386,7 @@ export const updateFieldAtom = atom(
);
if (!row) throw new Error("Could not find row");
- const isLocalRow =
- fieldName.startsWith("_rowy_formulaValue_") ||
- Boolean(find(tableRowsLocal, ["_rowy_ref.path", path]));
+ const isLocalRow = Boolean(find(tableRowsLocal, ["_rowy_ref.path", path]));
const update: Partial = {};
@@ -469,14 +467,6 @@ export const updateFieldAtom = atom(
deleteFields: deleteField ? [fieldName] : [],
});
- // TODO(han): Formula field persistence
- // const config = find(tableColumnsOrdered, (c) => {
- // const [, key] = fieldName.split("_rowy_formulaValue_");
- // return c.key === key;
- // });
- // if(!config.persist) return;
- if (fieldName.startsWith("_rowy_formulaValue")) return;
-
// If it has no missingRequiredFields, also write to db
// And write entire row to handle the case where it doesn’t exist in db yet
if (missingRequiredFields.length === 0) {
diff --git a/src/components/ColumnModals/FieldsDropdown.tsx b/src/components/ColumnModals/FieldsDropdown.tsx
index 18c9425e5..48e1be0c0 100644
--- a/src/components/ColumnModals/FieldsDropdown.tsx
+++ b/src/components/ColumnModals/FieldsDropdown.tsx
@@ -1,5 +1,6 @@
import MultiSelect from "@rowy/multiselect";
import { Box, ListItemIcon, Typography } from "@mui/material";
+import Fuse from 'fuse.js';
import { FIELDS } from "@src/components/fields";
import { FieldType } from "@src/constants/fields";
@@ -23,6 +24,15 @@ export interface IFieldsDropdownProps {
[key: string]: any;
}
+export interface OptionsType {
+ label: string;
+ value: string;
+ disabled: boolean;
+ requireCloudFunctionSetup: boolean;
+ requireCollectionTable: boolean;
+ keywords: string[];
+}
+
/**
* Returns dropdown component of all available types
*/
@@ -52,9 +62,21 @@ export default function FieldsDropdown({
disabled: requireCloudFunctionSetup || requireCollectionTable,
requireCloudFunctionSetup,
requireCollectionTable,
+ keywords: fieldConfig.keywords || []
};
});
+ const filterOptions = (options: OptionsType[], inputConfig: any) => {
+ const fuse = new Fuse(options, {
+ keys: [{name:'label', weight: 2}, 'keywords'],
+ includeScore: true,
+ threshold: 0.4,
+ });
+
+ const results = fuse.search(inputConfig?.inputValue);
+ return results.length > 0 ? results.map((result) => result.item) : options;
+ }
+
return (
(
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index ba17786ba..780b0a532 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -121,6 +121,7 @@ export default function Table({
const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope);
const setReactTable = useSetAtom(reactTableAtom, tableScope);
+ const setSelectedCell = useSetAtom(selectedCellAtom, tableScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
// Get user settings and tableId for applying sort sorting
@@ -313,6 +314,8 @@ export default function Table({
const { scrollHeight, scrollTop, clientHeight } = containerElement;
if (scrollHeight - scrollTop - clientHeight < 300) {
+ // deselect cell on next page load
+ setSelectedCell(null);
setTablePage((p) => p + 1);
}
},
diff --git a/src/components/Table/useMenuAction.tsx b/src/components/Table/useMenuAction.tsx
index 4cc202779..e7fffc7c2 100644
--- a/src/components/Table/useMenuAction.tsx
+++ b/src/components/Table/useMenuAction.tsx
@@ -1,29 +1,28 @@
-import { useCallback, useState, useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useSnackbar } from "notistack";
-import { get, find } from "lodash-es";
+import { find, get, isDate, isFunction } from "lodash-es";
import {
- tableScope,
- tableSchemaAtom,
+ SelectedCell,
tableRowsAtom,
+ tableSchemaAtom,
+ tableScope,
updateFieldAtom,
- SelectedCell,
} from "@src/atoms/tableScope";
import { getFieldProp, getFieldType } from "@src/components/fields";
import { ColumnConfig } from "@src/types/table";
import { FieldType } from "@src/constants/fields";
-import { format } from "date-fns";
+import { format, parse, isValid } from "date-fns";
import { DATE_FORMAT, DATE_TIME_FORMAT } from "@src/constants/dates";
-import { isDate, isFunction } from "lodash-es";
import { getDurationString } from "@src/components/fields/Duration/utils";
import { doc } from "firebase/firestore";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
import { projectScope } from "@src/atoms/projectScope";
-export const SUPPORTED_TYPES_COPY = new Set([
+export const SUPPORTED_TYPES_COPY = new Set([
// TEXT
FieldType.shortText,
FieldType.longText,
@@ -54,17 +53,24 @@ export const SUPPORTED_TYPES_COPY = new Set([
FieldType.code,
FieldType.markdown,
FieldType.array,
+ // CLOUD FUNCTION
+ FieldType.action,
+ FieldType.derivative,
+ FieldType.status,
// AUDIT
FieldType.createdBy,
FieldType.updatedBy,
FieldType.createdAt,
FieldType.updatedAt,
// CONNECTION
+ FieldType.arraySubTable,
FieldType.reference,
+ // METADATA
+ FieldType.user,
FieldType.id,
]);
-export const SUPPORTED_TYPES_PASTE = new Set([
+export const SUPPORTED_TYPES_PASTE = new Set([
// TEXT
FieldType.shortText,
FieldType.longText,
@@ -72,17 +78,34 @@ export const SUPPORTED_TYPES_PASTE = new Set([
FieldType.email,
FieldType.phone,
FieldType.url,
+ // SELECT
+ FieldType.singleSelect,
+ FieldType.multiSelect,
// NUMERIC
+ FieldType.checkbox,
FieldType.number,
FieldType.percentage,
FieldType.rating,
FieldType.slider,
+ FieldType.color,
+ FieldType.geoPoint,
+ // DATE & TIME
+ FieldType.date,
+ FieldType.dateTime,
+ FieldType.duration,
+ // FILE
+ FieldType.image,
+ FieldType.file,
// CODE
FieldType.json,
FieldType.code,
FieldType.markdown,
+ FieldType.array,
// CONNECTION
+ FieldType.arraySubTable,
FieldType.reference,
+ // METADATA
+ FieldType.user,
]);
export function useMenuAction(
@@ -163,93 +186,177 @@ export function useMenuAction(
const handlePaste = useCallback(
async (e?: ClipboardEvent) => {
- try {
- if (!selectedCell || !selectedCol) return;
+ if (!selectedCell || !selectedCol) return;
- // checks which element has focus, if it is not the gridcell it won't paste the copied content inside the gridcell
- if (document.activeElement?.role !== "gridcell") return;
+ // if the focus element is not gridcell or menuitem (click on paste menu action)
+ // it won't paste the copied content inside the gridcell
+ if (
+ !["gridcell", "menuitem"].includes(document.activeElement?.role ?? "")
+ )
+ return;
+
+ // prevent from pasting inside array subtable overwrites the whole object
+ if (
+ document.activeElement
+ ?.getAttribute?.("data-row-id")
+ ?.startsWith("subtable-array") &&
+ selectedCell.columnKey !==
+ document.activeElement?.getAttribute?.("data-col-id")
+ ) {
+ return;
+ }
- let text: string;
+ let clipboardText: string;
+ if (navigator.userAgent.includes("Firefox")) {
// Firefox doesn't allow for reading clipboard data, hence the workaround
- if (navigator.userAgent.includes("Firefox")) {
- if (!e || !e.clipboardData) {
- enqueueSnackbar(
- `If you're on Firefox, please use the hotkey instead (Ctrl + V / Cmd + V).`,
- {
- variant: "info",
- autoHideDuration: 7000,
- }
- );
- enqueueSnackbar(`Cannot read clipboard data.`, {
- variant: "error",
- });
- return;
- }
- text = e.clipboardData.getData("text/plain") || "";
- } else {
- try {
- text = await navigator.clipboard.readText();
- } catch (e) {
- enqueueSnackbar(`Read clipboard permission denied.`, {
- variant: "error",
- });
- return;
- }
+ if (!e || !e.clipboardData) {
+ enqueueSnackbar(
+ `If you're on Firefox, please use the hotkey instead (Ctrl + V / Cmd + V).`,
+ {
+ variant: "info",
+ autoHideDuration: 7000,
+ }
+ );
+ enqueueSnackbar(`Cannot read clipboard data.`, {
+ variant: "error",
+ });
+ return;
+ }
+ clipboardText = e.clipboardData.getData("text/plain") || "";
+ } else {
+ try {
+ clipboardText = await navigator.clipboard.readText();
+ } catch (e) {
+ enqueueSnackbar(`Read clipboard permission denied.`, {
+ variant: "error",
+ });
+ return;
}
+ }
+
+ try {
+ let parsedValue;
const cellDataType = getFieldProp(
"dataType",
getFieldType(selectedCol)
);
- let parsed;
- switch (cellDataType) {
- case "number":
- parsed = Number(text);
- if (isNaN(parsed)) throw new Error(`${text} is not a number`);
+
+ // parse value first by type if matches, then by column type
+ switch (selectedCol.type) {
+ case FieldType.percentage:
+ clipboardText = clipboardText.trim();
+ if (clipboardText.endsWith("%")) {
+ clipboardText = clipboardText.slice(0, -1);
+ parsedValue = Number(clipboardText) / 100;
+ } else {
+ parsedValue = Number(clipboardText);
+ }
+ if (isNaN(parsedValue))
+ throw new Error(`${clipboardText} is not a percentage`);
+ break;
+ case FieldType.date:
+ parsedValue = parse(
+ clipboardText,
+ selectedCol.config?.format || DATE_FORMAT,
+ new Date()
+ );
+ if (!isValid(parsedValue)) {
+ parsedValue = parse(clipboardText, DATE_FORMAT, new Date());
+ }
+ if (!isValid(parsedValue)) {
+ parsedValue = new Date(clipboardText);
+ }
+ if (!isValid(parsedValue)) {
+ throw new Error(`${clipboardText} is not a date`);
+ }
+ break;
+ case FieldType.dateTime:
+ parsedValue = parse(
+ clipboardText,
+ selectedCol.config?.format || DATE_TIME_FORMAT,
+ new Date()
+ );
+ if (!isValid(parsedValue)) {
+ parsedValue = parse(clipboardText, DATE_TIME_FORMAT, new Date());
+ }
+ if (!isValid(parsedValue)) {
+ parsedValue = new Date(clipboardText);
+ }
+ if (!isValid(parsedValue)) {
+ throw new Error(`${clipboardText} is not a date`);
+ }
break;
- case "string":
- parsed = text;
+ case FieldType.duration:
+ try {
+ const json = JSON.parse(clipboardText);
+ parsedValue = {
+ start: new Date(json.start),
+ end: new Date(json.end),
+ };
+ } catch (e: any) {
+ throw new Error(
+ `${clipboardText} does not have valida start and end dates`
+ );
+ }
break;
- case "reference":
+ case FieldType.arraySubTable:
try {
- parsed = doc(firebaseDb, text);
+ parsedValue = JSON.parse(clipboardText);
} catch (e: any) {
- enqueueSnackbar(`Invalid reference.`, { variant: "error" });
+ throw new Error(`${clipboardText} is not valid array subtable`);
+ }
+ if (!Array.isArray(parsedValue)) {
+ throw new Error(`${clipboardText} is not an array`);
}
break;
default:
- parsed = JSON.parse(text);
- break;
+ switch (cellDataType) {
+ case "number":
+ parsedValue = Number(clipboardText);
+ if (isNaN(parsedValue))
+ throw new Error(`${clipboardText} is not a number`);
+ break;
+ case "string":
+ parsedValue = clipboardText;
+ break;
+ case "reference":
+ try {
+ parsedValue = doc(firebaseDb, clipboardText);
+ } catch (e: any) {
+ enqueueSnackbar(`Invalid reference.`, { variant: "error" });
+ }
+ break;
+ default:
+ parsedValue = JSON.parse(clipboardText);
+ break;
+ }
}
+ // post process parsed values
if (selectedCol.type === FieldType.slider) {
- if (parsed < selectedCol.config?.min)
- parsed = selectedCol.config?.min;
- else if (parsed > selectedCol.config?.max)
- parsed = selectedCol.config?.max;
+ if (parsedValue < selectedCol.config?.min)
+ parsedValue = selectedCol.config?.min;
+ else if (parsedValue > (selectedCol.config?.max || 10))
+ parsedValue = selectedCol.config?.max || 10;
}
-
if (selectedCol.type === FieldType.rating) {
- if (parsed < 0) parsed = 0;
- if (parsed > (selectedCol.config?.max || 5))
- parsed = selectedCol.config?.max || 5;
+ if (parsedValue < 0) parsedValue = 0;
+ if (parsedValue > (selectedCol.config?.max || 5))
+ parsedValue = selectedCol.config?.max || 5;
}
- if (selectedCol.type === FieldType.percentage) {
- parsed = parsed / 100;
- }
updateField({
path: selectedCell.path,
fieldName: selectedCol.fieldName,
- value: parsed,
+ value: parsedValue,
arrayTableData: {
index: selectedCell.arrayIndex,
},
});
} catch (error) {
- enqueueSnackbar(
- `${selectedCol?.type} field does not support the data type being pasted`,
- { variant: "error" }
- );
+ enqueueSnackbar(`Paste error on ${selectedCol?.type}: ${error}`, {
+ variant: "error",
+ });
}
if (handleClose) handleClose();
},
@@ -286,7 +393,7 @@ export function useMenuAction(
if (SUPPORTED_TYPES_COPY.has(fieldType)) {
return func();
} else {
- enqueueSnackbar(`${fieldType} field cannot be copied`, {
+ enqueueSnackbar(`${fieldType} cannot be copied`, {
variant: "error",
});
}
@@ -309,12 +416,9 @@ export function useMenuAction(
if (SUPPORTED_TYPES_PASTE.has(fieldType)) {
return func(e);
} else {
- enqueueSnackbar(
- `${fieldType} field does not support paste functionality`,
- {
- variant: "error",
- }
- );
+ enqueueSnackbar(`${fieldType} does not support paste`, {
+ variant: "error",
+ });
}
};
},
@@ -324,11 +428,17 @@ export function useMenuAction(
const getValue = useCallback(
(cellValue: any) => {
switch (selectedCol?.type) {
- case FieldType.percentage:
- return cellValue * 100;
+ case FieldType.multiSelect:
case FieldType.json:
case FieldType.color:
case FieldType.geoPoint:
+ case FieldType.image:
+ case FieldType.file:
+ case FieldType.array:
+ case FieldType.arraySubTable:
+ case FieldType.createdBy:
+ case FieldType.updatedBy:
+ case FieldType.user:
return JSON.stringify(cellValue);
case FieldType.date:
if (
@@ -362,19 +472,23 @@ export function useMenuAction(
}
}
return;
+ case FieldType.percentage:
+ return `${cellValue * 100}%`;
case FieldType.duration:
- return getDurationString(
- cellValue.start.toDate(),
- cellValue.end.toDate()
- );
- case FieldType.image:
- case FieldType.file:
- return cellValue[0].downloadURL;
- case FieldType.createdBy:
- case FieldType.updatedBy:
- return cellValue.displayName;
+ return JSON.stringify({
+ duration: getDurationString(
+ cellValue.start.toDate(),
+ cellValue.end.toDate()
+ ),
+ start: cellValue.start.toDate(),
+ end: cellValue.end.toDate(),
+ });
+ case FieldType.action:
+ return cellValue.status || "";
case FieldType.reference:
return cellValue.path;
+ case FieldType.formula:
+ return cellValue.formula || "";
default:
return cellValue;
}
diff --git a/src/components/fields/Array/index.tsx b/src/components/fields/Array/index.tsx
index 0c750f215..f6e859eac 100644
--- a/src/components/fields/Array/index.tsx
+++ b/src/components/fields/Array/index.tsx
@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
filter: { operators, defaultValue: [] },
requireConfiguration: false,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["list"]
};
export default config;
diff --git a/src/components/fields/Checkbox/index.tsx b/src/components/fields/Checkbox/index.tsx
index 4cbfc988e..9fcec0daa 100644
--- a/src/components/fields/Checkbox/index.tsx
+++ b/src/components/fields/Checkbox/index.tsx
@@ -45,5 +45,6 @@ export const config: IFieldConfig = {
},
SideDrawerField,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["boolean", "switch", "true", "false", "on", "off"]
};
export default config;
diff --git a/src/components/fields/Code/index.tsx b/src/components/fields/Code/index.tsx
index b075ff64b..873dbc0b9 100644
--- a/src/components/fields/Code/index.tsx
+++ b/src/components/fields/Code/index.tsx
@@ -34,5 +34,6 @@ export const config: IFieldConfig = {
SideDrawerField,
settings: Settings,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["snippet", "block"]
};
export default config;
diff --git a/src/components/fields/Color/index.tsx b/src/components/fields/Color/index.tsx
index c5c95fa10..4659408f4 100644
--- a/src/components/fields/Color/index.tsx
+++ b/src/components/fields/Color/index.tsx
@@ -44,5 +44,6 @@ export const config: IFieldConfig = {
}
},
contextMenuActions: BasicContextMenuActions,
+ keywords: ["hexcode"]
};
export default config;
diff --git a/src/components/fields/CreatedAt/index.tsx b/src/components/fields/CreatedAt/index.tsx
index 836206b1c..8de99a6cc 100644
--- a/src/components/fields/CreatedAt/index.tsx
+++ b/src/components/fields/CreatedAt/index.tsx
@@ -30,5 +30,6 @@ export const config: IFieldConfig = {
settings: Settings,
requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["date", "time"]
};
export default config;
diff --git a/src/components/fields/CreatedBy/index.tsx b/src/components/fields/CreatedBy/index.tsx
index 39fbb8fbf..bc4166ddc 100644
--- a/src/components/fields/CreatedBy/index.tsx
+++ b/src/components/fields/CreatedBy/index.tsx
@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
settings: Settings,
requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["date", "time"]
};
export default config;
diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts
index 3cf762f7c..c31281d1e 100644
--- a/src/components/fields/Formula/TableSourcePreview.ts
+++ b/src/components/fields/Formula/TableSourcePreview.ts
@@ -6,9 +6,7 @@ import {
_deleteRowDbAtom,
_updateRowDbAtom,
tableNextPageAtom,
- tableRowsAtom,
tableRowsDbAtom,
- tableRowsLocalAtom,
tableScope,
tableSettingsAtom,
} from "@src/atoms/tableScope";
diff --git a/src/components/fields/Formula/index.tsx b/src/components/fields/Formula/index.tsx
index 6bfe3a799..0f78401ab 100644
--- a/src/components/fields/Formula/index.tsx
+++ b/src/components/fields/Formula/index.tsx
@@ -27,5 +27,6 @@ export const config: IFieldConfig = {
settings: Settings,
settingsValidator: settingsValidator,
requireConfiguration: true,
+ keywords: ["equation"]
};
export default config;
diff --git a/src/components/fields/GeoPoint/index.tsx b/src/components/fields/GeoPoint/index.tsx
index 8a793e598..5685fedd0 100644
--- a/src/components/fields/GeoPoint/index.tsx
+++ b/src/components/fields/GeoPoint/index.tsx
@@ -26,5 +26,6 @@ export const config: IFieldConfig = {
}),
SideDrawerField,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["location", "latitude", "longitude", "point"]
};
export default config;
diff --git a/src/components/fields/Id/index.tsx b/src/components/fields/Id/index.tsx
index 0215e7fe7..1ee67caf9 100644
--- a/src/components/fields/Id/index.tsx
+++ b/src/components/fields/Id/index.tsx
@@ -19,5 +19,6 @@ export const config: IFieldConfig = {
description: "Displays the row’s ID. Read-only. Cannot be sorted.",
TableCell: withRenderTableCell(DisplayCell, null),
SideDrawerField,
+ keywords: ["unique"]
};
export default config;
diff --git a/src/components/fields/Image/index.tsx b/src/components/fields/Image/index.tsx
index 71d5a32dc..24c27c27d 100644
--- a/src/components/fields/Image/index.tsx
+++ b/src/components/fields/Image/index.tsx
@@ -28,6 +28,7 @@ export const config: IFieldConfig = {
}),
SideDrawerField,
contextMenuActions: ContextMenuActions,
+ keywords: ["picture"]
};
export default config;
diff --git a/src/components/fields/LongText/index.tsx b/src/components/fields/LongText/index.tsx
index 6b926126a..34a647c7c 100644
--- a/src/components/fields/LongText/index.tsx
+++ b/src/components/fields/LongText/index.tsx
@@ -35,5 +35,6 @@ export const config: IFieldConfig = {
filter: {
operators: filterOperators,
},
+ keywords: ["string"]
};
export default config;
diff --git a/src/components/fields/Markdown/index.tsx b/src/components/fields/Markdown/index.tsx
index 243bfb35e..81ee9c79c 100644
--- a/src/components/fields/Markdown/index.tsx
+++ b/src/components/fields/Markdown/index.tsx
@@ -25,5 +25,6 @@ export const config: IFieldConfig = {
TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"),
SideDrawerField,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["md"]
};
export default config;
diff --git a/src/components/fields/MultiSelect/index.tsx b/src/components/fields/MultiSelect/index.tsx
index e93e0756c..cdd840de4 100644
--- a/src/components/fields/MultiSelect/index.tsx
+++ b/src/components/fields/MultiSelect/index.tsx
@@ -50,5 +50,6 @@ export const config: IFieldConfig = {
operators: filterOperators,
},
contextMenuActions: BasicContextMenuActions,
+ keywords: ["options"]
};
export default config;
diff --git a/src/components/fields/Number/index.tsx b/src/components/fields/Number/index.tsx
index 2f04cadd3..601d2dc06 100644
--- a/src/components/fields/Number/index.tsx
+++ b/src/components/fields/Number/index.tsx
@@ -35,5 +35,6 @@ export const config: IFieldConfig = {
return null;
}
},
+ keywords: ["digit"]
};
export default config;
diff --git a/src/components/fields/Phone/index.tsx b/src/components/fields/Phone/index.tsx
index 0d72c31a5..bdd64be59 100644
--- a/src/components/fields/Phone/index.tsx
+++ b/src/components/fields/Phone/index.tsx
@@ -28,5 +28,6 @@ export const config: IFieldConfig = {
filter: {
operators: filterOperators,
},
+ keywords: ["number", "contact"]
};
export default config;
diff --git a/src/components/fields/Rating/index.tsx b/src/components/fields/Rating/index.tsx
index b1f6ced9a..14bcdde01 100644
--- a/src/components/fields/Rating/index.tsx
+++ b/src/components/fields/Rating/index.tsx
@@ -46,5 +46,6 @@ export const config: IFieldConfig = {
}
},
contextMenuActions: BasicContextMenuActions,
+ keywords: ["star"]
};
export default config;
diff --git a/src/components/fields/RichText/index.tsx b/src/components/fields/RichText/index.tsx
index c9c1ddbe5..830c9d5e5 100644
--- a/src/components/fields/RichText/index.tsx
+++ b/src/components/fields/RichText/index.tsx
@@ -25,5 +25,6 @@ export const config: IFieldConfig = {
contextMenuActions: BasicContextMenuActions,
TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"),
SideDrawerField,
+ keywords: ["string"]
};
export default config;
diff --git a/src/components/fields/ShortText/index.tsx b/src/components/fields/ShortText/index.tsx
index 1288f9bb9..e35fa7e52 100644
--- a/src/components/fields/ShortText/index.tsx
+++ b/src/components/fields/ShortText/index.tsx
@@ -36,5 +36,6 @@ export const config: IFieldConfig = {
filter: {
operators: filterOperators,
},
+ keywords: ["string"]
};
export default config;
diff --git a/src/components/fields/SingleSelect/index.tsx b/src/components/fields/SingleSelect/index.tsx
index 21d961c27..8cdee992d 100644
--- a/src/components/fields/SingleSelect/index.tsx
+++ b/src/components/fields/SingleSelect/index.tsx
@@ -37,5 +37,6 @@ export const config: IFieldConfig = {
filter: { operators: filterOperators },
requireConfiguration: true,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["options"]
};
export default config;
diff --git a/src/components/fields/UpdatedAt/index.tsx b/src/components/fields/UpdatedAt/index.tsx
index 10a16d739..b907635db 100644
--- a/src/components/fields/UpdatedAt/index.tsx
+++ b/src/components/fields/UpdatedAt/index.tsx
@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
settings: Settings,
requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["date", "time"]
};
export default config;
diff --git a/src/components/fields/UpdatedBy/index.tsx b/src/components/fields/UpdatedBy/index.tsx
index 5aacfaa82..abfe894d3 100644
--- a/src/components/fields/UpdatedBy/index.tsx
+++ b/src/components/fields/UpdatedBy/index.tsx
@@ -33,5 +33,6 @@ export const config: IFieldConfig = {
settings: Settings,
requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions,
+ keywords: ["date", "time"]
};
export default config;
diff --git a/src/components/fields/Url/index.tsx b/src/components/fields/Url/index.tsx
index be2763882..f1c6e3a9f 100644
--- a/src/components/fields/Url/index.tsx
+++ b/src/components/fields/Url/index.tsx
@@ -30,5 +30,6 @@ export const config: IFieldConfig = {
filter: {
operators: filterOperators,
},
+ keywords: ["link", "path"]
};
export default config;
diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx
index f7d42921f..6329f5a68 100644
--- a/src/components/fields/User/index.tsx
+++ b/src/components/fields/User/index.tsx
@@ -29,5 +29,6 @@ export const config: IFieldConfig = {
}),
SideDrawerField,
settings: Settings,
+ keywords: ["entity"]
};
export default config;
diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts
index f8e935722..04799893f 100644
--- a/src/components/fields/types.ts
+++ b/src/components/fields/types.ts
@@ -42,6 +42,7 @@ export interface IFieldConfig {
sortKey?: string;
csvExportFormatter?: (value: any, config?: any) => string;
csvImportParser?: (value: string, config?: any) => any;
+ keywords?: string[];
}
/** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */
diff --git a/src/hooks/useFirestoreCollectionWithAtom.ts b/src/hooks/useFirestoreCollectionWithAtom.ts
index 901d2970a..bfdea9e2f 100644
--- a/src/hooks/useFirestoreCollectionWithAtom.ts
+++ b/src/hooks/useFirestoreCollectionWithAtom.ts
@@ -26,6 +26,7 @@ import {
DocumentData,
or,
QueryFieldFilterConstraint,
+ Timestamp,
} from "firebase/firestore";
import { useErrorHandler } from "react-error-boundary";
@@ -402,6 +403,26 @@ const getQuery = (
}
};
+/**
+ * Parse datetime to Date object
+ * \{ nanoseconds: number; seconds: number \} is a Timestamp object without toDate() method, we need to calculate it manually
+ * */
+const parseDateFilterValue = (
+ date: Date | Timestamp | { nanoseconds: number; seconds: number }
+) => {
+ if (date instanceof Date) {
+ return date;
+ } else if ("toDate" in date) {
+ return date.toDate();
+ } else if (date.seconds) {
+ return new Date(date.seconds * 1000 + date.nanoseconds / 1_000_000);
+ } else if (date instanceof Timestamp) {
+ return date.toDate();
+ } else {
+ throw new Error(`Invalid date ${date}`);
+ }
+};
+
/**
* Support custom filter operators not supported by Firestore.
* e.g. date-range-equal: `>=` && `<=` operators when `==` is used on dates.
@@ -414,8 +435,7 @@ export const tableFiltersToFirestoreFilters = (filters: TableFilter[]) => {
for (const filter of filters) {
if (filter.operator.startsWith("date-")) {
if (!filter.value) continue;
- const filterDate =
- "toDate" in filter.value ? filter.value.toDate() : filter.value;
+ const filterDate = parseDateFilterValue(filter.value);
const [startDate, endDate] = getDateRange(filterDate);
if (filter.operator === "date-equal") {
@@ -433,8 +453,7 @@ export const tableFiltersToFirestoreFilters = (filters: TableFilter[]) => {
continue;
} else if (filter.operator === "time-minute-equal") {
if (!filter.value) continue;
- const filterDate =
- "toDate" in filter.value ? filter.value.toDate() : filter.value;
+ const filterDate = parseDateFilterValue(filter.value);
const [startDate, endDate] = getTimeRange(filterDate);
firestoreFilters.push(where(filter.key, ">=", startDate));
diff --git a/yarn.lock b/yarn.lock
index 66b5f6692..84047f603 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5208,6 +5208,11 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+fuse.js@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2"
+ integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==
+
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"