Merge pull request #1541 from rowyio/rc

Rc
This commit is contained in:
shams-mosowi
2024-11-24 06:46:34 +11:00
committed by GitHub
33 changed files with 281 additions and 104 deletions

View File

@@ -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. 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 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. not access or store any of your data on Rowy.
[<img width="200" alt="Guided quick start button" src="https://user-images.githubusercontent.com/307298/185548050-e9208fb6-fe53-4c84-bbfa-53c08e03c15f.png">](https://rowy.app/) [<img width="200" alt="Guided quick start button" src="https://user-images.githubusercontent.com/307298/185548050-e9208fb6-fe53-4c84-bbfa-53c08e03c15f.png">](https://rowy.app/)

View File

@@ -34,6 +34,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"firebase": "^9.12.1", "firebase": "^9.12.1",
"firebaseui": "^6.0.1", "firebaseui": "^6.0.1",
"fuse.js": "^7.0.0",
"jotai": "^1.8.4", "jotai": "^1.8.4",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.0", "jszip": "^3.10.0",

View File

@@ -386,9 +386,7 @@ export const updateFieldAtom = atom(
); );
if (!row) throw new Error("Could not find row"); if (!row) throw new Error("Could not find row");
const isLocalRow = const isLocalRow = Boolean(find(tableRowsLocal, ["_rowy_ref.path", path]));
fieldName.startsWith("_rowy_formulaValue_") ||
Boolean(find(tableRowsLocal, ["_rowy_ref.path", path]));
const update: Partial<TableRow> = {}; const update: Partial<TableRow> = {};
@@ -469,14 +467,6 @@ export const updateFieldAtom = atom(
deleteFields: deleteField ? [fieldName] : [], 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 // If it has no missingRequiredFields, also write to db
// And write entire row to handle the case where it doesnt exist in db yet // And write entire row to handle the case where it doesnt exist in db yet
if (missingRequiredFields.length === 0) { if (missingRequiredFields.length === 0) {

View File

@@ -1,5 +1,6 @@
import MultiSelect from "@rowy/multiselect"; import MultiSelect from "@rowy/multiselect";
import { Box, ListItemIcon, Typography } from "@mui/material"; import { Box, ListItemIcon, Typography } from "@mui/material";
import Fuse from 'fuse.js';
import { FIELDS } from "@src/components/fields"; import { FIELDS } from "@src/components/fields";
import { FieldType } from "@src/constants/fields"; import { FieldType } from "@src/constants/fields";
@@ -23,6 +24,15 @@ export interface IFieldsDropdownProps {
[key: string]: any; [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 * Returns dropdown component of all available types
*/ */
@@ -52,9 +62,21 @@ export default function FieldsDropdown({
disabled: requireCloudFunctionSetup || requireCollectionTable, disabled: requireCloudFunctionSetup || requireCollectionTable,
requireCloudFunctionSetup, requireCloudFunctionSetup,
requireCollectionTable, 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 ( return (
<MultiSelect <MultiSelect
multiple={false} multiple={false}
@@ -80,6 +102,7 @@ export default function FieldsDropdown({
}, },
}, },
}, },
filterOptions
}, },
} as any)} } as any)}
itemRenderer={(option) => ( itemRenderer={(option) => (

View File

@@ -121,6 +121,7 @@ export default function Table({
const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope); const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope);
const setReactTable = useSetAtom(reactTableAtom, tableScope); const setReactTable = useSetAtom(reactTableAtom, tableScope);
const setSelectedCell = useSetAtom(selectedCellAtom, tableScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope); const updateColumn = useSetAtom(updateColumnAtom, tableScope);
// Get user settings and tableId for applying sort sorting // Get user settings and tableId for applying sort sorting
@@ -313,6 +314,8 @@ export default function Table({
const { scrollHeight, scrollTop, clientHeight } = containerElement; const { scrollHeight, scrollTop, clientHeight } = containerElement;
if (scrollHeight - scrollTop - clientHeight < 300) { if (scrollHeight - scrollTop - clientHeight < 300) {
// deselect cell on next page load
setSelectedCell(null);
setTablePage((p) => p + 1); setTablePage((p) => p + 1);
} }
}, },

View File

@@ -1,29 +1,28 @@
import { useCallback, useState, useEffect } from "react"; import { useCallback, useEffect, useState } from "react";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { get, find } from "lodash-es"; import { find, get, isDate, isFunction } from "lodash-es";
import { import {
tableScope,
tableSchemaAtom,
tableRowsAtom,
updateFieldAtom,
SelectedCell, SelectedCell,
tableRowsAtom,
tableSchemaAtom,
tableScope,
updateFieldAtom,
} from "@src/atoms/tableScope"; } from "@src/atoms/tableScope";
import { getFieldProp, getFieldType } from "@src/components/fields"; import { getFieldProp, getFieldType } from "@src/components/fields";
import { ColumnConfig } from "@src/types/table"; import { ColumnConfig } from "@src/types/table";
import { FieldType } from "@src/constants/fields"; 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 { DATE_FORMAT, DATE_TIME_FORMAT } from "@src/constants/dates";
import { isDate, isFunction } from "lodash-es";
import { getDurationString } from "@src/components/fields/Duration/utils"; import { getDurationString } from "@src/components/fields/Duration/utils";
import { doc } from "firebase/firestore"; import { doc } from "firebase/firestore";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
import { projectScope } from "@src/atoms/projectScope"; import { projectScope } from "@src/atoms/projectScope";
export const SUPPORTED_TYPES_COPY = new Set([ export const SUPPORTED_TYPES_COPY = new Set<FieldType>([
// TEXT // TEXT
FieldType.shortText, FieldType.shortText,
FieldType.longText, FieldType.longText,
@@ -54,17 +53,24 @@ export const SUPPORTED_TYPES_COPY = new Set([
FieldType.code, FieldType.code,
FieldType.markdown, FieldType.markdown,
FieldType.array, FieldType.array,
// CLOUD FUNCTION
FieldType.action,
FieldType.derivative,
FieldType.status,
// AUDIT // AUDIT
FieldType.createdBy, FieldType.createdBy,
FieldType.updatedBy, FieldType.updatedBy,
FieldType.createdAt, FieldType.createdAt,
FieldType.updatedAt, FieldType.updatedAt,
// CONNECTION // CONNECTION
FieldType.arraySubTable,
FieldType.reference, FieldType.reference,
// METADATA
FieldType.user,
FieldType.id, FieldType.id,
]); ]);
export const SUPPORTED_TYPES_PASTE = new Set([ export const SUPPORTED_TYPES_PASTE = new Set<FieldType>([
// TEXT // TEXT
FieldType.shortText, FieldType.shortText,
FieldType.longText, FieldType.longText,
@@ -72,17 +78,34 @@ export const SUPPORTED_TYPES_PASTE = new Set([
FieldType.email, FieldType.email,
FieldType.phone, FieldType.phone,
FieldType.url, FieldType.url,
// SELECT
FieldType.singleSelect,
FieldType.multiSelect,
// NUMERIC // NUMERIC
FieldType.checkbox,
FieldType.number, FieldType.number,
FieldType.percentage, FieldType.percentage,
FieldType.rating, FieldType.rating,
FieldType.slider, FieldType.slider,
FieldType.color,
FieldType.geoPoint,
// DATE & TIME
FieldType.date,
FieldType.dateTime,
FieldType.duration,
// FILE
FieldType.image,
FieldType.file,
// CODE // CODE
FieldType.json, FieldType.json,
FieldType.code, FieldType.code,
FieldType.markdown, FieldType.markdown,
FieldType.array,
// CONNECTION // CONNECTION
FieldType.arraySubTable,
FieldType.reference, FieldType.reference,
// METADATA
FieldType.user,
]); ]);
export function useMenuAction( export function useMenuAction(
@@ -163,93 +186,177 @@ export function useMenuAction(
const handlePaste = useCallback( const handlePaste = useCallback(
async (e?: ClipboardEvent) => { 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 the focus element is not gridcell or menuitem (click on paste menu action)
if (document.activeElement?.role !== "gridcell") return; // it won't paste the copied content inside the gridcell
if (
!["gridcell", "menuitem"].includes(document.activeElement?.role ?? "")
)
return;
let text: string; // 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 clipboardText: string;
if (navigator.userAgent.includes("Firefox")) {
// Firefox doesn't allow for reading clipboard data, hence the workaround // Firefox doesn't allow for reading clipboard data, hence the workaround
if (navigator.userAgent.includes("Firefox")) { if (!e || !e.clipboardData) {
if (!e || !e.clipboardData) { enqueueSnackbar(
enqueueSnackbar( `If you're on Firefox, please use the hotkey instead (Ctrl + V / Cmd + V).`,
`If you're on Firefox, please use the hotkey instead (Ctrl + V / Cmd + V).`, {
{ variant: "info",
variant: "info", autoHideDuration: 7000,
autoHideDuration: 7000, }
} );
); enqueueSnackbar(`Cannot read clipboard data.`, {
enqueueSnackbar(`Cannot read clipboard data.`, { variant: "error",
variant: "error", });
}); return;
return;
}
text = e.clipboardData.getData("text/plain") || "";
} else {
try {
text = await navigator.clipboard.readText();
} catch (e) {
enqueueSnackbar(`Read clipboard permission denied.`, {
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( const cellDataType = getFieldProp(
"dataType", "dataType",
getFieldType(selectedCol) getFieldType(selectedCol)
); );
let parsed;
switch (cellDataType) { // parse value first by type if matches, then by column type
case "number": switch (selectedCol.type) {
parsed = Number(text); case FieldType.percentage:
if (isNaN(parsed)) throw new Error(`${text} is not a number`); 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; break;
case "string": case FieldType.date:
parsed = text; 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; break;
case "reference": 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 FieldType.duration:
try { try {
parsed = doc(firebaseDb, text); const json = JSON.parse(clipboardText);
parsedValue = {
start: new Date(json.start),
end: new Date(json.end),
};
} catch (e: any) { } catch (e: any) {
enqueueSnackbar(`Invalid reference.`, { variant: "error" }); throw new Error(
`${clipboardText} does not have valida start and end dates`
);
}
break;
case FieldType.arraySubTable:
try {
parsedValue = JSON.parse(clipboardText);
} catch (e: any) {
throw new Error(`${clipboardText} is not valid array subtable`);
}
if (!Array.isArray(parsedValue)) {
throw new Error(`${clipboardText} is not an array`);
} }
break; break;
default: default:
parsed = JSON.parse(text); switch (cellDataType) {
break; 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 (selectedCol.type === FieldType.slider) {
if (parsed < selectedCol.config?.min) if (parsedValue < selectedCol.config?.min)
parsed = selectedCol.config?.min; parsedValue = selectedCol.config?.min;
else if (parsed > selectedCol.config?.max) else if (parsedValue > (selectedCol.config?.max || 10))
parsed = selectedCol.config?.max; parsedValue = selectedCol.config?.max || 10;
} }
if (selectedCol.type === FieldType.rating) { if (selectedCol.type === FieldType.rating) {
if (parsed < 0) parsed = 0; if (parsedValue < 0) parsedValue = 0;
if (parsed > (selectedCol.config?.max || 5)) if (parsedValue > (selectedCol.config?.max || 5))
parsed = selectedCol.config?.max || 5; parsedValue = selectedCol.config?.max || 5;
} }
if (selectedCol.type === FieldType.percentage) {
parsed = parsed / 100;
}
updateField({ updateField({
path: selectedCell.path, path: selectedCell.path,
fieldName: selectedCol.fieldName, fieldName: selectedCol.fieldName,
value: parsed, value: parsedValue,
arrayTableData: { arrayTableData: {
index: selectedCell.arrayIndex, index: selectedCell.arrayIndex,
}, },
}); });
} catch (error) { } catch (error) {
enqueueSnackbar( enqueueSnackbar(`Paste error on ${selectedCol?.type}: ${error}`, {
`${selectedCol?.type} field does not support the data type being pasted`, variant: "error",
{ variant: "error" } });
);
} }
if (handleClose) handleClose(); if (handleClose) handleClose();
}, },
@@ -286,7 +393,7 @@ export function useMenuAction(
if (SUPPORTED_TYPES_COPY.has(fieldType)) { if (SUPPORTED_TYPES_COPY.has(fieldType)) {
return func(); return func();
} else { } else {
enqueueSnackbar(`${fieldType} field cannot be copied`, { enqueueSnackbar(`${fieldType} cannot be copied`, {
variant: "error", variant: "error",
}); });
} }
@@ -309,12 +416,9 @@ export function useMenuAction(
if (SUPPORTED_TYPES_PASTE.has(fieldType)) { if (SUPPORTED_TYPES_PASTE.has(fieldType)) {
return func(e); return func(e);
} else { } else {
enqueueSnackbar( enqueueSnackbar(`${fieldType} does not support paste`, {
`${fieldType} field does not support paste functionality`, variant: "error",
{ });
variant: "error",
}
);
} }
}; };
}, },
@@ -324,11 +428,17 @@ export function useMenuAction(
const getValue = useCallback( const getValue = useCallback(
(cellValue: any) => { (cellValue: any) => {
switch (selectedCol?.type) { switch (selectedCol?.type) {
case FieldType.percentage: case FieldType.multiSelect:
return cellValue * 100;
case FieldType.json: case FieldType.json:
case FieldType.color: case FieldType.color:
case FieldType.geoPoint: 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); return JSON.stringify(cellValue);
case FieldType.date: case FieldType.date:
if ( if (
@@ -362,19 +472,23 @@ export function useMenuAction(
} }
} }
return; return;
case FieldType.percentage:
return `${cellValue * 100}%`;
case FieldType.duration: case FieldType.duration:
return getDurationString( return JSON.stringify({
cellValue.start.toDate(), duration: getDurationString(
cellValue.end.toDate() cellValue.start.toDate(),
); cellValue.end.toDate()
case FieldType.image: ),
case FieldType.file: start: cellValue.start.toDate(),
return cellValue[0].downloadURL; end: cellValue.end.toDate(),
case FieldType.createdBy: });
case FieldType.updatedBy: case FieldType.action:
return cellValue.displayName; return cellValue.status || "";
case FieldType.reference: case FieldType.reference:
return cellValue.path; return cellValue.path;
case FieldType.formula:
return cellValue.formula || "";
default: default:
return cellValue; return cellValue;
} }

View File

@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
filter: { operators, defaultValue: [] }, filter: { operators, defaultValue: [] },
requireConfiguration: false, requireConfiguration: false,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["list"]
}; };
export default config; export default config;

View File

@@ -45,5 +45,6 @@ export const config: IFieldConfig = {
}, },
SideDrawerField, SideDrawerField,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["boolean", "switch", "true", "false", "on", "off"]
}; };
export default config; export default config;

View File

@@ -34,5 +34,6 @@ export const config: IFieldConfig = {
SideDrawerField, SideDrawerField,
settings: Settings, settings: Settings,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["snippet", "block"]
}; };
export default config; export default config;

View File

@@ -44,5 +44,6 @@ export const config: IFieldConfig = {
} }
}, },
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["hexcode"]
}; };
export default config; export default config;

View File

@@ -30,5 +30,6 @@ export const config: IFieldConfig = {
settings: Settings, settings: Settings,
requireCollectionTable: true, requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["date", "time"]
}; };
export default config; export default config;

View File

@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
settings: Settings, settings: Settings,
requireCollectionTable: true, requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["date", "time"]
}; };
export default config; export default config;

View File

@@ -6,9 +6,7 @@ import {
_deleteRowDbAtom, _deleteRowDbAtom,
_updateRowDbAtom, _updateRowDbAtom,
tableNextPageAtom, tableNextPageAtom,
tableRowsAtom,
tableRowsDbAtom, tableRowsDbAtom,
tableRowsLocalAtom,
tableScope, tableScope,
tableSettingsAtom, tableSettingsAtom,
} from "@src/atoms/tableScope"; } from "@src/atoms/tableScope";

View File

@@ -27,5 +27,6 @@ export const config: IFieldConfig = {
settings: Settings, settings: Settings,
settingsValidator: settingsValidator, settingsValidator: settingsValidator,
requireConfiguration: true, requireConfiguration: true,
keywords: ["equation"]
}; };
export default config; export default config;

View File

@@ -26,5 +26,6 @@ export const config: IFieldConfig = {
}), }),
SideDrawerField, SideDrawerField,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["location", "latitude", "longitude", "point"]
}; };
export default config; export default config;

