mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-16 19:57:49 +01:00
Merge branch 'v3' of https://github.com/notsidney/xtable into data-layer-rewrite
This commit is contained in:
1
.husky/pre-commit
Executable file
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
|||||||
|
yarn lint-staged
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.yarn
|
.yarn
|
||||||
|
emulators/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { whenDev } = require("@craco/craco");
|
||||||
const CracoAlias = require("craco-alias");
|
const CracoAlias = require("craco-alias");
|
||||||
const CracoSwcPlugin = require("craco-swc");
|
const CracoSwcPlugin = require("craco-swc");
|
||||||
|
|
||||||
@@ -11,12 +12,18 @@ module.exports = {
|
|||||||
tsConfigPath: "./tsconfig.extend.json",
|
tsConfigPath: "./tsconfig.extend.json",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Use Babel on dev since Jotai doesn’t have swc plugins yet
|
||||||
|
// See https://github.com/pmndrs/jotai/discussions/1057
|
||||||
|
// Use swc on production and test since Babel seems to break Jest
|
||||||
|
...whenDev(
|
||||||
|
() => [],
|
||||||
|
[
|
||||||
{
|
{
|
||||||
plugin: CracoSwcPlugin,
|
plugin: CracoSwcPlugin,
|
||||||
options: {
|
options: {
|
||||||
swcLoaderOptions: {
|
swcLoaderOptions: {
|
||||||
jsc: {
|
jsc: {
|
||||||
target: "es2019",
|
target: "es2021",
|
||||||
transform: {
|
transform: {
|
||||||
react: {
|
react: {
|
||||||
runtime: "automatic",
|
runtime: "automatic",
|
||||||
@@ -26,5 +33,22 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
babel: {
|
||||||
|
plugins: [
|
||||||
|
"jotai/babel/plugin-debug-label",
|
||||||
|
"./node_modules/jotai/babel/plugin-react-refresh",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
jest: {
|
||||||
|
configure: (jestConfig) => {
|
||||||
|
jestConfig.setupFilesAfterEnv = ["./src/test/setupTests.ts"];
|
||||||
|
jestConfig.forceExit = true; // jest hangs if we don't have this
|
||||||
|
|
||||||
|
jestConfig.moduleNameMapper["^lodash-es$"] = "lodash";
|
||||||
|
return jestConfig;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
1
emulators/auth_export/accounts.json
Normal file
1
emulators/auth_export/accounts.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651630548960","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"}],"validSince":"1651630530","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-05-04T02:15:48.960Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651223181908","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"}],"validSince":"1651630530","email":"editor@example.com","emailVerified":true,"disabled":false}]}
|
||||||
1
emulators/auth_export/config.json
Normal file
1
emulators/auth_export/config.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"signIn":{"allowDuplicateEmails":false},"usageMode":"DEFAULT"}
|
||||||
12
emulators/firebase-export-metadata.json
Normal file
12
emulators/firebase-export-metadata.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "10.6.0",
|
||||||
|
"firestore": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"path": "firestore_export",
|
||||||
|
"metadata_file": "firestore_export/firestore_export.overall_export_metadata"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"path": "auth_export"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
emulators/firestore_export/all_namespaces/all_kinds/output-0
Normal file
BIN
emulators/firestore_export/all_namespaces/all_kinds/output-0
Normal file
Binary file not shown.
Binary file not shown.
@@ -8,5 +8,26 @@
|
|||||||
"destination": "/index.html"
|
"destination": "/index.html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"firestore": {
|
||||||
|
"rules": "firestore.rules",
|
||||||
|
"indexes": "firestore.indexes.json"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"rules": "storage.rules"
|
||||||
|
},
|
||||||
|
"emulators": {
|
||||||
|
"auth": {
|
||||||
|
"port": 9099
|
||||||
|
},
|
||||||
|
"firestore": {
|
||||||
|
"port": 9299
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"port": 9199
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
firestore.indexes.json
Normal file
4
firestore.indexes.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"indexes": [],
|
||||||
|
"fieldOverrides": []
|
||||||
|
}
|
||||||
36
firestore.rules
Normal file
36
firestore.rules
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
// Allow admins to read and write all documents
|
||||||
|
match /{document=**} {
|
||||||
|
allow read, write: if hasAnyRole(["ADMIN", "OWNER"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rowy: Allow signed in users to read Rowy configuration and admins to write
|
||||||
|
match /_rowy_/{docId} {
|
||||||
|
allow read: if request.auth.token.roles.size() > 0;
|
||||||
|
allow write: if hasAnyRole(["ADMIN", "OWNER"]);
|
||||||
|
match /{document=**} {
|
||||||
|
allow read: if request.auth.token.roles.size() > 0;
|
||||||
|
allow write: if hasAnyRole(["ADMIN", "OWNER"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rowy: Allow users to edit their settings
|
||||||
|
match /_rowy_/userManagement/users/{userId} {
|
||||||
|
allow get, update, delete: if isDocOwner(userId);
|
||||||
|
allow create: if request.auth != null;
|
||||||
|
}
|
||||||
|
// Rowy: Allow public to read public Rowy configuration
|
||||||
|
match /_rowy_/publicSettings {
|
||||||
|
allow get: if true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rowy: Utility functions
|
||||||
|
function isDocOwner(docId) {
|
||||||
|
return request.auth != null && (request.auth.uid == resource.id || request.auth.uid == docId);
|
||||||
|
}
|
||||||
|
function hasAnyRole(roles) {
|
||||||
|
return request.auth != null && request.auth.token.roles.hasAny(roles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
213
package.json
213
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rowy",
|
"name": "rowy",
|
||||||
"version": "2.5.0",
|
"version": "2.6.0-alpha.0",
|
||||||
"homepage": "https://rowy.io",
|
"homepage": "https://rowy.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -8,94 +8,106 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@craco/craco": "^6.2.0",
|
"@emotion/react": "^11.9.0",
|
||||||
"@date-io/date-fns": "1.x",
|
"@emotion/styled": "^11.8.1",
|
||||||
"@emotion/react": "^11.4.0",
|
"@mdi/js": "^6.6.96",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@monaco-editor/react": "^4.4.4",
|
||||||
"@hookform/resolvers": "^2.8.5",
|
"@mui/icons-material": "^5.6.0",
|
||||||
"@mdi/js": "^6.5.95",
|
"@mui/lab": "^5.0.0-alpha.76",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@mui/material": "^5.6.0",
|
||||||
"@mui/icons-material": "^5.5.1",
|
"@mui/styles": "^5.6.2",
|
||||||
"@mui/lab": "^5.0.0-alpha.73",
|
"@rowy/form-builder": "^0.5.5",
|
||||||
"@mui/material": "^5.5.1",
|
"@rowy/multiselect": "^0.3.0",
|
||||||
"@mui/styles": "^5.5.1",
|
"compare-versions": "^4.1.3",
|
||||||
"@rowy/form-builder": "^0.5.3",
|
"date-fns": "^2.28.0",
|
||||||
"@rowy/multiselect": "^0.2.3",
|
"dompurify": "^2.3.6",
|
||||||
"@tinymce/tinymce-react": "^3.12.6",
|
"firebase": "^9.6.11",
|
||||||
"algoliasearch": "^4.8.6",
|
"firebaseui": "^6.0.1",
|
||||||
"ansi-to-react": "^6.1.5",
|
"jotai": "^1.6.5",
|
||||||
"colord": "^2.7.0",
|
|
||||||
"compare-versions": "^4.1.1",
|
|
||||||
"craco-swc": "^0.1.3",
|
|
||||||
"csv-parse": "^4.15.3",
|
|
||||||
"date-fns": "^2.19.0",
|
|
||||||
"dompurify": "^2.2.6",
|
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"firebase": "8.6.8",
|
|
||||||
"hotkeys-js": "^3.7.2",
|
|
||||||
"jotai": "^1.5.3",
|
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"json2csv": "^5.0.6",
|
"lodash-es": "^4.17.21",
|
||||||
"jszip": "^3.6.0",
|
|
||||||
"jwt-decode": "^3.1.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"match-sorter": "^6.3.1",
|
"match-sorter": "^6.3.1",
|
||||||
"notistack": "^2.0.2",
|
"notistack": "^2.0.4",
|
||||||
"pb-util": "^1.0.1",
|
"quicktype-core": "^6.0.71",
|
||||||
"query-string": "^6.8.3",
|
"react": "^18.0.0",
|
||||||
"quicktype-core": "^6.0.70",
|
"react-color-palette": "^6.2.0",
|
||||||
"react": "^17.0.2",
|
"react-data-grid": "7.0.0-beta.5",
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-div-100vh": "^0.7.0",
|
||||||
"react-color-palette": "^6.1.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-data-grid": "^7.0.0-beta.5",
|
|
||||||
"react-div-100vh": "^0.6.0",
|
|
||||||
"react-dnd": "^11.1.3",
|
|
||||||
"react-dnd-html5-backend": "^11.1.3",
|
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-dropzone": "^10.1.8",
|
|
||||||
"react-element-scroll-hook": "^1.1.0",
|
"react-element-scroll-hook": "^1.1.0",
|
||||||
"react-firebaseui": "^5.0.2",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-hook-form": "^7.21.2",
|
"react-hook-form": "^7.30.0",
|
||||||
"react-image": "^4.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-json-view": "^1.19.1",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-markdown": "^8.0.0",
|
|
||||||
"react-router-dom": "^5.0.1",
|
|
||||||
"react-router-hash-link": "^2.4.3",
|
"react-router-hash-link": "^2.4.3",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^5.0.0",
|
||||||
"react-usestateref": "^1.0.5",
|
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"serve": "^11.3.2",
|
"swr": "^1.3.0",
|
||||||
"swr": "^1.0.1",
|
"tss-react": "^3.6.2",
|
||||||
"tinymce": "^5.10.0",
|
"typescript": "^4.6.3",
|
||||||
"typescript": "^4.4.2",
|
"use-debounce": "^7.0.1",
|
||||||
"use-algolia": "^1.4.1",
|
"web-vitals": "^2.1.4"
|
||||||
"use-debounce": "^3.3.0",
|
|
||||||
"use-persisted-state": "^0.3.3",
|
|
||||||
"yarn": "^1.22.10"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"upstream": "git fetch upstream;git merge upstream/main;git commit -m'merge upstream';git push",
|
"start": "cross-env PORT=7699 craco start",
|
||||||
"serve": "serve -s build",
|
"startWithEmulator": "cross-env PORT=7699 REACT_APP_FIREBASE_EMULATOR=true craco start",
|
||||||
"start": "craco start",
|
"emulators": "firebase emulators:start --only firestore,auth --import ./emulators/ --export-on-exit",
|
||||||
"build": "craco build CI=false",
|
"test": "craco test --env ./src/test/custom-jest-env.js",
|
||||||
"test": "craco test --env=jsdom",
|
"build": "craco build",
|
||||||
"eject": "craco eject",
|
"analyze": "source-map-explorer ./build/static/js/*.js",
|
||||||
|
"prepare": "husky install",
|
||||||
"env": "node createDotEnv",
|
"env": "node createDotEnv",
|
||||||
"target": "firebase target:apply hosting rowy",
|
"target": "firebase target:apply hosting rowy",
|
||||||
"deploy": "firebase deploy"
|
"deploy": "firebase deploy --only hosting"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"plugins": [
|
||||||
|
"eslint-plugin-no-relative-import-paths",
|
||||||
|
"eslint-plugin-tsdoc",
|
||||||
|
"eslint-plugin-local-rules"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-relative-import-paths/no-relative-import-paths": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowSameFolder": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"group": [
|
||||||
|
"lodash",
|
||||||
|
"lodash/*"
|
||||||
|
],
|
||||||
|
"message": "Use lodash-es instead"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tsdoc/syntax": "warn",
|
||||||
|
"local-rules/no-jotai-use-atom-without-scope": "error"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
"> 0.5%",
|
||||||
"not dead",
|
"not dead",
|
||||||
"not op_mini all"
|
"not op_mini all",
|
||||||
|
"not ie > 0",
|
||||||
|
"not and_uc > 0",
|
||||||
|
"not ios_saf < 14"
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version",
|
||||||
@@ -104,34 +116,41 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dompurify": "^2.2.1",
|
"@craco/craco": "^6.4.3",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@types/lodash": "^4.14.168",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@types/node": "^14.14.6",
|
"@testing-library/user-event": "^14.0.4",
|
||||||
"@types/react": "^17.0.11",
|
"@types/dompurify": "^2.3.3",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/react-color": "^3.0.1",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/react-div-100vh": "^0.3.0",
|
"@types/node": "^17.0.23",
|
||||||
"@types/react-dom": "^17.0.8",
|
"@types/react": "^18.0.5",
|
||||||
"@types/react-helmet": "^6.1.2",
|
"@types/react-div-100vh": "^0.4.0",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@types/react-router-hash-link": "^2.4.1",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/use-persisted-state": "^0.3.0",
|
"@types/react-router-hash-link": "^2.4.5",
|
||||||
|
"@typescript-eslint/parser": "^5.18.0",
|
||||||
"craco-alias": "^3.0.1",
|
"craco-alias": "^3.0.1",
|
||||||
"firebase-tools": "^10.1.0",
|
"craco-swc": "^0.5.1",
|
||||||
"husky": "^4.2.5",
|
"cross-env": "^7.0.3",
|
||||||
"monaco-editor": "^0.21.2",
|
"eslint": "^8.12.0",
|
||||||
"playwright": "^1.5.2",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"prettier": "^2.2.1",
|
"eslint-config-react-app": "^7.0.0",
|
||||||
"pretty-quick": "^3.0.0",
|
"eslint-plugin-local-rules": "^1.1.0",
|
||||||
"raw-loader": "^4.0.2"
|
"eslint-plugin-no-relative-import-paths": "^1.2.0",
|
||||||
|
"eslint-plugin-tsdoc": "^0.2.16",
|
||||||
|
"husky": ">=7.0.4",
|
||||||
|
"lint-staged": ">=12.3.7",
|
||||||
|
"monaco-editor": "^0.33.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
|
"source-map-explorer": "^2.5.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"react-hook-form": "^7.21.2"
|
"@types/react": "^18"
|
||||||
},
|
},
|
||||||
"husky": {
|
"lint-staged": {
|
||||||
"hooks": {
|
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||||
"pre-commit": "pretty-quick --staged"
|
"**/*": "prettier --write --ignore-unknown"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
# Rewrite a path
|
|
||||||
/* /index.html 200
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<TileColor>#4200ff</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -1,2 +1,3 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
User-agent: *
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 268 KiB |
@@ -1,9 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
ReactDOM.render(<App />, div);
|
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
|
||||||
});
|
|
||||||
274
src/App.tsx
274
src/App.tsx
@@ -1,221 +1,119 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { Route, Switch, Redirect } from "react-router-dom";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
import LocalizationProvider from "@mui/lab/LocalizationProvider";
|
import { useAtom } from "jotai";
|
||||||
import AdapterDateFns from "@mui/lab/AdapterDateFns";
|
|
||||||
|
|
||||||
import { StyledEngineProvider } from "@mui/material/styles";
|
|
||||||
import "./space-grotesk.css";
|
|
||||||
|
|
||||||
import CustomBrowserRouter from "@src/utils/CustomBrowserRouter";
|
|
||||||
import PrivateRoute from "@src/utils/PrivateRoute";
|
|
||||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
|
||||||
import Loading from "@src/components/Loading";
|
import Loading from "@src/components/Loading";
|
||||||
import Navigation from "@src/components/Navigation";
|
import ProjectSourceFirebase from "@src/sources/ProjectSourceFirebase";
|
||||||
import Logo from "@src/assets/Logo";
|
import ConfirmDialog from "@src/components/ConfirmDialog";
|
||||||
import RowyRunModal from "@src/components/RowyRunModal";
|
import RowyRunModal from "@src/components/RowyRunModal";
|
||||||
|
import NotFound from "@src/pages/NotFound";
|
||||||
|
import RequireAuth from "@src/layouts/RequireAuth";
|
||||||
|
import Navigation from "@src/layouts/Navigation";
|
||||||
|
import TableSettingsDialog from "@src/components/TableSettingsDialog";
|
||||||
|
|
||||||
import SwrProvider from "@src/contexts/SwrContext";
|
import { globalScope, currentUserAtom } from "@src/atoms/globalScope";
|
||||||
import ConfirmationProvider from "@src/components/ConfirmationDialog/Provider";
|
import { ROUTES } from "@src/constants/routes";
|
||||||
import { AppProvider } from "@src/contexts/AppContext";
|
|
||||||
import { ProjectContextProvider } from "@src/contexts/ProjectContext";
|
|
||||||
import { SnackbarProvider } from "@src/contexts/SnackbarContext";
|
|
||||||
import { SnackLogProvider } from "@src/contexts/SnackLogContext";
|
|
||||||
import routes from "@src/constants/routes";
|
|
||||||
|
|
||||||
import AuthPage from "@src/pages/Auth";
|
import JotaiTestPage from "@src/pages/JotaiTest";
|
||||||
import SignOutPage from "@src/pages/Auth/SignOut";
|
import SignOutPage from "@src/pages/Auth/SignOut";
|
||||||
import SignUpPage from "@src/pages/Auth/SignUp";
|
|
||||||
import TestPage from "@src/pages/Test";
|
|
||||||
import RowyRunTestPage from "@src/pages/RowyRunTest";
|
|
||||||
import PageNotFound from "@src/pages/PageNotFound";
|
|
||||||
|
|
||||||
import Favicon from "@src/assets/Favicon";
|
|
||||||
import "@src/analytics";
|
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const AuthSetupGuidePage = lazy(() => import("@src/pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */));
|
const AuthPage = lazy(() => import("@src/pages/Auth/index" /* webpackChunkName: "AuthPage" */));
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const ImpersonatorAuthPage = lazy(() => import("./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */));
|
const SignUpPage = lazy(() => import("@src/pages/Auth/SignUp" /* webpackChunkName: "SignUpPage" */));
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const JwtAuthPage = lazy(() => import("./pages/Auth/JwtAuth" /* webpackChunkName: "JwtAuthPage" */));
|
const JwtAuthPage = lazy(() => import("@src/pages/Auth/JwtAuth" /* webpackChunkName: "JwtAuthPage" */));
|
||||||
|
// prettier-ignore
|
||||||
|
const ImpersonatorAuthPage = lazy(() => import("@src/pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */));
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const HomePage = lazy(() => import("./pages/Home" /* webpackChunkName: "HomePage" */));
|
|
||||||
// prettier-ignore
|
|
||||||
const TablePage = lazy(() => import("./pages/Table" /* webpackChunkName: "TablePage" */));
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const ProjectSettingsPage = lazy(() => import("./pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
|
|
||||||
// prettier-ignore
|
|
||||||
const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
|
|
||||||
// prettier-ignore
|
|
||||||
const UserManagementPage = lazy(() => import("./pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "SetupPage" */));
|
const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "SetupPage" */));
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const TablesPage = lazy(() => import("@src/pages/Tables" /* webpackChunkName: "TablesPage" */));
|
||||||
|
// prettier-ignore
|
||||||
|
const TablePage = lazy(() => import("@src/pages/TableTest" /* webpackChunkName: "TablePage" */));
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
|
||||||
|
// prettier-ignore
|
||||||
|
const ProjectSettingsPage = lazy(() => import("@src/pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
|
||||||
|
// prettier-ignore
|
||||||
|
const UserManagementPage = lazy(() => import("@src/pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
|
||||||
|
// const RowyRunTestPage = lazy(() => import("@src/pages/RowyRunTest" /* webpackChunkName: "RowyRunTestPage" */));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [currentUser] = useAtom(currentUserAtom, globalScope);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEngineProvider injectFirst>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<SwrProvider>
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
||||||
<AppProvider>
|
|
||||||
<Favicon />
|
|
||||||
<SnackbarProvider>
|
|
||||||
<ConfirmationProvider>
|
|
||||||
<SnackLogProvider>
|
|
||||||
<CustomBrowserRouter>
|
|
||||||
<RowyRunModal />
|
|
||||||
<Suspense fallback={<Loading fullScreen />}>
|
<Suspense fallback={<Loading fullScreen />}>
|
||||||
<Switch>
|
<ProjectSourceFirebase />
|
||||||
|
<ConfirmDialog />
|
||||||
|
<RowyRunModal />
|
||||||
|
|
||||||
|
{currentUser === undefined ? (
|
||||||
|
<Loading fullScreen message="Authenticating" />
|
||||||
|
) : (
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|
||||||
|
<Route path={ROUTES.auth} element={<AuthPage />} />
|
||||||
|
<Route path={ROUTES.signUp} element={<SignUpPage />} />
|
||||||
|
<Route path={ROUTES.signOut} element={<SignOutPage />} />
|
||||||
|
<Route path={ROUTES.jwtAuth} element={<JwtAuthPage />} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
path={ROUTES.impersonatorAuth}
|
||||||
path={routes.auth}
|
element={
|
||||||
render={() => <AuthPage />}
|
<RequireAuth>
|
||||||
/>
|
<ImpersonatorAuthPage />
|
||||||
<Route
|
</RequireAuth>
|
||||||
exact
|
}
|
||||||
path={routes.authSetup}
|
|
||||||
render={() => <AuthSetupGuidePage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={routes.jwtAuth}
|
|
||||||
render={() => <JwtAuthPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={routes.signOut}
|
|
||||||
render={() => <SignOutPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={routes.signUp}
|
|
||||||
render={() => <SignUpPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={routes.setup}
|
|
||||||
render={() => <SetupPage />}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route path={ROUTES.setup} element={<SetupPage />} />
|
||||||
exact
|
|
||||||
path={"/test"}
|
|
||||||
render={() => <TestPage />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PrivateRoute
|
|
||||||
exact
|
|
||||||
path={[
|
|
||||||
routes.home,
|
|
||||||
routes.tableWithId,
|
|
||||||
routes.tableGroupWithId,
|
|
||||||
routes.settings,
|
|
||||||
routes.projectSettings,
|
|
||||||
routes.userSettings,
|
|
||||||
routes.userManagement,
|
|
||||||
routes.impersonatorAuth,
|
|
||||||
routes.rowyRunTest,
|
|
||||||
]}
|
|
||||||
render={() => (
|
|
||||||
<ProjectContextProvider>
|
|
||||||
<Switch>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
path="/"
|
||||||
path={routes.impersonatorAuth}
|
element={
|
||||||
render={() => <ImpersonatorAuthPage />}
|
<RequireAuth>
|
||||||
/>
|
<Navigation>
|
||||||
<Route
|
<TableSettingsDialog />
|
||||||
exact
|
</Navigation>
|
||||||
path={routes.rowyRunTest}
|
</RequireAuth>
|
||||||
render={() => <RowyRunTestPage />}
|
|
||||||
/>
|
|
||||||
<PrivateRoute
|
|
||||||
exact
|
|
||||||
path={routes.home}
|
|
||||||
render={() => (
|
|
||||||
<Navigation
|
|
||||||
title="Home"
|
|
||||||
titleComponent={(open, pinned) =>
|
|
||||||
!(open && pinned) && (
|
|
||||||
<Logo
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
margin: "0 auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HomePage />
|
<Route
|
||||||
</Navigation>
|
path={ROUTES.home}
|
||||||
)}
|
element={<Navigate to={ROUTES.tables} replace />}
|
||||||
/>
|
|
||||||
<PrivateRoute
|
|
||||||
path={routes.tableWithId}
|
|
||||||
render={() => <TablePage />}
|
|
||||||
/>
|
|
||||||
<PrivateRoute
|
|
||||||
path={routes.tableGroupWithId}
|
|
||||||
render={() => <TablePage />}
|
|
||||||
/>
|
/>
|
||||||
|
<Route path={ROUTES.tables} element={<TablesPage />} />
|
||||||
|
|
||||||
<PrivateRoute
|
<Route path={ROUTES.table}>
|
||||||
exact
|
<Route index element={<Navigate to={ROUTES.tables} replace />} />
|
||||||
path={routes.settings}
|
<Route path=":id" element={<TablePage />} />
|
||||||
render={() => (
|
</Route>
|
||||||
<Redirect to={routes.userSettings} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<PrivateRoute
|
|
||||||
exact
|
|
||||||
path={routes.projectSettings}
|
|
||||||
render={() => (
|
|
||||||
<Navigation title="Project Settings">
|
|
||||||
<ProjectSettingsPage />
|
|
||||||
</Navigation>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<PrivateRoute
|
|
||||||
exact
|
|
||||||
path={routes.userSettings}
|
|
||||||
render={() => (
|
|
||||||
<Navigation title="Settings">
|
|
||||||
<UserSettingsPage />
|
|
||||||
</Navigation>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<PrivateRoute
|
|
||||||
exact
|
|
||||||
path={routes.userManagement}
|
|
||||||
render={() => (
|
|
||||||
<Navigation title="User Management">
|
|
||||||
<UserManagementPage />
|
|
||||||
</Navigation>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</ProjectContextProvider>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
path={ROUTES.settings}
|
||||||
path={routes.pageNotFound}
|
element={<Navigate to={ROUTES.userSettings} replace />}
|
||||||
render={() => <PageNotFound />}
|
|
||||||
/>
|
/>
|
||||||
<Route render={() => <PageNotFound />} />
|
<Route path={ROUTES.userSettings} element={<UserSettingsPage />} />
|
||||||
</Switch>
|
<Route
|
||||||
|
path={ROUTES.projectSettings}
|
||||||
|
element={<ProjectSettingsPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={ROUTES.userManagement}
|
||||||
|
element={<UserManagementPage />}
|
||||||
|
/>
|
||||||
|
{/* <Route path={ROUTES.rowyRunTest} element={<RowyRunTestPage />} /> */}
|
||||||
|
|
||||||
|
<Route path="/jotaiTest" element={<JotaiTestPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* <Route path="/jotaiTest" element={<JotaiTestPage />} /> */}
|
||||||
|
</Routes>
|
||||||
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</CustomBrowserRouter>
|
|
||||||
</SnackLogProvider>
|
|
||||||
</ConfirmationProvider>
|
|
||||||
</SnackbarProvider>
|
|
||||||
</AppProvider>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</SwrProvider>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</StyledEngineProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/Providers.tsx
Normal file
47
src/Providers.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import ErrorFallback from "@src/components/ErrorFallback";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
|
import { Provider, Atom } from "jotai";
|
||||||
|
import { globalScope } from "@src/atoms/globalScope";
|
||||||
|
import createCache from "@emotion/cache";
|
||||||
|
import { CacheProvider } from "@emotion/react";
|
||||||
|
import RowyThemeProvider from "@src/theme/RowyThemeProvider";
|
||||||
|
import SnackbarProvider from "@src/contexts/SnackbarContext";
|
||||||
|
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import Loading from "@src/components/Loading";
|
||||||
|
|
||||||
|
export const muiCache = createCache({ key: "mui", prepend: true });
|
||||||
|
|
||||||
|
export interface IProvidersProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
initialAtomValues?: Iterable<readonly [Atom<unknown>, unknown]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Providers({
|
||||||
|
children,
|
||||||
|
initialAtomValues,
|
||||||
|
}: IProvidersProps) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<HelmetProvider>
|
||||||
|
<Provider scope={globalScope} initialValues={initialAtomValues}>
|
||||||
|
<CacheProvider value={muiCache}>
|
||||||
|
<RowyThemeProvider>
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
<SnackbarProvider>
|
||||||
|
<Suspense fallback={<Loading fullScreen />}>
|
||||||
|
{children}
|
||||||
|
</Suspense>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</RowyThemeProvider>
|
||||||
|
</CacheProvider>
|
||||||
|
</Provider>
|
||||||
|
</HelmetProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import firebase from "firebase/app";
|
import { initializeApp } from "firebase/app";
|
||||||
import "firebase/analytics";
|
import { getAnalytics, logEvent } from "firebase/analytics";
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyArABiYGK7dZgwSk0pw_6vKbOt6U1ZRPpc",
|
apiKey: "AIzaSyArABiYGK7dZgwSk0pw_6vKbOt6U1ZRPpc",
|
||||||
@@ -11,7 +11,6 @@ const firebaseConfig = {
|
|||||||
measurementId: "G-0VWE25LFZJ",
|
measurementId: "G-0VWE25LFZJ",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize Firebase
|
const rowyServiceApp = initializeApp(firebaseConfig, "rowy-service");
|
||||||
const rowyServiceApp = firebase.initializeApp(firebaseConfig, "rowy-service");
|
export const analytics = getAnalytics(rowyServiceApp);
|
||||||
|
export { logEvent };
|
||||||
export const analytics = firebase.analytics(rowyServiceApp);
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import Helmet from "react-helmet";
|
|
||||||
import { use100vh } from "react-div-100vh";
|
import { use100vh } from "react-div-100vh";
|
||||||
|
|
||||||
import { useTheme, alpha } from "@mui/material/styles";
|
import { GlobalStyles, Box, BoxProps } from "@mui/material";
|
||||||
import { Box, BoxProps } from "@mui/material";
|
import { alpha } from "@mui/material/styles";
|
||||||
|
|
||||||
import bgPattern from "@src/assets/bg-pattern.svg";
|
import bgPattern from "@src/assets/bg-pattern.svg";
|
||||||
import bgPatternDark from "@src/assets/bg-pattern-dark.svg";
|
import bgPatternDark from "@src/assets/bg-pattern-dark.svg";
|
||||||
|
|
||||||
export default function BrandedBackground() {
|
export default function BrandedBackground() {
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<GlobalStyles
|
||||||
<style type="text/css">
|
styles={(theme) => `
|
||||||
{`
|
|
||||||
body {
|
body {
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-image: ${
|
background-image: ${
|
||||||
@@ -44,8 +40,7 @@ export default function BrandedBackground() {
|
|||||||
mix-blend-mode: overlay;
|
mix-blend-mode: overlay;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
/>
|
||||||
</Helmet>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ export default function LogoRowyRun({
|
|||||||
<title id="rowy-run-logo-title">Rowy Run</title>
|
<title id="rowy-run-logo-title">Rowy Run</title>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fillRule="evenodd"
|
||||||
clip-rule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M32 7.75a6.25 6.25 0 1 1 0 12.5 6.25 6.25 0 0 1 0-12.5Zm0 2a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM20 20V8h6v2h-4v10h-2Zm24 0 3-9 3 9h2l4-12 5 11.5-2 4.5h2l7-16h-2l-4 9-4-9h-4l-3 9-3-9h-2l-3 9-3-9h-2l4 12h2Z"
|
d="M32 7.75a6.25 6.25 0 1 1 0 12.5 6.25 6.25 0 0 1 0-12.5Zm0 2a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM20 20V8h6v2h-4v10h-2Zm24 0 3-9 3 9h2l4-12 5 11.5-2 4.5h2l7-16h-2l-4 9-4-9h-4l-3 9-3-9h-2l-3 9-3-9h-2l4 12h2Z"
|
||||||
fill={theme.palette.text.primary}
|
fill={theme.palette.text.primary}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fillRule="evenodd"
|
||||||
clip-rule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M0 8v10a3 3 0 1 0 6 0v-7h7a3 3 0 1 0 0-6H8a2.997 2.997 0 0 0-2.5 1.341A3 3 0 0 0 0 8Zm10-2H8a2 2 0 0 0-1.995 1.85L6 8v2h4V6Zm-5 4V8a2 2 0 0 0-1.85-1.995L3 6a2 2 0 0 0-1.995 1.85L1 8v2h4Zm0 1H1v4h4v-4Zm-4 5v2a2 2 0 0 0 1.85 1.994L3 20a2 2 0 0 0 1.995-1.85L5 18v-2H1ZM11.001 6H13l.15.005A2 2 0 0 1 15 8l-.005.15A2 2 0 0 1 13 10h-1.999V6Z"
|
d="M0 8v10a3 3 0 1 0 6 0v-7h7a3 3 0 1 0 0-6H8a2.997 2.997 0 0 0-2.5 1.341A3 3 0 0 0 0 8Zm10-2H8a2 2 0 0 0-1.995 1.85L6 8v2h4V6Zm-5 4V8a2 2 0 0 0-1.85-1.995L3 6a2 2 0 0 0-1.995 1.85L1 8v2h4Zm0 1H1v4h4v-4Zm-4 5v2a2 2 0 0 0 1.85 1.994L3 20a2 2 0 0 0 1.995-1.85L5 18v-2H1ZM11.001 6H13l.15.005A2 2 0 0 1 15 8l-.005.15A2 2 0 0 1 13 10h-1.999V6Z"
|
||||||
fill={theme.palette.primary.main}
|
fill={theme.palette.primary.main}
|
||||||
/>
|
/>
|
||||||
|
|||||||
5
src/assets/favicon.svg
Normal file
5
src/assets/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M13 0a3 3 0 010 6l-2-.001V6H6v7a3 3 0 01-6 0V3a3 3 0 015.501-1.657A2.989 2.989 0 018 0h5zM5 11H1v2a2 2 0 001.85 1.995L3 15a2 2 0 001.995-1.85L5 13v-2zm0-5H1v4h4V6zM3 1a2 2 0 00-1.995 1.85L1 3v2h4V3a2 2 0 00-1.85-1.995L3 1zm8.001 0v4H13a2 2 0 001.995-1.85L15 3a2 2 0 00-1.85-1.995L13 1h-1.999zM10 1H8a2 2 0 00-1.995 1.85L6 3v2h4V1z"
|
||||||
|
fill="currentColor" fill-rule="nonzero" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 465 B |
@@ -1,5 +1,5 @@
|
|||||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||||
import { mdiFirebase } from '@mdi/js';
|
import { mdiFirebase } from "@mdi/js";
|
||||||
|
|
||||||
export default function AddColumn(props: SvgIconProps) {
|
export default function AddColumn(props: SvgIconProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useAtom } from "jotai";
|
|
||||||
import { atomWithReset, useResetAtom, useUpdateAtom } from "jotai/utils";
|
|
||||||
|
|
||||||
export type SelectedCell = {
|
|
||||||
rowIndex: number;
|
|
||||||
colIndex: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type anchorEl = HTMLElement;
|
|
||||||
|
|
||||||
const selectedCellAtom = atomWithReset<SelectedCell | null>(null);
|
|
||||||
const anchorEleAtom = atomWithReset<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
export function useSetAnchorEle() {
|
|
||||||
const setAnchorEle = useUpdateAtom(anchorEleAtom);
|
|
||||||
return { setAnchorEle };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSetSelectedCell() {
|
|
||||||
const setSelectedCell = useUpdateAtom(selectedCellAtom);
|
|
||||||
return { setSelectedCell };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContextMenuAtom() {
|
|
||||||
const [anchorEle] = useAtom(anchorEleAtom);
|
|
||||||
const [selectedCell] = useAtom(selectedCellAtom);
|
|
||||||
const resetAnchorEle = useResetAtom(anchorEleAtom);
|
|
||||||
const resetSelectedCell = useResetAtom(selectedCellAtom);
|
|
||||||
|
|
||||||
const resetContextMenu = async () => {
|
|
||||||
await resetAnchorEle();
|
|
||||||
await resetSelectedCell();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
anchorEle,
|
|
||||||
selectedCell,
|
|
||||||
resetContextMenu,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
|
||||||
|
|
||||||
export const rowyRunModalAtom = atom({ open: false, feature: "", version: "" });
|
|
||||||
|
|
||||||
export const useRowyRunModal = () => {
|
|
||||||
const [, setOpen] = useAtom(rowyRunModalAtom);
|
|
||||||
|
|
||||||
return (feature: string = "", version: string = "") =>
|
|
||||||
setOpen({ open: true, feature, version });
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { atomWithHash } from "jotai/utils";
|
|
||||||
|
|
||||||
export const modalAtom = atomWithHash<
|
|
||||||
"cloudLogs" | "extensions" | "webhooks" | "export" | ""
|
|
||||||
>("modal", "");
|
|
||||||
8
src/atoms/globalScope/auth.ts
Normal file
8
src/atoms/globalScope/auth.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import type { User } from "firebase/auth";
|
||||||
|
|
||||||
|
/** Currently signed in user. `undefined` means loading. */
|
||||||
|
export const currentUserAtom = atom<User | null | undefined>(undefined);
|
||||||
|
|
||||||
|
/** User roles from Firebase Auth user custom claims */
|
||||||
|
export const userRolesAtom = atom<string[]>([]);
|
||||||
9
src/atoms/globalScope/index.ts
Normal file
9
src/atoms/globalScope/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/** Scope for atoms stored at the root of the app */
|
||||||
|
export const globalScope = Symbol("globalScope");
|
||||||
|
|
||||||
|
export * from "./auth";
|
||||||
|
export * from "./project";
|
||||||
|
export * from "./user";
|
||||||
|
export * from "./ui";
|
||||||
|
|
||||||
|
export * from "./rowyRun";
|
||||||
94
src/atoms/globalScope/project.ts
Normal file
94
src/atoms/globalScope/project.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { sortBy } from "lodash-es";
|
||||||
|
import { ThemeOptions } from "@mui/material";
|
||||||
|
|
||||||
|
import { userRolesAtom } from "./auth";
|
||||||
|
import { UserSettings } from "./user";
|
||||||
|
import {
|
||||||
|
UpdateDocFunction,
|
||||||
|
UpdateCollectionFunction,
|
||||||
|
TableSettings,
|
||||||
|
} from "@src/types/table";
|
||||||
|
|
||||||
|
export const projectIdAtom = atom<string>("");
|
||||||
|
|
||||||
|
/** Public settings are visible to unauthenticated users */
|
||||||
|
export type PublicSettings = Partial<{
|
||||||
|
signInOptions: Array<
|
||||||
|
| "google"
|
||||||
|
| "twitter"
|
||||||
|
| "facebook"
|
||||||
|
| "github"
|
||||||
|
| "microsoft"
|
||||||
|
| "apple"
|
||||||
|
| "yahoo"
|
||||||
|
| "email"
|
||||||
|
| "phone"
|
||||||
|
| "anonymous"
|
||||||
|
>;
|
||||||
|
theme: Record<"base" | "light" | "dark", ThemeOptions>;
|
||||||
|
}>;
|
||||||
|
/** Public settings are visible to unauthenticated users */
|
||||||
|
export const publicSettingsAtom = atom<PublicSettings>({});
|
||||||
|
/** Stores a function that updates public settings */
|
||||||
|
export const updatePublicSettingsAtom =
|
||||||
|
atom<UpdateDocFunction<PublicSettings> | null>(null);
|
||||||
|
|
||||||
|
/** Project settings are visible to authenticated users */
|
||||||
|
export type ProjectSettings = Partial<{
|
||||||
|
tables: TableSettings[];
|
||||||
|
|
||||||
|
setupCompleted: boolean;
|
||||||
|
|
||||||
|
rowyRunUrl: string;
|
||||||
|
rowyRunRegion: string;
|
||||||
|
rowyRunBuildStatus: "BUILDING" | "COMPLETE";
|
||||||
|
services: Partial<{
|
||||||
|
hooks: string;
|
||||||
|
builder: string;
|
||||||
|
terminal: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
/** Project settings are visible to authenticated users */
|
||||||
|
export const projectSettingsAtom = atom<ProjectSettings>({});
|
||||||
|
/** Stores a function that updates project settings */
|
||||||
|
export const updateProjectSettingsAtom =
|
||||||
|
atom<UpdateDocFunction<ProjectSettings> | null>(null);
|
||||||
|
|
||||||
|
/** Tables visible to the signed-in user based on roles */
|
||||||
|
export const tablesAtom = atom<TableSettings[]>((get) => {
|
||||||
|
const userRoles = get(userRolesAtom);
|
||||||
|
const tables = get(projectSettingsAtom).tables || [];
|
||||||
|
|
||||||
|
return sortBy(tables, "name")
|
||||||
|
.filter(
|
||||||
|
(table) =>
|
||||||
|
userRoles.includes("ADMIN") ||
|
||||||
|
table.roles.some((role) => userRoles.includes(role))
|
||||||
|
)
|
||||||
|
.map((table) => ({
|
||||||
|
...table,
|
||||||
|
// Ensure id exists for backwards compatibility
|
||||||
|
id: table.id || table.collection,
|
||||||
|
// Ensure section exists
|
||||||
|
section: table.section ? table.section.trim() : "Other",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Roles used in the project based on table settings */
|
||||||
|
export const rolesAtom = atom((get) =>
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
get(tablesAtom).reduce(
|
||||||
|
(a, c) => [...a, ...c.roles],
|
||||||
|
["ADMIN", "EDITOR", "VIEWER"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** User management page: all users */
|
||||||
|
export const allUsersAtom = atom<UserSettings[]>([]);
|
||||||
|
/** Stores a function that updates a user document */
|
||||||
|
export const updateUserAtom =
|
||||||
|
atom<UpdateCollectionFunction<UserSettings> | null>(null);
|
||||||
134
src/atoms/globalScope/rowyRun.ts
Normal file
134
src/atoms/globalScope/rowyRun.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { selectAtom, atomWithStorage } from "jotai/utils";
|
||||||
|
import { isEqual } from "lodash-es";
|
||||||
|
import { getIdTokenResult } from "firebase/auth";
|
||||||
|
|
||||||
|
import { projectSettingsAtom } from "./project";
|
||||||
|
import { currentUserAtom } from "./auth";
|
||||||
|
import { RunRoute } from "@src/constants/runRoutes";
|
||||||
|
import meta from "@root/package.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rowyRunUrl from projectSettings, but only update when this field changes */
|
||||||
|
const rowyRunUrlAtom = selectAtom(
|
||||||
|
projectSettingsAtom,
|
||||||
|
(projectSettings) => projectSettings.rowyRunUrl
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* Get services from projectSettings, but only update when this field changes
|
||||||
|
*/
|
||||||
|
const rowyRunServicesAtom = selectAtom(
|
||||||
|
projectSettingsAtom,
|
||||||
|
(projectSettings) => projectSettings.services,
|
||||||
|
isEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface IRowyRunRequestProps {
|
||||||
|
/** Optionally force refresh the token */
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
service?: "hooks" | "builder";
|
||||||
|
/** Optionally use Rowy Run instance on localhost */
|
||||||
|
localhost?: boolean;
|
||||||
|
|
||||||
|
route: RunRoute;
|
||||||
|
body?: any;
|
||||||
|
/** Params appended to the URL. Will be transforme to a `/`-separated string. */
|
||||||
|
params?: string[];
|
||||||
|
/** Parse response as JSON. Default: true */
|
||||||
|
json?: boolean;
|
||||||
|
/** Optionally pass an abort signal to abort the request */
|
||||||
|
signal?: AbortSignal;
|
||||||
|
/** Optionally pass a callback that’s called if Rowy Run not set up */
|
||||||
|
handleNotSetUp?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An atom that returns a function to call Rowy Run endpoints using the URL
|
||||||
|
* defined in project settings and retrieving a JWT token.
|
||||||
|
*
|
||||||
|
* Returns `false` if user not signed in or Rowy Run not set up.
|
||||||
|
*
|
||||||
|
* @example Basic usage:
|
||||||
|
* ```
|
||||||
|
* const [rowyRun] = useAtom(rowyRunAtom, globalScope);
|
||||||
|
* ...
|
||||||
|
* await rowyRun(...);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const rowyRunAtom = atom((get) => {
|
||||||
|
const rowyRunUrl = get(rowyRunUrlAtom);
|
||||||
|
const rowyRunServices = get(rowyRunServicesAtom);
|
||||||
|
const currentUser = get(currentUserAtom);
|
||||||
|
|
||||||
|
return async ({
|
||||||
|
forceRefresh,
|
||||||
|
localhost = false,
|
||||||
|
service,
|
||||||
|
route,
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
signal,
|
||||||
|
json = true,
|
||||||
|
handleNotSetUp,
|
||||||
|
}: IRowyRunRequestProps): Promise<Response | any | false> => {
|
||||||
|
if (!currentUser) {
|
||||||
|
console.log("Rowy Run: Not signed in", route.path);
|
||||||
|
if (handleNotSetUp) handleNotSetUp();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const authToken = await getIdTokenResult(currentUser!, forceRefresh);
|
||||||
|
|
||||||
|
const serviceUrl = localhost
|
||||||
|
? "http://localhost:8080"
|
||||||
|
: service
|
||||||
|
? rowyRunServices?.[service]
|
||||||
|
: rowyRunUrl;
|
||||||
|
if (!serviceUrl) {
|
||||||
|
console.log("Rowy Run: Not set up", route.path);
|
||||||
|
if (handleNotSetUp) handleNotSetUp();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { method, path } = route;
|
||||||
|
let url = serviceUrl + path;
|
||||||
|
if (params && params.length > 0) url = url + "/" + params.join("/");
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
mode: "cors",
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + authToken,
|
||||||
|
},
|
||||||
|
redirect: "follow",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
// body data type must match "Content-Type" header
|
||||||
|
body: body && method !== "GET" ? JSON.stringify(body) : null,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (json) return await response.json();
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
type RowyRunLatestUpdate = {
|
||||||
|
lastChecked: string;
|
||||||
|
rowy: null | Record<string, any>;
|
||||||
|
rowyRun: null | Record<string, any>;
|
||||||
|
deployedRowy: string;
|
||||||
|
deployedRowyRun: string;
|
||||||
|
};
|
||||||
|
/** Store latest update from GitHub releases and currently deployed versions */
|
||||||
|
export const rowyRunLatestUpdateAtom = atomWithStorage<RowyRunLatestUpdate>(
|
||||||
|
"__ROWY__UPDATE_CHECK",
|
||||||
|
{
|
||||||
|
lastChecked: "",
|
||||||
|
rowy: null,
|
||||||
|
rowyRun: null,
|
||||||
|
deployedRowy: meta.version,
|
||||||
|
deployedRowyRun: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
126
src/atoms/globalScope/ui.ts
Normal file
126
src/atoms/globalScope/ui.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
|
||||||
|
import { DialogProps, ButtonProps } from "@mui/material";
|
||||||
|
import { TableSettings } from "@src/types/table";
|
||||||
|
|
||||||
|
/** Nav open state stored in local storage. */
|
||||||
|
export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false);
|
||||||
|
/** Nav pinned state stored in local storage. */
|
||||||
|
export const navPinnedAtom = atomWithStorage("__ROWY__NAV_PINNED", false);
|
||||||
|
|
||||||
|
/** View for tables page */
|
||||||
|
export const tablesViewAtom = atomWithStorage<"grid" | "list">(
|
||||||
|
"__ROWY__HOME_VIEW",
|
||||||
|
"grid"
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ConfirmDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
|
||||||
|
title?: string;
|
||||||
|
/** Pass a string to display basic styled text */
|
||||||
|
body?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Callback called when user clicks confirm */
|
||||||
|
handleConfirm?: () => void;
|
||||||
|
/** Optionally override confirm button text */
|
||||||
|
confirm?: string | JSX.Element;
|
||||||
|
/** Optionally require user to type this string to enable the confirm button */
|
||||||
|
confirmationCommand?: string;
|
||||||
|
/** Optionally set confirm button color */
|
||||||
|
confirmColor?: ButtonProps["color"];
|
||||||
|
|
||||||
|
/** Callback called when user clicks cancel */
|
||||||
|
handleCancel?: () => void;
|
||||||
|
/** Optionally override cancel button text */
|
||||||
|
cancel?: string;
|
||||||
|
/** Optionally hide cancel button */
|
||||||
|
hideCancel?: boolean;
|
||||||
|
|
||||||
|
/** Optionally set dialog max width */
|
||||||
|
maxWidth?: DialogProps["maxWidth"];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Open a confirm dialog
|
||||||
|
*
|
||||||
|
* @example Basic usage:
|
||||||
|
* ```
|
||||||
|
* const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||||
|
* confirm({ handleConfirm: () => ... });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const confirmDialogAtom = atom(
|
||||||
|
{ open: false } as ConfirmDialogProps,
|
||||||
|
(get, set, update: Partial<ConfirmDialogProps>) => {
|
||||||
|
set(confirmDialogAtom, {
|
||||||
|
...get(confirmDialogAtom),
|
||||||
|
open: true, // Don’t require this to be set explicitly
|
||||||
|
...update,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type RowyRunModalState = {
|
||||||
|
open: boolean;
|
||||||
|
feature: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Open global Rowy Run modal if feature not available.
|
||||||
|
* Calling the set function resets props.
|
||||||
|
*
|
||||||
|
* @example Basic usage:
|
||||||
|
* ```
|
||||||
|
* const openRowyRunModal = useSetAtom(rowyRunModalAtom, globalScope);
|
||||||
|
* openRowyRunModal({ feature: ... , version: ... });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example Close dialog:
|
||||||
|
* ```
|
||||||
|
* openRowyRunModal({ open: false })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const rowyRunModalAtom = atom(
|
||||||
|
{ open: false, feature: "", version: "" } as RowyRunModalState,
|
||||||
|
(_, set, update?: Partial<RowyRunModalState>) => {
|
||||||
|
set(rowyRunModalAtom, {
|
||||||
|
open: true,
|
||||||
|
feature: "",
|
||||||
|
version: "",
|
||||||
|
...update,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TableSettingsDialogState = {
|
||||||
|
open: boolean;
|
||||||
|
mode: "create" | "update";
|
||||||
|
data: TableSettings | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Open table settings dialog.
|
||||||
|
* Calling the set function resets props.
|
||||||
|
*
|
||||||
|
* @example Basic usage:
|
||||||
|
* ```
|
||||||
|
* const openTableSettingsDialog = useSetAtom(tableSettingsDialogAtom, globalScope);
|
||||||
|
* openTableSettingsDialog({ data: ... });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example Clear dialog:
|
||||||
|
* ```
|
||||||
|
* openTableSettingsDialog({ open: false })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const tableSettingsDialogAtom = atom(
|
||||||
|
{ open: false, mode: "create", data: null } as TableSettingsDialogState,
|
||||||
|
(_, set, update?: Partial<TableSettingsDialogState>) => {
|
||||||
|
set(tableSettingsDialogAtom, {
|
||||||
|
open: true,
|
||||||
|
mode: "create",
|
||||||
|
data: null,
|
||||||
|
...update,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
80
src/atoms/globalScope/user.ts
Normal file
80
src/atoms/globalScope/user.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
import { merge } from "lodash-es";
|
||||||
|
import { ThemeOptions } from "@mui/material";
|
||||||
|
|
||||||
|
import themes from "@src/theme";
|
||||||
|
import { publicSettingsAtom } from "./project";
|
||||||
|
import { UpdateDocFunction, TableFilter } from "@src/types/table";
|
||||||
|
|
||||||
|
/** User info and settings */
|
||||||
|
export type UserSettings = Partial<{
|
||||||
|
_rowy_id: string;
|
||||||
|
/** Synced from user auth info */
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
displayName?: string;
|
||||||
|
photoURL?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
};
|
||||||
|
roles: string[];
|
||||||
|
|
||||||
|
theme: Record<"base" | "light" | "dark", ThemeOptions>;
|
||||||
|
|
||||||
|
favoriteTables: string[];
|
||||||
|
/** Stores user overrides */
|
||||||
|
tables: Record<
|
||||||
|
string,
|
||||||
|
Partial<{
|
||||||
|
filters: TableFilter[];
|
||||||
|
hiddenFields: string[];
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
}>;
|
||||||
|
/** User info and settings */
|
||||||
|
export const userSettingsAtom = atom<UserSettings>({});
|
||||||
|
/** Stores a function that updates user settings */
|
||||||
|
export const updateUserSettingsAtom =
|
||||||
|
atom<UpdateDocFunction<UserSettings> | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores which theme is currently active, based on user or OS setting.
|
||||||
|
* Saved in localStorage.
|
||||||
|
*/
|
||||||
|
export const themeAtom = atomWithStorage<"light" | "dark">(
|
||||||
|
"__ROWY__THEME",
|
||||||
|
"light"
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* User can override OS theme. Saved in localStorage.
|
||||||
|
*/
|
||||||
|
export const themeOverriddenAtom = atomWithStorage(
|
||||||
|
"__ROWY__THEME_OVERRIDDEN",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Customized base theme based on project and user settings */
|
||||||
|
export const customizedThemesAtom = atom((get) => {
|
||||||
|
const publicSettings = get(publicSettingsAtom);
|
||||||
|
const userSettings = get(userSettingsAtom);
|
||||||
|
|
||||||
|
const lightCustomizations = merge(
|
||||||
|
{},
|
||||||
|
publicSettings.theme?.base,
|
||||||
|
publicSettings.theme?.light,
|
||||||
|
userSettings.theme?.base,
|
||||||
|
userSettings.theme?.light
|
||||||
|
);
|
||||||
|
const darkCustomizations = merge(
|
||||||
|
{},
|
||||||
|
publicSettings.theme?.base,
|
||||||
|
publicSettings.theme?.dark,
|
||||||
|
userSettings.theme?.base,
|
||||||
|
userSettings.theme?.dark
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
light: themes.light(lightCustomizations),
|
||||||
|
dark: themes.dark(darkCustomizations),
|
||||||
|
};
|
||||||
|
});
|
||||||
4
src/atoms/tableScope/index.ts
Normal file
4
src/atoms/tableScope/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/** Scope for atoms stored at the table level */
|
||||||
|
export const tableScope = Symbol("tableScope");
|
||||||
|
|
||||||
|
export * from "./table";
|
||||||
18
src/atoms/tableScope/table.ts
Normal file
18
src/atoms/tableScope/table.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import {
|
||||||
|
TableSettings,
|
||||||
|
TableSchema,
|
||||||
|
TableFilter,
|
||||||
|
TableOrder,
|
||||||
|
} from "@src/types/table";
|
||||||
|
|
||||||
|
export const tableIdAtom = atom<string | undefined>(undefined);
|
||||||
|
export const tableSettingsAtom = atom<TableSettings | undefined>(undefined);
|
||||||
|
export const tableSchemaAtom = atom<TableSchema | undefined>(undefined);
|
||||||
|
|
||||||
|
export const tableFiltersAtom = atom<TableFilter[]>([]);
|
||||||
|
export const tableOrdersAtom = atom<TableOrder[]>([]);
|
||||||
|
export const tablePageAtom = atom(0);
|
||||||
|
|
||||||
|
export const tableRowsAtom = atom<Record<string, any>[]>([]);
|
||||||
|
export const tableLoadingMoreAtom = atom(false);
|
||||||
93
src/components/AccessDenied.tsx
Normal file
93
src/components/AccessDenied.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
Avatar,
|
||||||
|
Alert,
|
||||||
|
Divider,
|
||||||
|
Link as MuiLink,
|
||||||
|
Button,
|
||||||
|
} from "@mui/material";
|
||||||
|
import SecurityIcon from "@mui/icons-material/SecurityOutlined";
|
||||||
|
|
||||||
|
import EmptyState from "@src/components/EmptyState";
|
||||||
|
|
||||||
|
import {
|
||||||
|
globalScope,
|
||||||
|
currentUserAtom,
|
||||||
|
userRolesAtom,
|
||||||
|
} from "@src/atoms/globalScope";
|
||||||
|
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||||
|
import { ROUTES } from "@src/constants/routes";
|
||||||
|
|
||||||
|
export default function AccessDenied() {
|
||||||
|
const [currentUser] = useAtom(currentUserAtom, globalScope);
|
||||||
|
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||||
|
|
||||||
|
if (!currentUser) window.location.reload();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
fullScreen
|
||||||
|
Icon={SecurityIcon}
|
||||||
|
message="Access denied"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<div style={{ textAlign: "left", width: "100%" }}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1.25}
|
||||||
|
alignItems="flex-start"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
<Avatar src={currentUser?.photoURL || ""} />
|
||||||
|
<div>
|
||||||
|
<Typography>{currentUser?.displayName}</Typography>
|
||||||
|
<Typography variant="button">{currentUser?.email}</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{(!userRoles || userRoles.length === 0) && (
|
||||||
|
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||||
|
Your account has no roles set
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography>
|
||||||
|
You do not have access to this project. Please contact the project
|
||||||
|
owner.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button href={ROUTES.signOut}>Sign out</Button>
|
||||||
|
|
||||||
|
<Divider flexItem sx={{ typography: "overline" }}>
|
||||||
|
OR
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<Typography>
|
||||||
|
If you are the project owner, please follow{" "}
|
||||||
|
<MuiLink
|
||||||
|
href={WIKI_LINKS.setupRoles}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
these instructions
|
||||||
|
</MuiLink>{" "}
|
||||||
|
to set up this project’s security rules.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
bgcolor: "background.default",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
import StyledFirebaseAuth from "react-firebaseui/StyledFirebaseAuth";
|
|
||||||
import { Props as FirebaseUiProps } from "react-firebaseui";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
import { alpha } from "@mui/material/styles";
|
|
||||||
import Skeleton from "@mui/material/Skeleton";
|
|
||||||
|
|
||||||
import { auth, db } from "@src/firebase";
|
|
||||||
import { defaultUiConfig, getSignInOptions } from "@src/firebase/firebaseui";
|
|
||||||
import { PUBLIC_SETTINGS } from "@src/config/dbPaths";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) =>
|
|
||||||
createStyles({
|
|
||||||
"@global": {
|
|
||||||
".rowy-firebaseui": {
|
|
||||||
width: "100%",
|
|
||||||
minHeight: 32,
|
|
||||||
|
|
||||||
"& .firebaseui-container": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
fontFamily: theme.typography.fontFamily,
|
|
||||||
},
|
|
||||||
"& .firebaseui-text": {
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontFamily: theme.typography.fontFamily,
|
|
||||||
},
|
|
||||||
"& .firebaseui-tos": {
|
|
||||||
...theme.typography.caption,
|
|
||||||
color: theme.palette.text.disabled,
|
|
||||||
},
|
|
||||||
"& .firebaseui-country-selector": {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
"& .firebaseui-title": {
|
|
||||||
...theme.typography.h5,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
"& .firebaseui-subtitle": {
|
|
||||||
...theme.typography.h6,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
"& .firebaseui-error": {
|
|
||||||
...theme.typography.caption,
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .firebaseui-card-content, & .firebaseui-card-footer": { padding: 0 },
|
|
||||||
"& .firebaseui-idp-list, & .firebaseui-tenant-list": { margin: 0 },
|
|
||||||
"& .firebaseui-idp-list>.firebaseui-list-item, & .firebaseui-tenant-list>.firebaseui-list-item":
|
|
||||||
{
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
"& .firebaseui-list-item + .firebaseui-list-item": {
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .mdl-button": {
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
...theme.typography.button,
|
|
||||||
},
|
|
||||||
"& .mdl-button--raised": {
|
|
||||||
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[2]}`,
|
|
||||||
"&:hover": {
|
|
||||||
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[4]}`,
|
|
||||||
},
|
|
||||||
"&:active, &:focus": {
|
|
||||||
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[8]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"& .mdl-card": {
|
|
||||||
boxShadow: "none",
|
|
||||||
minHeight: 0,
|
|
||||||
},
|
|
||||||
"& .mdl-button--primary.mdl-button--primary": {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
"& .mdl-button--raised.mdl-button--colored": {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
color: theme.palette.primary.contrastText,
|
|
||||||
|
|
||||||
"&:active, &:focus:not(:active), &:hover": {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .firebaseui-idp-button.mdl-button--raised, & .firebaseui-tenant-button.mdl-button--raised":
|
|
||||||
{
|
|
||||||
maxWidth: "none",
|
|
||||||
minHeight: 32,
|
|
||||||
padding: theme.spacing(0.5, 1),
|
|
||||||
|
|
||||||
backgroundColor: theme.palette.action.input + " !important",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: theme.palette.action.hover + " !important",
|
|
||||||
},
|
|
||||||
"&:active, &:focus": {
|
|
||||||
backgroundColor:
|
|
||||||
theme.palette.action.disabledBackground + " !important",
|
|
||||||
},
|
|
||||||
|
|
||||||
"&, &:hover, &.Mui-disabled": { border: "none" },
|
|
||||||
"&, &:hover, &:active, &:focus": {
|
|
||||||
boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset,
|
|
||||||
0 ${theme.palette.mode === "dark" ? "" : "-"}1px 0 0 ${
|
|
||||||
theme.palette.action.inputOutline
|
|
||||||
} inset`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"& .firebaseui-idp-icon": {
|
|
||||||
display: "block",
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
},
|
|
||||||
"& .firebaseui-idp-text": {
|
|
||||||
...theme.typography.button,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
paddingRight: Number(theme.spacing(2).replace("px", "")) + 18,
|
|
||||||
marginLeft: -18,
|
|
||||||
width: "100%",
|
|
||||||
textAlign: "center",
|
|
||||||
|
|
||||||
"&.firebaseui-idp-text-long": { display: "none" },
|
|
||||||
"&.firebaseui-idp-text-short": { display: "table-cell" },
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .firebaseui-idp-google > .firebaseui-idp-text": {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
"& .firebaseui-idp-github .firebaseui-idp-icon, & [data-provider-id='apple.com'] .firebaseui-idp-icon":
|
|
||||||
{
|
|
||||||
filter: theme.palette.mode === "dark" ? "invert(1)" : "",
|
|
||||||
},
|
|
||||||
"& [data-provider-id='microsoft.com'] .firebaseui-idp-icon": {
|
|
||||||
width: 21,
|
|
||||||
height: 21,
|
|
||||||
position: "relative",
|
|
||||||
left: -1,
|
|
||||||
top: -1,
|
|
||||||
},
|
|
||||||
"& [data-provider-id='yahoo.com'] > .firebaseui-idp-icon-wrapper > .firebaseui-idp-icon":
|
|
||||||
{
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
filter:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "invert(1) saturate(0) brightness(1.5)"
|
|
||||||
: "",
|
|
||||||
},
|
|
||||||
"& .firebaseui-idp-password .firebaseui-idp-icon, & .firebaseui-idp-phone .firebaseui-idp-icon, & .firebaseui-idp-anonymous .firebaseui-idp-icon":
|
|
||||||
{
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
position: "relative",
|
|
||||||
left: -2,
|
|
||||||
filter: theme.palette.mode === "light" ? "invert(1)" : "",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .firebaseui-card-header": { padding: 0 },
|
|
||||||
"& .firebaseui-card-actions": { padding: 0 },
|
|
||||||
|
|
||||||
"& .firebaseui-input, & .firebaseui-input-invalid": {
|
|
||||||
...theme.typography.body1,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
"& .firebaseui-textfield.mdl-textfield .firebaseui-input": {
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
"& .mdl-textfield.is-invalid .mdl-textfield__input": {
|
|
||||||
borderColor: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
"& .firebaseui-label": {
|
|
||||||
...theme.typography.subtitle2,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
"& .mdl-textfield--floating-label.is-dirty .mdl-textfield__label, .mdl-textfield--floating-label.is-focused .mdl-textfield__label":
|
|
||||||
{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
"& .firebaseui-textfield.mdl-textfield .firebaseui-label:after": {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
"& .mdl-textfield.is-invalid .mdl-textfield__label:after": {
|
|
||||||
backgroundColor: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .mdl-progress>.bufferbar": {
|
|
||||||
background: alpha(theme.palette.primary.main, 0.33),
|
|
||||||
},
|
|
||||||
"& .mdl-progress>.progressbar": {
|
|
||||||
backgroundColor: theme.palette.primary.main + " !important",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
signInText: {
|
|
||||||
display: "block",
|
|
||||||
textAlign: "center",
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
margin: theme.spacing(-1, 0, -3),
|
|
||||||
},
|
|
||||||
|
|
||||||
skeleton: {
|
|
||||||
width: "100%",
|
|
||||||
marginBottom: "calc(var(--spacing-contents) * -1)",
|
|
||||||
|
|
||||||
"& > *": {
|
|
||||||
width: "100%",
|
|
||||||
height: 32,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
},
|
|
||||||
|
|
||||||
"& > * + *": {
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function FirebaseUi(props: Partial<FirebaseUiProps>) {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const [signInOptions, setSignInOptions] = useState<
|
|
||||||
Parameters<typeof getSignInOptions>[0] | undefined
|
|
||||||
>();
|
|
||||||
useEffect(() => {
|
|
||||||
db.doc(PUBLIC_SETTINGS)
|
|
||||||
.get()
|
|
||||||
.then((doc) => {
|
|
||||||
const options = doc?.get("signInOptions");
|
|
||||||
if (!options) {
|
|
||||||
setSignInOptions(["google"]);
|
|
||||||
} else {
|
|
||||||
setSignInOptions(options);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => setSignInOptions(["google"]));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!signInOptions)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography variant="button" className={classes.signInText}>
|
|
||||||
Continue with
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<div id="rowy-firebaseui-skeleton" className={classes.skeleton}>
|
|
||||||
<Skeleton variant="rectangular" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const uiConfig: firebaseui.auth.Config = {
|
|
||||||
...defaultUiConfig,
|
|
||||||
...props.uiConfig,
|
|
||||||
callbacks: {
|
|
||||||
uiShown: () => {
|
|
||||||
const node = document.getElementById("rowy-firebaseui-skeleton");
|
|
||||||
if (node) node.style.display = "none";
|
|
||||||
},
|
|
||||||
...props.uiConfig?.callbacks,
|
|
||||||
},
|
|
||||||
signInOptions: getSignInOptions(signInOptions),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography variant="button" className={classes.signInText}>
|
|
||||||
Continue with
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<StyledFirebaseAuth
|
|
||||||
{...props}
|
|
||||||
firebaseAuth={auth}
|
|
||||||
uiConfig={uiConfig}
|
|
||||||
className={clsx("rowy-firebaseui", props.className)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
import { Button, ButtonProps } from "@mui/material";
|
|
||||||
import { alpha } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles((theme) =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
position: "relative",
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
active: {
|
|
||||||
color:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? theme.palette.primary.light
|
|
||||||
: theme.palette.primary.dark,
|
|
||||||
backgroundColor: alpha(
|
|
||||||
theme.palette.primary.main,
|
|
||||||
theme.palette.action.selectedOpacity
|
|
||||||
),
|
|
||||||
borderColor: theme.palette.primary.main,
|
|
||||||
|
|
||||||
"&:hover": {
|
|
||||||
color:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? theme.palette.primary.light
|
|
||||||
: theme.palette.primary.dark,
|
|
||||||
backgroundColor: alpha(
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? theme.palette.primary.light
|
|
||||||
: theme.palette.primary.dark,
|
|
||||||
theme.palette.action.selectedOpacity +
|
|
||||||
theme.palette.action.hoverOpacity
|
|
||||||
),
|
|
||||||
borderColor: "currentColor",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface IButtonWithStatusProps extends ButtonProps {
|
|
||||||
active?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ButtonWithStatus = React.forwardRef(function ButtonWithStatus_(
|
|
||||||
{ active = false, className, ...props }: IButtonWithStatusProps,
|
|
||||||
ref: React.Ref<HTMLButtonElement>
|
|
||||||
) {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
variant="outlined"
|
|
||||||
color={active ? "primary" : "secondary"}
|
|
||||||
className={clsx(classes.root, active && classes.active, className)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ButtonWithStatus;
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
import { Stack, Typography, Grid, Tooltip, IconButton } from "@mui/material";
|
import { Stack, Typography, Grid, Tooltip, IconButton } from "@mui/material";
|
||||||
import SecretsIcon from "@mui/icons-material/VpnKeyOutlined";
|
import SecretsIcon from "@mui/icons-material/VpnKeyOutlined";
|
||||||
import FunctionsIcon from "@mui/icons-material/CloudOutlined";
|
import FunctionsIcon from "@mui/icons-material/CloudOutlined";
|
||||||
import DocsIcon from "@mui/icons-material/DescriptionOutlined";
|
import DocsIcon from "@mui/icons-material/DescriptionOutlined";
|
||||||
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||||
|
|
||||||
export interface ICodeEditorHelperProps {
|
export interface ICodeEditorHelperProps {
|
||||||
docLink: string;
|
docLink: string;
|
||||||
@@ -17,7 +19,8 @@ export default function CodeEditorHelper({
|
|||||||
docLink,
|
docLink,
|
||||||
additionalVariables,
|
additionalVariables,
|
||||||
}: ICodeEditorHelperProps) {
|
}: ICodeEditorHelperProps) {
|
||||||
const { projectId } = useAppContext();
|
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||||
|
|
||||||
const availableVariables = [
|
const availableVariables = [
|
||||||
{
|
{
|
||||||
key: "row",
|
key: "row",
|
||||||
|
|||||||
1
src/components/CodeEditor/extensions.d.ts
vendored
1
src/components/CodeEditor/extensions.d.ts
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable tsdoc/syntax */
|
||||||
type Trigger = "create" | "update" | "delete";
|
type Trigger = "create" | "update" | "delete";
|
||||||
type Triggers = Trigger[];
|
type Triggers = Trigger[];
|
||||||
|
|
||||||
|
|||||||
1
src/components/CodeEditor/firebaseAuth.d.ts
vendored
1
src/components/CodeEditor/firebaseAuth.d.ts
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable tsdoc/syntax */
|
||||||
/*! firebase-admin v8.11.0 */
|
/*! firebase-admin v8.11.0 */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/ban-types */
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable tsdoc/syntax */
|
||||||
/* eslint-disable @typescript-eslint/ban-types */
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
|
|
||||||
// node_modules/@google-cloud/storage/build/src/bucket.d.ts
|
// node_modules/@google-cloud/storage/build/src/bucket.d.ts
|
||||||
|
|||||||
1
src/components/CodeEditor/firestore.d.ts
vendored
1
src/components/CodeEditor/firestore.d.ts
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable tsdoc/syntax */
|
||||||
/**
|
/**
|
||||||
* @fileoverview Firestore Server API.
|
* @fileoverview Firestore Server API.
|
||||||
*
|
*
|
||||||
|
|||||||
2
src/components/CodeEditor/index.ts
Normal file
2
src/components/CodeEditor/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./CodeEditor";
|
||||||
|
export { default } from "./CodeEditor";
|
||||||
@@ -13,8 +13,8 @@ import githubDarkTheme from "./github-dark-default.json";
|
|||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import type { SystemStyleObject, Theme } from "@mui/system";
|
import type { SystemStyleObject, Theme } from "@mui/system";
|
||||||
|
|
||||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
// TODO:
|
||||||
import { getColumnType, getFieldProp } from "@src/components/fields";
|
// import { getColumnType, getFieldProp } from "@src/components/fields";
|
||||||
|
|
||||||
/* eslint-disable import/no-webpack-loader-syntax */
|
/* eslint-disable import/no-webpack-loader-syntax */
|
||||||
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
|
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
|
||||||
@@ -23,7 +23,6 @@ import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts";
|
|||||||
import utilsDefs from "!!raw-loader!./utils.d.ts";
|
import utilsDefs from "!!raw-loader!./utils.d.ts";
|
||||||
import rowyUtilsDefs from "!!raw-loader!./rowy.d.ts";
|
import rowyUtilsDefs from "!!raw-loader!./rowy.d.ts";
|
||||||
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
||||||
import defaultValueDefs from "!!raw-loader!./defaultValue.d.ts";
|
|
||||||
import { runRoutes } from "@src/constants/runRoutes";
|
import { runRoutes } from "@src/constants/runRoutes";
|
||||||
|
|
||||||
export interface IUseMonacoCustomizationsProps {
|
export interface IUseMonacoCustomizationsProps {
|
||||||
@@ -54,7 +53,6 @@ export default function useMonacoCustomizations({
|
|||||||
fullScreen,
|
fullScreen,
|
||||||
}: IUseMonacoCustomizationsProps) {
|
}: IUseMonacoCustomizationsProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { tableState, rowyRun } = useProjectContext();
|
|
||||||
|
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
|
|
||||||
@@ -141,111 +139,115 @@ export default function useMonacoCustomizations({
|
|||||||
}
|
}
|
||||||
}, [monaco, stringifiedDiagnosticsOptions]);
|
}, [monaco, stringifiedDiagnosticsOptions]);
|
||||||
|
|
||||||
const addJsonFieldDefinition = async (columnKey, interfaceName) => {
|
// TODO:
|
||||||
const samples = tableState?.rows
|
// const addJsonFieldDefinition = async (columnKey, interfaceName) => {
|
||||||
.map((row) => row[columnKey])
|
// const samples = tableState?.rows
|
||||||
.filter((entry) => entry !== undefined)
|
// .map((row) => row[columnKey])
|
||||||
.map((entry) => JSON.stringify(entry));
|
// .filter((entry) => entry !== undefined)
|
||||||
if (!samples || samples.length === 0) {
|
// .map((entry) => JSON.stringify(entry));
|
||||||
monaco?.languages.typescript.javascriptDefaults.addExtraLib(
|
// if (!samples || samples.length === 0) {
|
||||||
`type ${interfaceName} = any;`
|
// monaco?.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
);
|
// `type ${interfaceName} = any;`
|
||||||
return;
|
// );
|
||||||
} else {
|
// return;
|
||||||
const jsonInput = jsonInputForTargetLanguage("typescript");
|
// } else {
|
||||||
await jsonInput.addSource({
|
// const jsonInput = jsonInputForTargetLanguage("typescript");
|
||||||
name: interfaceName,
|
// await jsonInput.addSource({
|
||||||
samples,
|
// name: interfaceName,
|
||||||
});
|
// samples,
|
||||||
|
// });
|
||||||
|
|
||||||
const inputData = new InputData();
|
// const inputData = new InputData();
|
||||||
inputData.addInput(jsonInput);
|
// inputData.addInput(jsonInput);
|
||||||
const result = await quicktype({
|
// const result = await quicktype({
|
||||||
inputData,
|
// inputData,
|
||||||
lang: "typescript",
|
// lang: "typescript",
|
||||||
rendererOptions: { "just-types": "true" },
|
// rendererOptions: { "just-types": "true" },
|
||||||
});
|
// });
|
||||||
const newLib = result.lines.join("\n").replaceAll("export ", "");
|
// const newLib = result.lines.join("\n").replaceAll("export ", "");
|
||||||
monaco?.languages.typescript.javascriptDefaults.addExtraLib(newLib);
|
// monaco?.languages.typescript.javascriptDefaults.addExtraLib(newLib);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const setSecrets = async (monaco, rowyRun) => {
|
// TODO: types
|
||||||
// set secret options
|
// const setSecrets = async (monaco, rowyRun) => {
|
||||||
try {
|
// // set secret options
|
||||||
const listSecrets = await rowyRun({
|
// try {
|
||||||
route: runRoutes.listSecrets,
|
// const listSecrets = await rowyRun({
|
||||||
});
|
// route: runRoutes.listSecrets,
|
||||||
const secretsDef = `type SecretNames = ${listSecrets
|
// });
|
||||||
.map((secret) => `"${secret}"`)
|
// const secretsDef = `type SecretNames = ${listSecrets
|
||||||
.join(" | ")}
|
// .map((secret) => `"${secret}"`)
|
||||||
enum secrets {
|
// .join(" | ")}
|
||||||
${listSecrets.map((secret) => `${secret} = "${secret}"`).join("\n")}
|
// enum secrets {
|
||||||
}
|
// ${listSecrets.map((secret) => `${secret} = "${secret}"`).join("\n")}
|
||||||
`;
|
// }
|
||||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(secretsDef);
|
// `;
|
||||||
} catch (error) {
|
// monaco.languages.typescript.javascriptDefaults.addExtraLib(secretsDef);
|
||||||
console.error("Could not set secret definitions: ", error);
|
// } catch (error) {
|
||||||
}
|
// console.error("Could not set secret definitions: ", error);
|
||||||
};
|
// }
|
||||||
const setBaseDefinitions = (monaco, columns) => {
|
// };
|
||||||
const rowDefinition =
|
// TODO: types
|
||||||
[
|
// const setBaseDefinitions = (monaco, columns) => {
|
||||||
Object.keys(columns).map((columnKey: string) => {
|
// const rowDefinition =
|
||||||
const column = columns[columnKey];
|
// [
|
||||||
const type = getColumnType(column);
|
// Object.keys(columns).map((columnKey: string) => {
|
||||||
if (type === "JSON") {
|
// const column = columns[columnKey];
|
||||||
const interfaceName =
|
// const type = getColumnType(column);
|
||||||
columnKey[0].toUpperCase() + columnKey.slice(1);
|
// if (type === "JSON") {
|
||||||
addJsonFieldDefinition(columnKey, interfaceName);
|
// const interfaceName =
|
||||||
const def = `static "${columnKey}": ${interfaceName}`;
|
// columnKey[0].toUpperCase() + columnKey.slice(1);
|
||||||
return def;
|
// addJsonFieldDefinition(columnKey, interfaceName);
|
||||||
}
|
// const def = `static "${columnKey}": ${interfaceName}`;
|
||||||
return `static "${columnKey}": ${getFieldProp("dataType", type)}`;
|
// return def;
|
||||||
}),
|
// }
|
||||||
].join(";\n") + ";";
|
// return `static "${columnKey}": ${getFieldProp("dataType", type)}`;
|
||||||
|
// }),
|
||||||
|
// ].join(";\n") + ";";
|
||||||
|
|
||||||
const availableFields = Object.keys(columns)
|
// const availableFields = Object.keys(columns)
|
||||||
.map((columnKey: string) => `"${columnKey}"`)
|
// .map((columnKey: string) => `"${columnKey}"`)
|
||||||
.join("|\n");
|
// .join("|\n");
|
||||||
|
|
||||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
// monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
["/**", " * extensions type configuration", " */", extensionsDefs].join(
|
// ["/**", " * extensions type configuration", " */", extensionsDefs].join(
|
||||||
"\n"
|
// "\n"
|
||||||
),
|
// ),
|
||||||
"ts:filename/extensions.d.ts"
|
// "ts:filename/extensions.d.ts"
|
||||||
);
|
// );
|
||||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
// monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
[
|
// [
|
||||||
"// basic types that are used in all places",
|
// "// basic types that are used in all places",
|
||||||
"declare var require: any;",
|
// "declare var require: any;",
|
||||||
"declare var Buffer: any;",
|
// "declare var Buffer: any;",
|
||||||
"const ref: FirebaseFirestore.DocumentReference;",
|
// "const ref: FirebaseFirestore.DocumentReference;",
|
||||||
"const storage: firebasestorage.Storage;",
|
// "const storage: firebasestorage.Storage;",
|
||||||
"const db: FirebaseFirestore.Firestore;",
|
// "const db: FirebaseFirestore.Firestore;",
|
||||||
"const auth: firebaseauth.BaseAuth;",
|
// "const auth: firebaseauth.BaseAuth;",
|
||||||
`type Row = {${rowDefinition}};`,
|
// `type Row = {${rowDefinition}};`,
|
||||||
`type Field = ${availableFields} | string | object;`,
|
// `type Field = ${availableFields} | string | object;`,
|
||||||
`type Fields = Field[];`,
|
// `type Fields = Field[];`,
|
||||||
].join("\n"),
|
// ].join("\n"),
|
||||||
"ts:filename/rowFields.d.ts"
|
// "ts:filename/rowFields.d.ts"
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
// TODO:
|
||||||
// Set row definitions
|
// Set row definitions
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (!monaco || !rowyRun || !tableState?.columns) return;
|
// if (!monaco || !rowyRun || !tableState?.columns) return;
|
||||||
try {
|
// try {
|
||||||
setBaseDefinitions(monaco, tableState.columns);
|
// setBaseDefinitions(monaco, tableState.columns);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error("Could not set basic", error);
|
// console.error("Could not set basic", error);
|
||||||
}
|
// }
|
||||||
// set available secrets from secretManager
|
// // set available secrets from secretManager
|
||||||
try {
|
// try {
|
||||||
setSecrets(monaco, rowyRun);
|
// setSecrets(monaco, rowyRun);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error("Could not set secrets: ", error);
|
// console.error("Could not set secrets: ", error);
|
||||||
}
|
// }
|
||||||
}, [monaco, tableState?.columns, rowyRun]);
|
// }, [monaco, tableState?.columns, rowyRun]);
|
||||||
|
|
||||||
let boxSx: SystemStyleObject<Theme> = {
|
let boxSx: SystemStyleObject<Theme> = {
|
||||||
minWidth: 400,
|
minWidth: 400,
|
||||||
|
|||||||
1
src/components/CodeEditor/utils.d.ts
vendored
1
src/components/CodeEditor/utils.d.ts
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/**
|
/**
|
||||||
* utility functions
|
* utility functions
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -11,22 +12,35 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
||||||
|
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
|
||||||
|
|
||||||
export default function Confirmation({
|
/**
|
||||||
title,
|
* Display a confirm dialog using `confirmDialogAtom` in `globalState`
|
||||||
customBody,
|
* {@link confirmDialogAtom | See usage example}
|
||||||
body,
|
*/
|
||||||
cancel,
|
export default function ConfirmDialog() {
|
||||||
hideCancel,
|
const [
|
||||||
confirm,
|
{
|
||||||
confirmationCommand,
|
|
||||||
handleConfirm,
|
|
||||||
handleCancel,
|
|
||||||
confirmColor,
|
|
||||||
open,
|
open,
|
||||||
handleClose,
|
|
||||||
|
title = "Are you sure?",
|
||||||
|
body,
|
||||||
|
|
||||||
|
handleConfirm,
|
||||||
|
confirm = "Confirm",
|
||||||
|
confirmationCommand,
|
||||||
|
confirmColor,
|
||||||
|
|
||||||
|
handleCancel,
|
||||||
|
cancel = "Cancel",
|
||||||
|
hideCancel,
|
||||||
|
|
||||||
maxWidth = "xs",
|
maxWidth = "xs",
|
||||||
}: any) {
|
},
|
||||||
|
setState,
|
||||||
|
] = useAtom(confirmDialogAtom, globalScope);
|
||||||
|
const handleClose = () => setState({ open: false });
|
||||||
|
|
||||||
const [dryText, setDryText] = useState("");
|
const [dryText, setDryText] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,11 +54,14 @@ export default function Confirmation({
|
|||||||
TransitionComponent={SlideTransitionMui}
|
TransitionComponent={SlideTransitionMui}
|
||||||
style={{ cursor: "default" }}
|
style={{ cursor: "default" }}
|
||||||
>
|
>
|
||||||
<DialogTitle>{title ?? "Are you sure?"}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{customBody}
|
{typeof body === "string" ? (
|
||||||
{body && <DialogContentText>{body}</DialogContentText>}
|
<DialogContentText>{body}</DialogContentText>
|
||||||
|
) : (
|
||||||
|
body
|
||||||
|
)}
|
||||||
{confirmationCommand && (
|
{confirmationCommand && (
|
||||||
<TextField
|
<TextField
|
||||||
value={dryText}
|
value={dryText}
|
||||||
@@ -67,12 +84,12 @@ export default function Confirmation({
|
|||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cancel ?? "Cancel"}
|
{cancel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleConfirm();
|
if (handleConfirm) handleConfirm();
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
color={confirmColor || "primary"}
|
color={confirmColor || "primary"}
|
||||||
@@ -82,7 +99,7 @@ export default function Confirmation({
|
|||||||
confirmationCommand ? dryText !== confirmationCommand : false
|
confirmationCommand ? dryText !== confirmationCommand : false
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{confirm ?? "Confirm"}
|
{confirm}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Dialog from "@mui/material/Dialog";
|
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
|
||||||
import DialogContentText from "@mui/material/DialogContentText";
|
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
},
|
|
||||||
dryWrapper: {},
|
|
||||||
dryField: {},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface IConfirmationProps {
|
|
||||||
children: JSX.Element;
|
|
||||||
message?: {
|
|
||||||
title?: string;
|
|
||||||
customBody?: string;
|
|
||||||
body?: string | React.ReactNode;
|
|
||||||
cancel?: string;
|
|
||||||
confirm?: string | JSX.Element;
|
|
||||||
color?: "error";
|
|
||||||
};
|
|
||||||
confirmationCommand?: string;
|
|
||||||
functionName?: string;
|
|
||||||
stopPropagation?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Confirmation({
|
|
||||||
children,
|
|
||||||
message,
|
|
||||||
confirmationCommand,
|
|
||||||
functionName = "onClick",
|
|
||||||
stopPropagation = false,
|
|
||||||
}: IConfirmationProps) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
|
||||||
const [dryText, setDryText] = useState("");
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setShowDialog(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmHandler = children.props[functionName];
|
|
||||||
const button = React.cloneElement(children, {
|
|
||||||
[functionName]: (e) => {
|
|
||||||
if (stopPropagation && e && e.stopPropagation) e.stopPropagation();
|
|
||||||
setShowDialog(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{button}
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={showDialog}
|
|
||||||
onClose={handleClose}
|
|
||||||
TransitionComponent={SlideTransitionMui}
|
|
||||||
maxWidth="xs"
|
|
||||||
>
|
|
||||||
<DialogTitle>
|
|
||||||
{(message && message.title) || "Are you sure?"}
|
|
||||||
</DialogTitle>
|
|
||||||
{message && (
|
|
||||||
<DialogContent>
|
|
||||||
{message.customBody}
|
|
||||||
{message.body &&
|
|
||||||
(typeof message.body === "string" ? (
|
|
||||||
<DialogContentText>{message.body}</DialogContentText>
|
|
||||||
) : (
|
|
||||||
message.body
|
|
||||||
))}
|
|
||||||
{confirmationCommand && (
|
|
||||||
<div className={classes.dryWrapper}>
|
|
||||||
<DialogContentText>
|
|
||||||
Type {confirmationCommand} below to continue:
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
|
||||||
value={dryText}
|
|
||||||
variant="filled"
|
|
||||||
onChange={(e) => {
|
|
||||||
setDryText(e.target.value);
|
|
||||||
}}
|
|
||||||
className={classes.dryField}
|
|
||||||
InputProps={{ disableUnderline: true }}
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
label={confirmationCommand}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
)}
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleClose}>
|
|
||||||
{(message && message.cancel) || "Cancel"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
confirmHandler();
|
|
||||||
handleClose();
|
|
||||||
}}
|
|
||||||
variant="contained"
|
|
||||||
color={message?.color || "primary"}
|
|
||||||
autoFocus
|
|
||||||
disabled={
|
|
||||||
confirmationCommand ? dryText !== confirmationCommand : false
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(message && message.confirm) || "Confirm"}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import React, { useContext } from "react";
|
|
||||||
import { IConfirmation, CONFIRMATION_EMPTY_STATE } from "./props";
|
|
||||||
const ConfirmationContext = React.createContext<IConfirmation>(
|
|
||||||
CONFIRMATION_EMPTY_STATE
|
|
||||||
);
|
|
||||||
export default ConfirmationContext;
|
|
||||||
|
|
||||||
export const useConfirmation = () => useContext(ConfirmationContext);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import { confirmationProps } from "./props";
|
|
||||||
import Dialog from "./Dialog";
|
|
||||||
import ConfirmationContext from "./Context";
|
|
||||||
interface IConfirmationProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfirmationProvider: React.FC<IConfirmationProviderProps> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [state, setState] = useState<confirmationProps>();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const handleClose = () => {
|
|
||||||
setOpen(false);
|
|
||||||
setTimeout(() => setState(undefined), 300);
|
|
||||||
};
|
|
||||||
const requestConfirmation = (props: confirmationProps) => {
|
|
||||||
setState(props);
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<ConfirmationContext.Provider
|
|
||||||
value={{
|
|
||||||
dialogProps: state,
|
|
||||||
open,
|
|
||||||
handleClose,
|
|
||||||
requestConfirmation,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Dialog {...state} open={open} handleClose={handleClose} />
|
|
||||||
</ConfirmationContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfirmationProvider;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { useConfirmation } from "./Context";
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
export type confirmationProps =
|
|
||||||
| {
|
|
||||||
title?: string;
|
|
||||||
customBody?: React.ReactNode;
|
|
||||||
body?: string;
|
|
||||||
cancel?: string;
|
|
||||||
hideCancel?: boolean;
|
|
||||||
confirm?: string | JSX.Element;
|
|
||||||
confirmationCommand?: string;
|
|
||||||
handleConfirm: () => void;
|
|
||||||
handleCancel?: () => void;
|
|
||||||
open?: Boolean;
|
|
||||||
confirmColor?: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
export interface IConfirmation {
|
|
||||||
dialogProps?: confirmationProps;
|
|
||||||
handleClose: () => void;
|
|
||||||
open: boolean;
|
|
||||||
requestConfirmation: (props: confirmationProps) => void;
|
|
||||||
}
|
|
||||||
export const CONFIRMATION_EMPTY_STATE = {
|
|
||||||
dialogProps: undefined,
|
|
||||||
open: false,
|
|
||||||
handleClose: () => {},
|
|
||||||
requestConfirmation: () => {},
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { Component } from "react";
|
|
||||||
import EmptyState, { IEmptyStateProps } from "./EmptyState";
|
|
||||||
|
|
||||||
import { Button } from "@mui/material";
|
|
||||||
import ReloadIcon from "@mui/icons-material/Refresh";
|
|
||||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
|
||||||
|
|
||||||
import meta from "@root/package.json";
|
|
||||||
|
|
||||||
class ErrorBoundary extends Component<
|
|
||||||
IEmptyStateProps & { render?: (errorMessage: string) => React.ReactNode }
|
|
||||||
> {
|
|
||||||
state = { hasError: false, errorMessage: "" };
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error) {
|
|
||||||
// Update state so the next render will show the fallback UI.
|
|
||||||
return { hasError: true, errorMessage: error.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: object) {
|
|
||||||
console.log(error, errorInfo);
|
|
||||||
// You can also log the error to an error reporting service
|
|
||||||
//logErrorToMyService(error, errorInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
if (this.props.render) return this.props.render(this.state.errorMessage);
|
|
||||||
|
|
||||||
if (this.state.errorMessage.startsWith("Loading chunk"))
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
Icon={ReloadIcon}
|
|
||||||
message="New update available"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<span>Reload this page to get the latest update</span>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<ReloadIcon />}
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
Reload
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
fullScreen
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
message="Something went wrong"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<span>{this.state.errorMessage}</span>
|
|
||||||
<Button
|
|
||||||
href={
|
|
||||||
meta.repository.url.replace(".git", "") + "/issues/new/choose"
|
|
||||||
}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Report issue
|
|
||||||
<InlineOpenInNewIcon />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
fullScreen
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary;
|
|
||||||
67
src/components/ErrorFallback.tsx
Normal file
67
src/components/ErrorFallback.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { FallbackProps } from "react-error-boundary";
|
||||||
|
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
import ReloadIcon from "@mui/icons-material/Refresh";
|
||||||
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||||
|
|
||||||
|
import EmptyState, { IEmptyStateProps } from "@src/components/EmptyState";
|
||||||
|
import AccessDenied from "@src/components/AccessDenied";
|
||||||
|
import meta from "@root/package.json";
|
||||||
|
|
||||||
|
export interface IErrorFallbackProps extends FallbackProps, IEmptyStateProps {}
|
||||||
|
|
||||||
|
export default function ErrorFallback({
|
||||||
|
error,
|
||||||
|
resetErrorBoundary,
|
||||||
|
...props
|
||||||
|
}: IErrorFallbackProps) {
|
||||||
|
if (error.message.startsWith("Loading chunk"))
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
Icon={ReloadIcon}
|
||||||
|
message="New update available"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<span>Reload this page to get the latest update</span>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<ReloadIcon />}
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
fullScreen
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((error as any).code === "permission-denied") return <AccessDenied />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
message="Something went wrong"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{(error as any).code && <b>{(error as any).code}: </b>}
|
||||||
|
{error.message}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
href={
|
||||||
|
meta.repository.url.replace(".git", "") + "/issues/new/choose"
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Report issue
|
||||||
|
<InlineOpenInNewIcon />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
fullScreen
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
272
src/components/FirebaseUi.tsx
Normal file
272
src/components/FirebaseUi.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { useMemo, useEffect } from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
import * as firebaseui from "firebaseui";
|
||||||
|
import "firebaseui/dist/firebaseui.css";
|
||||||
|
import { onAuthStateChanged } from "firebase/auth";
|
||||||
|
|
||||||
|
import { makeStyles } from "tss-react/mui";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
|
||||||
|
import { globalScope, publicSettingsAtom } from "@src/atoms/globalScope";
|
||||||
|
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
|
||||||
|
import { defaultUiConfig, getSignInOptions } from "@src/config/firebaseui";
|
||||||
|
|
||||||
|
const ELEMENT_ID = "firebaseui_container";
|
||||||
|
|
||||||
|
const useStyles = makeStyles()((theme) => ({
|
||||||
|
root: {
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 32,
|
||||||
|
|
||||||
|
"& .firebaseui-container": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
},
|
||||||
|
"& .firebaseui-text": {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
},
|
||||||
|
"& .firebaseui-tos": {
|
||||||
|
...(theme.typography.caption as any),
|
||||||
|
color: theme.palette.text.disabled,
|
||||||
|
},
|
||||||
|
"& .firebaseui-country-selector": {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
"& .firebaseui-title": {
|
||||||
|
...(theme.typography.h5 as any),
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
"& .firebaseui-subtitle": {
|
||||||
|
...(theme.typography.h6 as any),
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
"& .firebaseui-error": {
|
||||||
|
...(theme.typography.caption as any),
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
|
||||||
|
"& .firebaseui-card-content, & .firebaseui-card-footer": { padding: 0 },
|
||||||
|
"& .firebaseui-idp-list, & .firebaseui-tenant-list": { margin: 0 },
|
||||||
|
"& .firebaseui-idp-list>.firebaseui-list-item, & .firebaseui-tenant-list>.firebaseui-list-item":
|
||||||
|
{
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
"& .firebaseui-list-item + .firebaseui-list-item": {
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
|
||||||
|
"& .mdl-button": {
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
...(theme.typography.button as any),
|
||||||
|
},
|
||||||
|
"& .mdl-button--raised": {
|
||||||
|
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[2]}`,
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[4]}`,
|
||||||
|
},
|
||||||
|
"&:active, &:focus": {
|
||||||
|
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[8]}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .mdl-card": {
|
||||||
|
boxShadow: "none",
|
||||||
|
minHeight: 0,
|
||||||
|
},
|
||||||
|
"& .mdl-button--primary.mdl-button--primary": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
"& .mdl-button--raised.mdl-button--colored": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
|
||||||
|
"&:active, &:focus:not(:active), &:hover": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"& .firebaseui-idp-button.mdl-button--raised, & .firebaseui-tenant-button.mdl-button--raised":
|
||||||
|
{
|
||||||
|
maxWidth: "none",
|
||||||
|
minHeight: 32,
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
|
||||||
|
backgroundColor: theme.palette.action.input + " !important",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.action.hover + " !important",
|
||||||
|
},
|
||||||
|
"&:active, &:focus": {
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.action.disabledBackground + " !important",
|
||||||
|
},
|
||||||
|
|
||||||
|
"&, &:hover, &.Mui-disabled": { border: "none" },
|
||||||
|
"&, &:hover, &:active, &:focus": {
|
||||||
|
boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset,
|
||||||
|
0 ${theme.palette.mode === "dark" ? "" : "-"}1px 0 0 ${
|
||||||
|
theme.palette.action.inputOutline
|
||||||
|
} inset`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .firebaseui-idp-icon": {
|
||||||
|
display: "block",
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
"& .firebaseui-idp-text": {
|
||||||
|
...(theme.typography.button as any),
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
paddingRight: Number(theme.spacing(2).replace("px", "")) + 18,
|
||||||
|
marginLeft: -18,
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
|
||||||
|
"&.firebaseui-idp-text-long": { display: "none" },
|
||||||
|
"&.firebaseui-idp-text-short": { display: "table-cell" },
|
||||||
|
},
|
||||||
|
|
||||||
|
"& .firebaseui-idp-google > .firebaseui-idp-text": {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
"& .firebaseui-idp-github .firebaseui-idp-icon, & [data-provider-id='apple.com'] .firebaseui-idp-icon":
|
||||||
|
{
|
||||||
|
filter: theme.palette.mode === "dark" ? "invert(1)" : "",
|
||||||
|
},
|
||||||
|
"& [data-provider-id='microsoft.com'] .firebaseui-idp-icon": {
|
||||||
|
width: 21,
|
||||||
|
height: 21,
|
||||||
|
position: "relative",
|
||||||
|
left: -1,
|
||||||
|
top: -1,
|
||||||
|
},
|
||||||
|
"& [data-provider-id='yahoo.com'] > .firebaseui-idp-icon-wrapper > .firebaseui-idp-icon":
|
||||||
|
{
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
filter:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "invert(1) saturate(0) brightness(1.5)"
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
"& .firebaseui-idp-password .firebaseui-idp-icon, & .firebaseui-idp-phone .firebaseui-idp-icon, & .firebaseui-idp-anonymous .firebaseui-idp-icon":
|
||||||
|
{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
position: "relative",
|
||||||
|
left: -2,
|
||||||
|
filter: theme.palette.mode === "light" ? "invert(1)" : "",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& .firebaseui-card-header": { padding: 0 },
|
||||||
|
"& .firebaseui-card-actions": { padding: 0 },
|
||||||
|
|
||||||
|
"& .firebaseui-input, & .firebaseui-input-invalid": {
|
||||||
|
...(theme.typography.body1 as any),
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
"& .firebaseui-textfield.mdl-textfield .firebaseui-input": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
"& .mdl-textfield.is-invalid .mdl-textfield__input": {
|
||||||
|
borderColor: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
"& .firebaseui-label": {
|
||||||
|
...(theme.typography.subtitle2 as any),
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
"& .mdl-textfield--floating-label.is-dirty .mdl-textfield__label, .mdl-textfield--floating-label.is-focused .mdl-textfield__label":
|
||||||
|
{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
"& .firebaseui-textfield.mdl-textfield .firebaseui-label:after": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
"& .mdl-textfield.is-invalid .mdl-textfield__label:after": {
|
||||||
|
backgroundColor: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
|
||||||
|
"& .mdl-progress>.bufferbar": {
|
||||||
|
background: alpha(theme.palette.primary.main, 0.33),
|
||||||
|
},
|
||||||
|
"& .mdl-progress>.progressbar": {
|
||||||
|
backgroundColor: theme.palette.primary.main + " !important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface IFirebaseUiProps {
|
||||||
|
className?: string;
|
||||||
|
uiConfig?: firebaseui.auth.Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FirebaseUi(props: IFirebaseUiProps) {
|
||||||
|
const { classes, cx } = useStyles();
|
||||||
|
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
|
||||||
|
const [publicSettings] = useAtom(publicSettingsAtom, globalScope);
|
||||||
|
|
||||||
|
const signInOptions: typeof publicSettings.signInOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(publicSettings.signInOptions) &&
|
||||||
|
publicSettings.signInOptions.length > 0
|
||||||
|
? publicSettings.signInOptions
|
||||||
|
: ["google"],
|
||||||
|
[publicSettings.signInOptions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const uiConfig: firebaseui.auth.Config = useMemo(
|
||||||
|
() => ({
|
||||||
|
...defaultUiConfig,
|
||||||
|
...props.uiConfig,
|
||||||
|
signInOptions: getSignInOptions(signInOptions),
|
||||||
|
}),
|
||||||
|
[props.uiConfig, signInOptions]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let firebaseUiWidget: firebaseui.auth.AuthUI;
|
||||||
|
let userSignedIn = false;
|
||||||
|
let unregisterAuthObserver: ReturnType<typeof onAuthStateChanged>;
|
||||||
|
|
||||||
|
// Get or Create a firebaseUI instance.
|
||||||
|
firebaseUiWidget =
|
||||||
|
firebaseui.auth.AuthUI.getInstance() ||
|
||||||
|
new firebaseui.auth.AuthUI(firebaseAuth);
|
||||||
|
|
||||||
|
if (uiConfig.signInFlow === "popup") firebaseUiWidget.reset();
|
||||||
|
|
||||||
|
// We track the auth state to reset firebaseUi if the user signs out.
|
||||||
|
unregisterAuthObserver = onAuthStateChanged(firebaseAuth, (user) => {
|
||||||
|
if (!user && userSignedIn) firebaseUiWidget.reset();
|
||||||
|
userSignedIn = !!user;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render the firebaseUi Widget.
|
||||||
|
firebaseUiWidget.start("#" + ELEMENT_ID, uiConfig);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterAuthObserver();
|
||||||
|
firebaseUiWidget.reset();
|
||||||
|
};
|
||||||
|
}, [firebaseAuth, uiConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
variant="button"
|
||||||
|
display="block"
|
||||||
|
textAlign="center"
|
||||||
|
color="textSecondary"
|
||||||
|
sx={{ mt: -1, mb: -3 }}
|
||||||
|
>
|
||||||
|
Continue with
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div className={cx(classes.root, props.className)} id={ELEMENT_ID} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
|
||||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||||
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
|
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
|
||||||
|
|
||||||
export interface IFloatingSearchProps extends Partial<FilledTextFieldProps> {
|
export interface IFloatingSearchProps extends Partial<FilledTextFieldProps> {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -120,6 +120,12 @@ export default function FloatingSearch({
|
|||||||
}px)`,
|
}px)`,
|
||||||
left: (theme) => (theme.shape.borderRadius as number) * 2,
|
left: (theme) => (theme.shape.borderRadius as number) * 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
bgcolor: "transparent",
|
||||||
|
boxShadow: "none",
|
||||||
|
"& .MuiInputAdornment-root": { color: "text.disabled" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Chip, ChipProps } from "@mui/material";
|
|
||||||
|
|
||||||
export const VARIANTS = ["yes", "no", "maybe"];
|
|
||||||
const paletteColor = {
|
|
||||||
yes: "success",
|
|
||||||
maybe: "warning",
|
|
||||||
no: "error",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// TODO: Create a more generalised solution for this
|
|
||||||
export default function FormattedChip(props: ChipProps) {
|
|
||||||
const label =
|
|
||||||
typeof props.label === "string" ? props.label.toLowerCase() : "";
|
|
||||||
|
|
||||||
if (VARIANTS.includes(label)) {
|
|
||||||
return <Chip size="small" color={paletteColor[label]} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Chip size="small" {...props} />;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useTheme } from "@mui/material";
|
|
||||||
|
|
||||||
export interface IHelperTextProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HelperText(props: IHelperTextProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
style={{
|
|
||||||
marginTop: theme.spacing(-3),
|
|
||||||
padding: theme.spacing(0, 1.5),
|
|
||||||
...(theme.typography.body2 as any),
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Typography, Link as MuiLink, Button } from "@mui/material";
|
|
||||||
import SecurityIcon from "@mui/icons-material/SecurityOutlined";
|
|
||||||
|
|
||||||
import EmptyState from "@src/components/EmptyState";
|
|
||||||
|
|
||||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
|
||||||
import routes from "@src/constants/routes";
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
|
||||||
|
|
||||||
export default function AccessDenied() {
|
|
||||||
const { currentUser } = useAppContext();
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
fullScreen
|
|
||||||
Icon={SecurityIcon}
|
|
||||||
message="Access denied"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<Typography>
|
|
||||||
You are signed in as <strong>{currentUser?.email}</strong>
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
You do not have access to this project. Please contact the project
|
|
||||||
owner.
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
If you are the project owner, please follow{" "}
|
|
||||||
<MuiLink
|
|
||||||
href={WIKI_LINKS.setupRoles}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
these instructions
|
|
||||||
</MuiLink>{" "}
|
|
||||||
to set up this project’s security rules.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Button component={Link} to={routes.signOut}>
|
|
||||||
Sign out
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
position: "fixed",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
bgcolor: "background.default",
|
|
||||||
zIndex: 9999,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Container, Paper, Box, Grid } from "@mui/material";
|
|
||||||
|
|
||||||
import SectionHeadingSkeleton from "@src/components/SectionHeadingSkeleton";
|
|
||||||
import TableCardSkeleton from "./TableCardSkeleton";
|
|
||||||
|
|
||||||
export default function TableGridSkeleton() {
|
|
||||||
return (
|
|
||||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
height: 48,
|
|
||||||
maxWidth: (theme) => theme.breakpoints.values.sm - 48,
|
|
||||||
width: { xs: "100%", md: "50%", lg: "100%" },
|
|
||||||
mx: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box component="section" sx={{ mt: 4 }}>
|
|
||||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
|
||||||
<TableCardSkeleton />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
|
||||||
<TableCardSkeleton />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
|
||||||
<TableCardSkeleton />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
<Box component="section" sx={{ mt: 4 }}>
|
|
||||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
|
||||||
<TableCardSkeleton />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
|
||||||
<TableCardSkeleton />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
|
||||||
<TableCardSkeleton />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Container, Box, Paper } from "@mui/material";
|
|
||||||
|
|
||||||
import SectionHeadingSkeleton from "@src/components/SectionHeadingSkeleton";
|
|
||||||
import TableListItemSkeleton from "./TableListItemSkeleton";
|
|
||||||
|
|
||||||
export default function TableGridSkeleton() {
|
|
||||||
return (
|
|
||||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
height: 48,
|
|
||||||
maxWidth: (theme) => theme.breakpoints.values.sm - 48,
|
|
||||||
width: { xs: "100%", md: "50%", lg: "100%" },
|
|
||||||
mx: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box component="section" sx={{ mt: 4 }}>
|
|
||||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
|
||||||
<Paper>
|
|
||||||
<TableListItemSkeleton />
|
|
||||||
<TableListItemSkeleton />
|
|
||||||
<TableListItemSkeleton />
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
<Box component="section" sx={{ mt: 4 }}>
|
|
||||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
|
||||||
<Paper>
|
|
||||||
<TableListItemSkeleton />
|
|
||||||
<TableListItemSkeleton />
|
|
||||||
<TableListItemSkeleton />
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import _merge from "lodash/merge";
|
|
||||||
|
|
||||||
import { Tooltip, IconButton } from "@mui/material";
|
|
||||||
import { alpha } from "@mui/material/styles";
|
|
||||||
import InfoIcon from "@mui/icons-material/InfoOutlined";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
|
|
||||||
export interface IInfoTooltipProps {
|
|
||||||
description: React.ReactNode;
|
|
||||||
buttonLabel?: string;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
onClose?: () => void;
|
|
||||||
|
|
||||||
buttonProps?: Partial<React.ComponentProps<typeof IconButton>>;
|
|
||||||
tooltipProps?: Partial<React.ComponentProps<typeof Tooltip>>;
|
|
||||||
iconProps?: Partial<React.ComponentProps<typeof InfoIcon>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InfoTooltip({
|
|
||||||
description,
|
|
||||||
buttonLabel = "Info",
|
|
||||||
defaultOpen,
|
|
||||||
onClose,
|
|
||||||
|
|
||||||
buttonProps,
|
|
||||||
tooltipProps,
|
|
||||||
iconProps,
|
|
||||||
}: IInfoTooltipProps) {
|
|
||||||
const [open, setOpen] = useState(defaultOpen || false);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setOpen(false);
|
|
||||||
if (onClose) onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleOpen = () => {
|
|
||||||
if (open) {
|
|
||||||
setOpen(false);
|
|
||||||
if (onClose) onClose();
|
|
||||||
} else {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{description}
|
|
||||||
<IconButton
|
|
||||||
aria-label={`Close ${buttonLabel}`}
|
|
||||||
size="small"
|
|
||||||
onClick={handleClose}
|
|
||||||
sx={{
|
|
||||||
m: -0.5,
|
|
||||||
opacity: 0.8,
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: (theme) =>
|
|
||||||
alpha("#fff", theme.palette.action.hoverOpacity),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
disableFocusListener
|
|
||||||
disableHoverListener
|
|
||||||
disableTouchListener
|
|
||||||
arrow
|
|
||||||
placement="right-start"
|
|
||||||
describeChild
|
|
||||||
{...tooltipProps}
|
|
||||||
open={open}
|
|
||||||
componentsProps={_merge(
|
|
||||||
{
|
|
||||||
tooltip: {
|
|
||||||
style: {
|
|
||||||
marginLeft: "8px",
|
|
||||||
transformOrigin: "-8px 14px",
|
|
||||||
},
|
|
||||||
sx: {
|
|
||||||
typography: "body2",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
gap: 1.5,
|
|
||||||
alignItems: "flex-start",
|
|
||||||
pr: 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltipProps?.componentsProps
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
aria-label={buttonLabel}
|
|
||||||
size="small"
|
|
||||||
{...buttonProps}
|
|
||||||
onClick={toggleOpen}
|
|
||||||
>
|
|
||||||
{buttonProps?.children || <InfoIcon fontSize="small" {...iconProps} />}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
FormGroup,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
} from "@mui/material";
|
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
|
||||||
import RemoveIcon from "@mui/icons-material/DeleteOutline";
|
|
||||||
|
|
||||||
export interface IKeyValueInputProps {
|
|
||||||
value: Record<string, string>;
|
|
||||||
onChange: (value: Record<string, string>) => void;
|
|
||||||
label?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function KeyValueInput({
|
|
||||||
value: valueProp,
|
|
||||||
onChange,
|
|
||||||
label,
|
|
||||||
}: IKeyValueInputProps) {
|
|
||||||
const [value, setValue] = useState(
|
|
||||||
Object.keys(valueProp).length > 0
|
|
||||||
? Object.keys(valueProp)
|
|
||||||
.sort()
|
|
||||||
.map((key) => [key, valueProp[key]])
|
|
||||||
: [["", ""]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveValue = (v: typeof value) => {
|
|
||||||
onChange(
|
|
||||||
v.reduce((acc, [key, value]) => {
|
|
||||||
if (key.length > 0) acc[key] = value;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, string>)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = (i: number) => () =>
|
|
||||||
setValue((v) => {
|
|
||||||
const newValue = [...v];
|
|
||||||
newValue.splice(i + 1, 0, ["", ""]);
|
|
||||||
setTimeout(() =>
|
|
||||||
document.getElementById(`keyValue-${i + 1}-key`)?.focus()
|
|
||||||
);
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
const handleRemove = (i: number) => () =>
|
|
||||||
setValue((v) => {
|
|
||||||
const newValue = [...v];
|
|
||||||
newValue.splice(i, 1);
|
|
||||||
saveValue(newValue);
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChange =
|
|
||||||
(i: number, j: number) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setValue((v) => {
|
|
||||||
const newValue = [...v];
|
|
||||||
newValue[i][j] = e.target.value;
|
|
||||||
saveValue(newValue);
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl variant="filled" style={{ alignItems: "flex-start" }}>
|
|
||||||
<FormLabel
|
|
||||||
component="legend"
|
|
||||||
sx={{ typography: "button", color: "text.primary", mb: 0.25, ml: 0.25 }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
{value.map(([propKey, propValue], i) => (
|
|
||||||
<Stack
|
|
||||||
key={i}
|
|
||||||
direction="row"
|
|
||||||
alignItems="flex-start"
|
|
||||||
sx={{ "& + &": { mt: 1 } }}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
id={`keyValue-${i}-key`}
|
|
||||||
aria-label="Key"
|
|
||||||
placeholder="Key"
|
|
||||||
value={propKey}
|
|
||||||
sx={{
|
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
borderTopRightRadius: 0,
|
|
||||||
borderBottomRightRadius: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onChange={handleChange(i, 0)}
|
|
||||||
error={propKey.length === 0}
|
|
||||||
helperText={propKey.length === 0 ? "Required" : ""}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
id={`keyValue-${i}-value`}
|
|
||||||
aria-label="Value"
|
|
||||||
placeholder="Value"
|
|
||||||
value={propValue}
|
|
||||||
sx={{
|
|
||||||
ml: "-1px",
|
|
||||||
"& .MuiInputBase-root": {
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderBottomLeftRadius: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onChange={handleChange(i, 1)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleRemove(i)}
|
|
||||||
aria-label="Remove row"
|
|
||||||
sx={{ ml: 1, px: "0 !important", minWidth: 32 }}
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
<RemoveIcon />
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleAdd(value.length - 1)}
|
|
||||||
color="primary"
|
|
||||||
startIcon={<AddIcon />}
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
Add row
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,9 +20,9 @@ export interface IFullScreenModalProps
|
|||||||
disableEscapeKeyDown?: boolean;
|
disableEscapeKeyDown?: boolean;
|
||||||
|
|
||||||
"aria-labelledby": DialogProps["aria-labelledby"];
|
"aria-labelledby": DialogProps["aria-labelledby"];
|
||||||
header?: ReactNode;
|
header?: React.ReactNode;
|
||||||
children?: ReactNode;
|
children?: React.ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: React.ReactNode;
|
||||||
|
|
||||||
hideCloseButton?: boolean;
|
hideCloseButton?: boolean;
|
||||||
ScrollableDialogContentProps?: Partial<IScrollableDialogContentProps>;
|
ScrollableDialogContentProps?: Partial<IScrollableDialogContentProps>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useTheme,
|
useTheme,
|
||||||
@@ -26,12 +26,12 @@ export interface IModalProps extends Partial<Omit<DialogProps, "title">> {
|
|||||||
disableBackdropClick?: boolean;
|
disableBackdropClick?: boolean;
|
||||||
disableEscapeKeyDown?: boolean;
|
disableEscapeKeyDown?: boolean;
|
||||||
|
|
||||||
title: ReactNode;
|
title: React.ReactNode;
|
||||||
header?: ReactNode;
|
header?: React.ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: React.ReactNode;
|
||||||
|
|
||||||
children?: ReactNode;
|
children?: React.ReactNode;
|
||||||
body?: ReactNode;
|
body?: React.ReactNode;
|
||||||
|
|
||||||
actions?: {
|
actions?: {
|
||||||
primary?: Partial<LoadingButtonProps>;
|
primary?: Partial<LoadingButtonProps>;
|
||||||
@@ -94,7 +94,7 @@ export default function Modal({
|
|||||||
...props.sx,
|
...props.sx,
|
||||||
"& .MuiDialog-paper": {
|
"& .MuiDialog-paper": {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
...props.sx?.["& .MuiDialog-paper"],
|
...(props.sx as any)?.["& .MuiDialog-paper"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: props.sx
|
: props.sx
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import { forwardRef, cloneElement } from "react";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { Transition } from "react-transition-group";
|
import { Transition } from "react-transition-group";
|
||||||
import { TransitionProps } from "react-transition-group/Transition";
|
import { TransitionProps } from "react-transition-group/Transition";
|
||||||
@@ -6,7 +6,7 @@ import { TransitionProps as MuiTransitionProps } from "@mui/material/transitions
|
|||||||
|
|
||||||
export const SlideTransition: React.ForwardRefExoticComponent<
|
export const SlideTransition: React.ForwardRefExoticComponent<
|
||||||
Pick<TransitionProps, React.ReactText> & React.RefAttributes<any>
|
Pick<TransitionProps, React.ReactText> & React.RefAttributes<any>
|
||||||
> = React.forwardRef(
|
> = forwardRef(
|
||||||
({ children, ...props }: TransitionProps, ref: React.Ref<any>) => {
|
({ children, ...props }: TransitionProps, ref: React.Ref<any>) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@@ -57,8 +57,9 @@ export const SlideTransition: React.ForwardRefExoticComponent<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{(state) =>
|
{(state) =>
|
||||||
React.cloneElement(children as any, {
|
cloneElement(children as any, {
|
||||||
style: { ...defaultStyle, ...transitionStyles[state] },
|
style: { ...defaultStyle, ...transitionStyles[state] },
|
||||||
|
tabIndex: -1,
|
||||||
ref,
|
ref,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,7 @@ export const SlideTransition: React.ForwardRefExoticComponent<
|
|||||||
|
|
||||||
export default SlideTransition;
|
export default SlideTransition;
|
||||||
|
|
||||||
export const SlideTransitionMui = React.forwardRef(function Transition(
|
export const SlideTransitionMui = forwardRef(function Transition(
|
||||||
props: MuiTransitionProps & { children?: React.ReactElement<any, any> },
|
props: MuiTransitionProps & { children?: React.ReactElement<any, any> },
|
||||||
ref: React.Ref<unknown>
|
ref: React.Ref<unknown>
|
||||||
) {
|
) {
|
||||||
|
|||||||
2
src/components/Modal/index.ts
Normal file
2
src/components/Modal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./Modal";
|
||||||
|
export { default } from "./Modal";
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import _find from "lodash/find";
|
|
||||||
import queryString from "query-string";
|
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
|
||||||
import _camelCase from "lodash/camelCase";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { atomWithStorage } from "jotai/utils";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Breadcrumbs as MuiBreadcrumbs,
|
|
||||||
BreadcrumbsProps,
|
|
||||||
Link,
|
|
||||||
Typography,
|
|
||||||
Tooltip,
|
|
||||||
} from "@mui/material";
|
|
||||||
import ArrowRightIcon from "@mui/icons-material/ChevronRight";
|
|
||||||
import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined";
|
|
||||||
|
|
||||||
import InfoTooltip from "@src/components/InfoTooltip";
|
|
||||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
|
||||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
|
||||||
import useRouter from "@src/hooks/useRouter";
|
|
||||||
import routes from "@src/constants/routes";
|
|
||||||
|
|
||||||
const tableDescriptionDismissedAtom = atomWithStorage<string[]>(
|
|
||||||
"tableDescriptionDismissed",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Breadcrumbs({ sx = [], ...props }: BreadcrumbsProps) {
|
|
||||||
const { userClaims } = useAppContext();
|
|
||||||
const { tables, table, tableState } = useProjectContext();
|
|
||||||
const id = tableState?.config.id || "";
|
|
||||||
const collection = id || tableState?.tablePath || "";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
let parentLabel = decodeURIComponent(
|
|
||||||
queryString.parse(router.location.search).parentLabel as string
|
|
||||||
);
|
|
||||||
if (parentLabel === "undefined") parentLabel = "";
|
|
||||||
|
|
||||||
const breadcrumbs = collection.split("/");
|
|
||||||
|
|
||||||
const section = table?.section || "";
|
|
||||||
const getLabel = (id: string) => _find(tables, ["id", id])?.name || id;
|
|
||||||
|
|
||||||
const [dismissed, setDismissed] = useAtom(tableDescriptionDismissedAtom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MuiBreadcrumbs
|
|
||||||
separator={<ArrowRightIcon />}
|
|
||||||
aria-label="Sub-table breadcrumbs"
|
|
||||||
{...props}
|
|
||||||
sx={[
|
|
||||||
{
|
|
||||||
"& .MuiBreadcrumbs-ol": {
|
|
||||||
userSelect: "none",
|
|
||||||
flexWrap: "nowrap",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(Array.isArray(sx) ? sx : [sx]),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* Section name */}
|
|
||||||
{section && (
|
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to={`${routes.home}#${_camelCase(section)}`}
|
|
||||||
variant="h6"
|
|
||||||
color="textSecondary"
|
|
||||||
underline="hover"
|
|
||||||
>
|
|
||||||
{section}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{breadcrumbs.map((crumb: string, index) => {
|
|
||||||
// If it’s the first breadcrumb, show with specific style
|
|
||||||
const crumbProps = {
|
|
||||||
key: index,
|
|
||||||
variant: "h6",
|
|
||||||
component: index === 0 ? "h1" : "div",
|
|
||||||
color:
|
|
||||||
index === breadcrumbs.length - 1 ? "textPrimary" : "textSecondary",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// If it’s the last crumb, just show the label without linking
|
|
||||||
if (index === breadcrumbs.length - 1)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography {...crumbProps}>
|
|
||||||
{getLabel(crumb) || crumb.replace(/([A-Z])/g, " $1")}
|
|
||||||
</Typography>
|
|
||||||
{crumb === table?.id && table?.readOnly && (
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
userClaims?.roles.includes("ADMIN")
|
|
||||||
? "Table is read-only for non-ADMIN users"
|
|
||||||
: "Table is read-only"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ReadOnlyIcon fontSize="small" sx={{ ml: 0.5 }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{crumb === table?.id && table?.description && (
|
|
||||||
<InfoTooltip
|
|
||||||
description={
|
|
||||||
<div>
|
|
||||||
<RenderedMarkdown
|
|
||||||
children={table?.description}
|
|
||||||
restrictionPreset="singleLine"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
buttonLabel="Table info"
|
|
||||||
tooltipProps={{
|
|
||||||
componentsProps: {
|
|
||||||
popper: { sx: { zIndex: "appBar" } },
|
|
||||||
tooltip: { sx: { maxWidth: "75vw" } },
|
|
||||||
} as any,
|
|
||||||
}}
|
|
||||||
defaultOpen={!dismissed.includes(table?.id)}
|
|
||||||
onClose={() => setDismissed((d) => [...d, table?.id])}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If odd: breadcrumb points to a document — link to rowRef
|
|
||||||
// TODO: show a picker here to switch between sub tables
|
|
||||||
if (index % 2 === 1)
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
{...crumbProps}
|
|
||||||
component={RouterLink}
|
|
||||||
to={`${routes.table}/${encodeURIComponent(
|
|
||||||
breadcrumbs.slice(0, index).join("/")
|
|
||||||
)}?rowRef=${breadcrumbs.slice(0, index + 1).join("%2F")}`}
|
|
||||||
underline="hover"
|
|
||||||
>
|
|
||||||
{getLabel(
|
|
||||||
parentLabel.split(",")[Math.ceil(index / 2) - 1] || crumb
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Otherwise, even: breadcrumb points to a Firestore collection
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
{...crumbProps}
|
|
||||||
component={RouterLink}
|
|
||||||
to={`${routes.table}/${encodeURIComponent(
|
|
||||||
breadcrumbs.slice(0, index + 1).join("/")
|
|
||||||
)}`}
|
|
||||||
underline="hover"
|
|
||||||
>
|
|
||||||
{getLabel(crumb) || crumb.replace(/([A-Z])/g, " $1")}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</MuiBreadcrumbs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
import { List, ListItemText, Collapse } from "@mui/material";
|
|
||||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
|
||||||
import NavItem from "./NavItem";
|
|
||||||
|
|
||||||
import { Table } from "@src/contexts/ProjectContext";
|
|
||||||
import { routes } from "@src/constants/routes";
|
|
||||||
|
|
||||||
export interface INavDrawerItemProps {
|
|
||||||
open?: boolean;
|
|
||||||
section: string;
|
|
||||||
tables: Table[];
|
|
||||||
currentSection?: string;
|
|
||||||
closeDrawer?: (e: {}) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NavDrawerItem({
|
|
||||||
open: openProp,
|
|
||||||
section,
|
|
||||||
tables,
|
|
||||||
currentSection,
|
|
||||||
closeDrawer,
|
|
||||||
}: INavDrawerItemProps) {
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
const [open, setOpen] = useState(openProp || section === currentSection);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<NavItem
|
|
||||||
{...({ component: "button" } as any)}
|
|
||||||
selected={!open && currentSection === section}
|
|
||||||
onClick={() => setOpen((o) => !o)}
|
|
||||||
>
|
|
||||||
<ListItemText primary={section} style={{ textAlign: "left" }} />
|
|
||||||
|
|
||||||
<ArrowDropDownIcon
|
|
||||||
sx={{
|
|
||||||
color: "action.active",
|
|
||||||
transform: open ? "rotate(180deg)" : "rotate(0)",
|
|
||||||
transition: (theme) => theme.transitions.create("transform"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</NavItem>
|
|
||||||
|
|
||||||
<Collapse in={open}>
|
|
||||||
<List disablePadding>
|
|
||||||
{tables
|
|
||||||
.filter((x) => x)
|
|
||||||
.map((table) => {
|
|
||||||
const route =
|
|
||||||
table.tableType === "collectionGroup"
|
|
||||||
? `${routes.tableGroup}/${table.id}`
|
|
||||||
: `${routes.table}/${table.id.replace(/\//g, "~2F")}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={table.id}>
|
|
||||||
<NavItem
|
|
||||||
to={route}
|
|
||||||
selected={pathname.split("%2F")[0] === route}
|
|
||||||
onClick={closeDrawer}
|
|
||||||
sx={{
|
|
||||||
ml: 2,
|
|
||||||
width: (theme) =>
|
|
||||||
`calc(100% - ${theme.spacing(2 + 0.5)})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemText primary={table.name} />
|
|
||||||
</NavItem>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
Popover,
|
|
||||||
List,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItem,
|
|
||||||
Avatar,
|
|
||||||
ListItemText,
|
|
||||||
ListItemSecondaryAction,
|
|
||||||
Badge,
|
|
||||||
} from "@mui/material";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
|
|
||||||
import ErrorIcon from "@mui/icons-material/Error";
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
|
|
||||||
import BellIcon from "@mui/icons-material/Notifications";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) =>
|
|
||||||
createStyles({
|
|
||||||
typography: {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
type Notification = {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
link?: string;
|
|
||||||
variant: "error" | "success" | "info" | "warning";
|
|
||||||
};
|
|
||||||
|
|
||||||
const Notification = () => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const id = open ? "simple-popover" : undefined;
|
|
||||||
|
|
||||||
const notifications: Notification[] = [
|
|
||||||
{
|
|
||||||
title: "a",
|
|
||||||
subtitle: "a",
|
|
||||||
variant: "error",
|
|
||||||
link: "https://console.cloud.google.com/cloud-build/builds;region=global/ID",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const notificationsCount = notifications.length;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton onClick={handleClick}>
|
|
||||||
<Badge
|
|
||||||
color={"primary"}
|
|
||||||
variant="standard"
|
|
||||||
badgeContent={notificationsCount}
|
|
||||||
>
|
|
||||||
<BellIcon />
|
|
||||||
</Badge>
|
|
||||||
</IconButton>
|
|
||||||
<Popover
|
|
||||||
id={id}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={handleClose}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "center",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<List>
|
|
||||||
{notifications.map((notification) => (
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<ErrorIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={notification.title}
|
|
||||||
secondary={notification.subtitle}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<IconButton edge="end" aria-label="Delete">
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Notification;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import DOMPurify from "dompurify";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
maxWidth: "33em",
|
|
||||||
...theme.typography.body2,
|
|
||||||
|
|
||||||
"& * + *": {
|
|
||||||
marginTop: "1em !important",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
|
||||||
fontFamily: theme.typography.fontFamily,
|
|
||||||
margin: 0,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
"& p": {
|
|
||||||
margin: 0,
|
|
||||||
marginTop: "inherit",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& a": {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& ul, & ol": {
|
|
||||||
margin: 0,
|
|
||||||
paddingLeft: "1.5em",
|
|
||||||
},
|
|
||||||
"& li + li": {
|
|
||||||
marginTop: "0.5em",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& table": {
|
|
||||||
borderCollapse: "collapse",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& table th, & table td": {
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
padding: "0.4rem",
|
|
||||||
},
|
|
||||||
"& figure": {
|
|
||||||
display: "table",
|
|
||||||
margin: "1rem auto",
|
|
||||||
},
|
|
||||||
"& figure figcaption": {
|
|
||||||
color: "#999",
|
|
||||||
display: "block",
|
|
||||||
marginTop: "0.25rem",
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
"& hr": {
|
|
||||||
borderColor: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderWidth: "1px 0 0 0",
|
|
||||||
},
|
|
||||||
"& code": {
|
|
||||||
backgroundColor: "#e8e8e8",
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
padding: "0.1rem 0.2rem",
|
|
||||||
fontFamily: theme.typography.fontFamilyMono,
|
|
||||||
},
|
|
||||||
"& pre": {
|
|
||||||
fontFamily: theme.typography.fontFamilyMono,
|
|
||||||
},
|
|
||||||
'& .mceContent-body:not([dir="rtl"]) blockquote': {
|
|
||||||
borderLeft: `2px solid ${theme.palette.divider}`,
|
|
||||||
marginLeft: "1.5rem",
|
|
||||||
paddingLeft: "1rem",
|
|
||||||
},
|
|
||||||
'& .mceContent-body[dir="rtl"] blockquote': {
|
|
||||||
borderRight: `2px solid ${theme.palette.divider}`,
|
|
||||||
marginRight: "1.5rem",
|
|
||||||
paddingRight: "1rem",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface IRenderedHtmlProps
|
|
||||||
extends React.DetailedHTMLProps<
|
|
||||||
React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
HTMLDivElement
|
|
||||||
> {
|
|
||||||
html: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RenderedHtml({
|
|
||||||
html,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: IRenderedHtmlProps) {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(classes.root, className)}
|
|
||||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,12 @@ import remarkGfm from "remark-gfm";
|
|||||||
import { Typography, Link } from "@mui/material";
|
import { Typography, Link } from "@mui/material";
|
||||||
|
|
||||||
const remarkPlugins = [remarkGfm];
|
const remarkPlugins = [remarkGfm];
|
||||||
const components = {
|
const components: ReactMarkdownOptions["components"] = {
|
||||||
a: (props) => <Link color="inherit" {...props} />,
|
a: (props) => <Link color="inherit" {...props} />,
|
||||||
p: Typography,
|
p: Typography,
|
||||||
// eslint-disable-next-line jsx-a11y/alt-text
|
// eslint-disable-next-line jsx-a11y/alt-text
|
||||||
img: (props) => (
|
img: (props) => (
|
||||||
<img style={{ maxWidth: "100%", borderRadius: 4 }} {...props} />
|
<img style={{ maxWidth: "100%", borderRadius: 4 }} alt="" {...props} />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,7 +29,9 @@ export default function RenderedMarkdown({
|
|||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
{...props}
|
{...props}
|
||||||
allowedElements={restrictionPresets[restrictionPreset || ""]}
|
allowedElements={
|
||||||
|
restrictionPreset ? restrictionPresets[restrictionPreset] : undefined
|
||||||
|
}
|
||||||
unwrapDisallowed
|
unwrapDisallowed
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={remarkPlugins}
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
// TinyMCE so the global var exists
|
|
||||||
import "tinymce/tinymce.min.js";
|
|
||||||
// Theme
|
|
||||||
import "tinymce/themes/silver";
|
|
||||||
// Toolbar icons
|
|
||||||
import "tinymce/icons/default";
|
|
||||||
// Editor styles
|
|
||||||
import "tinymce/skins/ui/oxide/skin.min.css";
|
|
||||||
// Content styles, including inline UI like fake cursors
|
|
||||||
/* eslint import/no-webpack-loader-syntax: off */
|
|
||||||
import contentCss from "!!raw-loader!tinymce/skins/content/default/content.min.css";
|
|
||||||
import contentUiCss from "!!raw-loader!tinymce/skins/ui/oxide/content.min.css";
|
|
||||||
import contentCssDark from "!!raw-loader!tinymce/skins/content/dark/content.min.css";
|
|
||||||
import contentUiCssDark from "!!raw-loader!tinymce/skins/ui/oxide-dark/content.min.css";
|
|
||||||
|
|
||||||
// Plugins
|
|
||||||
import "tinymce/plugins/autoresize";
|
|
||||||
import "tinymce/plugins/lists";
|
|
||||||
import "tinymce/plugins/link";
|
|
||||||
import "tinymce/plugins/image";
|
|
||||||
import "tinymce/plugins/paste";
|
|
||||||
import "tinymce/plugins/help";
|
|
||||||
import "tinymce/plugins/code";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
import { useTheme } from "@mui/material";
|
|
||||||
const useStyles = makeStyles((theme) =>
|
|
||||||
createStyles({
|
|
||||||
"@global": {
|
|
||||||
body: {
|
|
||||||
fontFamily: theme.typography.fontFamily + " !important",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
root: {
|
|
||||||
"& .tox": {
|
|
||||||
"&.tox-tinymce": {
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
border: "none",
|
|
||||||
|
|
||||||
backgroundColor: theme.palette.action.input,
|
|
||||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset,
|
|
||||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
|
||||||
transition: theme.transitions.create("box-shadow", {
|
|
||||||
duration: theme.transitions.duration.short,
|
|
||||||
}),
|
|
||||||
|
|
||||||
"&:hover": {
|
|
||||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
|
|
||||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .tox-toolbar-overlord, & .tox-edit-area__iframe, & .tox-toolbar__primary":
|
|
||||||
{
|
|
||||||
background: "transparent",
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
},
|
|
||||||
"& .tox-edit-area__iframe": { colorScheme: "auto" },
|
|
||||||
|
|
||||||
"& .tox-toolbar__group": { border: "none !important" },
|
|
||||||
|
|
||||||
"& .tox-tbtn": {
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
cursor: "pointer",
|
|
||||||
margin: 0,
|
|
||||||
|
|
||||||
transition: theme.transitions.create(["color", "background-color"], {
|
|
||||||
duration: theme.transitions.duration.shortest,
|
|
||||||
}),
|
|
||||||
|
|
||||||
"&:hover": {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& svg": { fill: "currentColor" },
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .tox-tbtn--enabled, & .tox-tbtn--enabled:hover": {
|
|
||||||
backgroundColor: theme.palette.action.selected + " !important",
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
focus: {
|
|
||||||
"& .tox.tox-tinymce, & .tox.tox-tinymce:hover": {
|
|
||||||
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
|
|
||||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
disabled: {
|
|
||||||
"& .tox.tox-tinymce, & .tox.tox-tinymce:hover": {
|
|
||||||
backgroundColor:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "transparent"
|
|
||||||
: theme.palette.action.disabledBackground,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface IRichTextEditorProps {
|
|
||||||
value?: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RichTextEditor({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
disabled,
|
|
||||||
id,
|
|
||||||
}: IRichTextEditorProps) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const theme = useTheme();
|
|
||||||
const [focus, setFocus] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
classes.root,
|
|
||||||
focus && classes.focus,
|
|
||||||
disabled && classes.disabled
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
disabled={disabled}
|
|
||||||
init={{
|
|
||||||
skin: false,
|
|
||||||
content_css: false,
|
|
||||||
content_style: [
|
|
||||||
theme.palette.mode === "dark" ? contentCssDark : contentCss,
|
|
||||||
theme.palette.mode === "dark" ? contentUiCssDark : contentUiCss,
|
|
||||||
`
|
|
||||||
:root {
|
|
||||||
font-size: 14px;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body { background-color: transparent; }
|
|
||||||
body {
|
|
||||||
font-family: ${theme.typography.fontFamily};
|
|
||||||
color: ${theme.palette.text.primary};
|
|
||||||
caret-color: ${theme.palette.primary.main};
|
|
||||||
margin: ${theme.spacing(12 / 8)};
|
|
||||||
margin-top: ${theme.spacing(1)};
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
body :first-child { margin-top: 0; }
|
|
||||||
a { color: ${theme.palette.primary.main}; }
|
|
||||||
`,
|
|
||||||
].join("\n"),
|
|
||||||
minHeight: 300,
|
|
||||||
menubar: false,
|
|
||||||
plugins: ["autoresize", "lists link image", "paste help", "code"],
|
|
||||||
statusbar: false,
|
|
||||||
toolbar:
|
|
||||||
"formatselect | bold italic forecolor | link | bullist numlist outdent indent | removeformat code | help",
|
|
||||||
body_id: id,
|
|
||||||
}}
|
|
||||||
value={value}
|
|
||||||
onEditorChange={onChange}
|
|
||||||
onFocus={() => setFocus(true)}
|
|
||||||
onBlur={() => setFocus(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
import { makeStyles, createStyles } from "@mui/styles";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipProps,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
ButtonProps,
|
|
||||||
} from "@mui/material";
|
|
||||||
|
|
||||||
import { colord, extend } from "colord";
|
|
||||||
import mixPlugin from "colord/plugins/lch";
|
|
||||||
extend([mixPlugin]);
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) =>
|
|
||||||
createStyles({
|
|
||||||
popper: {
|
|
||||||
zIndex: theme.zIndex.drawer - 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor:
|
|
||||||
theme.palette.mode === "light"
|
|
||||||
? theme.palette.background.default
|
|
||||||
: colord(theme.palette.background.paper)
|
|
||||||
.mix("#fff", 0.16)
|
|
||||||
.toHslString(),
|
|
||||||
boxShadow: theme.shadows[8],
|
|
||||||
|
|
||||||
...theme.typography.body2,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
arrow: {
|
|
||||||
"&::before": {
|
|
||||||
backgroundColor:
|
|
||||||
theme.palette.mode === "light"
|
|
||||||
? theme.palette.background.default
|
|
||||||
: colord(theme.palette.background.paper)
|
|
||||||
.mix("#fff", 0.16)
|
|
||||||
.toHslString(),
|
|
||||||
boxShadow: theme.shadows[8],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
grid: {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
cursor: "default",
|
|
||||||
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "48px auto",
|
|
||||||
gap: theme.spacing(1, 1.5),
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
marginTop: theme.spacing(-0.5),
|
|
||||||
fontSize: `${48 / 16}rem`,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
alignSelf: "center",
|
|
||||||
},
|
|
||||||
dismissButton: {
|
|
||||||
gridColumn: 2,
|
|
||||||
justifySelf: "flex-start",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface IRichTooltipProps
|
|
||||||
extends Partial<Omit<TooltipProps, "title">> {
|
|
||||||
render: (props: {
|
|
||||||
openTooltip: () => void;
|
|
||||||
closeTooltip: () => void;
|
|
||||||
toggleTooltip: () => void;
|
|
||||||
}) => TooltipProps["children"];
|
|
||||||
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
title: React.ReactNode;
|
|
||||||
message?: React.ReactNode;
|
|
||||||
dismissButtonText?: React.ReactNode;
|
|
||||||
dismissButtonProps?: Partial<ButtonProps>;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
onOpen?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
onToggle?: (state: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RichTooltip({
|
|
||||||
render,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
dismissButtonText,
|
|
||||||
dismissButtonProps,
|
|
||||||
defaultOpen,
|
|
||||||
onOpen,
|
|
||||||
onClose,
|
|
||||||
onToggle,
|
|
||||||
...props
|
|
||||||
}: IRichTooltipProps) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const [open, setOpen] = useState(defaultOpen || false);
|
|
||||||
|
|
||||||
const openTooltip = () => {
|
|
||||||
setOpen(true);
|
|
||||||
if (onOpen) onOpen();
|
|
||||||
};
|
|
||||||
const closeTooltip = () => {
|
|
||||||
setOpen(false);
|
|
||||||
if (onClose) onClose();
|
|
||||||
};
|
|
||||||
const toggleTooltip = () =>
|
|
||||||
setOpen((state) => {
|
|
||||||
if (onToggle) onToggle(!state);
|
|
||||||
return !state;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
disableFocusListener
|
|
||||||
disableHoverListener
|
|
||||||
disableTouchListener
|
|
||||||
arrow
|
|
||||||
open={open}
|
|
||||||
onClose={closeTooltip}
|
|
||||||
classes={{
|
|
||||||
popper: classes.popper,
|
|
||||||
tooltip: classes.tooltip,
|
|
||||||
arrow: classes.arrow,
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
<div className={classes.grid} onClick={closeTooltip}>
|
|
||||||
<span className={classes.icon}>{icon}</span>
|
|
||||||
|
|
||||||
<div className={classes.message}>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<Typography>{message}</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{dismissButtonText ? (
|
|
||||||
<Button
|
|
||||||
{...dismissButtonProps}
|
|
||||||
onClick={closeTooltip}
|
|
||||||
className={clsx(
|
|
||||||
classes.dismissButton,
|
|
||||||
dismissButtonProps?.className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{dismissButtonText}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.disabled"
|
|
||||||
className={classes.dismissButton}
|
|
||||||
>
|
|
||||||
Click to dismiss
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
PopperProps={{
|
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: "preventOverflow",
|
|
||||||
enabled: true,
|
|
||||||
options: {
|
|
||||||
altAxis: true,
|
|
||||||
altBoundary: true,
|
|
||||||
tether: false,
|
|
||||||
rootBoundary: "document",
|
|
||||||
padding: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{render({ openTooltip, closeTooltip, toggleTooltip })}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { rowyRunModalAtom } from "@src/atoms/RowyRunModal";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
@@ -13,23 +12,35 @@ import Modal from "@src/components/Modal";
|
|||||||
import Logo from "@src/assets/LogoRowyRun";
|
import Logo from "@src/assets/LogoRowyRun";
|
||||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||||
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import {
|
||||||
import { routes } from "@src/constants/routes";
|
globalScope,
|
||||||
|
userRolesAtom,
|
||||||
|
projectSettingsAtom,
|
||||||
|
rowyRunModalAtom,
|
||||||
|
} from "@src/atoms/globalScope";
|
||||||
|
import { ROUTES } from "@src/constants/routes";
|
||||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a modal asking the user to deploy or upgrade Rowy Run
|
||||||
|
* using `rowyRunModalAtom` in `globalState`
|
||||||
|
* {@link rowyRunModalAtom | See usage example}
|
||||||
|
*/
|
||||||
export default function RowyRunModal() {
|
export default function RowyRunModal() {
|
||||||
const { userRoles } = useAppContext();
|
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||||
const { settings } = useProjectContext();
|
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
|
||||||
|
const [rowyRunModal, setRowyRunModal] = useAtom(
|
||||||
|
rowyRunModalAtom,
|
||||||
|
globalScope
|
||||||
|
);
|
||||||
|
|
||||||
const [state, setState] = useAtom(rowyRunModalAtom);
|
const handleClose = () => setRowyRunModal({ ...rowyRunModal, open: false });
|
||||||
const handleClose = () => setState((s) => ({ ...s, open: false }));
|
|
||||||
|
|
||||||
const showUpdateModal = state.version && settings?.rowyRunUrl;
|
const showUpdateModal = rowyRunModal.version && projectSettings?.rowyRunUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={state.open}
|
open={rowyRunModal.open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
title={
|
title={
|
||||||
<Logo
|
<Logo
|
||||||
@@ -47,13 +58,13 @@ export default function RowyRunModal() {
|
|||||||
<>
|
<>
|
||||||
<Typography variant="h5" paragraph align="center">
|
<Typography variant="h5" paragraph align="center">
|
||||||
{showUpdateModal ? "Update" : "Set up"} Rowy Run to use{" "}
|
{showUpdateModal ? "Update" : "Set up"} Rowy Run to use{" "}
|
||||||
{state.feature || "this feature"}
|
{rowyRunModal.feature || "this feature"}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{showUpdateModal && (
|
{showUpdateModal && (
|
||||||
<DialogContentText variant="body1" paragraph textAlign="center">
|
<DialogContentText variant="body1" paragraph textAlign="center">
|
||||||
{state.feature || "This feature"} requires Rowy Run v
|
{rowyRunModal.feature || "This feature"} requires Rowy Run v
|
||||||
{state.version} or later.
|
{rowyRunModal.version} or later.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -73,7 +84,7 @@ export default function RowyRunModal() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to={routes.projectSettings + "#rowyRun"}
|
to={ROUTES.projectSettings + "#rowyRun"}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import _camelCase from "lodash/camelCase";
|
import { camelCase } from "lodash-es";
|
||||||
import { HashLink } from "react-router-hash-link";
|
import { HashLink } from "react-router-hash-link";
|
||||||
|
|
||||||
import { Stack, StackProps, Typography, IconButton } from "@mui/material";
|
import { Stack, StackProps, Typography, IconButton } from "@mui/material";
|
||||||
import LinkIcon from "@mui/icons-material/Link";
|
import LinkIcon from "@mui/icons-material/Link";
|
||||||
|
|
||||||
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
|
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
|
||||||
|
|
||||||
export interface ISectionHeadingProps extends Omit<StackProps, "children"> {
|
export interface ISectionHeadingProps extends Omit<StackProps, "children"> {
|
||||||
children: string;
|
children: string;
|
||||||
@@ -15,7 +15,7 @@ export const SectionHeading = forwardRef(function SectionHeading_(
|
|||||||
{ children, sx, ...props }: ISectionHeadingProps,
|
{ children, sx, ...props }: ISectionHeadingProps,
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const sectionLink = _camelCase(children);
|
const sectionLink = camelCase(children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
import { Grid, Typography, Button, Link, Divider } from "@mui/material";
|
import { Grid, Typography, Button, Link, Divider } from "@mui/material";
|
||||||
import LoadingButton from "@mui/lab/LoadingButton";
|
import LoadingButton from "@mui/lab/LoadingButton";
|
||||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||||
@@ -7,13 +9,13 @@ import TwitterIcon from "@mui/icons-material/Twitter";
|
|||||||
import Logo from "@src/assets/Logo";
|
import Logo from "@src/assets/Logo";
|
||||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||||
|
|
||||||
import { version } from "@root/package.json";
|
import meta from "@root/package.json";
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const { projectId } = useAppContext();
|
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||||
|
|
||||||
const [latestUpdate, checkForUpdates, loading] = useUpdateCheck();
|
const [latestUpdate, checkForUpdates, loading] = useUpdateCheck();
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ export default function About() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Typography display="block" color="textSecondary">
|
<Typography display="block" color="textSecondary">
|
||||||
Rowy v{version}
|
Rowy v{meta.version}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { authOptions } from "firebase/firebaseui";
|
import { startCase } from "lodash-es";
|
||||||
import _startCase from "lodash/startCase";
|
|
||||||
|
|
||||||
import MultiSelect from "@rowy/multiselect";
|
import MultiSelect from "@rowy/multiselect";
|
||||||
import { Typography, Link } from "@mui/material";
|
import { Typography, Link } from "@mui/material";
|
||||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||||
|
|
||||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||||
|
import { authOptions } from "@src/config/firebaseui";
|
||||||
|
|
||||||
export default function Authentication({
|
export default function Authentication({
|
||||||
publicSettings,
|
publicSettings,
|
||||||
@@ -25,7 +25,7 @@ export default function Authentication({
|
|||||||
value={signInOptions}
|
value={signInOptions}
|
||||||
options={Object.keys(authOptions).map((option) => ({
|
options={Object.keys(authOptions).map((option) => ({
|
||||||
value: option,
|
value: option,
|
||||||
label: _startCase(option).replace("Github", "GitHub"),
|
label: startCase(option).replace("Github", "GitHub"),
|
||||||
}))}
|
}))}
|
||||||
onChange={setSignInOptions}
|
onChange={setSignInOptions}
|
||||||
onClose={() => updatePublicSettings({ signInOptions })}
|
onClose={() => updatePublicSettings({ signInOptions })}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { lazy, Suspense, useState } from "react";
|
import { lazy, Suspense, useState } from "react";
|
||||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||||
import _merge from "lodash/merge";
|
import { merge, unset } from "lodash-es";
|
||||||
import _unset from "lodash/unset";
|
|
||||||
|
|
||||||
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
||||||
import Loading from "@src/components/Loading";
|
import Loading from "@src/components/Loading";
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const ThemeColorPicker = lazy(() => import("@src/components/Settings/ThemeColorPicker") /* webpackChunkName: "Settings/ThemeColorPicker" */);
|
const ThemeColorPicker = lazy(() => import("@src/components/Settings/ThemeColorPicker") /* webpackChunkName: "ThemeColorPicker" */);
|
||||||
|
|
||||||
export default function Customization({
|
export default function Customization({
|
||||||
publicSettings,
|
publicSettings,
|
||||||
@@ -20,7 +19,7 @@ export default function Customization({
|
|||||||
|
|
||||||
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
|
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
|
||||||
updatePublicSettings({
|
updatePublicSettings({
|
||||||
theme: _merge(publicSettings.theme, {
|
theme: merge(publicSettings.theme, {
|
||||||
light: { palette: { primary: { main: light } } },
|
light: { palette: { primary: { main: light } } },
|
||||||
dark: { palette: { primary: { main: dark } } },
|
dark: { palette: { primary: { main: dark } } },
|
||||||
}),
|
}),
|
||||||
@@ -37,8 +36,8 @@ export default function Customization({
|
|||||||
setCustomizedThemeColor(e.target.checked);
|
setCustomizedThemeColor(e.target.checked);
|
||||||
if (!e.target.checked) {
|
if (!e.target.checked) {
|
||||||
const newTheme = publicSettings.theme;
|
const newTheme = publicSettings.theme;
|
||||||
_unset(newTheme, "light.palette.primary.main");
|
unset(newTheme, "light.palette.primary.main");
|
||||||
_unset(newTheme, "dark.palette.primary.main");
|
unset(newTheme, "dark.palette.primary.main");
|
||||||
updatePublicSettings({ theme: newTheme });
|
updatePublicSettings({ theme: newTheme });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|||||||
|
|
||||||
import LogoRowyRun from "@src/assets/LogoRowyRun";
|
import LogoRowyRun from "@src/assets/LogoRowyRun";
|
||||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||||
import { runRoutes } from "@src/constants/runRoutes";
|
import { runRoutes } from "@src/constants/runRoutes";
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function ThemeColorPicker({
|
|||||||
width={244}
|
width={244}
|
||||||
height={140}
|
height={140}
|
||||||
color={toColor("hex", light)}
|
color={toColor("hex", light)}
|
||||||
onChange={(c: any) => setLight(c.hex)}
|
onChange={(c) => setLight(c.hex)}
|
||||||
dark={theme.palette.mode === "dark"}
|
dark={theme.palette.mode === "dark"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
|
|
||||||
@@ -14,15 +15,22 @@ import AddIcon from "@mui/icons-material/PersonAddOutlined";
|
|||||||
import MultiSelect from "@rowy/multiselect";
|
import MultiSelect from "@rowy/multiselect";
|
||||||
import Modal from "@src/components/Modal";
|
import Modal from "@src/components/Modal";
|
||||||
|
|
||||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
import {
|
||||||
import routes from "@src/constants/routes";
|
globalScope,
|
||||||
|
rolesAtom,
|
||||||
|
projectSettingsAtom,
|
||||||
|
rowyRunAtom,
|
||||||
|
rowyRunModalAtom,
|
||||||
|
} from "@src/atoms/globalScope";
|
||||||
|
import { ROUTES } from "@src/constants/routes";
|
||||||
import { runRoutes } from "@src/constants/runRoutes";
|
import { runRoutes } from "@src/constants/runRoutes";
|
||||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
|
||||||
|
|
||||||
export default function InviteUser() {
|
export default function InviteUser() {
|
||||||
const { roles: projectRoles, rowyRun, settings } = useProjectContext();
|
const [projectRoles] = useAtom(rolesAtom, globalScope);
|
||||||
|
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
|
||||||
|
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
|
||||||
|
const openRowyRunModal = useSetAtom(rowyRunModalAtom, globalScope);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const openRowyRunModal = useRowyRunModal();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [status, setStatus] = useState<"LOADING" | string>("");
|
const [status, setStatus] = useState<"LOADING" | string>("");
|
||||||
@@ -33,7 +41,7 @@ export default function InviteUser() {
|
|||||||
const handleInvite = async () => {
|
const handleInvite = async () => {
|
||||||
try {
|
try {
|
||||||
setStatus("LOADING");
|
setStatus("LOADING");
|
||||||
const res = await rowyRun?.({
|
const res = await rowyRun({
|
||||||
route: runRoutes.inviteUser,
|
route: runRoutes.inviteUser,
|
||||||
body: { email, roles },
|
body: { email, roles },
|
||||||
});
|
});
|
||||||
@@ -52,9 +60,9 @@ export default function InviteUser() {
|
|||||||
<Button
|
<Button
|
||||||
aria-label="Invite user"
|
aria-label="Invite user"
|
||||||
onClick={
|
onClick={
|
||||||
settings?.rowyRunUrl
|
projectSettings.rowyRunUrl
|
||||||
? () => setOpen(true)
|
? () => setOpen(true)
|
||||||
: () => openRowyRunModal("Invite user")
|
: () => openRowyRunModal({ feature: "Invite user" })
|
||||||
}
|
}
|
||||||
variant="text"
|
variant="text"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -78,7 +86,7 @@ export default function InviteUser() {
|
|||||||
They can sign up with any of the sign-in options{" "}
|
They can sign up with any of the sign-in options{" "}
|
||||||
<MuiLink
|
<MuiLink
|
||||||
component={Link}
|
component={Link}
|
||||||
to={routes.projectSettings + "#authentication"}
|
to={ROUTES.projectSettings + "#authentication"}
|
||||||
>
|
>
|
||||||
you have enabled
|
you have enabled
|
||||||
</MuiLink>
|
</MuiLink>
|
||||||
@@ -104,7 +112,7 @@ export default function InviteUser() {
|
|||||||
TextFieldProps={{
|
TextFieldProps={{
|
||||||
id: "invite-roles",
|
id: "invite-roles",
|
||||||
SelectProps: {
|
SelectProps: {
|
||||||
renderValue: (_) => {
|
renderValue: () => {
|
||||||
if (Array.isArray(roles)) {
|
if (Array.isArray(roles)) {
|
||||||
if (roles.length >= 1) return roles.join(", ");
|
if (roles.length >= 1) return roles.join(", ");
|
||||||
return (
|
return (
|
||||||
@@ -113,6 +121,7 @@ export default function InviteUser() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sx: { mt: 3 },
|
sx: { mt: 3 },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -14,18 +15,33 @@ import CopyIcon from "@src/assets/icons/Copy";
|
|||||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||||
|
|
||||||
import MultiSelect from "@rowy/multiselect";
|
import MultiSelect from "@rowy/multiselect";
|
||||||
import { User } from "@src/pages/Settings/UserManagement";
|
|
||||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
import {
|
||||||
|
globalScope,
|
||||||
|
rolesAtom,
|
||||||
|
projectSettingsAtom,
|
||||||
|
rowyRunAtom,
|
||||||
|
rowyRunModalAtom,
|
||||||
|
UserSettings,
|
||||||
|
updateUserAtom,
|
||||||
|
confirmDialogAtom,
|
||||||
|
} from "@src/atoms/globalScope";
|
||||||
import { runRoutes } from "@src/constants/runRoutes";
|
import { runRoutes } from "@src/constants/runRoutes";
|
||||||
import { db } from "@src/firebase";
|
|
||||||
import { USERS } from "@src/config/dbPaths";
|
import { USERS } from "@src/config/dbPaths";
|
||||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
|
||||||
|
|
||||||
export default function UserItem({ id, user, roles: rolesProp }: User) {
|
export default function UserItem({
|
||||||
|
_rowy_id,
|
||||||
|
user,
|
||||||
|
roles: rolesProp,
|
||||||
|
}: UserSettings) {
|
||||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||||
const { requestConfirmation } = useConfirmation();
|
const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||||
|
const openRowyRunModal = useSetAtom(rowyRunModalAtom, globalScope);
|
||||||
|
|
||||||
const { roles: projectRoles, rowyRun } = useProjectContext();
|
const [projectRoles] = useAtom(rolesAtom, globalScope);
|
||||||
|
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
|
||||||
|
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
|
||||||
|
const [updateUser] = useAtom(updateUserAtom, globalScope);
|
||||||
|
|
||||||
const [value, setValue] = useState(Array.isArray(rolesProp) ? rolesProp : []);
|
const [value, setValue] = useState(Array.isArray(rolesProp) ? rolesProp : []);
|
||||||
const allRoles = new Set(["ADMIN", ...(projectRoles ?? []), ...value]);
|
const allRoles = new Set(["ADMIN", ...(projectRoles ?? []), ...value]);
|
||||||
@@ -42,7 +58,8 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
|||||||
body: { email: user!.email, roles: value },
|
body: { email: user!.email, roles: value },
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
await db.collection(USERS).doc(id).update({ roles: value });
|
if (!updateUser) throw new Error("Could not update user document");
|
||||||
|
await updateUser(_rowy_id!, { roles: value });
|
||||||
closeSnackbar(loadingSnackbarId);
|
closeSnackbar(loadingSnackbarId);
|
||||||
enqueueSnackbar(`Set roles for ${user!.email}: ${value.join(", ")}`);
|
enqueueSnackbar(`Set roles for ${user!.email}: ${value.join(", ")}`);
|
||||||
}
|
}
|
||||||
@@ -72,9 +89,15 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
requestConfirmation({
|
if (!projectSettings.rowyRunUrl) {
|
||||||
|
openRowyRunModal({ feature: "User Management" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm({
|
||||||
|
open: true,
|
||||||
title: "Delete user?",
|
title: "Delete user?",
|
||||||
customBody: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<ListItem children={listItemChildren} disablePadding sx={{ mb: 3 }} />
|
<ListItem children={listItemChildren} disablePadding sx={{ mb: 3 }} />
|
||||||
You will delete the user in Firebase Authentication and the
|
You will delete the user in Firebase Authentication and the
|
||||||
@@ -86,12 +109,12 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
|||||||
handleConfirm: async () => {
|
handleConfirm: async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
const loadingSnackbarId = enqueueSnackbar("Deleting user…");
|
const loadingSnackbarId = enqueueSnackbar("Deleting user…");
|
||||||
await rowyRun?.({
|
const response = await rowyRun({
|
||||||
route: runRoutes.deleteUser,
|
route: runRoutes.deleteUser,
|
||||||
body: { email: user.email },
|
body: { email: user.email },
|
||||||
});
|
});
|
||||||
closeSnackbar(loadingSnackbarId);
|
closeSnackbar(loadingSnackbarId);
|
||||||
enqueueSnackbar(`Deleted user: ${user.email}`);
|
if (response) enqueueSnackbar(`Deleted user: ${user.email}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -109,7 +132,7 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
|||||||
freeText
|
freeText
|
||||||
TextFieldProps={{
|
TextFieldProps={{
|
||||||
SelectProps: {
|
SelectProps: {
|
||||||
renderValue: (_) => {
|
renderValue: () => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
if (value.length === 1) return value[0];
|
if (value.length === 1) return value[0];
|
||||||
if (value.length > 1) return `${value.length} roles`;
|
if (value.length > 1) return `${value.length} roles`;
|
||||||
@@ -119,6 +142,7 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -155,9 +179,9 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Copy UID"
|
aria-label="Copy UID"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!id) return;
|
if (!_rowy_id) return;
|
||||||
await navigator.clipboard.writeText(id);
|
await navigator.clipboard.writeText(_rowy_id);
|
||||||
enqueueSnackbar(`Copied UID for ${user?.email}: ${id}`);
|
enqueueSnackbar(`Copied UID for ${user?.email}: ${_rowy_id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
|
|||||||
|
|
||||||
import { Grid, Avatar, Typography, Button } from "@mui/material";
|
import { Grid, Avatar, Typography, Button } from "@mui/material";
|
||||||
|
|
||||||
import routes from "@src/constants/routes";
|
import { ROUTES } from "@src/constants/routes";
|
||||||
|
|
||||||
export default function Account({ settings }: IUserSettingsChildProps) {
|
export default function Account({ settings }: IUserSettingsChildProps) {
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +26,7 @@ export default function Account({ settings }: IUserSettingsChildProps) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button component={Link} to={routes.signOut}>
|
<Button component={Link} to={ROUTES.signOut}>
|
||||||
Sign out
|
Sign out
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import { lazy, Suspense, useState } from "react";
|
import { lazy, Suspense, useState } from "react";
|
||||||
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
||||||
import _merge from "lodash/merge";
|
import { merge, unset } from "lodash-es";
|
||||||
import _unset from "lodash/unset";
|
|
||||||
|
|
||||||
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
||||||
import Loading from "@src/components/Loading";
|
import Loading from "@src/components/Loading";
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const ThemeColorPicker = lazy(() => import("@src/components/Settings/ThemeColorPicker") /* webpackChunkName: "Settings/ThemeColorPicker" */);
|
const ThemeColorPicker = lazy(() => import("@src/components/Settings/ThemeColorPicker") /* webpackChunkName: "ThemeColorPicker" */);
|
||||||
|
|
||||||
export default function Personalization({
|
export default function Personalization({
|
||||||
settings,
|
settings,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
}: IUserSettingsChildProps) {
|
}: IUserSettingsChildProps) {
|
||||||
const [customizedThemeColor, setCustomizedThemeColor] = useState(
|
const [customizedThemeColor, setCustomizedThemeColor] = useState(
|
||||||
|
Boolean(
|
||||||
settings.theme?.light?.palette?.primary?.main ||
|
settings.theme?.light?.palette?.primary?.main ||
|
||||||
settings.theme?.dark?.palette?.primary?.main
|
settings.theme?.dark?.palette?.primary?.main
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
|
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
theme: _merge(settings.theme, {
|
theme: merge(settings.theme, {
|
||||||
light: { palette: { primary: { main: light } } },
|
light: { palette: { primary: { main: light } } },
|
||||||
dark: { palette: { primary: { main: dark } } },
|
dark: { palette: { primary: { main: dark } } },
|
||||||
}),
|
}),
|
||||||
@@ -32,13 +33,13 @@ export default function Personalization({
|
|||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={customizedThemeColor}
|
defaultChecked={customizedThemeColor}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCustomizedThemeColor(e.target.checked);
|
setCustomizedThemeColor(e.target.checked);
|
||||||
if (!e.target.checked) {
|
if (!e.target.checked) {
|
||||||
const newTheme = settings.theme;
|
const newTheme = settings.theme;
|
||||||
_unset(newTheme, "light.palette.primary.main");
|
unset(newTheme, "light.palette.primary.main");
|
||||||
_unset(newTheme, "dark.palette.primary.main");
|
unset(newTheme, "dark.palette.primary.main");
|
||||||
updateSettings({ theme: newTheme });
|
updateSettings({ theme: newTheme });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -49,7 +50,7 @@ export default function Personalization({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
|
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading style={{ height: "auto" }} />}>
|
||||||
<ThemeColorPicker
|
<ThemeColorPicker
|
||||||
currentLight={settings.theme?.light?.palette?.primary?.main}
|
currentLight={settings.theme?.light?.palette?.primary?.main}
|
||||||
currentDark={settings.theme?.dark?.palette?.primary?.main}
|
currentDark={settings.theme?.dark?.palette?.primary?.main}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { merge } from "lodash-es";
|
||||||
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
||||||
import _merge from "lodash/merge";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -10,18 +11,29 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import {
|
||||||
|
globalScope,
|
||||||
|
themeAtom,
|
||||||
|
themeOverriddenAtom,
|
||||||
|
} from "@src/atoms/globalScope";
|
||||||
|
|
||||||
export default function Theme({
|
export default function Theme({
|
||||||
settings,
|
settings,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
}: IUserSettingsChildProps) {
|
}: IUserSettingsChildProps) {
|
||||||
const { theme, themeOverridden, setTheme, setThemeOverridden } =
|
const [theme, setTheme] = useAtom(themeAtom, globalScope);
|
||||||
useAppContext();
|
const [themeOverridden, setThemeOverridden] = useAtom(
|
||||||
|
themeOverriddenAtom,
|
||||||
|
globalScope
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormControl component="fieldset" variant="standard" sx={{ my: -10 / 8 }}>
|
<FormControl
|
||||||
|
component="fieldset"
|
||||||
|
variant="standard"
|
||||||
|
sx={{ my: -10 / 8, display: "flex" }}
|
||||||
|
>
|
||||||
<legend style={{ fontSize: 0 }}>Theme</legend>
|
<legend style={{ fontSize: 0 }}>Theme</legend>
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -50,10 +62,10 @@ export default function Theme({
|
|||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.theme?.dark?.palette?.darker}
|
defaultChecked={Boolean(settings.theme?.dark?.palette?.darker)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
theme: _merge(settings.theme, {
|
theme: merge(settings.theme, {
|
||||||
dark: { palette: { darker: e.target.checked } },
|
dark: { palette: { darker: e.target.checked } },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, createElement } from "react";
|
import React, { useState, createElement } from "react";
|
||||||
import { use100vh } from "react-div-100vh";
|
import { use100vh } from "react-div-100vh";
|
||||||
import { SwitchTransition } from "react-transition-group";
|
import { SwitchTransition } from "react-transition-group";
|
||||||
import type { ISetupStep } from "./types";
|
import type { ISetupStep } from "./SetupStep";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
@@ -25,7 +25,7 @@ import Logo from "@src/assets/Logo";
|
|||||||
import ScrollableDialogContent from "@src/components/Modal/ScrollableDialogContent";
|
import ScrollableDialogContent from "@src/components/Modal/ScrollableDialogContent";
|
||||||
import { SlideTransition } from "@src/components/Modal/SlideTransition";
|
import { SlideTransition } from "@src/components/Modal/SlideTransition";
|
||||||
|
|
||||||
import { analytics } from "analytics";
|
import { analytics, logEvent } from "@src/analytics";
|
||||||
|
|
||||||
const BASE_WIDTH = 1024;
|
const BASE_WIDTH = 1024;
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export default function SetupLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextStepId = steps[nextIncompleteStepIndex].id;
|
const nextStepId = steps[nextIncompleteStepIndex].id;
|
||||||
analytics.logEvent("setup_step", { step: nextStepId });
|
logEvent(analytics, "setup_step", { step: nextStepId });
|
||||||
setStepId(nextStepId);
|
setStepId(nextStepId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { signInWithPopup, GoogleAuthProvider, signOut } from "firebase/auth";
|
||||||
|
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
import LoadingButton, { LoadingButtonProps } from "@mui/lab/LoadingButton";
|
import LoadingButton, { LoadingButtonProps } from "@mui/lab/LoadingButton";
|
||||||
|
|
||||||
import { auth, googleProvider } from "@src/firebase";
|
import { globalScope } from "@src/atoms/globalScope";
|
||||||
|
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
|
||||||
|
|
||||||
|
const googleProvider = new GoogleAuthProvider();
|
||||||
|
googleProvider.setCustomParameters({ prompt: "select_account" });
|
||||||
|
|
||||||
export interface ISignInWithGoogleProps extends Partial<LoadingButtonProps> {
|
export interface ISignInWithGoogleProps extends Partial<LoadingButtonProps> {
|
||||||
matchEmail?: string;
|
matchEmail?: string;
|
||||||
@@ -13,12 +19,13 @@ export default function SignInWithGoogle({
|
|||||||
matchEmail,
|
matchEmail,
|
||||||
...props
|
...props
|
||||||
}: ISignInWithGoogleProps) {
|
}: ISignInWithGoogleProps) {
|
||||||
|
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
|
||||||
const [status, setStatus] = useState<"IDLE" | "LOADING" | string>("IDLE");
|
const [status, setStatus] = useState<"IDLE" | "LOADING" | string>("IDLE");
|
||||||
|
|
||||||
const handleSignIn = async () => {
|
const handleSignIn = async () => {
|
||||||
setStatus("LOADING");
|
setStatus("LOADING");
|
||||||
try {
|
try {
|
||||||
const result = await auth.signInWithPopup(googleProvider);
|
const result = await signInWithPopup(firebaseAuth, googleProvider);
|
||||||
if (!result.user) throw new Error("Missing user");
|
if (!result.user) throw new Error("Missing user");
|
||||||
if (
|
if (
|
||||||
matchEmail &&
|
matchEmail &&
|
||||||
@@ -28,7 +35,7 @@ export default function SignInWithGoogle({
|
|||||||
|
|
||||||
setStatus("IDLE");
|
setStatus("IDLE");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (auth.currentUser) auth.signOut();
|
if (firebaseAuth.currentUser) signOut(firebaseAuth);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
setStatus(error.message);
|
setStatus(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { ISetupStep } from "../types";
|
import { doc, updateDoc } from "firebase/firestore";
|
||||||
|
import type { ISetupStep } from "@src/components/Setup/SetupStep";
|
||||||
|
|
||||||
import { Typography, Stack, RadioGroup, Radio, Button } from "@mui/material";
|
import {
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupProps,
|
||||||
|
Radio,
|
||||||
|
Button,
|
||||||
|
} from "@mui/material";
|
||||||
import ThumbUpIcon from "@mui/icons-material/ThumbUpAlt";
|
import ThumbUpIcon from "@mui/icons-material/ThumbUpAlt";
|
||||||
import ThumbUpOffIcon from "@mui/icons-material/ThumbUpOffAlt";
|
import ThumbUpOffIcon from "@mui/icons-material/ThumbUpOffAlt";
|
||||||
import ThumbDownIcon from "@mui/icons-material/ThumbDownAlt";
|
import ThumbDownIcon from "@mui/icons-material/ThumbDownAlt";
|
||||||
import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
|
import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
|
||||||
|
|
||||||
import { analytics } from "analytics";
|
import { analytics, logEvent } from "@src/analytics";
|
||||||
import { db } from "@src/firebase";
|
import { globalScope } from "@src/atoms/globalScope";
|
||||||
import { routes } from "@src/constants/routes";
|
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||||
|
import { ROUTES } from "@src/constants/routes";
|
||||||
|
import { SETTINGS } from "config/dbPaths";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "finish",
|
id: "finish",
|
||||||
@@ -24,16 +35,17 @@ export default {
|
|||||||
} as ISetupStep;
|
} as ISetupStep;
|
||||||
|
|
||||||
function StepFinish() {
|
function StepFinish() {
|
||||||
|
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
db.doc("_rowy_/settings").update({ setupCompleted: true });
|
updateDoc(doc(firebaseDb, SETTINGS), { setupCompleted: true });
|
||||||
}, []);
|
}, [firebaseDb]);
|
||||||
const [rating, setRating] = useState<"up" | "down" | undefined>();
|
const [rating, setRating] = useState<"up" | "down" | undefined>();
|
||||||
|
|
||||||
const handleRate = (e) => {
|
const handleRate: RadioGroupProps["onChange"] = (e) => {
|
||||||
setRating(e.target.value);
|
setRating(e.target.value as typeof rating);
|
||||||
analytics.logEvent("setup_rating", { rating: e.target.value });
|
logEvent(analytics, "setup_rating", { rating: e.target.value });
|
||||||
enqueueSnackbar("Thanks for your feedback!");
|
enqueueSnackbar("Thanks for your feedback!");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,7 +92,7 @@ function StepFinish() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={routes.auth}
|
to={ROUTES.auth}
|
||||||
>
|
>
|
||||||
Sign in to your Rowy project
|
Sign in to your Rowy project
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
import type {
|
||||||
|
ISetupStep,
|
||||||
|
ISetupStepBodyProps,
|
||||||
|
} from "@src/components/Setup/SetupStep";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
@@ -13,9 +17,9 @@ import CopyIcon from "@src/assets/icons/Copy";
|
|||||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||||
import DoneIcon from "@mui/icons-material/Done";
|
import DoneIcon from "@mui/icons-material/Done";
|
||||||
|
|
||||||
import SetupItem from "../SetupItem";
|
import SetupItem from "@src/components/Setup/SetupItem";
|
||||||
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||||
import { CONFIG } from "@src/config/dbPaths";
|
import { CONFIG } from "@src/config/dbPaths";
|
||||||
import {
|
import {
|
||||||
RULES_START,
|
RULES_START,
|
||||||
@@ -40,7 +44,7 @@ export default {
|
|||||||
} as ISetupStep;
|
} as ISetupStep;
|
||||||
|
|
||||||
function StepRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
function StepRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||||
const { projectId } = useAppContext();
|
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
const [adminRule, setAdminRule] = useState(true);
|
const [adminRule, setAdminRule] = useState(true);
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { useAtom } from "jotai";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
import type {
|
||||||
|
ISetupStep,
|
||||||
|
ISetupStepBodyProps,
|
||||||
|
} from "@src/components/Setup/SetupStep";
|
||||||
|
|
||||||
import { Typography, Button, Grid } from "@mui/material";
|
import { Typography, Button, Grid } from "@mui/material";
|
||||||
import CopyIcon from "@src/assets/icons/Copy";
|
import CopyIcon from "@src/assets/icons/Copy";
|
||||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||||
import DoneIcon from "@mui/icons-material/Done";
|
import DoneIcon from "@mui/icons-material/Done";
|
||||||
|
|
||||||
import SetupItem from "../SetupItem";
|
import SetupItem from "@src/components/Setup/SetupItem";
|
||||||
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||||
import {
|
import {
|
||||||
RULES_START,
|
RULES_START,
|
||||||
RULES_END,
|
RULES_END,
|
||||||
@@ -27,7 +31,7 @@ export default {
|
|||||||
const rules = RULES_START + REQUIRED_RULES + RULES_END;
|
const rules = RULES_START + REQUIRED_RULES + RULES_END;
|
||||||
|
|
||||||
function StepStorageRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
function StepStorageRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||||
const { projectId } = useAppContext();
|
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
import { useAtom } from "jotai";
|
||||||
|
import type {
|
||||||
|
ISetupStep,
|
||||||
|
ISetupStepBodyProps,
|
||||||
|
} from "@src/components/Setup/SetupStep";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
@@ -9,7 +13,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "welcome",
|
id: "welcome",
|
||||||
@@ -29,7 +33,7 @@ export default {
|
|||||||
} as ISetupStep;
|
} as ISetupStep;
|
||||||
|
|
||||||
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
|
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||||
const { projectId } = useAppContext();
|
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
import _isEqual from "lodash/isEqual";
|
|
||||||
import _pick from "lodash/pick";
|
|
||||||
import _pickBy from "lodash/pickBy";
|
|
||||||
|
|
||||||
import { Control, UseFormReturn, useWatch } from "react-hook-form";
|
|
||||||
import { Values } from "./utils";
|
|
||||||
|
|
||||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
|
||||||
import { TableState } from "@src/hooks/useTable";
|
|
||||||
|
|
||||||
export interface IAutosaveProps {
|
|
||||||
control: Control;
|
|
||||||
docRef: firebase.default.firestore.DocumentReference;
|
|
||||||
row: any;
|
|
||||||
reset: UseFormReturn["reset"];
|
|
||||||
dirtyFields: UseFormReturn["formState"]["dirtyFields"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEditables = (values: Values, tableState?: TableState) =>
|
|
||||||
_pick(
|
|
||||||
values,
|
|
||||||
(tableState &&
|
|
||||||
(Array.isArray(tableState?.columns)
|
|
||||||
? tableState?.columns
|
|
||||||
: Object.values(tableState?.columns)
|
|
||||||
).map((c) => c.key)) ??
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Autosave({
|
|
||||||
control,
|
|
||||||
docRef,
|
|
||||||
row,
|
|
||||||
reset,
|
|
||||||
dirtyFields,
|
|
||||||
}: IAutosaveProps) {
|
|
||||||
const { tableState, updateCell } = useProjectContext();
|
|
||||||
|
|
||||||
const values = useWatch({ control });
|
|
||||||
const [debouncedValue] = useDebounce(getEditables(values, tableState), 1000, {
|
|
||||||
equalityFn: _isEqual,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!row || !row.ref) return;
|
|
||||||
if (row.ref.id !== docRef.id) return;
|
|
||||||
if (!updateCell) return;
|
|
||||||
|
|
||||||
// Get only fields that have had their value updated by the user
|
|
||||||
const updatedValues = _pickBy(
|
|
||||||
_pickBy(debouncedValue, (_, key) => dirtyFields[key]),
|
|
||||||
(value, key) => !_isEqual(value, row[key])
|
|
||||||
);
|
|
||||||
if (Object.keys(updatedValues).length === 0) return;
|
|
||||||
|
|
||||||
// Update the document
|
|
||||||
Object.entries(updatedValues).forEach(([key, value]) =>
|
|
||||||
updateCell(
|
|
||||||
row.ref,
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
// After the cell is updated, set this field to be not dirty
|
|
||||||
// so it doesn’t get updated again when a different field in the form
|
|
||||||
// is updated + make sure the new value is kept after reset
|
|
||||||
() => reset({ ...values, [key]: value })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [debouncedValue]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Skeleton, SkeletonProps } from "@mui/material";
|
|
||||||
|
|
||||||
export default function FieldSkeleton(props: SkeletonProps) {
|
|
||||||
return (
|
|
||||||
<Skeleton
|
|
||||||
variant="rectangular"
|
|
||||||
width="100%"
|
|
||||||
height={32}
|
|
||||||
animation="wave"
|
|
||||||
sx={{ borderRadius: 1 }}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
|
|
||||||
import { Stack, InputLabel, Typography, IconButton } from "@mui/material";
|
|
||||||
import DocumentPathIcon from "@src/assets/icons/DocumentPath";
|
|
||||||
import LaunchIcon from "@mui/icons-material/Launch";
|
|
||||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
|
||||||
|
|
||||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
|
||||||
import FieldSkeleton from "./FieldSkeleton";
|
|
||||||
|
|
||||||
import { FieldType } from "@src/constants/fields";
|
|
||||||
import { getFieldProp } from "@src/components/fields";
|
|
||||||
import { useAppContext } from "@src/contexts/AppContext";
|
|
||||||
|
|
||||||
export interface IFieldWrapperProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
type: FieldType | "debug";
|
|
||||||
name?: string;
|
|
||||||
label?: React.ReactNode;
|
|
||||||
debugText?: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FieldWrapper({
|
|
||||||
children,
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
debugText,
|
|
||||||
disabled,
|
|
||||||
}: IFieldWrapperProps) {
|
|
||||||
const { projectId } = useAppContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
sx={{
|
|
||||||
color: "text.primary",
|
|
||||||
height: 24,
|
|
||||||
scrollMarginTop: 24,
|
|
||||||
"& svg": {
|
|
||||||
display: "block",
|
|
||||||
color: "action.active",
|
|
||||||
fontSize: `${18 / 16}rem`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{type === "debug" ? <DocumentPathIcon /> : getFieldProp("icon", type)}
|
|
||||||
<InputLabel
|
|
||||||
id={`sidedrawer-label-${name}`}
|
|
||||||
htmlFor={`sidedrawer-field-${name}`}
|
|
||||||
sx={{ flexGrow: 1, lineHeight: "18px", ml: 0.75 }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</InputLabel>
|
|
||||||
{disabled && <LockIcon />}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<ErrorBoundary fullScreen={false} basic>
|
|
||||||
<Suspense fallback={<FieldSkeleton />}>
|
|
||||||
{children ??
|
|
||||||
(!debugText && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ paddingLeft: 18 / 8 + 0.75 }}
|
|
||||||
>
|
|
||||||
This field cannot be edited here.
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
|
|
||||||
{debugText && (
|
|
||||||
<Stack direction="row" alignItems="center">
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
paddingLeft: 18 / 8 + 0.75,
|
|
||||||
|
|
||||||
fontFamily: "mono",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
userSelect: "all",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{debugText}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
href={`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${(
|
|
||||||
debugText as string
|
|
||||||
).replace(/\//g, "~2F")}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
aria-label="Open in Firebase Console"
|
|
||||||
size="small"
|
|
||||||
edge="end"
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
>
|
|
||||||
<LaunchIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user