mirror of
https://github.com/rowyio/rowy.git
synced 2026-09-03 04:30:54 +02:00
10
.gitignore
vendored
10
.gitignore
vendored
@@ -3,6 +3,8 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
www/node_modules
|
||||
www/.firebaserc
|
||||
|
||||
Firetable/node_modules
|
||||
cloud_functions/functions/node_modules
|
||||
/.pnp
|
||||
@@ -20,17 +22,23 @@ cloud_functions/functions/lib
|
||||
cloud_functions/functions/src/collectionSync/config.json
|
||||
cloud_functions/functions/src/history/config.json
|
||||
cloud_functions/functions/src/algolia/config.json
|
||||
cloud_functions/functions/src/snapshotSync/config.json
|
||||
|
||||
cloud_functions/functions/firebase-credentials.json
|
||||
cloud_functions/.firebaserc
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.enc
|
||||
!*.enc
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
firebase-debug.log*
|
||||
|
||||
|
||||
|
||||
# Accidental package installs to root directories
|
||||
yarn.lock
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"default":"antler-vc"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -33,6 +33,10 @@ const main = async () => {
|
||||
"_FIRETABLE_/_SETTINGS_/_CONFIG_/_COLLECTION_SYNC_",
|
||||
"./src/collectionSync/config.json"
|
||||
);
|
||||
await docConfig2json(
|
||||
"_FIRETABLE_/_SETTINGS_/_CONFIG_/_SNAPSHOT_SYNC_",
|
||||
"./src/snapshotSync/config.json"
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"serve": "npm run build && firebase serve --only functions",
|
||||
"shell": "npm run build && firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"deploy": "firebase deploy --token \"$FIREBASE_TOKEN\" --project antler-vc --only",
|
||||
"deploy": "firebase deploy",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
"engines": {
|
||||
@@ -15,6 +15,8 @@
|
||||
},
|
||||
"main": "lib/src/index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/cloudbuild": "^1.5.0",
|
||||
"@google-cloud/firestore": "^3.7.5",
|
||||
"@types/algoliasearch": "^3.34.5",
|
||||
"@types/json2csv": "^4.5.0",
|
||||
"@types/lodash": "^4.14.149",
|
||||
@@ -27,6 +29,8 @@
|
||||
"devDependencies": {
|
||||
"firebase-tools": "^7.16.1",
|
||||
"husky": "^3.0.9",
|
||||
"prettier": "^2.0.5",
|
||||
"pretty-quick": "^2.0.1",
|
||||
"ts-node": "^8.6.2",
|
||||
"tslint": "^6.1.0",
|
||||
"typescript": "^3.2.2"
|
||||
|
||||
100
cloud_functions/functions/src/backup.ts
Normal file
100
cloud_functions/functions/src/backup.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import * as functions from "firebase-functions";
|
||||
import * as firestore from "@google-cloud/firestore";
|
||||
import { hasAnyRole } from "./utils/auth";
|
||||
|
||||
const client = new firestore.v1.FirestoreAdminClient();
|
||||
|
||||
// Replace BUCKET_NAME
|
||||
const bucket = "gs://BUCKET_NAME";
|
||||
/*
|
||||
const restoreFirestoreBackup = (collectionIds: string[] = []) => {
|
||||
const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT;
|
||||
const databaseName = client.databasePath(projectId, "(default)");
|
||||
const date = new Date();
|
||||
|
||||
const backupFolder = `${date.getFullYear()}-${
|
||||
date.getMonth() + 1
|
||||
}-${date.getDate()}`;
|
||||
const inputUriPrefix = bucket + "/" + backupFolder;
|
||||
console.log(inputUriPrefix);
|
||||
return client
|
||||
.importDocuments({
|
||||
name: databaseName,
|
||||
inputUriPrefix,
|
||||
// Leave collectionIds empty to export all collections
|
||||
// or set to a list of collection IDs to export,
|
||||
// collectionIds: ['users', 'posts']
|
||||
collectionIds,
|
||||
})
|
||||
.then((responses) => {
|
||||
const response = responses[0];
|
||||
console.log(`Operation Name: ${response["name"]}`);
|
||||
return response;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw new Error("import operation failed");
|
||||
});
|
||||
};
|
||||
|
||||
export const scheduledFirestoreImport = functions.pubsub
|
||||
.schedule("every 24 hours")
|
||||
.onRun((context) => {
|
||||
console.log(context);
|
||||
return restoreFirestoreBackup();
|
||||
});
|
||||
*/
|
||||
|
||||
const firestoreBackup = (collectionIds: string[] = []) => {
|
||||
const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT;
|
||||
const databaseName = client.databasePath(projectId, "(default)");
|
||||
const date = new Date();
|
||||
|
||||
const backupFolder = `${date.getFullYear()}-${date.getMonth() +
|
||||
1}-${date.getDate()}`;
|
||||
|
||||
return client
|
||||
.exportDocuments({
|
||||
name: databaseName,
|
||||
outputUriPrefix: bucket + "/" + backupFolder,
|
||||
// Leave collectionIds empty to export all collections
|
||||
// or set to a list of collection IDs to export,
|
||||
// collectionIds: ['users', 'posts']
|
||||
collectionIds,
|
||||
})
|
||||
.then(responses => {
|
||||
const response = responses[0];
|
||||
console.log(`Operation Name: ${response["name"]}`);
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
throw new Error("Export operation failed");
|
||||
});
|
||||
};
|
||||
export const scheduledFirestoreBackup = functions.pubsub
|
||||
.schedule("every 24 hours")
|
||||
.onRun(context => {
|
||||
console.log(context);
|
||||
return firestoreBackup();
|
||||
});
|
||||
|
||||
export const callableFirestoreBackup = functions.https.onCall(
|
||||
async (data, context) => {
|
||||
console.log(data);
|
||||
const authorized = hasAnyRole(["ADMIN"], context);
|
||||
if (!context.auth || !authorized) {
|
||||
console.warn(`unauthorized user${context}`);
|
||||
return {
|
||||
success: false,
|
||||
message: "you don't have permissions to send this email",
|
||||
};
|
||||
} else {
|
||||
await firestoreBackup();
|
||||
return {
|
||||
success: true,
|
||||
message: "backup ran",
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
74
cloud_functions/functions/src/buildTriggers/index.ts
Normal file
74
cloud_functions/functions/src/buildTriggers/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as functions from "firebase-functions";
|
||||
const { CloudBuildClient } = require("@google-cloud/cloudbuild");
|
||||
const cb = new CloudBuildClient();
|
||||
|
||||
export const triggerCloudBuild = functions.https.onCall(
|
||||
async (
|
||||
data: {
|
||||
ref: {
|
||||
id: string;
|
||||
path: string;
|
||||
parentId: string;
|
||||
};
|
||||
row: any;
|
||||
action: "run" | "redo" | "undo";
|
||||
},
|
||||
context: functions.https.CallableContext
|
||||
) => {
|
||||
const {
|
||||
row, //ref, action
|
||||
} = data;
|
||||
|
||||
if (!context.auth) {
|
||||
return false;
|
||||
}
|
||||
const { triggerId, branch, projectId, groupName } = row;
|
||||
|
||||
// Starts a build against the branch provided.
|
||||
const [resp] = await cb.runBuildTrigger({
|
||||
projectId, //project hosting cloud build
|
||||
triggerId,
|
||||
source: {
|
||||
branchName: branch,
|
||||
substitutions: {
|
||||
_PROJECT_ID: projectId,
|
||||
_FUNCTIONS_GROUP: groupName,
|
||||
},
|
||||
},
|
||||
});
|
||||
console.info(`triggered build for ${triggerId}`);
|
||||
const [build] = await resp.promise();
|
||||
|
||||
const STATUS_LOOKUP = [
|
||||
"UNKNOWN",
|
||||
"Queued",
|
||||
"Working",
|
||||
"Success",
|
||||
"Failure",
|
||||
"Error",
|
||||
"Timeout",
|
||||
"Cancelled",
|
||||
];
|
||||
for (const step of build.steps) {
|
||||
console.info(
|
||||
`step:\n\tname: ${step.name}\n\tstatus: ${STATUS_LOOKUP[build.status]}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(context.auth.token);
|
||||
if (triggerId) {
|
||||
return {
|
||||
message: "cloud functions are snow flakes",
|
||||
cellValue: {
|
||||
redo: true,
|
||||
status: `Triggered`,
|
||||
// completedAt: serverTimestamp(),
|
||||
meta: { ranBy: context.auth.token.email },
|
||||
undo: false,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
@@ -16,6 +16,8 @@ import synonymsFnsGenerator from "./synonyms";
|
||||
import synonymsConfig from "./synonyms/config";
|
||||
|
||||
export { exportTable } from "./export";
|
||||
export { triggerCloudBuild } from "./buildTriggers";
|
||||
export { scheduledFirestoreBackup, callableFirestoreBackup } from "./backup";
|
||||
import * as callableFns from "./callable";
|
||||
|
||||
export const callable = callableFns;
|
||||
@@ -35,14 +37,20 @@ export const FT_sync = collectionSyncConfig.reduce((acc: any, collection) => {
|
||||
}, {});
|
||||
export const FT_snapshotSync = snapshotSyncConfig.reduce(
|
||||
(acc: any, collection) => {
|
||||
return {
|
||||
...acc,
|
||||
[`${`${`${collection.source}`
|
||||
.replace(/\//g, "_")
|
||||
.replace(/_{.*?}_/g, "_")}`}2${`${`${collection.target}`
|
||||
.replace(/\//g, "_")
|
||||
.replace(/_{.*?}_/g, "_")}`}`]: snapshotSyncFnsGenerator(collection),
|
||||
};
|
||||
if (collection.fnName) {
|
||||
return {
|
||||
...acc,
|
||||
[collection.fnName]: snapshotSyncFnsGenerator(collection),
|
||||
};
|
||||
} else
|
||||
return {
|
||||
...acc,
|
||||
[`${`${`${collection.source}`
|
||||
.replace(/\//g, "_")
|
||||
.replace(/_{.*?}_/g, "_")}`}2${`${`${collection.target}`
|
||||
.replace(/\//g, "_")
|
||||
.replace(/_{.*?}_/g, "_")}`}`]: snapshotSyncFnsGenerator(collection),
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
[
|
||||
{
|
||||
"source": "teams",
|
||||
"onUpdate": true,
|
||||
"target": "teams/{{id}}/dueDiligence",
|
||||
"snapshotField": "team",
|
||||
"fieldsToSync": [
|
||||
"cohort",
|
||||
"teamName",
|
||||
"trackOutDate",
|
||||
"teamMembers",
|
||||
"focusArea",
|
||||
"isDissolved",
|
||||
"oneLineDescription",
|
||||
"documents",
|
||||
"longDescription",
|
||||
"shortPitchDeck",
|
||||
"logo",
|
||||
"problem",
|
||||
"model",
|
||||
"geography",
|
||||
"targetCustomer",
|
||||
"marketSize",
|
||||
"competitors",
|
||||
"innovation",
|
||||
"gtm",
|
||||
"defensibility"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -4,38 +4,37 @@ import { db } from "../config";
|
||||
|
||||
import * as _ from "lodash";
|
||||
import { replacer } from "../utils/email";
|
||||
// returns object of fieldsToSync
|
||||
|
||||
enum TargetTypes {
|
||||
subCollection = "subCollection",
|
||||
document = "document",
|
||||
}
|
||||
/**
|
||||
* returns object with only keys included in fieldsToSync
|
||||
* @param docData
|
||||
*/
|
||||
const docReducer = (docData: FirebaseFirestore.DocumentData) => (
|
||||
acc: any,
|
||||
curr: string
|
||||
) => {
|
||||
if (docData[curr] !== undefined && docData[curr] !== null)
|
||||
if (docData[curr] !== undefined && docData[curr] !== null) {
|
||||
return { ...acc, [curr]: docData[curr] };
|
||||
else return acc;
|
||||
} else return acc;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param targetCollection
|
||||
* @param targetPath
|
||||
* @param fieldsToSync
|
||||
*/
|
||||
const syncDoc = (
|
||||
targetCollection: string,
|
||||
const syncSubCollection = async (
|
||||
targetPath: string,
|
||||
snapshotField: string,
|
||||
fieldsToSync: string[]
|
||||
) => async (snapshot: FirebaseFirestore.DocumentSnapshot) => {
|
||||
const docData = snapshot.data();
|
||||
if (!docData) return false; // returns if theres no data in the doc
|
||||
const syncData = fieldsToSync.reduce(docReducer(docData), {});
|
||||
|
||||
const collectionPath = targetCollection.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer({ ...docData, id: snapshot.id })
|
||||
);
|
||||
if (Object.keys(syncData).length === 0) return false; // returns if theres nothing to sync
|
||||
const targetDocs = await db.collection(collectionPath).get();
|
||||
syncData: any,
|
||||
snapshot: FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>
|
||||
) => {
|
||||
const targetDocs = await db.collection(targetPath).get();
|
||||
if (targetDocs.empty) return false;
|
||||
|
||||
for (let i = 0; i < targetDocs.docs.length; i++) {
|
||||
const doc = targetDocs.docs[i];
|
||||
await doc.ref.update({
|
||||
@@ -48,25 +47,98 @@ const syncDoc = (
|
||||
return true;
|
||||
};
|
||||
|
||||
const syncDocSnapshot = async (
|
||||
targetPath,
|
||||
isArray,
|
||||
snapshotField,
|
||||
newSnapshotData,
|
||||
snapshot
|
||||
) => {
|
||||
console.log({
|
||||
targetPath,
|
||||
isArray,
|
||||
snapshotField,
|
||||
});
|
||||
const targetRef = db.doc(targetPath);
|
||||
const targetSnapshot = await targetRef.get();
|
||||
const targetData = targetSnapshot.data();
|
||||
if (!targetData) {
|
||||
console.warn("target does not exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isArray) {
|
||||
const oldSnapshotsArray = targetData[snapshotField];
|
||||
const snapshotDocPath = snapshot.ref.path;
|
||||
const oldSnapshot = _.find(oldSnapshotsArray, { docPath: snapshotDocPath });
|
||||
|
||||
const updatedSnapshotsArray = oldSnapshotsArray.filter(item => {
|
||||
console.log({ snapshotDocPath, item });
|
||||
return item.docPath !== snapshotDocPath;
|
||||
});
|
||||
updatedSnapshotsArray.push({ ...oldSnapshot, snapshot: newSnapshotData });
|
||||
|
||||
return targetRef.update({ [snapshotField]: updatedSnapshotsArray });
|
||||
} else {
|
||||
return targetRef.update({
|
||||
[snapshotField]: {
|
||||
...targetData[snapshotField],
|
||||
snapshot: newSnapshotData,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* onUpdate change to snapshot adapter
|
||||
* @param targetCollection
|
||||
* @param fieldsToSync
|
||||
*/
|
||||
const syncDocOnUpdate = (
|
||||
targetCollection: string,
|
||||
snapshotField: string,
|
||||
fieldsToSync: string[]
|
||||
) => (snapshot: functions.Change<FirebaseFirestore.DocumentSnapshot>) => {
|
||||
const afterData = snapshot.after.data();
|
||||
const beforeData = snapshot.before.data();
|
||||
const syncDocOnUpdate = (config: {
|
||||
target: string;
|
||||
snapshotField: string;
|
||||
targetType: TargetTypes;
|
||||
fieldsToSync: string[];
|
||||
isArray: boolean;
|
||||
}) => (snapshot: functions.Change<FirebaseFirestore.DocumentSnapshot>) => {
|
||||
const { fieldsToSync, target, snapshotField, targetType, isArray } = config;
|
||||
|
||||
const afterDocData = snapshot.after.data();
|
||||
const beforeDocData = snapshot.before.data();
|
||||
if (!afterDocData || !beforeDocData) return false;
|
||||
const afterData = fieldsToSync.reduce(docReducer(afterDocData), {});
|
||||
const beforeData = fieldsToSync.reduce(docReducer(beforeDocData), {});
|
||||
const hasChanged = !_.isEqual(afterData, beforeData);
|
||||
console.log("nothing important changed");
|
||||
if (Object.keys(afterData).length === 0) return false; // returns if theres nothing to sync
|
||||
|
||||
const targetPath = target.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer({ ...snapshot.after.data(), id: snapshot.after.id })
|
||||
);
|
||||
|
||||
console.log({ hasChanged, targetType });
|
||||
if (hasChanged) {
|
||||
return syncDoc(
|
||||
targetCollection,
|
||||
snapshotField,
|
||||
fieldsToSync
|
||||
)(snapshot.after);
|
||||
switch (targetType) {
|
||||
case TargetTypes.subCollection:
|
||||
return syncSubCollection(
|
||||
targetPath,
|
||||
snapshotField,
|
||||
afterData,
|
||||
snapshot.after
|
||||
);
|
||||
|
||||
case TargetTypes.document:
|
||||
return syncDocSnapshot(
|
||||
targetPath,
|
||||
isArray,
|
||||
snapshotField,
|
||||
afterData,
|
||||
snapshot.after
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.warn("no change detected");
|
||||
return false;
|
||||
@@ -75,20 +147,14 @@ const syncDocOnUpdate = (
|
||||
|
||||
/**
|
||||
* returns 2 different trigger functions (onCreate,onUpdate) in an object
|
||||
* @param collection configuration object
|
||||
* @param config configuration object
|
||||
*/
|
||||
const snapshotSyncFnsGenerator = collection =>
|
||||
const snapshotSyncFnsGenerator = config =>
|
||||
Object.entries({
|
||||
onUpdate: collection.onUpdate
|
||||
onUpdate: config.onUpdate
|
||||
? functions.firestore
|
||||
.document(`${collection.source}/{docId}`)
|
||||
.onUpdate(
|
||||
syncDocOnUpdate(
|
||||
collection.target,
|
||||
collection.snapshotField,
|
||||
collection.fieldsToSync
|
||||
)
|
||||
)
|
||||
.document(`${config.source}/{docId}`)
|
||||
.onUpdate(syncDocOnUpdate(config))
|
||||
: null,
|
||||
}).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {});
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ const config = [
|
||||
listenerField: "cohort",
|
||||
synonymField: "icPage",
|
||||
transformer: (cohort, doc) => {
|
||||
if (doc.icType === "IC") {
|
||||
if (doc.icType === "IC" || doc.icType === "Delayed IC") {
|
||||
return `https://firepage.antler.co/IC/${cohort}`;
|
||||
} else return "";
|
||||
},
|
||||
@@ -217,9 +217,8 @@ const config = [
|
||||
listenerField: "icResult",
|
||||
synonymField: "isDecided",
|
||||
transformer: icResult => {
|
||||
if (["Yes", "No"].includes(icResult)) {
|
||||
return true;
|
||||
} else return false;
|
||||
if (icResult && ["Yes", "No"].includes(icResult)) return true;
|
||||
else return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -9,28 +9,31 @@ type synonymGroup = {
|
||||
transformer: Function;
|
||||
};
|
||||
|
||||
const synonyms = (docData, groups: synonymGroup[]) =>
|
||||
groups.reduce((update: any, currGroup) => {
|
||||
const synonyms = async (docData, groups: synonymGroup[]) => {
|
||||
const updates = await groups.reduce(async (update: any, currGroup) => {
|
||||
const newValue = await currGroup.transformer(
|
||||
docData[currGroup.listenerField],
|
||||
docData
|
||||
);
|
||||
if (
|
||||
currGroup.isForced ||
|
||||
(docData[currGroup.listenerField] &&
|
||||
docData[currGroup.synonymField] !==
|
||||
currGroup.transformer(docData[currGroup.listenerField], docData))
|
||||
docData[currGroup.listenerField] &&
|
||||
docData[currGroup.synonymField] !== newValue
|
||||
) {
|
||||
return {
|
||||
...update,
|
||||
[currGroup.synonymField]: currGroup.transformer(
|
||||
docData[currGroup.listenerField],
|
||||
docData
|
||||
),
|
||||
[currGroup.synonymField]: newValue,
|
||||
};
|
||||
} else return update;
|
||||
}, {});
|
||||
|
||||
console.log({ updates });
|
||||
return updates;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const addSynonymOnUpdate = (groups: synonymGroup[]) => (
|
||||
const addSynonymOnUpdate = (groups: synonymGroup[]) => async (
|
||||
change: functions.Change<FirebaseFirestore.DocumentSnapshot>
|
||||
) => {
|
||||
const beforeData = change.before.data();
|
||||
@@ -41,6 +44,7 @@ const addSynonymOnUpdate = (groups: synonymGroup[]) => (
|
||||
}
|
||||
const changedGroups = groups.reduce((acc: synonymGroup[], currGroup) => {
|
||||
if (
|
||||
currGroup.isForced ||
|
||||
beforeData[currGroup.listenerField] !== afterData[currGroup.listenerField]
|
||||
) {
|
||||
return [...acc, currGroup];
|
||||
@@ -51,7 +55,7 @@ const addSynonymOnUpdate = (groups: synonymGroup[]) => (
|
||||
if (changedGroups.length === 0) {
|
||||
return false; // no changes detected
|
||||
}
|
||||
const updates = synonyms(
|
||||
const updates = await synonyms(
|
||||
{ ...afterData, id: change.after.id },
|
||||
changedGroups
|
||||
);
|
||||
@@ -63,14 +67,14 @@ const addSynonymOnUpdate = (groups: synonymGroup[]) => (
|
||||
}
|
||||
};
|
||||
|
||||
const addSynonymOnCreate = (groups: synonymGroup[]) => (
|
||||
const addSynonymOnCreate = (groups: synonymGroup[]) => async (
|
||||
snapshot: FirebaseFirestore.DocumentSnapshot
|
||||
) => {
|
||||
const docData = snapshot.data();
|
||||
if (!docData) {
|
||||
return false;
|
||||
}
|
||||
const updates = synonyms({ ...docData, id: snapshot.id }, groups);
|
||||
const updates = await synonyms({ ...docData, id: snapshot.id }, groups);
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -83,7 +87,7 @@ const addSynonymOnCreate = (groups: synonymGroup[]) => (
|
||||
*
|
||||
* @param collection configuration object
|
||||
*/
|
||||
const synonymsFnsGenerator = collection => ({
|
||||
const synonymsFnsGenerator = (collection) => ({
|
||||
onCreate: functions.firestore
|
||||
.document(`${collection.name}/{docId}`)
|
||||
.onCreate(addSynonymOnCreate(collection.groups)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,40 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=www/.env.enc
|
||||
- --plaintext-file=www/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "www"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- env
|
||||
- "${_PROJECT_ID}"
|
||||
- "${_FIREBASE_WEB_API_KEY}"
|
||||
- "${_ALGOLIA_APP_ID}"
|
||||
- "${_ALGOLIA_APP_KEY}"
|
||||
dir: "www"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["build"]
|
||||
dir: "www"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["deploy"]
|
||||
args:
|
||||
- "target"
|
||||
- "${_HOSTING_TARGET}"
|
||||
- --project
|
||||
- "${_PROJECT_ID}"
|
||||
dir: "www"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- deploy
|
||||
- --project
|
||||
- "${_PROJECT_ID}"
|
||||
- --debug
|
||||
- --token
|
||||
- "${_FIREBASE_TOKEN}"
|
||||
- --only
|
||||
- hosting
|
||||
dir: "www"
|
||||
substitutions:
|
||||
_PROJECT_ID: "project-id" # default value
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:FT_algolia"
|
||||
dir: "cloud_functions/functions"
|
||||
@@ -1,29 +0,0 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:FT_sync"
|
||||
dir: "cloud_functions/functions"
|
||||
@@ -1,29 +0,0 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:FT_history"
|
||||
dir: "cloud_functions/functions"
|
||||
@@ -1,29 +0,0 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:FT_permissions"
|
||||
dir: "cloud_functions/functions"
|
||||
@@ -1,29 +0,0 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:FT_snapshotSync"
|
||||
dir: "cloud_functions/functions"
|
||||
@@ -1,29 +0,0 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
dir: "cloud_functions/functions"
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:FT_synonyms"
|
||||
dir: "cloud_functions/functions"
|
||||
@@ -1,22 +1,4 @@
|
||||
steps:
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/.env.enc
|
||||
- --plaintext-file=cloud_functions/functions/.env
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: gcr.io/cloud-builders/gcloud
|
||||
args:
|
||||
- kms
|
||||
- decrypt
|
||||
- --ciphertext-file=cloud_functions/functions/firebase-credentials.json.enc
|
||||
- --plaintext-file=cloud_functions/functions/firebase-credentials.json
|
||||
- --location=global
|
||||
- --keyring=antler-vc
|
||||
- --key=cloudbuild-env
|
||||
- name: node:10.15.1
|
||||
entrypoint: yarn
|
||||
args: ["install"]
|
||||
@@ -25,5 +7,14 @@ steps:
|
||||
entrypoint: yarn
|
||||
args:
|
||||
- "deploy"
|
||||
- "functions:exportTable"
|
||||
- "--project"
|
||||
- "${_PROJECT_ID}"
|
||||
- "--token"
|
||||
- "${_FIREBASE_TOKEN}"
|
||||
- "--only"
|
||||
- "functions:${_FUNCTIONS_GROUP}"
|
||||
dir: "cloud_functions/functions"
|
||||
|
||||
substitutions:
|
||||
_PROJECT_ID: "project-id" # default value
|
||||
_FUNCTIONS_GROUP: "exportTable" # default value
|
||||
|
||||
1
fireform
Submodule
1
fireform
Submodule
Submodule fireform added at dd6aa49a10
BIN
www/.env.development.enc
Normal file
BIN
www/.env.development.enc
Normal file
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "antler-vc"
|
||||
},
|
||||
"targets": {
|
||||
"antler-vc": {
|
||||
"hosting": {
|
||||
"firetable": [
|
||||
"antler-admin"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
www/createDotEnv.js
Normal file
18
www/createDotEnv.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const fs = require("fs");
|
||||
|
||||
const main = (
|
||||
projectID = "",
|
||||
firebaseWebApiKey = "",
|
||||
algoliaAppId = "",
|
||||
algoliaSearhApiKey = ""
|
||||
) => {
|
||||
return fs.writeFileSync(
|
||||
".env",
|
||||
`REACT_APP_FIREBASE_PROJECT_ID = ${projectID}
|
||||
REACT_APP_FIREBASE_PROJECT_WEB_API_KEY = ${firebaseWebApiKey}
|
||||
REACT_APP_ALGOLIA_APP_ID = ${algoliaAppId}
|
||||
REACT_APP_ALGOLIA_SEARCH_API_KEY = ${algoliaSearhApiKey}`
|
||||
);
|
||||
};
|
||||
|
||||
main(process.argv[2], process.argv[3], process.argv[4], process.argv[5]);
|
||||
@@ -3,10 +3,12 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@antlerengineering/components": "^0.2.7",
|
||||
"@antlerengineering/multiselect": "^0.3.9",
|
||||
"@date-io/date-fns": "1.x",
|
||||
"@material-ui/core": "^4.9.4",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.44",
|
||||
"@material-ui/lab": "^4.0.0-alpha.52",
|
||||
"@material-ui/pickers": "^3.2.10",
|
||||
"@mdi/js": "^4.9.95",
|
||||
"@tinymce/tinymce-react": "^3.4.0",
|
||||
@@ -51,6 +53,7 @@
|
||||
"react-scripts": "^3.3.0",
|
||||
"tinymce": "^5.2.0",
|
||||
"typescript": "^3.7.2",
|
||||
"use-algolia": "^1.3.0",
|
||||
"use-debounce": "^3.3.0",
|
||||
"use-persisted-state": "^0.3.0",
|
||||
"yarn": "^1.19.0",
|
||||
@@ -61,7 +64,9 @@
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"deploy": "firebase deploy --project \"$PROJECT_ID\" --debug --token \"$FIREBASE_TOKEN\" --only hosting"
|
||||
"env": "node createDotEnv",
|
||||
"target": "firebase target:apply hosting firetable",
|
||||
"deploy": "firebase deploy"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10"
|
||||
|
||||
@@ -47,7 +47,8 @@ a {
|
||||
color: #e22729;
|
||||
}
|
||||
|
||||
ul {
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ import SignOutView from "views/SignOutView";
|
||||
const TableView = lazy(() =>
|
||||
import("./views/TableView" /* webpackChunkName: "TableView" */)
|
||||
);
|
||||
const GridView = lazy(() =>
|
||||
import("./views/GridView" /* webpackChunkName: "GridView" */)
|
||||
);
|
||||
const TablesView = lazy(() =>
|
||||
import("./views/TablesView" /* webpackChunkName: "TablesView" */)
|
||||
);
|
||||
@@ -52,7 +55,7 @@ const App: React.FC = () => {
|
||||
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={[routes.home, routes.tableWithId]}
|
||||
path={[routes.home, routes.tableWithId, routes.gridWithId]}
|
||||
render={() => (
|
||||
<FiretableContextProvider>
|
||||
<Switch>
|
||||
@@ -65,6 +68,10 @@ const App: React.FC = () => {
|
||||
path={routes.tableWithId}
|
||||
render={() => <TableView />}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path={routes.gridWithId}
|
||||
render={() => <GridView />}
|
||||
/>
|
||||
</Switch>
|
||||
</FiretableContextProvider>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import React from "react";
|
||||
import _merge from "lodash/merge";
|
||||
|
||||
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
|
||||
import { createMuiTheme, ThemeOptions, fade } from "@material-ui/core/styles";
|
||||
import ClearIcon from "@material-ui/icons/Clear";
|
||||
|
||||
const HEADING_TEXT = "Europa, sans-serif";
|
||||
const BODY_TEXT = '"Open Sans", sans-serif';
|
||||
export const HEADING_FONT = "Europa, sans-serif";
|
||||
export const BODY_FONT = '"Open Sans", sans-serif';
|
||||
|
||||
export const ANTLER_RED = "#ef4747";
|
||||
export const ANTLER_RED_ACCESSIBLE = "#e22729";
|
||||
export const SECONDARY_GREY = "#282829";
|
||||
export const SECONDARY_TEXT = "rgba(0, 0, 0, 0.6)";
|
||||
export const ERROR = "#b00020";
|
||||
|
||||
export const ROOT_FONT_SIZE = 16;
|
||||
export const toRem = (px: number) => `${px / ROOT_FONT_SIZE}rem`;
|
||||
export const toEm = (px: number, root: number) => `${px / root}em`;
|
||||
|
||||
declare module "@material-ui/core/styles/transitions" {
|
||||
interface Easing {
|
||||
@@ -12,161 +23,271 @@ declare module "@material-ui/core/styles/transitions" {
|
||||
}
|
||||
}
|
||||
|
||||
const Theme = createMuiTheme({
|
||||
export const themeBase = createMuiTheme({
|
||||
palette: {
|
||||
primary: { main: "#e22729" },
|
||||
secondary: { main: "#282829" },
|
||||
text: { secondary: "rgba(0, 0, 0, 0.6)" },
|
||||
primary: { main: ANTLER_RED, light: ANTLER_RED },
|
||||
secondary: { main: SECONDARY_GREY },
|
||||
text: { secondary: SECONDARY_TEXT },
|
||||
error: { main: ERROR },
|
||||
},
|
||||
typography: {
|
||||
fontFamily: BODY_TEXT,
|
||||
fontFamily: BODY_FONT,
|
||||
h1: { fontFamily: HEADING_FONT },
|
||||
h2: { fontFamily: HEADING_FONT },
|
||||
h3: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "2.25rem",
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(36),
|
||||
fontWeight: "bold",
|
||||
fontStyle: "normal",
|
||||
lineHeight: "normal",
|
||||
letterSpacing: "normal",
|
||||
},
|
||||
h4: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(32),
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
h5: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "1.5rem",
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(24),
|
||||
fontWeight: "bold",
|
||||
fontStyle: "normal",
|
||||
lineHeight: 1.25,
|
||||
letterSpacing: "normal",
|
||||
},
|
||||
h6: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "1.125rem",
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(18),
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
overline: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "0.8125rem",
|
||||
fontWeight: "bold",
|
||||
fontStyle: "normal",
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: 2,
|
||||
color: "rgba(0, 0, 0, 0.6)",
|
||||
},
|
||||
subtitle1: {
|
||||
fontSize: "1rem",
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: 0.15,
|
||||
},
|
||||
body1: {
|
||||
lineHeight: 1.75,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
subtitle2: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(16),
|
||||
fontWeight: "bold",
|
||||
fontSize: "1rem",
|
||||
letterSpacing: toEm(0.4, 16),
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
body1: {
|
||||
letterSpacing: toEm(0.5, 16),
|
||||
lineHeight: 1.75,
|
||||
},
|
||||
body2: {
|
||||
fontSize: 13.8,
|
||||
fontWeight: "normal",
|
||||
lineHeight: 1.45,
|
||||
letterSpacing: 0.25,
|
||||
fontSize: toRem(14),
|
||||
letterSpacing: toEm(0.25, 14),
|
||||
},
|
||||
button: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "1rem",
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(16),
|
||||
fontWeight: "bold",
|
||||
letterSpacing: toEm(0.75, 16),
|
||||
lineHeight: 1,
|
||||
letterSpacing: 0.75,
|
||||
},
|
||||
caption: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "0.875rem",
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(13),
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.25,
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: toEm(0.4, 13),
|
||||
lineHeight: 16 / 13,
|
||||
},
|
||||
overline: {
|
||||
fontFamily: HEADING_FONT,
|
||||
fontSize: toRem(13),
|
||||
fontWeight: "bold",
|
||||
letterSpacing: toEm(2, 13),
|
||||
lineHeight: 16 / 13,
|
||||
color: SECONDARY_TEXT,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const defaultOverrides: ThemeOptions = {
|
||||
transitions: {
|
||||
easing: {
|
||||
custom: "cubic-bezier(0.25, 0.1, 0.25, 1)",
|
||||
},
|
||||
easing: { custom: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
|
||||
},
|
||||
overrides: {
|
||||
MuiChip: {
|
||||
MuiContainer: {
|
||||
root: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
outlined: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.08)",
|
||||
borderColor: "rgba(0, 0, 0, 0.08)",
|
||||
color: "rgba(0, 0, 0, 0.6)",
|
||||
},
|
||||
label: {
|
||||
// overline style
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: 13.4,
|
||||
fontWeight: "bold",
|
||||
fontStyle: "normal",
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: 1.125,
|
||||
},
|
||||
labelSmall: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 9,
|
||||
},
|
||||
deleteIcon: {
|
||||
color: "inherit",
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
rounded: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
MuiFormLabel: {
|
||||
root: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.4,
|
||||
"@supports (padding: max(0px))": {
|
||||
paddingLeft: `max(${themeBase.spacing(
|
||||
2
|
||||
)}px, env(safe-area-inset-left))`,
|
||||
paddingRight: `max(${themeBase.spacing(
|
||||
2
|
||||
)}px, env(safe-area-inset-right))`,
|
||||
|
||||
"@media (min-width: 640px)": {
|
||||
paddingLeft: `max(${themeBase.spacing(
|
||||
3
|
||||
)}px, env(safe-area-inset-left))`,
|
||||
paddingRight: `max(${themeBase.spacing(
|
||||
3
|
||||
)}px, env(safe-area-inset-right))`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTooltip: {
|
||||
tooltip: {
|
||||
fontFamily: HEADING_TEXT,
|
||||
fontSize: "0.8125rem",
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.4,
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
root: { fontSize: "1rem !important" },
|
||||
tooltip: themeBase.typography.caption,
|
||||
},
|
||||
MuiButton: {
|
||||
root: { minHeight: 36 },
|
||||
sizeSmall: { minHeight: 30 },
|
||||
sizeLarge: { minHeight: 48 },
|
||||
|
||||
contained: {
|
||||
borderRadius: 500,
|
||||
minHeight: 32,
|
||||
boxShadow: "none",
|
||||
},
|
||||
containedSizeLarge: {
|
||||
padding: "8px 32px",
|
||||
minHeight: 48,
|
||||
padding: themeBase.spacing(1, 4),
|
||||
},
|
||||
|
||||
outlinedPrimary: {
|
||||
// Same as outlined text field
|
||||
borderColor: "rgba(0, 0, 0, 0.23)",
|
||||
},
|
||||
outlinedSizeLarge: {
|
||||
padding: themeBase.spacing(1, 4),
|
||||
borderRadius: 500,
|
||||
|
||||
"&$outlinedPrimary": { borderColor: ANTLER_RED },
|
||||
},
|
||||
},
|
||||
MuiSvgIcon: {
|
||||
fontSizeLarge: { fontSize: toRem(36) },
|
||||
},
|
||||
// Override text field label
|
||||
MuiFormLabel: {
|
||||
root: {
|
||||
...themeBase.typography.subtitle2,
|
||||
lineHeight: 1,
|
||||
},
|
||||
},
|
||||
// Override radio & checkbox labels
|
||||
MuiFormControlLabel: {
|
||||
root: { display: "flex" },
|
||||
label: themeBase.typography.body1,
|
||||
},
|
||||
MuiChip: {
|
||||
root: {
|
||||
borderRadius: 4,
|
||||
maxWidth: "100%",
|
||||
|
||||
height: "auto",
|
||||
minHeight: 32,
|
||||
|
||||
color: themeBase.palette.text.secondary,
|
||||
},
|
||||
label: {
|
||||
...themeBase.typography.caption,
|
||||
color: "inherit",
|
||||
padding: themeBase.spacing(1, 1.5),
|
||||
// whiteSpace: "normal",
|
||||
|
||||
"$outlined &": {
|
||||
paddingTop: themeBase.spacing(0.875),
|
||||
paddingBottom: themeBase.spacing(0.875),
|
||||
},
|
||||
},
|
||||
sizeSmall: { minHeight: 24 },
|
||||
labelSmall: {
|
||||
padding: themeBase.spacing(0.5, 1.5),
|
||||
},
|
||||
|
||||
outlined: {
|
||||
backgroundColor: themeBase.palette.action.selected,
|
||||
borderColor: themeBase.palette.action.selected,
|
||||
},
|
||||
outlinedPrimary: {
|
||||
backgroundColor: fade(
|
||||
ANTLER_RED,
|
||||
themeBase.palette.action.selectedOpacity
|
||||
),
|
||||
},
|
||||
|
||||
deleteIcon: { color: "inherit" },
|
||||
},
|
||||
MuiBadge: {
|
||||
badge: {
|
||||
...themeBase.typography.caption,
|
||||
fontFeatureSettings: '"tnum"',
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
rounded: { borderRadius: 8 },
|
||||
},
|
||||
MuiSlider: {
|
||||
disabled: {},
|
||||
rail: {
|
||||
backgroundColor: "#e7e7e7",
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
mark: {
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
marginLeft: -2,
|
||||
marginTop: -1,
|
||||
backgroundColor: "#69696a",
|
||||
"$disabled &": { backgroundColor: "currentColor" },
|
||||
},
|
||||
markActive: {
|
||||
opacity: 1,
|
||||
backgroundColor: "currentColor",
|
||||
"$disabled &": { backgroundColor: "currentColor" },
|
||||
},
|
||||
|
||||
thumb: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginTop: -7,
|
||||
marginLeft: -8,
|
||||
|
||||
"$disabled &": {
|
||||
width: 12,
|
||||
height: 12,
|
||||
marginTop: -5,
|
||||
marginLeft: -6,
|
||||
},
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
top: -22,
|
||||
...themeBase.typography.caption,
|
||||
color: themeBase.palette.primary.main,
|
||||
|
||||
"& > *": {
|
||||
width: "auto",
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
|
||||
whiteSpace: "nowrap",
|
||||
borderRadius: 500,
|
||||
|
||||
padding: themeBase.spacing(0, 1),
|
||||
paddingRight: themeBase.spacing(0.875),
|
||||
},
|
||||
"& *": { transform: "none" },
|
||||
},
|
||||
markLabel: themeBase.typography.caption,
|
||||
},
|
||||
MuiLinearProgress: {
|
||||
colorPrimary: { backgroundColor: "#e7e7e7" },
|
||||
colorSecondary: { backgroundColor: "#e7e7e7" },
|
||||
},
|
||||
},
|
||||
props: {
|
||||
MuiTypography: {
|
||||
variantMapping: {
|
||||
subtitle1: "div",
|
||||
subtitle2: "div",
|
||||
},
|
||||
},
|
||||
MuiRadio: { color: "primary" },
|
||||
MuiCheckbox: { color: "primary" },
|
||||
MuiButton: { color: "primary" },
|
||||
MuiTabs: {
|
||||
indicatorColor: "primary",
|
||||
textColor: "primary",
|
||||
},
|
||||
MuiCircularProgress: { size: 44 },
|
||||
// Select: show dropdown below text field to follow new Material spec
|
||||
MuiSelect: {
|
||||
@@ -176,13 +297,17 @@ const Theme = createMuiTheme({
|
||||
transformOrigin: { vertical: "top", horizontal: "center" },
|
||||
},
|
||||
},
|
||||
MuiLink: {
|
||||
color: "primary",
|
||||
underline: "hover",
|
||||
},
|
||||
MuiChip: {
|
||||
size: "medium",
|
||||
variant: "outlined",
|
||||
deleteIcon: <ClearIcon />,
|
||||
},
|
||||
MuiButton: { color: "primary" },
|
||||
MuiTextField: { variant: "filled" },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const Theme = createMuiTheme(_merge(themeBase, defaultOverrides));
|
||||
export default Theme;
|
||||
|
||||
@@ -33,6 +33,10 @@ export interface IConnectTableSelectProps {
|
||||
TextFieldProps?: Partial<TextFieldProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Update this to use @antlerengineering/multiselect
|
||||
* This is a copy-paste of the old MultiSelect
|
||||
*/
|
||||
export default function ConnectTableSelect({
|
||||
value = [],
|
||||
className,
|
||||
|
||||
@@ -3,7 +3,7 @@ import _camelCase from "lodash/camelCase";
|
||||
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import useRouter from "../hooks/useRouter";
|
||||
import MultiSelect from "../components/MultiSelect";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import {
|
||||
Tooltip,
|
||||
Fab,
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { makeStyles, createStyles, Chip, ChipProps } from "@material-ui/core";
|
||||
|
||||
export const VARIANTS = ["yes", "no", "maybe"];
|
||||
|
||||
const useStyles = makeStyles(
|
||||
createStyles({
|
||||
yes: {
|
||||
@@ -23,11 +25,10 @@ const useStyles = makeStyles(
|
||||
export default function FormattedChip(props: ChipProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const variants = ["yes", "no", "maybe"];
|
||||
const label =
|
||||
typeof props.label === "string" ? props.label.toLowerCase() : "";
|
||||
|
||||
if (variants.includes(label)) {
|
||||
if (VARIANTS.includes(label)) {
|
||||
return (
|
||||
<Chip {...props} className={clsx(props.className, classes[label])} />
|
||||
);
|
||||
|
||||
237
www/src/components/Grid/AlgoliaFilters.tsx
Normal file
237
www/src/components/Grid/AlgoliaFilters.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SearchIndex } from "algoliasearch/lite";
|
||||
import { FacetHit } from "@algolia/client-search";
|
||||
import useAlgolia from "use-algolia";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
ListItemSecondaryAction,
|
||||
} from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
resetFilters: { marginRight: -theme.spacing(1) },
|
||||
|
||||
filterGrid: {
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
|
||||
listItemText: { whiteSpace: "pre-line" },
|
||||
count: {
|
||||
position: "static",
|
||||
marginLeft: "auto",
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
transform: "none",
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Generates the string to dispatch as filters for the query
|
||||
* @param filterValues The user-selected filters
|
||||
* @param requiredFilters Filters not selected by the user
|
||||
*/
|
||||
const generateFiltersString = (
|
||||
filterValues: Record<string, string[]>,
|
||||
requiredFilters?: string
|
||||
) => {
|
||||
if (Object.keys(filterValues).length === 0) return null;
|
||||
|
||||
let filtersString = Object.entries(filterValues)
|
||||
.filter(([, values]) => values.length > 0)
|
||||
.map(
|
||||
([facet, values]) =>
|
||||
`(${values
|
||||
.map(value => `${facet}:"${value.replace(/"/g, '\\"')}"`)
|
||||
.join(" OR ")})`
|
||||
)
|
||||
.join(" AND ");
|
||||
|
||||
if (requiredFilters) {
|
||||
if (filtersString)
|
||||
filtersString = requiredFilters + " AND " + filtersString;
|
||||
else filtersString = requiredFilters;
|
||||
}
|
||||
|
||||
return filtersString;
|
||||
};
|
||||
|
||||
export interface IAlgoliaFiltersProps {
|
||||
index: SearchIndex;
|
||||
request: ReturnType<typeof useAlgolia>[0]["request"];
|
||||
requestDispatch: ReturnType<typeof useAlgolia>[1];
|
||||
requiredFilters?: string;
|
||||
label: string;
|
||||
filters: {
|
||||
label: string;
|
||||
facet: string;
|
||||
labelTransformer?: (value: string) => string;
|
||||
}[];
|
||||
search?: boolean;
|
||||
}
|
||||
|
||||
export default function AlgoliaFilters({
|
||||
index,
|
||||
request,
|
||||
requestDispatch,
|
||||
requiredFilters,
|
||||
label,
|
||||
filters,
|
||||
search = true,
|
||||
}: IAlgoliaFiltersProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
// Store filter values
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string[]>>(
|
||||
{}
|
||||
);
|
||||
// Push filter values to dispatch
|
||||
useEffect(() => {
|
||||
const filtersString = generateFiltersString(filterValues, requiredFilters);
|
||||
if (filtersString === null) return;
|
||||
requestDispatch({ filters: filtersString });
|
||||
}, [filterValues]);
|
||||
|
||||
// Store facet values
|
||||
const [facetValues, setFacetValues] = useState<
|
||||
Record<string, readonly FacetHit[]>
|
||||
>({});
|
||||
// Get facet values
|
||||
useEffect(() => {
|
||||
if (!index) return;
|
||||
|
||||
filters.forEach(filter => {
|
||||
const params = { ...request, maxFacetHits: 100 };
|
||||
// Ignore current user-selected value for these filters so all options
|
||||
// continue to show up
|
||||
params.filters =
|
||||
generateFiltersString(
|
||||
{ ...filterValues, [filter.facet]: [] },
|
||||
requiredFilters
|
||||
) ?? "";
|
||||
|
||||
index
|
||||
.searchForFacetValues(filter.facet, "", params)
|
||||
.then(({ facetHits }) =>
|
||||
setFacetValues(other => ({ ...other, [filter.facet]: facetHits }))
|
||||
);
|
||||
});
|
||||
}, [filters, index, filterValues, requiredFilters]);
|
||||
|
||||
// Reset filters
|
||||
const handleResetFilters = () => {
|
||||
setFilterValues({});
|
||||
setQuery("");
|
||||
requestDispatch({ filters: requiredFilters ?? "", query: "" });
|
||||
};
|
||||
|
||||
// Store search query
|
||||
const [query, setQuery] = useState("");
|
||||
const [handleQueryChange] = useDebouncedCallback(
|
||||
(query: string) => requestDispatch({ query }),
|
||||
500
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={1} alignItems="center">
|
||||
<Grid item xs>
|
||||
<Typography variant="overline">
|
||||
Filter{label ? " " + label : "s"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleResetFilters}
|
||||
className={classes.resetFilters}
|
||||
disabled={query === "" && Object.keys(filterValues).length === 0}
|
||||
>
|
||||
Reset Filters
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
className={classes.filterGrid}
|
||||
>
|
||||
{search && (
|
||||
<Grid item xs={12} md={4} lg={3}>
|
||||
<TextField
|
||||
value={query}
|
||||
onChange={e => {
|
||||
setQuery(e.target.value);
|
||||
handleQueryChange(e.target.value);
|
||||
}}
|
||||
variant="filled"
|
||||
type="search"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
aria-label={`Search${label ? " " + label : ""}`}
|
||||
placeholder={`Search${label ? " " + label : ""}`}
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{filters.map(filter => (
|
||||
<Grid item key={filter.facet} xs={12} sm={6} md={4} lg={3}>
|
||||
<MultiSelect
|
||||
label={filter.label}
|
||||
value={filterValues[filter.facet] ?? []}
|
||||
onChange={value =>
|
||||
setFilterValues(other => ({ ...other, [filter.facet]: value }))
|
||||
}
|
||||
options={
|
||||
facetValues[filter.facet]?.map(item => ({
|
||||
value: item.value,
|
||||
label: filter.labelTransformer
|
||||
? filter.labelTransformer(item.value)
|
||||
: item.value,
|
||||
count: item.count,
|
||||
})) ?? []
|
||||
}
|
||||
itemRenderer={option => (
|
||||
<React.Fragment key={option.value}>
|
||||
{option.label}
|
||||
<ListItemSecondaryAction className={classes.count}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
component="span"
|
||||
>
|
||||
{(option as any).count}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</React.Fragment>
|
||||
)}
|
||||
searchable={facetValues[filter.facet]?.length > 10}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
249
www/src/components/Grid/Card/index.tsx
Normal file
249
www/src/components/Grid/Card/index.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
Card,
|
||||
Grid,
|
||||
Typography,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Divider,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from "@material-ui/core";
|
||||
import { ButtonProps } from "@material-ui/core/Button";
|
||||
import { GoIcon } from "@antlerengineering/components";
|
||||
|
||||
import useStyles from "./styles";
|
||||
export interface ICardProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
|
||||
overline?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
imageSource?: string;
|
||||
imageShape?: "square" | "circle";
|
||||
imageClassName?: string;
|
||||
|
||||
tabs?: {
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
bodyContent?: React.ReactNode;
|
||||
|
||||
primaryButton?: { label: string } & Partial<ButtonProps>;
|
||||
primaryLink?: {
|
||||
href?: string;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
label: string;
|
||||
} & Partial<ButtonProps>;
|
||||
secondaryAction?: React.ReactNode;
|
||||
}
|
||||
|
||||
const a11yProps = (index: number) => ({
|
||||
id: `full-width-tab-${index}`,
|
||||
"aria-controls": `full-width-tabpanel-${index}`,
|
||||
});
|
||||
|
||||
export default function BasicCard({
|
||||
className,
|
||||
style,
|
||||
|
||||
overline,
|
||||
title,
|
||||
imageSource,
|
||||
imageShape = "square",
|
||||
imageClassName,
|
||||
|
||||
tabs,
|
||||
bodyContent,
|
||||
|
||||
primaryButton,
|
||||
primaryLink,
|
||||
secondaryAction,
|
||||
}: ICardProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
const handleChangeTab = (event: React.ChangeEvent<{}>, newValue: number) =>
|
||||
setTab(newValue);
|
||||
|
||||
return (
|
||||
<Card className={clsx(classes.root, className)} style={style}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
className={classes.container}
|
||||
>
|
||||
<Grid item xs className={classes.cardContentContainer}>
|
||||
<CardContent className={clsx(classes.container, classes.cardContent)}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
className={classes.container}
|
||||
>
|
||||
{(overline || title || imageSource) && (
|
||||
<Grid item className={classes.headerContainer}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs>
|
||||
{overline && (
|
||||
<Typography
|
||||
variant="overline"
|
||||
className={classes.overline}
|
||||
>
|
||||
{overline}
|
||||
</Typography>
|
||||
)}
|
||||
{title && (
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{imageSource && (
|
||||
<Grid item>
|
||||
<CardMedia
|
||||
className={clsx(
|
||||
classes.image,
|
||||
imageShape === "circle" && classes.imageCircle,
|
||||
imageClassName
|
||||
)}
|
||||
image={imageSource}
|
||||
title={typeof title === "string" ? title : ""}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{tabs && (
|
||||
<Grid item className={classes.tabsContainer}>
|
||||
<Tabs
|
||||
className={classes.tabs}
|
||||
value={tab}
|
||||
onChange={handleChangeTab}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="fullWidth"
|
||||
aria-label="full width tabs"
|
||||
>
|
||||
{tabs?.map((tab, index) => (
|
||||
<Tab
|
||||
key={`card-tab-${index}`}
|
||||
className={classes.tab}
|
||||
label={tab.label}
|
||||
disabled={tab.disabled}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Divider className={clsx(classes.tabs, classes.tabDivider)} />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{(tabs || bodyContent) && (
|
||||
<Grid item xs className={classes.contentContainer}>
|
||||
{tabs && (
|
||||
<div className={classes.tabSection}>
|
||||
{tabs[tab].content && Array.isArray(tabs[tab].content) ? (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
justify="space-between"
|
||||
spacing={3}
|
||||
className={classes.tabContentGrid}
|
||||
>
|
||||
{(tabs[tab].content as React.ReactNode[]).map(
|
||||
(element, index) => (
|
||||
<Grid item key={`tab-content-${index}`}>
|
||||
{element}
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</Grid>
|
||||
) : (
|
||||
tabs[tab].content
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bodyContent && Array.isArray(bodyContent) ? (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
justify="space-between"
|
||||
className={classes.container}
|
||||
>
|
||||
{bodyContent.map((element, i) => (
|
||||
<Grid item key={i}>
|
||||
{element}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
bodyContent
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Grid>
|
||||
|
||||
{(primaryButton || primaryLink || secondaryAction) && (
|
||||
<Grid item>
|
||||
<Divider className={classes.divider} />
|
||||
<CardActions className={classes.cardActions}>
|
||||
<Grid item>
|
||||
{primaryButton && (
|
||||
<Button
|
||||
{...primaryButton}
|
||||
color={primaryButton.color || "primary"}
|
||||
disabled={!!primaryButton.disabled}
|
||||
endIcon={
|
||||
primaryButton.endIcon === undefined ? (
|
||||
<GoIcon />
|
||||
) : (
|
||||
primaryButton.endIcon
|
||||
)
|
||||
}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
{primaryLink && (
|
||||
<Button
|
||||
classes={{ label: classes.primaryLinkLabel }}
|
||||
{...(primaryLink as any)}
|
||||
color={primaryLink.color || "primary"}
|
||||
component="a"
|
||||
endIcon={
|
||||
primaryLink.endIcon === undefined ? (
|
||||
<GoIcon />
|
||||
) : (
|
||||
primaryLink.endIcon
|
||||
)
|
||||
}
|
||||
>
|
||||
{primaryLink.label}
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
{secondaryAction && <Grid item>{secondaryAction}</Grid>}
|
||||
</CardActions>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
58
www/src/components/Grid/Card/styles.ts
Normal file
58
www/src/components/Grid/Card/styles.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { makeStyles, createStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: { width: "100%", height: "100%" },
|
||||
container: { height: "100%" },
|
||||
|
||||
cardContentContainer: {
|
||||
"&:last-child": { paddingBottom: theme.spacing(3) },
|
||||
},
|
||||
cardContent: {
|
||||
"&:last-child": { paddingBottom: 0 },
|
||||
},
|
||||
|
||||
headerContainer: {},
|
||||
tabsContainer: { "$headerContainer + &": { marginTop: theme.spacing(2) } },
|
||||
contentContainer: {
|
||||
"$headerContainer + &": { marginTop: theme.spacing(2) },
|
||||
},
|
||||
|
||||
overline: {
|
||||
marginBottom: theme.spacing(3),
|
||||
color: theme.palette.text.disabled,
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
title: {
|
||||
whiteSpace: "pre-line",
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
image: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
imageCircle: { borderRadius: "50%" },
|
||||
|
||||
tabs: { margin: theme.spacing(0, -2) },
|
||||
tab: { minWidth: 0 },
|
||||
tabDivider: { marginTop: -1 },
|
||||
|
||||
tabSection: { paddingTop: theme.spacing(2), height: "100%" },
|
||||
tabContentGrid: { height: `calc(100% + ${theme.spacing(3)}px)` },
|
||||
|
||||
divider: {
|
||||
margin: theme.spacing(2),
|
||||
marginBottom: 0,
|
||||
},
|
||||
cardActions: {
|
||||
padding: theme.spacing(0.75, 1),
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
|
||||
primaryLinkLabel: { whiteSpace: "nowrap" },
|
||||
})
|
||||
);
|
||||
|
||||
export default useStyles;
|
||||
83
www/src/components/Grid/index.tsx
Normal file
83
www/src/components/Grid/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
|
||||
import { Grid as MuiGrid } from "@material-ui/core";
|
||||
import Card from "./Card";
|
||||
import useAlgolia from "use-algolia";
|
||||
import AlgoliaFilters from "./AlgoliaFilters";
|
||||
import _get from "lodash/get";
|
||||
const advisorsFilters = [
|
||||
{ label: "Type", facet: "type" },
|
||||
{ label: "Experience (Industry)", facet: "expertise" },
|
||||
{ label: "Location", facet: "location" },
|
||||
];
|
||||
|
||||
interface IGridProps {
|
||||
collection: string;
|
||||
filters: any[];
|
||||
}
|
||||
export const replacer = (data: any) => (m: string, key: string) => {
|
||||
const objKey = key.split(":")[0];
|
||||
const defaultValue = key.split(":")[1] || "";
|
||||
return _get(data, objKey, defaultValue);
|
||||
};
|
||||
|
||||
const CARD_CONFIG = {
|
||||
title: `{{firstName}} {{lastName}}`,
|
||||
image: "{{profilePhoto[0].downloadURL}}",
|
||||
body: "{{bio}}",
|
||||
};
|
||||
export default function Grid({ collection, filters }: IGridProps) {
|
||||
const [algoliaState, requestDispatch, , setAlgoliaConfig] = useAlgolia(
|
||||
process.env.REACT_APP_ALGOLIA_APP_ID!,
|
||||
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY!,
|
||||
collection,
|
||||
{ hitsPerPage: 100 }
|
||||
);
|
||||
|
||||
const isLoading = algoliaState.loading || !algoliaState.index;
|
||||
const noResults = algoliaState.hits.length === 0;
|
||||
const requiredFilters = ``;
|
||||
const isEmpty =
|
||||
noResults &&
|
||||
algoliaState.request?.query === undefined &&
|
||||
algoliaState.request?.filters === requiredFilters;
|
||||
return (
|
||||
<>
|
||||
{algoliaState.index && !isEmpty && (
|
||||
<AlgoliaFilters
|
||||
index={algoliaState.index}
|
||||
request={algoliaState.request}
|
||||
requestDispatch={requestDispatch}
|
||||
requiredFilters={requiredFilters}
|
||||
label={collection}
|
||||
filters={[]}
|
||||
search
|
||||
/>
|
||||
)}
|
||||
<MuiGrid container spacing={4}>
|
||||
{" "}
|
||||
{algoliaState.hits.map(hit => {
|
||||
return (
|
||||
<MuiGrid key={hit.objectID} item lg={4} md={6} xs={12}>
|
||||
{" "}
|
||||
<Card
|
||||
bodyContent={CARD_CONFIG.body.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(hit)
|
||||
)}
|
||||
title={CARD_CONFIG.title.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(hit)
|
||||
)}
|
||||
imageSource={CARD_CONFIG.image.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(hit)
|
||||
)}
|
||||
/>{" "}
|
||||
</MuiGrid>
|
||||
);
|
||||
})}
|
||||
</MuiGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
TextField,
|
||||
Chip,
|
||||
Grid,
|
||||
Button,
|
||||
Typography,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Portal,
|
||||
} from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import SelectedIcon from "@material-ui/icons/Check";
|
||||
import RadioButtonUncheckedIcon from "@material-ui/icons/RadioButtonUnchecked";
|
||||
import RadioButtonCheckedIcon from "@material-ui/icons/RadioButtonChecked";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
import useStyles from "./styles";
|
||||
import { IMultiSelectProps, OptionType } from ".";
|
||||
|
||||
export interface IPopupContentsProps
|
||||
extends Omit<
|
||||
IMultiSelectProps,
|
||||
"options" | "label" | "className" | "TextFieldProps"
|
||||
> {
|
||||
options: OptionType[];
|
||||
|
||||
dropdownWidth: number;
|
||||
setDropdownWidth: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contents of the popup of the MultiSelect dropdown.
|
||||
* Broken out into separate component since the effects and functions here
|
||||
* are quite heavy, so they will only be mounted/executed when the dropdown
|
||||
* is open.
|
||||
*/
|
||||
export default function PopupContents({
|
||||
onChange,
|
||||
value = [],
|
||||
searchable = true,
|
||||
itemRenderer,
|
||||
freeText = false,
|
||||
multiple = true,
|
||||
selectAll = true,
|
||||
|
||||
options,
|
||||
|
||||
dropdownWidth,
|
||||
setDropdownWidth,
|
||||
}: IPopupContentsProps) {
|
||||
const classes = useStyles({
|
||||
searchable,
|
||||
freeText,
|
||||
multiple,
|
||||
width: dropdownWidth,
|
||||
});
|
||||
|
||||
const [filterState, setFilterState] = useState("");
|
||||
const select = option => {
|
||||
if (multiple) onChange([...value, option.value]);
|
||||
else onChange([option.value]);
|
||||
};
|
||||
const deselect = (option: { label: string; value: string }) => {
|
||||
if (multiple) onChange(value?.filter(v => v !== option.value));
|
||||
else onChange([]);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => onChange(options.map(o => o.value));
|
||||
const clearSelection = () => onChange([]);
|
||||
|
||||
// `freeText`: Handle custom field
|
||||
const [customField, setCustomField] = useState("");
|
||||
const handleAddCustom = () => {
|
||||
setCustomField("");
|
||||
// Prevent duplicate being added
|
||||
if (value.includes(customField)) return;
|
||||
select({ value: customField, label: customField });
|
||||
};
|
||||
|
||||
// Get longest item label
|
||||
const longestLabel = options.reduce(
|
||||
(acc, curr) => (curr.label?.length > acc.length ? curr.label : acc),
|
||||
""
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid container direction="column">
|
||||
{searchable && (
|
||||
<Grid item className={classes.searchRow}>
|
||||
<TextField
|
||||
value={filterState}
|
||||
onChange={e => setFilterState(e.target.value)}
|
||||
fullWidth
|
||||
variant="filled"
|
||||
margin="dense"
|
||||
label="Search items"
|
||||
className={classes.noMargins}
|
||||
InputProps={{
|
||||
//disableUnderline: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs className={classes.chipListRow}>
|
||||
<ul className={classes.chipList}>
|
||||
{options
|
||||
.filter(option => {
|
||||
return (
|
||||
filterState === "" ||
|
||||
option.label.toUpperCase().includes(filterState.toUpperCase())
|
||||
);
|
||||
})
|
||||
.map(option => {
|
||||
const isSelected = value?.includes(option.value);
|
||||
|
||||
let icon = <></>;
|
||||
if (multiple) {
|
||||
if (isSelected) icon = <SelectedIcon />;
|
||||
} else {
|
||||
if (isSelected) icon = <RadioButtonCheckedIcon />;
|
||||
else icon = <RadioButtonUncheckedIcon />;
|
||||
}
|
||||
|
||||
if (itemRenderer)
|
||||
return itemRenderer(option, select, deselect, isSelected);
|
||||
else
|
||||
return (
|
||||
<Chip
|
||||
key={`select-chip-${option.value}`}
|
||||
className={clsx(
|
||||
classes.chip,
|
||||
isSelected && classes.selectedChip
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (isSelected) deselect(option);
|
||||
else select(option);
|
||||
}}
|
||||
icon={icon}
|
||||
label={option.label}
|
||||
variant="outlined"
|
||||
component="li"
|
||||
size="medium"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Grid>
|
||||
|
||||
{freeText && (
|
||||
<Grid item className={classes.footerRow}>
|
||||
<Grid container alignItems="center" spacing={1}>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
onClick={handleAddCustom}
|
||||
className={classes.addCustomButton}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="filled"
|
||||
label="Add new item"
|
||||
margin="dense"
|
||||
className={classes.noMargins}
|
||||
value={customField}
|
||||
onChange={e => setCustomField(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
if (e.key === "Enter") handleAddCustom();
|
||||
}}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{multiple && (
|
||||
<Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justify="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
className={classes.selectedNum}
|
||||
>
|
||||
{value?.length} of {options.length}
|
||||
</Typography>
|
||||
<Button
|
||||
disabled={selectAll === false && value?.length === 0}
|
||||
onClick={
|
||||
value?.length === options.length || !selectAll
|
||||
? clearSelection
|
||||
: handleSelectAll
|
||||
}
|
||||
color="primary"
|
||||
className={classes.selectAllButton}
|
||||
>
|
||||
{value?.length === options.length || !selectAll
|
||||
? "Clear Selection"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Portal>
|
||||
<Chip
|
||||
className={clsx(classes.chip, classes.measureChip)}
|
||||
icon={<SelectedIcon />}
|
||||
label={longestLabel}
|
||||
variant="outlined"
|
||||
role="presentation"
|
||||
ref={el => {
|
||||
if (!el) return;
|
||||
const width = el.getBoundingClientRect().width;
|
||||
if (dropdownWidth < width) setDropdownWidth(width + 32);
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import _unionWith from "lodash/unionWith";
|
||||
import _find from "lodash/find";
|
||||
import { TextField, TextFieldProps } from "@material-ui/core";
|
||||
|
||||
import useStyles from "./styles";
|
||||
import PopupContents from "./PopupContents";
|
||||
|
||||
export type OptionType = { label: string; value: string; data?: any };
|
||||
|
||||
export interface IMultiSelectProps {
|
||||
label: string;
|
||||
value: string[];
|
||||
editable?: boolean;
|
||||
/** The list of options to display. Passing `string[]` will auto-transform */
|
||||
options: OptionType[] | string[];
|
||||
itemRenderer?: (
|
||||
option: OptionType,
|
||||
select: Function,
|
||||
deselect: Function,
|
||||
isSelected: Boolean
|
||||
) => React.ReactNode;
|
||||
searchable?: boolean;
|
||||
onChange: (value: string[]) => void;
|
||||
|
||||
/** Optionally allow the user to select all options */
|
||||
selectAll?: boolean;
|
||||
/** Optionally allow the user to add a custom option */
|
||||
freeText?: boolean;
|
||||
/** Optionally set this prop to `false` to only allow one option */
|
||||
multiple?: boolean;
|
||||
/** Optional style overrides for root MUI `TextField` component */
|
||||
className?: string;
|
||||
/** Override any props of the root MUI `TextField` component */
|
||||
TextFieldProps?: Partial<TextFieldProps>;
|
||||
}
|
||||
|
||||
export default function MultiSelect({
|
||||
options: optionsProp,
|
||||
label,
|
||||
className,
|
||||
TextFieldProps = {},
|
||||
...props
|
||||
}: IMultiSelectProps) {
|
||||
const {
|
||||
value = [],
|
||||
searchable = true,
|
||||
freeText = false,
|
||||
multiple = true,
|
||||
} = props;
|
||||
|
||||
const [dropdownWidth, setDropdownWidth] = useState(200);
|
||||
const classes = useStyles({
|
||||
searchable,
|
||||
freeText,
|
||||
multiple,
|
||||
width: dropdownWidth,
|
||||
});
|
||||
|
||||
const sanitisedValue = value.filter(v => v?.length > 0);
|
||||
|
||||
// Transform `option` prop if it’s just strings
|
||||
let options =
|
||||
typeof optionsProp[0] === "string"
|
||||
? (optionsProp as string[]).map(
|
||||
item => ({ label: item, value: item } as OptionType)
|
||||
)
|
||||
: (optionsProp as OptionType[]);
|
||||
// If `freeText` enabled, show the user’s custom fields
|
||||
if (freeText) {
|
||||
// `value` prop is an array of all values. It removes labels
|
||||
const formattedValues = sanitisedValue?.map(x => ({ label: x, value: x }));
|
||||
options = _unionWith(
|
||||
options,
|
||||
formattedValues,
|
||||
(a, b) => a.value === b.value
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TextField
|
||||
label={label}
|
||||
variant={"filled" as any}
|
||||
select
|
||||
value={sanitisedValue}
|
||||
className={clsx(classes.root, className)}
|
||||
{...TextFieldProps}
|
||||
SelectProps={{
|
||||
renderValue: value => {
|
||||
const selected = value as string[];
|
||||
if (selected.length === 1 && typeof selected[0] === "string") {
|
||||
const selectedOption = _find(options, { value: selected[0] });
|
||||
return selectedOption?.label;
|
||||
}
|
||||
return `${selected.length} of ${options.length} selected`;
|
||||
},
|
||||
displayEmpty: true,
|
||||
classes: { root: classes.selectRoot },
|
||||
...TextFieldProps.SelectProps,
|
||||
// Must have this set to prevent MUI transforming `value`
|
||||
// prop for this component to a comma-separated string
|
||||
multiple: true,
|
||||
MenuProps: {
|
||||
classes: { paper: classes.paper, list: classes.menuChild },
|
||||
MenuListProps: { disablePadding: true },
|
||||
getContentAnchorEl: null,
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "center" },
|
||||
transformOrigin: { vertical: "top", horizontal: "center" },
|
||||
...TextFieldProps.SelectProps?.MenuProps,
|
||||
},
|
||||
}}
|
||||
ref={el => {
|
||||
if (!el) return;
|
||||
const width = el.getBoundingClientRect().width;
|
||||
if (dropdownWidth < width) setDropdownWidth(width);
|
||||
}}
|
||||
>
|
||||
<div className={classes.popupContentsWrapper}>
|
||||
<PopupContents
|
||||
{...props}
|
||||
options={options}
|
||||
dropdownWidth={dropdownWidth}
|
||||
setDropdownWidth={setDropdownWidth}
|
||||
/>
|
||||
</div>
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { makeStyles, createStyles } from "@material-ui/core";
|
||||
|
||||
interface StylesProps {
|
||||
searchable: boolean;
|
||||
freeText: boolean;
|
||||
multiple: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: { minWidth: 200 },
|
||||
selectRoot: { paddingRight: theme.spacing(4) },
|
||||
|
||||
paper: { overflow: "hidden", maxHeight: "calc(100% - 48px)" },
|
||||
popupContentsWrapper: { outline: 0 },
|
||||
menuChild: {
|
||||
padding: `0 ${theme.spacing(2)}px`,
|
||||
width: ({ width }: StylesProps) => width || 480,
|
||||
maxWidth: `calc(100vw - ${theme.spacing(4)}px)`,
|
||||
minWidth: 300,
|
||||
},
|
||||
|
||||
noMargins: { margin: 0 },
|
||||
|
||||
searchRow: { marginTop: theme.spacing(2) },
|
||||
|
||||
chipListRow: {
|
||||
background: `${theme.palette.background.paper} no-repeat`,
|
||||
backgroundImage:
|
||||
"linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,0)), linear-gradient(to top, rgba(255,255,255,1), rgba(255,255,255,0))",
|
||||
backgroundPosition: `-12px 0, -24px 100%`,
|
||||
backgroundSize: `calc(100% + 12px + 12px) 16px`,
|
||||
|
||||
position: "relative",
|
||||
|
||||
"&::before, &::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 9,
|
||||
|
||||
display: "block",
|
||||
height: 16,
|
||||
|
||||
background: `linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0))`,
|
||||
},
|
||||
|
||||
"&::after": {
|
||||
top: "auto",
|
||||
bottom: 0,
|
||||
background: `linear-gradient(to top, #fff, rgba(255, 255, 255, 0))`,
|
||||
},
|
||||
},
|
||||
chipList: ({ searchable, freeText, multiple }: StylesProps) => {
|
||||
let maxHeightDeductions = 0;
|
||||
if (searchable) maxHeightDeductions -= 64;
|
||||
if (multiple) maxHeightDeductions -= 48;
|
||||
if (freeText) maxHeightDeductions -= 48;
|
||||
if (freeText && !multiple) maxHeightDeductions -= theme.spacing(2);
|
||||
|
||||
return {
|
||||
margin: `0px -${theme.spacing(2)}px`,
|
||||
padding: `12px ${theme.spacing(2) - theme.spacing(0.5)}px`,
|
||||
overflowY: "auto" as "auto",
|
||||
maxHeight: `calc(100vh - 48px - ${-maxHeightDeductions}px)`,
|
||||
};
|
||||
},
|
||||
chip: {
|
||||
margin: theme.spacing(0.5),
|
||||
// Allow multi-line chip
|
||||
maxWidth: `calc(100% - ${theme.spacing(1)}px)`,
|
||||
},
|
||||
selectedChip: { backgroundColor: theme.palette.divider },
|
||||
|
||||
footerRow: { marginBottom: theme.spacing(2) },
|
||||
addCustomButton: { marginLeft: -theme.spacing(1) },
|
||||
selectedRow: {
|
||||
"$chipListRow + &": { marginTop: -theme.spacing(1) },
|
||||
"$footerRow + &": { marginTop: -theme.spacing(2) },
|
||||
|
||||
marginBottom: 0,
|
||||
"& > div": { height: 48 },
|
||||
},
|
||||
selectAllButton: { marginRight: -theme.spacing(1) },
|
||||
selectedNum: { fontFeatureSettings: '"tnum"' },
|
||||
|
||||
measureChip: {
|
||||
visibility: "hidden",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default useStyles;
|
||||
@@ -32,7 +32,7 @@ const useStyles = makeStyles(theme =>
|
||||
"&:hover": { color: theme.palette.primary.dark },
|
||||
},
|
||||
|
||||
"& ul": {
|
||||
"& ul, & ol": {
|
||||
margin: 0,
|
||||
paddingLeft: "1.5em",
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function DatePicker(props: KeyboardDatePickerProps) {
|
||||
transformedValue = props.field.value;
|
||||
|
||||
const handleChange = (date: Date | null) => {
|
||||
if (!date || isNaN(date.valueOf())) return;
|
||||
if (isNaN(date?.valueOf() ?? 0)) return;
|
||||
props.form.setFieldValue(props.field.name, date);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function DateTimePicker(props: KeyboardDateTimePickerProps) {
|
||||
transformedValue = props.field.value;
|
||||
|
||||
const handleChange = (date: Date | null) => {
|
||||
if (!date || isNaN(date.valueOf())) return;
|
||||
if (isNaN(date?.valueOf() ?? 0)) return;
|
||||
props.form.setFieldValue(props.field.name, date);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import { FieldProps } from "formik";
|
||||
|
||||
import { useTheme, Grid } from "@material-ui/core";
|
||||
|
||||
import MultiSelect_, {
|
||||
IMultiSelectProps as IMultiSelectProps_,
|
||||
} from "components/MultiSelect";
|
||||
import MultiSelect_, { MultiSelectProps } from "@antlerengineering/multiselect";
|
||||
import FormattedChip from "components/FormattedChip";
|
||||
|
||||
export default function MultiSelect({
|
||||
@@ -13,22 +11,24 @@ export default function MultiSelect({
|
||||
form,
|
||||
editable,
|
||||
...props
|
||||
}: FieldProps<string[]> & IMultiSelectProps_) {
|
||||
}: FieldProps<string[]> & MultiSelectProps<string> & { editable?: boolean }) {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleDelete = (index: number) => () => {
|
||||
const newValues = [...field.value];
|
||||
newValues.splice(index, 1);
|
||||
form.setFieldValue(field.name, newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiSelect_
|
||||
{...props}
|
||||
multiple
|
||||
value={field.value}
|
||||
onChange={value => form.setFieldValue(field.name, value)}
|
||||
disabled={editable === false}
|
||||
TextFieldProps={{
|
||||
disabled: editable === false,
|
||||
fullWidth: true,
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
error: !!(form.touched[field.name] && form.errors[field.name]),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FieldProps } from "formik";
|
||||
|
||||
import { useTheme } from "@material-ui/core";
|
||||
|
||||
import MultiSelect, { IMultiSelectProps } from "components/MultiSelect";
|
||||
import MultiSelect, { MultiSelectProps } from "@antlerengineering/multiselect";
|
||||
import FormattedChip from "components/FormattedChip";
|
||||
|
||||
/**
|
||||
@@ -13,22 +13,22 @@ import FormattedChip from "components/FormattedChip";
|
||||
export default function SingleSelect({
|
||||
field,
|
||||
form,
|
||||
editable,
|
||||
...props
|
||||
}: FieldProps<string[]> & IMultiSelectProps) {
|
||||
}: FieldProps<string> & MultiSelectProps<string> & { editable: boolean }) {
|
||||
const theme = useTheme();
|
||||
|
||||
const value = ([field.value] as unknown) as string[];
|
||||
const handleChange = value =>
|
||||
form.setFieldValue(field.name, value.join(", "));
|
||||
const handleChange = value => form.setFieldValue(field.name, value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
{...props}
|
||||
value={value}
|
||||
multiple={false}
|
||||
value={field.value}
|
||||
onChange={handleChange}
|
||||
disabled={editable === false}
|
||||
TextFieldProps={{
|
||||
fullWidth: true,
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
error: !!(form.touched[field.name] && form.errors[field.name]),
|
||||
@@ -37,8 +37,7 @@ export default function SingleSelect({
|
||||
onBlur: () => form.setFieldTouched(field.name),
|
||||
}}
|
||||
searchable
|
||||
freeText
|
||||
multiple={false}
|
||||
freeText={false}
|
||||
/>
|
||||
|
||||
{field.value?.length > 0 && (
|
||||
|
||||
78
www/src/components/SideDrawer/Form/Fields/SubTable.tsx
Normal file
78
www/src/components/SideDrawer/Form/Fields/SubTable.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from "react";
|
||||
import { FieldProps } from "formik";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import useRouter from "hooks/useRouter";
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Grid,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import LaunchIcon from "@material-ui/icons/Launch";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
labelContainer: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor:
|
||||
theme.palette.type === "light"
|
||||
? "rgba(0, 0, 0, 0.09)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
padding: theme.spacing(9 / 8, 1, 9 / 8, 1.5),
|
||||
|
||||
textAlign: "left",
|
||||
minHeight: 56,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function SubTable({
|
||||
form,
|
||||
field,
|
||||
label,
|
||||
parentLabel,
|
||||
}: FieldProps<any> & { parentLabel?: string; label: string }) {
|
||||
const classes = useStyles();
|
||||
|
||||
const router = useRouter();
|
||||
const parentLabels = queryString.parse(router.location.search).parentLabel;
|
||||
|
||||
let subTablePath = "";
|
||||
if (parentLabels)
|
||||
subTablePath =
|
||||
encodeURIComponent(`${form.values.ref.path}/${field.name}`) +
|
||||
`?parentLabel=${parentLabels},${
|
||||
parentLabel ? form.values[parentLabel] : ""
|
||||
}`;
|
||||
else
|
||||
subTablePath =
|
||||
encodeURIComponent(`${form.values.ref.path}/${field.name}`) +
|
||||
`?parentLabel=${
|
||||
parentLabel ? encodeURIComponent(form.values[parentLabel]) : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Grid container wrap="nowrap">
|
||||
<Grid container alignItems="center" className={classes.labelContainer}>
|
||||
<Typography variant="body1">
|
||||
{label}
|
||||
{parentLabel && `: ${form.values[parentLabel]}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={subTablePath}
|
||||
style={{ width: 56 }}
|
||||
disabled={!subTablePath}
|
||||
>
|
||||
<LaunchIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -72,6 +72,9 @@ const ConnectTable = lazy(() =>
|
||||
"./Fields/ConnectTable" /* webpackChunkName: "SideDrawer-ConnectTable" */
|
||||
)
|
||||
);
|
||||
const SubTable = lazy(() =>
|
||||
import("./Fields/SubTable" /* webpackChunkName: "SideDrawer-SubTable" */)
|
||||
);
|
||||
const Action = lazy(() =>
|
||||
import("./Fields/Action" /* webpackChunkName: "SideDrawer-Action" */)
|
||||
);
|
||||
@@ -299,7 +302,11 @@ export default function Form({ fields, values }: IFormProps) {
|
||||
);
|
||||
break;
|
||||
|
||||
// case FieldType.subTable:
|
||||
case FieldType.subTable:
|
||||
renderedField = (
|
||||
<Field {...fieldProps} component={SubTable} />
|
||||
);
|
||||
break;
|
||||
|
||||
case FieldType.action:
|
||||
renderedField = (
|
||||
|
||||
@@ -87,6 +87,10 @@ export default function SideDrawer() {
|
||||
field.config = column.config;
|
||||
break;
|
||||
|
||||
case FieldType.subTable:
|
||||
field.parentLabel = column.parentLabel;
|
||||
break;
|
||||
|
||||
case FieldType.action:
|
||||
field.callableName = column.callableName;
|
||||
break;
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import useAlgolia from "hooks/useAlgolia";
|
||||
import { createStyles, makeStyles } from "@material-ui/core";
|
||||
|
||||
import MultiSelect from "components/MultiSelect";
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: { minWidth: 200 },
|
||||
})
|
||||
);
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
|
||||
const AlgoliaSelect = (props: any) => {
|
||||
const {
|
||||
@@ -17,11 +11,7 @@ const AlgoliaSelect = (props: any) => {
|
||||
labelReducer,
|
||||
filters,
|
||||
} = props;
|
||||
const [searchState, searchDispatch] = useAlgolia(
|
||||
algoliaIndex,
|
||||
algoliaKey,
|
||||
filters
|
||||
);
|
||||
const [searchState] = useAlgolia(algoliaIndex, algoliaKey, filters);
|
||||
|
||||
console.log(filters);
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
@@ -40,6 +30,7 @@ const AlgoliaSelect = (props: any) => {
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple
|
||||
options={options}
|
||||
{...props}
|
||||
searchable={options.length > 10} //shows type to filter after 10 options
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import FilterIcon from "@material-ui/icons/FilterList";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
|
||||
import MultiSelect from "components/MultiSelect";
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { FireTableFilter } from "hooks/useFiretable";
|
||||
@@ -207,32 +207,36 @@ const Filters = ({ columns, setFilters }: any) => {
|
||||
);
|
||||
|
||||
case FieldType.singleSelect:
|
||||
const val = query?.value
|
||||
? Array.isArray(query.value)
|
||||
? query.value
|
||||
: [query.value as string]
|
||||
: [];
|
||||
if (operator === "in")
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple
|
||||
onChange={value => setQuery(query => ({ ...query, value }))}
|
||||
options={selectedColumn.options}
|
||||
label=""
|
||||
value={Array.isArray(query?.value) ? query.value : []}
|
||||
TextFieldProps={{ hiddenLabel: true }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple={false}
|
||||
onChange={value => {
|
||||
if (operator === "==")
|
||||
setQuery(query => ({ ...query, value: value[0] }));
|
||||
else setQuery(query => ({ ...query, value }));
|
||||
if (value !== null) setQuery(query => ({ ...query, value }));
|
||||
}}
|
||||
options={selectedColumn.options}
|
||||
label=""
|
||||
value={val}
|
||||
multiple={operator === "in"}
|
||||
value={typeof query?.value === "string" ? query.value : null}
|
||||
TextFieldProps={{ hiddenLabel: true }}
|
||||
/>
|
||||
);
|
||||
|
||||
case FieldType.multiSelect:
|
||||
return (
|
||||
<MultiSelect
|
||||
onChange={value => {
|
||||
setQuery(query => ({ ...query, value }));
|
||||
}}
|
||||
multiple
|
||||
onChange={value => setQuery(query => ({ ...query, value }))}
|
||||
value={query.value as string[]}
|
||||
options={selectedColumn.options}
|
||||
label={""}
|
||||
|
||||
@@ -18,10 +18,11 @@ 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";
|
||||
|
||||
export const TABLE_HEADER_HEIGHT = 56;
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`,
|
||||
@@ -85,7 +86,16 @@ export default function TableHeader({
|
||||
>
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => tableActions?.row.add()}
|
||||
onClick={() => {
|
||||
const initialVal = columns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
tableActions?.row.add(initialVal);
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
@@ -118,7 +128,7 @@ export default function TableHeader({
|
||||
variant="filled"
|
||||
className={classes.formControl}
|
||||
value={rowHeight ?? 43}
|
||||
onChange={event => {
|
||||
onChange={(event) => {
|
||||
updateConfig("rowHeight", event.target.value);
|
||||
}}
|
||||
inputProps={{
|
||||
|
||||
@@ -79,6 +79,7 @@ class TextEditor extends React.Component<
|
||||
defaultValue={value}
|
||||
type={inputType}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{ ref: this.inputRef }}
|
||||
className={classes.root}
|
||||
InputProps={{
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { green } from "@material-ui/core/colors";
|
||||
import Confirmation from "components/Confirmation";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { paddingLeft: theme.spacing(1.5) },
|
||||
|
||||
@@ -22,12 +22,12 @@ const useStyles = makeStyles(theme =>
|
||||
overflowX: "hidden",
|
||||
},
|
||||
|
||||
switchBase: {
|
||||
"&$switchChecked": { color: green["A700"] },
|
||||
"&$switchChecked + $switchTrack": { backgroundColor: green["A700"] },
|
||||
},
|
||||
switchChecked: {},
|
||||
switchTrack: {},
|
||||
// switchBase: {
|
||||
// "&$switchChecked": { color: green["A700"] },
|
||||
// "&$switchChecked + $switchTrack": { backgroundColor: green["A700"] },
|
||||
// },
|
||||
// switchChecked: {},
|
||||
// switchTrack: {},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -49,11 +49,12 @@ export default function Checkbox({
|
||||
checked={!!value}
|
||||
onChange={() => onSubmit(!value)}
|
||||
disabled={!column.editable}
|
||||
classes={{
|
||||
switchBase: classes.switchBase,
|
||||
checked: classes.switchChecked,
|
||||
track: classes.switchTrack,
|
||||
}}
|
||||
classes={
|
||||
{
|
||||
// checked: classes.switchChecked,
|
||||
// track: classes.switchTrack,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function Date({
|
||||
|
||||
const [handleDateChange] = useDebouncedCallback<DatePickerProps["onChange"]>(
|
||||
date => {
|
||||
if (!date || isNaN(date.valueOf())) return;
|
||||
if (isNaN(date?.valueOf() ?? 0)) return;
|
||||
|
||||
onSubmit(date);
|
||||
if (dataGridRef?.current?.selectCell)
|
||||
@@ -75,8 +75,10 @@ export default function Date({
|
||||
onClick={e => e.stopPropagation()}
|
||||
format={fieldType === FieldType.date ? DATE_FORMAT : DATE_TIME_FORMAT}
|
||||
fullWidth
|
||||
clearable
|
||||
keyboardIcon={<Icon />}
|
||||
className={clsx("cell-collapse-padding", classes.root)}
|
||||
inputVariant="standard"
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.inputBase, input: classes.input },
|
||||
|
||||
@@ -4,43 +4,48 @@ import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { makeStyles, createStyles, Grid } from "@material-ui/core";
|
||||
|
||||
import MultiSelect_ from "components/MultiSelect";
|
||||
import FormattedChip from "components/FormattedChip";
|
||||
import MultiSelect_ from "@antlerengineering/multiselect";
|
||||
import FormattedChip, { VARIANTS } from "components/FormattedChip";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
|
||||
const useStyles = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
minWidth: 0,
|
||||
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
fullHeight: {
|
||||
|
||||
inputBase: {
|
||||
height: "100%",
|
||||
font: "inherit",
|
||||
color: "inherit",
|
||||
color: "inherit !important",
|
||||
letterSpacing: "inherit",
|
||||
},
|
||||
|
||||
select: {
|
||||
padding: theme.spacing(0, 3, 0, 1.5),
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
||||
whiteSpace: "pre-line",
|
||||
padding: theme.spacing(0, 4, 0, 1.5),
|
||||
"&&": { paddingRight: theme.spacing(4) },
|
||||
},
|
||||
icon: { marginRight: theme.spacing(1) },
|
||||
selectSingleLabel: {
|
||||
maxHeight: "100%",
|
||||
overflow: "hidden",
|
||||
},
|
||||
icon: { right: theme.spacing(1) },
|
||||
|
||||
chipList: {
|
||||
overflowX: "hidden",
|
||||
width: "100%",
|
||||
},
|
||||
chip: { cursor: "inherit" },
|
||||
chipLabel: { whiteSpace: "nowrap" },
|
||||
})
|
||||
);
|
||||
|
||||
@@ -58,67 +63,70 @@ export default function MultiSelect({
|
||||
// Support SingleSelect field
|
||||
const isSingle = (column as any).type === FieldType.singleSelect;
|
||||
|
||||
// If SingleSelect, transform string to array of strings
|
||||
const transformedValue = isSingle
|
||||
? (([value] as unknown) as string[])
|
||||
: value;
|
||||
// And support transforming array of strings back to string
|
||||
const handleChange = value => onSubmit(isSingle ? value.join(", ") : value);
|
||||
// Render chips or basic string
|
||||
const renderValue = isSingle
|
||||
? () =>
|
||||
typeof value === "string" && VARIANTS.includes(value.toLowerCase()) ? (
|
||||
<FormattedChip label={value} className={classes.chip} />
|
||||
) : (
|
||||
<span className={classes.selectSingleLabel}>{value}</span>
|
||||
)
|
||||
: () => (
|
||||
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
|
||||
{value?.map(
|
||||
item =>
|
||||
typeof item === "string" && (
|
||||
<Grid item key={item}>
|
||||
<FormattedChip
|
||||
label={item}
|
||||
className={classes.chip}
|
||||
classes={{ label: classes.chipLabel }}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
// Render chips
|
||||
const renderValue = value => (
|
||||
<Grid container spacing={1} wrap="nowrap" className={classes.chipList}>
|
||||
{value?.map(
|
||||
item =>
|
||||
typeof item === "string" && (
|
||||
<Grid item key={item}>
|
||||
<FormattedChip label={item} className={classes.chip} />
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const onClick = e => e.stopPropagation();
|
||||
const onClose = () => {
|
||||
const handleOpen = () => {
|
||||
if (dataGridRef?.current?.selectCell)
|
||||
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelect_
|
||||
value={transformedValue}
|
||||
onChange={handleChange}
|
||||
label={column.name}
|
||||
options={options}
|
||||
TextFieldProps={{
|
||||
disabled: column.editable === false,
|
||||
fullWidth: true,
|
||||
label: "",
|
||||
hiddenLabel: true,
|
||||
variant: "standard" as "filled",
|
||||
InputProps: {
|
||||
disableUnderline: true,
|
||||
classes: { root: classes.fullHeight },
|
||||
},
|
||||
SelectProps: {
|
||||
onClose,
|
||||
classes: {
|
||||
root: clsx(classes.fullHeight, classes.select),
|
||||
icon: classes.icon,
|
||||
},
|
||||
renderValue,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
onClick,
|
||||
}}
|
||||
searchable
|
||||
value={value === undefined ? (isSingle ? null : []) : value}
|
||||
onChange={onSubmit}
|
||||
freeText={false}
|
||||
className={clsx(classes.fullHeight, classes.root)}
|
||||
multiple={!isSingle}
|
||||
multiple={!isSingle as any}
|
||||
label={column.name}
|
||||
labelPlural={column.name}
|
||||
options={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
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import React from "react";
|
||||
import { CustomCellProps } from "./withCustomCell";
|
||||
|
||||
import { Link } from "@material-ui/core";
|
||||
|
||||
export default function Url({ value }: CustomCellProps) {
|
||||
if (!value) return null;
|
||||
const href = value.includes("http") ? value : `https://${value}`;
|
||||
return (
|
||||
<a href={value} target="_blank" rel="noopener noreferrer">
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="always"
|
||||
style={{ fontWeight: "bold" }}
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export enum routes {
|
||||
|
||||
table = "/table",
|
||||
tableWithId = "/table/:id",
|
||||
grid = "/grid",
|
||||
gridWithId = "/grid/:id",
|
||||
editor = "/editor",
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export const FiretableContextProvider: React.FC = ({ children }) => {
|
||||
fieldName: string,
|
||||
value: any
|
||||
) => {
|
||||
if (value === null || value === undefined) return;
|
||||
if (value === undefined) return;
|
||||
|
||||
const ftUser = firetableUser(currentUser);
|
||||
const _ft_updatedAt = new Date();
|
||||
|
||||
39
www/src/views/GridView.tsx
Normal file
39
www/src/views/GridView.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { Hidden } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
import Grid from "components/Grid";
|
||||
import SideDrawer from "components/SideDrawer";
|
||||
|
||||
import { FireTableFilter } from "hooks/useFiretable";
|
||||
import useRouter from "hooks/useRouter";
|
||||
|
||||
export default function GridView() {
|
||||
const router = useRouter();
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
|
||||
let filters: FireTableFilter[] = [];
|
||||
const parsed = queryString.parse(router.location.search);
|
||||
if (typeof parsed.filters === "string") {
|
||||
// decoded
|
||||
//[{"key":"cohort","operator":"==","value":"AMS1"}]
|
||||
filters = JSON.parse(parsed.filters);
|
||||
//TODO: json schema validator
|
||||
}
|
||||
|
||||
return (
|
||||
<Navigation tableCollection={tableCollection}>
|
||||
<Grid
|
||||
key={tableCollection}
|
||||
collection={tableCollection}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
<Hidden smDown>
|
||||
<SideDrawer />
|
||||
</Hidden>
|
||||
</Navigation>
|
||||
);
|
||||
}
|
||||
112
www/yarn.lock
112
www/yarn.lock
@@ -106,6 +106,20 @@
|
||||
"@algolia/logger-common" "4.1.0"
|
||||
"@algolia/requester-common" "4.1.0"
|
||||
|
||||
"@antlerengineering/components@^0.2.7":
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@antlerengineering/components/-/components-0.2.7.tgz#9d05d5ad841f85ab75d0c5b3bd8e37f1aa58a679"
|
||||
integrity sha512-3lK/3VO+hEdNkOsPComvGVxr/30rTre0W5H4kWQcmdUi7nkB65WMRjnUcu1/IGBctTGlZiygv38W9DterND/7A==
|
||||
dependencies:
|
||||
dompurify "^2.0.10"
|
||||
lodash "^4.17.15"
|
||||
lodash-es "^4.17.15"
|
||||
|
||||
"@antlerengineering/multiselect@^0.3.9":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@antlerengineering/multiselect/-/multiselect-0.3.9.tgz#a368c8b26227c37c80ac225d0007a999b0242997"
|
||||
integrity sha512-lSWjuiESiM4GgjSVF0eqWvVrSxQcFMB9dlO5stM21NtAH4UNnoTtEolKdfGataA3CPCEM3lFxSagiC3is2bK+Q==
|
||||
|
||||
"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
|
||||
@@ -1555,7 +1569,7 @@
|
||||
"@babel/traverse" "^7.6.2"
|
||||
jscodeshift-add-imports "^1.0.1"
|
||||
|
||||
"@material-ui/core@^4.7.1", "@material-ui/core@^4.9.4":
|
||||
"@material-ui/core@^4.7.1":
|
||||
version "4.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.9.7.tgz#0c1caf123278770f34c5d8e9ecd9e1314f87a621"
|
||||
integrity sha512-RTRibZgq572GHEskMAG4sP+bt3P3XyIkv3pOTR8grZAW2rSUd6JoGZLRM4S2HkuO7wS7cAU5SpU2s1EsmTgWog==
|
||||
@@ -1573,6 +1587,25 @@
|
||||
react-is "^16.8.0"
|
||||
react-transition-group "^4.3.0"
|
||||
|
||||
"@material-ui/core@^4.9.13":
|
||||
version "4.9.13"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.9.13.tgz#024962bcdda05139e1bad17a1815bf4088702b15"
|
||||
integrity sha512-GEXNwUr+laZ0N+F1efmHB64Fyg+uQIRXLqbSejg3ebSXgLYNpIjnMOPRfWdu4rICq0dAIgvvNXGkKDMcf3AMpA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
"@material-ui/react-transition-group" "^4.3.0"
|
||||
"@material-ui/styles" "^4.9.13"
|
||||
"@material-ui/system" "^4.9.13"
|
||||
"@material-ui/types" "^5.0.1"
|
||||
"@material-ui/utils" "^4.9.12"
|
||||
"@types/react-transition-group" "^4.2.0"
|
||||
clsx "^1.0.4"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
popper.js "^1.16.1-lts"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.0"
|
||||
react-transition-group "^4.3.0"
|
||||
|
||||
"@material-ui/icons@^4.5.1", "@material-ui/icons@^4.9.1":
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665"
|
||||
@@ -1580,10 +1613,10 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
|
||||
"@material-ui/lab@^4.0.0-alpha.44":
|
||||
version "4.0.0-alpha.46"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.46.tgz#453546d58d3a0064263c198dedb6a1c54d24d1a4"
|
||||
integrity sha512-JGgZmj1UNP8bbYNAGvndipjXRK3x2+9mFBzbX7MyCj+WpfnJbeqTmJK2No9MXvPj/EZJ1piaKif46FdDc4U93A==
|
||||
"@material-ui/lab@^4.0.0-alpha.52":
|
||||
version "4.0.0-alpha.52"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.52.tgz#a868d8c772a90db091a6bfc89aed21a0e2c486bf"
|
||||
integrity sha512-aDoRWA+q3/T2spvvQrxPz8qjtf9l8NhaG9ZISBy9zD3cJ05s3KfEP2nrsoBOBgonlSMFQQzUTnxAwThwsJfllw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
"@material-ui/utils" "^4.9.6"
|
||||
@@ -1603,6 +1636,38 @@
|
||||
react-transition-group "^4.0.0"
|
||||
rifm "^0.7.0"
|
||||
|
||||
"@material-ui/react-transition-group@^4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/react-transition-group/-/react-transition-group-4.3.0.tgz#92529142addb5cc179dbf42d246c7e3fe4d6104b"
|
||||
integrity sha512-CwQ0aXrlUynUTY6sh3UvKuvye1o92en20VGAs6TORnSxUYeRmkX8YeTUN3lAkGiBX1z222FxLFO36WWh6q73rQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@material-ui/styles@^4.9.13":
|
||||
version "4.9.13"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.13.tgz#08b3976bdd21c38bc076693d95834f97539f3b15"
|
||||
integrity sha512-lWlXJanBdHQ18jW/yphedRokHcvZD1GdGzUF/wQxKDsHwDDfO45ZkAxuSBI202dG+r1Ph483Z3pFykO2obeSRA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
"@emotion/hash" "^0.8.0"
|
||||
"@material-ui/types" "^5.0.1"
|
||||
"@material-ui/utils" "^4.9.6"
|
||||
clsx "^1.0.4"
|
||||
csstype "^2.5.2"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
jss "^10.0.3"
|
||||
jss-plugin-camel-case "^10.0.3"
|
||||
jss-plugin-default-unit "^10.0.3"
|
||||
jss-plugin-global "^10.0.3"
|
||||
jss-plugin-nested "^10.0.3"
|
||||
jss-plugin-props-sort "^10.0.3"
|
||||
jss-plugin-rule-value-function "^10.0.3"
|
||||
jss-plugin-vendor-prefixer "^10.0.3"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@material-ui/styles@^4.9.6":
|
||||
version "4.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.6.tgz#924a30bf7c9b91af9c8f19c12c8573b8a4ecd085"
|
||||
@@ -1625,6 +1690,15 @@
|
||||
jss-plugin-vendor-prefixer "^10.0.3"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@material-ui/system@^4.9.13":
|
||||
version "4.9.13"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.9.13.tgz#adefb3b6a5ddf0b00fe4e82ac63bb48276e9749d"
|
||||
integrity sha512-6AlpvdW6KJJ5bF1Xo2OD13sCN8k+nlL36412/bWnWZOKIfIMo/Lb8c8d1DOIaT/RKWxTEUaWnKZjabVnA3eZjA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
"@material-ui/utils" "^4.9.6"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@material-ui/system@^4.9.6":
|
||||
version "4.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.9.6.tgz#fd060540224da4d1740da8ca6e7af288e217717e"
|
||||
@@ -1639,6 +1713,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.0.0.tgz#26d6259dc6b39f4c2e1e9aceff7a11e031941741"
|
||||
integrity sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg==
|
||||
|
||||
"@material-ui/types@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.0.1.tgz#c4954063cdc196eb327ee62c041368b1aebb6d61"
|
||||
integrity sha512-wURPSY7/3+MAtng3i26g+WKwwNE3HEeqa/trDBR5+zWKmcjO+u9t7Npu/J1r+3dmIa/OeziN9D/18IrBKvKffw==
|
||||
|
||||
"@material-ui/utils@^4.9.12":
|
||||
version "4.9.12"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.9.12.tgz#0d639f1c1ed83fffb2ae10c21d15a938795d9e65"
|
||||
integrity sha512-/0rgZPEOcZq5CFA4+4n6Q6zk7fi8skHhH2Bcra8R3epoJEYy5PL55LuMazPtPH1oKeRausDV/Omz4BbgFsn1HQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.0"
|
||||
|
||||
"@material-ui/utils@^4.9.6":
|
||||
version "4.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.9.6.tgz#5f1f9f6e4df9c8b6a263293b68c94834248ff157"
|
||||
@@ -4997,6 +5085,11 @@ domhandler@^3.0.0:
|
||||
dependencies:
|
||||
domelementtype "^2.0.1"
|
||||
|
||||
dompurify@^2.0.10:
|
||||
version "2.0.11"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.11.tgz#cd47935774230c5e478b183a572e726300b3891d"
|
||||
integrity sha512-qVoGPjIW9IqxRij7klDQQ2j6nSe4UNWANBhZNLnsS7ScTtLb+3YdxkRY8brNTpkUiTtcXsCJO+jS0UCDfenLuA==
|
||||
|
||||
dompurify@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.8.tgz#6ef89d2d227d041af139c7b01d9f67ed59c2eb3c"
|
||||
@@ -8899,7 +8992,7 @@ locate-path@^5.0.0:
|
||||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lodash-es@^4.17.14, lodash-es@^4.2.1:
|
||||
lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.2.1:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
@@ -10643,7 +10736,7 @@ pnp-webpack-plugin@1.6.0:
|
||||
dependencies:
|
||||
ts-pnp "^1.1.2"
|
||||
|
||||
popper.js@^1.14.1:
|
||||
popper.js@^1.14.1, popper.js@^1.16.1-lts:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
|
||||
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
|
||||
@@ -14007,6 +14100,11 @@ url@^0.11.0:
|
||||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
use-algolia@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/use-algolia/-/use-algolia-1.3.0.tgz#a53623b9c248bad14ca1cba551658c625613cb72"
|
||||
integrity sha512-t6rY8apz0CmeNUbVchsi0+7UIeutJ8FPNfGTbx/f2vCw1LFYAo9ZUrG81vIsKMAhSLP2FKEXeztDWYr976MEpw==
|
||||
|
||||
use-debounce@^3.3.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-3.4.0.tgz#e61653fd4daad9beaa6e4695bc1d3fbd35f7e5b3"
|
||||
|
||||
Reference in New Issue
Block a user