View File

@@ -19,5 +19,6 @@ export const config: IFieldConfig = {
description: "Displays the rows ID. Read-only. Cannot be sorted.", description: "Displays the rows ID. Read-only. Cannot be sorted.",
TableCell: withRenderTableCell(DisplayCell, null), TableCell: withRenderTableCell(DisplayCell, null),
SideDrawerField, SideDrawerField,
keywords: ["unique"]
}; };
export default config; export default config;

View File

@@ -28,6 +28,7 @@ export const config: IFieldConfig = {
}), }),
SideDrawerField, SideDrawerField,
contextMenuActions: ContextMenuActions, contextMenuActions: ContextMenuActions,
keywords: ["picture"]
}; };
export default config; export default config;

View File

@@ -35,5 +35,6 @@ export const config: IFieldConfig = {
filter: { filter: {
operators: filterOperators, operators: filterOperators,
}, },
keywords: ["string"]
}; };
export default config; export default config;

View File

@@ -25,5 +25,6 @@ export const config: IFieldConfig = {
TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"), TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"),
SideDrawerField, SideDrawerField,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["md"]
}; };
export default config; export default config;

View File

@@ -50,5 +50,6 @@ export const config: IFieldConfig = {
operators: filterOperators, operators: filterOperators,
}, },
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["options"]
}; };
export default config; export default config;

