mirror of
https://github.com/rowyio/rowy.git
synced 2026-05-18 05:05:28 +02:00
@@ -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/)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 doesn’t exist in db yet
|
// And write entire row to handle the case where it doesn’t exist in db yet
|
||||||
if (missingRequiredFields.length === 0) {
|
if (missingRequiredFields.length === 0) {
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -44,5 +44,6 @@ export const config: IFieldConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
contextMenuActions: BasicContextMenuActions,
|
contextMenuActions: BasicContextMenuActions,
|
||||||
|
keywords: ["hexcode"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ export const config: IFieldConfig = {
|
|||||||
description: "Displays the row’s ID. Read-only. Cannot be sorted.",
|
description: "Displays the row’s ID. Read-only. Cannot be sorted.",
|
||||||
TableCell: withRenderTableCell(DisplayCell, null),
|
TableCell: withRenderTableCell(DisplayCell, null),
|
||||||
SideDrawerField,
|
SideDrawerField,
|
||||||
|
keywords: ["unique"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const config: IFieldConfig = {
|
|||||||
}),
|
}),
|
||||||
SideDrawerField,
|
SideDrawerField,
|
||||||
contextMenuActions: ContextMenuActions,
|
contextMenuActions: ContextMenuActions,
|
||||||
|
keywords: ["picture"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ export const config: IFieldConfig = {
|
|||||||
filter: {
|
filter: {
|
||||||
operators: filterOperators,
|
operators: filterOperators,
|
||||||
},
|
},
|
||||||
|
keywords: ["string"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -50,5 +50,6 @@ export const config: IFieldConfig = {
|
|||||||
operators: filterOperators,
|
operators: filterOperators,
|
||||||
},
|
},
|
||||||
contextMenuActions: BasicContextMenuActions,
|
contextMenuActions: BasicContextMenuActions,
|
||||||
|
keywords: ["options"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ export const config: IFieldConfig = {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
keywords: ["digit"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ export const config: IFieldConfig = {
|
|||||||
filter: {
|
filter: {
|
||||||
operators: filterOperators,
|
operators: filterOperators,
|
||||||
},
|
},
|
||||||
|
keywords: ["number", "contact"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -46,5 +46,6 @@ export const config: IFieldConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
contextMenuActions: BasicContextMenuActions,
|
contextMenuActions: BasicContextMenuActions,
|
||||||
|
keywords: ["star"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -36,5 +36,6 @@ export const config: IFieldConfig = {
|
|||||||
filter: {
|
filter: {
|
||||||
operators: filterOperators,
|
operators: filterOperators,
|
||||||
},
|
},
|
||||||
|
keywords: ["string"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ export const config: IFieldConfig = {
|
|||||||
filter: {
|
filter: {
|
||||||
operators: filterOperators,
|
operators: filterOperators,
|
||||||
},
|
},
|
||||||
|
keywords: ["link", "path"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ export const config: IFieldConfig = {
|
|||||||
}),
|
}),
|
||||||
SideDrawerField,
|
SideDrawerField,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
keywords: ["entity"]
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user