Merge pull request #207 from AntlerVC/develop

Develop
This commit is contained in:
AntlerEngineering
2020-09-21 14:24:01 +10:00
committed by GitHub
13 changed files with 239 additions and 101 deletions

View File

@@ -150,5 +150,5 @@ At [Antler](https://antler.co), we identify and invest in exceptional people.
Were a global startup generator and early-stage VC firm that builds
groundbreaking technology companies.
[Apply now](https://antler.co/apply) to be part of a global cohort of tech
[Apply now](https://www.antler.co/apply?utm_source=Firetable&utm_medium=website&utm_campaign=Thu%20Apr%2016%202020%2018:00:00%20GMT%2B0200%20(CEST)&utm_content=TechTracking) to be part of a global cohort of tech
founders.

View File

@@ -1329,11 +1329,6 @@ node-forge@^0.10.0:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
node-forge@^0.9.1:
version "0.9.2"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a"
integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==
npm-run-path@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"

View File

@@ -0,0 +1,81 @@
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import { db } from "../config";
import config, { collectionPath } from "../functionConfig";
// generated using generateConfig.ts
const incrementor = (v: number) => admin.firestore.FieldValue.increment(v);
const functionConfig: any = config;
const subDocTrigger = async (
change: functions.Change<functions.firestore.DocumentSnapshot>,
context: functions.EventContext
) => {
const beforeData = change.before?.data();
const afterData = change.after?.data();
const triggerType =
Boolean(beforeData) && Boolean(afterData)
? "update"
: Boolean(afterData)
? "create"
: "delete";
const parentDocRef = change.after
? change.after.ref.parent.parent
: change.before.ref.parent.parent;
const parentDoc = await parentDocRef?.get();
const parentDocData = parentDoc?.data();
console.log(
JSON.stringify({
parentDocRef,
parentDocData,
beforeData,
afterData,
triggerType,
})
);
//return false;
const aggregateData = await functionConfig.reduce(
async (accAggregate: any, currAggregate) => {
// check relavent sub-table
if (currAggregate.subtables.includes(context.params.subCollectionId)) {
const newValue = await currAggregate.eval(db)({
beforeData,
afterData,
incrementor,
triggerType,
});
if (newValue !== undefined) {
return {
...(await accAggregate),
...Object.keys(newValue).reduce((acc, curr) => {
return {
...acc,
[`${currAggregate.fieldName}.${curr}`]: newValue[curr],
};
}, {}),
};
} else return await accAggregate;
} else return await accAggregate;
},
{}
);
const update = Object.keys(aggregateData).reduce((acc: any, curr: string) => {
if (aggregateData[curr] !== undefined) {
return { ...acc, [curr]: aggregateData[curr] };
} else {
return acc;
}
}, {});
console.log({ update, aggregateData });
if (parentDocRef && Object.keys(update).length !== 0) {
return parentDocRef.update(update);
}
return false;
};
export const FT_aggregates = {
[collectionPath.replace("-", "_")]: functions.firestore
.document(`${collectionPath}/{parentId}/{subCollectionId}/{docId}`)
.onWrite(subDocTrigger),
};

View File

@@ -1,70 +1,45 @@
export default {
collectionPath: "feedback",
onCreate: true,
onUpdate: false,
onDelete: false,
requiredFields: ["email", "message", "type", "user", "source", "pagePath"],
messageDocGenerator: (snapshot) => {
const docData = snapshot.data();
const { email, message, type, user, source, pagePath } = docData;
const { displayName } = user;
console.log({ displayName, email, message, type, user, source, pagePath });
// General/Bug/Idea
const generalText =
`Hey!, ` +
displayName +
"(" +
email +
")" +
` has the following general feedback for (` +
source.split(".")[0] +
`):*` +
message +
`* `;
const bugText =
`Bug report: *` +
message +
`*\n by *` +
displayName +
"(" +
email +
")*\n" +
"reported on:" +
source +
pagePath;
const ideaText =
displayName +
"(" +
email +
")" +
"has an amazing idea! for " +
source.split(".")[0] +
" \n messaged saying:" +
message;
let text = "blank";
switch (type) {
case "Bug":
text = bugText;
break;
case "Idea":
text = ideaText;
break;
case "General":
text = generalText;
break;
default:
text = "unknown feedback type";
}
if (source.includes("education")) {
return {
text,
emails: ["heath@antler.co", "harini@antler.co", "shams@antler.co"],
};
} else {
return { text, channel: "C01561T4AMB" };
}
export default [
{
fieldName: "reviewsSummary",
eval: (db) => async ({
aggregateState,
incrementor,
triggerType,
change,
afterData,
beforeData,
}) => {
//triggerType: create | update | delete
//aggregateState: the subtable accumenlator stored in the cell of this column
//change: the triggered document change snapshot of the the subcollection
//afterData
//beforeData
//incrementor: short for firebase.firestore.FieldValue.increment(n);
//This script needs to return the new aggregateState cell value.
switch (triggerType) {
case "create":
const rating = afterData?.rating ?? 0;
return {
numberOfReviews: incrementor(1),
accumulatedStars: incrementor(rating),
};
case "update":
const prevRating = beforeData?.rating ?? 0;
const newRating = afterData?.rating ?? 0;
return {
accumulatedStars: incrementor(newRating - prevRating),
};
case "delete":
const removeStars = -(beforeData?.rating ?? 0);
return {
numberOfReviews: incrementor(-1),
accumulatedStars: incrementor(removeStars),
};
default:
return {};
}
},
subtables: ["reviews"],
},
} as any;
export const collectionPath = "";
];
export const collectionPath = "mvpResources";

View File

@@ -34,8 +34,6 @@ if (serviceAccount) {
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) =>{${
@@ -48,6 +46,28 @@ if (serviceAccount) {
configData = `export default [${config}]\nexport const collectionPath ="${configString}"`;
break;
case "FT_aggregates":
const _schemaDoc = await db
.doc(`_FIRETABLE_/settings/schema/${configString}`)
.get();
const _schemaData = _schemaDoc.data();
if (!_schemaData) return;
const aggregateColumns = Object.values(_schemaData.columns).filter(
(col: any) => col.type === "AGGREGATE"
);
const _config = aggregateColumns.reduce((acc, currColumn: any) => {
return `${acc}{
fieldName:'${
currColumn.key
}',eval:(db)=> async ({aggregateState,incrementor,triggerType,change,afterData,beforeData}) =>{${
currColumn.config.script
}},subtables:[${currColumn.config.subtables
.map((t) => `"${t}"`)
.join(",")}]},`;
}, ``);
configData = `export default [${_config}]\nexport const collectionPath ="${configString}"`;
break;
case "FT_subTableStats":
configData = `export const collectionPath ="${configString}"\nexport default []`;
break;

View File

@@ -9,6 +9,7 @@ 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
export { FT_derivatives } from "./derivatives";
export { FT_aggregates } from "./aggregates";
export { FT_algolia } from "./algolia";
export { FT_sync } from "./collectionSync";
export { FT_snapshotSync } from "./snapshotSync";

View File

@@ -4639,15 +4639,6 @@ node-forge@^0.10.0:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
node-forge@^0.7.6:
version "0.7.6"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
node-forge@^0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a"
integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==
noop-logger@^0.1.1:
version "0.1.1"

View File

@@ -145,7 +145,7 @@ export default function Form({ fields, values }: IFormProps) {
const { type, ...fieldProps } = field;
let _type = type;
// Derivative field support
// Derivative/aggregate field support
if (field.config && field.config.renderFieldType) {
_type = field.config.renderFieldType;
}

View File

@@ -95,6 +95,7 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
label="filter template"
name="filters"
fullWidth
value={config.filters}
onChange={(e) => {
handleChange("filters")(e.target.value);
}}
@@ -271,6 +272,73 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
)}
</>
);
case FieldType.aggregate:
return (
<>
<ColumnSelector
label={"Sub Tables"}
validTypes={[FieldType.subTable]}
value={config.subtables}
tableColumns={
columns
? Array.isArray(columns)
? columns
: Object.values(columns)
: []
}
handleChange={handleChange("subtables")}
/>
<Typography variant="overline">Aggergate script</Typography>
<Suspense fallback={<FieldSkeleton height={200} />}>
<CodeEditor
script={
config.script ??
`//triggerType: create | update | delete\n//aggregateState: the subtable accumenlator stored in the cell of this column\n//snapshot: the triggered document snapshot of the the subcollection\n//incrementor: short for firebase.firestore.FieldValue.increment(n);\n//This script needs to return the new aggregateState cell value.
switch (triggerType){
case "create":return {
count:incrementor(1)
}
case "update":return {}
case "delete":
return {
count:incrementor(-1)
}
}`
}
extraLibs={[
` /**
* increaments firestore field value
*/",
function incrementor(value:number):number {
}`,
]}
handleChange={handleChange("script")}
/>
</Suspense>
<Typography variant="overline">Field type of the output</Typography>
<FieldsDropdown
value={config.renderFieldType}
onChange={(newType: any) => {
handleChange("renderFieldType")(newType.target.value);
}}
/>
{config.renderFieldType && (
<>
<Typography variant="overline">Rendered field config</Typography>
<ConfigFields
fieldType={config.renderFieldType}
config={config}
handleChange={handleChange}
tables={tables}
columns={columns}
/>
</>
)}
</>
);
case FieldType.derivative:
return (
<>

View File

@@ -7,10 +7,11 @@ import { setTimeout } from "timers";
const useStyles = makeStyles((theme) =>
createStyles({
editorWrapper: { position: "relative", minWidth: 800 },
editorWrapper: { position: "relative", minWidth: 800, minHeight: 300 },
editor: {
border: `1px solid ${theme.palette.divider}`,
minHeight: 300,
borderRadius: theme.shape.borderRadius,
resize: "both",
fontFamily: "SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace",
@@ -31,7 +32,7 @@ const useStyles = makeStyles((theme) =>
);
export default function CodeEditor(props: any) {
const { handleChange, script, height = "90hv" } = props;
const { handleChange, extraLibs, script, height = 400 } = props;
const [initialEditorValue] = useState(script ?? "");
const { tableState } = useFiretableContext();
@@ -84,7 +85,11 @@ export default function CodeEditor(props: any) {
allowNonTsExtensions: true,
}
);
if (extraLibs) {
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
extraLibs.join("\n")
);
}
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
[
" /**",
@@ -148,7 +153,6 @@ export default function CodeEditor(props: any) {
);
listenEditorChanges();
}, [tableState?.columns]);
return (
<>
<div className={classes.editorWrapper}>

View File

@@ -48,7 +48,6 @@ class TextEditor extends React.Component<
getValue() {
if ((this.props.column as any).type === FieldType.number)
return Number(this.inputRef?.current?.value);
return this.inputRef?.current?.value;
}

View File

@@ -32,6 +32,7 @@ import ActionIcon from "assets/icons/Action";
import JsonIcon from "assets/icons/Json";
import CodeIcon from "@material-ui/icons/Code";
import DerivativeIcon from "assets/icons/Derivative";
import AggregateIcon from "@material-ui/icons/Layers";
import RichTextIcon from "@material-ui/icons/TextFormat";
import ColorIcon from "@material-ui/icons/Colorize";
@@ -61,6 +62,7 @@ export {
JsonIcon,
CodeIcon,
DerivativeIcon,
AggregateIcon,
RichTextIcon,
ColorIcon,
SliderIcon,
@@ -97,6 +99,7 @@ export enum FieldType {
json = "JSON",
code = "CODE",
derivative = "DERIVATIVE",
aggregate = "AGGREGATE",
richText = "RICH_TEXT",
color = "COLOR",
@@ -148,10 +151,12 @@ export const FIELDS = [
type: FieldType.subTable,
},
{ icon: <ActionIcon />, name: "Action", type: FieldType.action },
{ icon: <JsonIcon />, name: "JSON", type: FieldType.json },
{ icon: <CodeIcon />, name: "Code", type: FieldType.code },
{ icon: <ActionIcon />, name: "Action", type: FieldType.action },
{ icon: <DerivativeIcon />, name: "Derivative", type: FieldType.derivative },
{ icon: <AggregateIcon />, name: "Aggregate", type: FieldType.aggregate },
{ icon: <RichTextIcon />, name: "Rich Text", type: FieldType.richText },
{ icon: <ColorIcon />, name: "Color", type: FieldType.color },
@@ -178,7 +183,8 @@ export const FIELD_TYPE_DESCRIPTIONS = {
[FieldType.duration]: "Duration calculated from two timestamps.",
[FieldType.url]: "Web address. Firetable does not validate URLs.",
[FieldType.rating]: "Rating displayed as stars from 0 to 4.",
[FieldType.rating]:
"Rating displayed as stars from 0 to configurable number of stars(5 by default).",
[FieldType.image]:
"Image file uploaded to Firebase Storage. Supports JPEG, PNG, SVG, GIF, WebP.",
@@ -201,6 +207,8 @@ export const FIELD_TYPE_DESCRIPTIONS = {
[FieldType.code]: "Raw code editable with Monaco Editor.",
[FieldType.derivative]:
"Value derived from the rest of the rows values. Displayed using any other field type. Requires Cloud Function setup.",
[FieldType.aggregate]:
"Value aggregated from a specified subcollection of the row. Displayed using any other field type. Requires Cloud Function setup.",
[FieldType.richText]: "Rich text editor with predefined HTML text styles.",
[FieldType.color]: "Visual color picker. Supports Hex, RGBA, HSLA.",

View File

@@ -9964,14 +9964,10 @@ node-fetch@^2.3.0, node-fetch@^2.6.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
node-forge@^0.9.0:
version "0.9.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.1.0.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
node-int64@^0.4.0: