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. [Guided quick start button](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"