├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ └── tweet_release.yml ├── .gitignore ├── .opensource └── project.json ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── cloud_functions ├── firebase.json └── functions │ ├── .prettierignore │ ├── .yarn │ └── releases │ │ └── yarn-1.22.5.cjs │ ├── .yarnrc │ ├── package.json │ ├── src │ ├── actionScript │ │ └── index.ts │ ├── aggregates │ │ └── index.ts │ ├── algolia │ │ └── index.ts │ ├── algoliaSearchKey.ts │ ├── backup.ts │ ├── buildTriggers │ │ └── index.ts │ ├── callable.ts │ ├── collectionSync │ │ └── index.ts │ ├── compressedThumbnail │ │ └── index.ts │ ├── config.ts │ ├── constants │ │ └── Collections.ts │ ├── derivatives │ │ └── index.ts │ ├── emailOnTrigger │ │ └── index.ts │ ├── functionConfig.ts │ ├── generateConfig.ts │ ├── history │ │ └── index.ts │ ├── index.ts │ ├── slackOnTrigger │ │ ├── index.ts │ │ └── trigger.ts │ ├── snapshotSync │ │ └── index.ts │ ├── subTableStats.ts │ ├── utils │ │ ├── auth.ts │ │ ├── email.ts │ │ └── index.ts │ └── webhooks │ │ └── index.ts │ ├── tsconfig.json │ ├── tslint.json │ ├── updateDeployStatus.ts │ └── yarn.lock ├── cloudbuild.yaml ├── cloudbuildfunctions.yaml ├── e2e ├── .gitignore ├── auth.ts ├── credentials.ts ├── index.ts ├── package.json └── yarn.lock ├── ft_actions ├── firebase.json ├── functions │ ├── .eslintrc.js │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── firebaseConfig.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── auth.ts │ │ │ └── index.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock └── yarn.lock ├── ft_build └── sparksLib │ └── template.ts ├── icon.png └── www ├── .env.development.enc ├── .env.enc ├── .env.example ├── .gitignore ├── .yarn └── releases │ └── yarn-1.22.5.cjs ├── .yarnrc ├── createDotEnv.js ├── firebase.json ├── package.json ├── public ├── _redirects ├── auth.d.ts ├── browserconfig.xml ├── favicon.ico ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── firetable.svg │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg ├── firestore.d.ts ├── index.html ├── manifest.json ├── robots.txt ├── site.webmanifest ├── static │ ├── tinymce_content-dark.css │ └── tinymce_content.css └── storage.d.ts ├── src ├── App.test.tsx ├── App.tsx ├── Theme │ └── antlerPalette.ts ├── Themes.tsx ├── analytics.ts ├── assets │ ├── FiretableLogo.tsx │ ├── antler.svg │ ├── bg-pattern.svg │ ├── firetable-with-wordmark.svg │ ├── firetable.svg │ └── icons │ │ ├── Action.tsx │ │ ├── AddColumn.tsx │ │ ├── AddColumnCircle.tsx │ │ ├── AddRow.tsx │ │ ├── AddRowCircle.tsx │ │ ├── Backburger.tsx │ │ ├── CellResize.tsx │ │ ├── ColumnPlusAfter.tsx │ │ ├── ColumnPlusBefore.tsx │ │ ├── ColumnRemove.tsx │ │ ├── ConnectTable.tsx │ │ ├── CopyCells.tsx │ │ ├── CornerResize.tsx │ │ ├── DarkTheme.tsx │ │ ├── Derivative.tsx │ │ ├── Export.tsx │ │ ├── FileUpload.tsx │ │ ├── Freeze.tsx │ │ ├── Go.tsx │ │ ├── Id.tsx │ │ ├── Import.tsx │ │ ├── Json.tsx │ │ ├── MultiSelect.tsx │ │ ├── Number.tsx │ │ ├── Percentage.tsx │ │ ├── Slider.tsx │ │ ├── Status.tsx │ │ ├── SubTable.tsx │ │ ├── Unfreeze.tsx │ │ └── Upload.tsx ├── components │ ├── AppBar.tsx │ ├── Auth │ │ ├── AuthLayout.tsx │ │ └── FirebaseUi.tsx │ ├── BuilderInstaller.tsx │ ├── ButtonWithStatus.tsx │ ├── CodeEditor.tsx │ ├── CodeEditorHelper │ │ └── index.tsx │ ├── Confirmation.tsx │ ├── ConfirmationDialog │ │ ├── Context.ts │ │ ├── Dialog.tsx │ │ ├── Provider.tsx │ │ ├── index.ts │ │ └── props.ts │ ├── ConnectServiceSelect │ │ ├── PopupContents.tsx │ │ ├── index.tsx │ │ └── styles.ts │ ├── EmptyState.tsx │ ├── ErrorBoundary.tsx │ ├── FormattedChip.tsx │ ├── Grid │ │ ├── AlgoliaFilters.tsx │ │ ├── Card │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── index.tsx │ ├── HelperText.tsx │ ├── HomeNavigation │ │ ├── NavDrawer.tsx │ │ └── index.tsx │ ├── Loading.tsx │ ├── Modal │ │ ├── SlideTransition.tsx │ │ └── index.tsx │ ├── Navigation │ │ ├── Breadcrumbs.tsx │ │ ├── NavDrawer.tsx │ │ ├── NavDrawerItem.tsx │ │ ├── Notifications │ │ │ └── index.tsx │ │ ├── UpdateChecker.tsx │ │ ├── UserMenu.tsx │ │ └── index.tsx │ ├── ProjectSettings │ │ ├── form.tsx │ │ └── index.tsx │ ├── RenderedHtml.tsx │ ├── RichTextEditor.tsx │ ├── RichTooltip.tsx │ ├── SideDrawer │ │ ├── Form │ │ │ ├── Autosave.tsx │ │ │ ├── FieldSkeleton.tsx │ │ │ ├── FieldWrapper.tsx │ │ │ ├── Label.tsx │ │ │ ├── Reset.tsx │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── index.tsx │ │ └── useStyles.ts │ ├── Snack.tsx │ ├── StyledCard.tsx │ ├── Table │ │ ├── BulkActions │ │ │ └── index.tsx │ │ ├── CellValidation.tsx │ │ ├── ColumnHeader.tsx │ │ ├── ColumnMenu │ │ │ ├── FieldSettings │ │ │ │ ├── DefaultValueInput.tsx │ │ │ │ ├── FormAutosave.tsx │ │ │ │ └── index.tsx │ │ │ ├── FieldsDropdown.tsx │ │ │ ├── MenuContents.tsx │ │ │ ├── NameChange.tsx │ │ │ ├── NewColumn.tsx │ │ │ ├── Subheading.tsx │ │ │ ├── TypeChange.tsx │ │ │ └── index.tsx │ │ ├── EmptyTable.tsx │ │ ├── Filters │ │ │ ├── Row.tsx │ │ │ └── index.tsx │ │ ├── FinalColumnHeader.tsx │ │ ├── HiddenFields.tsx │ │ ├── HotKeys.tsx │ │ ├── Settings │ │ │ ├── Menu.tsx │ │ │ ├── Webhooks.tsx │ │ │ └── index.tsx │ │ ├── Skeleton │ │ │ ├── HeaderRowSkeleton.tsx │ │ │ └── TableHeaderSkeleton.tsx │ │ ├── TableHeader │ │ │ ├── Export │ │ │ │ ├── Download.tsx │ │ │ │ ├── Export.tsx │ │ │ │ └── index.tsx │ │ │ ├── ImportCsv.tsx │ │ │ ├── ReExecute.tsx │ │ │ ├── Sparks.tsx │ │ │ ├── TableHeaderButton.tsx │ │ │ ├── TableLogs.tsx │ │ │ ├── TableSettings.tsx │ │ │ └── index.tsx │ │ ├── editors │ │ │ ├── CodeEditor.tsx │ │ │ ├── NullEditor.tsx │ │ │ ├── TextEditor.tsx │ │ │ ├── styles.ts │ │ │ └── withSideDrawerEditor.tsx │ │ ├── formatters │ │ │ └── FinalColumn.tsx │ │ ├── index.tsx │ │ └── styles.ts │ ├── TableSettings │ │ ├── form.tsx │ │ └── index.tsx │ ├── Thumbnail.tsx │ ├── Wizards │ │ ├── Cell.tsx │ │ ├── Column.tsx │ │ ├── FadeList.tsx │ │ ├── ImportCsvWizard │ │ │ ├── Step1Columns.tsx │ │ │ ├── Step2NewColumns.tsx │ │ │ ├── Step3Preview.tsx │ │ │ └── index.tsx │ │ ├── ImportWizard │ │ │ ├── Step1Columns.tsx │ │ │ ├── Step2Rename.tsx │ │ │ ├── Step3Types.tsx │ │ │ ├── Step4Preview.tsx │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ └── WizardDialog.tsx │ └── fields │ │ ├── Action │ │ ├── ActionFab.tsx │ │ ├── BasicCell.tsx │ │ ├── FormDialog │ │ │ ├── Context.ts │ │ │ ├── Dialog.tsx │ │ │ ├── Provider.tsx │ │ │ ├── index.ts │ │ │ └── props.ts │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── Aggregate │ │ ├── Settings.tsx │ │ └── index.tsx │ │ ├── Checkbox │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ ├── index.tsx │ │ └── styles.ts │ │ ├── Code │ │ ├── BasicCell.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── Color │ │ ├── InlineCell.tsx │ │ ├── PopoverCell.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── ConnectService │ │ ├── ConnectServiceSelect │ │ │ ├── PopupContents.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── InlineCell.tsx │ │ ├── PopoverCell.tsx │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── ConnectTable │ │ ├── ConnectTableSelect.tsx │ │ ├── InlineCell.tsx │ │ ├── PopoverCell.tsx │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── Date │ │ ├── BasicCell.tsx │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── DateTime │ │ ├── BasicCell.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── Derivative │ │ ├── Settings.tsx │ │ └── index.tsx │ │ ├── Duration │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── Email │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── File │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── Id │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── Image │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── Json │ │ ├── BasicCell.tsx │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── LongText │ │ ├── BasicCell.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── MultiSelect │ │ ├── ConvertStringToArray.tsx │ │ ├── InlineCell.tsx │ │ ├── PopoverCell.tsx │ │ ├── SideDrawerField.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── Number │ │ ├── BasicCell.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── Percentage │ │ ├── BasicCell.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── Phone │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── Rating │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ ├── index.tsx │ │ └── styles.ts │ │ ├── RichText │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── ShortText │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── SingleSelect │ │ ├── InlineCell.tsx │ │ ├── PopoverCell.tsx │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── Slider │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── Status │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ ├── index.tsx │ │ └── styles.ts │ │ ├── SubTable │ │ ├── Settings.tsx │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ ├── index.tsx │ │ └── utils.ts │ │ ├── Url │ │ ├── BasicCell.tsx │ │ ├── SideDrawerField.tsx │ │ └── index.tsx │ │ ├── User │ │ ├── SideDrawerField.tsx │ │ ├── TableCell.tsx │ │ └── index.tsx │ │ ├── _BasicCell │ │ ├── BasicCellName.tsx │ │ ├── BasicCellNull.tsx │ │ └── BasicCellValue.tsx │ │ ├── _withTableCell │ │ ├── withBasicCell.tsx │ │ ├── withHeavyCell.tsx │ │ └── withPopoverCell.tsx │ │ ├── index.tsx │ │ └── types.ts ├── constants │ ├── dates.tsx │ ├── fields.ts │ ├── routes.ts │ └── wikiLinks.ts ├── contexts │ ├── AppContext.tsx │ ├── EditorContext.ts │ ├── FiretableContext.tsx │ ├── SnackContext.tsx │ └── SnackLogContext.tsx ├── firebase │ ├── callables.ts │ ├── config.ts │ ├── firebaseui.ts │ └── index.ts ├── hooks │ ├── useCollection.ts │ ├── useDoc.ts │ ├── useFiretable │ │ ├── index.ts │ │ ├── useTable.tsx │ │ ├── useTableConfig.ts │ │ └── useUploader.ts │ ├── useHotkeys.ts │ ├── useKeyPress.ts │ ├── useRouter.ts │ ├── useSettings.ts │ └── useWindowSize.ts ├── index.tsx ├── pages │ ├── Auth │ │ ├── ImpersonatorAuth.tsx │ │ ├── JwtAuth.tsx │ │ ├── SetupGuide.tsx │ │ ├── SignOut.tsx │ │ └── index.tsx │ ├── Grid.tsx │ ├── Home.tsx │ ├── Table.tsx │ └── Test.tsx ├── react-app-env.d.ts ├── serviceWorker.ts └── utils │ ├── CustomBrowserRouter.tsx │ ├── PrivateRoute.tsx │ ├── auth.ts │ ├── color.ts │ └── fns.ts ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Firetable 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | www/node_modules 6 | 7 | 8 | Firetable/node_modules 9 | cloud_functions/functions/node_modules 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | /coverage 15 | 16 | # production 17 | www/build 18 | cloud_functions/functions/lib 19 | 20 | 21 | # cloud function config 22 | cloud_functions/functions/src/functionConfig.ts 23 | 24 | cloud_functions/functions/firebase-credentials.json 25 | 26 | # misc 27 | .DS_Store 28 | .env* 29 | !.env.example 30 | !*.enc 31 | 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | firebase-debug.log* 36 | 37 | *.firebaserc 38 | 39 | 40 | # Accidental package installs to root directories 41 | /yarn.lock 42 | /package.json 43 | package-lock.json 44 | node_modules/ 45 | 46 | cloud_functions/functions/src/functionConfig.json 47 | *.iml 48 | .idea 49 | *-firebase.json 50 | -------------------------------------------------------------------------------- /.opensource/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Firetable", 3 | "platforms": ["Node", "Admin", "Web"], 4 | "content": "README.md", 5 | "related": ["firebase/firebase-js-sdk"], 6 | "tabs": [] 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "proseWrap": "always" 4 | } 5 | -------------------------------------------------------------------------------- /cloud_functions/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint", 5 | "npm --prefix \"$RESOURCE_DIR\" run build" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cloud_functions/functions/.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /cloud_functions/functions/.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn-path ".yarn/releases/yarn-1.22.5.cjs" 6 | -------------------------------------------------------------------------------- /cloud_functions/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "updateStatus": "ts-node updateDeployStatus.ts", 5 | "generateConfig": "ts-node src/generateConfig.ts", 6 | "lint": "tslint --project tsconfig.json", 7 | "build": "tsc", 8 | "serve": "npm run build && firebase serve --only functions", 9 | "shell": "npm run build && firebase functions:shell", 10 | "start": "npm run shell", 11 | "deployFT": "echo 'n\n' | firebase deploy --interactive", 12 | "logs": "firebase functions:log" 13 | }, 14 | "engines": { 15 | "node": "14" 16 | }, 17 | "main": "lib/index.js", 18 | "dependencies": { 19 | "@google-cloud/cloudbuild": "^2.0.6", 20 | "@google-cloud/firestore": "^4.9.7", 21 | "@google-cloud/pubsub": "^2.5.0", 22 | "@google-cloud/storage": "^5.1.2", 23 | "@sendgrid/mail": "^7.4.2", 24 | "@slack/web-api": "^6.0.0", 25 | "algoliasearch": "^4.8.6", 26 | "firebase-admin": "^9.4.2", 27 | "firebase-functions": "^3.13.1", 28 | "imagemin": "^7.0.1", 29 | "imagemin-mozjpeg": "^9.0.0", 30 | "imagemin-pngquant": "^9.0.2", 31 | "lodash": "^4.17.21", 32 | "sharp": "^0.25.4" 33 | }, 34 | "devDependencies": { 35 | "@types/algoliasearch": "^3.34.10", 36 | "@types/imagemin": "^7.0.0", 37 | "@types/imagemin-mozjpeg": "^8.0.0", 38 | "@types/imagemin-pngquant": "^7.0.0", 39 | "@types/json2csv": "^5.0.1", 40 | "@types/lodash": "^4.14.158", 41 | "@types/sharp": "^0.25.1", 42 | "firebase-tools": "^9.2.2", 43 | "husky": "^4.2.5", 44 | "prettier": "^2.1.1", 45 | "pretty-quick": "^3.0.0", 46 | "ts-node": "^8.6.2", 47 | "tslint": "^6.1.0", 48 | "typescript": "^3.2.2" 49 | }, 50 | "husky": { 51 | "hooks": { 52 | "pre-commit": "pretty-quick --staged" 53 | } 54 | }, 55 | "private": true 56 | } 57 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/algoliaSearchKey.ts: -------------------------------------------------------------------------------- 1 | import algoliasearch from "algoliasearch"; 2 | import * as functions from "firebase-functions"; 3 | import { env } from "./config"; 4 | 5 | const algoliaClient = algoliasearch(env.algolia.app, env.algolia.key); 6 | 7 | export const getAlgoliaSearchKey = functions.https.onCall(async ( 8 | data: { index: string }, 9 | context: functions.https.CallableContext 10 | ) => { 11 | const requestedIndex = data.index 12 | try { 13 | if (!context.auth || !context.auth.token) throw new Error("Unauthenticated") 14 | 15 | const allIndicesRoles = ['ADMIN',"TEAM"] // you can add more roles here that need access to all algolia indices 16 | 17 | const rolesIndicesAccess = { 18 | "ROLE":["index_1","index_2"] 19 | } 20 | const userRoles = context.auth.token.roles 21 | if (userRoles.some(role=> allIndicesRoles.includes(role)||rolesIndicesAccess[role].includes(requestedIndex))){ 22 | const validUntil = Math.floor(Date.now() / 1000) + 3600; 23 | const key = algoliaClient.generateSecuredApiKey( 24 | env.algolia.search, 25 | { 26 | filters:"", 27 | validUntil, 28 | restrictIndices: [requestedIndex], 29 | userToken: context.auth.uid, 30 | } 31 | ); 32 | return { 33 | data: key, 34 | success: true, 35 | }; 36 | }else{ 37 | return { 38 | message: 'Missing Required roles for this index', 39 | success: false, 40 | }; 41 | } 42 | 43 | } catch (error) { 44 | return { 45 | success: false, 46 | error, 47 | message: error.message, 48 | }; 49 | } 50 | }) 51 | 52 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/callable.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import * as _ from "lodash"; 3 | import { hasAnyRole } from "./utils/auth"; 4 | import { auth } from "./config"; 5 | 6 | // Impersonator Auth callable takes email and returns JWT of user on firebaseAuth 7 | // requires a user admin role 8 | 9 | export const ImpersonatorAuth = functions.https.onCall( 10 | async (data, context) => { 11 | try { 12 | if (hasAnyRole(["ADMIN"], context)) { 13 | const user = await auth.getUserByEmail(data.email); 14 | const jwt = await auth.createCustomToken(user.uid); 15 | return { 16 | success: true, 17 | jwt, 18 | message: "successfully generated token", 19 | }; 20 | } else { 21 | return { 22 | success: false, 23 | message: "admin role is required", 24 | }; 25 | } 26 | } catch (error) { 27 | return { 28 | success: false, 29 | message: JSON.stringify(error), 30 | }; 31 | } 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/config.ts: -------------------------------------------------------------------------------- 1 | // Initialize Firebase Admin 2 | import * as functions from "firebase-functions"; 3 | import * as admin from "firebase-admin"; 4 | admin.initializeApp(); 5 | 6 | // Initialize Cloud Firestore Database 7 | export const db = admin.firestore(); 8 | // Initialize Auth 9 | export const auth = admin.auth(); 10 | 11 | const settings = { timestampsInSnapshots: true }; 12 | db.settings(settings); 13 | export const env = functions.config(); 14 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/constants/Collections.ts: -------------------------------------------------------------------------------- 1 | enum Collections { 2 | fireMail = "firemail", 3 | emailOTP = "emailOTP", 4 | } 5 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/functionConfig.ts: -------------------------------------------------------------------------------- 1 | export const collectionPath = '' 2 | export const functionName = '' 3 | export default {} -------------------------------------------------------------------------------- /cloud_functions/functions/src/history/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import * as _ from "lodash"; 3 | import { db } from "../config"; 4 | 5 | import config from "../functionConfig"; // generated using generateConfig.ts 6 | const functionConfig: any = config; 7 | 8 | const historySnapshot = (trackedFields: string[]) => async ( 9 | change: functions.Change 10 | ) => { 11 | const before = change.before.data(); 12 | const after = change.after.data(); 13 | const docPath = change.after.ref.path; 14 | if (!before || !after) return false; 15 | const trackedChanges: any = {}; 16 | trackedFields.forEach((field) => { 17 | if (!_.isEqual(before[field], after[field])) 18 | trackedChanges[field] = after[field]; 19 | }); 20 | if (!_.isEmpty(trackedChanges)) { 21 | await db 22 | .doc(docPath) 23 | .collection("historySnapshots") 24 | .add({ ...before, archivedAt: new Date() }); 25 | return true; 26 | } else return false; 27 | }; 28 | 29 | const historySnapshotFnsGenerator = (collection) => 30 | functions.firestore 31 | .document(`${collection.name}/{docId}`) 32 | .onUpdate(historySnapshot(collection.trackedFields)); 33 | 34 | //export default historySnapshotFnsGenerator; 35 | 36 | export const FT_history = { 37 | [functionConfig.name 38 | .replace(/\//g, "_") 39 | .replace(/_{.*?}_/g, "_")]: historySnapshotFnsGenerator(functionConfig), 40 | }; 41 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | // export { triggerCloudBuild, cloudBuildUpdates } from "./buildTriggers"; // a callable used for triggering cloudbuild to build and deploy configurable cloud functions 2 | // export { 3 | // scheduledFirestoreBackup, // callableFirestoreBackup 4 | // } from "./backup"; 5 | // import * as callableFns from "./callable"; 6 | 7 | // export const callable = callableFns; 8 | 9 | // // all the cloud functions bellow are deployed using the triggerCloudBuild callable function 10 | // // these functions are designed to be built and deployed based on the configuration passed through the callable 11 | 12 | // export { FT_aggregates } from "./aggregates"; 13 | // export { FT_subTableStats } from "./subTableStats"; 14 | 15 | // export { actionScript } from "./actionScript"; 16 | 17 | // export { webhook } from "./webhooks"; 18 | 19 | // export { FT_snapshotSync } from "./snapshotSync"; 20 | 21 | // export { FT_compressedThumbnail } from "./compressedThumbnail"; 22 | 23 | export {getAlgoliaSearchKey} from './algoliaSearchKey' 24 | 25 | //deprecated, updated implementation moved to FT_build folder and used within sparks table functions 26 | // export { FT_derivatives } from "./derivatives"; 27 | // export { FT_algolia } from "./algolia"; 28 | // export { FT_email } from "./emailOnTrigger"; 29 | // export { FT_slack } from "./slackOnTrigger"; 30 | // export { FT_sync } from "./collectionSync"; 31 | // export { FT_spark } from "./sparks"; 32 | // export { FT_history } from "./history"; 33 | // export { slackBotMessageOnCreate } from "./slackOnTrigger/trigger"; -------------------------------------------------------------------------------- /cloud_functions/functions/src/subTableStats.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import * as admin from "firebase-admin"; 3 | 4 | import { collectionPath } from "./functionConfig"; // generated using generateConfig.ts 5 | const increment = admin.firestore.FieldValue.increment(1); 6 | const decrement = admin.firestore.FieldValue.increment(-1); 7 | const docCreated = ( 8 | snapshot: functions.firestore.DocumentSnapshot, 9 | context: functions.EventContext 10 | ) => { 11 | const { subCollectionId } = context.params; 12 | return snapshot.ref.parent.parent?.update({ 13 | [`${subCollectionId}.count`]: increment, 14 | }); 15 | }; 16 | 17 | const docDelete = ( 18 | snapshot: functions.firestore.DocumentSnapshot, 19 | context: functions.EventContext 20 | ) => { 21 | const { subCollectionId } = context.params; 22 | return snapshot.ref.parent.parent?.update({ 23 | [subCollectionId]: decrement, 24 | }); 25 | }; 26 | const subTableFnsGenerator = { 27 | onCreate: functions.firestore 28 | .document(`${collectionPath}/{parentId}/{subCollectionId}/{docId}`) 29 | .onCreate(docCreated), 30 | onDelete: functions.firestore 31 | .document(`${collectionPath}/{parentId}/{subCollectionId}/{docId}`) 32 | .onDelete(docDelete), 33 | }; 34 | 35 | export const FT_subTableStats = { 36 | [collectionPath]: { ...subTableFnsGenerator }, 37 | }; 38 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | 3 | export const hasAnyRole = ( 4 | authorizedRoles: string[], 5 | context: functions.https.CallableContext 6 | ) => { 7 | if (!context.auth || !context.auth.token.roles) return false; 8 | const userRoles = context.auth.token.roles as string[]; 9 | const authorization = authorizedRoles.reduce( 10 | (authorized: boolean, role: string) => { 11 | if (userRoles.includes(role)) return true; 12 | else return authorized; 13 | }, 14 | false 15 | ); 16 | return authorization; 17 | }; 18 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/utils/email.ts: -------------------------------------------------------------------------------- 1 | const sgMail = require("@sendgrid/mail"); 2 | import { env } from "../config"; 3 | sgMail.setSubstitutionWrappers("{{", "}}"); 4 | if (env.send_grid) sgMail.setApiKey(env.send_grid.key); 5 | 6 | export const sendEmail = (msg: any) => sgMail.send(msg); 7 | -------------------------------------------------------------------------------- /cloud_functions/functions/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as admin from "firebase-admin"; 2 | import * as _ from "lodash"; 3 | export const serverTimestamp = admin.firestore.FieldValue.serverTimestamp; 4 | import { sendEmail } from "./email"; 5 | import { hasAnyRole } from "./auth"; 6 | 7 | const characters = 8 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 9 | export function generateId(length) { 10 | let result = ""; 11 | const charactersLength = characters.length; 12 | for (let i = 0; i < length; i++) { 13 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 14 | } 15 | return result; 16 | } 17 | 18 | export default { generateId, sendEmail, serverTimestamp, hasAnyRole }; 19 | export const replacer = (data: any) => (m: string, key: string) => { 20 | const objKey = key.split(":")[0]; 21 | const defaultValue = key.split(":")[1] || ""; 22 | return _.get(data, objKey, defaultValue); 23 | }; 24 | 25 | export const hasRequiredFields = (requiredFields: string[], data: any) => 26 | requiredFields.reduce((acc: boolean, currField: string) => { 27 | if (data[currField] === undefined || data[currField] === null) return false; 28 | else return acc; 29 | }, true); 30 | export async function asyncForEach(array: any[], callback: Function) { 31 | for (let index = 0; index < array.length; index++) { 32 | await callback(array[index], index, array); 33 | } 34 | } 35 | 36 | export const identifyTriggerType = (beforeData, afterData) => 37 | Boolean(beforeData) && Boolean(afterData) 38 | ? "update" 39 | : Boolean(afterData) 40 | ? "create" 41 | : "delete"; 42 | -------------------------------------------------------------------------------- /cloud_functions/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "resolveJsonModule": true, 11 | "target": "es2017" 12 | }, 13 | "compileOnSave": true, 14 | "include": ["src", "generateConfig.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /cloud_functions/functions/updateDeployStatus.ts: -------------------------------------------------------------------------------- 1 | // Initialize Firebase Admin 2 | import * as admin from "firebase-admin"; 3 | // Initialize Firebase Admin 4 | const serverTimestamp = admin.firestore.FieldValue.serverTimestamp; 5 | admin.initializeApp(); 6 | const db = admin.firestore(); 7 | 8 | const main = async (deployRequestPath: string, currentBuild) => { 9 | await db.doc(deployRequestPath).update({ 10 | deployedAt: serverTimestamp(), 11 | currentBuild: currentBuild ?? "", 12 | }); 13 | return true; 14 | }; 15 | 16 | main(process.argv[2], process.argv[3]) 17 | .catch((err) => console.log(err)) 18 | .then(() => console.log("this will succeed")) 19 | .catch(() => "obligatory catch"); 20 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: node:10.17.0 3 | entrypoint: yarn 4 | args: ["install"] 5 | dir: "www" 6 | - name: node:10.17.0 7 | entrypoint: yarn 8 | args: 9 | - env 10 | - "${_PROJECT_ID}" 11 | - "${_FIREBASE_WEB_API_KEY}" 12 | - "${_ALGOLIA_APP_ID}" 13 | - "${_ALGOLIA_APP_KEY}" 14 | dir: "www" 15 | - name: node:10.17.0 16 | entrypoint: yarn 17 | args: ["build"] 18 | dir: "www" 19 | - name: node:10.17.0 20 | entrypoint: yarn 21 | args: 22 | - "target" 23 | - "${_HOSTING_TARGET}" 24 | - --project 25 | - "${_PROJECT_ID}" 26 | dir: "www" 27 | - name: node:10.17.0 28 | entrypoint: yarn 29 | args: 30 | - deploy 31 | - --project 32 | - "${_PROJECT_ID}" 33 | - --debug 34 | - --token 35 | - "${_FIREBASE_TOKEN}" 36 | - --only 37 | - hosting 38 | dir: "www" 39 | substitutions: 40 | _PROJECT_ID: "project-id" # default value 41 | options: 42 | machineType: "N1_HIGHCPU_8" 43 | -------------------------------------------------------------------------------- /cloudbuildfunctions.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | # - name: "gcr.io/kaniko-project/executor:latest" 3 | # args: 4 | # - --destination=gcr.io/$PROJECT_ID/image 5 | # - --cache=true 6 | # - --cache-ttl=24h 7 | - name: node:10.15.1 8 | entrypoint: yarn 9 | args: ["install"] 10 | dir: "cloud_functions/functions" 11 | - name: node:10.15.1 12 | entrypoint: yarn 13 | args: 14 | - "generateConfig" 15 | - "${_FUNCTIONS_GROUP}" 16 | - "${_FUNCTION_CONFIG}" 17 | dir: "cloud_functions/functions" 18 | - name: node:10.15.1 19 | entrypoint: yarn 20 | args: 21 | - "deployFT" 22 | - "--project" 23 | - "${_PROJECT_ID}" 24 | - "--token" 25 | - "${_FIREBASE_TOKEN}" 26 | - "--only" 27 | - "functions:${_FUNCTIONS_GROUP}" 28 | dir: "cloud_functions/functions" 29 | # - name: gcr.io/cloud-builders/gcloud 30 | # args: 31 | # - functions 32 | # - deploy 33 | # - "${_FUNCTIONS_GROUP}" 34 | # dir: "cloud_functions/functions" 35 | - name: node:10.15.1 36 | entrypoint: yarn 37 | args: 38 | - "updateStatus" 39 | - "${_REQUEST_DOC_PATH}" 40 | - "${_FUNCTION_CONFIG}" 41 | dir: "cloud_functions/functions" 42 | 43 | substitutions: 44 | _PROJECT_ID: "project-id" # default value 45 | _FUNCTIONS_GROUP: "exportTable" # default value 46 | options: 47 | machineType: "N1_HIGHCPU_8" 48 | -------------------------------------------------------------------------------- /e2e/.gitignore: -------------------------------------------------------------------------------- 1 | firebase-credentials.json -------------------------------------------------------------------------------- /e2e/auth.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "./credentials"; 2 | 3 | export const authenticateUser = async (page, email = "shams@antler.co") => { 4 | const user = await auth.getUserByEmail(email); 5 | const jwt = await auth.createCustomToken(user.uid); 6 | // Go to http://localhost:3000/jwtAuth 7 | await page.goto("http://localhost:3000/jwtAuth"); 8 | 9 | // Click input[name="JWT"] 10 | await page.click('input[name="JWT"]'); 11 | 12 | // Fill input[name="JWT"] 13 | await page.fill('input[name="JWT"]', jwt); 14 | 15 | // Click text="Sign in" 16 | return await Promise.all([ 17 | page.waitForNavigation(/*{ url: 'http://localhost:3000/' }*/), 18 | page.click('text="Sign in"'), 19 | ]); 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/credentials.ts: -------------------------------------------------------------------------------- 1 | // Initialize Firebase Admin 2 | import * as admin from "firebase-admin"; 3 | // Initialize Firebase Admin 4 | const serverTimestamp = admin.firestore.FieldValue.serverTimestamp; 5 | const serviceAccount = require("./firebase-credentials.json"); 6 | 7 | admin.initializeApp({ 8 | credential: admin.credential.cert(serviceAccount), 9 | databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`, 10 | }); 11 | export const db = admin.firestore(); 12 | export const auth = admin.auth(); 13 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/node": "^14.14.33", 4 | "playwright": "^1.9.2", 5 | "playwright-cli": "^0.180.0" 6 | }, 7 | "dependencies": { 8 | "firebase-admin": "^9.5.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ft_actions/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint", 5 | "npm --prefix \"$RESOURCE_DIR\" run build" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ft_actions/functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Except the ESLint config file 6 | !.eslintrc.js 7 | 8 | # TypeScript v1 declaration files 9 | typings/ 10 | 11 | # Node.js dependency directory 12 | node_modules/ 13 | -------------------------------------------------------------------------------- /ft_actions/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deployFT": "echo 'n\n' | firebase deploy --interactive", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "14" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "firebase-admin": "^9.11.0", 18 | "firebase-functions": "^3.14.1", 19 | "lodash": "^4.17.21" 20 | }, 21 | "devDependencies": { 22 | "firebase-tools": "^8.7.0", 23 | "husky": "^4.2.5", 24 | "prettier": "^2.1.1", 25 | "pretty-quick": "^3.0.0", 26 | "ts-node": "^8.6.2", 27 | "tslint": "^6.1.0", 28 | "typescript": "^3.2.2" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "pretty-quick --staged" 33 | } 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /ft_actions/functions/src/firebaseConfig.ts: -------------------------------------------------------------------------------- 1 | // Initialize Firebase Admin 2 | import * as functions from "firebase-functions"; 3 | import * as admin from "firebase-admin"; 4 | admin.initializeApp(); 5 | 6 | // Initialize Cloud Firestore Database 7 | export const db = admin.firestore(); 8 | // Initialize Auth 9 | export const auth = admin.auth(); 10 | 11 | const settings = { timestampsInSnapshots: true, ignoreUndefinedProperties: true}; 12 | db.settings(settings); 13 | export const env = functions.config(); 14 | -------------------------------------------------------------------------------- /ft_actions/functions/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | 3 | export const hasAnyRole = ( 4 | authorizedRoles: string[], 5 | context: functions.https.CallableContext 6 | ) => { 7 | if (!context.auth || !context.auth.token.roles) return false; 8 | const userRoles = context.auth.token.roles as string[]; 9 | const authorization = authorizedRoles.reduce( 10 | (authorized: boolean, role: string) => { 11 | if (userRoles.includes(role)) return true; 12 | else return authorized; 13 | }, 14 | false 15 | ); 16 | return authorization; 17 | }; 18 | -------------------------------------------------------------------------------- /ft_actions/functions/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as admin from "firebase-admin"; 2 | export const serverTimestamp = admin.firestore.FieldValue.serverTimestamp; 3 | import { hasAnyRole } from "./auth"; 4 | 5 | const characters = 6 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 7 | export function generateId(length: number): string { 8 | let result = ""; 9 | const charactersLength = characters.length; 10 | for (let i = 0; i < length; i++) { 11 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 12 | } 13 | return result; 14 | } 15 | 16 | const hasRequiredFields = (requiredFields: string[], data: any) => 17 | requiredFields.reduce((acc: boolean, currField: string) => { 18 | if (data[currField] === undefined || data[currField] === null) return false; 19 | else return acc; 20 | }, true); 21 | async function asyncForEach(array: any[], callback: Function) { 22 | for (let index = 0; index < array.length; index++) { 23 | await callback(array[index], index, array); 24 | } 25 | } 26 | const identifyTriggerType = (beforeData: any, afterData: any) => 27 | Boolean(beforeData) && Boolean(afterData) 28 | ? "update" 29 | : Boolean(afterData) 30 | ? "create" 31 | : "delete"; 32 | 33 | export default { 34 | hasRequiredFields, 35 | generateId, 36 | serverTimestamp, 37 | hasAnyRole, 38 | asyncForEach, 39 | identifyTriggerType, 40 | }; 41 | -------------------------------------------------------------------------------- /ft_actions/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "target": "ESNext" 11 | }, 12 | "compileOnSave": true, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /ft_actions/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /ft_build/sparksLib/template.ts: -------------------------------------------------------------------------------- 1 | export const dependencies = { 2 | // --- Add your dependencies 3 | // algoliasearch: "^4.8.3", 4 | }; 5 | // Define your spark 6 | const sparkName = async (data, sparkContext) => { 7 | 8 | // Your spark inputs 9 | const { row, targetPath, fieldsToSync } = data; 10 | const { triggerType, change } = sparkContext; 11 | 12 | // --------------------------------------------- 13 | // --- Utilise your dependencies --- 14 | // const algoliasearch = require("algoliasearch"); 15 | 16 | // --------------------------------------------- 17 | // --- Get the secret from Secrets Manager 18 | // Example: Algolia Secret 19 | // const { getSecret } = require("../utils"); 20 | // const { appId, adminKey } = await getSecret("algolia"); 21 | 22 | // --------------------------------------------- 23 | // --- Connect to any third party extensions --- 24 | // Example Algolia 25 | // const client = algoliasearch(appId, adminKey); 26 | // const _index = client.initIndex(index); 27 | 28 | 29 | // --------------------------------------------- 30 | // --- Handle required trigger actions --- 31 | switch (triggerType) { 32 | 33 | case "create": 34 | // create trigger actions 35 | break; 36 | 37 | case "update": 38 | // update trigger actions 39 | break; 40 | 41 | case "delete": 42 | // delete trigger actions 43 | break; 44 | 45 | default: 46 | break; 47 | } 48 | return true; 49 | 50 | 51 | }; 52 | 53 | export default sparkName; 54 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/icon.png -------------------------------------------------------------------------------- /www/.env.development.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/.env.development.enc -------------------------------------------------------------------------------- /www/.env.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/.env.enc -------------------------------------------------------------------------------- /www/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_ALGOLIA_APP_ID= 2 | REACT_APP_ALGOLIA_SEARCH_API_KEY= 3 | 4 | REACT_APP_FIREBASE_PROJECT_ID= 5 | REACT_APP_FIREBASE_PROJECT_WEB_API_KEY= 6 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | firebase-debug.log -------------------------------------------------------------------------------- /www/.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn-path ".yarn/releases/yarn-1.22.5.cjs" 6 | -------------------------------------------------------------------------------- /www/createDotEnv.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const main = ( 4 | projectID = "", 5 | firebaseWebApiKey = "", 6 | algoliaAppId = "", 7 | algoliaSearhApiKey = "" 8 | ) => { 9 | return fs.writeFileSync( 10 | ".env", 11 | `REACT_APP_FIREBASE_PROJECT_ID = ${projectID} 12 | REACT_APP_FIREBASE_PROJECT_WEB_API_KEY = ${firebaseWebApiKey} 13 | REACT_APP_ALGOLIA_APP_ID = ${algoliaAppId} 14 | REACT_APP_ALGOLIA_SEARCH_API_KEY = ${algoliaSearhApiKey}` 15 | ); 16 | }; 17 | 18 | main(process.argv[2], process.argv[3], process.argv[4], process.argv[5]); 19 | -------------------------------------------------------------------------------- /www/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "firetable", 4 | "public": "build", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "**", 9 | "destination": "/index.html" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /www/public/_redirects: -------------------------------------------------------------------------------- 1 | # Rewrite a path 2 | /* /index.html 200 3 | -------------------------------------------------------------------------------- /www/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ED4746 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /www/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /www/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /www/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /www/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /www/public/favicon/firetable.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /www/public/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /www/public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /www/public/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /www/public/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /www/public/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiretableProject/firetable/5261e67c74858973fc8d86fea741f7ad17911ff2/www/public/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /www/public/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 20 | 24 | 25 | 26 | 28 | 29 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /www/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Firetable", 3 | "name": "Firetable", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "favicon/android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "favicon/android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /www/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /www/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /www/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /www/src/analytics.ts: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/analytics"; 3 | var firebaseConfig = { 4 | apiKey: "AIzaSyBwgfb-GmsCZ_d4B5kRElzWMoPWwjdKioM", 5 | authDomain: "firetable-service.firebaseapp.com", 6 | projectId: "firetable-service", 7 | storageBucket: "firetable-service.appspot.com", 8 | messagingSenderId: "831080389", 9 | appId: "1:831080389:web:ab0bbacccdd887ab3b6dac", 10 | measurementId: "G-K97G7PBDNT", 11 | }; 12 | // Initialize Firebase 13 | const firetableServiceApp = firebase.initializeApp( 14 | firebaseConfig, 15 | "firetable-service" 16 | ); 17 | export const analytics = firebase.analytics(firetableServiceApp); 18 | -------------------------------------------------------------------------------- /www/src/assets/antler.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /www/src/assets/firetable.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /www/src/assets/icons/Action.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiGestureTap } from "@mdi/js"; 3 | 4 | export default function Action(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/AddColumn.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function AddColumn(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/AddColumnCircle.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function AddColumnCircle(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/AddRow.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function AddRow(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/AddRowCircle.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function AddRowCircle(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/Backburger.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiBackburger } from "@mdi/js"; 3 | 4 | export default function Backburger(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/CellResize.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function FileDownload(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/ColumnPlusAfter.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function FileDownload(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/ColumnPlusBefore.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function FileDownload(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/ColumnRemove.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function FileDownload(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/ConnectTable.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function ConnectTable(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/CopyCells.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function CopyCells(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/CornerResize.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiResizeBottomRight } from "@mdi/js"; 3 | 4 | export default function Derivative(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/DarkTheme.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiCircleHalfFull } from "@mdi/js"; 3 | 4 | export default function DarkTheme(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/Derivative.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiFunctionVariant } from "@mdi/js"; 3 | 4 | export default function Derivative(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/Export.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function Export(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/FileUpload.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiUpload } from "@mdi/js"; 3 | 4 | export default function FileUpload(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/Freeze.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function FileDownload(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/Go.tsx: -------------------------------------------------------------------------------- 1 | import ChevronRightIcon from "@material-ui/icons/ChevronRight"; 2 | import { SvgIconProps } from "@material-ui/core/SvgIcon"; 3 | 4 | /** Right chevron icon with optical alignment */ 5 | export default function Go(props: SvgIconProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /www/src/assets/icons/Id.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function Id(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/Import.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function Export(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/Json.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function Json(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/MultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function MultiSelect(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/Number.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiNumeric } from "@mdi/js"; 3 | 4 | export default function Number(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/Percentage.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiPercent } from "@mdi/js"; 3 | 4 | export default function Percentage(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/Slider.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function Slider(props: SvgIconProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/assets/icons/Status.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | import { mdiPulse } from "@mdi/js"; 3 | 4 | export default function Status(props: SvgIconProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/assets/icons/SubTable.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function SubTable(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/Unfreeze.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 2 | 3 | export default function FileDownload(props: SvgIconProps) { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /www/src/assets/icons/Upload.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; 3 | import { mdiUpload } from "@mdi/js"; 4 | 5 | export default function Upload(props: SvgIconProps) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /www/src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | import { 4 | createStyles, 5 | makeStyles, 6 | useTheme, 7 | useScrollTrigger, 8 | AppBar as MuiAppBar, 9 | Toolbar, 10 | Grid, 11 | Button, 12 | } from "@material-ui/core"; 13 | 14 | import FiretableLogo from "assets/FiretableLogo"; 15 | import routes from "constants/routes"; 16 | 17 | const useStyles = makeStyles((theme) => 18 | createStyles({ 19 | appBar: { 20 | backgroundColor: theme.palette.background.paper, 21 | marginBottom: theme.spacing(6), 22 | }, 23 | 24 | logo: { 25 | display: "block", 26 | marginRight: theme.spacing(1), 27 | }, 28 | heading: { 29 | textTransform: "none", 30 | color: theme.palette.primary.main, 31 | cursor: "default", 32 | userSelect: "none", 33 | fontFeatureSettings: '"liga"', 34 | }, 35 | 36 | locationDropdown: { 37 | minWidth: 140, 38 | margin: 0, 39 | }, 40 | }) 41 | ); 42 | 43 | interface IAppBarProps {} 44 | 45 | const AppBar: React.FunctionComponent = () => { 46 | const classes = useStyles(); 47 | const theme = useTheme(); 48 | const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 }); 49 | 50 | return ( 51 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default AppBar; 78 | -------------------------------------------------------------------------------- /www/src/components/ButtonWithStatus.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | 4 | import { 5 | makeStyles, 6 | createStyles, 7 | Button, 8 | ButtonProps, 9 | } from "@material-ui/core"; 10 | import { fade } from "@material-ui/core/styles"; 11 | 12 | export const useStyles = makeStyles((theme) => 13 | createStyles({ 14 | active: { 15 | borderColor: "currentColor", 16 | backgroundColor: fade( 17 | theme.palette.primary.main, 18 | theme.palette.action.hoverOpacity 19 | ), 20 | 21 | "&:hover": { 22 | color: theme.palette.primary.dark, 23 | backgroundColor: fade( 24 | theme.palette.primary.dark, 25 | theme.palette.action.hoverOpacity 26 | ), 27 | borderColor: "currentColor", 28 | }, 29 | }, 30 | }) 31 | ); 32 | 33 | export interface IButtonWithStatusProps extends ButtonProps { 34 | active?: boolean; 35 | } 36 | 37 | export const ButtonWithStatus = React.forwardRef(function ButtonWithStatus_( 38 | { active = false, className, ...props }: IButtonWithStatusProps, 39 | ref: React.Ref 40 | ) { 41 | const classes = useStyles(); 42 | 43 | return ( 44 | 69 | 70 | ); 71 | }; 72 | 73 | export default FinalColumnHeader; 74 | -------------------------------------------------------------------------------- /www/src/components/Table/Settings/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconButton from "@material-ui/core/IconButton"; 3 | import Menu from "@material-ui/core/Menu"; 4 | import MenuItem from "@material-ui/core/MenuItem"; 5 | import MoreVertIcon from "@material-ui/icons/MoreVert"; 6 | 7 | const options = ["Webhooks", "Rules", "Algolia", "CollectionSync"]; 8 | 9 | const ITEM_HEIGHT = 48; 10 | 11 | export default function SettingsMenu({ modal, setModal }) { 12 | const [anchorEl, setAnchorEl] = React.useState(null); 13 | const open = Boolean(anchorEl); 14 | 15 | const handleClick = (event: React.MouseEvent) => { 16 | setAnchorEl(event.currentTarget); 17 | }; 18 | 19 | const handleClose = (option: string) => () => { 20 | setModal(option); 21 | setAnchorEl(null); 22 | }; 23 | 24 | return ( 25 |
26 | 32 | 33 | 34 | 47 | {options.map((option) => ( 48 | 55 | {option} 56 | 57 | ))} 58 | 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /www/src/components/Table/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import SettingsMenu from "./Menu"; 3 | //import Webhooks from "./Webhooks"; 4 | export default function Settings() { 5 | const [modal, setModal] = useState(""); 6 | return ( 7 | <> 8 | 9 | {/* { 12 | setModal(""); 13 | }} 14 | /> */} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /www/src/components/Table/Skeleton/HeaderRowSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles, Button } from "@material-ui/core"; 2 | import Skeleton from "@material-ui/lab/Skeleton"; 3 | import AddColumnIcon from "assets/icons/AddColumn"; 4 | 5 | const useStyles = makeStyles((theme) => 6 | createStyles({ 7 | root: { 8 | display: "flex", 9 | alignItems: "center", 10 | }, 11 | cell: { 12 | backgroundColor: theme.palette.background.default, 13 | border: `1px solid #e0e0e0`, 14 | borderLeftWidth: 0, 15 | 16 | width: 150, 17 | height: 44, 18 | }, 19 | 20 | addColumn: { 21 | borderRadius: 500, 22 | height: 32, 23 | marginLeft: -46 + 12, 24 | }, 25 | addColumnIcon: { fontSize: "24px !important" }, 26 | }) 27 | ); 28 | 29 | const NUM_CELLS = 5; 30 | 31 | export default function HeaderRowSkeleton() { 32 | const classes = useStyles(); 33 | 34 | return ( 35 |
36 | {new Array(NUM_CELLS).fill(undefined).map((_, i) => ( 37 | 38 | ))} 39 | 40 | 41 | 42 | 43 | 49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /www/src/components/Table/TableHeader/TableHeaderButton.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Button, ButtonProps } from "@material-ui/core"; 2 | 3 | export interface ITableHeaderButtonProps extends Partial { 4 | title: string; 5 | icon: React.ReactNode; 6 | } 7 | 8 | export default function TableHeaderButton({ 9 | title, 10 | icon, 11 | ...props 12 | }: ITableHeaderButtonProps) { 13 | return ( 14 | 15 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /www/src/components/Table/TableHeader/TableSettings.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import TableHeaderButton from "./TableHeaderButton"; 4 | import SettingsIcon from "@material-ui/icons/Settings"; 5 | 6 | import TableSettingsDialog, { 7 | TableSettingsDialogModes, 8 | } from "components/TableSettings"; 9 | import { useFiretableContext } from "contexts/FiretableContext"; 10 | 11 | export default function TableSettings() { 12 | const [open, setOpen] = useState(false); 13 | 14 | const { tableState } = useFiretableContext(); 15 | 16 | return ( 17 | <> 18 | setOpen(true)} 21 | icon={} 22 | /> 23 | 24 | setOpen(false)} 26 | mode={open ? TableSettingsDialogModes.update : null} 27 | data={open ? tableState?.config.tableConfig.doc : null} 28 | /> 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /www/src/components/Table/editors/NullEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { EditorProps } from "react-data-grid"; 3 | // import _findIndex from "lodash/findIndex"; 4 | 5 | import { withStyles, WithStyles } from "@material-ui/core"; 6 | import styles from "./styles"; 7 | 8 | /** 9 | * Allow the cell to be editable, but disable react-data-grid’s default 10 | * text editor to show. 11 | * 12 | * Hides the editor container so the cell below remains editable inline. 13 | * 14 | * Use for cells that have inline editing and don’t need to be double-clicked. 15 | * 16 | * TODO: fix NullEditor overwriting the formatter component 17 | */ 18 | class NullEditor extends React.Component< 19 | EditorProps & WithStyles 20 | > { 21 | getInputNode = () => null; 22 | getValue = () => null; 23 | render = () => null; 24 | } 25 | 26 | export default withStyles(styles)(NullEditor); 27 | -------------------------------------------------------------------------------- /www/src/components/Table/editors/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from "@material-ui/core"; 2 | 3 | export const styles = createStyles({ 4 | "@global": { 5 | ".rdg-editor-container": { display: "none" }, 6 | }, 7 | }); 8 | 9 | export default styles; 10 | -------------------------------------------------------------------------------- /www/src/components/Table/editors/withSideDrawerEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { EditorProps } from "react-data-grid"; 3 | import { useFiretableContext } from "contexts/FiretableContext"; 4 | import { IHeavyCellProps } from "components/fields/types"; 5 | 6 | import { getCellValue } from "utils/fns"; 7 | 8 | /** 9 | * Allow the cell to be editable, but disable react-data-grid’s default 10 | * text editor to show. Opens the side drawer in the appropriate position. 11 | * 12 | * Displays the current HeavyCell or HeavyCell since it overwrites cell contents. 13 | * 14 | * Use for cells that do not support any type of in-cell editing. 15 | */ 16 | export default function withSideDrawerEditor( 17 | HeavyCell?: React.ComponentType 18 | ) { 19 | return function SideDrawerEditor(props: EditorProps) { 20 | const { row, column } = props; 21 | const { sideDrawerRef } = useFiretableContext(); 22 | 23 | useEffect(() => { 24 | if (!sideDrawerRef?.current?.open && sideDrawerRef?.current?.setOpen) 25 | sideDrawerRef?.current?.setOpen(true); 26 | }, [column]); 27 | 28 | return HeavyCell ? ( 29 | {}} 36 | disabled={props.column.editable === false} 37 | /> 38 | ) : null; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /www/src/components/Wizards/FadeList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | 4 | import { makeStyles, createStyles } from "@material-ui/core"; 5 | 6 | const useStyles = makeStyles((theme) => 7 | createStyles({ 8 | listWrapper: { 9 | position: "relative", 10 | 11 | "&::after": { 12 | content: '""', 13 | display: "block", 14 | pointerEvents: "none", 15 | 16 | position: "absolute", 17 | bottom: 0, 18 | left: 0, 19 | right: 0, 20 | 21 | height: theme.spacing(3), 22 | backgroundImage: `linear-gradient(to top, ${ 23 | theme.palette.background.elevation?.[24] ?? 24 | theme.palette.background.paper 25 | }, transparent)`, 26 | }, 27 | }, 28 | list: { 29 | listStyleType: "none", 30 | margin: 0, 31 | padding: theme.spacing(1.5, 0, 3), 32 | 33 | height: 400, 34 | overflowY: "overlay" as any, 35 | 36 | "& li": { margin: theme.spacing(0.5, 0) }, 37 | }, 38 | }) 39 | ); 40 | 41 | export interface IFadeListProps { 42 | children?: React.ReactNode | React.ElementType[]; 43 | classes?: Partial>; 44 | } 45 | 46 | export const FadeList = React.forwardRef( 47 | function FadeList_({ children, classes: classesProp }, ref) { 48 | const classes = useStyles(); 49 | 50 | return ( 51 |
55 |
    {children}
56 |
57 | ); 58 | } 59 | ); 60 | 61 | export default FadeList; 62 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | export default function Action({ name, value }: IBasicCellProps) { 4 | return <>{value ? value.status : name}; 5 | } 6 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/FormDialog/Context.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { IActionParams, CONFIRMATION_EMPTY_STATE } from "./props"; 3 | const ActionParamsContext = React.createContext( 4 | CONFIRMATION_EMPTY_STATE 5 | ); 6 | export default ActionParamsContext; 7 | 8 | export const useActionParams = () => useContext(ActionParamsContext); 9 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/FormDialog/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { IActionParams, paramsDialogProps } from "./props"; 4 | import Dialog from "./Dialog"; 5 | import ActionParamsContext from "./Context"; 6 | interface IActionParamsProviderProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | const ActionParamsProvider: React.FC = ({ 11 | children, 12 | }) => { 13 | const [state, setState] = useState(); 14 | const [open, setOpen] = useState(false); 15 | const handleClose = () => { 16 | setState(undefined); 17 | setOpen(false); 18 | }; 19 | const requestParams = (props: paramsDialogProps) => { 20 | setState(props); 21 | setOpen(true); 22 | }; 23 | return ( 24 | 32 | {children} 33 | 34 | {state && } 35 | 36 | ); 37 | }; 38 | 39 | export default ActionParamsProvider; 40 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/FormDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { useActionParams } from "./Context"; 2 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/FormDialog/props.ts: -------------------------------------------------------------------------------- 1 | export type paramsDialogProps = 2 | | { 3 | column: any; 4 | row: any; 5 | handleRun: (actionParams: any) => void; 6 | } 7 | | undefined; 8 | export interface IActionParams { 9 | dialogProps?: paramsDialogProps; 10 | handleClose: () => void; 11 | open: boolean; 12 | requestParams: (props: paramsDialogProps) => void; 13 | } 14 | export const CONFIRMATION_EMPTY_STATE = { 15 | dialogProps: undefined, 16 | open: false, 17 | handleClose: () => {}, 18 | requestParams: () => {}, 19 | }; 20 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | import clsx from "clsx"; 3 | import _get from "lodash/get"; 4 | 5 | import { createStyles, makeStyles, Grid } from "@material-ui/core"; 6 | 7 | import ActionFab from "./ActionFab"; 8 | import { sanitiseCallableName, isUrl } from "utils/fns"; 9 | 10 | const useStyles = makeStyles((theme) => 11 | createStyles({ 12 | root: { padding: theme.spacing(0, 0.375, 0, 1.5) }, 13 | labelContainer: { overflowX: "hidden" }, 14 | }) 15 | ); 16 | 17 | export default function Action({ 18 | column, 19 | row, 20 | value, 21 | onSubmit, 22 | disabled, 23 | }: IHeavyCellProps) { 24 | const classes = useStyles(); 25 | 26 | const hasRan = value && value.status; 27 | 28 | return ( 29 | 35 | 36 | {hasRan && isUrl(value.status) ? ( 37 | 38 | {value.status} 39 | 40 | ) : hasRan ? ( 41 | value.status 42 | ) : ( 43 | sanitiseCallableName(column.key) 44 | )} 45 | 46 | 47 | 48 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /www/src/components/fields/Action/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import ActionIcon from "assets/icons/Action"; 6 | import BasicCell from "./BasicCell"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Action" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Action" */) 15 | ); 16 | const Settings = lazy( 17 | () => import("./Settings" /* webpackChunkName: "Settings-Action" */) 18 | ); 19 | export const config: IFieldConfig = { 20 | type: FieldType.action, 21 | name: "Action", 22 | dataType: "any", 23 | initialValue: {}, 24 | icon: , 25 | description: 26 | "A button with a pre-defined action. Triggers a Cloud Function. 3 different states: Disabled, Enabled, Active (Clicked). Supports Undo and Redo.", 27 | TableCell: withHeavyCell(BasicCell, TableCell), 28 | TableEditor: NullEditor, 29 | SideDrawerField, 30 | settings: Settings, 31 | requireConfiguration: true, 32 | }; 33 | export default config; 34 | -------------------------------------------------------------------------------- /www/src/components/fields/Aggregate/index.tsx: -------------------------------------------------------------------------------- 1 | import { IFieldConfig, FieldType } from "components/fields/types"; 2 | import withBasicCell from "../_withTableCell/withBasicCell"; 3 | 4 | import AggregateIcon from "@material-ui/icons/Layers"; 5 | import BasicCell from "../_BasicCell/BasicCellNull"; 6 | import NullEditor from "components/Table/editors/NullEditor"; 7 | 8 | export const config: IFieldConfig = { 9 | type: FieldType.aggregate, 10 | name: "Aggregate", 11 | dataType: "string", 12 | initialValue: "", 13 | initializable: false, 14 | icon: , 15 | description: 16 | "Value aggregated from a specified sub-table of the row. Displayed using any other field type. Requires Cloud Function setup.", 17 | TableCell: withBasicCell(BasicCell), 18 | TableEditor: NullEditor, 19 | SideDrawerField: BasicCell as any, 20 | }; 21 | export default config; 22 | -------------------------------------------------------------------------------- /www/src/components/fields/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import CheckboxIcon from "@material-ui/icons/CheckBox"; 6 | import BasicCell from "../_BasicCell/BasicCellName"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Checkbox" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import( 15 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-Checkbox" */ 16 | ) 17 | ); 18 | 19 | export const config: IFieldConfig = { 20 | type: FieldType.checkbox, 21 | name: "Checkbox", 22 | dataType: "boolean", 23 | initialValue: false, 24 | initializable: true, 25 | icon: , 26 | description: "Either checked or unchecked. Unchecked by default.", 27 | TableCell: withHeavyCell(BasicCell, TableCell), 28 | TableEditor: NullEditor, 29 | csvImportParser: (value: string) => { 30 | if (["YES", "TRUE", "1"].includes(value.toUpperCase())) return true; 31 | else if (["NO", "FALSE", "0"].includes(value.toUpperCase())) return false; 32 | else return null; 33 | }, 34 | SideDrawerField, 35 | }; 36 | export default config; 37 | -------------------------------------------------------------------------------- /www/src/components/fields/Checkbox/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles } from "@material-ui/core"; 2 | import { green } from "@material-ui/core/colors"; 3 | 4 | export const useSwitchStyles = makeStyles(() => 5 | createStyles({ 6 | switchBase: { 7 | "&$checked": { color: green["A700"] }, 8 | "&$checked + $track": { backgroundColor: green["A700"] }, 9 | }, 10 | checked: {}, 11 | track: {}, 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /www/src/components/fields/Code/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | import { useTheme } from "@material-ui/core"; 4 | 5 | export default function Code({ value }: IBasicCellProps) { 6 | const theme = useTheme(); 7 | 8 | return ( 9 |
21 | {value} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /www/src/components/fields/Code/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import CodeEditor from "components/CodeEditor"; 5 | import { makeStyles, createStyles } from "@material-ui/core"; 6 | 7 | const useStyles = makeStyles((theme) => 8 | createStyles({ 9 | wrapper: { 10 | border: "1px solid", 11 | borderColor: 12 | theme.palette.type === "light" 13 | ? "rgba(0, 0, 0, 0.09)" 14 | : "rgba(255, 255, 255, 0.09)", 15 | 16 | borderRadius: theme.shape.borderRadius, 17 | overflow: "hidden", 18 | }, 19 | }) 20 | ); 21 | 22 | export default function Code({ 23 | control, 24 | column, 25 | disabled, 26 | }: ISideDrawerFieldProps) { 27 | const classes = useStyles(); 28 | 29 | return ( 30 | ( 34 | 45 | )} 46 | /> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /www/src/components/fields/Code/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import CodeIcon from "@material-ui/icons/Code"; 6 | import BasicCell from "./BasicCell"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Code" */) 12 | ); 13 | 14 | export const config: IFieldConfig = { 15 | type: FieldType.code, 16 | name: "Code", 17 | dataType: "string", 18 | initialValue: "", 19 | initializable: true, 20 | icon: , 21 | description: "Raw code editable with Monaco Editor.", 22 | TableCell: withBasicCell(BasicCell), 23 | TableEditor: withSideDrawerEditor(BasicCell), 24 | SideDrawerField, 25 | }; 26 | export default config; 27 | -------------------------------------------------------------------------------- /www/src/components/fields/Color/InlineCell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import { IPopoverInlineCellProps } from "../types"; 4 | 5 | import { makeStyles, createStyles, Grid, ButtonBase } from "@material-ui/core"; 6 | 7 | const useStyles = makeStyles((theme) => 8 | createStyles({ 9 | root: { 10 | font: "inherit", 11 | color: "inherit !important", 12 | letterSpacing: "inherit", 13 | textAlign: "inherit", 14 | 15 | padding: theme.spacing(0, 1), 16 | }, 17 | 18 | colorIndicator: { 19 | width: 20, 20 | height: 20, 21 | 22 | boxShadow: `0 0 0 1px ${theme.palette.text.disabled} inset`, 23 | borderRadius: theme.shape.borderRadius / 2, 24 | }, 25 | }) 26 | ); 27 | 28 | export const Color = React.forwardRef(function Color( 29 | { value, showPopoverCell, disabled }: IPopoverInlineCellProps, 30 | ref: React.Ref 31 | ) { 32 | const classes = useStyles(); 33 | 34 | return ( 35 | showPopoverCell(true)} 42 | ref={ref} 43 | disabled={disabled} 44 | > 45 | 46 |
50 | 51 | 52 | 53 | {value?.hex} 54 | 55 | 56 | ); 57 | }); 58 | export default Color; 59 | -------------------------------------------------------------------------------- /www/src/components/fields/Color/PopoverCell.tsx: -------------------------------------------------------------------------------- 1 | import { IPopoverCellProps } from "../types"; 2 | import { ChromePicker } from "react-color"; 3 | import _get from "lodash/get"; 4 | 5 | export default function Color({ value, onSubmit }: IPopoverCellProps) { 6 | const handleChangeComplete = (color) => onSubmit(color); 7 | 8 | return ( 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /www/src/components/fields/Color/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withPopoverCell from "../_withTableCell/withPopoverCell"; 4 | 5 | import ColorIcon from "@material-ui/icons/Colorize"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import InlineCell from "./InlineCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const PopoverCell = lazy( 11 | () => import("./PopoverCell" /* webpackChunkName: "PopoverCell-Color" */) 12 | ); 13 | const SideDrawerField = lazy( 14 | () => 15 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Color" */) 16 | ); 17 | 18 | export const config: IFieldConfig = { 19 | type: FieldType.color, 20 | name: "Color", 21 | dataType: "Record", 22 | initialValue: {}, 23 | initializable: true, 24 | icon: , 25 | description: "Visual color picker. Supports Hex, RGBA, HSLA.", 26 | TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { 27 | anchorOrigin: { horizontal: "left", vertical: "bottom" }, 28 | }), 29 | TableEditor: NullEditor, 30 | SideDrawerField, 31 | }; 32 | export default config; 33 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectService/PopoverCell.tsx: -------------------------------------------------------------------------------- 1 | import { IPopoverCellProps } from "../types"; 2 | import _get from "lodash/get"; 3 | 4 | import ConnectServiceSelect from "./ConnectServiceSelect"; 5 | 6 | export default function ConnectService({ 7 | value, 8 | onSubmit, 9 | column, 10 | parentRef, 11 | showPopoverCell, 12 | disabled, 13 | docRef, 14 | }: IPopoverCellProps) { 15 | const config = column.config ?? {}; 16 | if (!config) return null; 17 | 18 | return ( 19 | showPopoverCell(false), 35 | }, 36 | }} 37 | /> 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectService/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { TextField, FormControlLabel, Switch } from "@material-ui/core"; 2 | 3 | export default function Settings({ config, handleChange }) { 4 | return ( 5 | <> 6 | { 12 | handleChange("url")(e.target.value); 13 | }} 14 | /> 15 | { 23 | handleChange("resultsKey")(e.target.value); 24 | }} 25 | /> 26 | { 32 | handleChange("primaryKey")(e.target.value); 33 | }} 34 | /> 35 | { 41 | handleChange("titleKey")(e.target.value); 42 | }} 43 | /> 44 | { 50 | handleChange("subtitleKey")(e.target.value); 51 | }} 52 | /> 53 | handleChange("multiple")(!Boolean(config.multiple))} 58 | name="select-multiple" 59 | /> 60 | } 61 | label="Enable multiple item selection" 62 | /> 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectService/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withPopoverCell from "../_withTableCell/withPopoverCell"; 4 | 5 | import ConnectServiceIcon from "@material-ui/icons/Http"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import InlineCell from "./InlineCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const PopoverCell = lazy( 11 | () => 12 | import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectService" */) 13 | ); 14 | const SideDrawerField = lazy( 15 | () => 16 | import( 17 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-ConnectService" */ 18 | ) 19 | ); 20 | const Settings = lazy( 21 | () => import("./Settings" /* webpackChunkName: "Settings-ConnectService" */) 22 | ); 23 | 24 | export const config: IFieldConfig = { 25 | type: FieldType.connectService, 26 | name: "Connect Table", 27 | dataType: "{ docPath: string; snapshot: Record; }", 28 | initialValue: [], 29 | icon: , 30 | description: 31 | "Connects to an external web service to fetch a list of results.", 32 | TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { 33 | anchorOrigin: { horizontal: "left", vertical: "bottom" }, 34 | transparent: true, 35 | }), 36 | TableEditor: NullEditor, 37 | SideDrawerField, 38 | settings: Settings, 39 | }; 40 | export default config; 41 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectService/utils.ts: -------------------------------------------------------------------------------- 1 | export const sanitiseValue = (value: any) => { 2 | if (value === undefined || value === null || value === "") return []; 3 | else return value as string[]; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectTable/PopoverCell.tsx: -------------------------------------------------------------------------------- 1 | import { IPopoverCellProps } from "../types"; 2 | import _get from "lodash/get"; 3 | 4 | import ConnectTableSelect from "./ConnectTableSelect"; 5 | 6 | export default function ConnectTable({ 7 | value, 8 | onSubmit, 9 | column, 10 | parentRef, 11 | showPopoverCell, 12 | row, 13 | disabled, 14 | }: IPopoverCellProps) { 15 | const config = column.config ?? {}; 16 | if (!config || !config.primaryKeys) return null; 17 | 18 | return ( 19 | showPopoverCell(false)} 38 | loadBeforeOpen 39 | /> 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectTable/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withPopoverCell from "../_withTableCell/withPopoverCell"; 4 | 5 | import ConnectTableIcon from "assets/icons/ConnectTable"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import InlineCell from "./InlineCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const PopoverCell = lazy( 11 | () => 12 | import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectTable" */) 13 | ); 14 | const SideDrawerField = lazy( 15 | () => 16 | import( 17 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-ConnectTable" */ 18 | ) 19 | ); 20 | const Settings = lazy( 21 | () => import("./Settings" /* webpackChunkName: "Settings-ConnectTable" */) 22 | ); 23 | 24 | export const config: IFieldConfig = { 25 | type: FieldType.connectTable, 26 | name: "Connect Table", 27 | dataType: "{ docPath: string; snapshot: Record; }", 28 | initialValue: [], 29 | icon: , 30 | description: 31 | "Connects to an existing table to fetch a snapshot of values from a row. Requires Algolia integration.", 32 | TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { 33 | anchorOrigin: { horizontal: "left", vertical: "bottom" }, 34 | transparent: true, 35 | }), 36 | TableEditor: NullEditor, 37 | SideDrawerField, 38 | settings: Settings, 39 | }; 40 | export default config; 41 | -------------------------------------------------------------------------------- /www/src/components/fields/ConnectTable/utils.ts: -------------------------------------------------------------------------------- 1 | export const sanitiseValue = (value: any) => { 2 | if (value === undefined || value === null || value === "") return []; 3 | else return value as string[]; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/components/fields/Date/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | import { format } from "date-fns"; 3 | import { DATE_FORMAT } from "constants/dates"; 4 | import { DateIcon } from "."; 5 | 6 | export default function Date_({ value }: IBasicCellProps) { 7 | if (!!value && "toDate" in value) { 8 | try { 9 | const formatted = format(value.toDate(), DATE_FORMAT); 10 | return ( 11 | <> 12 | 13 | {formatted} 14 | 15 | ); 16 | } catch (e) { 17 | return null; 18 | } 19 | } 20 | 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /www/src/components/fields/Date/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ISettingsProps } from "../types"; 2 | import _sortBy from "lodash/sortBy"; 3 | import Subheading from "components/Table/ColumnMenu/Subheading"; 4 | import MultiSelect from "@antlerengineering/multiselect"; 5 | import { DATE_FORMAT } from "constants/dates"; 6 | 7 | export default function Settings({ handleChange, config }: ISettingsProps) { 8 | return ( 9 | <> 10 | Date Config 11 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /www/src/components/fields/Date/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | import { parse, format } from "date-fns"; 5 | import { DATE_FORMAT } from "constants/dates"; 6 | import DateIcon from "@material-ui/icons/Today"; 7 | import BasicCell from "./BasicCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const TableCell = lazy( 11 | () => import("./TableCell" /* webpackChunkName: "TableCell-Date" */) 12 | ); 13 | const SideDrawerField = lazy( 14 | () => 15 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Date" */) 16 | ); 17 | const Settings = lazy( 18 | () => import("./Settings" /* webpackChunkName: "Settings-ConnectTable" */) 19 | ); 20 | 21 | export const config: IFieldConfig = { 22 | type: FieldType.date, 23 | name: "Date", 24 | dataType: "firebase.firestore.Timestamp", 25 | initialValue: null, 26 | initializable: true, 27 | icon: , 28 | description: 29 | "Date displayed and input by default as YYYY/MM/DD or input using a picker module.", 30 | TableCell: withHeavyCell(BasicCell, TableCell), 31 | TableEditor: NullEditor, 32 | SideDrawerField, 33 | settings: Settings, 34 | csvImportParser: (value, config) => 35 | parse(value, config?.format ?? DATE_FORMAT, new Date()), 36 | csvExportFormatter: (value: any, config?: any) => 37 | format(value.toDate(), config?.format ?? DATE_FORMAT), 38 | }; 39 | export default config; 40 | 41 | export { DateIcon }; 42 | -------------------------------------------------------------------------------- /www/src/components/fields/Date/utils.ts: -------------------------------------------------------------------------------- 1 | export const transformValue = (value: any) => { 2 | if (typeof value === "number") return new Date(value); 3 | if (value && "toDate" in value) return value.toDate(); 4 | if (value !== undefined) return value; 5 | return null; 6 | }; 7 | 8 | export const sanitizeValue = (value: Date | null) => { 9 | if (isNaN(value?.valueOf() ?? 0)) return undefined; 10 | return value; 11 | }; 12 | -------------------------------------------------------------------------------- /www/src/components/fields/DateTime/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | import { format } from "date-fns"; 3 | import { DATE_TIME_FORMAT } from "constants/dates"; 4 | import { DateTimeIcon } from "."; 5 | 6 | export default function DateTime({ value }: IBasicCellProps) { 7 | if (!!value && "toDate" in value) { 8 | try { 9 | const formatted = format(value.toDate(), DATE_TIME_FORMAT); 10 | return ( 11 | <> 12 | 13 | {formatted} 14 | 15 | ); 16 | } catch (e) { 17 | return null; 18 | } 19 | } 20 | 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /www/src/components/fields/DateTime/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | import { parseJSON } from "date-fns"; 5 | 6 | import DateTimeIcon from "@material-ui/icons/AccessTime"; 7 | import BasicCell from "./BasicCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const TableCell = lazy( 11 | () => import("./TableCell" /* webpackChunkName: "TableCell-DateTime" */) 12 | ); 13 | const SideDrawerField = lazy( 14 | () => 15 | import( 16 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-DateTime" */ 17 | ) 18 | ); 19 | 20 | export const config: IFieldConfig = { 21 | type: FieldType.dateTime, 22 | name: "Time & Date", 23 | dataType: "firebase.firestore.Timestamp", 24 | initialValue: null, 25 | initializable: true, 26 | icon: , 27 | description: 28 | "Time and Date can be written as YYYY/MM/DD hh:mm (am/pm) or input using a picker module.", 29 | TableCell: withHeavyCell(BasicCell, TableCell), 30 | TableEditor: NullEditor, 31 | SideDrawerField, 32 | csvImportParser: (value) => parseJSON(value).getTime(), 33 | }; 34 | export default config; 35 | 36 | export { DateTimeIcon }; 37 | -------------------------------------------------------------------------------- /www/src/components/fields/Derivative/index.tsx: -------------------------------------------------------------------------------- 1 | import { IFieldConfig, FieldType } from "components/fields/types"; 2 | import withBasicCell from "../_withTableCell/withBasicCell"; 3 | 4 | import DerivativeIcon from "assets/icons/Derivative"; 5 | import BasicCell from "../_BasicCell/BasicCellNull"; 6 | import NullEditor from "components/Table/editors/NullEditor"; 7 | import Settings from "./Settings"; 8 | 9 | export const config: IFieldConfig = { 10 | type: FieldType.derivative, 11 | name: "Derivative", 12 | dataType: "string", 13 | initialValue: "", 14 | initializable: true, 15 | icon: , 16 | requireConfiguration: true, 17 | description: 18 | "Value derived from the rest of the row’s values. Displayed using any other field type. Requires Cloud Function setup.", 19 | TableCell: withBasicCell(BasicCell), 20 | TableEditor: NullEditor, 21 | SideDrawerField: BasicCell as any, 22 | settings: Settings, 23 | }; 24 | export default config; 25 | -------------------------------------------------------------------------------- /www/src/components/fields/Duration/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { useFieldStyles } from "components/SideDrawer/Form/utils"; 5 | import { getDurationString } from "./utils"; 6 | 7 | export default function Duration({ column, control }: ISideDrawerFieldProps) { 8 | const fieldClasses = useFieldStyles(); 9 | 10 | return ( 11 | { 15 | if ( 16 | !value || 17 | !value.start || 18 | !("toDate" in value.start) || 19 | !value.end || 20 | !("toDate" in value.end) 21 | ) 22 | return
; 23 | 24 | return ( 25 |
26 | {getDurationString(value.start.toDate(), value.end.toDate())} 27 |
28 | ); 29 | }} 30 | /> 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /www/src/components/fields/Duration/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | import _get from "lodash/get"; 3 | 4 | import { getDurationString } from "./utils"; 5 | 6 | export default function Duration({ value }: IHeavyCellProps) { 7 | if ( 8 | !value || 9 | !value.start || 10 | !("toDate" in value.start) || 11 | !value.end || 12 | !("toDate" in value.end) 13 | ) 14 | return null; 15 | 16 | return <>{getDurationString(value.start.toDate(), value.end.toDate())}; 17 | } 18 | -------------------------------------------------------------------------------- /www/src/components/fields/Duration/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import DurationIcon from "@material-ui/icons/Timer"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Duration" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import( 15 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-Duration" */ 16 | ) 17 | ); 18 | 19 | export const config: IFieldConfig = { 20 | type: FieldType.duration, 21 | name: "Duration", 22 | dataType: 23 | "{ start: firebase.firestore.Timestamp, end?: firebase.firestore.Timestamp }", 24 | initialValue: {}, 25 | icon: , 26 | description: "Duration calculated from two timestamps.", 27 | TableCell: withHeavyCell(BasicCell, TableCell), 28 | TableEditor: NullEditor, 29 | SideDrawerField, 30 | }; 31 | export default config; 32 | -------------------------------------------------------------------------------- /www/src/components/fields/Duration/utils.ts: -------------------------------------------------------------------------------- 1 | export const getDurationString = (start: Date, end: Date) => { 2 | let distance = Math.abs(end.getTime() - start.getTime()); 3 | const hours = Math.floor(distance / 3600000); 4 | distance -= hours * 3600000; 5 | const minutes = Math.floor(distance / 60000); 6 | distance -= minutes * 60000; 7 | const seconds = Math.floor(distance / 1000); 8 | 9 | return `${hours ? `${hours}h` : ""} ${("0" + minutes).slice(-2)}m ${( 10 | "0" + seconds 11 | ).slice(-2)}s`; 12 | }; 13 | -------------------------------------------------------------------------------- /www/src/components/fields/Email/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { TextField } from "@material-ui/core"; 5 | 6 | export default function Email({ 7 | control, 8 | column, 9 | disabled, 10 | }: ISideDrawerFieldProps) { 11 | return ( 12 | { 16 | return ( 17 | 34 | ); 35 | }} 36 | /> 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /www/src/components/fields/Email/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import EmailIcon from "@material-ui/icons/Mail"; 6 | import BasicCell from "../_BasicCell/BasicCellValue"; 7 | import TextEditor from "components/Table/editors/TextEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Email" */) 12 | ); 13 | 14 | export const config: IFieldConfig = { 15 | type: FieldType.email, 16 | name: "Email", 17 | dataType: "string", 18 | initialValue: "", 19 | initializable: true, 20 | icon: , 21 | description: "Email address. Firetable does not validate emails.", 22 | TableCell: withBasicCell(BasicCell), 23 | TableEditor: TextEditor, 24 | SideDrawerField, 25 | }; 26 | export default config; 27 | -------------------------------------------------------------------------------- /www/src/components/fields/File/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import FileIcon from "@material-ui/icons/AttachFile"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-File" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-File" */) 15 | ); 16 | 17 | export const config: IFieldConfig = { 18 | type: FieldType.file, 19 | name: "File", 20 | dataType: 21 | "{ downloadURL: string, lastModifiedTS: number, name: string, type, ref }[]", 22 | initialValue: [], 23 | icon: , 24 | description: "File uploaded to Firebase Storage. Supports any file type.", 25 | TableCell: withHeavyCell(BasicCell, TableCell), 26 | TableEditor: NullEditor, 27 | SideDrawerField, 28 | }; 29 | export default config; 30 | 31 | export { FileIcon }; 32 | -------------------------------------------------------------------------------- /www/src/components/fields/Id/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { ISideDrawerFieldProps } from "../types"; 2 | 3 | import { useTheme } from "@material-ui/core"; 4 | import { useFieldStyles } from "components/SideDrawer/Form/utils"; 5 | 6 | export default function Id({ docRef }: ISideDrawerFieldProps) { 7 | const theme = useTheme(); 8 | const fieldClasses = useFieldStyles(); 9 | 10 | return ( 11 |
15 | {docRef.id} 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /www/src/components/fields/Id/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | 3 | import { useTheme } from "@material-ui/core"; 4 | 5 | export default function Id({ docRef }: IHeavyCellProps) { 6 | const theme = useTheme(); 7 | 8 | return ( 9 | 12 | {docRef.id} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /www/src/components/fields/Id/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import IdIcon from "assets/icons/Id"; 6 | import BasicCell from "../_BasicCell/BasicCellValue"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Id" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Id" */) 14 | ); 15 | 16 | export const config: IFieldConfig = { 17 | type: FieldType.id, 18 | name: "ID", 19 | dataType: "undefined", 20 | initialValue: "", 21 | icon: , 22 | description: "Displays the row’s document ID. Cannot be sorted.", 23 | TableCell: withHeavyCell(BasicCell, TableCell), 24 | TableEditor: withSideDrawerEditor(TableCell), 25 | SideDrawerField, 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /www/src/components/fields/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import ImageIcon from "@material-ui/icons/PhotoSizeSelectActual"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Image" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Image" */) 15 | ); 16 | 17 | export const config: IFieldConfig = { 18 | type: FieldType.image, 19 | name: "Image", 20 | dataType: 21 | "{ downloadURL: string, lastModifiedTS: number, name: string, type, ref }[]", 22 | initialValue: [], 23 | icon: , 24 | description: 25 | "Image file uploaded to Firebase Storage. Supports JPEG, PNG, SVG, GIF, WebP.", 26 | TableCell: withHeavyCell(BasicCell, TableCell), 27 | TableEditor: NullEditor, 28 | SideDrawerField, 29 | }; 30 | export default config; 31 | 32 | export const IMAGE_MIME_TYPES = [ 33 | "image/jpeg", 34 | "image/png", 35 | "image/svg+xml", 36 | "image/gif", 37 | "image/webp", 38 | ]; 39 | -------------------------------------------------------------------------------- /www/src/components/fields/Json/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import jsonFormat from "json-format"; 2 | import { IBasicCellProps } from "../types"; 3 | 4 | import { useTheme } from "@material-ui/core"; 5 | 6 | export default function Json({ value }: IBasicCellProps) { 7 | const theme = useTheme(); 8 | 9 | if (!value) return null; 10 | 11 | const formattedJson = jsonFormat(value, { 12 | type: "space", 13 | char: " ", 14 | size: 2, 15 | }); 16 | 17 | return ( 18 |
30 | {formattedJson} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /www/src/components/fields/Json/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, FormControlLabel } from "@material-ui/core"; 2 | 3 | const Settings = ({ config, handleChange }) => { 4 | return ( 5 | <> 6 | handleChange("isArray")(!Boolean(config.isArray))} 11 | name="isArray" 12 | /> 13 | } 14 | label="Set as an array" 15 | /> 16 | 17 | ); 18 | }; 19 | export default Settings; 20 | -------------------------------------------------------------------------------- /www/src/components/fields/Json/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import JsonIcon from "assets/icons/Json"; 6 | import BasicCell from "./BasicCell"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Json" */) 12 | ); 13 | 14 | const Settings = lazy( 15 | () => import("./Settings" /* webpackChunkName: "Settings-Json" */) 16 | ); 17 | 18 | export const config: IFieldConfig = { 19 | type: FieldType.json, 20 | name: "JSON", 21 | dataType: "any", 22 | initialValue: {}, 23 | initializable: true, 24 | icon: , 25 | description: "JSON object editable with a visual JSON editor.", 26 | TableCell: withBasicCell(BasicCell), 27 | TableEditor: withSideDrawerEditor(BasicCell), 28 | csvImportParser: (value) => { 29 | try { 30 | return JSON.parse(value); 31 | } catch (e) { 32 | return null; 33 | } 34 | }, 35 | SideDrawerField, 36 | settings: Settings, 37 | }; 38 | export default config; 39 | -------------------------------------------------------------------------------- /www/src/components/fields/LongText/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | import { useTheme } from "@material-ui/core"; 4 | 5 | export default function LongText({ value }: IBasicCellProps) { 6 | const theme = useTheme(); 7 | 8 | return ( 9 |
19 | {value} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /www/src/components/fields/LongText/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { makeStyles, createStyles, TextField } from "@material-ui/core"; 5 | 6 | const useStyles = makeStyles((theme) => 7 | createStyles({ 8 | multiline: { padding: theme.spacing(2.25, 1.5) }, 9 | }) 10 | ); 11 | 12 | export default function LongText({ 13 | control, 14 | column, 15 | disabled, 16 | }: ISideDrawerFieldProps) { 17 | const classes = useStyles(); 18 | return ( 19 | { 23 | return ( 24 | 40 | ); 41 | }} 42 | /> 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /www/src/components/fields/LongText/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import LongTextIcon from "@material-ui/icons/Notes"; 6 | import BasicCell from "./BasicCell"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import( 12 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-LongText" */ 13 | ) 14 | ); 15 | 16 | export const config: IFieldConfig = { 17 | type: FieldType.longText, 18 | name: "Long Text", 19 | dataType: "string", 20 | initialValue: "", 21 | initializable: true, 22 | icon: , 23 | description: "Large amount of text, such as sentences and paragraphs.", 24 | TableCell: withBasicCell(BasicCell), 25 | TableEditor: withSideDrawerEditor(BasicCell), 26 | SideDrawerField, 27 | }; 28 | export default config; 29 | -------------------------------------------------------------------------------- /www/src/components/fields/MultiSelect/ConvertStringToArray.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Tooltip, Button } from "@material-ui/core"; 2 | 3 | export const ConvertStringToArray = ({ value, onSubmit }) => ( 4 | 5 | 6 | {value} 7 | 8 | 9 | 10 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /www/src/components/fields/MultiSelect/PopoverCell.tsx: -------------------------------------------------------------------------------- 1 | import { IPopoverCellProps } from "../types"; 2 | import _get from "lodash/get"; 3 | 4 | import MultiSelect_ from "@antlerengineering/multiselect"; 5 | 6 | import { sanitiseValue } from "./utils"; 7 | 8 | export default function MultiSelect({ 9 | value, 10 | onSubmit, 11 | column, 12 | parentRef, 13 | showPopoverCell, 14 | disabled, 15 | }: IPopoverCellProps) { 16 | const config = column.config ?? {}; 17 | 18 | return ( 19 | showPopoverCell(false)} 40 | /> 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /www/src/components/fields/MultiSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withPopoverCell from "../_withTableCell/withPopoverCell"; 4 | 5 | import MultiSelectIcon from "assets/icons/MultiSelect"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import InlineCell from "./InlineCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const PopoverCell = lazy( 11 | () => 12 | import("./PopoverCell" /* webpackChunkName: "PopoverCell-MultiSelect" */) 13 | ); 14 | const SideDrawerField = lazy( 15 | () => 16 | import( 17 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-MultiSelect" */ 18 | ) 19 | ); 20 | const Settings = lazy( 21 | () => 22 | import( 23 | "../SingleSelect/Settings" /* webpackChunkName: "Settings-SingleSelect" */ 24 | ) 25 | ); 26 | 27 | export const config: IFieldConfig = { 28 | type: FieldType.multiSelect, 29 | name: "Multi Select", 30 | dataType: "string[]", 31 | initialValue: [], 32 | initializable: true, 33 | icon: , 34 | description: 35 | "Dropdown selector with searchable options and check box behavior. Optionally allows users to input custom values. Max selection: all options.", 36 | TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { 37 | anchorOrigin: { horizontal: "left", vertical: "bottom" }, 38 | transparent: true, 39 | }), 40 | TableEditor: NullEditor, 41 | SideDrawerField, 42 | settings: Settings, 43 | csvImportParser: (v) => { 44 | if (v.includes(",")) { 45 | return v.split(",").map((i) => i.trim()); 46 | } else if (v !== "") return [v]; 47 | else return v; 48 | }, 49 | requireConfiguration: true, 50 | }; 51 | export default config; 52 | -------------------------------------------------------------------------------- /www/src/components/fields/MultiSelect/utils.ts: -------------------------------------------------------------------------------- 1 | export const sanitiseValue = (value: any) => { 2 | if (value === undefined || value === null || value === "") return []; 3 | else return value as string[]; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/components/fields/Number/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | export default function Number_({ value }: IBasicCellProps) { 4 | return <>{`${value ?? ""}`}; 5 | } 6 | -------------------------------------------------------------------------------- /www/src/components/fields/Number/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { TextField } from "@material-ui/core"; 5 | 6 | export default function Number_({ 7 | control, 8 | column, 9 | disabled, 10 | }: ISideDrawerFieldProps) { 11 | return ( 12 | { 16 | const handleChange = (e) => onChange(Number(e.target.value)); 17 | 18 | return ( 19 | 33 | ); 34 | }} 35 | /> 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /www/src/components/fields/Number/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import NumberIcon from "assets/icons/Number"; 6 | import BasicCell from "./BasicCell"; 7 | import TextEditor from "components/Table/editors/TextEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Number" */) 12 | ); 13 | 14 | export const config: IFieldConfig = { 15 | type: FieldType.number, 16 | name: "Number", 17 | dataType: "number", 18 | initialValue: 0, 19 | initializable: true, 20 | icon: , 21 | description: "Numeric data.", 22 | TableCell: withBasicCell(BasicCell), 23 | TableEditor: TextEditor, 24 | SideDrawerField, 25 | csvImportParser: (v) => { 26 | try { 27 | const parsedValue = parseFloat(v); 28 | return Number.isNaN(parsedValue) ? null : parsedValue; 29 | } catch (e) { 30 | return null; 31 | } 32 | }, 33 | }; 34 | export default config; 35 | -------------------------------------------------------------------------------- /www/src/components/fields/Percentage/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | import { useTheme } from "@material-ui/core"; 4 | import { resultColorsScale } from "utils/color"; 5 | 6 | export default function Percentage({ value }: IBasicCellProps) { 7 | const theme = useTheme(); 8 | 9 | if (typeof value === "number") 10 | return ( 11 | <> 12 |
26 |
35 | {Math.round(value * 100)}% 36 |
37 | 38 | ); 39 | 40 | return null; 41 | } 42 | -------------------------------------------------------------------------------- /www/src/components/fields/Percentage/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import PercentageIcon from "assets/icons/Percentage"; 6 | import BasicCell from "./BasicCell"; 7 | import TextEditor from "components/Table/editors/TextEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import( 12 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-Percentage" */ 13 | ) 14 | ); 15 | 16 | export const config: IFieldConfig = { 17 | type: FieldType.percentage, 18 | name: "Percentage", 19 | dataType: "number", 20 | initialValue: 0, 21 | initializable: true, 22 | icon: , 23 | description: "Percentage stored as a number between 0 and 1.", 24 | TableCell: withBasicCell(BasicCell), 25 | TableEditor: TextEditor, 26 | SideDrawerField, 27 | csvImportParser: (v) => { 28 | try { 29 | const parsedValue = parseFloat(v); 30 | return Number.isNaN(parsedValue) ? null : parsedValue; 31 | } catch (e) { 32 | return null; 33 | } 34 | }, 35 | }; 36 | export default config; 37 | -------------------------------------------------------------------------------- /www/src/components/fields/Phone/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { TextField } from "@material-ui/core"; 5 | 6 | export default function Phone({ 7 | control, 8 | column, 9 | disabled, 10 | }: ISideDrawerFieldProps) { 11 | return ( 12 | { 16 | return ( 17 | 35 | ); 36 | }} 37 | /> 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /www/src/components/fields/Phone/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import PhoneIcon from "@material-ui/icons/Phone"; 6 | import BasicCell from "../_BasicCell/BasicCellValue"; 7 | import TextEditor from "components/Table/editors/TextEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Phone" */) 12 | ); 13 | 14 | export const config: IFieldConfig = { 15 | type: FieldType.phone, 16 | name: "Phone", 17 | dataType: "string", 18 | initialValue: "", 19 | initializable: true, 20 | icon: , 21 | description: 22 | "Phone numbers stored as text. Firetable does not validate phone numbers.", 23 | TableCell: withBasicCell(BasicCell), 24 | TableEditor: TextEditor, 25 | SideDrawerField, 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /www/src/components/fields/Rating/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { ISettingsProps } from "../types"; 2 | 3 | import { Slider } from "@material-ui/core"; 4 | import Subheading from "components/Table/ColumnMenu/Subheading"; 5 | 6 | import _sortBy from "lodash/sortBy"; 7 | 8 | export default function Settings({ handleChange, config }: ISettingsProps) { 9 | return ( 10 | <> 11 | Maximum number of stars 12 | `${v} max stars`} 16 | aria-labelledby="max-slider" 17 | valueLabelDisplay="auto" 18 | onChange={(_, v) => { 19 | handleChange("max")(v); 20 | }} 21 | step={1} 22 | marks 23 | min={1} 24 | max={15} 25 | /> 26 | Slider precision 27 | `${v} rating step size`} 31 | aria-labelledby="precision-slider" 32 | valueLabelDisplay="auto" 33 | onChange={(_, v) => { 34 | handleChange("precision")(v); 35 | }} 36 | step={0.25} 37 | marks 38 | min={0.25} 39 | max={1} 40 | /> 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /www/src/components/fields/Rating/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { Grid } from "@material-ui/core"; 5 | import { Rating as MuiRating } from "@material-ui/lab"; 6 | import StarBorderIcon from "@material-ui/icons/StarBorder"; 7 | 8 | import { useFieldStyles } from "components/SideDrawer/Form/utils"; 9 | import { useRatingStyles } from "./styles"; 10 | 11 | export default function Rating({ 12 | control, 13 | column, 14 | disabled, 15 | }: ISideDrawerFieldProps) { 16 | const fieldClasses = useFieldStyles(); 17 | const ratingClasses = useRatingStyles(); 18 | 19 | // Set max and precision from config 20 | const { 21 | max, 22 | precision, 23 | }: { 24 | max: number; 25 | precision: number; 26 | } = { 27 | max: 5, 28 | precision: 1, 29 | ...column.config, 30 | }; 31 | 32 | return ( 33 | ( 37 | 38 | onChange(newValue)} 44 | emptyIcon={} 45 | max={max} 46 | precision={precision} 47 | classes={ratingClasses} 48 | /> 49 | 50 | )} 51 | /> 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /www/src/components/fields/Rating/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | 3 | import MuiRating from "@material-ui/lab/Rating"; 4 | import StarBorderIcon from "@material-ui/icons/StarBorder"; 5 | 6 | import { useRatingStyles } from "./styles"; 7 | 8 | export default function Rating({ 9 | row, 10 | column, 11 | value, 12 | onSubmit, 13 | disabled, 14 | }: IHeavyCellProps) { 15 | const ratingClasses = useRatingStyles(); 16 | 17 | // Set max and precision from config 18 | const { 19 | max, 20 | precision, 21 | }: { 22 | max: number; 23 | precision: number; 24 | } = { 25 | max: 5, 26 | precision: 1, 27 | ...column.config, 28 | }; 29 | 30 | return ( 31 | e.stopPropagation()} 35 | disabled={disabled} 36 | onChange={(_, newValue) => onSubmit(newValue)} 37 | emptyIcon={} 38 | max={max} 39 | precision={precision} 40 | classes={ratingClasses} 41 | /> 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /www/src/components/fields/Rating/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import RatingIcon from "@material-ui/icons/StarBorder"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Rating" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Rating" */) 15 | ); 16 | const Settings = lazy( 17 | () => import("./Settings" /* webpackChunkName: "Settings-Rating" */) 18 | ); 19 | 20 | export const config: IFieldConfig = { 21 | type: FieldType.rating, 22 | name: "Rating", 23 | dataType: "number", 24 | initialValue: 0, 25 | initializable: true, 26 | icon: , 27 | description: 28 | "Rating displayed as stars from 0 to configurable number of stars. Default: 5 stars.", 29 | TableCell: withHeavyCell(BasicCell, TableCell), 30 | TableEditor: NullEditor, 31 | settings: Settings, 32 | SideDrawerField, 33 | }; 34 | export default config; 35 | -------------------------------------------------------------------------------- /www/src/components/fields/Rating/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles } from "@material-ui/core"; 2 | 3 | export const useRatingStyles = makeStyles((theme) => 4 | createStyles({ 5 | root: { color: theme.palette.text.secondary }, 6 | iconEmpty: { color: theme.palette.text.secondary }, 7 | }) 8 | ); 9 | -------------------------------------------------------------------------------- /www/src/components/fields/RichText/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | import _RichText from "components/RichTextEditor"; 4 | 5 | export default function RichTextEditor({ 6 | control, 7 | column, 8 | disabled, 9 | }: ISideDrawerFieldProps) { 10 | return ( 11 | ( 15 | <_RichText disabled={disabled} value={value} onChange={onChange} /> 16 | )} 17 | /> 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /www/src/components/fields/RichText/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import RichTextIcon from "@material-ui/icons/TextFormat"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-RichText" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import( 15 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-RichText" */ 16 | ) 17 | ); 18 | 19 | export const config: IFieldConfig = { 20 | type: FieldType.richText, 21 | name: "Rich Text", 22 | dataType: "string", 23 | initialValue: "", 24 | initializable: true, 25 | icon: , 26 | description: "Rich text editor with predefined HTML text styles.", 27 | TableCell: withHeavyCell(BasicCell, TableCell), 28 | TableEditor: withSideDrawerEditor(TableCell), 29 | SideDrawerField, 30 | }; 31 | export default config; 32 | -------------------------------------------------------------------------------- /www/src/components/fields/ShortText/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { TextField } from "@material-ui/core"; 2 | import Subheading from "components/Table/ColumnMenu/Subheading"; 3 | 4 | import _sortBy from "lodash/sortBy"; 5 | 6 | export default function Settings({ handleChange, config }) { 7 | return ( 8 | <> 9 | Short Text Config 10 | { 16 | if (e.target.value === "0") handleChange("maxLength")(null); 17 | else handleChange("maxLength")(e.target.value); 18 | }} 19 | /> 20 | Validation Regex 21 | { 27 | if (e.target.value === "") handleChange("validationRegex")(null); 28 | else handleChange("validationRegex")(e.target.value); 29 | }} 30 | /> 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /www/src/components/fields/ShortText/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { TextField } from "@material-ui/core"; 5 | 6 | export default function ShortText({ 7 | control, 8 | column, 9 | disabled, 10 | }: ISideDrawerFieldProps) { 11 | return ( 12 | { 16 | return ( 17 | 31 | ); 32 | }} 33 | /> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /www/src/components/fields/ShortText/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import ShortTextIcon from "@material-ui/icons/ShortText"; 6 | import BasicCell from "../_BasicCell/BasicCellValue"; 7 | import TextEditor from "components/Table/editors/TextEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import( 12 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-ShortText" */ 13 | ) 14 | ); 15 | const Settings = lazy( 16 | () => import("./Settings" /* webpackChunkName: "Settings-ShortText" */) 17 | ); 18 | 19 | export const config: IFieldConfig = { 20 | type: FieldType.shortText, 21 | name: "Short Text", 22 | dataType: "string", 23 | initialValue: "", 24 | initializable: true, 25 | icon: , 26 | description: "Small amount of text, such as names and taglines.", 27 | TableCell: withBasicCell(BasicCell), 28 | TableEditor: TextEditor, 29 | SideDrawerField, 30 | settings: Settings, 31 | }; 32 | export default config; 33 | -------------------------------------------------------------------------------- /www/src/components/fields/SingleSelect/InlineCell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import { IPopoverInlineCellProps } from "../types"; 4 | 5 | import { makeStyles, createStyles, ButtonBase } from "@material-ui/core"; 6 | import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; 7 | 8 | import { sanitiseValue } from "./utils"; 9 | 10 | const useStyles = makeStyles((theme) => 11 | createStyles({ 12 | root: { 13 | height: "100%", 14 | padding: theme.spacing(0, 1, 0, 1.5), 15 | 16 | font: "inherit", 17 | color: "inherit !important", 18 | letterSpacing: "inherit", 19 | textAlign: "inherit", 20 | justifyContent: "flex-start", 21 | }, 22 | 23 | value: { 24 | flex: 1, 25 | maxWidth: `calc(100% - 24px)`, 26 | overflow: "hidden", 27 | textOverflow: "ellipsis", 28 | }, 29 | 30 | icon: { 31 | display: "block", 32 | color: theme.palette.action.active, 33 | }, 34 | disabled: { 35 | color: theme.palette.action.disabled, 36 | }, 37 | }) 38 | ); 39 | 40 | export const SingleSelect = React.forwardRef(function SingleSelect( 41 | { value, showPopoverCell, disabled }: IPopoverInlineCellProps, 42 | ref: React.Ref 43 | ) { 44 | const classes = useStyles(); 45 | 46 | return ( 47 | showPopoverCell(true)} 50 | ref={ref} 51 | disabled={disabled} 52 | > 53 |
{sanitiseValue(value)}
54 | 55 | 58 |
59 | ); 60 | }); 61 | export default SingleSelect; 62 | -------------------------------------------------------------------------------- /www/src/components/fields/SingleSelect/PopoverCell.tsx: -------------------------------------------------------------------------------- 1 | import { IPopoverCellProps } from "../types"; 2 | import _get from "lodash/get"; 3 | 4 | import MultiSelect_ from "@antlerengineering/multiselect"; 5 | 6 | import { sanitiseValue } from "./utils"; 7 | 8 | export default function SingleSelect({ 9 | value, 10 | onSubmit, 11 | column, 12 | parentRef, 13 | showPopoverCell, 14 | disabled, 15 | }: IPopoverCellProps) { 16 | const config = column.config ?? {}; 17 | 18 | return ( 19 | showPopoverCell(false)} 40 | /> 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /www/src/components/fields/SingleSelect/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { useTheme } from "@material-ui/core"; 5 | import MultiSelect_ from "@antlerengineering/multiselect"; 6 | import FormattedChip from "components/FormattedChip"; 7 | 8 | import { sanitiseValue } from "./utils"; 9 | 10 | export default function SingleSelect({ 11 | column, 12 | control, 13 | disabled, 14 | }: ISideDrawerFieldProps) { 15 | const theme = useTheme(); 16 | 17 | const config = column.config ?? {}; 18 | 19 | return ( 20 | ( 24 | <> 25 | 38 | 39 | {value?.length > 0 && ( 40 |
41 | 42 |
43 | )} 44 | 45 | )} 46 | /> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /www/src/components/fields/SingleSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withPopoverCell from "../_withTableCell/withPopoverCell"; 4 | 5 | import SingleSelectIcon from "@material-ui/icons/FormatListBulleted"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import InlineCell from "./InlineCell"; 8 | import NullEditor from "components/Table/editors/NullEditor"; 9 | 10 | const PopoverCell = lazy( 11 | () => 12 | import("./PopoverCell" /* webpackChunkName: "PopoverCell-SingleSelect" */) 13 | ); 14 | const SideDrawerField = lazy( 15 | () => 16 | import( 17 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-SingleSelect" */ 18 | ) 19 | ); 20 | const Settings = lazy( 21 | () => import("./Settings" /* webpackChunkName: "Settings-SingleSelect" */) 22 | ); 23 | 24 | export const config: IFieldConfig = { 25 | type: FieldType.singleSelect, 26 | name: "Single Select", 27 | dataType: "string | null", 28 | initialValue: null, 29 | initializable: true, 30 | icon: , 31 | description: 32 | "Dropdown selector with searchable options and radio button behavior. Optionally allows users to input custom values. Max selection: 1 option.", 33 | TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { 34 | anchorOrigin: { horizontal: "left", vertical: "bottom" }, 35 | transparent: true, 36 | }), 37 | TableEditor: NullEditor, 38 | SideDrawerField, 39 | settings: Settings, 40 | requireConfiguration: true, 41 | }; 42 | export default config; 43 | -------------------------------------------------------------------------------- /www/src/components/fields/SingleSelect/utils.ts: -------------------------------------------------------------------------------- 1 | export const sanitiseValue = (value: any) => { 2 | if (value === undefined || value === null || value === "") return null; 3 | else if (Array.isArray(value)) return value[0] as string; 4 | else return value as string; 5 | }; 6 | -------------------------------------------------------------------------------- /www/src/components/fields/Slider/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { TextField, FormControlLabel, Switch } from "@material-ui/core"; 2 | import Subheading from "components/Table/ColumnMenu/Subheading"; 3 | 4 | import _sortBy from "lodash/sortBy"; 5 | 6 | export default function Settings({ handleChange, config }) { 7 | return ( 8 | <> 9 | Slider Config 10 | 11 | handleChange("min")(parseFloat(e.target.value))} 16 | value={config["min"]} 17 | id={`settings-field-min`} 18 | label="Minimum Value" 19 | type="number" 20 | /> 21 | 22 | handleChange("max")(parseFloat(e.target.value))} 27 | value={config["max"]} 28 | id={`settings-field-max`} 29 | label="Maximum Value" 30 | type="number" 31 | /> 32 | 33 | handleChange("step")(parseFloat(e.target.value))} 38 | value={config["step"]} 39 | id={`settings-field-step`} 40 | label="Step Value" 41 | type="number" 42 | /> 43 | 44 | handleChange("marks")(!Boolean(config.marks))} 49 | name="marks" 50 | /> 51 | } 52 | label="Show slider steps" 53 | /> 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /www/src/components/fields/Slider/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | 3 | import { makeStyles, createStyles, Grid } from "@material-ui/core"; 4 | 5 | import { resultColorsScale } from "utils/color"; 6 | 7 | const useStyles = makeStyles((theme) => 8 | createStyles({ 9 | progress: { 10 | width: "100%", 11 | backgroundColor: theme.palette.divider, 12 | borderRadius: theme.shape.borderRadius, 13 | }, 14 | bar: { 15 | borderRadius: theme.shape.borderRadius, 16 | height: 16, 17 | maxWidth: "100%", 18 | }, 19 | }) 20 | ); 21 | 22 | export default function Slider({ column, value }: IHeavyCellProps) { 23 | const classes = useStyles(); 24 | 25 | const { 26 | max, 27 | min, 28 | unit, 29 | }: { 30 | max: number; 31 | min: number; 32 | unit?: string; 33 | } = { 34 | max: 10, 35 | min: 0, 36 | ...(column as any).config, 37 | }; 38 | 39 | const progress = 40 | value < min || typeof value !== "number" 41 | ? 0 42 | : ((value - min) / (max - min)) * 100; 43 | 44 | return ( 45 | 46 | 47 | {value ?? 0}/{max} {unit} 48 | 49 | 50 |
51 |
58 |
59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /www/src/components/fields/Slider/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import SliderIcon from "assets/icons/Slider"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Slider" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Slider" */) 15 | ); 16 | const Settings = lazy( 17 | () => import("./Settings" /* webpackChunkName: "Settings-Slider" */) 18 | ); 19 | 20 | export const config: IFieldConfig = { 21 | type: FieldType.slider, 22 | name: "Slider", 23 | dataType: "number", 24 | initialValue: 0, 25 | initializable: true, 26 | icon: , 27 | description: "Slider with adjustable range. Returns a numeric value.", 28 | TableCell: withHeavyCell(BasicCell, TableCell), 29 | TableEditor: withSideDrawerEditor(TableCell), 30 | settings: Settings, 31 | SideDrawerField, 32 | }; 33 | export default config; 34 | -------------------------------------------------------------------------------- /www/src/components/fields/Status/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { Grid } from "@material-ui/core"; 5 | import { Rating as MuiRating } from "@material-ui/lab"; 6 | import StarBorderIcon from "@material-ui/icons/StarBorder"; 7 | 8 | import { useFieldStyles } from "components/SideDrawer/Form/utils"; 9 | import { useStatusStyles } from "./styles"; 10 | 11 | export default function Rating({ 12 | control, 13 | column, 14 | disabled, 15 | }: ISideDrawerFieldProps) { 16 | const fieldClasses = useFieldStyles(); 17 | const ratingClasses = useStatusStyles(); 18 | 19 | return ( 20 | ( 24 | 25 | <>{value} 26 | 27 | )} 28 | /> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /www/src/components/fields/Status/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { IHeavyCellProps } from "../types"; 3 | 4 | import { useStatusStyles } from "./styles"; 5 | import _find from "lodash/find"; 6 | 7 | export default function Status({ column, value }: IHeavyCellProps) { 8 | const statusClasses = useStatusStyles(); 9 | 10 | const conditions = column.config?.conditions ?? []; 11 | const label = useMemo(() => { 12 | if (["null", "undefined"].includes(typeof value)) { 13 | const condition = _find(conditions, (c) => c.type === typeof value); 14 | return condition.label; 15 | } else if (typeof value === "number") { 16 | const numberConditions = conditions.filter((c) => c.type === "number"); 17 | for (let i = 0; i < numberConditions.length; i++) { 18 | const condition = numberConditions[i]; 19 | switch (condition.operator) { 20 | case "<": 21 | if (value < condition.value) return condition.label; 22 | break; 23 | case "<=": 24 | if (value <= condition.value) return condition.label; 25 | break; 26 | case ">=": 27 | if (value >= condition.value) return condition.label; 28 | break; 29 | case ">": 30 | if (value > condition.value) return condition.label; 31 | break; 32 | case "==": 33 | default: 34 | if (value == condition.value) return condition.label; 35 | break; 36 | } 37 | } 38 | } else { 39 | for (let i = 0; i < conditions.length; i++) { 40 | const condition = conditions[i]; 41 | if (value == condition.value) return condition.label; 42 | } 43 | } 44 | return JSON.stringify(value); 45 | }, [value, conditions]); 46 | 47 | return <>{label}; 48 | } 49 | -------------------------------------------------------------------------------- /www/src/components/fields/Status/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import StatusIcon from "assets/icons/Status"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-Status" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Status" */) 15 | ); 16 | const Settings = lazy( 17 | () => import("./Settings" /* webpackChunkName: "Settings-Status" */) 18 | ); 19 | 20 | export const config: IFieldConfig = { 21 | type: FieldType.status, 22 | name: "Status", 23 | dataType: "any", 24 | initialValue: undefined, 25 | initializable: true, 26 | icon: , 27 | description: 28 | "Status is read only field that displays field values in more visual format", 29 | TableCell: withHeavyCell(BasicCell, TableCell), 30 | TableEditor: NullEditor, 31 | settings: Settings, 32 | SideDrawerField, 33 | requireConfiguration: true, 34 | }; 35 | export default config; 36 | -------------------------------------------------------------------------------- /www/src/components/fields/Status/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, createStyles } from "@material-ui/core"; 2 | 3 | export const useStatusStyles = makeStyles((theme) => 4 | createStyles({ 5 | root: { color: theme.palette.text.secondary }, 6 | iconEmpty: { color: theme.palette.text.secondary }, 7 | }) 8 | ); 9 | -------------------------------------------------------------------------------- /www/src/components/fields/SubTable/Settings.tsx: -------------------------------------------------------------------------------- 1 | import MultiSelect from "@antlerengineering/multiselect"; 2 | import { FieldType } from "constants/fields"; 3 | import { useFiretableContext } from "contexts/FiretableContext"; 4 | 5 | const Settings = ({ config, handleChange }) => { 6 | const { tableState } = useFiretableContext(); 7 | if (!tableState?.columns) return <>; 8 | const columnOptions = Object.values(tableState.columns) 9 | .filter((column) => 10 | [ 11 | FieldType.shortText, 12 | FieldType.singleSelect, 13 | FieldType.email, 14 | FieldType.phone, 15 | ].includes(column.type) 16 | ) 17 | .map((c) => ({ label: c.name, value: c.key })); 18 | return ( 19 | <> 20 | 26 | 27 | ); 28 | }; 29 | export default Settings; 30 | -------------------------------------------------------------------------------- /www/src/components/fields/SubTable/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { useWatch } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import { Grid, IconButton } from "@material-ui/core"; 6 | import LaunchIcon from "@material-ui/icons/Launch"; 7 | 8 | import { useFieldStyles } from "components/SideDrawer/Form/utils"; 9 | import { useSubTableData } from "./utils"; 10 | 11 | export default function SubTable({ 12 | column, 13 | control, 14 | docRef, 15 | }: ISideDrawerFieldProps) { 16 | const fieldClasses = useFieldStyles(); 17 | 18 | const row = useWatch({ control }); 19 | const { documentCount, label, subTablePath } = useSubTableData( 20 | column, 21 | row, 22 | docRef 23 | ); 24 | 25 | return ( 26 | 27 |
28 | {documentCount} {column.name}: {label} 29 |
30 | 31 | 37 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /www/src/components/fields/SubTable/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | import clsx from "clsx"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import { createStyles, makeStyles, Grid, IconButton } from "@material-ui/core"; 6 | import LaunchIcon from "@material-ui/icons/Launch"; 7 | 8 | import { useSubTableData } from "./utils"; 9 | 10 | const useStyles = makeStyles((theme) => 11 | createStyles({ 12 | root: { padding: theme.spacing(0, 0.625, 0, 1) }, 13 | labelContainer: { overflowX: "hidden" }, 14 | }) 15 | ); 16 | 17 | export default function SubTable({ column, row }: IHeavyCellProps) { 18 | const classes = useStyles(); 19 | const { documentCount, label, subTablePath } = useSubTableData( 20 | column, 21 | row, 22 | row.ref 23 | ); 24 | 25 | if (!row.ref) return null; 26 | 27 | return ( 28 | 35 | 36 | {documentCount} {column.name}: {label} 37 | 38 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /www/src/components/fields/SubTable/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import SubTableIcon from "assets/icons/SubTable"; 6 | import BasicCell from "../_BasicCell/BasicCellName"; 7 | import NullEditor from "components/Table/editors/NullEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-SubTable" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import( 15 | "./SideDrawerField" /* webpackChunkName: "SideDrawerField-SubTable" */ 16 | ) 17 | ); 18 | const Settings = lazy( 19 | () => import("./Settings" /* webpackChunkName: "Settings-Subtable" */) 20 | ); 21 | export const config: IFieldConfig = { 22 | type: FieldType.subTable, 23 | name: "SubTable", 24 | dataType: "undefined", 25 | initialValue: null, 26 | icon: , 27 | settings: Settings, 28 | description: 29 | "Creates a sub-table. Also displays number of rows inside the sub-table. Max sub-table levels: 100.", 30 | TableCell: withHeavyCell(BasicCell, TableCell), 31 | TableEditor: NullEditor, 32 | SideDrawerField, 33 | initializable: false, 34 | requireConfiguration: true, 35 | }; 36 | export default config; 37 | -------------------------------------------------------------------------------- /www/src/components/fields/SubTable/utils.ts: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | import useRouter from "hooks/useRouter"; 3 | 4 | export const useSubTableData = ( 5 | column: any, 6 | row: any, 7 | docRef: firebase.default.firestore.DocumentReference 8 | ) => { 9 | const { parentLabel, config } = column as any; 10 | const label: string = parentLabel 11 | ? row[parentLabel] 12 | : config.parentLabel 13 | ? config.parentLabel.reduce((acc, curr) => { 14 | if (acc !== "") return `${acc} - ${row[curr]}`; 15 | else return row[curr]; 16 | }, "") 17 | : ""; 18 | const fieldName = column.key as string; 19 | const documentCount: string = row[fieldName]?.count ?? ""; 20 | 21 | const router = useRouter(); 22 | const parentLabels = queryString.parse(router.location.search).parentLabel; 23 | 24 | let subTablePath = ""; 25 | if (parentLabels) 26 | subTablePath = 27 | encodeURIComponent(`${docRef.path}/${fieldName}`) + 28 | `?parentLabel=${parentLabels},${label}`; 29 | else 30 | subTablePath = 31 | encodeURIComponent(`${docRef.path}/${fieldName}`) + 32 | `?parentLabel=${encodeURIComponent(label)}`; 33 | 34 | return { documentCount, label, subTablePath }; 35 | }; 36 | -------------------------------------------------------------------------------- /www/src/components/fields/Url/BasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | import { Link } from "@material-ui/core"; 4 | 5 | export default function Url({ value }: IBasicCellProps) { 6 | if (!value) return null; 7 | 8 | const href = value.includes("http") ? value : `https://${value}`; 9 | 10 | return ( 11 | 18 | {value} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /www/src/components/fields/Url/SideDrawerField.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "react-hook-form"; 2 | import { ISideDrawerFieldProps } from "../types"; 3 | 4 | import { Grid, TextField, IconButton } from "@material-ui/core"; 5 | import LaunchIcon from "@material-ui/icons/Launch"; 6 | 7 | export default function Url({ 8 | control, 9 | column, 10 | disabled, 11 | }: ISideDrawerFieldProps) { 12 | return ( 13 | { 17 | return ( 18 | 19 | 33 | 41 | 42 | 43 | 44 | ); 45 | }} 46 | /> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /www/src/components/fields/Url/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withBasicCell from "../_withTableCell/withBasicCell"; 4 | 5 | import UrlIcon from "@material-ui/icons/Link"; 6 | import BasicCell from "../_BasicCell/BasicCellValue"; 7 | import TextEditor from "components/Table/editors/TextEditor"; 8 | 9 | const SideDrawerField = lazy( 10 | () => 11 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Url" */) 12 | ); 13 | 14 | export const config: IFieldConfig = { 15 | type: FieldType.url, 16 | name: "URL", 17 | dataType: "string", 18 | initialValue: "", 19 | initializable: true, 20 | icon: , 21 | description: "Web address. Firetable does not validate URLs.", 22 | TableCell: withBasicCell(BasicCell), 23 | TableEditor: TextEditor, 24 | SideDrawerField, 25 | }; 26 | export default config; 27 | -------------------------------------------------------------------------------- /www/src/components/fields/User/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { IHeavyCellProps } from "../types"; 2 | 3 | import { Tooltip, Chip, Avatar } from "@material-ui/core"; 4 | 5 | import { format } from "date-fns"; 6 | import { DATE_TIME_FORMAT } from "constants/dates"; 7 | 8 | export default function User({ value }: IHeavyCellProps) { 9 | if (!value || !value.displayName || !value.timestamp) return null; 10 | const dateLabel = format( 11 | value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, 12 | DATE_TIME_FORMAT 13 | ); 14 | 15 | return ( 16 | 17 | } 19 | label={value.displayName} 20 | /> 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /www/src/components/fields/User/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { IFieldConfig, FieldType } from "components/fields/types"; 3 | import withHeavyCell from "../_withTableCell/withHeavyCell"; 4 | 5 | import UserIcon from "@material-ui/icons/Person"; 6 | import BasicCell from "../_BasicCell/BasicCellNull"; 7 | import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor"; 8 | 9 | const TableCell = lazy( 10 | () => import("./TableCell" /* webpackChunkName: "TableCell-User" */) 11 | ); 12 | const SideDrawerField = lazy( 13 | () => 14 | import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-User" */) 15 | ); 16 | 17 | export const config: IFieldConfig = { 18 | type: FieldType.user, 19 | name: "User", 20 | dataType: 21 | "{ displayName: string, email: string, emailVerified: boolean, isAnonymous: boolean, photoURL: string, timestamp: firebase.firestore.Timestamp, uid: string }", 22 | initialValue: null, 23 | icon: , 24 | description: "Displays the _ft_updatedBy field for editing history.", 25 | TableCell: withHeavyCell(BasicCell, TableCell), 26 | TableEditor: withSideDrawerEditor(TableCell), 27 | SideDrawerField, 28 | }; 29 | export default config; 30 | -------------------------------------------------------------------------------- /www/src/components/fields/_BasicCell/BasicCellName.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | export default function BasicCellName({ name }: IBasicCellProps) { 4 | return <>{name}; 5 | } 6 | -------------------------------------------------------------------------------- /www/src/components/fields/_BasicCell/BasicCellNull.tsx: -------------------------------------------------------------------------------- 1 | export default function BasicCellNull() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /www/src/components/fields/_BasicCell/BasicCellValue.tsx: -------------------------------------------------------------------------------- 1 | import { IBasicCellProps } from "../types"; 2 | 3 | export default function BasicCellName({ value }: IBasicCellProps) { 4 | return <>{value}; 5 | } 6 | -------------------------------------------------------------------------------- /www/src/components/fields/_withTableCell/withBasicCell.tsx: -------------------------------------------------------------------------------- 1 | import { FormatterProps } from "react-data-grid"; 2 | import { IBasicCellProps } from "../types"; 3 | 4 | import ErrorBoundary from "components/ErrorBoundary"; 5 | import CellValidation from "components/Table/CellValidation"; 6 | import { FieldType } from "constants/fields"; 7 | import { getCellValue } from "utils/fns"; 8 | 9 | /** 10 | * HOC to wrap around table cell components. 11 | * Renders read-only BasicCell only. 12 | * @param BasicCellComponent The light cell component to display at all times 13 | */ 14 | export default function withBasicCell( 15 | BasicCellComponent: React.ComponentType 16 | ) { 17 | return function BasicCell(props: FormatterProps) { 18 | const { name, key } = props.column; 19 | const value = getCellValue(props.row, key); 20 | 21 | const { validationRegex, required } = (props.column as any).config; 22 | 23 | return ( 24 | 25 | 30 | 35 | 36 | 37 | ); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /www/src/constants/dates.tsx: -------------------------------------------------------------------------------- 1 | export const DATE_FORMAT = "yyyy/MM/dd"; 2 | export const DATE_TIME_FORMAT = DATE_FORMAT + " hh:mm a"; 3 | -------------------------------------------------------------------------------- /www/src/constants/fields.ts: -------------------------------------------------------------------------------- 1 | // Define field type strings used in Firetable column config 2 | export enum FieldType { 3 | // TEXT 4 | shortText = "SIMPLE_TEXT", 5 | longText = "LONG_TEXT", 6 | email = "EMAIL", 7 | phone = "PHONE_NUMBER", 8 | url = "URL", 9 | // NUMERIC 10 | checkbox = "CHECK_BOX", 11 | number = "NUMBER", 12 | percentage = "PERCENTAGE", 13 | rating = "RATING", 14 | slider = "SLIDER", 15 | color = "COLOR", 16 | // DATE & TIME 17 | date = "DATE", 18 | dateTime = "DATE_TIME", 19 | duration = "DURATION", 20 | // FILE 21 | image = "IMAGE", 22 | file = "FILE", 23 | // SELECT 24 | singleSelect = "SINGLE_SELECT", 25 | multiSelect = "MULTI_SELECT", 26 | // CONNECTION 27 | subTable = "SUB_TABLE", 28 | connectTable = "DOCUMENT_SELECT", 29 | connectService = "SERVICE_SELECT", 30 | // CODE 31 | json = "JSON", 32 | code = "CODE", 33 | richText = "RICH_TEXT", 34 | // CLOUD FUNCTION 35 | action = "ACTION", 36 | derivative = "DERIVATIVE", 37 | aggregate = "AGGREGATE", 38 | // FIRETABLE 39 | user = "USER", 40 | id = "ID", 41 | last = "LAST", 42 | status = "STATUS", 43 | } 44 | -------------------------------------------------------------------------------- /www/src/constants/routes.ts: -------------------------------------------------------------------------------- 1 | export enum routes { 2 | home = "/", 3 | projectSettings = "/?modal=settings", 4 | auth = "/auth", 5 | impersonatorAuth = "/impersonatorAuth", 6 | jwtAuth = "/jwtAuth", 7 | signOut = "/signOut", 8 | authSetup = "/authSetup", 9 | 10 | table = "/table", 11 | tableGroup = "/tableGroup", 12 | 13 | tableWithId = "/table/:id", 14 | tableGroupWithId = "/tableGroup/:id", 15 | grid = "/grid", 16 | gridWithId = "/grid/:id", 17 | editor = "/editor", 18 | } 19 | 20 | export default routes; 21 | -------------------------------------------------------------------------------- /www/src/constants/wikiLinks.ts: -------------------------------------------------------------------------------- 1 | import _mapValues from "lodash/mapValues"; 2 | import meta from "../../package.json"; 3 | 4 | const WIKI_PATHS = { 5 | updatingFiretable: "/Updating-Firetable", 6 | derivatives: "/Derivative-Fields", 7 | defaultValues: "/Default-Values", 8 | FtFunctions: "/Firetable-Cloud-Functions", 9 | securityRules: "/Role-Based-Security-Rules", 10 | setUpAuth: "/Setting-Up-Firebase-Authentication", 11 | }; 12 | 13 | const WIKI_LINK_ROOT = meta.repository.url.replace(".git", "/wiki"); 14 | 15 | export const WIKI_LINKS = _mapValues( 16 | WIKI_PATHS, 17 | (path) => WIKI_LINK_ROOT + path 18 | ); 19 | export default WIKI_LINKS; 20 | -------------------------------------------------------------------------------- /www/src/contexts/EditorContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FieldType } from "constants/fields"; 3 | 4 | export interface EditorContextInterface { 5 | // row: any; 6 | // onSubmit: any; 7 | // value: any; 8 | // anchorEl: any; 9 | editorValue: any; 10 | open: any; 11 | close: any; 12 | cancel: any; 13 | setEditorValue: any; 14 | fieldType: FieldType | null; 15 | } 16 | 17 | const EditorContext = React.createContext({ 18 | // row: undefined, 19 | // onSubmit: undefined, 20 | // value: undefined, 21 | // anchorEl: undefined, 22 | open: undefined, 23 | close: undefined, 24 | cancel: undefined, 25 | editorValue: undefined, 26 | setEditorValue: undefined, 27 | fieldType: null, 28 | }); 29 | 30 | export default EditorContext; 31 | -------------------------------------------------------------------------------- /www/src/contexts/SnackContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { SnackbarOrigin } from "@material-ui/core/Snackbar"; 3 | 4 | import Snack from "components/Snack"; 5 | 6 | type Progress = { value: number; target: number }; 7 | type Variant = 8 | | "progress" 9 | | "error" 10 | | "success" 11 | | "info" 12 | | "warning" 13 | | undefined; 14 | 15 | const DEFAULT_STATE = { 16 | isOpen: false, 17 | duration: 10000, 18 | message: "", 19 | progress: { value: 0, target: 0 } as Progress, 20 | action: null as React.ReactNode, 21 | variant: undefined as Variant, 22 | position: { vertical: "bottom", horizontal: "left" } as SnackbarOrigin, 23 | }; 24 | const DEFAULT_FUNCTIONS = { 25 | close: () => {}, 26 | setProgress: (progress: Progress) => {}, 27 | open: (newState: Partial) => {}, 28 | }; 29 | 30 | export const SnackContext = React.createContext({ 31 | ...DEFAULT_STATE, 32 | ...DEFAULT_FUNCTIONS, 33 | }); 34 | export const useSnackContext = () => useContext(SnackContext); 35 | 36 | export function SnackProvider({ children }: React.PropsWithChildren<{}>) { 37 | const [state, setState] = useState(DEFAULT_STATE); 38 | 39 | const close = () => setState(DEFAULT_STATE); 40 | 41 | const open: typeof DEFAULT_FUNCTIONS.open = (newState) => 42 | setState({ ...DEFAULT_STATE, isOpen: true, ...newState }); 43 | 44 | const setProgress: typeof DEFAULT_FUNCTIONS.setProgress = (progress) => 45 | setState((state) => ({ ...state, progress })); 46 | 47 | return ( 48 | 56 | {children} 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /www/src/contexts/SnackLogContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | 3 | const DEFAULT_STATE = { 4 | isSnackLogOpen: false, 5 | latestBuildTimestamp: 0, 6 | }; 7 | const DEFAULT_FUNCTIONS = { 8 | requestSnackLog: () => {}, 9 | closeSnackLog: () => {}, 10 | }; 11 | 12 | export const SnackLogContext = React.createContext({ 13 | ...DEFAULT_STATE, 14 | ...DEFAULT_FUNCTIONS, 15 | }); 16 | export const useSnackLogContext = () => useContext(SnackLogContext); 17 | 18 | export function SnackLogProvider({ children }: React.PropsWithChildren<{}>) { 19 | const [state, setState] = useState(DEFAULT_STATE); 20 | 21 | const requestSnackLog: typeof DEFAULT_FUNCTIONS.requestSnackLog = () => { 22 | setTimeout(() => { 23 | setState({ 24 | ...state, 25 | latestBuildTimestamp: Date.now(), 26 | isSnackLogOpen: true, 27 | }); 28 | }, 500); 29 | }; 30 | 31 | const closeSnackLog: typeof DEFAULT_FUNCTIONS.closeSnackLog = () => 32 | setState({ 33 | ...state, 34 | latestBuildTimestamp: 0, 35 | isSnackLogOpen: false, 36 | }); 37 | 38 | return ( 39 | 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /www/src/firebase/callables.ts: -------------------------------------------------------------------------------- 1 | import { functions } from "./index"; 2 | 3 | export enum CLOUD_FUNCTIONS { 4 | ImpersonatorAuth = "callable-ImpersonatorAuth", 5 | getAlgoliaSearchKey = "getAlgoliaSearchKey", 6 | } 7 | 8 | export const cloudFunction = ( 9 | name: string, 10 | input: any, 11 | success?: Function, 12 | fail?: Function 13 | ) => 14 | new Promise((resolve, reject) => { 15 | const callable = functions.httpsCallable(name); 16 | callable(input) 17 | .then((result) => { 18 | if (success) { 19 | resolve(success(result)); 20 | } 21 | }) 22 | .catch((error) => { 23 | if (fail) { 24 | reject(fail(error)); 25 | } 26 | }); 27 | }); 28 | 29 | export const ImpersonatorAuth = (email: string) => 30 | functions.httpsCallable(CLOUD_FUNCTIONS.ImpersonatorAuth)({ email }); 31 | 32 | export const getAlgoliaSearchKey = (index: string) => 33 | functions.httpsCallable(CLOUD_FUNCTIONS.getAlgoliaSearchKey)({ index }); 34 | -------------------------------------------------------------------------------- /www/src/firebase/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY, 3 | authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`, 4 | databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`, 5 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 6 | storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`, 7 | appId: "x", 8 | }; 9 | -------------------------------------------------------------------------------- /www/src/firebase/firebaseui.ts: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import * as firebaseui from "firebaseui"; 3 | 4 | export const authOptions = { 5 | google: { 6 | provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID, 7 | buttonColor: "#4285F4", 8 | }, 9 | twitter: firebase.auth.TwitterAuthProvider.PROVIDER_ID, 10 | facebook: firebase.auth.FacebookAuthProvider.PROVIDER_ID, 11 | github: { 12 | provider: firebase.auth.GithubAuthProvider.PROVIDER_ID, 13 | buttonColor: "#000", 14 | }, 15 | microsoft: { 16 | provider: "microsoft.com", 17 | loginHintKey: "login_hint", 18 | buttonColor: "#000", 19 | }, 20 | apple: { provider: "apple.com" }, 21 | yahoo: { provider: "yahoo.com" }, 22 | email: { 23 | provider: firebase.auth.EmailAuthProvider.PROVIDER_ID, 24 | requireDisplayName: true, 25 | disableSignUp: { status: true }, 26 | }, 27 | phone: firebase.auth.PhoneAuthProvider.PROVIDER_ID, 28 | anonymous: firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID, 29 | }; 30 | 31 | export const defaultUiConfig: firebaseui.auth.Config = { 32 | signInFlow: "popup", 33 | signInSuccessUrl: "/", 34 | signInOptions: [authOptions.google], 35 | }; 36 | 37 | export const getSignInOptions = ( 38 | selected: Array 39 | ): firebaseui.auth.Config["signInOptions"] => 40 | selected.map((option) => authOptions[option]); 41 | -------------------------------------------------------------------------------- /www/src/firebase/index.ts: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/auth"; 3 | import "firebase/firestore"; 4 | import "firebase/functions"; 5 | import "firebase/storage"; 6 | 7 | import appConfig from "./config"; 8 | 9 | firebase.initializeApp(appConfig); 10 | 11 | export const auth = firebase.auth(); 12 | 13 | export const db = firebase.firestore(); 14 | db.settings({ 15 | cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED, 16 | ignoreUndefinedProperties: true, 17 | }); 18 | db.enablePersistence({ synchronizeTabs: true }); 19 | 20 | export const bucket = firebase.storage(); 21 | export const functions = firebase.functions(); 22 | 23 | export const projectId = process.env.REACT_APP_FIREBASE_PROJECT_ID; 24 | export const WEBHOOK_URL = `https://${(functions as any).region_}-${ 25 | appConfig.projectId 26 | }.cloudfunctions.net/webhook`; 27 | export const googleProvider = new firebase.auth.GoogleAuthProvider().setCustomParameters( 28 | { 29 | prompt: "select_account", 30 | } 31 | ); 32 | 33 | export const deleteField = firebase.firestore.FieldValue.delete; 34 | -------------------------------------------------------------------------------- /www/src/hooks/useHotkeys.ts: -------------------------------------------------------------------------------- 1 | import hotkeys, { HotkeysEvent } from "hotkeys-js"; 2 | import { useCallback, useEffect } from "react"; 3 | 4 | type CallbackFn = (event: KeyboardEvent, handler: HotkeysEvent) => void; 5 | /** 6 | * used for listening for keyboard shortcuts 7 | * @param keys 8 | * @param callback 9 | * @param deps 10 | */ 11 | export default function useHotkeys( 12 | keys: string, 13 | callback: CallbackFn, 14 | deps: any[] = [] 15 | ) { 16 | const memoisedCallback = useCallback(callback, deps); 17 | 18 | useEffect(() => { 19 | hotkeys(keys, memoisedCallback); 20 | 21 | return () => hotkeys.unbind(keys, memoisedCallback); 22 | }, [memoisedCallback]); 23 | } 24 | -------------------------------------------------------------------------------- /www/src/hooks/useKeyPress.ts: -------------------------------------------------------------------------------- 1 | import react, { useState, useEffect } from "react"; 2 | // Hook 3 | export default function useKeyPress(targetKey) { 4 | // State for keeping track of whether key is pressed 5 | const [keyPressed, setKeyPressed] = useState(false); 6 | 7 | // If pressed key is our target key then set to true 8 | function downHandler({ key }) { 9 | if (key === targetKey) { 10 | setKeyPressed(true); 11 | } 12 | } 13 | 14 | // If released key is our target key then set to false 15 | const upHandler = ({ key }) => { 16 | if (key === targetKey) { 17 | setKeyPressed(false); 18 | } 19 | }; 20 | 21 | // Add event listeners 22 | useEffect(() => { 23 | window.addEventListener("keydown", downHandler); 24 | window.addEventListener("keyup", upHandler); 25 | // Remove event listeners on cleanup 26 | return () => { 27 | window.removeEventListener("keydown", downHandler); 28 | window.removeEventListener("keyup", upHandler); 29 | }; 30 | }, []); // Empty array ensures that effect is only run on mount and unmount 31 | 32 | return keyPressed; 33 | } 34 | -------------------------------------------------------------------------------- /www/src/hooks/useRouter.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { __RouterContext, RouteComponentProps } from "react-router"; 3 | // used to transform routerContext into a hook 4 | // TODO : find alternate solution as this uses an internal variable 5 | export default function useRouter() { 6 | return useContext>( 7 | __RouterContext 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /www/src/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | function useWindowSize() { 3 | const isClient = typeof window === "object"; 4 | 5 | function getSize() { 6 | if (!isClient) return undefined; 7 | return { width: window.innerWidth, height: window.innerHeight }; 8 | } 9 | 10 | const [windowSize, setWindowSize] = useState(getSize); 11 | 12 | useEffect(() => { 13 | if (!isClient) { 14 | //return false; 15 | } 16 | 17 | function handleResize() { 18 | setWindowSize(getSize()); 19 | } 20 | 21 | window.addEventListener("resize", handleResize); 22 | return () => window.removeEventListener("resize", handleResize); 23 | }, []); // Empty array ensures that effect is only run on mount and unmount 24 | 25 | return windowSize; 26 | } 27 | 28 | export default useWindowSize; 29 | -------------------------------------------------------------------------------- /www/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import App from "./App"; 3 | import * as serviceWorker from "./serviceWorker"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | 7 | // If you want your app to work offline and load faster, you can change 8 | // unregister() to register() below. Note this comes with some pitfalls. 9 | // Learn more about service workers: https://bit.ly/CRA-PWA 10 | serviceWorker.unregister(); 11 | -------------------------------------------------------------------------------- /www/src/pages/Auth/JwtAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { TextField, Typography, Button } from "@material-ui/core"; 4 | 5 | import { auth } from "../../firebase"; 6 | import { useSnackContext } from "contexts/SnackContext"; 7 | import AuthLayout from "components/Auth/AuthLayout"; 8 | 9 | export default function JwtAuthPage() { 10 | const snack = useSnackContext(); 11 | 12 | const [jwt, setJWT] = useState(""); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleAuth = async () => { 16 | setLoading(true); 17 | 18 | try { 19 | await auth.signInWithCustomToken(jwt); 20 | snack.open({ message: "Success", variant: "success" }); 21 | window.location.assign("/"); 22 | } catch (e) { 23 | snack.open({ message: e.message, variant: "error" }); 24 | } finally { 25 | setLoading(false); 26 | } 27 | }; 28 | 29 | return ( 30 | 31 | 32 | Test Authentication 33 | 34 | 35 | setJWT(e.target.value)} 41 | /> 42 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /www/src/pages/Auth/SetupGuide.tsx: -------------------------------------------------------------------------------- 1 | import { Typography, Button } from "@material-ui/core"; 2 | import OpenInNewIcon from "@material-ui/icons/OpenInNew"; 3 | 4 | import AuthLayout from "components/Auth/AuthLayout"; 5 | import WIKI_LINKS from "constants/wikiLinks"; 6 | 7 | export default function AuthSetupGuide() { 8 | return ( 9 | 10 |
11 | 12 | Firebase Authentication Not Set Up 13 | 14 | 15 | 16 | Firebase Authentication must be enabled to sign in to Firetable. 17 | 18 |
19 | 20 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /www/src/pages/Auth/SignOut.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { Button } from "@material-ui/core"; 5 | import CheckIcon from "@material-ui/icons/Check"; 6 | 7 | import AuthLayout from "components/Auth/AuthLayout"; 8 | import EmptyState from "components/EmptyState"; 9 | import { auth } from "../../firebase"; 10 | 11 | export default function SignOutPage() { 12 | useEffect(() => { 13 | auth.signOut(); 14 | }, []); 15 | 16 | return ( 17 | 18 | 28 | Sign In Again 29 | 30 | } 31 | Icon={CheckIcon} 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /www/src/pages/Auth/index.tsx: -------------------------------------------------------------------------------- 1 | import AuthLayout from "components/Auth/AuthLayout"; 2 | import FirebaseUi from "components/Auth/FirebaseUi"; 3 | 4 | export default function AuthPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /www/src/pages/Grid.tsx: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | 3 | import { Hidden } from "@material-ui/core"; 4 | 5 | import Navigation from "components/Navigation"; 6 | import Grid from "components/Grid"; 7 | import SideDrawer from "components/SideDrawer"; 8 | 9 | import { FireTableFilter } from "hooks/useFiretable"; 10 | import useRouter from "hooks/useRouter"; 11 | 12 | export default function GridPage() { 13 | const router = useRouter(); 14 | const tableCollection = decodeURIComponent(router.match.params.id); 15 | 16 | let filters: FireTableFilter[] = []; 17 | const parsed = queryString.parse(router.location.search); 18 | if (typeof parsed.filters === "string") { 19 | // decoded 20 | //[{"key":"cohort","operator":"==","value":"AMS1"}] 21 | filters = JSON.parse(parsed.filters); 22 | //TODO: json schema validator 23 | } 24 | 25 | return ( 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /www/src/pages/Test.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { SnackContext } from "contexts/SnackContext"; 3 | 4 | const TestView = () => { 5 | const snackContext = useContext(SnackContext); 6 | 7 | useEffect(() => { 8 | // alert("OPEN"); 9 | snackContext.open({ 10 | variant: "progress", 11 | message: "Preparing files for download", 12 | duration: undefined, 13 | }); 14 | 15 | snackContext.setProgress({ value: 90, target: 120 }); 16 | }, []); 17 | 18 | return <>; 19 | }; 20 | 21 | export default TestView; 22 | -------------------------------------------------------------------------------- /www/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /www/src/utils/CustomBrowserRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Route } from "react-router-dom"; 3 | 4 | export const RouterContext = React.createContext({}); 5 | 6 | interface ICustomBrowserProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | const CustomBrowserRouter: React.FC = ({ children }) => ( 11 | 12 | 13 | {(routeProps) => ( 14 | 15 | {children} 16 | 17 | )} 18 | 19 | 20 | ); 21 | 22 | export default CustomBrowserRouter; 23 | -------------------------------------------------------------------------------- /www/src/utils/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Route, RouteProps, Redirect } from "react-router-dom"; 3 | 4 | import { AppContext } from "contexts/AppContext"; 5 | import Loading from "../components/Loading"; 6 | 7 | interface IPrivateRouteProps extends RouteProps { 8 | render: NonNullable; 9 | } 10 | 11 | const PrivateRoute: React.FC = ({ render, ...rest }) => { 12 | const { currentUser } = useContext(AppContext); 13 | 14 | if (!!currentUser) return ; 15 | 16 | if (currentUser === null) return ; 17 | 18 | return ( 19 | } 22 | /> 23 | ); 24 | }; 25 | 26 | export default PrivateRoute; 27 | -------------------------------------------------------------------------------- /www/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { auth, googleProvider } from "../firebase"; 2 | 3 | export const handleGoogleAuth = async ( 4 | success: Function, 5 | fail: Function, 6 | email?: string 7 | ) => { 8 | try { 9 | const authUser = await auth.signInWithPopup(googleProvider); 10 | if (!authUser.user) throw Error("Failed to authenticate"); 11 | if (email && email.toLowerCase() !== authUser.user.email?.toLowerCase()) 12 | throw Error(`Used account is not ${email}`); 13 | const result = await authUser.user.getIdTokenResult(); 14 | if (result.claims.roles && result.claims.roles.length !== 0) { 15 | success(authUser, result.claims.roles); 16 | } else { 17 | throw Error("This account does not have any roles"); 18 | } 19 | } catch (error) { 20 | if (auth.currentUser) { 21 | auth.signOut(); 22 | } 23 | fail(error); 24 | } 25 | }; 26 | export const signOut = () => { 27 | if (auth.currentUser) { 28 | auth.signOut(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /www/src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { scale } from "chroma-js"; 2 | 3 | export const resultColors = { 4 | No: "#ED4747", 5 | Maybe: "#f3c900", 6 | Yes: "#1fad5f", 7 | }; 8 | 9 | export const resultColorsScale = scale(Object.values(resultColors)).mode("lab"); 10 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es5", "es6", "dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "noImplicitAny": false, 17 | "jsx": "react-jsx", 18 | "baseUrl": "src", 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["src"] 22 | } 23 | --------------------------------------------------------------------------------