mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-15 19:27: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/
|
||||
.yarn
|
||||
emulators/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { whenDev } = require("@craco/craco");
|
||||
const CracoAlias = require("craco-alias");
|
||||
const CracoSwcPlugin = require("craco-swc");
|
||||
|
||||
@@ -11,20 +12,43 @@ module.exports = {
|
||||
tsConfigPath: "./tsconfig.extend.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugin: CracoSwcPlugin,
|
||||
options: {
|
||||
swcLoaderOptions: {
|
||||
jsc: {
|
||||
target: "es2019",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
// 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,
|
||||
options: {
|
||||
swcLoaderOptions: {
|
||||
jsc: {
|
||||
target: "es2021",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
),
|
||||
],
|
||||
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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.0-alpha.0",
|
||||
"homepage": "https://rowy.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,94 +8,106 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.2.0",
|
||||
"@date-io/date-fns": "1.x",
|
||||
"@emotion/react": "^11.4.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@hookform/resolvers": "^2.8.5",
|
||||
"@mdi/js": "^6.5.95",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@mui/icons-material": "^5.5.1",
|
||||
"@mui/lab": "^5.0.0-alpha.73",
|
||||
"@mui/material": "^5.5.1",
|
||||
"@mui/styles": "^5.5.1",
|
||||
"@rowy/form-builder": "^0.5.3",
|
||||
"@rowy/multiselect": "^0.2.3",
|
||||
"@tinymce/tinymce-react": "^3.12.6",
|
||||
"algoliasearch": "^4.8.6",
|
||||
"ansi-to-react": "^6.1.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",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@monaco-editor/react": "^4.4.4",
|
||||
"@mui/icons-material": "^5.6.0",
|
||||
"@mui/lab": "^5.0.0-alpha.76",
|
||||
"@mui/material": "^5.6.0",
|
||||
"@mui/styles": "^5.6.2",
|
||||
"@rowy/form-builder": "^0.5.5",
|
||||
"@rowy/multiselect": "^0.3.0",
|
||||
"compare-versions": "^4.1.3",
|
||||
"date-fns": "^2.28.0",
|
||||
"dompurify": "^2.3.6",
|
||||
"firebase": "^9.6.11",
|
||||
"firebaseui": "^6.0.1",
|
||||
"jotai": "^1.6.5",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"json2csv": "^5.0.6",
|
||||
"jszip": "^3.6.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"match-sorter": "^6.3.1",
|
||||
"notistack": "^2.0.2",
|
||||
"pb-util": "^1.0.1",
|
||||
"query-string": "^6.8.3",
|
||||
"quicktype-core": "^6.0.70",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-color-palette": "^6.1.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",
|
||||
"notistack": "^2.0.4",
|
||||
"quicktype-core": "^6.0.71",
|
||||
"react": "^18.0.0",
|
||||
"react-color-palette": "^6.2.0",
|
||||
"react-data-grid": "7.0.0-beta.5",
|
||||
"react-div-100vh": "^0.7.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-element-scroll-hook": "^1.1.0",
|
||||
"react-firebaseui": "^5.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.21.2",
|
||||
"react-image": "^4.0.3",
|
||||
"react-json-view": "^1.19.1",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-hook-form": "^7.30.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-usestateref": "^1.0.5",
|
||||
"react-scripts": "^5.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"serve": "^11.3.2",
|
||||
"swr": "^1.0.1",
|
||||
"tinymce": "^5.10.0",
|
||||
"typescript": "^4.4.2",
|
||||
"use-algolia": "^1.4.1",
|
||||
"use-debounce": "^3.3.0",
|
||||
"use-persisted-state": "^0.3.3",
|
||||
"yarn": "^1.22.10"
|
||||
"swr": "^1.3.0",
|
||||
"tss-react": "^3.6.2",
|
||||
"typescript": "^4.6.3",
|
||||
"use-debounce": "^7.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"upstream": "git fetch upstream;git merge upstream/main;git commit -m'merge upstream';git push",
|
||||
"serve": "serve -s build",
|
||||
"start": "craco start",
|
||||
"build": "craco build CI=false",
|
||||
"test": "craco test --env=jsdom",
|
||||
"eject": "craco eject",
|
||||
"start": "cross-env PORT=7699 craco start",
|
||||
"startWithEmulator": "cross-env PORT=7699 REACT_APP_FIREBASE_EMULATOR=true craco start",
|
||||
"emulators": "firebase emulators:start --only firestore,auth --import ./emulators/ --export-on-exit",
|
||||
"test": "craco test --env ./src/test/custom-jest-env.js",
|
||||
"build": "craco build",
|
||||
"analyze": "source-map-explorer ./build/static/js/*.js",
|
||||
"prepare": "husky install",
|
||||
"env": "node createDotEnv",
|
||||
"target": "firebase target:apply hosting rowy",
|
||||
"deploy": "firebase deploy"
|
||||
"deploy": "firebase deploy --only hosting"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=16"
|
||||
},
|
||||
"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": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"> 0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
"not op_mini all",
|
||||
"not ie > 0",
|
||||
"not and_uc > 0",
|
||||
"not ios_saf < 14"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
@@ -104,34 +116,41 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^2.2.1",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/react": "^17.0.11",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-color": "^3.0.1",
|
||||
"@types/react-div-100vh": "^0.3.0",
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"@types/react-helmet": "^6.1.2",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-router-hash-link": "^2.4.1",
|
||||
"@types/use-persisted-state": "^0.3.0",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^14.0.4",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/react": "^18.0.5",
|
||||
"@types/react-div-100vh": "^0.4.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-router-hash-link": "^2.4.5",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"craco-alias": "^3.0.1",
|
||||
"firebase-tools": "^10.1.0",
|
||||
"husky": "^4.2.5",
|
||||
"monaco-editor": "^0.21.2",
|
||||
"playwright": "^1.5.2",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-quick": "^3.0.0",
|
||||
"raw-loader": "^4.0.2"
|
||||
"craco-swc": "^0.5.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"eslint-plugin-local-rules": "^1.1.0",
|
||||
"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": {
|
||||
"react-hook-form": "^7.21.2"
|
||||
"@types/react": "^18"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
"lint-staged": {
|
||||
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||
"**/*": "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
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "Rowy",
|
||||
"short_name": "Rowy",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#4200ff",
|
||||
"background_color": "#4200ff",
|
||||
"display": "standalone"
|
||||
"name": "Rowy",
|
||||
"short_name": "Rowy",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#4200ff",
|
||||
"background_color": "#4200ff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
294
src/App.tsx
294
src/App.tsx
@@ -1,221 +1,119 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { Route, Switch, Redirect } from "react-router-dom";
|
||||
import LocalizationProvider from "@mui/lab/LocalizationProvider";
|
||||
import AdapterDateFns from "@mui/lab/AdapterDateFns";
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
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 Navigation from "@src/components/Navigation";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import ProjectSourceFirebase from "@src/sources/ProjectSourceFirebase";
|
||||
import ConfirmDialog from "@src/components/ConfirmDialog";
|
||||
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 ConfirmationProvider from "@src/components/ConfirmationDialog/Provider";
|
||||
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 { globalScope, currentUserAtom } from "@src/atoms/globalScope";
|
||||
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 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
|
||||
const AuthSetupGuidePage = lazy(() => import("@src/pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */));
|
||||
const AuthPage = lazy(() => import("@src/pages/Auth/index" /* webpackChunkName: "AuthPage" */));
|
||||
// prettier-ignore
|
||||
const ImpersonatorAuthPage = lazy(() => import("./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */));
|
||||
const SignUpPage = lazy(() => import("@src/pages/Auth/SignUp" /* webpackChunkName: "SignUpPage" */));
|
||||
// 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
|
||||
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() {
|
||||
const [currentUser] = useAtom(currentUserAtom, globalScope);
|
||||
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ErrorBoundary>
|
||||
<SwrProvider>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<AppProvider>
|
||||
<Favicon />
|
||||
<SnackbarProvider>
|
||||
<ConfirmationProvider>
|
||||
<SnackLogProvider>
|
||||
<CustomBrowserRouter>
|
||||
<RowyRunModal />
|
||||
<Suspense fallback={<Loading fullScreen />}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routes.auth}
|
||||
render={() => <AuthPage />}
|
||||
/>
|
||||
<Route
|
||||
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 />}
|
||||
/>
|
||||
<Suspense fallback={<Loading fullScreen />}>
|
||||
<ProjectSourceFirebase />
|
||||
<ConfirmDialog />
|
||||
<RowyRunModal />
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={"/test"}
|
||||
render={() => <TestPage />}
|
||||
/>
|
||||
{currentUser === undefined ? (
|
||||
<Loading fullScreen message="Authenticating" />
|
||||
) : (
|
||||
<Routes>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
||||
<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
|
||||
exact
|
||||
path={routes.impersonatorAuth}
|
||||
render={() => <ImpersonatorAuthPage />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={routes.rowyRunTest}
|
||||
render={() => <RowyRunTestPage />}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.home}
|
||||
render={() => (
|
||||
<Navigation
|
||||
title="Home"
|
||||
titleComponent={(open, pinned) =>
|
||||
!(open && pinned) && (
|
||||
<Logo
|
||||
style={{
|
||||
display: "block",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<HomePage />
|
||||
</Navigation>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path={routes.tableWithId}
|
||||
render={() => <TablePage />}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path={routes.tableGroupWithId}
|
||||
render={() => <TablePage />}
|
||||
/>
|
||||
<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
|
||||
path={ROUTES.impersonatorAuth}
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ImpersonatorAuthPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.settings}
|
||||
render={() => (
|
||||
<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 path={ROUTES.setup} element={<SetupPage />} />
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={routes.pageNotFound}
|
||||
render={() => <PageNotFound />}
|
||||
/>
|
||||
<Route render={() => <PageNotFound />} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</CustomBrowserRouter>
|
||||
</SnackLogProvider>
|
||||
</ConfirmationProvider>
|
||||
</SnackbarProvider>
|
||||
</AppProvider>
|
||||
</LocalizationProvider>
|
||||
</SwrProvider>
|
||||
</ErrorBoundary>
|
||||
</StyledEngineProvider>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Navigation>
|
||||
<TableSettingsDialog />
|
||||
</Navigation>
|
||||
</RequireAuth>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
path={ROUTES.home}
|
||||
element={<Navigate to={ROUTES.tables} replace />}
|
||||
/>
|
||||
<Route path={ROUTES.tables} element={<TablesPage />} />
|
||||
|
||||
<Route path={ROUTES.table}>
|
||||
<Route index element={<Navigate to={ROUTES.tables} replace />} />
|
||||
<Route path=":id" element={<TablePage />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={ROUTES.settings}
|
||||
element={<Navigate to={ROUTES.userSettings} replace />}
|
||||
/>
|
||||
<Route path={ROUTES.userSettings} element={<UserSettingsPage />} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 "firebase/analytics";
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAnalytics, logEvent } from "firebase/analytics";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyArABiYGK7dZgwSk0pw_6vKbOt6U1ZRPpc",
|
||||
@@ -11,7 +11,6 @@ const firebaseConfig = {
|
||||
measurementId: "G-0VWE25LFZJ",
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const rowyServiceApp = firebase.initializeApp(firebaseConfig, "rowy-service");
|
||||
|
||||
export const analytics = firebase.analytics(rowyServiceApp);
|
||||
const rowyServiceApp = initializeApp(firebaseConfig, "rowy-service");
|
||||
export const analytics = getAnalytics(rowyServiceApp);
|
||||
export { logEvent };
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import Helmet from "react-helmet";
|
||||
import { use100vh } from "react-div-100vh";
|
||||
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { Box, BoxProps } from "@mui/material";
|
||||
import { GlobalStyles, Box, BoxProps } from "@mui/material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
|
||||
import bgPattern from "@src/assets/bg-pattern.svg";
|
||||
import bgPatternDark from "@src/assets/bg-pattern-dark.svg";
|
||||
|
||||
export default function BrandedBackground() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<style type="text/css">
|
||||
{`
|
||||
<GlobalStyles
|
||||
styles={(theme) => `
|
||||
body {
|
||||
background-size: 100%;
|
||||
background-image: ${
|
||||
@@ -44,8 +40,7 @@ export default function BrandedBackground() {
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</Helmet>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@ export default function LogoRowyRun({
|
||||
<title id="rowy-run-logo-title">Rowy Run</title>
|
||||
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="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"
|
||||
fill={theme.palette.text.primary}
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="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"
|
||||
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 { mdiFirebase } from '@mdi/js';
|
||||
import { mdiFirebase } from "@mdi/js";
|
||||
|
||||
export default function AddColumn(props: SvgIconProps) {
|
||||
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 SecretsIcon from "@mui/icons-material/VpnKeyOutlined";
|
||||
import FunctionsIcon from "@mui/icons-material/CloudOutlined";
|
||||
import DocsIcon from "@mui/icons-material/DescriptionOutlined";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||
|
||||
export interface ICodeEditorHelperProps {
|
||||
docLink: string;
|
||||
@@ -17,7 +19,8 @@ export default function CodeEditorHelper({
|
||||
docLink,
|
||||
additionalVariables,
|
||||
}: ICodeEditorHelperProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
|
||||
const availableVariables = [
|
||||
{
|
||||
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 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 */
|
||||
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
// 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.
|
||||
*
|
||||
|
||||
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 type { SystemStyleObject, Theme } from "@mui/system";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { getColumnType, getFieldProp } from "@src/components/fields";
|
||||
// TODO:
|
||||
// import { getColumnType, getFieldProp } from "@src/components/fields";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
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 rowyUtilsDefs from "!!raw-loader!./rowy.d.ts";
|
||||
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
||||
import defaultValueDefs from "!!raw-loader!./defaultValue.d.ts";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export interface IUseMonacoCustomizationsProps {
|
||||
@@ -54,7 +53,6 @@ export default function useMonacoCustomizations({
|
||||
fullScreen,
|
||||
}: IUseMonacoCustomizationsProps) {
|
||||
const theme = useTheme();
|
||||
const { tableState, rowyRun } = useProjectContext();
|
||||
|
||||
const monaco = useMonaco();
|
||||
|
||||
@@ -141,111 +139,115 @@ export default function useMonacoCustomizations({
|
||||
}
|
||||
}, [monaco, stringifiedDiagnosticsOptions]);
|
||||
|
||||
const addJsonFieldDefinition = async (columnKey, interfaceName) => {
|
||||
const samples = tableState?.rows
|
||||
.map((row) => row[columnKey])
|
||||
.filter((entry) => entry !== undefined)
|
||||
.map((entry) => JSON.stringify(entry));
|
||||
if (!samples || samples.length === 0) {
|
||||
monaco?.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
`type ${interfaceName} = any;`
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const jsonInput = jsonInputForTargetLanguage("typescript");
|
||||
await jsonInput.addSource({
|
||||
name: interfaceName,
|
||||
samples,
|
||||
});
|
||||
// TODO:
|
||||
// const addJsonFieldDefinition = async (columnKey, interfaceName) => {
|
||||
// const samples = tableState?.rows
|
||||
// .map((row) => row[columnKey])
|
||||
// .filter((entry) => entry !== undefined)
|
||||
// .map((entry) => JSON.stringify(entry));
|
||||
// if (!samples || samples.length === 0) {
|
||||
// monaco?.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// `type ${interfaceName} = any;`
|
||||
// );
|
||||
// return;
|
||||
// } else {
|
||||
// const jsonInput = jsonInputForTargetLanguage("typescript");
|
||||
// await jsonInput.addSource({
|
||||
// name: interfaceName,
|
||||
// samples,
|
||||
// });
|
||||
|
||||
const inputData = new InputData();
|
||||
inputData.addInput(jsonInput);
|
||||
const result = await quicktype({
|
||||
inputData,
|
||||
lang: "typescript",
|
||||
rendererOptions: { "just-types": "true" },
|
||||
});
|
||||
const newLib = result.lines.join("\n").replaceAll("export ", "");
|
||||
monaco?.languages.typescript.javascriptDefaults.addExtraLib(newLib);
|
||||
}
|
||||
};
|
||||
// const inputData = new InputData();
|
||||
// inputData.addInput(jsonInput);
|
||||
// const result = await quicktype({
|
||||
// inputData,
|
||||
// lang: "typescript",
|
||||
// rendererOptions: { "just-types": "true" },
|
||||
// });
|
||||
// const newLib = result.lines.join("\n").replaceAll("export ", "");
|
||||
// monaco?.languages.typescript.javascriptDefaults.addExtraLib(newLib);
|
||||
// }
|
||||
// };
|
||||
|
||||
const setSecrets = async (monaco, rowyRun) => {
|
||||
// set secret options
|
||||
try {
|
||||
const listSecrets = await rowyRun({
|
||||
route: runRoutes.listSecrets,
|
||||
});
|
||||
const secretsDef = `type SecretNames = ${listSecrets
|
||||
.map((secret) => `"${secret}"`)
|
||||
.join(" | ")}
|
||||
enum secrets {
|
||||
${listSecrets.map((secret) => `${secret} = "${secret}"`).join("\n")}
|
||||
}
|
||||
`;
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(secretsDef);
|
||||
} catch (error) {
|
||||
console.error("Could not set secret definitions: ", error);
|
||||
}
|
||||
};
|
||||
const setBaseDefinitions = (monaco, columns) => {
|
||||
const rowDefinition =
|
||||
[
|
||||
Object.keys(columns).map((columnKey: string) => {
|
||||
const column = columns[columnKey];
|
||||
const type = getColumnType(column);
|
||||
if (type === "JSON") {
|
||||
const interfaceName =
|
||||
columnKey[0].toUpperCase() + columnKey.slice(1);
|
||||
addJsonFieldDefinition(columnKey, interfaceName);
|
||||
const def = `static "${columnKey}": ${interfaceName}`;
|
||||
return def;
|
||||
}
|
||||
return `static "${columnKey}": ${getFieldProp("dataType", type)}`;
|
||||
}),
|
||||
].join(";\n") + ";";
|
||||
// TODO: types
|
||||
// const setSecrets = async (monaco, rowyRun) => {
|
||||
// // set secret options
|
||||
// try {
|
||||
// const listSecrets = await rowyRun({
|
||||
// route: runRoutes.listSecrets,
|
||||
// });
|
||||
// const secretsDef = `type SecretNames = ${listSecrets
|
||||
// .map((secret) => `"${secret}"`)
|
||||
// .join(" | ")}
|
||||
// enum secrets {
|
||||
// ${listSecrets.map((secret) => `${secret} = "${secret}"`).join("\n")}
|
||||
// }
|
||||
// `;
|
||||
// monaco.languages.typescript.javascriptDefaults.addExtraLib(secretsDef);
|
||||
// } catch (error) {
|
||||
// console.error("Could not set secret definitions: ", error);
|
||||
// }
|
||||
// };
|
||||
// TODO: types
|
||||
// const setBaseDefinitions = (monaco, columns) => {
|
||||
// const rowDefinition =
|
||||
// [
|
||||
// Object.keys(columns).map((columnKey: string) => {
|
||||
// const column = columns[columnKey];
|
||||
// const type = getColumnType(column);
|
||||
// if (type === "JSON") {
|
||||
// const interfaceName =
|
||||
// columnKey[0].toUpperCase() + columnKey.slice(1);
|
||||
// addJsonFieldDefinition(columnKey, interfaceName);
|
||||
// const def = `static "${columnKey}": ${interfaceName}`;
|
||||
// return def;
|
||||
// }
|
||||
// return `static "${columnKey}": ${getFieldProp("dataType", type)}`;
|
||||
// }),
|
||||
// ].join(";\n") + ";";
|
||||
|
||||
const availableFields = Object.keys(columns)
|
||||
.map((columnKey: string) => `"${columnKey}"`)
|
||||
.join("|\n");
|
||||
// const availableFields = Object.keys(columns)
|
||||
// .map((columnKey: string) => `"${columnKey}"`)
|
||||
// .join("|\n");
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
["/**", " * extensions type configuration", " */", extensionsDefs].join(
|
||||
"\n"
|
||||
),
|
||||
"ts:filename/extensions.d.ts"
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
[
|
||||
"// basic types that are used in all places",
|
||||
"declare var require: any;",
|
||||
"declare var Buffer: any;",
|
||||
"const ref: FirebaseFirestore.DocumentReference;",
|
||||
"const storage: firebasestorage.Storage;",
|
||||
"const db: FirebaseFirestore.Firestore;",
|
||||
"const auth: firebaseauth.BaseAuth;",
|
||||
`type Row = {${rowDefinition}};`,
|
||||
`type Field = ${availableFields} | string | object;`,
|
||||
`type Fields = Field[];`,
|
||||
].join("\n"),
|
||||
"ts:filename/rowFields.d.ts"
|
||||
);
|
||||
};
|
||||
// monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// ["/**", " * extensions type configuration", " */", extensionsDefs].join(
|
||||
// "\n"
|
||||
// ),
|
||||
// "ts:filename/extensions.d.ts"
|
||||
// );
|
||||
// monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// [
|
||||
// "// basic types that are used in all places",
|
||||
// "declare var require: any;",
|
||||
// "declare var Buffer: any;",
|
||||
// "const ref: FirebaseFirestore.DocumentReference;",
|
||||
// "const storage: firebasestorage.Storage;",
|
||||
// "const db: FirebaseFirestore.Firestore;",
|
||||
// "const auth: firebaseauth.BaseAuth;",
|
||||
// `type Row = {${rowDefinition}};`,
|
||||
// `type Field = ${availableFields} | string | object;`,
|
||||
// `type Fields = Field[];`,
|
||||
// ].join("\n"),
|
||||
// "ts:filename/rowFields.d.ts"
|
||||
// );
|
||||
// };
|
||||
// TODO:
|
||||
// Set row definitions
|
||||
useEffect(() => {
|
||||
if (!monaco || !rowyRun || !tableState?.columns) return;
|
||||
try {
|
||||
setBaseDefinitions(monaco, tableState.columns);
|
||||
} catch (error) {
|
||||
console.error("Could not set basic", error);
|
||||
}
|
||||
// set available secrets from secretManager
|
||||
try {
|
||||
setSecrets(monaco, rowyRun);
|
||||
} catch (error) {
|
||||
console.error("Could not set secrets: ", error);
|
||||
}
|
||||
}, [monaco, tableState?.columns, rowyRun]);
|
||||
// useEffect(() => {
|
||||
// if (!monaco || !rowyRun || !tableState?.columns) return;
|
||||
// try {
|
||||
// setBaseDefinitions(monaco, tableState.columns);
|
||||
// } catch (error) {
|
||||
// console.error("Could not set basic", error);
|
||||
// }
|
||||
// // set available secrets from secretManager
|
||||
// try {
|
||||
// setSecrets(monaco, rowyRun);
|
||||
// } catch (error) {
|
||||
// console.error("Could not set secrets: ", error);
|
||||
// }
|
||||
// }, [monaco, tableState?.columns, rowyRun]);
|
||||
|
||||
let boxSx: SystemStyleObject<Theme> = {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -11,22 +12,35 @@ import {
|
||||
} from "@mui/material";
|
||||
|
||||
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
||||
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
|
||||
|
||||
/**
|
||||
* Display a confirm dialog using `confirmDialogAtom` in `globalState`
|
||||
* {@link confirmDialogAtom | See usage example}
|
||||
*/
|
||||
export default function ConfirmDialog() {
|
||||
const [
|
||||
{
|
||||
open,
|
||||
|
||||
title = "Are you sure?",
|
||||
body,
|
||||
|
||||
handleConfirm,
|
||||
confirm = "Confirm",
|
||||
confirmationCommand,
|
||||
confirmColor,
|
||||
|
||||
handleCancel,
|
||||
cancel = "Cancel",
|
||||
hideCancel,
|
||||
|
||||
maxWidth = "xs",
|
||||
},
|
||||
setState,
|
||||
] = useAtom(confirmDialogAtom, globalScope);
|
||||
const handleClose = () => setState({ open: false });
|
||||
|
||||
export default function Confirmation({
|
||||
title,
|
||||
customBody,
|
||||
body,
|
||||
cancel,
|
||||
hideCancel,
|
||||
confirm,
|
||||
confirmationCommand,
|
||||
handleConfirm,
|
||||
handleCancel,
|
||||
confirmColor,
|
||||
open,
|
||||
handleClose,
|
||||
maxWidth = "xs",
|
||||
}: any) {
|
||||
const [dryText, setDryText] = useState("");
|
||||
|
||||
return (
|
||||
@@ -40,11 +54,14 @@ export default function Confirmation({
|
||||
TransitionComponent={SlideTransitionMui}
|
||||
style={{ cursor: "default" }}
|
||||
>
|
||||
<DialogTitle>{title ?? "Are you sure?"}</DialogTitle>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{customBody}
|
||||
{body && <DialogContentText>{body}</DialogContentText>}
|
||||
{typeof body === "string" ? (
|
||||
<DialogContentText>{body}</DialogContentText>
|
||||
) : (
|
||||
body
|
||||
)}
|
||||
{confirmationCommand && (
|
||||
<TextField
|
||||
value={dryText}
|
||||
@@ -67,12 +84,12 @@ export default function Confirmation({
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
{cancel ?? "Cancel"}
|
||||
{cancel}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleConfirm();
|
||||
if (handleConfirm) handleConfirm();
|
||||
handleClose();
|
||||
}}
|
||||
color={confirmColor || "primary"}
|
||||
@@ -82,7 +99,7 @@ export default function Confirmation({
|
||||
confirmationCommand ? dryText !== confirmationCommand : false
|
||||
}
|
||||
>
|
||||
{confirm ?? "Confirm"}
|
||||
{confirm}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</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 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> {
|
||||
label: string;
|
||||
@@ -120,6 +120,12 @@ export default function FloatingSearch({
|
||||
}px)`,
|
||||
left: (theme) => (theme.shape.borderRadius as number) * 2,
|
||||
},
|
||||
|
||||
"&.Mui-disabled": {
|
||||
bgcolor: "transparent",
|
||||
boxShadow: "none",
|
||||
"& .MuiInputAdornment-root": { color: "text.disabled" },
|
||||
},
|
||||
},
|
||||
}}
|
||||
{...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 {
|
||||
Dialog,
|
||||
@@ -20,9 +20,9 @@ export interface IFullScreenModalProps
|
||||
disableEscapeKeyDown?: boolean;
|
||||
|
||||
"aria-labelledby": DialogProps["aria-labelledby"];
|
||||
header?: ReactNode;
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
header?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
|
||||
hideCloseButton?: boolean;
|
||||
ScrollableDialogContentProps?: Partial<IScrollableDialogContentProps>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
useTheme,
|
||||
@@ -26,12 +26,12 @@ export interface IModalProps extends Partial<Omit<DialogProps, "title">> {
|
||||
disableBackdropClick?: boolean;
|
||||
disableEscapeKeyDown?: boolean;
|
||||
|
||||
title: ReactNode;
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
|
||||
children?: ReactNode;
|
||||
body?: ReactNode;
|
||||
children?: React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
|
||||
actions?: {
|
||||
primary?: Partial<LoadingButtonProps>;
|
||||
@@ -94,7 +94,7 @@ export default function Modal({
|
||||
...props.sx,
|
||||
"& .MuiDialog-paper": {
|
||||
height: "100%",
|
||||
...props.sx?.["& .MuiDialog-paper"],
|
||||
...(props.sx as any)?.["& .MuiDialog-paper"],
|
||||
},
|
||||
}
|
||||
: props.sx
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import { forwardRef, cloneElement } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { Transition } from "react-transition-group";
|
||||
import { TransitionProps } from "react-transition-group/Transition";
|
||||
@@ -6,7 +6,7 @@ import { TransitionProps as MuiTransitionProps } from "@mui/material/transitions
|
||||
|
||||
export const SlideTransition: React.ForwardRefExoticComponent<
|
||||
Pick<TransitionProps, React.ReactText> & React.RefAttributes<any>
|
||||
> = React.forwardRef(
|
||||
> = forwardRef(
|
||||
({ children, ...props }: TransitionProps, ref: React.Ref<any>) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -57,8 +57,9 @@ export const SlideTransition: React.ForwardRefExoticComponent<
|
||||
{...props}
|
||||
>
|
||||
{(state) =>
|
||||
React.cloneElement(children as any, {
|
||||
cloneElement(children as any, {
|
||||
style: { ...defaultStyle, ...transitionStyles[state] },
|
||||
tabIndex: -1,
|
||||
ref,
|
||||
})
|
||||
}
|
||||
@@ -69,7 +70,7 @@ export const SlideTransition: React.ForwardRefExoticComponent<
|
||||
|
||||
export default SlideTransition;
|
||||
|
||||
export const SlideTransitionMui = React.forwardRef(function Transition(
|
||||
export const SlideTransitionMui = forwardRef(function Transition(
|
||||
props: MuiTransitionProps & { children?: React.ReactElement<any, any> },
|
||||
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";
|
||||
|
||||
const remarkPlugins = [remarkGfm];
|
||||
const components = {
|
||||
const components: ReactMarkdownOptions["components"] = {
|
||||
a: (props) => <Link color="inherit" {...props} />,
|
||||
p: Typography,
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
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 (
|
||||
<ReactMarkdown
|
||||
{...props}
|
||||
allowedElements={restrictionPresets[restrictionPreset || ""]}
|
||||
allowedElements={
|
||||
restrictionPreset ? restrictionPresets[restrictionPreset] : undefined
|
||||
}
|
||||
unwrapDisallowed
|
||||
linkTarget="_blank"
|
||||
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 { useAtom } from "jotai";
|
||||
import { rowyRunModalAtom } from "@src/atoms/RowyRunModal";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -13,23 +12,35 @@ import Modal from "@src/components/Modal";
|
||||
import Logo from "@src/assets/LogoRowyRun";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { routes } from "@src/constants/routes";
|
||||
import {
|
||||
globalScope,
|
||||
userRolesAtom,
|
||||
projectSettingsAtom,
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
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() {
|
||||
const { userRoles } = useAppContext();
|
||||
const { settings } = useProjectContext();
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
|
||||
const [rowyRunModal, setRowyRunModal] = useAtom(
|
||||
rowyRunModalAtom,
|
||||
globalScope
|
||||
);
|
||||
|
||||
const [state, setState] = useAtom(rowyRunModalAtom);
|
||||
const handleClose = () => setState((s) => ({ ...s, open: false }));
|
||||
const handleClose = () => setRowyRunModal({ ...rowyRunModal, open: false });
|
||||
|
||||
const showUpdateModal = state.version && settings?.rowyRunUrl;
|
||||
const showUpdateModal = rowyRunModal.version && projectSettings?.rowyRunUrl;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={state.open}
|
||||
open={rowyRunModal.open}
|
||||
onClose={handleClose}
|
||||
title={
|
||||
<Logo
|
||||
@@ -47,13 +58,13 @@ export default function RowyRunModal() {
|
||||
<>
|
||||
<Typography variant="h5" paragraph align="center">
|
||||
{showUpdateModal ? "Update" : "Set up"} Rowy Run to use{" "}
|
||||
{state.feature || "this feature"}
|
||||
{rowyRunModal.feature || "this feature"}
|
||||
</Typography>
|
||||
|
||||
{showUpdateModal && (
|
||||
<DialogContentText variant="body1" paragraph textAlign="center">
|
||||
{state.feature || "This feature"} requires Rowy Run v
|
||||
{state.version} or later.
|
||||
{rowyRunModal.feature || "This feature"} requires Rowy Run v
|
||||
{rowyRunModal.version} or later.
|
||||
</DialogContentText>
|
||||
)}
|
||||
|
||||
@@ -73,7 +84,7 @@ export default function RowyRunModal() {
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to={routes.projectSettings + "#rowyRun"}
|
||||
to={ROUTES.projectSettings + "#rowyRun"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { forwardRef } from "react";
|
||||
import _camelCase from "lodash/camelCase";
|
||||
import { camelCase } from "lodash-es";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
|
||||
import { Stack, StackProps, Typography, IconButton } from "@mui/material";
|
||||
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"> {
|
||||
children: string;
|
||||
@@ -15,7 +15,7 @@ export const SectionHeading = forwardRef(function SectionHeading_(
|
||||
{ children, sx, ...props }: ISectionHeadingProps,
|
||||
ref
|
||||
) {
|
||||
const sectionLink = _camelCase(children);
|
||||
const sectionLink = camelCase(children);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import { Grid, Typography, Button, Link, Divider } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
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 InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { version } from "@root/package.json";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import meta from "@root/package.json";
|
||||
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function About() {
|
||||
const { projectId } = useAppContext();
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
|
||||
const [latestUpdate, checkForUpdates, loading] = useUpdateCheck();
|
||||
|
||||
@@ -100,7 +102,7 @@ export default function About() {
|
||||
)}
|
||||
|
||||
<Typography display="block" color="textSecondary">
|
||||
Rowy v{version}
|
||||
Rowy v{meta.version}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { authOptions } from "firebase/firebaseui";
|
||||
import _startCase from "lodash/startCase";
|
||||
import { startCase } from "lodash-es";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import { Typography, Link } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||
import { authOptions } from "@src/config/firebaseui";
|
||||
|
||||
export default function Authentication({
|
||||
publicSettings,
|
||||
@@ -25,7 +25,7 @@ export default function Authentication({
|
||||
value={signInOptions}
|
||||
options={Object.keys(authOptions).map((option) => ({
|
||||
value: option,
|
||||
label: _startCase(option).replace("Github", "GitHub"),
|
||||
label: startCase(option).replace("Github", "GitHub"),
|
||||
}))}
|
||||
onChange={setSignInOptions}
|
||||
onClose={() => updatePublicSettings({ signInOptions })}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||
import _merge from "lodash/merge";
|
||||
import _unset from "lodash/unset";
|
||||
import { merge, unset } from "lodash-es";
|
||||
|
||||
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
// 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({
|
||||
publicSettings,
|
||||
@@ -20,7 +19,7 @@ export default function Customization({
|
||||
|
||||
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
|
||||
updatePublicSettings({
|
||||
theme: _merge(publicSettings.theme, {
|
||||
theme: merge(publicSettings.theme, {
|
||||
light: { palette: { primary: { main: light } } },
|
||||
dark: { palette: { primary: { main: dark } } },
|
||||
}),
|
||||
@@ -37,8 +36,8 @@ export default function Customization({
|
||||
setCustomizedThemeColor(e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
const newTheme = publicSettings.theme;
|
||||
_unset(newTheme, "light.palette.primary.main");
|
||||
_unset(newTheme, "dark.palette.primary.main");
|
||||
unset(newTheme, "light.palette.primary.main");
|
||||
unset(newTheme, "dark.palette.primary.main");
|
||||
updatePublicSettings({ theme: newTheme });
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -14,7 +14,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
|
||||
import LogoRowyRun from "@src/assets/LogoRowyRun";
|
||||
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 { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ThemeColorPicker({
|
||||
width={244}
|
||||
height={140}
|
||||
color={toColor("hex", light)}
|
||||
onChange={(c: any) => setLight(c.hex)}
|
||||
onChange={(c) => setLight(c.hex)}
|
||||
dark={theme.palette.mode === "dark"}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
@@ -14,15 +15,22 @@ import AddIcon from "@mui/icons-material/PersonAddOutlined";
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import Modal from "@src/components/Modal";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import routes from "@src/constants/routes";
|
||||
import {
|
||||
globalScope,
|
||||
rolesAtom,
|
||||
projectSettingsAtom,
|
||||
rowyRunAtom,
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { useRowyRunModal } from "@src/atoms/RowyRunModal";
|
||||
|
||||
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 openRowyRunModal = useRowyRunModal();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [status, setStatus] = useState<"LOADING" | string>("");
|
||||
@@ -33,7 +41,7 @@ export default function InviteUser() {
|
||||
const handleInvite = async () => {
|
||||
try {
|
||||
setStatus("LOADING");
|
||||
const res = await rowyRun?.({
|
||||
const res = await rowyRun({
|
||||
route: runRoutes.inviteUser,
|
||||
body: { email, roles },
|
||||
});
|
||||
@@ -52,9 +60,9 @@ export default function InviteUser() {
|
||||
<Button
|
||||
aria-label="Invite user"
|
||||
onClick={
|
||||
settings?.rowyRunUrl
|
||||
projectSettings.rowyRunUrl
|
||||
? () => setOpen(true)
|
||||
: () => openRowyRunModal("Invite user")
|
||||
: () => openRowyRunModal({ feature: "Invite user" })
|
||||
}
|
||||
variant="text"
|
||||
color="primary"
|
||||
@@ -78,7 +86,7 @@ export default function InviteUser() {
|
||||
They can sign up with any of the sign-in options{" "}
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={routes.projectSettings + "#authentication"}
|
||||
to={ROUTES.projectSettings + "#authentication"}
|
||||
>
|
||||
you have enabled
|
||||
</MuiLink>
|
||||
@@ -104,7 +112,7 @@ export default function InviteUser() {
|
||||
TextFieldProps={{
|
||||
id: "invite-roles",
|
||||
SelectProps: {
|
||||
renderValue: (_) => {
|
||||
renderValue: () => {
|
||||
if (Array.isArray(roles)) {
|
||||
if (roles.length >= 1) return roles.join(", ");
|
||||
return (
|
||||
@@ -113,6 +121,7 @@ export default function InviteUser() {
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
sx: { mt: 3 },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import {
|
||||
@@ -14,18 +15,33 @@ import CopyIcon from "@src/assets/icons/Copy";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
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 { db } from "@src/firebase";
|
||||
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 { 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 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 },
|
||||
});
|
||||
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);
|
||||
enqueueSnackbar(`Set roles for ${user!.email}: ${value.join(", ")}`);
|
||||
}
|
||||
@@ -72,9 +89,15 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
requestConfirmation({
|
||||
if (!projectSettings.rowyRunUrl) {
|
||||
openRowyRunModal({ feature: "User Management" });
|
||||
return;
|
||||
}
|
||||
|
||||
confirm({
|
||||
open: true,
|
||||
title: "Delete user?",
|
||||
customBody: (
|
||||
body: (
|
||||
<>
|
||||
<ListItem children={listItemChildren} disablePadding sx={{ mb: 3 }} />
|
||||
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 () => {
|
||||
if (!user) return;
|
||||
const loadingSnackbarId = enqueueSnackbar("Deleting user…");
|
||||
await rowyRun?.({
|
||||
const response = await rowyRun({
|
||||
route: runRoutes.deleteUser,
|
||||
body: { email: user.email },
|
||||
});
|
||||
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
|
||||
TextFieldProps={{
|
||||
SelectProps: {
|
||||
renderValue: (_) => {
|
||||
renderValue: () => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 1) return value[0];
|
||||
if (value.length > 1) return `${value.length} roles`;
|
||||
@@ -119,6 +142,7 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -155,9 +179,9 @@ export default function UserItem({ id, user, roles: rolesProp }: User) {
|
||||
<IconButton
|
||||
aria-label="Copy UID"
|
||||
onClick={async () => {
|
||||
if (!id) return;
|
||||
await navigator.clipboard.writeText(id);
|
||||
enqueueSnackbar(`Copied UID for ${user?.email}: ${id}`);
|
||||
if (!_rowy_id) return;
|
||||
await navigator.clipboard.writeText(_rowy_id);
|
||||
enqueueSnackbar(`Copied UID for ${user?.email}: ${_rowy_id}`);
|
||||
}}
|
||||
>
|
||||
<CopyIcon />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
|
||||
|
||||
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) {
|
||||
return (
|
||||
@@ -26,7 +26,7 @@ export default function Account({ settings }: IUserSettingsChildProps) {
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button component={Link} to={routes.signOut}>
|
||||
<Button component={Link} to={ROUTES.signOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
||||
import _merge from "lodash/merge";
|
||||
import _unset from "lodash/unset";
|
||||
import { merge, unset } from "lodash-es";
|
||||
|
||||
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
// 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({
|
||||
settings,
|
||||
updateSettings,
|
||||
}: IUserSettingsChildProps) {
|
||||
const [customizedThemeColor, setCustomizedThemeColor] = useState(
|
||||
settings.theme?.light?.palette?.primary?.main ||
|
||||
settings.theme?.dark?.palette?.primary?.main
|
||||
Boolean(
|
||||
settings.theme?.light?.palette?.primary?.main ||
|
||||
settings.theme?.dark?.palette?.primary?.main
|
||||
)
|
||||
);
|
||||
|
||||
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
|
||||
updateSettings({
|
||||
theme: _merge(settings.theme, {
|
||||
theme: merge(settings.theme, {
|
||||
light: { palette: { primary: { main: light } } },
|
||||
dark: { palette: { primary: { main: dark } } },
|
||||
}),
|
||||
@@ -32,13 +33,13 @@ export default function Personalization({
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={customizedThemeColor}
|
||||
defaultChecked={customizedThemeColor}
|
||||
onChange={(e) => {
|
||||
setCustomizedThemeColor(e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
const newTheme = settings.theme;
|
||||
_unset(newTheme, "light.palette.primary.main");
|
||||
_unset(newTheme, "dark.palette.primary.main");
|
||||
unset(newTheme, "light.palette.primary.main");
|
||||
unset(newTheme, "dark.palette.primary.main");
|
||||
updateSettings({ theme: newTheme });
|
||||
}
|
||||
}}
|
||||
@@ -49,7 +50,7 @@ export default function Personalization({
|
||||
/>
|
||||
|
||||
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Loading style={{ height: "auto" }} />}>
|
||||
<ThemeColorPicker
|
||||
currentLight={settings.theme?.light?.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 _merge from "lodash/merge";
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
@@ -10,18 +11,29 @@ import {
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import {
|
||||
globalScope,
|
||||
themeAtom,
|
||||
themeOverriddenAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
|
||||
export default function Theme({
|
||||
settings,
|
||||
updateSettings,
|
||||
}: IUserSettingsChildProps) {
|
||||
const { theme, themeOverridden, setTheme, setThemeOverridden } =
|
||||
useAppContext();
|
||||
const [theme, setTheme] = useAtom(themeAtom, globalScope);
|
||||
const [themeOverridden, setThemeOverridden] = useAtom(
|
||||
themeOverriddenAtom,
|
||||
globalScope
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
<RadioGroup
|
||||
@@ -50,10 +62,10 @@ export default function Theme({
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={settings.theme?.dark?.palette?.darker}
|
||||
defaultChecked={Boolean(settings.theme?.dark?.palette?.darker)}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
theme: _merge(settings.theme, {
|
||||
theme: merge(settings.theme, {
|
||||
dark: { palette: { darker: e.target.checked } },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, createElement } from "react";
|
||||
import { use100vh } from "react-div-100vh";
|
||||
import { SwitchTransition } from "react-transition-group";
|
||||
import type { ISetupStep } from "./types";
|
||||
import type { ISetupStep } from "./SetupStep";
|
||||
|
||||
import {
|
||||
useMediaQuery,
|
||||
@@ -25,7 +25,7 @@ import Logo from "@src/assets/Logo";
|
||||
import ScrollableDialogContent from "@src/components/Modal/ScrollableDialogContent";
|
||||
import { SlideTransition } from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { analytics } from "analytics";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
const BASE_WIDTH = 1024;
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function SetupLayout({
|
||||
}
|
||||
|
||||
const nextStepId = steps[nextIncompleteStepIndex].id;
|
||||
analytics.logEvent("setup_step", { step: nextStepId });
|
||||
logEvent(analytics, "setup_step", { step: nextStepId });
|
||||
setStepId(nextStepId);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { signInWithPopup, GoogleAuthProvider, signOut } from "firebase/auth";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
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> {
|
||||
matchEmail?: string;
|
||||
@@ -13,12 +19,13 @@ export default function SignInWithGoogle({
|
||||
matchEmail,
|
||||
...props
|
||||
}: ISignInWithGoogleProps) {
|
||||
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
|
||||
const [status, setStatus] = useState<"IDLE" | "LOADING" | string>("IDLE");
|
||||
|
||||
const handleSignIn = async () => {
|
||||
setStatus("LOADING");
|
||||
try {
|
||||
const result = await auth.signInWithPopup(googleProvider);
|
||||
const result = await signInWithPopup(firebaseAuth, googleProvider);
|
||||
if (!result.user) throw new Error("Missing user");
|
||||
if (
|
||||
matchEmail &&
|
||||
@@ -28,7 +35,7 @@ export default function SignInWithGoogle({
|
||||
|
||||
setStatus("IDLE");
|
||||
} catch (error: any) {
|
||||
if (auth.currentUser) auth.signOut();
|
||||
if (firebaseAuth.currentUser) signOut(firebaseAuth);
|
||||
console.log(error);
|
||||
setStatus(error.message);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { useSnackbar } from "notistack";
|
||||
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 ThumbUpOffIcon from "@mui/icons-material/ThumbUpOffAlt";
|
||||
import ThumbDownIcon from "@mui/icons-material/ThumbDownAlt";
|
||||
import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
|
||||
|
||||
import { analytics } from "analytics";
|
||||
import { db } from "@src/firebase";
|
||||
import { routes } from "@src/constants/routes";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
import { globalScope } from "@src/atoms/globalScope";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { SETTINGS } from "config/dbPaths";
|
||||
|
||||
export default {
|
||||
id: "finish",
|
||||
@@ -24,16 +35,17 @@ export default {
|
||||
} as ISetupStep;
|
||||
|
||||
function StepFinish() {
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
db.doc("_rowy_/settings").update({ setupCompleted: true });
|
||||
}, []);
|
||||
updateDoc(doc(firebaseDb, SETTINGS), { setupCompleted: true });
|
||||
}, [firebaseDb]);
|
||||
const [rating, setRating] = useState<"up" | "down" | undefined>();
|
||||
|
||||
const handleRate = (e) => {
|
||||
setRating(e.target.value);
|
||||
analytics.logEvent("setup_rating", { rating: e.target.value });
|
||||
const handleRate: RadioGroupProps["onChange"] = (e) => {
|
||||
setRating(e.target.value as typeof rating);
|
||||
logEvent(analytics, "setup_rating", { rating: e.target.value });
|
||||
enqueueSnackbar("Thanks for your feedback!");
|
||||
};
|
||||
|
||||
@@ -80,7 +92,7 @@ function StepFinish() {
|
||||
color="primary"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={routes.auth}
|
||||
to={ROUTES.auth}
|
||||
>
|
||||
Sign in to your Rowy project
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { useSnackbar } from "notistack";
|
||||
import type { ISetupStep, ISetupStepBodyProps } from "../types";
|
||||
import type {
|
||||
ISetupStep,
|
||||
ISetupStepBodyProps,
|
||||
} from "@src/components/Setup/SetupStep";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -13,9 +17,9 @@ import CopyIcon from "@src/assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
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 {
|
||||
RULES_START,
|
||||
@@ -40,7 +44,7 @@ export default {
|
||||
} as ISetupStep;
|
||||
|
||||
function StepRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [adminRule, setAdminRule] = useState(true);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useAtom } from "jotai";
|
||||
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 CopyIcon from "@src/assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
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 {
|
||||
RULES_START,
|
||||
RULES_END,
|
||||
@@ -27,7 +31,7 @@ export default {
|
||||
const rules = RULES_START + REQUIRED_RULES + RULES_END;
|
||||
|
||||
function StepStorageRules({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
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 {
|
||||
FormControlLabel,
|
||||
@@ -9,7 +13,7 @@ import {
|
||||
} from "@mui/material";
|
||||
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||
|
||||
export default {
|
||||
id: "welcome",
|
||||
@@ -29,7 +33,7 @@ export default {
|
||||
} as ISetupStep;
|
||||
|
||||
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user