diff --git a/cloud_functions/functions/src/algolia/index.ts b/cloud_functions/functions/src/algolia/index.ts index b6b4c28d..5c8d9a46 100644 --- a/cloud_functions/functions/src/algolia/index.ts +++ b/cloud_functions/functions/src/algolia/index.ts @@ -2,6 +2,8 @@ import * as algoliasearch from "algoliasearch"; import * as functions from "firebase-functions"; import * as _ from "lodash"; import { env } from "../config"; +import config from "../functionConfig"; // generated using generateConfig.ts +const functionConfig: any = config; const APP_ID = env.algolia.app; const ADMIN_KEY = env.algolia.key; @@ -181,4 +183,7 @@ const algoliaFnsGenerator = collection => ({ // console.log(JSON.stringify(afterData)); // }), // }); -export default algoliaFnsGenerator; +//export default algoliaFnsGenerator; +export const FT_algolia = { + [functionConfig.name]: { ...algoliaFnsGenerator(functionConfig) }, +}; diff --git a/cloud_functions/functions/src/collectionSync/index.ts b/cloud_functions/functions/src/collectionSync/index.ts index f3bdd462..d885bf26 100644 --- a/cloud_functions/functions/src/collectionSync/index.ts +++ b/cloud_functions/functions/src/collectionSync/index.ts @@ -4,6 +4,8 @@ import { db } from "../config"; import * as _ from "lodash"; import { replacer } from "../utils/email"; +import config from "../functionConfig"; // generated using generateConfig.ts +const functionConfig: any = config; // returns object of fieldsToSync const docReducer = (docData: FirebaseFirestore.DocumentData) => ( acc: any, @@ -127,4 +129,11 @@ const collectionSyncFnsGenerator = collection => : null, }).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {}); -export default collectionSyncFnsGenerator; +//export default collectionSyncFnsGenerator; +export const FT_sync = { + [`${`${`${functionConfig.source}` + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")}`}2${`${`${functionConfig.target}` + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")}`}`]: collectionSyncFnsGenerator(functionConfig), +}; diff --git a/cloud_functions/functions/src/derivatives/index.ts b/cloud_functions/functions/src/derivatives/index.ts new file mode 100644 index 00000000..00797816 --- /dev/null +++ b/cloud_functions/functions/src/derivatives/index.ts @@ -0,0 +1,58 @@ +import * as functions from "firebase-functions"; + +import { db } from "../config"; +import config, { collectionPath } from "../functionConfig"; +// generated using generateConfig.ts +const functionConfig: any = config; + +const shouldEvaluateReducer = (listeners, before, after) => + listeners.reduce((acc: Boolean, currField: string) => { + if (acc) return true; + else return before[currField] !== after[currField]; + }, false); +export const derivativeOnChange = async ( + Change: functions.Change +) => { + const beforeData = Change.before.data(); + const afterData = Change.after.data(); + if (!beforeData || !afterData) return false; + const update = await functionConfig.reduce( + async (accUpdates: any, currDerivative) => { + const shouldEval = shouldEvaluateReducer( + currDerivative.listenerFields, + beforeData, + afterData + ); + if (shouldEval) { + const newValue = await currDerivative.eval(db)(afterData); + if (newValue !== undefined) { + return { + ...(await accUpdates), + [currDerivative.fieldName]: newValue, + }; + } + } + return await accUpdates; + }, + {} + ); + if (Object.keys(update).length !== 0) { + return Change.after.ref.update(update); + } + return false; +}; + +/** + * + * @param collection configuration object + */ +export const FT_derivatives = { + [collectionPath]: { + // onCreate: functions.firestore + // .document(`employees/{docId}`) + // .onCreate(addSynonymOnCreate(collection.groups)), + onUpdate: functions.firestore + .document(`${collectionPath}/{docId}`) + .onUpdate(derivativeOnChange), + }, +}; diff --git a/cloud_functions/functions/src/functionConfig.ts b/cloud_functions/functions/src/functionConfig.ts new file mode 100644 index 00000000..44462430 --- /dev/null +++ b/cloud_functions/functions/src/functionConfig.ts @@ -0,0 +1,2 @@ +export const collectionPath = "teams"; +export default []; diff --git a/cloud_functions/functions/src/generateConfig.ts b/cloud_functions/functions/src/generateConfig.ts index 2ab3dc83..9c6d727f 100644 --- a/cloud_functions/functions/src/generateConfig.ts +++ b/cloud_functions/functions/src/generateConfig.ts @@ -1,7 +1,68 @@ const fs = require("fs"); +// Initialize Firebase Admin +import * as admin from "firebase-admin"; +// Initialize Firebase Admin +//const serverTimestamp = admin.firestore.FieldValue.serverTimestamp; -const main = configString => { - fs.writeFileSync("./src/functionConfig.ts", `export default ${configString}`); -}; +const serviceAccount = requireIfExists("../firebase-credentials.json"); -main(process.argv[2]); +function requireIfExists(module) { + try { + return require(module); + } catch (error) { + console.log("serviceAccount json not found"); + return false; + } +} +if (serviceAccount) { + console.log(`Running on ${serviceAccount.project_id}`); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`, + }); + + const db = admin.firestore(); + const main = async (functionType: string, configString: string) => { + let configData; + switch (functionType) { + case "FT_derivatives": + const schemaDoc = await db + .doc(`_FIRETABLE_/settings/schema/${configString}`) + .get(); + const schemaData = schemaDoc.data(); + if (!schemaData) return; + const derivativeColumns = Object.values(schemaData.columns).filter( + (col: any) => col.type === "DERIVATIVE" + ); + console.log(derivativeColumns); + + const config = derivativeColumns.reduce((acc, currColumn: any) => { + return `${acc}{ + fieldName:'${currColumn.key}',eval:(db)=> async (row) =>{${ + currColumn.config.script + }},listenerFields:[${currColumn.config.listenerFields + .map(f => `"${f}"`) + .join(",")}]},`; + }, ``); + + configData = `export default [${config}]\nexport const collectionPath ="${configString}"`; + break; + + case "FT_subTableStats": + configData = `export const collectionPath ="${configString}"\nexport default []`; + break; + default: + configData = `export default ${configString}`; + break; + } + fs.writeFileSync("./src/functionConfig.ts", configData); + return; + }; + + main(process.argv[2], process.argv[3]) + .catch(err => console.log(err)) + .then(() => console.log("this will succeed")) + .catch(() => "obligatory catch"); +} else { + console.log("did not run generator"); +} diff --git a/cloud_functions/functions/src/history/index.ts b/cloud_functions/functions/src/history/index.ts index 5f0418ca..182b3361 100644 --- a/cloud_functions/functions/src/history/index.ts +++ b/cloud_functions/functions/src/history/index.ts @@ -2,6 +2,9 @@ import * as functions from "firebase-functions"; import * as _ from "lodash"; import { db } from "../config"; +import config from "../functionConfig"; // generated using generateConfig.ts +const functionConfig: any = config; + const historySnapshot = (trackedFields: string[]) => async ( change: functions.Change ) => { @@ -28,4 +31,10 @@ const historySnapshotFnsGenerator = collection => .document(`${collection.name}/{docId}`) .onUpdate(historySnapshot(collection.trackedFields)); -export default historySnapshotFnsGenerator; +//export default historySnapshotFnsGenerator; + +export const FT_history = { + [functionConfig.name + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")]: historySnapshotFnsGenerator(functionConfig), +}; diff --git a/cloud_functions/functions/src/index.ts b/cloud_functions/functions/src/index.ts index 4cfc3e22..fe75071c 100644 --- a/cloud_functions/functions/src/index.ts +++ b/cloud_functions/functions/src/index.ts @@ -5,49 +5,9 @@ export const callable = callableFns; // all the cloud functions bellow are deployed using the triggerCloudBuild callable function // these functions are designed to be built and deployed based on the configuration passed through the callable -import config from "./functionConfig"; // generated using generateConfig.ts -import algoliaFnsGenerator from "./algolia"; // algolia sync functions template -import collectionSyncFnsGenerator from "./collectionSync"; -import snapshotSyncFnsGenerator from "./snapshotSync"; -import collectionSnapshotFnsGenerator from "./history"; -import permissionControlFnsGenerator from "./permissions"; - -const functionConfig: any = config; - -export const FT_algolia = { - [functionConfig.name]: { ...algoliaFnsGenerator(functionConfig) }, -}; - -// { -// [functionConfig.name]: { ...algoliaFnsGenerator(functionConfig) }, -// }; - -export const FT_sync = { - [`${`${`${functionConfig.source}` - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")}`}2${`${`${functionConfig.target}` - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")}`}`]: collectionSyncFnsGenerator(functionConfig), -}; - -export const FT_history = { - [functionConfig.name - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")]: collectionSnapshotFnsGenerator(functionConfig), -}; - -export const FT_permissions = { - [functionConfig.name]: permissionControlFnsGenerator(functionConfig), -}; - -export const FT_snapshotSync = functionConfig.fnName - ? { [functionConfig.fnName]: snapshotSyncFnsGenerator(functionConfig) } - : { - [`${`${`${functionConfig.source}` - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")}`}2${`${`${functionConfig.target}` - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")}`}`]: snapshotSyncFnsGenerator( - functionConfig - ), - }; +export { FT_derivatives } from "./derivatives"; +export { FT_algolia } from "./algolia"; +export { FT_sync } from "./collectionSync"; +export { FT_snapshotSync } from "./snapshotSync"; +//export { FT_history } from "./history"; +export { FT_subTableStats } from "./subTableStats"; diff --git a/cloud_functions/functions/src/permissions/config.json b/cloud_functions/functions/src/permissions/config.json deleted file mode 100644 index fe51488c..00000000 --- a/cloud_functions/functions/src/permissions/config.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/cloud_functions/functions/src/permissions/index.ts b/cloud_functions/functions/src/permissions/index.ts deleted file mode 100644 index a319241f..00000000 --- a/cloud_functions/functions/src/permissions/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { firestore, Change, auth as FCAuth } from "firebase-functions"; -import { auth, db } from "../config"; - -const email2uid = async (email: string) => { - const user = await auth.getUserByEmail(email); - console.log( - `${email} previous customClaims ${JSON.stringify(user.customClaims)}` - ); - return user ? user.uid : undefined; -}; - -const setClaims = async ( - uid: string, - doc: any, - customClaimsFields: string[] -) => { - const customClaimsTokens = customClaimsFields.reduce( - (token: any, currentField: string) => { - if (doc[currentField]) - return { ...token, [currentField]: doc[currentField] }; - else return token; - }, - {} - ); - - await auth - .setCustomUserClaims(uid, customClaimsTokens) - .catch(err => console.log(err)) - .then(() => { - console.info( - `${uid} has new claims ${JSON.stringify(customClaimsTokens)}` - ); - }) - .catch(() => "obligatory catch"); - return true; -}; -export const onSignup = ( - collectionName: string, - customTokenFields: string[] -) => async (user: FCAuth.UserRecord) => { - if (user.emailVerified) { - const email = user.email; - const invites = await db - .collection(collectionName) - .where("email", "==", email) - .get(); - if (invites.docs.length === 0) { - console.log("no invites found"); - return false; - } - - const inviteData = invites.docs[0].data(); - await setClaims(user.uid, inviteData, customTokenFields); - return true; - } - console.error("email not verified on signup"); - return false; -}; - -const permissions = (customClaimsFields: string[]) => async ( - snapshot: FirebaseFirestore.DocumentSnapshot -) => { - const docData = snapshot.data(); - if (!docData) { - console.log("no after Data"); - return false; - } - const { email } = docData; - if (!email) { - console.warn(`row has no email`); - return false; - } - const uid = await email2uid(email); - if (!uid) { - console.warn(`user ${uid} does not exist`); - return false; - } - await setClaims(uid, docData, customClaimsFields); - return true; -}; - -const permissionsFnsGenerator = collection => ({ - onCreate: firestore - .document(`${collection.name}/{docId}`) - .onCreate(permissions(collection.customTokenFields)), - onUpdate: firestore - .document(`${collection.name}/{docId}`) - .onUpdate((change: Change) => - permissions(collection.customTokenFields)(change.after) - ), - onSignup: FCAuth.user().onCreate( - onSignup(collection.name, collection.customTokenFields) - ), -}); - -export default permissionsFnsGenerator; diff --git a/cloud_functions/functions/src/snapshotSync/index.ts b/cloud_functions/functions/src/snapshotSync/index.ts index 900a8598..d4e151e2 100644 --- a/cloud_functions/functions/src/snapshotSync/index.ts +++ b/cloud_functions/functions/src/snapshotSync/index.ts @@ -5,6 +5,9 @@ import { db } from "../config"; import * as _ from "lodash"; import { replacer } from "../utils/email"; +import Config from "../functionConfig"; // generated using generateConfig.ts +const functionConfig: any = Config; + enum TargetTypes { subCollection = "subCollection", document = "document", @@ -162,4 +165,16 @@ const snapshotSyncFnsGenerator = config => : null, }).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {}); -export default snapshotSyncFnsGenerator; +//export default snapshotSyncFnsGenerator; + +export const FT_snapshotSync = functionConfig.fnName + ? { [functionConfig.fnName]: snapshotSyncFnsGenerator(functionConfig) } + : { + [`${`${`${functionConfig.source}` + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")}`}2${`${`${functionConfig.target}` + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")}`}`]: snapshotSyncFnsGenerator( + functionConfig + ), + }; diff --git a/cloud_functions/functions/src/subTableStats.ts b/cloud_functions/functions/src/subTableStats.ts new file mode 100644 index 00000000..8da27b14 --- /dev/null +++ b/cloud_functions/functions/src/subTableStats.ts @@ -0,0 +1,37 @@ +import * as functions from "firebase-functions"; +import * as admin from "firebase-admin"; + +import { collectionPath } from "./functionConfig"; // generated using generateConfig.ts +const increment = admin.firestore.FieldValue.increment(1); +const decrement = admin.firestore.FieldValue.increment(-1); +const docCreated = ( + snapshot: functions.firestore.DocumentSnapshot, + context: functions.EventContext +) => { + const { subCollectionId } = context.params; + return snapshot.ref.parent.parent?.update({ + [`${subCollectionId}.count`]: increment, + }); +}; + +const docDelete = ( + snapshot: functions.firestore.DocumentSnapshot, + context: functions.EventContext +) => { + const { subCollectionId } = context.params; + return snapshot.ref.parent.parent?.update({ + [subCollectionId]: decrement, + }); +}; +const subTableFnsGenerator = { + onCreate: functions.firestore + .document(`${collectionPath}/{parentId}/{subCollectionId}/{docId}`) + .onCreate(docCreated), + onDelete: functions.firestore + .document(`${collectionPath}/{parentId}/{subCollectionId}/{docId}`) + .onDelete(docDelete), +}; + +export const FT_subTableStats = { + [collectionPath]: { ...subTableFnsGenerator }, +}; diff --git a/cloudbuildfunctions.yaml b/cloudbuildfunctions.yaml index 2598e222..b8b6a7fd 100644 --- a/cloudbuildfunctions.yaml +++ b/cloudbuildfunctions.yaml @@ -21,6 +21,7 @@ steps: entrypoint: yarn args: - "generateConfig" + - "${_FUNCTIONS_GROUP}" - "${_FUNCTION_CONFIG}" dir: "cloud_functions/functions" - name: node:10.15.1 diff --git a/www/src/components/Navigation.tsx b/www/src/components/Navigation.tsx index fa1f6486..2609c2cd 100644 --- a/www/src/components/Navigation.tsx +++ b/www/src/components/Navigation.tsx @@ -72,28 +72,10 @@ export default function Navigation({ const section = _find(tables, ["collection", tableCollection?.split("/")[0]]) ?.section; - // Get the table path, including filtering for regions & user permissions + // Get the table path, including filtering for user permissions const getTablePath = (table: Table): LinkProps["to"] => { if (!table || !userClaims) return ""; - if ( - userClaims.regions && - table.regional && - !userClaims.regions?.includes("GL") - ) - return { - pathname: table.collection, - search: `?filters=${encodeURIComponent( - JSON.stringify([ - { - key: "region", - operator: "==", - value: userClaims.regions[0], - }, - ]) - )}`, - }; - return table.collection; }; diff --git a/www/src/components/Table/ColumnEditor/DocInput.tsx b/www/src/components/Table/ColumnEditor/DocInput.tsx deleted file mode 100644 index 7f4bebe2..00000000 --- a/www/src/components/Table/ColumnEditor/DocInput.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; -import Chip from "@material-ui/core/Chip"; -import Grid from "@material-ui/core/Grid"; -import TextField from "@material-ui/core/TextField"; -import Divider from "@material-ui/core/Divider"; -import Select from "@material-ui/core/Select"; - -import MenuItem from "@material-ui/core/MenuItem"; -import useTableConfig from "../../../hooks/useFiretable/useTableConfig"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; - -const useStyles = makeStyles(Theme => - createStyles({ - root: { - display: "flex", - justifyContent: "center", - flexWrap: "wrap", - maxWidth: 300, - padding: Theme.spacing(1), - }, - chip: { - margin: Theme.spacing(0.5), - }, - formControl: { - margin: Theme.spacing(1), - minWidth: 120, - maxWidth: 300, - }, - chips: { - display: "flex", - flexWrap: "wrap", - }, - }) -); - -const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; -const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - }, - }, -}; - -export default function DocInput(props: any) { - const { collectionPath, setValue } = props; - const [tableConfig, tableConfigActions] = useTableConfig( - collectionPath ? collectionPath : "initial" - ); - - const [columns, setColumns] = useState<{ key: string; name: string }[]>([]); - const [primaryKeys, setPrimaryKeys] = useState([]); - const [secondaryKeys, setSecondaryKeys] = useState([]); - - useEffect(() => { - setColumns(tableConfig.columns); - }, [tableConfig.columns]); - - const classes = useStyles(); - - const onChange = (e: any, v: any) => { - setValue("collectionPath", v.props.value); - setPrimaryKeys([]); - setSecondaryKeys([]); - setColumns([]); - tableConfigActions.setTable(v.props.value); - }; - useEffect(() => { - setValue("config", { - primaryKeys, - secondaryKeys, - }); - }, [primaryKeys, secondaryKeys]); - // WAS USING TABLES CONTEXT RIP - // if (tables.value) - // return ( - // <> - // - // - // Table - // - // - // - // {collectionPath ? ( - // <> - // - // - // Primary Text - // - // } - // renderValue={selected => ( - //
- // {(selected as string[]).map(value => ( - // - // ))} - //
- // )} - // MenuProps={MenuProps} - // > - // {columns && - // columns.length !== 0 && - // columns.map(column => ( - // - // {column.name} - // - // ))} - // - //
- - // - // - // Secondary Text - // - // } - // renderValue={selected => ( - //
- // {(selected as string[]).map(value => ( - // - // ))} - //
- // )} - // MenuProps={MenuProps} - // > - // {columns && - // columns.length !== 0 && - // columns.map(column => ( - // - // {column.name} - // - // ))} - // - //
- // - // ) : ( - // "please select a table" - // )} - // - // ); - // else - return
; -} diff --git a/www/src/components/Table/ColumnEditor/FieldsDropdown.tsx b/www/src/components/Table/ColumnEditor/FieldsDropdown.tsx deleted file mode 100644 index e2600349..00000000 --- a/www/src/components/Table/ColumnEditor/FieldsDropdown.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; - -import { TextField, MenuItem, ListItemIcon } from "@material-ui/core"; - -import { FIELDS, FieldType } from "constants/fields"; - -/** - * Returns dropdown component of all available types - */ -const FieldsDropdown = (value: FieldType | null, onChange: any) => { - return ( - - {FIELDS.map( - (field: { icon: JSX.Element; name: string; type: FieldType }) => { - return ( - - - {field.icon} - - {field.name} - - ); - } - )} - - ); -}; - -export default FieldsDropdown; diff --git a/www/src/components/Table/ColumnEditor/SelectOptionsInput.tsx b/www/src/components/Table/ColumnEditor/SelectOptionsInput.tsx deleted file mode 100644 index 94f3c09b..00000000 --- a/www/src/components/Table/ColumnEditor/SelectOptionsInput.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; -import Chip from "@material-ui/core/Chip"; -import TextField from "@material-ui/core/TextField"; -import Grid from "@material-ui/core/Grid"; -import Divider from "@material-ui/core/Divider"; -import _includes from "lodash/includes"; -import _camelCase from "lodash/camelCase"; - -import AddIcon from "@material-ui/icons/AddCircle"; -import IconButton from "@material-ui/core/IconButton"; -import InputAdornment from "@material-ui/core/InputAdornment"; -const useStyles = makeStyles(Theme => - createStyles({ - root: {}, - field: { - width: "100%", - }, - chipsContainer: { - display: "flex", - justifyContent: "center", - flexWrap: "wrap", - maxWidth: 300, - padding: Theme.spacing(1), - }, - chip: { - margin: Theme.spacing(0.5), - }, - }) -); - -export default function SelectOptionsInput(props: any) { - const { options, setValue } = props; - const classes = useStyles(); - const [newOption, setNewOption] = useState(""); - const handleAdd = () => { - // setOptions([...options, newOption]); - if (newOption !== "") { - setValue("options", [...options, newOption]); - setNewOption(""); - } - }; - const handleDelete = (optionToDelete: string) => () => { - const newOptions = options.filter( - (option: string) => option !== optionToDelete - ); - setValue("options", newOptions); - }; - - useEffect(() => { - setValue({ data: { options } }); - }, [options]); - - return ( - - - { - setNewOption(e.target.value); - }} - onKeyPress={(e: any) => { - if (e.key === "Enter") { - handleAdd(); - } - }} - InputProps={{ - endAdornment: ( - - { - handleAdd(); - }} - > - {} - - - ), - }} - /> - - - {options.map((option: string) => { - return ( - - ); - })} - - - ); -} diff --git a/www/src/components/Table/ColumnEditor/index.tsx b/www/src/components/Table/ColumnEditor/index.tsx deleted file mode 100644 index 1fbd94c0..00000000 --- a/www/src/components/Table/ColumnEditor/index.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import React, { useEffect, useState } from "react"; -import _findIndex from "lodash/findIndex"; -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; - -import TextField from "@material-ui/core/TextField"; -import Grid from "@material-ui/core/Grid"; -import Popover from "@material-ui/core/Popover"; -import { createStyles, makeStyles } from "@material-ui/core/styles"; -import FieldsDropdown from "./FieldsDropdown"; -import { FieldType } from "constants/fields"; -import ToggleButton from "@material-ui/lab/ToggleButton"; -import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup"; - -import LockIcon from "@material-ui/icons/Lock"; -import LockOpenIcon from "@material-ui/icons/LockOpen"; -import VisibilityIcon from "@material-ui/icons/Visibility"; -import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; -import FrozenIcon from "@material-ui/icons/AcUnit"; -import ResizeIcon from "@material-ui/icons/PhotoSizeSelectSmall"; -import DeleteIcon from "@material-ui/icons/Delete"; -import SelectOptionsInput from "./SelectOptionsInput"; - -import DocInput from "./DocInput"; -import { Tooltip } from "@material-ui/core"; -import Confirmation from "../../Confirmation"; - -import { useFiretableContext } from "contexts/firetableContext"; - -const useStyles = makeStyles(theme => - createStyles({ - container: { - padding: 15, - }, - typography: { - padding: 1, - }, - - button: { - // margin: theme.spacing(1) - }, - root: { - padding: 10, - display: "flex", - flexWrap: "wrap", - }, - formControl: { - margin: theme.spacing(1), - minWidth: 120, - }, - selectEmpty: { - marginTop: theme.spacing(2), - }, - toggleGrouped: { - margin: theme.spacing(0.5), - border: "none", - padding: theme.spacing(0, 1), - "&:not(:first-child)": { - borderRadius: theme.shape.borderRadius, - }, - "&:first-child": { - borderRadius: theme.shape.borderRadius, - }, - }, - }) -); - -// TODO: REMOVE THIS OLD COMPONENT -export default function ColumnEditor() { - const { - tableState, - tableActions, - selectedColumnHeader, - setSelectedColumnHeader, - } = useFiretableContext() as any; - const actions = tableActions!.column; - const { column, anchorEl } = selectedColumnHeader ?? {}; - - const handleClose = () => { - if (setSelectedColumnHeader) setSelectedColumnHeader(null); - }; - - const [values, setValues] = useState({ - type: null, - name: "", - options: [], - collectionPath: "", - config: {}, - parentLabel: "", - callableName: "", - }); - const [flags, setFlags] = useState(() => [""]); - const classes = useStyles(); - - function handleChange( - event: React.ChangeEvent<{ name?: string; value: unknown }> - ) { - event.stopPropagation(); - event.preventDefault(); - setValues(oldValues => ({ - ...oldValues, - [event.target.name as string]: event.target.value, - })); - } - const setValue = (key: string, value: any) => { - setValues(oldValues => ({ - ...oldValues, - [key]: value, - })); - }; - - useEffect(() => { - if (column && !column.isNew) { - setValues(oldValues => ({ - ...oldValues, - name: column.name, - type: column.type, - key: column.key, - isNew: column.isNew, - })); - if (column.options) { - setValue("options", column.options); - } else { - setValue("options", []); - } - if (column.collectionPath) { - setValue("collectionPath", column.collectionPath); - } - const flags = ["resizable", "editable", "fixed", "hidden"].filter( - flag => - column[flag] === true || - (flag === "resizable" && column[flag] !== false) - ); - setFlags(flags); - } - }, [column]); - - const clearValues = () => { - setValues({ - type: null, - name: "", - options: [], - collectionPath: "", - config: {}, - parentLabel: "", - callableName: "", - }); - }; - const onClose = (event: any) => { - handleClose(); - clearValues(); - }; - - const handleToggle = ( - event: React.MouseEvent, - newFlags: string[] - ) => { - setFlags(newFlags); - }; - - const createNewColumn = () => { - const { name, type, options, collectionPath, config, parentLabel } = values; - - actions.add(name, type, { options, collectionPath, config, parentLabel }); - - handleClose(); - clearValues(); - }; - - // WARNING: column.idx does NOT map to how we store column config in Firestore - // when a column is fixed AND it is not the first column. We should NOT be - // using indexes to manipulate column config anyway! - - const configColumnIndex = _findIndex(tableState?.columns, [ - "key", - column?.key, - ]); - - const deleteColumn = () => { - actions.remove(column?.key); - handleClose(); - clearValues(); - }; - - const updateColumn = () => { - let updatables: { field: string; value: any }[] = [ - { field: "name", value: values.name }, - { field: "type", value: values.type }, - // TEMP: disable resizing fixed columns due to the problem described - // on line 169 - { - field: "resizable", - value: !flags.includes("fixed") && flags.includes("resizable"), - }, - { field: "editable", value: flags.includes("editable") }, - { field: "hidden", value: flags.includes("hidden") }, - { field: "fixed", value: flags.includes("fixed") }, - ]; - if ( - values.type === FieldType.multiSelect || - values.type === FieldType.singleSelect - ) { - updatables.push({ - field: "options", - value: values.options, - }); - } - if (values.type === FieldType.connectTable) { - updatables.push({ - field: "collectionPath", - value: values.collectionPath, - }); - updatables.push({ - field: "config", - value: values.config, - }); - } - if (values.type === FieldType.subTable) { - updatables.push({ field: "parentLabel", value: values.parentLabel }); - } - if (values.type === FieldType.action) { - updatables.push({ field: "callableName", value: values.callableName }); - } - actions.update(configColumnIndex, updatables); - handleClose(); - clearValues(); - }; - - const disableAdd = () => { - const { type, name, options, collectionPath, config } = values; - if (!type || name === "") return true; - //TODO: Add more validation - return false; - }; - - if (column) - return ( - - - - - - {flags.includes("editable") ? : } - - - {/* - - {flags.includes("visible") ? ( - - ) : ( - - )} - - */} - - - - - - - - - - - - - - { - setValue("name", e.target.value); - }} - /> - - {FieldsDropdown(values.type, handleChange)} - - {(values.type === FieldType.singleSelect || - values.type === FieldType.multiSelect) && ( - - )} - {values.type === FieldType.connectTable && ( - - )} - {values.type === FieldType.subTable && ( - { - setValue("parentLabel", e.target.value); - }} - /> - )} - {values.type === FieldType.action && ( - { - setValue("callableName", e.target.value); - }} - /> - )} - - - {column.isNew ? ( - - ) : ( - - )} - - - {!column.isNew && ( - - - - - - )} - - - - - ); - - return null; -} diff --git a/www/src/components/Table/Filters/index.tsx b/www/src/components/Table/Filters/index.tsx index c941d741..914512a7 100644 --- a/www/src/components/Table/Filters/index.tsx +++ b/www/src/components/Table/Filters/index.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import _find from "lodash/find"; +import _sortBy from "lodash/sortBy"; import { makeStyles, @@ -8,12 +9,11 @@ import { Button, Typography, IconButton, - FormControl, Grid, - Select, MenuItem, TextField, Switch, + Chip, } from "@material-ui/core"; import FilterIcon from "@material-ui/icons/FilterList"; import CloseIcon from "@material-ui/icons/Close"; @@ -25,6 +25,8 @@ import { FireTableFilter } from "hooks/useFiretable"; import { useFiretableContext } from "contexts/firetableContext"; import DocSelector from "./DocSelector"; +import { useAppContext } from "contexts/appContext"; +import { DocActions } from "hooks/useDoc"; const OPERATORS = [ { value: "==", @@ -94,10 +96,30 @@ const useStyles = makeStyles(theme => }) ); -const Filters = ({ columns, setFilters }: any) => { - const { userClaims } = useFiretableContext(); - const filterColumns = columns - .filter(c => c.type !== FieldType.file && c.type !== FieldType.image) +const UNFILTERABLES = [ + FieldType.image, + FieldType.file, + FieldType.action, + FieldType.subTable, + FieldType.last, + FieldType.longText, +]; +const Filters = () => { + const { userClaims, tableState, tableActions } = useFiretableContext(); + const { userDoc } = useAppContext(); + + useEffect(() => { + if (userDoc.state.doc && tableState?.tablePath) { + if (userDoc.state.doc.tables?.[tableState?.tablePath]?.filters) { + console.log(userDoc.state.doc.tables[tableState?.tablePath].filters); + tableActions?.table.filter( + userDoc.state.doc.tables[tableState?.tablePath].filters + ); + } + } + }, [userDoc.state, tableState?.tablePath]); + const filterColumns = _sortBy(Object.values(tableState!.columns), "index") + .filter(c => !UNFILTERABLES.includes(c.type)) .map(c => ({ key: c.key, label: c.name, @@ -212,7 +234,11 @@ const Filters = ({ columns, setFilters }: any) => { multiple freeText={false} onChange={value => setQuery(query => ({ ...query, value }))} - options={selectedColumn.config.options ?? []} + options={ + selectedColumn.config.options + ? selectedColumn.config.options.sort() + : [] + } label="" value={Array.isArray(query?.value) ? query.value : []} TextFieldProps={{ hiddenLabel: true }} @@ -226,7 +252,11 @@ const Filters = ({ columns, setFilters }: any) => { onChange={value => { if (value !== null) setQuery(query => ({ ...query, value })); }} - options={selectedColumn.config.options ?? []} + options={ + selectedColumn.config.options + ? selectedColumn.config.options.sort() + : [] + } label="" value={typeof query?.value === "string" ? query.value : null} TextFieldProps={{ hiddenLabel: true }} @@ -239,7 +269,11 @@ const Filters = ({ columns, setFilters }: any) => { multiple onChange={value => setQuery(query => ({ ...query, value }))} value={query.value as string[]} - options={selectedColumn.config.options} + options={ + selectedColumn.config.options + ? selectedColumn.config.options.sort() + : [] + } label={""} searchable={false} freeText={true} @@ -273,19 +307,38 @@ const Filters = ({ columns, setFilters }: any) => { break; } }; + + const handleUpdateFilters = (filters: FireTableFilter[]) => { + userDoc.dispatch({ + action: DocActions.update, + data: { + tables: { [`${tableState?.tablePath}`]: { filters } }, + }, + }); + }; return ( <> - - + + + {(tableState?.filters ?? []).map(filter => ( + { + handleUpdateFilters([]); + }} + /> + ))} + {