mirror of
https://github.com/rowyio/rowy.git
synced 2026-02-24 20:20:05 +01:00
Merge branch 'develop' into feature/import-wizard
* develop: actionscript callable updated add db ref for onCreate email trigger debug email trigger recipient add db ref in emailOnTrigger action script configuration slack message document trigger Duration cell mvp console log slack message slackOnTrigger wrap FT_email collectionPath refactor send email cloud function email on trigger cloud function
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"@google-cloud/firestore": "^3.7.5",
|
||||
"@google-cloud/storage": "^5.1.2",
|
||||
"@sendgrid/mail": "^7.2.3",
|
||||
"@slack/web-api": "^5.11.0",
|
||||
"algoliasearch": "^3.35.1",
|
||||
"firebase-admin": "^8.9.2",
|
||||
"firebase-functions": "^3.3.0",
|
||||
|
||||
@@ -28,7 +28,7 @@ const missingFieldsReducer = (data: any) => (acc: string[], curr: string) => {
|
||||
} else return acc;
|
||||
};
|
||||
|
||||
const generateSchemaDocPath = tablePath => {
|
||||
const generateSchemaDocPath = (tablePath) => {
|
||||
const pathComponents = tablePath.split("/");
|
||||
return `_FIRETABLE_/settings/${
|
||||
pathComponents[1] === "table" ? "schema" : "groupSchema"
|
||||
@@ -43,7 +43,7 @@ export const actionScript = functions.https.onCall(
|
||||
throw Error(`You are unauthenticated`);
|
||||
}
|
||||
|
||||
const { ref, row, column } = data;
|
||||
const { ref, row, column, action } = data;
|
||||
|
||||
const schemaDocPath = generateSchemaDocPath(ref.tablePath);
|
||||
const schemaDoc = await db.doc(schemaDocPath).get();
|
||||
@@ -51,13 +51,16 @@ export const actionScript = functions.https.onCall(
|
||||
if (!schemaDocData) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
message: "no schema found",
|
||||
};
|
||||
}
|
||||
const { script, requiredRoles, requiredFields } = schemaDocData.columns[
|
||||
column.key
|
||||
].config;
|
||||
const {
|
||||
script,
|
||||
requiredRoles,
|
||||
requiredFields,
|
||||
undo,
|
||||
redo,
|
||||
} = schemaDocData.columns[column.key].config;
|
||||
if (!hasAnyRole(requiredRoles, context)) {
|
||||
throw Error(`You don't have the required roles permissions`);
|
||||
}
|
||||
@@ -89,24 +92,34 @@ export const actionScript = functions.https.onCall(
|
||||
message: string;
|
||||
status: string;
|
||||
success: boolean;
|
||||
} = await eval(`async({db, auth, utilFns})=>{${script}}`)({
|
||||
} = await eval(
|
||||
`async({db, auth, utilFns})=>{${
|
||||
action === "undo" ? undo.script : script
|
||||
}}`
|
||||
)({
|
||||
db,
|
||||
auth,
|
||||
utilFns,
|
||||
});
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
cellValue: {
|
||||
redo: true,
|
||||
status: result.status,
|
||||
completedAt: serverTimestamp(),
|
||||
meta: { ranBy: context.auth!.token.email },
|
||||
undo: false,
|
||||
},
|
||||
undo: false,
|
||||
redo: false,
|
||||
};
|
||||
if (result.success)
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
cellValue: {
|
||||
redo: redo.enabled,
|
||||
status: result.status,
|
||||
completedAt: serverTimestamp(),
|
||||
meta: { ranBy: context.auth!.token.email },
|
||||
undo: undo.enabled,
|
||||
},
|
||||
undo: undo.enabled,
|
||||
redo: redo.enabled,
|
||||
};
|
||||
else
|
||||
return {
|
||||
success: false,
|
||||
message: result.message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -87,7 +87,7 @@ export const cloudBuildUpdates = functions.pubsub
|
||||
|
||||
if (query.docs.length !== 0) {
|
||||
const update = { status };
|
||||
if (status === "SUCCESS") {
|
||||
if (status === "SUCCESS" || status === "FAILURE") {
|
||||
update["buildDuration.end"] = serverTimestamp();
|
||||
}
|
||||
await query.docs[0].ref.update(update);
|
||||
|
||||
128
cloud_functions/functions/src/emailOnTrigger/index.ts
Normal file
128
cloud_functions/functions/src/emailOnTrigger/index.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { firestore } from "firebase-functions";
|
||||
|
||||
import { sendEmail } from "../utils/email";
|
||||
import { hasRequiredFields } from "../utils";
|
||||
import { db } from "../config";
|
||||
import _config from "../functionConfig"; // generated using generateConfig.ts
|
||||
const functionConfig: any = _config;
|
||||
type EmailOnTriggerConfig = {
|
||||
collectionPath: string;
|
||||
templateId: string;
|
||||
categories: string[];
|
||||
onCreate: Boolean;
|
||||
from: Function;
|
||||
to: Function;
|
||||
requiredFields: string[];
|
||||
shouldSend: (
|
||||
snapshot:
|
||||
| firestore.DocumentSnapshot
|
||||
| {
|
||||
before: firestore.DocumentSnapshot;
|
||||
after: firestore.DocumentSnapshot;
|
||||
}
|
||||
) => Boolean;
|
||||
onUpdate: Boolean;
|
||||
};
|
||||
const emailOnCreate = (config: EmailOnTriggerConfig) =>
|
||||
firestore
|
||||
.document(`${config.collectionPath}/{docId}`)
|
||||
.onCreate(async (snapshot) => {
|
||||
try {
|
||||
const snapshotData = snapshot.data();
|
||||
if (!snapshotData) throw Error("no snapshot data");
|
||||
|
||||
const shouldSend = config.shouldSend(snapshot);
|
||||
const hasAllRequiredFields = hasRequiredFields(
|
||||
config.requiredFields,
|
||||
snapshotData
|
||||
);
|
||||
const to = await config.to(snapshotData, db);
|
||||
const from = await config.from(snapshotData, db);
|
||||
console.log(JSON.stringify({ to, from }));
|
||||
if (shouldSend && hasAllRequiredFields) {
|
||||
const msg = {
|
||||
from,
|
||||
personalizations: [
|
||||
{
|
||||
to,
|
||||
dynamic_template_data: {
|
||||
...snapshotData,
|
||||
},
|
||||
},
|
||||
],
|
||||
template_id: config.templateId,
|
||||
categories: config.categories,
|
||||
attachments: snapshotData.attachments,
|
||||
};
|
||||
const resp = await sendEmail(msg);
|
||||
console.log(JSON.stringify(resp));
|
||||
return true;
|
||||
} else {
|
||||
console.log("requirements were not met");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(JSON.stringify(error.response.body));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const emailOnUpdate = (config: EmailOnTriggerConfig) =>
|
||||
firestore
|
||||
.document(`${config.collectionPath}/{docId}`)
|
||||
.onUpdate(async (change) => {
|
||||
try {
|
||||
const beforeData = change.before.data();
|
||||
const afterData = change.after.data();
|
||||
if (!beforeData || !afterData) throw Error("no data found in snapshot");
|
||||
const shouldSend = config.shouldSend(change);
|
||||
const hasAllRequiredFields = hasRequiredFields(
|
||||
config.requiredFields,
|
||||
afterData
|
||||
);
|
||||
const dynamic_template_data = config.requiredFields.reduce(
|
||||
(acc: any, curr: string) => {
|
||||
return { ...acc, [curr]: afterData[curr] };
|
||||
},
|
||||
{}
|
||||
);
|
||||
if (shouldSend && hasAllRequiredFields) {
|
||||
const from = await config.from(afterData, db);
|
||||
const to = await config.to(afterData, db);
|
||||
console.log(JSON.stringify({ to }));
|
||||
const msg = {
|
||||
from,
|
||||
personalizations: [
|
||||
{
|
||||
to,
|
||||
dynamic_template_data,
|
||||
},
|
||||
],
|
||||
template_id: config.templateId,
|
||||
categories: config.categories,
|
||||
};
|
||||
const resp = await sendEmail(msg);
|
||||
console.log(JSON.stringify(resp));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log("requirements were not met");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const emailOnTriggerFns = (config: EmailOnTriggerConfig) =>
|
||||
Object.entries({
|
||||
onCreate: config.onCreate ? emailOnCreate(config) : null,
|
||||
onUpdate: config.onUpdate ? emailOnUpdate(config) : null,
|
||||
}).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {});
|
||||
|
||||
export const FT_email = {
|
||||
[`${`${functionConfig.collectionPath}`
|
||||
.replace(/\//g, "_")
|
||||
.replace(/_{.*?}_/g, "_")}`]: emailOnTriggerFns(functionConfig),
|
||||
};
|
||||
@@ -1,2 +1,70 @@
|
||||
export default {} as any;
|
||||
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" };
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
export const collectionPath = "";
|
||||
|
||||
@@ -17,3 +17,6 @@ export { FT_subTableStats } from "./subTableStats";
|
||||
export { FT_compressedThumbnail } from "./compressedThumbnail";
|
||||
export { actionScript } from "./actionScript";
|
||||
export { webhook } from "./webhooks";
|
||||
export { FT_email } from "./emailOnTrigger";
|
||||
export { FT_slack } from "./slackOnTrigger";
|
||||
export { slackBotMessageOnCreate } from "./slackOnTrigger/trigger";
|
||||
|
||||
100
cloud_functions/functions/src/slackOnTrigger/index.ts
Normal file
100
cloud_functions/functions/src/slackOnTrigger/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { firestore } from "firebase-functions";
|
||||
import { hasRequiredFields, serverTimestamp } from "../utils";
|
||||
import _config from "../functionConfig"; // generated using generateConfig.ts
|
||||
import { db } from "../config";
|
||||
const functionConfig: any = _config;
|
||||
type SlackOnTriggerConfig = {
|
||||
collectionPath: string;
|
||||
onCreate: Boolean;
|
||||
onUpdate: Boolean;
|
||||
onDelete: Boolean;
|
||||
requiredFields: string[];
|
||||
messageDocGenerator: (
|
||||
snapshot:
|
||||
| firestore.DocumentSnapshot
|
||||
| {
|
||||
before: firestore.DocumentSnapshot;
|
||||
after: firestore.DocumentSnapshot;
|
||||
}
|
||||
// db: FirebaseFirestore.Firestore
|
||||
) => Boolean | any;
|
||||
};
|
||||
const slackOnCreate = (config: SlackOnTriggerConfig) =>
|
||||
firestore
|
||||
.document(`${config.collectionPath}/{docId}`)
|
||||
.onCreate(async (snapshot) => {
|
||||
try {
|
||||
const snapshotData = snapshot.data();
|
||||
if (!snapshotData) throw Error("no snapshot data");
|
||||
const hasAllRequiredFields = hasRequiredFields(
|
||||
config.requiredFields,
|
||||
snapshotData
|
||||
);
|
||||
if (hasAllRequiredFields) {
|
||||
const messageDoc = await config.messageDocGenerator(snapshot);
|
||||
if (messageDoc && typeof messageDoc === "object") {
|
||||
await db
|
||||
.collection("slackBotMessages")
|
||||
.add({ createdAt: serverTimestamp(), ...messageDoc });
|
||||
return true;
|
||||
} else {
|
||||
console.log("message is not sent");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log("requirements were not met");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(JSON.stringify(error.response.body));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const slackOnUpdate = (config: SlackOnTriggerConfig) =>
|
||||
firestore
|
||||
.document(`${config.collectionPath}/{docId}`)
|
||||
.onUpdate(async (change) => {
|
||||
try {
|
||||
const beforeData = change.before.data();
|
||||
const afterData = change.after.data();
|
||||
if (!beforeData || !afterData) throw Error("no data found in snapshot");
|
||||
const hasAllRequiredFields = hasRequiredFields(
|
||||
config.requiredFields,
|
||||
afterData
|
||||
);
|
||||
if (hasAllRequiredFields) {
|
||||
const messageDoc = await config.messageDocGenerator(change);
|
||||
console.log({ messageDoc });
|
||||
if (messageDoc && typeof messageDoc === "object") {
|
||||
console.log("creating slack message doc");
|
||||
|
||||
await db
|
||||
.collection("slackBotMessages")
|
||||
.add({ createdAt: serverTimestamp(), ...messageDoc });
|
||||
return true;
|
||||
} else {
|
||||
console.log("message is not sent");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log("requirements were not met");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const slackOnTriggerFns = (config: SlackOnTriggerConfig) =>
|
||||
Object.entries({
|
||||
onCreate: config.onCreate ? slackOnCreate(config) : null,
|
||||
onUpdate: config.onUpdate ? slackOnUpdate(config) : null,
|
||||
}).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {});
|
||||
|
||||
export const FT_slack = {
|
||||
[`${`${functionConfig.collectionPath}`
|
||||
.replace(/\//g, "_")
|
||||
.replace(/_{.*?}_/g, "_")}`]: slackOnTriggerFns(functionConfig),
|
||||
};
|
||||
87
cloud_functions/functions/src/slackOnTrigger/trigger.ts
Normal file
87
cloud_functions/functions/src/slackOnTrigger/trigger.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { firestore } from "firebase-functions";
|
||||
|
||||
import { env } from "../config";
|
||||
import { WebClient } from "@slack/web-api";
|
||||
import { asyncForEach, serverTimestamp } from "../utils";
|
||||
// Initialize
|
||||
const web = new WebClient(env.slackbot.token);
|
||||
|
||||
const messageByChannel = async ({
|
||||
text,
|
||||
channel,
|
||||
}: {
|
||||
channel: string;
|
||||
text: string;
|
||||
}) =>
|
||||
await web.chat.postMessage({
|
||||
text,
|
||||
channel,
|
||||
});
|
||||
const messageByEmail = async ({
|
||||
email,
|
||||
text,
|
||||
}: {
|
||||
email: string;
|
||||
text: string;
|
||||
}) => {
|
||||
try {
|
||||
const user = await web.users.lookupByEmail({ email });
|
||||
if (user.ok) {
|
||||
const channel = (user as any).user.id as string;
|
||||
console.log({ channel });
|
||||
return await messageByChannel({
|
||||
text,
|
||||
channel,
|
||||
});
|
||||
} else {
|
||||
return await false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`${error} maybe${email} is not on slack`);
|
||||
console.log(`${error}`);
|
||||
return await false;
|
||||
}
|
||||
};
|
||||
|
||||
export const slackBotMessageOnCreate = firestore
|
||||
.document(`slackBotMessages/{docId}`)
|
||||
.onCreate(async (snapshot) => {
|
||||
const docData = snapshot.data();
|
||||
if (!docData) {
|
||||
return snapshot.ref.update({
|
||||
delivered: false,
|
||||
error: "undefined doc",
|
||||
});
|
||||
}
|
||||
try {
|
||||
const channels = docData.channel ? [docData.channel] : docData.channels;
|
||||
const emails = docData.email ? [docData.email] : docData.emails;
|
||||
if (channels) {
|
||||
await asyncForEach(channels, async (channel: string) => {
|
||||
await messageByChannel({
|
||||
text: docData.text,
|
||||
channel,
|
||||
});
|
||||
});
|
||||
} else if (emails) {
|
||||
await asyncForEach(emails, async (email: string) => {
|
||||
await messageByEmail({
|
||||
text: docData.text,
|
||||
email,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return snapshot.ref.update({
|
||||
delivered: true,
|
||||
updatedAt: serverTimestamp(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return snapshot.ref.update({
|
||||
delivered: false,
|
||||
updatedAt: serverTimestamp(),
|
||||
error: JSON.stringify(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -9,3 +9,14 @@ export const replacer = (data: any) => (m: string, key: string) => {
|
||||
const defaultValue = key.split(":")[1] || "";
|
||||
return _.get(data, objKey, defaultValue);
|
||||
};
|
||||
|
||||
export const hasRequiredFields = (requiredFields: string[], data: any) =>
|
||||
requiredFields.reduce((acc: boolean, currField: string) => {
|
||||
if (data[currField] === undefined || data[currField] === null) return false;
|
||||
else return acc;
|
||||
}, true);
|
||||
export async function asyncForEach(array: any[], callback: Function) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +378,35 @@
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
|
||||
|
||||
"@slack/logger@>=1.0.0 <3.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-2.0.0.tgz#6a4e1c755849bc0f66dac08a8be54ce790ec0e6b"
|
||||
integrity sha512-OkIJpiU2fz6HOJujhlhfIGrc8hB4ibqtf7nnbJQDerG0BqwZCfmgtK5sWzZ0TkXVRBKD5MpLrTmCYyMxoMCgPw==
|
||||
dependencies:
|
||||
"@types/node" ">=8.9.0"
|
||||
|
||||
"@slack/types@^1.7.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.8.0.tgz#a5a0b31bace03f524174991dfc41c60311e6f32f"
|
||||
integrity sha512-YvLCtxqbIdCCI+xMQBFH3GJVhRp8jJNl8BUE0RgJlZcDF+wXSB1wkcgLz7zHtD3oOF39GedYiE1e/rQrZ4Dr1A==
|
||||
|
||||
"@slack/web-api@^5.11.0":
|
||||
version "5.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-5.11.0.tgz#6549ec71d13c2837cc672cbf7793a88eef22802f"
|
||||
integrity sha512-4a/uj7IZjFLu7Qmq0nH74ecLqk1iI/9x3yRS/v6M5vXDyc5lEruRFp4d5/bz4eN5Bathlq4Bws0wioY516fPag==
|
||||
dependencies:
|
||||
"@slack/logger" ">=1.0.0 <3.0.0"
|
||||
"@slack/types" "^1.7.0"
|
||||
"@types/is-stream" "^1.1.0"
|
||||
"@types/node" ">=8.9.0"
|
||||
"@types/p-queue" "^2.3.2"
|
||||
axios "^0.19.0"
|
||||
eventemitter3 "^3.1.0"
|
||||
form-data "^2.5.0"
|
||||
is-stream "^1.1.0"
|
||||
p-queue "^2.4.2"
|
||||
p-retry "^4.0.0"
|
||||
|
||||
"@szmarczak/http-timer@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
|
||||
@@ -477,6 +506,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/is-stream@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1"
|
||||
integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/json2csv@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/json2csv/-/json2csv-5.0.1.tgz#576d38515dfedeabf46eb85e790894b8df72ab40"
|
||||
@@ -509,6 +545,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c"
|
||||
integrity sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==
|
||||
|
||||
"@types/node@>=8.9.0":
|
||||
version "14.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f"
|
||||
integrity sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A==
|
||||
|
||||
"@types/node@^8.10.59":
|
||||
version "8.10.60"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.60.tgz#73eb4d1e1c8aa5dc724363b57db019cf28863ef7"
|
||||
@@ -519,6 +560,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
|
||||
|
||||
"@types/p-queue@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.2.tgz#16bc5fece69ef85efaf2bce8b13f3ebe39c5a1c8"
|
||||
integrity sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ==
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7"
|
||||
@@ -529,6 +575,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||
|
||||
"@types/retry@^0.12.0":
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
|
||||
@@ -853,7 +904,7 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
|
||||
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
|
||||
|
||||
axios@^0.19.2:
|
||||
axios@^0.19.0, axios@^0.19.2:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
@@ -2205,6 +2256,11 @@ event-target-shim@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
eventemitter3@^3.1.0:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
|
||||
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
|
||||
|
||||
events@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||
@@ -2650,6 +2706,15 @@ forever-agent@~0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||
|
||||
form-data@^2.5.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
@@ -4910,11 +4975,24 @@ p-pipe@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e"
|
||||
integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw==
|
||||
|
||||
p-queue@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-2.4.2.tgz#03609826682b743be9a22dba25051bd46724fc34"
|
||||
integrity sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==
|
||||
|
||||
p-reduce@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
|
||||
integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
|
||||
|
||||
p-retry@^4.0.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.2.0.tgz#ea9066c6b44f23cab4cd42f6147cdbbc6604da5d"
|
||||
integrity sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==
|
||||
dependencies:
|
||||
"@types/retry" "^0.12.0"
|
||||
retry "^0.12.0"
|
||||
|
||||
p-timeout@^1.1.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386"
|
||||
@@ -5537,6 +5615,11 @@ retry-request@^4.1.1:
|
||||
debug "^4.1.1"
|
||||
through2 "^3.0.1"
|
||||
|
||||
retry@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
|
||||
@@ -18,12 +18,11 @@ import { getFieldIcon } from "constants/fields";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { FiretableOrderBy } from "hooks/useFiretable";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
height: "100%",
|
||||
"& svg, & button": { display: "block" },
|
||||
|
||||
color: theme.palette.text.secondary,
|
||||
transition: theme.transitions.create("color", {
|
||||
duration: theme.transitions.duration.short,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FieldType } from "constants/fields";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import { db } from "../../../../firebase";
|
||||
import { db } from "../../../../../firebase";
|
||||
const ColumnSelector = ({
|
||||
tableColumns,
|
||||
handleChange,
|
||||
@@ -18,7 +18,7 @@ const ColumnSelector = ({
|
||||
label?: string;
|
||||
}) => {
|
||||
const [columns, setColumns] = useState(tableColumns ?? []);
|
||||
const getColumns = async table => {
|
||||
const getColumns = async (table) => {
|
||||
const tableConfigDoc = await db
|
||||
.doc(`_FIRETABLE_/settings/schema/${table}`)
|
||||
.get();
|
||||
@@ -33,8 +33,8 @@ const ColumnSelector = ({
|
||||
}, [table]);
|
||||
const options = columns
|
||||
? Object.values(columns)
|
||||
.filter(col => (validTypes ? validTypes.includes(col.type) : true))
|
||||
.map(col => ({ value: col.key, label: col.name }))
|
||||
.filter((col) => (validTypes ? validTypes.includes(col.type) : true))
|
||||
.map((col) => ({ value: col.key, label: col.name }))
|
||||
: [];
|
||||
return (
|
||||
<MultiSelect
|
||||
@@ -9,7 +9,7 @@ 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 =>
|
||||
const useStyles = makeStyles((Theme) =>
|
||||
createStyles({
|
||||
root: {},
|
||||
field: {
|
||||
@@ -48,7 +48,7 @@ export default function OptionsInput(props: any) {
|
||||
value={newOption}
|
||||
className={classes.field}
|
||||
label={props.placeholder ?? "New Option"}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setNewOption(e.target.value);
|
||||
}}
|
||||
onKeyPress={(e: any) => {
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { FieldType } from "constants/fields";
|
||||
@@ -17,12 +18,12 @@ import OptionsInput from "./ConfigFields/OptionsInput";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
import FieldsDropdown from "../FieldsDropdown";
|
||||
import ColumnSelector from "./ConfigFields/ColumnSelector";
|
||||
import FieldSkeleton from "components/SideDrawer/Form/FieldSkeleton";
|
||||
import RoleSelector from "components/RolesSelector";
|
||||
const CodeEditor = lazy(() =>
|
||||
import("../editors/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
const CodeEditor = lazy(
|
||||
() => import("../../editors/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
switch (fieldType) {
|
||||
@@ -50,7 +51,7 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
);
|
||||
case FieldType.connectTable:
|
||||
const tableOptions = _sortBy(
|
||||
tables?.map(t => ({
|
||||
tables?.map((t) => ({
|
||||
label: `${t.section} - ${t.name}`,
|
||||
value: t.collection,
|
||||
})) ?? [],
|
||||
@@ -77,7 +78,7 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
label="filter template"
|
||||
name="filters"
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
handleChange("filters")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
@@ -103,6 +104,48 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
case FieldType.action:
|
||||
return (
|
||||
<>
|
||||
<Typography variant="overline">Allowed roles</Typography>
|
||||
<Typography variant="body2">
|
||||
Authenticated user must have at least one of these to run the script
|
||||
</Typography>
|
||||
<RoleSelector
|
||||
label={"Allowed Roles"}
|
||||
value={config.requiredRoles}
|
||||
handleChange={handleChange("requiredRoles")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">Required fields</Typography>
|
||||
<Typography variant="body2">
|
||||
All of the selected fields must have a value for the script to run
|
||||
</Typography>
|
||||
<ColumnSelector
|
||||
label={"Required fields"}
|
||||
value={config.requiredFields}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("requiredFields")}
|
||||
/>
|
||||
<Divider />
|
||||
<Typography variant="overline">Confirmation Template</Typography>
|
||||
<Typography variant="body2">
|
||||
The action button will not ask for confirmation if this is left
|
||||
empty
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Confirmation Template"
|
||||
placeholder="Are sure you want to invest {{stockName}}?"
|
||||
value={config.confirmation}
|
||||
onChange={(e) => {
|
||||
handleChange("confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -117,46 +160,77 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
}
|
||||
label="Set as an action script"
|
||||
/>
|
||||
<RoleSelector
|
||||
label={
|
||||
"Allowed Roles(Authenticated user must have at least one of these to run the script)"
|
||||
}
|
||||
value={config.requiredRoles}
|
||||
handleChange={handleChange("requiredRoles")}
|
||||
/>
|
||||
<ColumnSelector
|
||||
label={
|
||||
"Required fields(All of the selected fields must have a value for the script to run)"
|
||||
}
|
||||
value={config.requiredFields}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("requiredFields")}
|
||||
/>
|
||||
|
||||
{!Boolean(config.isActionScript) ? (
|
||||
<TextField
|
||||
label="callable name"
|
||||
name="callableName"
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
handleChange("callableName")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="overline">action script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<Suspense fallback={<FieldSkeleton height={180} />}>
|
||||
<CodeEditor
|
||||
height={180}
|
||||
script={config.script}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config["redo.enabled"]}
|
||||
onChange={() =>
|
||||
handleChange("redo.enabled")(
|
||||
!Boolean(config["redo.enabled"])
|
||||
)
|
||||
}
|
||||
name="redo toggle"
|
||||
/>
|
||||
}
|
||||
label="enable redo(reruns the same script)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config["undo.enabled"]}
|
||||
onChange={() =>
|
||||
handleChange("undo.enabled")(
|
||||
!Boolean(config["undo.enabled"])
|
||||
)
|
||||
}
|
||||
name="undo toggle"
|
||||
/>
|
||||
}
|
||||
label="enable undo"
|
||||
/>
|
||||
{config["undo.enabled"] && (
|
||||
<>
|
||||
<Typography variant="overline">
|
||||
Undo Confirmation Template
|
||||
</Typography>
|
||||
<TextField
|
||||
label="template"
|
||||
placeholder="are you sure you want to sell your stocks in {{stockName}}"
|
||||
value={config["undo.confirmation"]}
|
||||
onChange={(e) => {
|
||||
handleChange("undo.confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<Typography variant="overline">Undo Action script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={180} />}>
|
||||
<CodeEditor
|
||||
height={180}
|
||||
script={config["undo.script"]}
|
||||
handleChange={handleChange("undo.script")}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -178,6 +252,7 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
}
|
||||
handleChange={handleChange("listenerFields")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">derivative script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor
|
||||
@@ -259,6 +334,7 @@ export default function FormDialog({
|
||||
>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
style={{ minWidth: 450 }}
|
||||
container
|
||||
justify="space-between"
|
||||
alignContent="flex-start"
|
||||
@@ -278,7 +354,7 @@ export default function FormDialog({
|
||||
{
|
||||
<ConfigForm
|
||||
type={type}
|
||||
handleChange={key => update => {
|
||||
handleChange={(key) => (update) => {
|
||||
setNewConfig({ ...newConfig, [key]: update });
|
||||
}}
|
||||
config={newConfig}
|
||||
@@ -5,7 +5,7 @@ import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { setTimeout } from "timers";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
editorWrapper: { position: "relative", minWidth: 800 },
|
||||
|
||||
@@ -31,7 +31,7 @@ const useStyles = makeStyles(theme =>
|
||||
);
|
||||
|
||||
export default function CodeEditor(props: any) {
|
||||
const { handleChange, script } = props;
|
||||
const { handleChange, script, height = "90hv" } = props;
|
||||
|
||||
const [initialEditorValue] = useState(script ?? "");
|
||||
const { tableState } = useFiretableContext();
|
||||
@@ -45,7 +45,7 @@ export default function CodeEditor(props: any) {
|
||||
|
||||
function listenEditorChanges() {
|
||||
setTimeout(() => {
|
||||
editorRef.current?.onDidChangeModelContent(ev => {
|
||||
editorRef.current?.onDidChangeModelContent((ev) => {
|
||||
handleChange(editorRef.current.getValue());
|
||||
});
|
||||
}, 2000);
|
||||
@@ -63,7 +63,7 @@ export default function CodeEditor(props: any) {
|
||||
// console.timeLog(firebaseAuthDefs);
|
||||
monaco
|
||||
.init()
|
||||
.then(monacoInstance => {
|
||||
.then((monacoInstance) => {
|
||||
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firestoreDefs
|
||||
);
|
||||
@@ -122,7 +122,7 @@ export default function CodeEditor(props: any) {
|
||||
return `static ${columnKey}:string`;
|
||||
case FieldType.singleSelect:
|
||||
const typeString = [
|
||||
...column.config.options.map(opt => `"${opt}"`),
|
||||
...column.config.options.map((opt) => `"${opt}"`),
|
||||
// "string",
|
||||
].join(" | ");
|
||||
return `static ${columnKey}:${typeString}`;
|
||||
@@ -140,7 +140,7 @@ export default function CodeEditor(props: any) {
|
||||
);
|
||||
// monacoInstance.editor.create(wrapper, properties);
|
||||
})
|
||||
.catch(error =>
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
error
|
||||
@@ -154,7 +154,7 @@ export default function CodeEditor(props: any) {
|
||||
<div className={classes.editorWrapper}>
|
||||
{/* <div id="editor" className={classes.editor} /> */}
|
||||
<Editor
|
||||
height="90vh"
|
||||
height={height}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
language="javascript"
|
||||
value={initialEditorValue}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { SnackContext } from "contexts/snackContext";
|
||||
import { cloudFunction } from "firebase/callables";
|
||||
import { sanitiseCallableName, isUrl, sanitiseRowData } from "util/fns";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { padding: theme.spacing(0, 0.375, 0, 1.5) },
|
||||
labelContainer: { overflowX: "hidden" },
|
||||
@@ -32,6 +32,16 @@ const replacer = (data: any) => (m: string, key: string) => {
|
||||
return _get(data, objKey, defaultValue);
|
||||
};
|
||||
|
||||
const getStateIcon = (actionState) => {
|
||||
switch (actionState) {
|
||||
case "undo":
|
||||
return <UndoIcon />;
|
||||
case "redo":
|
||||
return <RefreshIcon />;
|
||||
default:
|
||||
return <PlayIcon />;
|
||||
}
|
||||
};
|
||||
export default function Action({
|
||||
column,
|
||||
row,
|
||||
@@ -66,13 +76,13 @@ export default function Action({
|
||||
cloudFunction(
|
||||
callableName,
|
||||
data,
|
||||
response => {
|
||||
(response) => {
|
||||
const { message, cellValue, success } = response.data;
|
||||
setIsRunning(false);
|
||||
snack.open({ message, severity: success ? "success" : "error" });
|
||||
if (cellValue) onSubmit(cellValue);
|
||||
},
|
||||
error => {
|
||||
(error) => {
|
||||
console.error("ERROR", callableName, error);
|
||||
setIsRunning(false);
|
||||
snack.open({ message: JSON.stringify(error), severity: "error" });
|
||||
@@ -80,6 +90,12 @@ export default function Action({
|
||||
);
|
||||
};
|
||||
const hasRan = value && value.status;
|
||||
|
||||
const actionState: "run" | "undo" | "redo" = hasRan
|
||||
? value.undo
|
||||
? "undo"
|
||||
: "redo"
|
||||
: "run";
|
||||
let component = (
|
||||
<Fab
|
||||
size="small"
|
||||
@@ -92,34 +108,28 @@ export default function Action({
|
||||
>
|
||||
{isRunning ? (
|
||||
<CircularProgress color="secondary" size={16} thickness={5.6} />
|
||||
) : hasRan ? (
|
||||
value.undo ? (
|
||||
<UndoIcon />
|
||||
) : (
|
||||
<RefreshIcon />
|
||||
)
|
||||
) : (
|
||||
<PlayIcon />
|
||||
getStateIcon(actionState)
|
||||
)}
|
||||
</Fab>
|
||||
);
|
||||
|
||||
if ((column as any)?.config?.confirmation)
|
||||
if (typeof config.confirmation === "string") {
|
||||
component = (
|
||||
<Confirmation
|
||||
message={{
|
||||
title: (column as any).config.confirmation.title,
|
||||
body: (column as any).config.confirmation.body.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(row)
|
||||
),
|
||||
title: `${column.name} Confirmation`,
|
||||
body: (actionState === "undo" && config.undoConfirmation
|
||||
? config.undoConfirmation
|
||||
: config.confirmation
|
||||
).replace(/\{\{(.*?)\}\}/g, replacer(row)),
|
||||
}}
|
||||
functionName="onClick"
|
||||
>
|
||||
{component}
|
||||
</Confirmation>
|
||||
);
|
||||
|
||||
}
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
|
||||
50
www/src/components/Table/formatters/Duration.tsx
Normal file
50
www/src/components/Table/formatters/Duration.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
|
||||
export const timeDistance = (date1, date2) => {
|
||||
let distance = Math.abs(date1 - date2);
|
||||
const hours = Math.floor(distance / 3600000);
|
||||
distance -= hours * 3600000;
|
||||
const minutes = Math.floor(distance / 60000);
|
||||
distance -= minutes * 60000;
|
||||
const seconds = Math.floor(distance / 1000);
|
||||
return `${hours ? `${hours}:` : ""}${("0" + minutes).slice(-2)}:${(
|
||||
"0" + seconds
|
||||
).slice(-2)}`;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
height: "100%",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function Duration({
|
||||
rowIdx,
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
}: CustomCellProps) {
|
||||
const classes = useStyles();
|
||||
const startDate = value.start?.toDate();
|
||||
const endDate = value.end?.toDate();
|
||||
if (!startDate && !endDate) {
|
||||
return <></>;
|
||||
}
|
||||
if (startDate && !endDate) {
|
||||
const now = new Date();
|
||||
const duration = timeDistance(startDate, now);
|
||||
|
||||
return <>{duration}</>;
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
const duration = timeDistance(endDate, startDate);
|
||||
|
||||
return <>{duration}</>;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
@@ -7,6 +7,9 @@ const MultiSelect = lazy(
|
||||
() => import("./MultiSelect" /* webpackChunkName: "MultiSelect" */)
|
||||
);
|
||||
const DatePicker = lazy(() => import("./Date" /* webpackChunkName: "Date" */));
|
||||
const Duration = lazy(
|
||||
() => import("./Duration" /* webpackChunkName: "Duration" */)
|
||||
);
|
||||
const Rating = lazy(() => import("./Rating" /* webpackChunkName: "Rating" */));
|
||||
const Checkbox = lazy(
|
||||
() => import("./Checkbox" /* webpackChunkName: "Checkbox" */)
|
||||
@@ -55,6 +58,8 @@ export const getFormatter = (column: any, readOnly: boolean = false) => {
|
||||
case FieldType.date:
|
||||
case FieldType.dateTime:
|
||||
return withCustomCell(DatePicker, readOnly);
|
||||
case FieldType.duration:
|
||||
return withCustomCell(Duration, readOnly);
|
||||
|
||||
case FieldType.rating:
|
||||
return withCustomCell(Rating, readOnly);
|
||||
@@ -91,8 +96,10 @@ export const getFormatter = (column: any, readOnly: boolean = false) => {
|
||||
|
||||
case FieldType.user:
|
||||
return withCustomCell(User, readOnly);
|
||||
|
||||
case FieldType.code:
|
||||
return withCustomCell(Code, readOnly);
|
||||
|
||||
case FieldType.richText:
|
||||
return withCustomCell(RichText, readOnly);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import PercentageIcon from "assets/icons/Percentage";
|
||||
|
||||
import DateIcon from "@material-ui/icons/Today";
|
||||
import DateTimeIcon from "@material-ui/icons/AccessTime";
|
||||
import DurationIcon from "@material-ui/icons/Timer";
|
||||
|
||||
import UrlIcon from "@material-ui/icons/Link";
|
||||
|
||||
@@ -47,6 +48,7 @@ export {
|
||||
PercentageIcon,
|
||||
DateIcon,
|
||||
DateTimeIcon,
|
||||
DurationIcon,
|
||||
UrlIcon,
|
||||
RatingIcon,
|
||||
ImageIcon,
|
||||
@@ -77,6 +79,7 @@ export enum FieldType {
|
||||
|
||||
date = "DATE",
|
||||
dateTime = "DATE_TIME",
|
||||
duration = "DURATION",
|
||||
|
||||
url = "URL",
|
||||
rating = "RATING",
|
||||
@@ -115,6 +118,7 @@ export const FIELDS = [
|
||||
|
||||
{ icon: <DateIcon />, name: "Date", type: FieldType.date },
|
||||
{ icon: <DateTimeIcon />, name: "Time & Date", type: FieldType.dateTime },
|
||||
{ icon: <DurationIcon />, name: "Duration", type: FieldType.duration },
|
||||
|
||||
{ icon: <UrlIcon />, name: "URL", type: FieldType.url },
|
||||
{ icon: <RatingIcon />, name: "Rating", type: FieldType.rating },
|
||||
@@ -171,6 +175,7 @@ export const FIELD_TYPE_DESCRIPTIONS = {
|
||||
"Date displayed and input as YYYY/MM/DD or input using a picker module.",
|
||||
[FieldType.dateTime]:
|
||||
"Time and Date can be written as YYYY/MM/DD hh:mm (am/pm) or input using a picker module.",
|
||||
[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.",
|
||||
|
||||
Reference in New Issue
Block a user