View File

@@ -35,5 +35,6 @@ export const config: IFieldConfig = {
return null; return null;
} }
}, },
keywords: ["digit"]
}; };
export default config; export default config;

View File

@@ -28,5 +28,6 @@ export const config: IFieldConfig = {
filter: { filter: {
operators: filterOperators, operators: filterOperators,
}, },
keywords: ["number", "contact"]
}; };
export default config; export default config;

View File

@@ -46,5 +46,6 @@ export const config: IFieldConfig = {
} }
}, },
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["star"]
}; };
export default config; export default config;

View File

@@ -25,5 +25,6 @@ export const config: IFieldConfig = {
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"), TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"),
SideDrawerField, SideDrawerField,
keywords: ["string"]
}; };
export default config; export default config;

View File

@@ -36,5 +36,6 @@ export const config: IFieldConfig = {
filter: { filter: {
operators: filterOperators, operators: filterOperators,
}, },
keywords: ["string"]
}; };
export default config; export default config;

View File

@@ -37,5 +37,6 @@ export const config: IFieldConfig = {
filter: { operators: filterOperators }, filter: { operators: filterOperators },
requireConfiguration: true, requireConfiguration: true,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["options"]
}; };
export default config; export default config;

View File

@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
settings: Settings, settings: Settings,
requireCollectionTable: true, requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["date", "time"]
}; };
export default config; export default config;

