mirror of
https://github.com/rowyio/rowy.git
synced 2026-07-12 21:39:28 +02:00
Merge pull request #147 from AntlerVC/develop
persistent personal filters, update cloudfunctions
This commit is contained in:
@@ -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) },
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
58
cloud_functions/functions/src/derivatives/index.ts
Normal file
58
cloud_functions/functions/src/derivatives/index.ts
Normal file
@@ -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<FirebaseFirestore.DocumentSnapshot>
|
||||
) => {
|
||||
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),
|
||||
},
|
||||
};
|
||||
2
cloud_functions/functions/src/functionConfig.ts
Normal file
2
cloud_functions/functions/src/functionConfig.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const collectionPath = "teams";
|
||||
export default [];
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<FirebaseFirestore.DocumentSnapshot>
|
||||
) => {
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -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<FirebaseFirestore.DocumentSnapshot>) =>
|
||||
permissions(collection.customTokenFields)(change.after)
|
||||
),
|
||||
onSignup: FCAuth.user().onCreate(
|
||||
onSignup(collection.name, collection.customTokenFields)
|
||||
),
|
||||
});
|
||||
|
||||
export default permissionsFnsGenerator;
|
||||
@@ -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
|
||||
),
|
||||
};
|
||||
|
||||
37
cloud_functions/functions/src/subTableStats.ts
Normal file
37
cloud_functions/functions/src/subTableStats.ts
Normal file
@@ -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 },
|
||||
};
|
||||
@@ -21,6 +21,7 @@ steps:
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "generateConfig"
|
||||
- "${_FUNCTIONS_GROUP}"
|
||||
- "${_FUNCTION_CONFIG}"
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
const [secondaryKeys, setSecondaryKeys] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
// <>
|
||||
// <FormControl className={classes.formControl}>
|
||||
// <InputLabel htmlFor="select-secondary-multiple-chip">
|
||||
// Table
|
||||
// </InputLabel>
|
||||
// <Select
|
||||
// value={collectionPath ? collectionPath : null}
|
||||
// onChange={onChange}
|
||||
// id={`select-key`}
|
||||
// inputProps={{
|
||||
// name: "Table",
|
||||
// id: "table",
|
||||
// }}
|
||||
// >
|
||||
// {tables.value.map(table => {
|
||||
// return (
|
||||
// <MenuItem
|
||||
// id={`select-collection-${table.collection}`}
|
||||
// value={table.collection}
|
||||
// >
|
||||
// <>{table.collection}</>
|
||||
// </MenuItem>
|
||||
// );
|
||||
// })}
|
||||
// </Select>
|
||||
// </FormControl>
|
||||
// {collectionPath ? (
|
||||
// <>
|
||||
// <FormControl className={classes.formControl}>
|
||||
// <InputLabel htmlFor="select-primary-multiple-chip">
|
||||
// Primary Text
|
||||
// </InputLabel>
|
||||
// <Select
|
||||
// multiple
|
||||
// value={primaryKeys}
|
||||
// onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
// setPrimaryKeys(event.target.value as string[]);
|
||||
// }}
|
||||
// input={<Input id="select-primary-multiple-chip" />}
|
||||
// renderValue={selected => (
|
||||
// <div className={classes.chips}>
|
||||
// {(selected as string[]).map(value => (
|
||||
// <Chip
|
||||
// key={value}
|
||||
// label={value}
|
||||
// className={classes.chip}
|
||||
// />
|
||||
// ))}
|
||||
// </div>
|
||||
// )}
|
||||
// MenuProps={MenuProps}
|
||||
// >
|
||||
// {columns &&
|
||||
// columns.length !== 0 &&
|
||||
// columns.map(column => (
|
||||
// <MenuItem
|
||||
// id={`select-primary-column-${column.key}`}
|
||||
// key={column.key}
|
||||
// value={column.key}
|
||||
// >
|
||||
// {column.name}
|
||||
// </MenuItem>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </FormControl>
|
||||
|
||||
// <FormControl className={classes.formControl}>
|
||||
// <InputLabel htmlFor="select-secondary-multiple-chip">
|
||||
// Secondary Text
|
||||
// </InputLabel>
|
||||
// <Select
|
||||
// multiple
|
||||
// value={secondaryKeys}
|
||||
// onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
// setSecondaryKeys(event.target.value as string[]);
|
||||
// }}
|
||||
// input={<Input id="select-secondary-multiple-chip" />}
|
||||
// renderValue={selected => (
|
||||
// <div className={classes.chips}>
|
||||
// {(selected as string[]).map(value => (
|
||||
// <Chip
|
||||
// key={value}
|
||||
// label={value}
|
||||
// className={classes.chip}
|
||||
// />
|
||||
// ))}
|
||||
// </div>
|
||||
// )}
|
||||
// MenuProps={MenuProps}
|
||||
// >
|
||||
// {columns &&
|
||||
// columns.length !== 0 &&
|
||||
// columns.map(column => (
|
||||
// <MenuItem
|
||||
// id={`select-secondary-column-${column.key}`}
|
||||
// key={column.key}
|
||||
// value={column.key}
|
||||
// >
|
||||
// {column.name}
|
||||
// </MenuItem>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </FormControl>
|
||||
// </>
|
||||
// ) : (
|
||||
// "please select a table"
|
||||
// )}
|
||||
// </>
|
||||
// );
|
||||
// else
|
||||
return <div />;
|
||||
}
|
||||
@@ -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 (
|
||||
<TextField
|
||||
select
|
||||
value={value ? value : ""}
|
||||
onChange={onChange}
|
||||
inputProps={{ name: "type", id: "type" }}
|
||||
label="Field type"
|
||||
>
|
||||
{FIELDS.map(
|
||||
(field: { icon: JSX.Element; name: string; type: FieldType }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={`select-field-${field.name}`}
|
||||
id={`select-field-${field.type}`}
|
||||
value={field.type}
|
||||
>
|
||||
<ListItemIcon style={{ verticalAlign: "text-bottom" }}>
|
||||
{field.icon}
|
||||
</ListItemIcon>
|
||||
{field.name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldsDropdown;
|
||||
@@ -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 (
|
||||
<Grid container direction="column" className={classes.root}>
|
||||
<Grid item>
|
||||
<TextField
|
||||
value={newOption}
|
||||
className={classes.field}
|
||||
label="New Option"
|
||||
onChange={e => {
|
||||
setNewOption(e.target.value);
|
||||
}}
|
||||
onKeyPress={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
handleAdd();
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="toggle password visibility"
|
||||
onClick={(e: any) => {
|
||||
handleAdd();
|
||||
}}
|
||||
>
|
||||
{<AddIcon />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item className={classes.chipsContainer}>
|
||||
{options.map((option: string) => {
|
||||
return (
|
||||
<Chip
|
||||
key={option}
|
||||
label={option}
|
||||
onDelete={handleDelete(option)}
|
||||
className={classes.chip}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLElement>,
|
||||
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 (
|
||||
<Popover
|
||||
className={classes.root}
|
||||
id={`id-${column.name}`}
|
||||
open={!!anchorEl}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
|
||||
transformOrigin={{ horizontal: "center", vertical: "top" }}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Grid container className={classes.container} direction="column">
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
value={flags}
|
||||
className={classes.toggleGrouped}
|
||||
onChange={handleToggle}
|
||||
arial-label="column settings"
|
||||
>
|
||||
<ToggleButton value="editable" aria-label="editable">
|
||||
<Tooltip title={flags.includes("editable") ? "Lock" : "Unlock"}>
|
||||
{flags.includes("editable") ? <LockOpenIcon /> : <LockIcon />}
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
{/* <Tooltip title="Hide Column">
|
||||
<ToggleButton value="visible" aria-label="visible">
|
||||
{flags.includes("visible") ? (
|
||||
<VisibilityIcon />
|
||||
) : (
|
||||
<VisibilityOffIcon />
|
||||
)}
|
||||
</ToggleButton>
|
||||
</Tooltip> */}
|
||||
<ToggleButton value="fixed" aria-label="fixed">
|
||||
<Tooltip
|
||||
title={
|
||||
flags.includes("fixed") ? "Unfreeze Column" : "Freeze Column"
|
||||
}
|
||||
>
|
||||
<FrozenIcon />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
|
||||
<ToggleButton
|
||||
value="resizable"
|
||||
aria-label="resizable"
|
||||
disabled={flags.includes("fixed")}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
flags.includes("resizable")
|
||||
? "Disable column resize"
|
||||
: "Enable column resize"
|
||||
}
|
||||
>
|
||||
<ResizeIcon />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<TextField
|
||||
label="Column name"
|
||||
name="name"
|
||||
defaultValue={values.name}
|
||||
onChange={e => {
|
||||
setValue("name", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<FormControl className={classes.formControl}>
|
||||
{FieldsDropdown(values.type, handleChange)}
|
||||
|
||||
{(values.type === FieldType.singleSelect ||
|
||||
values.type === FieldType.multiSelect) && (
|
||||
<SelectOptionsInput
|
||||
setValue={setValue}
|
||||
options={values.options}
|
||||
/>
|
||||
)}
|
||||
{values.type === FieldType.connectTable && (
|
||||
<DocInput
|
||||
setValue={setValue}
|
||||
collectionPath={values.collectionPath}
|
||||
/>
|
||||
)}
|
||||
{values.type === FieldType.subTable && (
|
||||
<TextField
|
||||
label={"Parent Label"}
|
||||
onChange={e => {
|
||||
setValue("parentLabel", e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{values.type === FieldType.action && (
|
||||
<TextField
|
||||
label={"Callable Name"}
|
||||
onChange={e => {
|
||||
setValue("callableName", e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
{column.isNew ? (
|
||||
<Button
|
||||
onClick={createNewColumn}
|
||||
disabled={disableAdd()}
|
||||
fullWidth
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={disableAdd()}
|
||||
onClick={updateColumn}
|
||||
fullWidth
|
||||
>
|
||||
update
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{!column.isNew && (
|
||||
<Grid item xs={6}>
|
||||
<Confirmation
|
||||
message={{
|
||||
customBody:
|
||||
"Are you sure you want to delete this nice column?",
|
||||
}}
|
||||
>
|
||||
<Button onClick={deleteColumn} fullWidth>
|
||||
Delete
|
||||
</Button>
|
||||
</Confirmation>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
startIcon={<FilterIcon />}
|
||||
>
|
||||
{filters.length !== 0 && filters.length}
|
||||
{" Filter"}
|
||||
{filters.length > 1 && "s"}
|
||||
</Button>
|
||||
|
||||
<Grid container direction="row">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
startIcon={<FilterIcon />}
|
||||
>
|
||||
{filters.length !== 0 && filters.length}
|
||||
{" Filter"}
|
||||
{filters.length > 1 && "s"}
|
||||
</Button>
|
||||
{(tableState?.filters ?? []).map(filter => (
|
||||
<Chip
|
||||
key={filter.key}
|
||||
label={`${filter.key} ${filter.operator} ${filter.value}`}
|
||||
onDelete={() => {
|
||||
handleUpdateFilters([]);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
@@ -404,7 +457,7 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
<Button
|
||||
disabled={query.key === ""}
|
||||
onClick={() => {
|
||||
setFilters([]);
|
||||
handleUpdateFilters([]);
|
||||
setQuery({
|
||||
key: "",
|
||||
operator: "",
|
||||
@@ -420,7 +473,7 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
disabled={query.value === null || query.value === undefined}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setFilters([query]);
|
||||
handleUpdateFilters([query]);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
MenuItem,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
@@ -16,7 +15,6 @@ import Filters from "./Filters";
|
||||
import ImportCSV from "./ImportCSV";
|
||||
import ExportCSV from "./ExportCSV";
|
||||
|
||||
import { FireTableFilter } from "hooks/useFiretable";
|
||||
import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
@@ -59,7 +57,6 @@ const useStyles = makeStyles(theme =>
|
||||
interface ITableHeaderProps {
|
||||
rowHeight: number;
|
||||
updateConfig: Function;
|
||||
filters: FireTableFilter[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,8 +65,6 @@ interface ITableHeaderProps {
|
||||
export default function TableHeader({
|
||||
rowHeight,
|
||||
updateConfig,
|
||||
|
||||
filters,
|
||||
}: ITableHeaderProps) {
|
||||
const classes = useStyles();
|
||||
const { tableActions, tableState } = useFiretableContext();
|
||||
@@ -111,11 +106,7 @@ export default function TableHeader({
|
||||
<Grid item />
|
||||
|
||||
<Grid item>
|
||||
<Filters
|
||||
columns={tempColumns}
|
||||
tableFilters={filters}
|
||||
setFilters={tableActions?.table.filter}
|
||||
/>
|
||||
<Filters />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs className={classes.spacer} />
|
||||
@@ -159,11 +150,9 @@ export default function TableHeader({
|
||||
|
||||
<Grid item />
|
||||
|
||||
{/* {userClaims && userClaims.roles?.includes("ADMIN") && ( */}
|
||||
<Grid item>
|
||||
<ImportCSV />
|
||||
</Grid>
|
||||
{/* )} */}
|
||||
|
||||
<Grid item>
|
||||
<ExportCSV />
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { makeStyles, createStyles, Grid } from "@material-ui/core";
|
||||
import { makeStyles, createStyles, Grid, Tooltip } from "@material-ui/core";
|
||||
|
||||
import MultiSelect_ from "@antlerengineering/multiselect";
|
||||
import FormattedChip, { VARIANTS } from "components/FormattedChip";
|
||||
@@ -94,41 +94,62 @@ export default function MultiSelect({
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelect_
|
||||
value={
|
||||
value === undefined || value === null ? (isSingle ? null : []) : value
|
||||
<Tooltip
|
||||
title={
|
||||
value
|
||||
? Array.isArray(value)
|
||||
? value.length > 1
|
||||
? value.join(", ")
|
||||
: ``
|
||||
: value
|
||||
: ``
|
||||
}
|
||||
onChange={onSubmit}
|
||||
freeText={!isSingle && config.freeText}
|
||||
multiple={!isSingle as any}
|
||||
label={column.name}
|
||||
labelPlural={column.name}
|
||||
options={config.options ?? []}
|
||||
disabled={column.editable === false}
|
||||
onOpen={handleOpen}
|
||||
TextFieldProps={
|
||||
{
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
variant: "standard",
|
||||
className: classes.root,
|
||||
InputProps: {
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.inputBase },
|
||||
},
|
||||
SelectProps: {
|
||||
classes: {
|
||||
root: clsx(classes.root, classes.select),
|
||||
icon: classes.icon,
|
||||
},
|
||||
renderValue,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
/>
|
||||
enterDelay={100}
|
||||
interactive
|
||||
placement="bottom-start"
|
||||
>
|
||||
<div>
|
||||
<MultiSelect_
|
||||
value={
|
||||
value === undefined || value === null
|
||||
? isSingle
|
||||
? null
|
||||
: []
|
||||
: value
|
||||
}
|
||||
onChange={onSubmit}
|
||||
freeText={!isSingle && config.freeText}
|
||||
multiple={!isSingle as any}
|
||||
label={column.name}
|
||||
labelPlural={column.name}
|
||||
options={config.options ?? []}
|
||||
disabled={column.editable === false}
|
||||
onOpen={handleOpen}
|
||||
TextFieldProps={
|
||||
{
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
variant: "standard",
|
||||
className: classes.root,
|
||||
InputProps: {
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.inputBase },
|
||||
},
|
||||
SelectProps: {
|
||||
classes: {
|
||||
root: clsx(classes.root, classes.select),
|
||||
icon: classes.icon,
|
||||
},
|
||||
renderValue,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function SubTable({ column, row }: CustomCellProps) {
|
||||
}, "")
|
||||
: "";
|
||||
const fieldName = column.key as string;
|
||||
const documentCount = row[fieldName]?.count ?? "";
|
||||
|
||||
const router = useRouter();
|
||||
const parentLabels = queryString.parse(router.location.search).parentLabel;
|
||||
@@ -54,7 +55,7 @@ export default function SubTable({ column, row }: CustomCellProps) {
|
||||
className={clsx("cell-collapse-padding", classes.root)}
|
||||
>
|
||||
<Grid item xs className={classes.labelContainer}>
|
||||
{column.name}: {label}
|
||||
{documentCount} {column.name}: {label}
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
key: "new",
|
||||
name: "Add column",
|
||||
type: FieldType.last,
|
||||
index: columns.length,
|
||||
index: columns.length ?? 0,
|
||||
width: 160,
|
||||
headerRenderer: FinalColumnHeader,
|
||||
cellClass: finalColumnClasses.cell,
|
||||
@@ -152,7 +152,6 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
<TableHeader
|
||||
rowHeight={rowHeight}
|
||||
updateConfig={tableActions.table.updateConfig}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
{!tableState.loadingColumns ? (
|
||||
|
||||
@@ -18,7 +18,6 @@ export type Table = {
|
||||
name: string;
|
||||
roles: string[];
|
||||
description: string;
|
||||
regional: boolean;
|
||||
section: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import equals from "ramda/es/equals";
|
||||
import firebase from "firebase/app";
|
||||
import { FireTableFilter, FiretableOrderBy } from ".";
|
||||
import { SnackContext } from "../../contexts/snackContext";
|
||||
|
||||
import { cloudFunction } from "../../firebase/callables";
|
||||
const CAP = 1000; // safety paramter sets the upper limit of number of docs fetched by this hook
|
||||
const serverTimestamp = firebase.firestore.FieldValue.serverTimestamp;
|
||||
var characters =
|
||||
@@ -140,12 +140,24 @@ const useTable = (initialOverrides: any) => {
|
||||
),
|
||||
});
|
||||
} else if (error.code === "permission-denied") {
|
||||
snack.open({
|
||||
position: { horizontal: "center", vertical: "top" },
|
||||
severity: "error",
|
||||
message: "You don't have permissions to see the results.",
|
||||
duration: 10000,
|
||||
});
|
||||
if (filters.length === 0) {
|
||||
cloudFunction(
|
||||
"callable-setFiretablePersonalizedFilter",
|
||||
{
|
||||
table: tableState.path,
|
||||
},
|
||||
resp => {
|
||||
console.log(resp);
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
} else
|
||||
snack.open({
|
||||
position: { horizontal: "center", vertical: "top" },
|
||||
severity: "error",
|
||||
message: "You don't have permissions to see the results.",
|
||||
duration: 10000,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -78,11 +78,6 @@ const useStyles = makeStyles(theme =>
|
||||
})
|
||||
);
|
||||
|
||||
const regionalFilter = (regional, userClaims) =>
|
||||
regional && userClaims?.regions && !userClaims?.regions?.includes("GL")
|
||||
? `?filters=%5B%7B%22key%22%3A%22region%22%2C%22operator%22%3A%22%3D%3D%22%2C%22value%22%3A%22${userClaims?.regions[0]}%22%7D%5D`
|
||||
: "";
|
||||
|
||||
const TablesView = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -142,10 +137,7 @@ const TablesView = () => {
|
||||
}
|
||||
bodyContent={table.description}
|
||||
primaryLink={{
|
||||
to: `${routes.table}/${table.collection}${regionalFilter(
|
||||
table?.regional ?? false,
|
||||
userClaims
|
||||
)}`,
|
||||
to: `${routes.table}/${table.collection}`,
|
||||
label: "Open",
|
||||
}}
|
||||
secondaryAction={
|
||||
|
||||
Reference in New Issue
Block a user