View File

@@ -33,5 +33,6 @@ export const config: IFieldConfig = {
settings: Settings, settings: Settings,
requireCollectionTable: true, requireCollectionTable: true,
contextMenuActions: BasicContextMenuActions, contextMenuActions: BasicContextMenuActions,
keywords: ["date", "time"]
}; };
export default config; export default config;

View File

@@ -30,5 +30,6 @@ export const config: IFieldConfig = {
filter: { filter: {
operators: filterOperators, operators: filterOperators,
}, },
keywords: ["link", "path"]
}; };
export default config; export default config;

View File

@@ -29,5 +29,6 @@ export const config: IFieldConfig = {
}), }),
SideDrawerField, SideDrawerField,
settings: Settings, settings: Settings,
keywords: ["entity"]
}; };
export default config; export default config;

View File

@@ -42,6 +42,7 @@ export interface IFieldConfig {
sortKey?: string; sortKey?: string;
csvExportFormatter?: (value: any, config?: any) => string; csvExportFormatter?: (value: any, config?: any) => string;
csvImportParser?: (value: string, config?: any) => any; csvImportParser?: (value: string, config?: any) => any;
keywords?: string[];
} }
/** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */ /** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */

View File

@@ -26,6 +26,7 @@ import {
DocumentData, DocumentData,
or, or,
QueryFieldFilterConstraint, QueryFieldFilterConstraint,
Timestamp,
} from "firebase/firestore"; } from "firebase/firestore";
import { useErrorHandler } from "react-error-boundary"; import { useErrorHandler } from "react-error-boundary";
@@ -402,6 +403,26 @@ const getQuery = <T>(
} }
}; };
/**
* 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. * Support custom filter operators not supported by Firestore.
* e.g. date-range-equal: `>=` && `<=` operators when `==` is used on dates. * 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) { for (const filter of filters) {
if (filter.operator.startsWith("date-")) { if (filter.operator.startsWith("date-")) {
if (!filter.value) continue; if (!filter.value) continue;
const filterDate = const filterDate = parseDateFilterValue(filter.value);
"toDate" in filter.value ? filter.value.toDate() : filter.value;
const [startDate, endDate] = getDateRange(filterDate); const [startDate, endDate] = getDateRange(filterDate);
if (filter.operator === "date-equal") { if (filter.operator === "date-equal") {
@@ -433,8 +453,7 @@ export const tableFiltersToFirestoreFilters = (filters: TableFilter[]) => {
continue; continue;
} else if (filter.operator === "time-minute-equal") { } else if (filter.operator === "time-minute-equal") {
if (!filter.value) continue; if (!filter.value) continue;
const filterDate = const filterDate = parseDateFilterValue(filter.value);
"toDate" in filter.value ? filter.value.toDate() : filter.value;
const [startDate, endDate] = getTimeRange(filterDate); const [startDate, endDate] = getTimeRange(filterDate);
firestoreFilters.push(where(filter.key, ">=", startDate)); firestoreFilters.push(where(filter.key, ">=", startDate));

View File

@@ -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" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== 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: gensync@^1.0.0-beta.2:
version "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" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"