Merge pull request #1008 from rowyio/rc

Rc
This commit is contained in:
Shams
2022-12-09 08:38:00 +01:00
committed by GitHub
441 changed files with 25797 additions and 19241 deletions

File diff suppressed because it is too large Load Diff

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
yarnPath: .yarn/releases/yarn-1.22.19.cjs

View File

@@ -1,43 +1,71 @@
# Contributing
We welcome any feedback and contributions from the community. Please read the following guidelines to contribute to Rowy.
We welcome any feedback and contributions from the community. Please read the
following guidelines to contribute to Rowy.
## Code of conduct
We want to have a welcoming and inclusive environment, towards this please read and follow our [our code of conduct](https://github.com/rowyio/rowy/blob/main/CODE_OF_CONDUCT.md).
We want to have a welcoming and inclusive environment, towards this please read
and follow our
[our code of conduct](https://github.com/rowyio/rowy/blob/main/CODE_OF_CONDUCT.md).
## Getting started
Read the documentation on getting started [here](http://docs.rowy.io/). To get familiar with the project, [good first issues](https://github.com/rowyio/rowy/projects/3) is a good place to start.
Read the documentation on setting up your local development environment
[here](https://docs.rowy.io/setup/install#option-2-manual-install).
Read how to submit a pull request [here](https://docs.rowy.io/contributing).
To get familiar with the project,
[good first issues](https://github.com/rowyio/rowy/projects/3) is a good place
to start.
## Working on existing issues
Before you get started working on an [issue](https://github.com/rowyio/rowy/issues), please make sure to share that you are working on it by commenting on the issue and posting a message on #contributions channel in Rowy's [Discord](https://discord.com/invite/fjBugmvzZP). The maintainers will then assign the issue to you after making sure any relevant information or context in addition is provided before you can start on the task.
Once you are assigned a task, please provide periodic updates or share any questions or roadblocks on either discord or the Github issue, so that the commmunity or the project maintainers can provide you any feedback or guidance as needed. If you are inactive for more than 1-2 week on a issue that was assigned to you, then we will assume you have stopped working on it and we will unassign it from you - so that we can give a chance to others in the community to work on it.
## File a feature request
If you have some interesting idea that will be a good addition to Rowy, then create a new issue using [Feature Request Template](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=feature_request.md)
to share your idea. If you are working on this to contribute to the project, then let others in the community and project maintainers know by posting on #contributions channel in Rowy's [Discord](https://discord.com/invite/fjBugmvzZP).
This allows others in the community and the maintainers a chance to provide feedback and guidance before you spend time working on it.
If you have some interesting idea that will be a good addition to Rowy, then
create a new issue using
[Feature Request Template](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=feature_request.md)
to share your idea. If you are working on this to contribute to the project,
then let others in the community and project maintainers know by posting on
#contributions channel in Rowy's
[Discord](https://discord.com/invite/fjBugmvzZP). This allows others in the
community and the maintainers a chance to provide feedback and guidance before
you spend time working on it.
## Report an issue
You can report all issues through using [Report A Bug](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=bug_report.md) template. Please provide as much information as possible so that it can be resolved.
You can report all issues through using
[Report A Bug](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=bug_report.md)
template. Please provide as much information as possible so that it can be
resolved.
## Review
All submissions, including code and copy changes, will require review by project maintainers.
All submissions, including code and copy changes, will require review by project
maintainers.
## Create a pull request
When making any pull requests to the repository, please follow these instructions:
When making any pull requests to the repository, please follow these
instructions:
- Submit your PR to the develop branch
- Add as much information as possible in your PR's description including link to any related issues.
- Ensure all your commits have clear commit message along with comments in the code as required
Make sure youve read our technical documentation
[here](https://docs.rowy.io/contributing).
- Submit your PR to the `develop` branch
- Add as much information as possible in your PR's description including link to
any related issues.
- Ensure all your commits have clear commit message along with comments in the
code as required
## License
By contributing to Rowy, you agree that your contributions will be licensed with the same license that is specified in the repository you are contributing to.
By contributing to Rowy, you agree that your contributions will be licensed with
the same license that is specified in the repository you are contributing to.

View File

@@ -30,7 +30,8 @@ Connect to your database, manage data in table-UI with role based access control
<img width="100%" src="https://user-images.githubusercontent.com/307298/157184506-f94f3f5b-e6d3-49df-9a2c-f665511883f2.png" />
## Live Demo
💥 Check out the [live demo](https://demo.rowy.io/) of Rowy 💥
💥 Check out the [live demo](https://demo.rowy.io/) of Rowy 💥
## Quick Deploy
@@ -42,7 +43,7 @@ https://rowy.app
## Documentation
You can find the full documentation with how-to guides and templates
You can find the full documentation with how-to guides and templates
[here](http://docs.rowy.io/).
## Features
@@ -51,6 +52,7 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
<!-- <img width="85%" src="https://firebasestorage.googleapis.com/v0/b/rowyio.appspot.com/o/publicDemo%2FRowy%20Website%20Video%20GIF%20Small.gif?alt=media&token=3f699a8f-c1f2-4046-8ed5-e4ff66947cd8" />
-->
### Powerful spreadsheet interface for Firestore
- CMS for Firestore
@@ -62,22 +64,22 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
### Automate with cloud functions and ready made extensions
- Build cloud functions workflows on field level data changes
- Use any NPM modules or APIs
- Build cloud functions workflows on field level data changes
- Use any NPM modules or APIs
- Connect to your favourite tool with pre-built code blocks or create your own
- SendGrid, Algolia, Twilio, Bigquery and more
- SendGrid, Algolia, Twilio, Bigquery and more
### Rich and flexible data fields
- [30+ fields supported](https://docs.rowy.io/field-types/supported-fields)
- Basic types: Short Text, Long Text, Email, Phone, URL…
- Custom UI pickers: Date, Checkbox, Single Select, Multi Select…
- Uploaders: Image, File
- Rich Editors: JSON, Code, Rich Text (HTML), Markdown
- Basic types: Short Text, Long Text, Email, Phone, URL…
- Custom UI pickers: Date, Checkbox, Single Select, Multi Select…
- Uploaders: Image, File
- Rich Editors: JSON, Code, Rich Text (HTML), Markdown
- Data validation, default values, required fields
- Action field: Clickable trigger for any Cloud Function
- Aggregate field: Populate cell with value aggregated from the rows sub-table
- Connector field: Connect data from multiple table collections
- Connector field: Connect data from multiple table collections
- Connect Service: Get data from any HTTP endpoint
### Collaborate with your team
@@ -89,7 +91,8 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
## Install
Set up Rowy on your Google Cloud project with this one-click deploy button. Your data and cloud functions stay on your own Firestore/GCP.
Set up Rowy on your Google Cloud project with this one-click deploy button. Your
data and cloud functions stay on your own Firestore/GCP.
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://rowy.app/)
@@ -105,16 +108,24 @@ Alternatively, you can manually install by
## Roadmap
[View our roadmap](https://demo.rowy.io/table/roadmap) on Rowy - Upvote, downvote, share your thoughts!
[View our roadmap](https://demo.rowy.io/table/roadmap) on Rowy - Upvote,
downvote, share your thoughts!
If you'd like to propose a feature, submit an issue [here](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=feature_request.md&title=).
If you'd like to propose a feature, submit an issue
[here](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=feature_request.md&title=).
## Support the project
- Join a community of developers on [Discord](https://discord.gg/fjBugmvzZP) and share your ideas/feedback 💬
- Follow us on [Twitter](https://twitter.com/rowyio) and help [spread the word](https://twitter.com/intent/tweet?text=Check%20out%20@rowyio%20-%20It%27s%20like%20an%20open-source%20Airtable%20for%20your%20database,%20but%20with%20a%20built-in%20code%20editor%20for%20cloud%20functions%20to%20run%20on%20data%20CRUD!%0a%0aEsp%20if%20building%20on%20@googlecloud%20and%20@Firebase%20stack,%20it%20is%20the%20fastest%20way%20to%20build%20your%20product.%20Live%20demo:%20https://demo.rowy.io) 🙏
- Join a community of developers on [Discord](https://discord.gg/fjBugmvzZP) and
share your ideas/feedback 💬
- Follow us on [Twitter](https://twitter.com/rowyio) and help
[spread the word](https://twitter.com/intent/tweet?text=Check%20out%20@rowyio%20-%20It%27s%20like%20an%20open-source%20Airtable%20for%20your%20database,%20but%20with%20a%20built-in%20code%20editor%20for%20cloud%20functions%20to%20run%20on%20data%20CRUD!%0a%0aEsp%20if%20building%20on%20@googlecloud%20and%20@Firebase%20stack,%20it%20is%20the%20fastest%20way%20to%20build%20your%20product.%20Live%20demo:%20https://demo.rowy.io)
🙏
- Give us a star to this Github repo ⭐️
- Submit a PR. Take a look at our [contribution guide](https://github.com/rowyio/rowy/blob/main/CONTRIBUTING.md) and get started with [good first issues](https://github.com/rowyio/rowy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
- Submit a PR. Take a look at our
[contribution guide](https://github.com/rowyio/rowy/blob/main/CONTRIBUTING.md)
and get started with
[good first issues](https://github.com/rowyio/rowy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Help

View File

@@ -1,6 +1,6 @@
{
"name": "rowy",
"version": "2.6.1",
"version": "3.0.0",
"homepage": "https://rowy.io",
"repository": {
"type": "git",
@@ -8,17 +8,17 @@
},
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mdi/js": "^6.6.96",
"@monaco-editor/react": "^4.4.4",
"@mui/icons-material": "5.6.0",
"@mui/icons-material": "^5.10.16",
"@mui/lab": "^5.0.0-alpha.76",
"@mui/material": "5.6.0",
"@mui/styles": "5.6.0",
"@mui/material": "^5.10.16",
"@mui/x-date-pickers": "^5.0.0-alpha.4",
"@rowy/form-builder": "^0.6.2",
"@rowy/multiselect": "^0.4.0",
"@rowy/form-builder": "^0.8.0",
"@rowy/multiselect": "^0.4.1",
"@tanstack/react-table": "^8.5.15",
"@tinymce/tinymce-react": "^3",
"@uiw/react-md-editor": "^3.14.1",
"algoliasearch": "^4.13.1",
@@ -29,28 +29,29 @@
"date-fns": "^2.28.0",
"dompurify": "^2.3.6",
"file-saver": "^2.0.5",
"firebase": "^9.6.11",
"firebase": "^9.12.1",
"firebaseui": "^6.0.1",
"jotai": "^1.7.2",
"jotai": "^1.8.4",
"json-stable-stringify-without-jsonify": "^1.0.1",
"json2csv": "^5.0.7",
"jszip": "^3.10.0",
"lodash-es": "^4.17.21",
"match-sorter": "^6.3.1",
"material-ui-popup-state": "^4.0.1",
"mdi-material-ui": "^7.3.0",
"monaco-editor-auto-typings": "^0.4.0",
"notistack": "^2.0.4",
"path-browserify": "^1.0.1",
"pb-util": "^1.0.3",
"quicktype-core": "^6.0.71",
"react": "^18.0.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.0",
"react-color-palette": "^6.2.0",
"react-data-grid": "7.0.0-beta.5",
"react-detect-offline": "^2.4.5",
"react-div-100vh": "^0.7.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.0.0",
"react-dom": "^18.2.0",
"react-dropzone": "^10",
"react-element-scroll-hook": "^1.1.0",
"react-error-boundary": "^3.1.4",
@@ -59,21 +60,24 @@
"react-image": "^4",
"react-json-view": "^1.21.3",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.3.0",
"react-router-dom": "6.3.0",
"react-router-hash-link": "^2.4.3",
"react-scripts": "^5.0.0",
"react-scripts": "^5.0.1",
"react-usestateref": "^1.0.8",
"react-virtual": "^2.10.4",
"remark-gfm": "^3.0.1",
"seedrandom": "^3.0.5",
"stream-browserify": "^3.0.0",
"swr": "^1.3.0",
"tinymce": "^5",
"tss-react": "^3.6.2",
"typescript": "^4.6.3",
"tss-react": "^4.4.4",
"typescript": "^4.9.3",
"use-algolia": "^1.5.3",
"use-async-memo": "^1.2.4",
"use-debounce": "^8.0.0",
"use-memo-value": "^1.0.1",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"workbox-webpack-plugin": "^6.5.4"
},
"scripts": {
"start": "cross-env PORT=7699 craco start",
@@ -86,7 +90,7 @@
"env": "node createDotEnv",
"target": "firebase target:apply hosting rowy",
"deploy": "firebase deploy --only hosting",
"typedoc": "typedoc src/atoms/tableScope/index.ts src/atoms/globalScope/index.ts --out typedoc"
"typedoc": "typedoc --options typedoc.json"
},
"engines": {
"node": ">=16"
@@ -153,19 +157,22 @@
"@types/json2csv": "^5.0.3",
"@types/lodash-es": "^4.17.6",
"@types/node": "^17.0.23",
"@types/react": "^18.0.5",
"@types/react": "^18.0.25",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-detect-offline": "^2.4.1",
"@types/react-div-100vh": "^0.4.0",
"@types/react-dom": "^18.0.0",
"@types/react-dom": "^18.0.9",
"@types/react-router-dom": "^5.3.3",
"@types/react-router-hash-link": "^2.4.5",
"@typescript-eslint/parser": "^5.18.0",
"@types/seedrandom": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"craco-alias": "^3.0.1",
"craco-swc": "^0.5.1",
"cross-env": "^7.0.3",
"eslint": "^8.12.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-react-app": "^7.0.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-local-rules": "^1.1.0",
"eslint-plugin-no-relative-import-paths": "^1.2.0",
"eslint-plugin-tsdoc": "^0.2.16",
@@ -176,7 +183,7 @@
"raw-loader": "^4.0.2",
"source-map-explorer": "^2.5.2",
"ts-jest": "^28.0.2",
"typedoc": "^0.22.17"
"typedoc": "^0.23.21"
},
"resolutions": {
"@types/react": "^18"
@@ -184,5 +191,6 @@
"lint-staged": {
"*.{js,ts,tsx}": "eslint --cache --fix",
"**/*": "prettier --write --ignore-unknown"
}
},
"packageManager": "yarn@1.22.19"
}

View File

@@ -6,16 +6,7 @@
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<meta
name="theme-color"
content="#FAF9FB"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="#0F0F12"
media="(prefers-color-scheme: dark)"
/>
<meta name="color-scheme" content="default" />
<meta name="apple-mobile-web-app-capable" content="yes" />

View File

@@ -13,7 +13,5 @@
"type": "image/png"
}
],
"theme_color": "#4200ff",
"background_color": "#4200ff",
"display": "standalone"
}

View File

@@ -2,23 +2,26 @@ import { lazy, Suspense } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { useAtom } from "jotai";
import { Backdrop } from "@mui/material";
import Loading from "@src/components/Loading";
import ProjectSourceFirebase from "@src/sources/ProjectSourceFirebase";
import MembersSourceFirebase from "@src/sources/MembersSourceFirebase";
import ConfirmDialog from "@src/components/ConfirmDialog";
import RowyRunModal from "@src/components/RowyRunModal";
import NotFound from "@src/pages/NotFoundPage";
import RequireAuth from "@src/layouts/RequireAuth";
import AdminRoute from "@src/layouts/AdminRoute";
import {
globalScope,
projectScope,
currentUserAtom,
userRolesAtom,
altPressAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { ROUTES } from "@src/constants/routes";
import useKeyPressWithAtom from "@src/hooks/useKeyPressWithAtom";
import TableGroupRedirectPage from "./pages/TableGroupRedirectPage";
import JotaiTestPage from "@src/pages/Test/JotaiTestPage";
import SignOutPage from "@src/pages/Auth/SignOutPage";
// prettier-ignore
@@ -44,6 +47,8 @@ const TablesPage = lazy(() => import("@src/pages/TablesPage" /* webpackChunkName
const ProvidedTablePage = lazy(() => import("@src/pages/Table/ProvidedTablePage" /* webpackChunkName: "ProvidedTablePage" */));
// prettier-ignore
const ProvidedSubTablePage = lazy(() => import("@src/pages/Table/ProvidedSubTablePage" /* webpackChunkName: "ProvidedSubTablePage" */));
// prettier-ignore
const TableTutorialPage = lazy(() => import("@src/pages/Table/TableTutorialPage" /* webpackChunkName: "TableTutorialPage" */));
// prettier-ignore
const FunctionPage = lazy(() => import("@src/pages/FunctionPage" /* webpackChunkName: "FunctionPage" */));
@@ -52,21 +57,19 @@ const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettingsPage
// prettier-ignore
const ProjectSettingsPage = lazy(() => import("@src/pages/Settings/ProjectSettingsPage" /* webpackChunkName: "ProjectSettingsPage" */));
// prettier-ignore
const UserManagementPage = lazy(() => import("@src/pages/Settings/UserManagementPage" /* webpackChunkName: "UserManagementPage" */));
const MembersPage = lazy(() => import("@src/pages/Settings/MembersPage" /* webpackChunkName: "MembersPage" */));
// prettier-ignore
const DebugSettingsPage = lazy(() => import("@src/pages/Settings/DebugSettingsPage" /* webpackChunkName: "DebugSettingsPage" */));
// prettier-ignore
const ThemeTestPage = lazy(() => import("@src/pages/Test/ThemeTestPage" /* webpackChunkName: "ThemeTestPage" */));
// const RowyRunTestPage = lazy(() => import("@src/pages/RowyRunTestPage" /* webpackChunkName: "RowyRunTestPage" */));
const DebugPage = lazy(() => import("@src/pages/Settings/DebugPage" /* webpackChunkName: "DebugPage" */));
export default function App() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
useKeyPressWithAtom("Alt", altPressAtom, globalScope);
const [currentUser] = useAtom(currentUserAtom, projectScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
useKeyPressWithAtom("Alt", altPressAtom, projectScope);
return (
<Suspense fallback={<Loading fullScreen />}>
<ProjectSourceFirebase />
{userRoles.includes("ADMIN") && <MembersSourceFirebase />}
<ConfirmDialog />
<RowyRunModal />
@@ -114,7 +117,21 @@ export default function App() {
<Route index element={<NotFound />} />
<Route
path=":docPath/:subTableKey"
element={<ProvidedSubTablePage />}
element={
<Suspense
fallback={
<Backdrop
key="sub-table-modal-backdrop"
open
sx={{ zIndex: "modal" }}
>
<Loading />
</Backdrop>
}
>
<ProvidedSubTablePage />
</Suspense>
}
/>
</Route>
</Route>
@@ -125,6 +142,11 @@ export default function App() {
<Route path=":id" element={<TableGroupRedirectPage />} />
</Route>
<Route
path={ROUTES.tableTutorial}
element={<TableTutorialPage />}
/>
<Route path={ROUTES.function}>
<Route
index
@@ -139,22 +161,16 @@ export default function App() {
<Route path={ROUTES.userSettings} element={<UserSettingsPage />} />
<Route
path={ROUTES.projectSettings}
element={<ProjectSettingsPage />}
element={
<AdminRoute>
<ProjectSettingsPage />
</AdminRoute>
}
/>
<Route
path={ROUTES.userManagement}
element={<UserManagementPage />}
/>
<Route
path={ROUTES.debugSettings}
element={<DebugSettingsPage />}
/>
{/* <Route path={ROUTES.rowyRunTest} element={<RowyRunTestPage />} /> */}
<Route path={ROUTES.members} element={<MembersPage />} />
<Route path="/test/jotai" element={<JotaiTestPage />} />
<Route path={ROUTES.debug} element={<DebugPage />} />
</Route>
<Route path={ROUTES.themeTest} element={<ThemeTestPage />} />
</Routes>
)}
</Suspense>

View File

@@ -3,11 +3,11 @@ import ErrorFallback from "@src/components/ErrorFallback";
// import SwrProvider from "@src/contexts/SwrContext";
import { BrowserRouter } from "react-router-dom";
import { HelmetProvider } from "react-helmet-async";
import { Provider, Atom } from "jotai";
import { globalScope } from "@src/atoms/globalScope";
import { Provider as JotaiProvider, Atom } from "jotai";
import { projectScope } from "@src/atoms/projectScope";
import { DebugAtoms } from "@src/atoms/utils";
import LocalizationProvider from "@mui/lab/LocalizationProvider";
import AdapterDateFns from "@mui/lab/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import RowyThemeProvider from "@src/theme/RowyThemeProvider";
@@ -29,15 +29,26 @@ export default function Providers({
initialAtomValues,
}: IProvidersProps) {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ErrorBoundary
fallbackRender={
// Cant use <ErrorFallback> here because it uses useLocation,
// which needs to be inside a <Router>
({ error }) => (
<div role="alert">
<h1>Something went wrong</h1>
<p>{error.message}</p>
</div>
)
}
>
<BrowserRouter>
<HelmetProvider>
<Provider
key={globalScope.description}
scope={globalScope}
<JotaiProvider
key={projectScope.description}
scope={projectScope}
initialValues={initialAtomValues}
>
<DebugAtoms scope={globalScope} />
<DebugAtoms scope={projectScope} />
<LocalizationProvider dateAdapter={AdapterDateFns}>
<CacheProvider value={muiCache}>
<RowyThemeProvider>
@@ -53,7 +64,7 @@ export default function Providers({
</RowyThemeProvider>
</CacheProvider>
</LocalizationProvider>
</Provider>
</JotaiProvider>
</HelmetProvider>
</BrowserRouter>
</ErrorBoundary>

View File

@@ -20,14 +20,49 @@ export default function Logo({ size = 1.5, ...props }: ILogoProps) {
>
<title id="rowy-logo-title">Rowy</title>
<defs>
<linearGradient x1="83.349%" y1="50%" x2="0%" y2="50%" id="rowy-b">
<stop stopColor="#F0A" offset="0%" />
<stop stopColor="#FA0" offset="100%" />
</linearGradient>
<linearGradient x1="50%" y1="16.276%" x2="50%" y2="100%" id="rowy-c">
<stop stopColor="#4200FF" offset="0%" />
<stop stopColor="#F0A" offset="100%" />
</linearGradient>
<linearGradient x1="83.052%" y1="50%" x2="0%" y2="50%" id="rowy-d">
<stop stopColor="#0AF" offset="0%" />
<stop stopColor="#0FA" offset="100%" />
</linearGradient>
<linearGradient x1="100%" y1="50%" x2="16.614%" y2="50%" id="rowy-e">
<stop stopColor="#0AF" offset="0%" />
<stop stopColor="#4200FF" offset="100%" />
</linearGradient>
</defs>
<path
d="M6 10v6H3a3 3 0 010-6h3zm-1 5v-4H3a2 2 0 00-1.995 1.85L1 13a2 2 0 001.85 1.995L3 15h2z"
fill="url(#rowy-b)"
transform="rotate(-90 3 13)"
/>
<path
d="M6 6H0V3a3 3 0 116 0v3zM1 5h4V3a2 2 0 00-1.85-1.995L3 1a2 2 0 00-1.995 1.85L1 3v2z"
fill="#4200FF"
/>
<path d="M6 5v6H0V5h6zM5 6H1v4h4V6z" fill="url(#rowy-c)" />
<path
d="M16 0v6h-3a3 3 0 010-6h3zm-1.001 5V1H13a2 2 0 00-1.995 1.85L11 3a2 2 0 001.85 1.995L13 5h1.999z"
fill="url(#rowy-d)"
transform="rotate(-180 13 3)"
/>
<path
d="M11 0v6H5V3a3 3 0 013-3h3zm-1 1H8a2 2 0 00-1.995 1.85L6 3v2h4V1z"
fill="url(#rowy-e)"
/>
<path
d="M58 3l4 9 4-9h2l-7 16h-2l2-4.5L56 3h2zm-26-.25a6.25 6.25 0 110 12.5 6.25 6.25 0 010-12.5zM26 3v2h-4v10h-2V3h6zm14 0l3 9 3-9h2l3 9 3-9h2l-4 12h-2l-3-9-3 9h-2L38 3h2zm-8 1.75a4.25 4.25 0 100 8.5 4.25 4.25 0 000-8.5z"
fill={theme.palette.text.primary}
/>
<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={theme.palette.primary.main}
/>
</svg>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1,4 +1,4 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<svg width="16" height="16" viewBox="0 0 16 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" />

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 485 B

View File

@@ -94,6 +94,18 @@ export { Table };
import { Leaf } from "mdi-material-ui";
export { Leaf };
import { FormatListChecks } from "mdi-material-ui";
export { FormatListChecks as Checklist };
import { FileTableBoxOutline } from "mdi-material-ui";
export { FileTableBoxOutline as Project };
import { TableColumn } from "mdi-material-ui";
export { TableColumn };
import { DragVertical } from "mdi-material-ui";
export { DragVertical };
export * from "./AddRow";
export * from "./AddRowTop";
export * from "./ChevronDown";

View File

@@ -1,5 +1,5 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 23">
<g clip-path="url(#a)">
<g clip-path="url(#sticker)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 15V5A5 5 0 0 1 7.5.669 4.977 4.977 0 0 1 10 0h5a5 5 0 0 1 0 10h-5v5a5 5 0 0 1-10 0Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 5a2 2 0 0 1 2-2h6c.721 0 1.354.382 1.705.955A8.212 8.212 0 0 1 34 2.75c1.573 0 3.044.44 4.295 1.205A2 2 0 0 1 40 3h2a2 2 0 0 1 1.897 1.368L45 7.675l1.103-3.307A2 2 0 0 1 48 3h2a2 2 0 0 1 1.897 1.368L53 7.675l1.103-3.307A2 2 0 0 1 56 3h4a2 2 0 0 1 1.828 1.188L64 9.076l2.172-4.888A2 2 0 0 1 68 3h2a2 2 0 0 1 1.832 2.802l-7 16A2 2 0 0 1 63 23h-2a2 2 0 0 1-1.828-2.812l1.643-3.697-2.568-5.907-2.35 7.049A2 2 0 0 1 54 19h-2a2 2 0 0 1-1.897-1.367L49 14.325l-1.103 3.308A2 2 0 0 1 46 19h-2a2 2 0 0 1-1.897-1.367l-.88-2.642A8.253 8.253 0 0 1 26 13.024V17a2 2 0 0 1-2.001 2h-2a2 2 0 0 1-2-2V5Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34 4.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.5ZM22 17V5h6v2h-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="#000"/>
@@ -28,7 +28,7 @@
<stop stop-color="#0AF"/>
<stop offset="1" stop-color="#4200FF"/>
</linearGradient>
<clipPath id="a">
<clipPath id="sticker">
<path fill="#fff" d="M0 0h72v23H0z"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,37 @@
<svg width="117" height="120" viewBox="0 0 117 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M104.634 68.6831C104.634 80.1972 100.53 90.6702 93.6709 98.8771C85.0353 109.228 72.0513 115.781 57.4749 115.781C43.511 115.781 31.0169 109.717 22.3813 100.102C14.9706 91.7726 10.4385 80.7484 10.4385 68.6831C10.4385 42.6538 31.5069 21.5854 57.5361 21.5854C83.5654 21.5854 104.634 42.6538 104.634 68.6831Z" stroke="#3E4450" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 4"/>
<path d="M74.8781 71.7725C74.5226 73.2834 73.9893 74.8833 73.3671 76.2164C71.6785 79.5049 69.0121 82.0824 65.7236 83.7711C62.3462 85.4598 58.3467 86.1708 54.3471 85.282C44.926 83.3267 38.8823 74.0833 40.8376 64.6622C42.7929 55.2411 51.9474 49.1085 61.3685 51.1527C64.7459 51.8637 67.6789 53.5524 70.1675 55.8632C74.3448 60.0405 76.1224 66.0843 74.8781 71.7725Z" fill="#3E4450" stroke="#949BAB" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.4127 66.7063H60.4132C59.8609 66.7063 59.4132 66.2586 59.4132 65.7063V62.7067C59.4132 61.9068 58.791 61.1958 57.9023 61.1958C57.1024 61.1958 56.3913 61.818 56.3913 62.7067V65.7063C56.3913 66.2586 55.9436 66.7063 55.3913 66.7063H52.3918C51.5919 66.7063 50.8809 67.3284 50.8809 68.2172C50.8809 69.106 51.503 69.7281 52.3918 69.7281H55.3913C55.9436 69.7281 56.3913 70.1759 56.3913 70.7281V73.7277C56.3913 74.5276 57.0135 75.2386 57.9023 75.2386C58.7022 75.2386 59.4132 74.6165 59.4132 73.7277V70.7281C59.4132 70.1759 59.8609 69.7281 60.4132 69.7281H63.4127C64.2126 69.7281 64.9237 69.106 64.9237 68.2172C64.9237 67.3284 64.2126 66.7063 63.4127 66.7063Z" fill="#C7CDDB"/>
<path d="M78.3516 21.6057C78.3516 27.1603 76.1318 32.0976 72.6787 35.8007C72.4321 36.171 72.0621 36.4178 71.6921 36.6647C67.9924 40.1209 63.0594 42.2192 57.6332 42.2192C53.3169 42.2192 49.2472 40.8615 45.9174 38.5162C44.9308 37.899 44.0675 37.035 43.2043 36.2944C39.3812 32.5914 37.0381 27.4071 37.0381 21.6057C37.0381 10.2498 46.2874 0.992188 57.6332 0.992188C69.1023 0.992188 78.3516 10.2498 78.3516 21.6057Z" fill="black"/>
<path d="M78.3516 21.6043C78.3516 27.6526 75.7618 32.9603 71.6921 36.7868C67.9924 40.2429 63.0594 42.3413 57.6332 42.3413C53.3169 42.3413 49.2472 40.9835 45.9174 38.6383C40.4912 34.9353 37.0381 28.7635 37.0381 21.7278C37.0381 10.3718 46.2874 1.11426 57.6332 1.11426C68.979 1.11426 78.3516 10.2484 78.3516 21.6043Z" fill="#1F2229" stroke="#3E4450" stroke-width="2" stroke-miterlimit="10"/>
<path d="M72.679 35.8005C72.4324 36.1708 72.0624 36.4176 71.6924 36.6645C67.9927 40.1207 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8613 45.9177 38.516C44.9311 37.8988 44.0679 37.0348 43.2046 36.2942C43.6979 35.677 44.4378 35.3067 46.041 34.6895L46.6577 34.4427C47.8909 33.9489 49.6174 33.3318 51.8373 32.3443C52.2072 32.2209 52.4539 31.974 52.7005 31.7271C52.8239 31.6037 52.9472 31.4803 52.9472 31.2334C53.0705 30.9865 53.1938 30.6162 53.1938 30.3693V26.1726C53.0705 26.0492 53.0705 26.0492 52.9472 25.9257C52.5772 25.432 52.3306 24.8148 52.3306 24.0742L52.0839 23.9508C50.974 24.1976 51.0973 23.0867 50.8507 20.8649C50.7274 20.0009 50.8507 19.754 51.344 19.6306L51.7139 19.1368C50.974 17.4088 50.604 15.8041 50.604 14.5698C50.604 12.4714 51.4673 11.1136 52.7005 10.4964C51.9606 9.01522 51.9606 8.52148 51.9606 8.52148C51.9606 8.52148 56.2769 9.26209 57.7568 9.01522C59.6067 8.64492 62.5664 9.13866 63.6764 11.6073C65.5262 12.3479 66.1428 13.4589 66.3895 14.6932C66.6361 16.6681 65.5262 18.7665 65.2796 19.6306V19.754C65.5262 19.8774 65.6495 20.1243 65.5262 20.9883C65.2796 23.0867 65.2796 24.3211 64.293 24.0742L63.3064 25.8023C63.3064 26.0492 63.3064 26.0492 63.1831 26.1726C63.1831 26.5429 63.1831 27.1601 63.1831 30.4928C63.1831 30.8631 63.3064 31.3568 63.553 31.6037C63.6764 31.7271 63.6764 31.8506 63.7997 31.8506C64.0463 32.0974 64.293 32.3443 64.5396 32.3443C67.0061 33.3318 68.7326 34.0724 70.0892 34.5661C71.3224 35.0599 72.1857 35.4302 72.679 35.8005Z" fill="#2E333D"/>
<path d="M72.679 35.8004C72.4324 36.1707 72.0624 36.4176 71.6924 36.6644C67.9927 40.1206 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8612 45.9177 38.5159C44.9311 37.8988 44.0679 37.0347 43.2046 36.2941C43.6979 35.6769 44.4378 35.3066 46.041 34.6895L46.6577 34.4426C47.8909 33.9489 49.6174 33.3317 51.8373 32.3442C52.2072 32.2208 52.4539 31.9739 52.7005 31.7271C53.9338 33.4551 55.907 34.566 58.2501 34.566C60.4699 34.566 62.4431 33.4551 63.6764 31.8505C63.923 32.0974 64.1697 32.3442 64.4163 32.3442C66.8828 33.3317 68.6093 34.0723 69.9659 34.566C71.3224 35.0598 72.1857 35.4301 72.679 35.8004Z" fill="#3E4450"/>
<path d="M65.1564 19.5071C65.2797 19.0134 65.0331 18.2728 64.7864 17.9025C64.7864 17.7791 64.6631 17.7791 64.6631 17.6556C63.7999 15.9275 61.95 15.3104 60.2235 15.1869C55.6605 14.9401 55.2905 15.8041 53.9339 14.5698C54.4272 15.1869 54.4272 16.2978 53.6873 17.5322C53.194 18.3962 52.3307 18.89 51.4675 19.1368C49.371 14.4463 50.4809 11.4839 52.454 10.4964C51.7141 9.01522 51.7141 8.52148 51.7141 8.52148C51.7141 8.52148 56.0304 9.26209 57.5103 9.01522C59.3602 8.64492 62.32 9.13866 63.4299 11.6073C65.2797 12.3479 65.8964 13.4589 66.143 14.6932C66.513 16.5447 65.4031 18.6431 65.1564 19.5071Z" fill="#949BAB"/>
<path d="M53.317 30.3692V26.1724C53.1936 26.049 53.1936 26.049 53.0703 25.9256V25.6787C53.317 26.049 53.5636 26.4193 53.9336 26.6662L57.2633 29.0114C58.0033 29.6286 59.1132 29.6286 59.8531 29.0114L62.9362 26.2959C63.0595 26.1724 63.1829 26.1724 63.3062 26.049C63.3062 26.4193 63.3062 27.0365 63.3062 30.3692C63.3062 30.6161 63.3062 30.7395 63.4295 30.9864H53.317C53.1936 30.7395 53.317 30.6161 53.317 30.3692Z" fill="url(#paint0_linear)"/>
<path d="M115.285 97.8074C115.285 103.362 113.065 108.299 109.612 112.002C109.365 112.373 108.995 112.619 108.625 112.866C104.925 116.323 99.9925 118.421 94.5663 118.421C90.25 118.421 86.1803 117.063 82.8505 114.718C81.8639 114.101 81.0007 113.237 80.1374 112.496C76.3143 108.793 73.9712 103.609 73.9712 97.8074C73.9712 86.4514 83.2205 77.1938 94.5663 77.1938C106.035 77.1938 115.285 86.4514 115.285 97.8074Z" fill="black"/>
<path d="M115.285 97.8065C115.285 103.855 112.695 109.162 108.625 112.989C104.925 116.445 99.9925 118.543 94.5663 118.543C90.25 118.543 86.1803 117.186 82.8505 114.84C77.4243 111.137 73.9712 104.966 73.9712 97.9299C73.9712 86.574 83.2205 77.3164 94.5663 77.3164C105.912 77.3164 115.285 86.4505 115.285 97.8065Z" fill="#1F2229" stroke="#3E4450" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.613 112.003C109.366 112.373 108.996 112.62 108.626 112.867C104.927 116.323 99.9938 118.421 94.5676 118.421C90.2512 118.421 86.1815 117.063 82.8518 114.718C81.8652 114.101 81.0019 113.237 80.1387 112.496C80.632 111.879 81.3719 111.509 82.9751 110.892L83.5917 110.645C84.825 110.151 86.5515 109.534 88.7713 108.546C89.1413 108.423 89.388 108.176 89.6346 107.929C89.7579 107.806 89.8813 107.682 89.8813 107.436C90.0046 107.189 90.1279 106.818 90.1279 106.571V102.375C90.0046 102.251 90.0046 102.251 89.8813 102.128C89.5113 101.634 89.2646 101.017 89.2646 100.276L89.018 100.153C87.9081 100.4 88.0314 99.2889 87.7848 97.0671C87.6614 96.203 87.7848 95.9562 88.278 95.8327L88.648 95.339C87.9081 93.6109 87.5381 92.0063 87.5381 90.7719C87.5381 88.6735 88.4014 87.3158 89.6346 86.6986C88.8947 85.2174 88.8947 84.7236 88.8947 84.7236C88.8947 84.7236 93.211 85.4642 94.6909 85.2174C96.5408 84.8471 99.5005 85.3408 100.61 87.8095C102.46 88.5501 103.077 89.661 103.324 90.8953C103.57 92.8703 102.46 94.9687 102.214 95.8327V95.9562C102.46 96.0796 102.584 96.3265 102.46 97.1905C102.214 99.2889 102.214 100.523 101.227 100.276L100.24 102.004C100.24 102.251 100.24 102.251 100.117 102.375C100.117 102.745 100.117 103.362 100.117 106.695C100.117 107.065 100.24 107.559 100.487 107.806C100.61 107.929 100.61 108.053 100.734 108.053C100.98 108.3 101.227 108.546 101.474 108.546C103.94 109.534 105.667 110.275 107.023 110.768C108.257 111.262 109.12 111.632 109.613 112.003Z" fill="#2E333D"/>
<path d="M109.612 112.003C109.365 112.373 108.995 112.62 108.626 112.867C104.926 116.323 99.9928 118.421 94.5666 118.421C90.2503 118.421 86.1806 117.063 82.8508 114.718C81.8642 114.101 81.001 113.237 80.1377 112.496C80.631 111.879 81.3709 111.509 82.9741 110.892L83.5908 110.645C84.824 110.151 86.5505 109.534 88.7704 108.546C89.1403 108.423 89.387 108.176 89.6336 107.929C90.8669 109.657 92.8401 110.768 95.1832 110.768C97.403 110.768 99.3762 109.657 100.609 108.053C100.856 108.3 101.103 108.546 101.349 108.546C103.816 109.534 105.542 110.274 106.899 110.768C108.256 111.262 109.119 111.632 109.612 112.003Z" fill="#3E4450"/>
<path d="M102.09 95.7093C102.213 95.2155 101.966 94.4749 101.72 94.1046C101.72 93.9812 101.596 93.9812 101.596 93.8578C100.733 92.1297 98.8831 91.5125 97.1566 91.3891C92.5936 91.1422 92.2236 92.0063 90.867 90.7719C91.3603 91.3891 91.3603 92.5 90.6204 93.7343C90.1271 94.5984 89.2638 95.0921 88.4006 95.339C86.3041 90.6485 87.414 87.6861 89.3872 86.6986C88.6472 85.2174 88.6472 84.7236 88.6472 84.7236C88.6472 84.7236 92.9635 85.4642 94.4434 85.2174C96.2933 84.8471 99.2531 85.3408 100.363 87.8095C102.213 88.5501 102.829 89.661 103.076 90.8953C103.446 92.7469 102.336 94.8452 102.09 95.7093Z" fill="#949BAB"/>
<path d="M90.2501 106.571V102.375C90.1267 102.251 90.1267 102.251 90.0034 102.128V101.881C90.2501 102.251 90.4967 102.621 90.8667 102.868L94.1964 105.214C94.9364 105.831 96.0463 105.831 96.7862 105.214L99.8693 102.498C99.9927 102.375 100.116 102.375 100.239 102.251C100.239 102.621 100.239 103.239 100.239 106.571C100.239 106.818 100.239 106.942 100.363 107.189H90.2501C90.1267 106.942 90.2501 106.818 90.2501 106.571Z" fill="url(#paint1_linear)"/>
<path d="M41.2036 98.1168C41.2036 103.918 38.7371 109.102 34.7908 112.805C33.6808 113.793 32.5709 114.657 31.2144 115.398C28.2546 117.126 24.8015 118.113 21.1018 118.113C17.4021 118.113 13.949 117.126 10.9892 115.398C10.4959 115.151 10.126 114.904 9.63268 114.534C4.45307 110.954 1 104.906 1 98.1168C1 87.0077 10.0026 78.1204 20.9785 78.1204C32.201 77.997 41.2036 87.0077 41.2036 98.1168Z" fill="#1F2229" stroke="#3E4450" stroke-width="2" stroke-miterlimit="10"/>
<path d="M17.0323 102.56C17.279 102.806 17.5256 103.177 17.8956 103.424C18.1422 103.67 18.3889 103.794 18.6355 104.041C18.7589 104.164 19.0055 104.288 19.1288 104.411C19.1288 104.411 19.2522 104.411 19.2522 104.534L19.3755 104.658V106.386C19.3755 106.386 19.3755 106.386 19.2522 106.263C19.1288 106.139 18.8822 106.016 18.7589 105.892C18.5122 105.769 18.2656 105.522 18.0189 105.399C17.8956 105.399 17.8956 105.275 17.7723 105.275C16.909 104.781 16.1691 104.288 16.1691 103.67C16.2924 103.424 16.539 103.053 17.0323 102.56ZM34.1878 112.107C33.4478 110.502 32.4478 108.978 30.7213 108.114C29.858 107.744 28.8714 107.373 27.8848 107.373C27.6382 107.373 27.2682 107.373 27.0216 107.373C26.8982 107.373 26.7749 107.373 26.6516 107.373C25.295 107.25 25.1717 107.003 25.1717 107.003V104.164C26.035 103.424 26.8982 102.56 27.6382 101.695C28.2548 100.831 28.7481 99.844 28.9947 98.6096C30.1047 98.3628 30.8446 97.3753 30.7213 96.1409C30.7213 95.6472 30.3513 95.1535 30.3513 94.6597C30.3513 94.4129 30.3513 94.166 30.3513 93.9191C30.3513 93.7957 30.3513 93.5488 30.3513 93.4254C30.3513 93.3019 30.3513 93.0551 30.3513 92.9316C30.228 92.0676 29.9813 91.2036 29.488 90.2161C28.0082 87.5005 25.295 85.7725 22.0886 85.7725C21.472 85.7725 20.8554 85.8959 20.2387 86.0193C19.1288 86.2662 18.0189 86.7599 17.1556 87.5005C17.0323 87.624 16.7857 87.7474 16.6624 87.9943L16.539 88.1177C15.5524 89.1052 14.6892 90.2161 14.3192 91.5739C13.8259 92.9316 13.8259 94.2894 13.9492 95.6472C13.9492 95.6472 13.9492 95.6472 13.9492 95.7706V95.8941C13.9492 96.1409 14.0726 96.1409 13.9492 96.2644C13.9492 96.3878 13.8259 96.3878 13.8259 96.5112C13.5793 96.8815 13.4559 97.3753 13.7026 98.1159C14.1959 99.3502 14.9358 99.2268 15.7991 99.844C15.7991 99.844 15.6758 99.844 15.6758 99.9674L14.8125 100.214C10.8661 101.449 9.50957 104.781 11.2361 106.88C11.8527 107.62 12.8393 108.238 14.3192 108.608C13.9492 108.608 13.5793 108.855 13.3326 109.102C11.6061 110.459 10.4962 112.558 10.2495 114.533C10.2495 114.656 10.2495 114.78 10.2495 114.903C10.7428 115.15 11.1128 115.52 11.6061 115.767L30.9175 115.315C32.1507 114.575 32.6963 114.003 33.8062 113.016C33.6829 112.399 34.3111 112.23 34.1878 112.107Z" fill="#2E333D"/>
<path d="M34.7909 112.805C33.681 113.792 32.5711 114.656 31.2145 115.397C28.2547 117.125 24.8017 118.113 21.1019 118.113C17.4022 118.113 13.9491 117.125 10.9894 115.397C10.4961 115.15 10.1261 114.903 9.63281 114.533C9.63281 114.41 9.63281 114.286 9.63281 114.163C9.87946 112.188 10.9894 110.089 12.7159 108.732C12.9626 108.485 13.3325 108.361 13.7025 108.238C12.2226 107.991 11.236 107.374 10.6194 106.51H15.3057C16.6623 108.361 18.7588 109.472 21.2253 109.472C23.3218 109.472 25.1716 108.608 26.5282 107.25C26.6515 107.25 26.7748 107.25 26.8982 107.25C27.1448 107.25 27.3915 107.25 27.7614 107.25C28.748 107.25 29.7346 107.497 30.5979 107.991C32.3244 108.855 33.5577 110.336 34.4209 112.064C34.6676 112.311 34.6676 112.558 34.7909 112.805Z" fill="#3E4450"/>
<path d="M25.2953 104.165V106.757L17.5259 107.004L17.8958 105.275C18.0192 105.275 18.0192 105.399 18.1425 105.399C18.3891 105.522 18.6358 105.769 18.8824 105.893C19.0058 106.016 19.1291 106.139 19.3757 106.263C19.3757 106.263 19.4991 106.263 19.4991 106.386V104.658L19.3757 104.535C20.7323 105.275 22.5822 105.769 25.2953 104.165Z" fill="url(#paint2_linear)"/>
<path d="M30.351 93.4261C28.8711 93.9198 27.1446 94.1667 25.5414 94.0432C22.9516 93.7964 20.4851 92.8089 18.5119 91.0808C17.8953 92.9323 16.2921 94.2901 14.4422 95.1541C14.1956 95.2776 13.949 95.401 13.7023 95.401C13.7023 95.401 13.7023 95.401 13.7023 95.2776C13.579 93.9198 13.579 92.562 14.0723 91.2042C14.4422 89.8465 15.3055 88.7356 16.2921 87.7481L16.4154 87.6247C16.5388 87.5012 16.7854 87.3778 16.9087 87.1309C17.772 86.3903 18.8819 85.8966 19.9918 85.6497C20.6084 85.5263 21.2251 85.4028 21.8417 85.4028C25.0481 85.4028 27.8846 87.1309 29.2411 89.8465C29.7344 90.8339 29.9811 91.8214 30.1044 92.562C30.351 93.0558 30.351 93.3026 30.351 93.4261Z" fill="#949BAB"/>
<path d="M20.4853 111.694C19.7453 112.558 18.5121 112.558 17.4022 112.558C18.5121 111.447 17.8955 107.868 13.9491 108.238C8.52286 107.251 9.01616 101.573 14.4424 99.8445L15.3057 99.5977L15.429 99.7211C15.799 100.832 16.4156 101.819 17.0322 102.56C14.8124 104.412 17.8955 104.905 19.3754 106.387C20.6086 107.127 21.7185 110.213 20.4853 111.694Z" fill="#949BAB"/>
<defs>
<linearGradient id="paint0_linear" x1="58.2299" y1="30.8211" x2="58.2299" y2="28.0409" gradientUnits="userSpaceOnUse">
<stop stop-color="#2E333D"/>
<stop offset="0.9913" stop-color="#222427"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="95.163" y1="107.023" x2="95.163" y2="104.243" gradientUnits="userSpaceOnUse">
<stop stop-color="#2E333D"/>
<stop offset="0.9913" stop-color="#222427"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="21.3956" y1="106.915" x2="21.3956" y2="105.428" gradientUnits="userSpaceOnUse">
<stop stop-color="#2E333D"/>
<stop offset="0.9913" stop-color="#222427"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,37 @@
<svg width="117" height="120" viewBox="0 0 117 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M104.634 68.6908C104.634 80.2049 100.53 90.6778 93.6709 98.8847C85.0353 109.235 72.0513 115.788 57.4749 115.788C43.511 115.788 31.0169 109.725 22.3813 100.11C14.9706 91.7803 10.4385 80.7561 10.4385 68.6908C10.4385 42.6615 31.5069 21.5931 57.5361 21.5931C83.5654 21.5931 104.634 42.6615 104.634 68.6908Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 4"/>
<path d="M74.8781 71.7799C74.5226 73.2909 73.9893 74.8907 73.3671 76.2239C71.6785 79.5124 69.0121 82.0898 65.7236 83.7785C62.3462 85.4672 58.3467 86.1783 54.3471 85.2895C44.926 83.3341 38.8823 74.0908 40.8376 64.6697C42.7929 55.2485 51.9474 49.1159 61.3685 51.1601C64.7459 51.8712 67.6789 53.5598 70.1675 55.8707C74.3448 60.048 76.1224 66.0917 74.8781 71.7799Z" fill="#D6DCE8" stroke="#AAB2C5" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.4127 65.7137H60.4132V62.7142C60.4132 61.4008 59.3814 60.2032 57.9023 60.2032C56.5889 60.2032 55.3913 61.235 55.3913 62.7142V65.7137H52.3918C51.0785 65.7137 49.8809 66.7455 49.8809 68.2246C49.8809 68.9086 50.1243 69.5489 50.5959 70.0205C51.0675 70.4921 51.7079 70.7356 52.3918 70.7356H55.3913V73.7351C55.3913 75.0484 56.4231 76.246 57.9023 76.246C59.2156 76.246 60.4132 75.2142 60.4132 73.7351V70.7356H63.4127C64.726 70.7356 65.9237 69.7038 65.9237 68.2246C65.9237 66.7455 64.726 65.7137 63.4127 65.7137Z" fill="white" stroke="#AAB2C5" stroke-width="2"/>
<path d="M78.3516 21.6135C78.3516 27.1681 76.1318 32.1054 72.6787 35.8085C72.4321 36.1788 72.0621 36.4256 71.6921 36.6725C67.9924 40.1287 63.0594 42.2271 57.6332 42.2271C53.3169 42.2271 49.2472 40.8693 45.9174 38.524C44.9308 37.9069 44.0675 37.0428 43.2043 36.3022C39.3812 32.5992 37.0381 27.4149 37.0381 21.6135C37.0381 10.2576 46.2874 1 57.6332 1C69.1023 1 78.3516 10.2576 78.3516 21.6135Z" fill="#E9F0F8"/>
<path d="M78.3516 21.6124C78.3516 27.6607 75.7618 32.9684 71.6921 36.7948C67.9924 40.251 63.0594 42.3494 57.6332 42.3494C53.3169 42.3494 49.2472 40.9916 45.9174 38.6463C40.4912 34.9433 37.0381 28.7716 37.0381 21.7358C37.0381 10.3799 46.2874 1.12231 57.6332 1.12231C68.979 1.12231 78.3516 10.2564 78.3516 21.6124Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10"/>
<path d="M72.679 35.8085C72.4324 36.1788 72.0624 36.4257 71.6924 36.6726C67.9927 40.1287 63.0597 42.2271 57.6335 42.2271C53.3172 42.2271 49.2475 40.8693 45.9177 38.5241C44.9311 37.9069 44.0679 37.0429 43.2046 36.3023C43.6979 35.6851 44.4378 35.3148 46.041 34.6976L46.6577 34.4507C47.8909 33.957 49.6174 33.3398 51.8373 32.3524C52.2072 32.2289 52.4539 31.9821 52.7005 31.7352C52.8239 31.6118 52.9472 31.4883 52.9472 31.2415C53.0705 30.9946 53.1938 30.6243 53.1938 30.3774V26.1806C53.0705 26.0572 53.0705 26.0572 52.9472 25.9338C52.5772 25.44 52.3306 24.8229 52.3306 24.0823L52.0839 23.9588C50.974 24.2057 51.0973 23.0948 50.8507 20.873C50.7274 20.0089 50.8507 19.7621 51.344 19.6386L51.7139 19.1449C50.974 17.4168 50.604 15.8122 50.604 14.5778C50.604 12.4794 51.4673 11.1217 52.7005 10.5045C51.9606 9.02328 51.9606 8.52954 51.9606 8.52954C51.9606 8.52954 56.2769 9.27015 57.7568 9.02328C59.6067 8.65298 62.5664 9.14671 63.6764 11.6154C65.5262 12.356 66.1428 13.4669 66.3895 14.7013C66.6361 16.6762 65.5262 18.7746 65.2796 19.6386V19.7621C65.5262 19.8855 65.6495 20.1324 65.5262 20.9964C65.2796 23.0948 65.2796 24.3291 64.293 24.0823L63.3064 25.8103C63.3064 26.0572 63.3064 26.0572 63.1831 26.1806C63.1831 26.5509 63.1831 27.1681 63.1831 30.5008C63.1831 30.8711 63.3064 31.3649 63.553 31.6117C63.6764 31.7352 63.6764 31.8586 63.7997 31.8586C64.0463 32.1055 64.293 32.3524 64.5396 32.3524C67.0061 33.3398 68.7326 34.0804 70.0892 34.5742C71.3224 35.0679 72.1857 35.4382 72.679 35.8085Z" fill="white"/>
<path d="M72.679 35.8084C72.4324 36.1787 72.0624 36.4256 71.6924 36.6725C67.9927 40.1286 63.0597 42.227 57.6335 42.227C53.3172 42.227 49.2475 40.8692 45.9177 38.524C44.9311 37.9068 44.0679 37.0428 43.2046 36.3022C43.6979 35.685 44.4378 35.3147 46.041 34.6975L46.6577 34.4507C47.8909 33.9569 49.6174 33.3398 51.8373 32.3523C52.2072 32.2288 52.4539 31.982 52.7005 31.7351C53.9338 33.4632 55.907 34.5741 58.2501 34.5741C60.4699 34.5741 62.4431 33.4632 63.6764 31.8585C63.923 32.1054 64.1697 32.3523 64.4163 32.3523C66.8828 33.3398 68.6093 34.0804 69.9659 34.5741C71.3224 35.0678 72.1857 35.4381 72.679 35.8084Z" fill="#D6DCE8"/>
<path d="M65.1564 19.5152C65.2797 19.0215 65.0331 18.2808 64.7864 17.9105C64.7864 17.7871 64.6631 17.7871 64.6631 17.6637C63.7999 15.9356 61.95 15.3184 60.2235 15.195C55.6605 14.9481 55.2905 15.8122 53.9339 14.5778C54.4272 15.195 54.4272 16.3059 53.6873 17.5402C53.194 18.4043 52.3307 18.898 51.4675 19.1449C49.371 14.4544 50.4809 11.492 52.454 10.5045C51.7141 9.02328 51.7141 8.52954 51.7141 8.52954C51.7141 8.52954 56.0304 9.27015 57.5103 9.02328C59.3602 8.65298 62.32 9.14671 63.4299 11.6154C65.2797 12.356 65.8964 13.4669 66.143 14.7013C66.513 16.5528 65.4031 18.6512 65.1564 19.5152Z" fill="#AAB2C5"/>
<path d="M53.317 30.3773V26.1805C53.1936 26.0571 53.1936 26.0571 53.0703 25.9336V25.6868C53.317 26.0571 53.5636 26.4274 53.9336 26.6742L57.2633 29.0195C58.0033 29.6367 59.1132 29.6367 59.8531 29.0195L62.9362 26.3039C63.0595 26.1805 63.1829 26.1805 63.3062 26.0571C63.3062 26.4274 63.3062 27.0445 63.3062 30.3773C63.3062 30.6241 63.3062 30.7476 63.4295 30.9944H53.317C53.1936 30.7476 53.317 30.6241 53.317 30.3773Z" fill="url(#paint0_linear)"/>
<path d="M115.285 97.8154C115.285 103.37 113.065 108.307 109.612 112.01C109.365 112.381 108.995 112.628 108.625 112.874C104.925 116.331 99.9925 118.429 94.5663 118.429C90.25 118.429 86.1803 117.071 82.8505 114.726C81.8639 114.109 81.0007 113.245 80.1374 112.504C76.3143 108.801 73.9712 103.617 73.9712 97.8154C73.9712 86.4595 83.2205 77.2019 94.5663 77.2019C106.035 77.2019 115.285 86.4595 115.285 97.8154Z" fill="#E9F0F8"/>
<path d="M115.285 97.8143C115.285 103.863 112.695 109.17 108.625 112.997C104.925 116.453 99.9925 118.551 94.5663 118.551C90.25 118.551 86.1803 117.194 82.8505 114.848C77.4243 111.145 73.9712 104.974 73.9712 97.9378C73.9712 86.5818 83.2205 77.3242 94.5663 77.3242C105.912 77.3242 115.285 86.4584 115.285 97.8143Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.613 112.01C109.366 112.381 108.996 112.628 108.626 112.874C104.927 116.331 99.9938 118.429 94.5676 118.429C90.2512 118.429 86.1815 117.071 82.8518 114.726C81.8652 114.109 81.0019 113.245 80.1387 112.504C80.632 111.887 81.3719 111.517 82.9751 110.9L83.5917 110.653C84.825 110.159 86.5515 109.542 88.7713 108.554C89.1413 108.431 89.388 108.184 89.6346 107.937C89.7579 107.814 89.8813 107.69 89.8813 107.443C90.0046 107.197 90.1279 106.826 90.1279 106.579V102.383C90.0046 102.259 90.0046 102.259 89.8813 102.136C89.5113 101.642 89.2646 101.025 89.2646 100.284L89.018 100.161C87.9081 100.408 88.0314 99.2967 87.7848 97.0749C87.6614 96.2108 87.7848 95.964 88.278 95.8405L88.648 95.3468C87.9081 93.6187 87.5381 92.0141 87.5381 90.7797C87.5381 88.6814 88.4014 87.3236 89.6346 86.7064C88.8947 85.2252 88.8947 84.7315 88.8947 84.7315C88.8947 84.7315 93.211 85.4721 94.6909 85.2252C96.5408 84.8549 99.5005 85.3486 100.61 87.8173C102.46 88.5579 103.077 89.6688 103.324 90.9032C103.57 92.8781 102.46 94.9765 102.214 95.8405V95.964C102.46 96.0874 102.584 96.3343 102.46 97.1983C102.214 99.2967 102.214 100.531 101.227 100.284L100.24 102.012C100.24 102.259 100.24 102.259 100.117 102.383C100.117 102.753 100.117 103.37 100.117 106.703C100.117 107.073 100.24 107.567 100.487 107.814C100.61 107.937 100.61 108.061 100.734 108.061C100.98 108.307 101.227 108.554 101.474 108.554C103.94 109.542 105.667 110.282 107.023 110.776C108.257 111.27 109.12 111.64 109.613 112.01Z" fill="white"/>
<path d="M109.612 112.01C109.365 112.381 108.995 112.628 108.626 112.874C104.926 116.331 99.9928 118.429 94.5666 118.429C90.2503 118.429 86.1806 117.071 82.8508 114.726C81.8642 114.109 81.001 113.245 80.1377 112.504C80.631 111.887 81.3709 111.517 82.9741 110.899L83.5908 110.653C84.824 110.159 86.5505 109.542 88.7704 108.554C89.1403 108.431 89.387 108.184 89.6336 107.937C90.8669 109.665 92.8401 110.776 95.1832 110.776C97.403 110.776 99.3762 109.665 100.609 108.06C100.856 108.307 101.103 108.554 101.349 108.554C103.816 109.542 105.542 110.282 106.899 110.776C108.256 111.27 109.119 111.64 109.612 112.01Z" fill="#D6DCE8"/>
<path d="M102.09 95.7171C102.213 95.2234 101.966 94.4828 101.72 94.1125C101.72 93.989 101.596 93.989 101.596 93.8656C100.733 92.1375 98.8831 91.5203 97.1566 91.3969C92.5936 91.15 92.2236 92.0141 90.867 90.7797C91.3603 91.3969 91.3603 92.5078 90.6204 93.7422C90.1271 94.6062 89.2638 95.0999 88.4006 95.3468C86.3041 90.6563 87.414 87.6939 89.3872 86.7064C88.6472 85.2252 88.6472 84.7315 88.6472 84.7315C88.6472 84.7315 92.9635 85.4721 94.4434 85.2252C96.2933 84.8549 99.2531 85.3486 100.363 87.8173C102.213 88.5579 102.829 89.6688 103.076 90.9032C103.446 92.7547 102.336 94.8531 102.09 95.7171Z" fill="#AAB2C5"/>
<path d="M90.2501 106.579V102.382C90.1267 102.259 90.1267 102.259 90.0034 102.136V101.889C90.2501 102.259 90.4967 102.629 90.8667 102.876L94.1964 105.221C94.9364 105.839 96.0463 105.839 96.7862 105.221L99.8693 102.506C99.9927 102.382 100.116 102.382 100.239 102.259C100.239 102.629 100.239 103.246 100.239 106.579C100.239 106.826 100.239 106.949 100.363 107.196H90.2501C90.1267 106.949 90.2501 106.826 90.2501 106.579Z" fill="url(#paint1_linear)"/>
<path d="M41.2036 98.1248C41.2036 103.926 38.7371 109.11 34.7908 112.813C33.6808 113.801 32.5709 114.665 31.2144 115.406C28.2546 117.134 24.8015 118.121 21.1018 118.121C17.4021 118.121 13.949 117.134 10.9892 115.406C10.4959 115.159 10.126 114.912 9.63268 114.542C4.45307 110.962 1 104.914 1 98.1248C1 87.0157 10.0026 78.1284 20.9785 78.1284C32.201 78.005 41.2036 87.0157 41.2036 98.1248Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10"/>
<path d="M17.0323 102.568C17.279 102.815 17.5256 103.185 17.8956 103.432C18.1422 103.679 18.3889 103.802 18.6355 104.049C18.7589 104.172 19.0055 104.296 19.1288 104.419C19.1288 104.419 19.2522 104.419 19.2522 104.543L19.3755 104.666V106.394C19.3755 106.394 19.3755 106.394 19.2522 106.271C19.1288 106.147 18.8822 106.024 18.7589 105.901C18.5122 105.777 18.2656 105.53 18.0189 105.407C17.8956 105.407 17.8956 105.283 17.7723 105.283C16.909 104.79 16.1691 104.296 16.1691 103.679C16.2924 103.432 16.539 103.062 17.0323 102.568ZM34.1878 112.115C33.4478 110.511 32.4478 108.986 30.7213 108.122C29.858 107.752 28.8714 107.382 27.8848 107.382C27.6382 107.382 27.2682 107.382 27.0216 107.382C26.8982 107.382 26.7749 107.382 26.6516 107.382C25.295 107.258 25.1717 107.011 25.1717 107.011V104.172C26.035 103.432 26.8982 102.568 27.6382 101.704C28.2548 100.84 28.7481 99.8523 28.9947 98.6179C30.1047 98.371 30.8446 97.3836 30.7213 96.1492C30.7213 95.6555 30.3513 95.1617 30.3513 94.668C30.3513 94.4211 30.3513 94.1743 30.3513 93.9274C30.3513 93.804 30.3513 93.5571 30.3513 93.4337C30.3513 93.3102 30.3513 93.0634 30.3513 92.9399C30.228 92.0759 29.9813 91.2119 29.488 90.2244C28.0082 87.5088 25.295 85.7807 22.0886 85.7807C21.472 85.7807 20.8554 85.9042 20.2387 86.0276C19.1288 86.2745 18.0189 86.7682 17.1556 87.5088C17.0323 87.6323 16.7857 87.7557 16.6624 88.0026L16.539 88.126C15.5524 89.1135 14.6892 90.2244 14.3192 91.5822C13.8259 92.9399 13.8259 94.2977 13.9492 95.6555C13.9492 95.6555 13.9492 95.6555 13.9492 95.7789V95.9024C13.9492 96.1492 14.0726 96.1492 13.9492 96.2727C13.9492 96.3961 13.8259 96.3961 13.8259 96.5195C13.5793 96.8898 13.4559 97.3836 13.7026 98.1242C14.1959 99.3585 14.9358 99.2351 15.7991 99.8523C15.7991 99.8523 15.6758 99.8523 15.6758 99.9757L14.8125 100.223C10.8661 101.457 9.50957 104.79 11.2361 106.888C11.8527 107.629 12.8393 108.246 14.3192 108.616C13.9492 108.616 13.5793 108.863 13.3326 109.11C11.6061 110.468 10.4962 112.566 10.2495 114.541C10.2495 114.664 10.2495 114.788 10.2495 114.911C10.7428 115.158 11.1128 115.528 11.6061 115.775L30.9175 115.324C32.1507 114.583 32.6963 114.011 33.8062 113.024C33.6829 112.407 34.3111 112.239 34.1878 112.115Z" fill="white"/>
<path d="M34.7909 112.813C33.681 113.801 32.5711 114.665 31.2145 115.405C28.2547 117.133 24.8017 118.121 21.1019 118.121C17.4022 118.121 13.9491 117.133 10.9894 115.405C10.4961 115.158 10.1261 114.912 9.63281 114.541C9.63281 114.418 9.63281 114.294 9.63281 114.171C9.87946 112.196 10.9894 110.098 12.7159 108.74C12.9626 108.493 13.3325 108.37 13.7025 108.246C12.2226 107.999 11.236 107.382 10.6194 106.518H15.3057C16.6623 108.37 18.7588 109.48 21.2253 109.48C23.3218 109.48 25.1716 108.616 26.5282 107.259C26.6515 107.259 26.7748 107.259 26.8982 107.259C27.1448 107.259 27.3915 107.259 27.7614 107.259C28.748 107.259 29.7346 107.506 30.5979 107.999C32.3244 108.863 33.5577 110.345 34.4209 112.073C34.6676 112.319 34.6676 112.566 34.7909 112.813Z" fill="#D6DCE8"/>
<path d="M25.2953 104.173V106.765L17.5259 107.012L17.8958 105.284C18.0192 105.284 18.0192 105.407 18.1425 105.407C18.3891 105.531 18.6358 105.777 18.8824 105.901C19.0058 106.024 19.1291 106.148 19.3757 106.271C19.3757 106.271 19.4991 106.271 19.4991 106.395V104.667L19.3757 104.543C20.7323 105.284 22.5822 105.777 25.2953 104.173Z" fill="url(#paint2_linear)"/>
<path d="M30.351 93.4341C28.8711 93.9278 27.1446 94.1747 25.5414 94.0513C22.9516 93.8044 20.4851 92.8169 18.5119 91.0888C17.8953 92.9404 16.2921 94.2981 14.4422 95.1622C14.1956 95.2856 13.949 95.409 13.7023 95.409C13.7023 95.409 13.7023 95.4091 13.7023 95.2856C13.579 93.9278 13.579 92.5701 14.0723 91.2123C14.4422 89.8545 15.3055 88.7436 16.2921 87.7561L16.4154 87.6327C16.5388 87.5093 16.7854 87.3858 16.9087 87.139C17.772 86.3983 18.8819 85.9046 19.9918 85.6577C20.6084 85.5343 21.2251 85.4109 21.8417 85.4109C25.0481 85.4109 27.8846 87.139 29.2411 89.8545C29.7344 90.842 29.9811 91.8295 30.1044 92.5701C30.351 93.0638 30.351 93.3107 30.351 93.4341Z" fill="#AAB2C5"/>
<path d="M20.4853 111.703C19.7453 112.567 18.5121 112.567 17.4022 112.567C18.5121 111.456 17.8955 107.876 13.9491 108.246C8.52286 107.259 9.01616 101.581 14.4424 99.8528L15.3057 99.6059L15.429 99.7294C15.799 100.84 16.4156 101.828 17.0322 102.568C14.8124 104.42 17.8955 104.914 19.3754 106.395C20.6086 107.135 21.7185 110.221 20.4853 111.703Z" fill="#AAB2C5"/>
<defs>
<linearGradient id="paint0_linear" x1="58.2299" y1="30.8292" x2="58.2299" y2="28.049" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.9913" stop-color="#D6DEEA"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="95.163" y1="107.031" x2="95.163" y2="104.251" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.9913" stop-color="#D6DEEA"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="21.3956" y1="106.923" x2="21.3956" y2="105.436" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.9913" stop-color="#D6DEEA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -0,0 +1,12 @@
<svg width="119" height="117" viewBox="0 0 119 117" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.0114 115.804C80.1842 115.804 103.023 92.8871 103.023 64.6183C103.023 36.3495 80.1842 13.4331 52.0114 13.4331C23.8386 13.4331 1 36.3495 1 64.6183C1 92.8871 23.8386 115.804 52.0114 115.804Z" fill="#1F2229" stroke="#484E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M110.666 21.9438C111.335 21.3298 111.382 20.2875 110.77 19.6157C110.158 18.9439 109.12 18.897 108.45 19.5109C107.781 20.1249 107.734 21.1672 108.346 21.839C108.957 22.5108 109.996 22.5577 110.666 21.9438Z" fill="#3B414E"/>
<path opacity="0.3" d="M83.4192 41.2397C85.2331 41.2397 86.7036 39.7642 86.7036 37.9441C86.7036 36.1239 85.2331 34.6484 83.4192 34.6484C81.6053 34.6484 80.1348 36.1239 80.1348 37.9441C80.1348 39.7642 81.6053 41.2397 83.4192 41.2397Z" fill="#3E4450"/>
<path d="M61.2816 45.569C60.8718 47.7548 59.0958 49.804 56.7733 50.3505C54.9973 50.7604 53.2213 50.3505 51.9918 49.3942V65.2415H37.2374C37.7839 64.1486 37.9205 63.0557 37.7839 61.8261C37.5106 59.0939 35.1882 56.908 32.5925 56.6348C29.0405 56.2249 25.8984 58.9572 25.8984 62.5092C25.8984 63.4655 26.035 64.4218 26.4448 65.1049H9.77783V34.6398C9.77783 28.3555 14.8326 23.3008 21.2535 23.3008H51.8552V39.1481C52.9481 38.465 54.041 38.0552 55.2706 38.0552C58.9592 38.6016 61.9647 41.7438 61.2816 45.569Z" fill="#2E333D" stroke="#3E4450" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M42.019 85.187C41.4726 88.8756 44.3415 92.0177 48.0301 92.0177C49.5328 92.0177 50.899 91.4713 51.9919 90.6516V107.045H21.3902C15.1059 107.045 9.91455 101.854 9.91455 95.7063V65.1046H26.4449C26.0351 64.2849 25.8985 63.4653 25.8985 62.3723C26.0351 59.5034 28.2209 57.0444 30.9532 56.6345C34.6418 56.2247 37.784 58.957 37.784 62.5089C37.784 63.4652 37.6474 64.2849 37.2375 65.1046H51.7187V81.4984C50.4891 80.4055 48.8498 79.859 47.0738 80.1323C44.4781 80.4055 42.4289 82.5913 42.019 85.187Z" fill="#1F2229" stroke="#3E4450" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M94.0691 65.1047V95.4331C94.0691 101.717 88.8777 106.772 82.5934 106.772H51.9917V90.3784C50.8988 91.3347 49.5327 91.7446 48.0299 91.7446C44.4779 91.7446 41.7456 88.6024 42.0188 84.9138C42.4287 82.1815 44.6145 80.1323 47.0736 79.7224C48.8496 79.5858 50.6256 80.1323 51.7185 81.0886V64.6948H69.3418C68.7953 65.5145 68.6587 66.4708 68.6587 67.4271C68.6587 70.8425 71.6642 73.7114 75.3528 73.3016C78.0851 73.0283 80.4076 70.7059 80.6808 68.1102C80.8174 66.8807 80.5442 65.7877 80.1344 64.6948L94.0691 65.1047Z" fill="#2E333D" stroke="#3E4450" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M106.911 52.6728L93.3859 50.3503C93.6591 51.4432 93.7958 52.5362 93.3859 53.6291C92.5662 56.2247 90.1072 58.0007 87.3749 57.8641C83.8229 57.5909 81.2272 54.4488 81.7737 51.0334C81.9103 50.0771 82.3201 49.2574 82.8666 48.5743L65.3799 45.4322L68.1122 29.7215C69.2051 30.8144 70.8445 31.4975 72.7571 31.4975C75.2161 31.3609 77.2654 29.8581 77.9484 27.6723C79.3146 24.1203 76.9921 20.2951 73.3035 19.7486C72.074 19.4754 70.8445 19.7486 69.7516 20.1585L72.4839 4.44775L102.539 9.63912C108.823 10.732 112.922 16.6065 111.829 22.7541L106.911 52.6728Z" fill="#6D7381" stroke="#949BAB" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M113.473 64.1927L118.015 62.8237C118.344 62.7282 118.344 62.2506 118.015 62.1551L113.473 60.7861C113.356 60.7543 113.271 60.6694 113.24 60.5526L111.871 56.0212C111.775 55.6922 111.297 55.6922 111.202 56.0212L109.833 60.5526C109.801 60.6694 109.716 60.7543 109.6 60.7861L105.068 62.1551C104.739 62.2506 104.739 62.7282 105.068 62.8237L109.61 64.1927C109.727 64.2245 109.812 64.3094 109.844 64.4261L111.213 68.9682C111.308 69.2972 111.786 69.2972 111.881 68.9682L113.25 64.4261C113.271 64.3094 113.356 64.2245 113.473 64.1927Z" fill="#3E4450"/>
<path d="M103.424 24.4523L109.073 22.7496C109.482 22.6308 109.482 22.0369 109.073 21.9181L103.424 20.2153C103.279 20.1757 103.173 20.0701 103.133 19.925L101.431 14.2888C101.312 13.8796 100.718 13.8796 100.599 14.2888L98.8965 19.925C98.8569 20.0701 98.7513 20.1757 98.6061 20.2153L92.97 21.9181C92.5608 22.0369 92.5608 22.6308 92.97 22.7496L98.6193 24.4523C98.7645 24.4919 98.8701 24.5975 98.9097 24.7427L100.612 30.3921C100.731 30.8012 101.325 30.8012 101.444 30.3921L103.147 24.7427C103.173 24.5975 103.279 24.4919 103.424 24.4523Z" fill="#C7CDDB"/>
<path d="M62.357 5.26099L65.1958 4.40543C65.4015 4.34575 65.4015 4.0473 65.1958 3.98761L62.357 3.13205C62.2841 3.11216 62.231 3.0591 62.2111 2.98615L61.3555 0.154199C61.2958 -0.0513995 60.9973 -0.0513995 60.9376 0.154199L60.082 2.98615C60.0621 3.0591 60.009 3.11216 59.9361 3.13205L57.1039 3.98761C56.8983 4.0473 56.8983 4.34575 57.1039 4.40543L59.9427 5.26099C60.0157 5.28088 60.0687 5.33394 60.0886 5.4069L60.9443 8.24548C61.004 8.45107 61.3024 8.45107 61.3621 8.24548L62.2177 5.4069C62.231 5.33394 62.2841 5.28088 62.357 5.26099Z" fill="#3E4450"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,12 @@
<svg width="119" height="117" viewBox="0 0 119 117" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.0114 115.804C80.1842 115.804 103.023 92.8871 103.023 64.6183C103.023 36.3495 80.1842 13.4331 52.0114 13.4331C23.8386 13.4331 1 36.3495 1 64.6183C1 92.8871 23.8386 115.804 52.0114 115.804Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10"/>
<path d="M110.666 21.9437C111.335 21.3297 111.382 20.2874 110.77 19.6156C110.158 18.9438 109.12 18.8969 108.45 19.5108C107.781 20.1247 107.734 21.167 108.346 21.8389C108.957 22.5107 109.996 22.5576 110.666 21.9437Z" fill="#EAEEF9"/>
<path opacity="0.3" d="M83.4192 41.2399C85.2331 41.2399 86.7036 39.7644 86.7036 37.9443C86.7036 36.1242 85.2331 34.6487 83.4192 34.6487C81.6053 34.6487 80.1348 36.1242 80.1348 37.9443C80.1348 39.7644 81.6053 41.2399 83.4192 41.2399Z" fill="#AAB2C5"/>
<path d="M61.2816 45.5689C60.8718 47.7547 59.0958 49.8039 56.7733 50.3504C54.9973 50.7602 53.2213 50.3504 51.9918 49.3941V65.2414H37.2374C37.7839 64.1485 37.9205 63.0556 37.7839 61.826C37.5106 59.0937 35.1882 56.9079 32.5925 56.6347C29.0405 56.2248 25.8984 58.9571 25.8984 62.5091C25.8984 63.4654 26.035 64.4217 26.4448 65.1048H9.77783V34.6397C9.77783 28.3554 14.8326 23.3007 21.2535 23.3007H51.8552V39.148C52.9481 38.4649 54.041 38.0551 55.2706 38.0551C58.9592 38.6015 61.9647 41.7437 61.2816 45.5689Z" fill="white" stroke="#C5CCDA" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M42.019 85.1871C41.4726 88.8757 44.3415 92.0178 48.0301 92.0178C49.5328 92.0178 50.899 91.4713 51.9919 90.6516V107.045H21.3902C15.1059 107.045 9.91455 101.854 9.91455 95.7064V65.1047H26.4449C26.0351 64.285 25.8985 63.4653 25.8985 62.3724C26.0351 59.5035 28.2209 57.0444 30.9532 56.6346C34.6418 56.2247 37.784 58.957 37.784 62.509C37.784 63.4653 37.6474 64.285 37.2375 65.1047H51.7187V81.4985C50.4891 80.4055 48.8498 79.8591 47.0738 80.1323C44.4781 80.4055 42.4289 82.5914 42.019 85.1871Z" fill="white" stroke="#C5CCDA" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M94.0691 65.1049V95.4333C94.0691 101.718 88.8777 106.772 82.5934 106.772H51.9917V90.3786C50.8988 91.3349 49.5327 91.7447 48.0299 91.7447C44.4779 91.7447 41.7456 88.6026 42.0188 84.914C42.4287 82.1817 44.6145 80.1325 47.0736 79.7226C48.8496 79.586 50.6256 80.1325 51.7185 81.0888V64.695H69.3418C68.7953 65.5147 68.6587 66.471 68.6587 67.4273C68.6587 70.8427 71.6642 73.7116 75.3528 73.3017C78.0851 73.0285 80.4076 70.7061 80.6808 68.1104C80.8174 66.8808 80.5442 65.7879 80.1344 64.695L94.0691 65.1049Z" fill="white" stroke="#C5CCDA" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M106.911 52.6728L93.3859 50.3503C93.6591 51.4432 93.7958 52.5362 93.3859 53.6291C92.5662 56.2247 90.1072 58.0007 87.3749 57.8641C83.8229 57.5909 81.2272 54.4488 81.7737 51.0334C81.9103 50.0771 82.3201 49.2574 82.8666 48.5743L65.3799 45.4322L68.1122 29.7215C69.2051 30.8144 70.8445 31.4975 72.7571 31.4975C75.2161 31.3609 77.2654 29.8581 77.9484 27.6723C79.3146 24.1203 76.9921 20.2951 73.3035 19.7486C72.074 19.4754 70.8445 19.7486 69.7516 20.1585L72.4839 4.44775L102.539 9.63912C108.823 10.732 112.922 16.6065 111.829 22.7541L106.911 52.6728Z" fill="#D6DCE8" stroke="#AAB2C5" stroke-width="2" stroke-miterlimit="10"/>
<path d="M113.473 64.1926L118.015 62.8236C118.344 62.7281 118.344 62.2505 118.015 62.155L113.473 60.786C113.356 60.7542 113.271 60.6693 113.24 60.5526L111.871 56.0211C111.775 55.6921 111.297 55.6921 111.202 56.0211L109.833 60.5526C109.801 60.6693 109.716 60.7542 109.6 60.786L105.068 62.155C104.739 62.2505 104.739 62.7281 105.068 62.8236L109.61 64.1926C109.727 64.2244 109.812 64.3093 109.844 64.4261L111.213 68.9682C111.308 69.2971 111.786 69.2971 111.881 68.9682L113.25 64.4261C113.271 64.3093 113.356 64.2244 113.473 64.1926Z" fill="#AAB2C5"/>
<path d="M103.424 24.4522L109.073 22.7495C109.482 22.6307 109.482 22.0367 109.073 21.9179L103.424 20.2152C103.279 20.1756 103.173 20.07 103.133 19.9248L101.431 14.2887C101.312 13.8795 100.718 13.8795 100.599 14.2887L98.8965 19.9248C98.8569 20.07 98.7513 20.1756 98.6061 20.2152L92.97 21.9179C92.5608 22.0367 92.5608 22.6307 92.97 22.7495L98.6193 24.4522C98.7645 24.4918 98.8701 24.5974 98.9097 24.7426L100.612 30.3919C100.731 30.8011 101.325 30.8011 101.444 30.3919L103.147 24.7426C103.173 24.5974 103.279 24.4918 103.424 24.4522Z" fill="#AAB2C5"/>
<path d="M62.357 5.26099L65.1958 4.40543C65.4015 4.34575 65.4015 4.0473 65.1958 3.98761L62.357 3.13205C62.2841 3.11216 62.231 3.0591 62.2111 2.98615L61.3555 0.154199C61.2958 -0.0513995 60.9973 -0.0513995 60.9376 0.154199L60.082 2.98615C60.0621 3.0591 60.009 3.11216 59.9361 3.13205L57.1039 3.98761C56.8983 4.0473 56.8983 4.34575 57.1039 4.40543L59.9427 5.26099C60.0157 5.28088 60.0687 5.33394 60.0886 5.4069L60.9443 8.24548C61.004 8.45107 61.3024 8.45107 61.3621 8.24548L62.2177 5.4069C62.231 5.33394 62.2841 5.28088 62.357 5.26099Z" fill="#AAB2C5"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,24 @@
<svg width="145" height="110" viewBox="0 0 145 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M74.6528 108.28C104.218 108.28 128.187 84.3113 128.187 54.6402C128.187 24.9692 104.113 1 74.6528 1C45.0873 1 21.1182 24.9692 21.1182 54.6402C21.1182 84.3113 45.0873 108.28 74.6528 108.28Z" fill="#1F2229" stroke="#3B414E" stroke-width="2" stroke-miterlimit="10"/>
<path d="M134.984 37.645C137.374 37.645 139.313 35.7068 139.313 33.3158C139.313 30.9248 137.374 28.9866 134.984 28.9866C132.593 28.9866 130.654 30.9248 130.654 33.3158C130.654 35.7068 132.593 37.645 134.984 37.645Z" fill="#3B414E"/>
<path d="M141.319 20.7505C142.952 20.7505 144.275 19.4268 144.275 17.7939C144.275 16.1611 142.952 14.8374 141.319 14.8374C139.686 14.8374 138.362 16.1611 138.362 17.7939C138.362 19.4268 139.686 20.7505 141.319 20.7505Z" fill="#3B414E"/>
<path d="M23.5469 19.4783C25.1797 19.4783 26.5034 18.1546 26.5034 16.5217C26.5034 14.8889 25.1797 13.5652 23.5469 13.5652C21.914 13.5652 20.5903 14.8889 20.5903 16.5217C20.5903 18.1546 21.914 19.4783 23.5469 19.4783Z" fill="#1F2229"/>
<path d="M5.49073 76.4976C8.52318 76.4976 10.9815 74.0393 10.9815 71.0068C10.9815 67.9744 8.52318 65.5161 5.49073 65.5161C2.45828 65.5161 0 67.9744 0 71.0068C0 74.0393 2.45828 76.4976 5.49073 76.4976Z" fill="#1F2229"/>
<path d="M85.5262 69.1928V89.5045C85.5262 91.2707 84.4985 93.037 83.0304 93.9201L67.4679 102.898C66.587 103.487 65.4125 103.782 64.3848 103.782V78.4656L84.7921 66.6907C85.2326 67.4266 85.5262 68.3097 85.5262 69.1928Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M64.3846 78.4656V103.782C63.3569 103.782 62.3292 103.487 61.3015 102.898L45.739 93.9201C44.1241 93.037 43.2432 91.4179 43.2432 89.5045V69.1928C43.2432 68.3097 43.5368 67.4266 43.9772 66.6907L64.3846 78.4656Z" fill="#1F2229" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M84.7921 66.6908L64.3848 78.4657L43.8306 66.5437C44.271 65.8077 44.8583 65.0718 45.7392 64.6302L61.7421 55.3575C63.2102 54.4744 65.1188 54.4744 66.7338 55.2103L83.1772 64.7774C83.1772 64.7774 83.1772 64.7774 83.324 64.7774C83.9112 65.3662 84.4985 65.9549 84.7921 66.6908Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M127.809 69.1928V89.5045C127.809 91.2707 126.782 93.037 125.314 93.9201L109.751 102.898C108.87 103.487 107.696 103.782 106.668 103.782V78.4656L127.075 66.6907C127.516 67.4266 127.809 68.3097 127.809 69.1928Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M106.668 78.4656V103.782C105.64 103.782 104.612 103.487 103.585 102.898L88.0222 93.9201C86.4073 93.037 85.5264 91.4179 85.5264 89.5045V69.1928C85.5264 68.3097 85.82 67.4266 86.2604 66.6907L106.668 78.4656Z" fill="#1F2229" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M127.075 66.6908L106.668 78.4657L86.1138 66.5437C86.5542 65.8077 87.1415 65.0718 88.0224 64.6302L104.025 55.3575C105.493 54.4744 107.402 54.4744 109.017 55.2103L125.46 64.7774C125.46 64.7774 125.46 64.7774 125.607 64.7774C126.194 65.3662 126.782 65.9549 127.075 66.6908Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M64.2381 30.7776V51.0892C64.2381 52.8554 63.2104 54.6217 61.7423 55.5048L46.1798 64.4831C45.2989 65.0719 44.1244 65.3662 43.0967 65.3662V40.0503L63.5041 28.2754C64.0913 29.0113 64.2381 29.8944 64.2381 30.7776Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M43.0965 40.0503V65.3662C42.0688 65.3662 41.0411 65.0719 40.0134 64.4831L24.4509 55.5048C22.836 54.6217 21.9551 53.0026 21.9551 51.0892V30.7776C21.9551 29.8944 22.2487 29.0113 22.6892 28.2754L43.0965 40.0503Z" fill="#1F2229" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.6505 28.2751L43.2432 40.0499L22.689 28.1279C23.1294 27.392 23.7167 26.656 24.5976 26.2145L40.6005 16.9418C42.0686 16.0586 43.9772 16.0586 45.5922 16.7946L61.8887 26.5088C61.8887 26.5088 61.8887 26.5088 62.0356 26.5088C62.6228 26.9504 63.2101 27.5391 63.6505 28.2751Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.311 30.3358V53.0024C109.311 55.063 108.283 56.9765 106.374 58.0068L88.9031 68.1626C87.8754 68.7513 86.7009 69.0457 85.5264 69.0457V40.6388L108.576 27.3921C109.017 28.2752 109.311 29.3055 109.311 30.3358Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M85.5262 40.6388V58.5955V69.0457C84.3516 69.0457 83.1771 68.7513 82.1494 68.1626L74.515 63.747L64.5315 58.0067C62.7697 56.9764 61.742 55.063 61.5952 53.1496V30.1886C61.5952 29.1583 61.8888 28.128 62.3293 27.2449L85.5262 40.6388Z" fill="#1F2229" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path d="M108.429 27.3921L85.3792 40.6388L62.3291 27.2449C62.7695 26.3618 63.5036 25.6258 64.3845 25.1843L82.296 14.7341C84.0578 13.7038 86.1132 13.7038 87.875 14.5869L106.374 25.3315C106.374 25.3315 106.521 25.3315 106.521 25.4787C107.402 26.0674 107.989 26.6561 108.429 27.3921Z" fill="#2E333D" stroke="#494E59" stroke-width="2" stroke-miterlimit="10"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.206 12.7656C108.856 12.7656 109.383 13.2922 109.383 13.9418V20.9987C109.383 21.6483 108.856 22.1749 108.206 22.1749C107.557 22.1749 107.03 21.6483 107.03 20.9987V13.9418C107.03 13.2922 107.557 12.7656 108.206 12.7656Z" fill="#949BAB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M123.936 13.1101C124.396 13.5694 124.396 14.3141 123.936 14.7735L115.311 23.3986C114.852 23.8579 114.107 23.8579 113.648 23.3986C113.188 22.9393 113.188 22.1946 113.648 21.7353L122.273 13.1101C122.732 12.6508 123.477 12.6508 123.936 13.1101Z" fill="#949BAB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.087 29.6239C114.087 28.9743 114.614 28.4478 115.264 28.4478H123.105C123.754 28.4478 124.281 28.9743 124.281 29.6239C124.281 30.2735 123.754 30.8001 123.105 30.8001H115.264C114.614 30.8001 114.087 30.2735 114.087 29.6239Z" fill="#949BAB"/>
<path d="M96.9656 43.7842C96.7253 44.8023 96.3648 45.8804 95.9443 46.7788C94.8027 48.9948 93.0003 50.7317 90.7772 51.8696C88.4942 53.0076 85.7905 53.4867 83.0868 52.8878C76.7182 51.5702 72.6326 45.3414 73.9544 38.9928C75.2762 32.6442 81.4646 28.5116 87.8333 29.8891C90.1164 30.3683 92.099 31.5062 93.7813 33.0634C96.6052 35.8784 97.8068 39.9511 96.9656 43.7842Z" fill="#6D7381" stroke="#949BAB" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M89.1596 40.3732H86.4797V37.6933C86.4797 37.1573 86.0628 36.6809 85.4673 36.6809C84.9314 36.6809 84.4549 37.0978 84.4549 37.6933V40.3732H81.7751C81.2391 40.3732 80.7627 40.79 80.7627 41.3855C80.7627 41.9811 81.1796 42.3979 81.7751 42.3979H84.4549V45.0778C84.4549 45.6137 84.8718 46.0902 85.4673 46.0902C86.0033 46.0902 86.4797 45.6733 86.4797 45.0778V42.3979H89.1596C89.6955 42.3979 90.172 41.9811 90.172 41.3855C90.172 40.79 89.6955 40.3732 89.1596 40.3732Z" fill="#C7CDDB"/>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,24 @@
<svg width="145" height="110" viewBox="0 0 145 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M74.6528 108.28C104.218 108.28 128.187 84.3113 128.187 54.6402C128.187 24.9692 104.113 1 74.6528 1C45.0873 1 21.1182 24.9692 21.1182 54.6402C21.1182 84.3113 45.0873 108.28 74.6528 108.28Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10"/>
<path d="M134.984 37.645C137.374 37.645 139.313 35.7068 139.313 33.3158C139.313 30.9248 137.374 28.9866 134.984 28.9866C132.593 28.9866 130.654 30.9248 130.654 33.3158C130.654 35.7068 132.593 37.645 134.984 37.645Z" fill="#F1F3F9"/>
<path d="M141.319 20.7505C142.952 20.7505 144.275 19.4268 144.275 17.7939C144.275 16.1611 142.952 14.8374 141.319 14.8374C139.686 14.8374 138.362 16.1611 138.362 17.7939C138.362 19.4268 139.686 20.7505 141.319 20.7505Z" fill="#F1F3F9"/>
<path d="M23.5469 19.4783C25.1797 19.4783 26.5034 18.1546 26.5034 16.5217C26.5034 14.8889 25.1797 13.5652 23.5469 13.5652C21.914 13.5652 20.5903 14.8889 20.5903 16.5217C20.5903 18.1546 21.914 19.4783 23.5469 19.4783Z" fill="#F1F3F9"/>
<path d="M5.49073 76.4977C8.52318 76.4977 10.9815 74.0394 10.9815 71.007C10.9815 67.9745 8.52318 65.5162 5.49073 65.5162C2.45828 65.5162 0 67.9745 0 71.007C0 74.0394 2.45828 76.4977 5.49073 76.4977Z" fill="#F1F3F9"/>
<path d="M85.5262 69.1928V89.5045C85.5262 91.2707 84.4985 93.037 83.0304 93.9201L67.4679 102.898C66.587 103.487 65.4125 103.782 64.3848 103.782V78.4656L84.7921 66.6907C85.2326 67.4266 85.5262 68.3097 85.5262 69.1928Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M64.3846 78.4656V103.782C63.3569 103.782 62.3292 103.487 61.3015 102.898L45.739 93.9201C44.1241 93.037 43.2432 91.4179 43.2432 89.5045V69.1928C43.2432 68.3097 43.5368 67.4266 43.9772 66.6907L64.3846 78.4656Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M84.7921 66.6908L64.3848 78.4657L43.8306 66.5437C44.271 65.8077 44.8583 65.0718 45.7392 64.6302L61.7421 55.3575C63.2102 54.4744 65.1188 54.4744 66.7338 55.2103L83.1772 64.7774C83.1772 64.7774 83.1772 64.7774 83.324 64.7774C83.9112 65.3662 84.4985 65.9549 84.7921 66.6908Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M127.809 69.1928V89.5045C127.809 91.2707 126.782 93.037 125.314 93.9201L109.751 102.898C108.87 103.487 107.696 103.782 106.668 103.782V78.4656L127.075 66.6907C127.516 67.4266 127.809 68.3097 127.809 69.1928Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M106.668 78.4656V103.782C105.64 103.782 104.612 103.487 103.585 102.898L88.0222 93.9201C86.4073 93.037 85.5264 91.4179 85.5264 89.5045V69.1928C85.5264 68.3097 85.82 67.4266 86.2604 66.6907L106.668 78.4656Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M127.075 66.6908L106.668 78.4657L86.1138 66.5437C86.5542 65.8077 87.1415 65.0718 88.0224 64.6302L104.025 55.3575C105.493 54.4744 107.402 54.4744 109.017 55.2103L125.46 64.7774C125.46 64.7774 125.46 64.7774 125.607 64.7774C126.194 65.3662 126.782 65.9549 127.075 66.6908Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M64.2381 30.7773V51.089C64.2381 52.8552 63.2104 54.6214 61.7423 55.5045L46.1798 64.4829C45.2989 65.0716 44.1244 65.366 43.0967 65.366V40.05L63.5041 28.2751C64.0913 29.0111 64.2381 29.8942 64.2381 30.7773Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M43.0965 40.05V65.366C42.0688 65.366 41.0411 65.0716 40.0134 64.4829L24.4509 55.5045C22.836 54.6214 21.9551 53.0024 21.9551 51.089V30.7773C21.9551 29.8942 22.2487 29.0111 22.6892 28.2751L43.0965 40.05Z" fill="#F1F3F9" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M63.6505 28.275L43.2432 40.0498L22.689 28.1278C23.1294 27.3918 23.7167 26.6559 24.5976 26.2143L40.6005 16.9416C42.0686 16.0585 43.9772 16.0585 45.5922 16.7944L61.8887 26.5087C61.8887 26.5087 61.8887 26.5087 62.0356 26.5087C62.6228 26.9503 63.2101 27.539 63.6505 28.275Z" fill="white" stroke="#D6DCE8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M109.311 30.3358V53.0024C109.311 55.063 108.283 56.9765 106.374 58.0068L88.9031 68.1626C87.8754 68.7513 86.7009 69.0457 85.5264 69.0457V40.6388L108.576 27.3921C109.017 28.2752 109.311 29.3055 109.311 30.3358Z" fill="white" stroke="#AAB2C5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M85.5262 40.6388V58.5955V69.0457C84.3516 69.0457 83.1771 68.7513 82.1494 68.1626L74.515 63.747L64.5315 58.0067C62.7697 56.9764 61.742 55.063 61.5952 53.1496V30.1886C61.5952 29.1583 61.8888 28.128 62.3293 27.2449L85.5262 40.6388Z" fill="#F1F3F9" stroke="#AAB2C5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M108.429 27.3921L85.3792 40.6388L62.3291 27.2449C62.7695 26.3618 63.5036 25.6258 64.3845 25.1843L82.296 14.7341C84.0578 13.7038 86.1132 13.7038 87.875 14.5869L106.374 25.3315C106.374 25.3315 106.521 25.3315 106.521 25.4787C107.402 26.0674 107.989 26.6561 108.429 27.3921Z" fill="white" stroke="#AAB2C5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.206 12.7656C108.856 12.7656 109.383 13.2922 109.383 13.9418V20.9987C109.383 21.6483 108.856 22.1749 108.206 22.1749C107.557 22.1749 107.03 21.6483 107.03 20.9987V13.9418C107.03 13.2922 107.557 12.7656 108.206 12.7656Z" fill="#AAB2C5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M123.936 13.1101C124.396 13.5694 124.396 14.3141 123.936 14.7735L115.311 23.3986C114.852 23.8579 114.107 23.8579 113.648 23.3986C113.188 22.9393 113.188 22.1946 113.648 21.7353L122.273 13.1101C122.732 12.6508 123.477 12.6508 123.936 13.1101Z" fill="#AAB2C5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.087 29.624C114.087 28.9745 114.614 28.4479 115.264 28.4479H123.105C123.754 28.4479 124.281 28.9745 124.281 29.624C124.281 30.2736 123.754 30.8002 123.105 30.8002H115.264C114.614 30.8002 114.087 30.2736 114.087 29.624Z" fill="#AAB2C5"/>
<path d="M96.9656 43.7842C96.7253 44.8023 96.3648 45.8804 95.9443 46.7788C94.8027 48.9948 93.0003 50.7317 90.7772 51.8696C88.4942 53.0076 85.7905 53.4867 83.0868 52.8878C76.7182 51.5702 72.6326 45.3414 73.9544 38.9928C75.2762 32.6442 81.4646 28.5116 87.8333 29.8891C90.1164 30.3683 92.099 31.5062 93.7813 33.0634C96.6052 35.8784 97.8068 39.9511 96.9656 43.7842Z" fill="#D6DCE8" stroke="#AAB2C5" stroke-width="2" stroke-miterlimit="10"/>
<path d="M89.1596 39.3733H87.4797V37.6934C87.4797 36.644 86.6532 35.681 85.4673 35.681C84.4179 35.681 83.4549 36.5075 83.4549 37.6934V39.3733H81.7751C80.7257 39.3733 79.7627 40.1998 79.7627 41.3857C79.7627 41.923 79.9549 42.4387 80.3385 42.8223C80.7221 43.2059 81.2378 43.398 81.7751 43.398H83.4549V45.0779C83.4549 46.1273 84.2814 47.0903 85.4673 47.0903C86.5167 47.0903 87.4797 46.2638 87.4797 45.0779V43.398H89.1596C90.2089 43.398 91.172 42.5716 91.172 41.3857C91.172 40.1998 90.2089 39.3733 89.1596 39.3733Z" fill="white" stroke="#AAB2C5" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,5 +1,5 @@
/** Scope for atoms stored at the root of the app */
export const globalScope = Symbol("globalScope");
export const projectScope = Symbol("projectScope");
export * from "./auth";
export * from "./project";

View File

@@ -1,9 +1,12 @@
import { atom } from "jotai";
import { sortBy } from "lodash-es";
import { ThemeOptions } from "@mui/material";
import { userRolesAtom } from "./auth";
import { UserSettings } from "./user";
import {
PublicSettings,
ProjectSettings,
UserSettings,
} from "@src/types/settings";
import {
UpdateDocFunction,
UpdateCollectionDocFunction,
@@ -15,22 +18,6 @@ import { FunctionSettings } from "@src/types/function";
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 */
@@ -38,21 +25,6 @@ export const updatePublicSettingsAtom = atom<
UpdateDocFunction<PublicSettings> | undefined
>(undefined);
/** Project settings are visible to authenticated users */
export type ProjectSettings = Partial<{
tables: TableSettings[];
setupCompleted: boolean;
rowyRunUrl: string;
rowyRunRegion: string;
rowyRunDeployStatus: "BUILDING" | "COMPLETE";
services: Partial<{
hooks: string;
builder: string;
terminal: string;
}>;
}>;
/** Project settings are visible to authenticated users */
export const projectSettingsAtom = atom<ProjectSettings>({});
/**
@@ -60,7 +32,7 @@ export const projectSettingsAtom = atom<ProjectSettings>({});
*
* @example Basic usage:
* ```
* const [updateProjectSettings] = useAtom(updateProjectSettingsAtom, globalScope);
* const [updateProjectSettings] = useAtom(updateProjectSettingsAtom, projectScope);
* if (updateProjectSettings) updateProjectSettings({ ... });
* ```
*/
@@ -74,10 +46,10 @@ export const tablesAtom = atom<TableSettings[]>((get) => {
const tables = get(projectSettingsAtom).tables || [];
return sortBy(tables, "name")
.filter(
(table) =>
userRoles.includes("ADMIN") ||
table.roles.some((role) => userRoles.includes(role))
.filter((table) =>
userRoles.includes("ADMIN") || Array.isArray(table.roles)
? table.roles.some((role) => userRoles.includes(role))
: false
)
.map((table) => ({
...table,

View File

@@ -51,7 +51,7 @@ export interface IRowyRunRequestProps {
*
* @example Basic usage:
* ```
* const [rowyRun] = useAtom(rowyRunAtom, globalScope);
* const [rowyRun] = useAtom(rowyRunAtom, projectScope);
* ...
* await rowyRun(...);
* ```
@@ -117,8 +117,13 @@ export const rowyRunAtom = atom((get) => {
/** Store deployed Rowy Run version */
export const rowyRunVersionAtom = atom(async (get) => {
const rowyRun = get(rowyRunAtom);
const response = await rowyRun({ route: runRoutes.version });
return response.version as string | false;
try {
const response = await rowyRun({ route: runRoutes.version });
return response.version as string | false;
} catch (e) {
console.log(e);
return false;
}
});
/**

View File

@@ -13,8 +13,6 @@ export const altPressAtom = atom(false);
/** 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">(
@@ -47,21 +45,22 @@ export type ConfirmDialogProps = {
/** Optionally set dialog max width */
maxWidth?: DialogProps["maxWidth"];
/** Optionally set button layout */
buttonLayout?: "horizontal" | "vertical";
};
/**
* Open a confirm dialog
*
* @example Basic usage:
* ```
* const confirm = useSetAtom(confirmDialogAtom, globalScope);
* const confirm = useSetAtom(confirmDialogAtom, projectScope);
* confirm({ handleConfirm: () => ... });
* ```
*/
export const confirmDialogAtom = atom(
{ open: false } as ConfirmDialogProps,
(get, set, update: Partial<ConfirmDialogProps>) => {
(_, set, update: Partial<ConfirmDialogProps>) => {
set(confirmDialogAtom, {
...get(confirmDialogAtom),
open: true, // Dont require this to be set explicitly
...update,
});
@@ -79,7 +78,7 @@ export type RowyRunModalState = {
*
* @example Basic usage:
* ```
* const openRowyRunModal = useSetAtom(rowyRunModalAtom, globalScope);
* const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
* openRowyRunModal({ feature: ... , version: ... });
* ```
*
@@ -111,7 +110,7 @@ export type TableSettingsDialogState = {
*
* @example Basic usage:
* ```
* const openTableSettingsDialog = useSetAtom(tableSettingsDialogAtom, globalScope);
* const openTableSettingsDialog = useSetAtom(tableSettingsDialogAtom, projectScope);
* openTableSettingsDialog({ data: ... });
* ```
*
@@ -145,6 +144,9 @@ export const tableSettingsDialogSchemaAtom = atom(async (get) => {
return getTableSchema(tableId);
});
/** Open the Get Started checklist from anywhere */
export const getStartedChecklistAtom = atom(false);
/** Persist the state of the add row ID type */
export const tableAddRowIdTypeAtom = atomWithStorage<
"decrement" | "random" | "custom"

View File

@@ -1,42 +1,12 @@
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,
TableRowRef,
TableSort,
} from "@src/types/table";
import { UserSettings } from "@src/types/settings";
import { UpdateDocFunction } from "@src/types/table";
/** User info and settings */
export type UserSettings = Partial<{
_rowy_ref: TableRowRef;
/** 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[];
sorts: TableSort[];
}>
>;
}>;
/** User info and settings */
export const userSettingsAtom = atom<UserSettings>({});
/** Stores a function that updates user settings */

View File

@@ -1,10 +1,12 @@
import { atom } from "jotai";
import { findIndex } from "lodash-es";
import { FieldType } from "@src/constants/fields";
import {
tableColumnsOrderedAtom,
tableColumnsReducer,
updateTableSchemaAtom,
tableSchemaAtom,
} from "./table";
import { ColumnConfig } from "@src/types/table";
@@ -14,6 +16,7 @@ export interface IAddColumnOptions {
/** Index to add column at. If undefined, adds to end */
index?: number;
}
/**
* Set function adds a column to tableSchema, to the end or by index.
* Also fixes any issues with column indexes, so they go from 0 to length - 1
@@ -52,6 +55,7 @@ export interface IUpdateColumnOptions {
/** If passed, reorders the column to the index */
index?: number;
}
/**
* Set function updates a column in tableSchema
* @throws Error if column not found
@@ -75,7 +79,7 @@ export const updateColumnAtom = atom(
throw new Error(`Column with key "${key}" not found`);
// If column is not being reordered, just update the config
if (!index) {
if (index === undefined) {
tableColumnsOrdered[currentIndex] = {
...tableColumnsOrdered[currentIndex],
...config,
@@ -110,13 +114,50 @@ export const updateColumnAtom = atom(
* ```
*/
export const deleteColumnAtom = atom(null, async (get, _set, key: string) => {
const tableSchema = get(tableSchemaAtom);
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) throw new Error("Cannot update table schema");
const updatedColumns = tableColumnsOrdered
.filter((c) => c.key !== key)
.map((c) => {
// remove column from derivatives listener fields
if (c.type === FieldType.derivative) {
return {
...c,
config: {
...c.config,
listenerFields:
c.config?.listenerFields?.filter((f) => f !== key) ?? [],
},
};
} else if (c.type === FieldType.action) {
return {
...c,
config: {
...c.config,
requiredFields:
c.config?.requiredFields?.filter((f) => f !== key) ?? [],
},
};
} else {
return c;
}
})
.reduce(tableColumnsReducer, {});
await updateTableSchema({ columns: updatedColumns }, [`columns.${key}`]);
const updatedExtensionObjects = tableSchema?.extensionObjects?.map(
(extension) => {
return {
...extension,
requiredFields: extension.requiredFields.filter((f) => f !== key),
};
}
);
await updateTableSchema(
{ columns: updatedColumns, extensionObjects: updatedExtensionObjects },
[`columns.${key}`]
);
});

View File

@@ -4,7 +4,7 @@ import { useAtomValue, useSetAtom } from "jotai";
import { useAtomCallback } from "jotai/utils";
import { find, findIndex, sortBy } from "lodash-es";
import { currentUserAtom } from "@src/atoms/globalScope";
import { currentUserAtom } from "@src/atoms/projectScope";
import {
tableScope,
tableSettingsAtom,

View File

@@ -6,16 +6,16 @@ import {
set as _set,
isEqual,
unset,
filter,
} from "lodash-es";
import { currentUserAtom } from "@src/atoms/globalScope";
import { currentUserAtom } from "@src/atoms/projectScope";
import {
auditChangeAtom,
tableSettingsAtom,
tableColumnsOrderedAtom,
tableFiltersAtom,
tableRowsLocalAtom,
tableRowsDbAtom,
tableRowsAtom,
_updateRowDbAtom,
_deleteRowDbAtom,
@@ -30,6 +30,7 @@ import {
updateRowData,
omitRowyFields,
} from "@src/utils/table";
import { arrayRemove, arrayUnion } from "firebase/firestore";
export interface IAddRowOptions {
/** The row or array of rows to add */
@@ -62,7 +63,7 @@ export const addRowAtom = atom(
const auditChange = get(auditChangeAtom);
const tableFilters = get(tableFiltersAtom);
const tableColumnsOrdered = get(tableColumnsOrderedAtom);
const tableRowsDb = get(tableRowsDbAtom);
const tableRows = get(tableRowsAtom);
const _addSingleRowAndAudit = async (row: TableRow) => {
// Store initial values to be written
@@ -114,43 +115,52 @@ export const addRowAtom = atom(
// Combine initial values with row values
const rowValues = { ...initialValues, ...row };
// Add to rowsLocal (i.e. display on top, out of order) if:
// - any required fields are missing
// (**not out of order if IDs are not decrementing**)
// Add to rowsLocal (display on top, out of order) if:
// - deliberately out of order
// - there are filters set and we couldnt set the value of a field to
// fit in the filtered query
// - user did not set ID to decrement
if (
missingRequiredFields.length > 0 ||
row._rowy_outOfOrder === true ||
outOfOrderFilters.size > 0 ||
setId !== "decrement"
) {
set(tableRowsLocalAtom, {
type: "add",
row: {
...rowValues,
_rowy_outOfOrder:
row._rowy_outOfOrder === true ||
outOfOrderFilters.size > 0 ||
setId !== "decrement",
},
row: { ...rowValues, _rowy_outOfOrder: true },
});
}
// Also add to rowsLocal if any required fields are missing
// (not out of order since those cases are handled above)
else if (missingRequiredFields.length > 0) {
set(tableRowsLocalAtom, {
type: "add",
row: { ...rowValues, _rowy_outOfOrder: false },
});
}
// Write to database if no required fields are missing
if (missingRequiredFields.length === 0) {
else {
await updateRowDb(row._rowy_ref.path, omitRowyFields(rowValues));
}
if (auditChange) auditChange("ADD_ROW", row._rowy_ref.path);
};
// Find the first row in order to be used to decrement ID
let firstInOrderRowId = tableRows[0]?._rowy_ref.id;
for (const row of tableRows) {
if (row._rowy_outOfOrder === false) {
firstInOrderRowId = row._rowy_ref.id;
break;
}
}
if (Array.isArray(row)) {
const promises: Promise<void>[] = [];
let lastId = tableRowsDb[0]?._rowy_ref.id;
let lastId = firstInOrderRowId;
for (const r of row) {
const id =
setId === "random"
@@ -175,7 +185,7 @@ export const addRowAtom = atom(
setId === "random"
? generateId()
: setId === "decrement"
? decrementId(tableRowsDb[0]?._rowy_ref.id)
? decrementId(firstInOrderRowId)
: row._rowy_ref.id;
const path = setId
@@ -213,7 +223,9 @@ export const deleteRowAtom = atom(
find(tableRowsLocal, ["_rowy_ref.path", path])
);
if (isLocalRow) set(tableRowsLocalAtom, { type: "delete", path });
else await deleteRowDb(path);
// Always delete from db in case it exists
await deleteRowDb(path);
if (auditChange) auditChange("DELETE_ROW", path);
};
@@ -265,21 +277,21 @@ export const bulkAddRowsAtom = atom(
// Assign a random ID to each row
const operations = rows.map((row) => ({
type: "add" as "add",
path: `${collection}/${generateId()}`,
type: row?._rowy_ref?.id ? ("update" as "update") : ("add" as "add"),
path: `${collection}/${row?._rowy_ref?.id ?? generateId()}`,
data: { ...initialValues, ...omitRowyFields(row) },
}));
// Write to db
await bulkWriteDb(operations, onBatchCommit);
if (auditChange) {
const auditChangePromises: Promise<void>[] = [];
for (const operation of operations) {
auditChangePromises.push(auditChange("ADD_ROW", operation.path));
}
await Promise.all(auditChangePromises);
}
// if (auditChange) {
// const auditChangePromises: Promise<void>[] = [];
// for (const operation of operations) {
// auditChangePromises.push(auditChange("ADD_ROW", operation.path));
// }
// await Promise.all(auditChangePromises);
// }
}
);
@@ -296,6 +308,10 @@ export interface IUpdateFieldOptions {
ignoreRequiredFields?: boolean;
/** Optionally, disable checking if the updated value is equal to the current value. By default, we skip the update if theyre equal. */
disableCheckEquality?: boolean;
/** Optionally, uses firestore's arrayUnion with the given value. Appends given value items to the existing array */
useArrayUnion?: boolean;
/** Optionally, uses firestore's arrayRemove with the given value. Removes given value items from the existing array */
useArrayRemove?: boolean;
}
/**
* Set function updates or deletes a field in a row.
@@ -321,6 +337,8 @@ export const updateFieldAtom = atom(
deleteField,
ignoreRequiredFields,
disableCheckEquality,
useArrayUnion,
useArrayRemove,
}: IUpdateFieldOptions
) => {
const updateRowDb = get(_updateRowDbAtom);
@@ -357,8 +375,36 @@ export const updateFieldAtom = atom(
_set(update, fieldName, value);
}
const localUpdate = cloneDeep(update);
const dbUpdate = cloneDeep(update);
// apply arrayUnion
if (useArrayUnion) {
if (!Array.isArray(update[fieldName]))
throw new Error("Field must be an array");
// use basic array merge on local row value
localUpdate[fieldName] = [
...(row[fieldName] ?? []),
...localUpdate[fieldName],
];
dbUpdate[fieldName] = arrayUnion(...dbUpdate[fieldName]);
}
//apply arrayRemove
if (useArrayRemove) {
if (!Array.isArray(update[fieldName]))
throw new Error("Field must be an array");
// use basic array filter on local row value
localUpdate[fieldName] = filter(
row[fieldName] ?? [],
(el) => !find(localUpdate[fieldName], el)
);
dbUpdate[fieldName] = arrayRemove(...dbUpdate[fieldName]);
}
// Check for required fields
const newRowValues = updateRowData(cloneDeep(row), update);
const newRowValues = updateRowData(cloneDeep(row), dbUpdate);
const requiredFields = ignoreRequiredFields
? []
: tableColumnsOrdered
@@ -373,7 +419,7 @@ export const updateFieldAtom = atom(
set(tableRowsLocalAtom, {
type: "update",
path,
row: update,
row: localUpdate,
deleteFields: deleteField ? [fieldName] : [],
});
@@ -393,7 +439,7 @@ export const updateFieldAtom = atom(
else {
await updateRowDb(
row._rowy_ref.path,
omitRowyFields(update),
omitRowyFields(dbUpdate),
deleteField ? [fieldName] : []
);
}

View File

@@ -1,6 +1,6 @@
import { atom } from "jotai";
import { atomWithReducer, atomWithHash } from "jotai/utils";
import { uniqBy, findIndex, cloneDeep, unset, orderBy } from "lodash-es";
import { findIndex, cloneDeep, unset, orderBy } from "lodash-es";
import {
TableSettings,
@@ -102,49 +102,53 @@ const tableRowsLocalReducer = (
prev: TableRow[],
action: TableRowsLocalAction
): TableRow[] => {
if (action.type === "set") {
return [...action.rows];
}
if (action.type === "add") {
if (Array.isArray(action.row)) return [...action.row, ...prev];
return [action.row, ...prev];
}
if (action.type === "update") {
const index = findIndex(prev, ["_rowy_ref.path", action.path]);
if (index > -1) {
const updatedRows = [...prev];
if (Array.isArray(action.deleteFields)) {
switch (action.type) {
case "set":
return [...action.rows];
case "add":
if (Array.isArray(action.row)) return [...action.row, ...prev];
return [action.row, ...prev];
case "update":
const index = findIndex(prev, ["_rowy_ref.path", action.path]);
if (index > -1) {
const updatedRows = [...prev];
updatedRows[index] = cloneDeep(prev[index]);
for (const field of action.deleteFields) {
unset(updatedRows[index], field);
if (Array.isArray(action.deleteFields)) {
for (const field of action.deleteFields) {
unset(updatedRows[index], field);
}
}
updatedRows[index] = updateRowData(updatedRows[index], action.row);
return updatedRows;
}
updatedRows[index] = updateRowData(updatedRows[index], action.row);
return updatedRows;
}
// If not found, add to start
if (index === -1)
return [
{
...action.row,
_rowy_ref: {
path: action.path,
id: action.path.split("/").pop() || action.path,
// If not found, add to start
else {
return [
{
...action.row,
_rowy_ref: {
path: action.path,
id: action.path.split("/").pop() || action.path,
},
},
},
...prev,
];
}
if (action.type === "delete") {
return prev.filter((row) => {
if (Array.isArray(action.path)) {
return !action.path.includes(row._rowy_ref.path);
} else {
return row._rowy_ref.path !== action.path;
...prev,
];
}
});
case "delete":
return prev.filter((row) => {
if (Array.isArray(action.path)) {
return !action.path.includes(row._rowy_ref.path);
} else {
return row._rowy_ref.path !== action.path;
}
});
default:
throw new Error("Invalid action");
}
throw new Error("Invalid action");
};
/**
* Store rows that are out of order or not ready to be written to the db.
@@ -157,13 +161,34 @@ export const tableRowsLocalAtom = atomWithReducer(
/** Store rows from the db listener */
export const tableRowsDbAtom = atom<TableRow[]>([]);
/** Combine tableRowsLocal and tableRowsDb */
export const tableRowsAtom = atom<TableRow[]>((get) =>
uniqBy(
[...get(tableRowsLocalAtom), ...get(tableRowsDbAtom)],
"_rowy_ref.path"
)
);
export const tableRowsAtom = atom<TableRow[]>((get) => {
const rowsDb = [...get(tableRowsDbAtom)];
const rowsLocal = get(tableRowsLocalAtom);
// Optimization: create Map of rowsDb by path to index for faster lookup
const rowsDbMap = new Map<string, number>();
rowsDb.forEach((row, i) => rowsDbMap.set(row._rowy_ref.path, i));
// Loop through rowsLocal, which is usually the smaller of the two arrays
const rowsLocalToMerge = rowsLocal.map((row, i) => {
// If row is in rowsDb, merge the two
// and remove from rowsDb to prevent duplication
if (rowsDbMap.has(row._rowy_ref.path)) {
const index = rowsDbMap.get(row._rowy_ref.path)!;
const merged = updateRowData({ ...rowsDb[index] }, row);
rowsDb.splice(index, 1);
return merged;
}
return row;
});
// Merge the two arrays
return [...rowsLocalToMerge, ...rowsDb];
});
/** Store next page state for infinite scroll */
export const tableNextPageAtom = atom({
loading: false,
@@ -213,3 +238,9 @@ export type AuditChangeFunction = (
* @param data - Optional additional data to log
*/
export const auditChangeAtom = atom<AuditChangeFunction | undefined>(undefined);
/**
* Store total number of rows in the table, respecting current filters.
* If `undefined`, the query hasnt loaded yet.
*/
export const serverDocCountAtom = atom<number | undefined>(undefined);

View File

@@ -10,7 +10,7 @@ import { SEVERITY_LEVELS } from "@src/components/TableModals/CloudLogsModal/Clou
*
* @example Basic usage:
* ```
* const openColumnMenu = useSetAtom(columnMenuAtom, globalScope);
* const openColumnMenu = useSetAtom(columnMenuAtom, projectScope);
* openColumnMenu({ column, anchorEl: ... });
* ```
*
@@ -30,7 +30,7 @@ export const columnMenuAtom = atom<{
*
* @example Basic usage:
* ```
* const openColumnModal = useSetAtom(columnModalAtom, globalScope);
* const openColumnModal = useSetAtom(columnModalAtom, projectScope);
* openColumnModal({ type: "...", column });
* ```
*
@@ -56,7 +56,7 @@ export type TableFiltersPopoverState = {
*
* @example Basic usage:
* ```
* const openTableFiltersPopover = useSetAtom(tableFiltersPopoverAtom, globalScope);
* const openTableFiltersPopover = useSetAtom(tableFiltersPopoverAtom, projectScope);
* openTableFiltersPopover({ query: ... });
* ```
*
@@ -84,7 +84,7 @@ export const sideDrawerShowHiddenFieldsAtom = atomWithStorage(
*
* @example Basic usage:
* ```
* const openTableModal = useSetAtom(tableModalAtom, globalScope);
* const openTableModal = useSetAtom(tableModalAtom, projectScope);
* openTableModal("...");
* ```
*
@@ -100,20 +100,41 @@ export const tableModalAtom = atomWithHash<
| "export"
| "importExisting"
| "importCsv"
| "importAirtable"
| null
>("tableModal", null, { replaceState: true });
export type ImportCsvData = { columns: string[]; rows: Record<string, any>[] };
export type ImportAirtableData = { records: Record<string, any>[] };
/** Store import CSV popover and wizard state */
export const importCsvAtom = atom<{
importType: "csv" | "tsv";
csvData: ImportCsvData | null;
}>({ importType: "csv", csvData: null });
/** Store import Airtable popover and wizard state */
export const importAirtableAtom = atom<{
airtableData: ImportAirtableData | null;
apiKey: string;
baseId: string;
tableId: string;
}>({ airtableData: null, apiKey: "", baseId: "", tableId: "" });
/** Store side drawer open state */
export const sideDrawerAtom = atomWithHash<"table-information" | null>(
"sideDrawer",
null,
{ replaceState: true }
);
/** Store side drawer open state */
export const sideDrawerOpenAtom = atom(false);
export type SelectedCell = { path: string; columnKey: string };
export type SelectedCell = {
path: string | "_rowy_header";
columnKey: string | "_rowy_row_actions";
focusInside: boolean;
};
/** Store selected cell in table. Used in side drawer and context menu */
export const selectedCellAtom = atom<SelectedCell | null>(null);

View File

@@ -1,9 +1,35 @@
import { Scope } from "jotai/core/atom";
import { useEffect } from "react";
import { useSetAtom } from "jotai";
import { useAtomsDebugValue } from "jotai/devtools";
import useMemoValue from "use-memo-value";
import { isEqual } from "lodash-es";
export function DebugAtoms(
options: { scope: Scope } & Parameters<typeof useAtomsDebugValue>[0]
options: NonNullable<Parameters<typeof useAtomsDebugValue>[0]>
) {
useAtomsDebugValue(options);
return null;
}
/**
* Sets an atoms value when the `value` prop changes.
* Useful when setting an atoms initialValue and you want to keep it in sync.
*/
export function SyncAtomValue<T>({
atom,
scope,
value,
}: {
atom: Parameters<typeof useSetAtom>[0];
scope: Parameters<typeof useSetAtom>[1];
value: T;
}) {
const memoized = useMemoValue(value, isEqual);
const setAtom = useSetAtom(atom, scope);
useEffect(() => {
setAtom(memoized);
}, [setAtom, memoized]);
return null;
}

View File

@@ -11,28 +11,29 @@ import {
Link as MuiLink,
Button,
} from "@mui/material";
import SecurityIcon from "@mui/icons-material/SecurityOutlined";
import LockIcon from "@mui/icons-material/LockOutlined";
import EmptyState from "@src/components/EmptyState";
import {
globalScope,
projectScope,
currentUserAtom,
userRolesAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { ROUTES } from "@src/constants/routes";
export default function AccessDenied({ resetErrorBoundary }: FallbackProps) {
const [currentUser] = useAtom(currentUserAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [currentUser] = useAtom(currentUserAtom, projectScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
if (!currentUser) window.location.reload();
return (
<EmptyState
role="alert"
fullScreen
Icon={SecurityIcon}
Icon={LockIcon}
message="Access denied"
description={
<>

View File

@@ -1,9 +1,13 @@
import { CircularProgress, CircularProgressProps } from "@mui/material";
export interface ICircularProgressOpticalProps extends CircularProgressProps {
size?: number;
}
export default function CircularProgressOptical({
size = 40,
...props
}: CircularProgressProps & { size?: number }) {
}: ICircularProgressOpticalProps) {
const DEFAULT_SIZE = 40;
const DEFAULT_THICKNESS = 3.6;
const linearThickness = (DEFAULT_SIZE / size) * DEFAULT_THICKNESS;

View File

@@ -0,0 +1,117 @@
import { useState, useEffect } from "react";
import CircularProgressOptical, {
ICircularProgressOpticalProps,
} from "@src/components/CircularProgressOptical";
import { Box } from "@mui/material";
export interface ICircularProgressTimedProps
extends ICircularProgressOpticalProps {
/** Duration in seconds */
duration: number;
complete: boolean;
}
export default function CircularProgressTimed({
duration,
complete,
size = 64,
...props
}: ICircularProgressTimedProps) {
const [count, setCount] = useState(0);
useEffect(() => {
if (complete) {
setCount(0);
return;
}
const interval = setInterval(() => {
setCount((c) => {
if (c >= duration) {
clearInterval(interval);
return c;
}
return c + 1;
});
}, 1000);
return () => clearInterval(interval);
}, [duration, complete]);
const DEFAULT_SIZE = 24;
const DEFAULT_THICKNESS = 2.6;
const linearThickness = (DEFAULT_SIZE / size) * DEFAULT_THICKNESS;
const opticalRatio = 1 - (1 - size / DEFAULT_SIZE) / 2;
return (
<Box
sx={{ position: "relative", width: size, height: size, ...props.sx }}
style={props.style}
>
<CircularProgressOptical
{...props}
size={size}
variant="determinate"
value={complete ? 100 : Math.min(90, (count / duration) * 90)}
style={{ position: "absolute", top: 0, left: 0 }}
sx={{
transition: (theme) =>
theme.transitions.create(["color"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.standard,
}),
}}
color={complete ? "success" : "primary"}
/>
{complete ? (
<Box
component="svg"
viewBox="0 -0.5 18 18"
sx={{
position: "absolute",
inset: size * 0.33 * 0.5,
width: size * 0.67,
height: size * 0.67,
"& .tick": {
stroke: (theme) => theme.palette.success.main,
strokeDasharray: 18,
animationName: "draw-tick",
animationTimingFunction: (theme) =>
theme.transitions.easing.easeOut,
animationDuration: (theme) =>
theme.transitions.duration.standard + "ms",
animationDelay: (theme) =>
theme.transitions.duration.standard + "ms",
animationFillMode: "both",
"@keyframes draw-tick": {
from: { strokeDashoffset: 18 },
to: { strokeDashoffset: 0 },
},
},
}}
>
<polyline
strokeWidth={linearThickness * opticalRatio}
strokeLinecap="round"
strokeLinejoin="round"
points="2.705 8.29 7 12.585 15.295 4.29"
fill="none"
className="tick"
/>
</Box>
) : (
<CircularProgressOptical
{...props}
size={size}
style={{ position: "absolute", top: 0, left: 0 }}
sx={{ color: "primary.contrastText", opacity: 0.33 }}
disableShrink
/>
)}
</Box>
);
}

View File

@@ -12,6 +12,8 @@ import useMonacoCustomizations, {
} from "./useMonacoCustomizations";
import FullScreenButton from "@src/components/FullScreenButton";
import { spreadSx } from "@src/utils/ui";
import githubLightTheme from "@src/components/CodeEditor/github-light-default.json";
import githubDarkTheme from "@src/components/CodeEditor/github-dark-default.json";
export interface ICodeEditorProps
extends Partial<EditorProps>,
@@ -89,12 +91,17 @@ export default function CodeEditor({
value={initialEditorValue}
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
className="editor"
beforeMount={(monaco) => {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
}}
onMount={(editor) => {
if (onFocus) editor.onDidFocusEditorWidget(onFocus);
if (onBlur) editor.onDidBlurEditorWidget(onBlur);
}}
{...props}
onValidate={onValidate_}
theme={`github-${theme.palette.mode}`}
options={{
readOnly: disabled,
fontFamily: theme.typography.fontFamilyMono,

View File

@@ -5,7 +5,7 @@ import SecretsIcon from "@mui/icons-material/VpnKeyOutlined";
import FunctionsIcon from "@mui/icons-material/CloudOutlined";
import DocsIcon from "@mui/icons-material/DescriptionOutlined";
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
import { projectScope, projectIdAtom } from "@src/atoms/projectScope";
export interface ICodeEditorHelperProps {
docLink: string;
@@ -19,21 +19,13 @@ export default function CodeEditorHelper({
docLink,
additionalVariables,
}: ICodeEditorHelperProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
const availableVariables = [
{
key: "row",
description: `row has the value of doc.data() it has type definitions using this table's schema, but you can access any field in the document.`,
},
{
key: "db",
description: `db object provides access to firestore database instance of this project. giving you access to any collection or document in this firestore instance`,
},
{
key: "ref",
description: `ref object that represents the reference to the current row in firestore db (ie: doc.ref).`,
},
{
key: "auth",
description: `auth provides access to a firebase auth instance, can be used to manage auth users or generate tokens.`,
@@ -44,7 +36,7 @@ export default function CodeEditorHelper({
},
{
key: "rowy",
description: `rowy provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`,
},
];

View File

@@ -15,6 +15,8 @@ import useMonacoCustomizations, {
} from "./useMonacoCustomizations";
import FullScreenButton from "@src/components/FullScreenButton";
import { spreadSx } from "@src/utils/ui";
import githubLightTheme from "@src/components/CodeEditor/github-light-default.json";
import githubDarkTheme from "@src/components/CodeEditor/github-dark-default.json";
export interface IDiffEditorProps
extends Partial<DiffEditorProps>,
@@ -73,7 +75,12 @@ export default function DiffEditor({
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
className="editor"
{...props}
beforeMount={(monaco) => {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
}}
onMount={handleEditorMount}
theme={`github-${theme.palette.mode}`}
options={
{
readOnly: disabled,

View File

@@ -30,7 +30,7 @@ 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 { runRoutes } from "@src/constants/runRoutes";
import { rowyRunAtom, globalScope } from "@src/atoms/globalScope";
import { rowyRunAtom, projectScope } from "@src/atoms/projectScope";
import { getFieldProp } from "@src/components/fields";
export interface IUseMonacoCustomizationsProps {
@@ -63,38 +63,26 @@ export default function useMonacoCustomizations({
const theme = useTheme();
const monaco = useMonaco();
const [tableRows] = useAtom(tableRowsAtom, tableScope);
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
useEffect(() => {
return () => {
onUnmount?.();
};
}, []);
// Initialize theme
useEffect(() => {
if (!monaco) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
setTimeout(() => {
try {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
monaco.editor.setTheme("github-" + theme.palette.mode);
} catch (error) {
console.error("Could not set Monaco theme: ", error);
}
});
}, [monaco, theme.palette.mode]);
// Initialize external libs & TypeScript compiler options
useEffect(() => {
if (!monaco) return;
try {
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
});
monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
firebaseAuthDefs
@@ -102,11 +90,6 @@ export default function useMonacoCustomizations({
monaco.languages.typescript.javascriptDefaults.addExtraLib(
firebaseStorageDefs
);
// Compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
});
monaco.languages.typescript.javascriptDefaults.addExtraLib(
utilsDefs,
"ts:filename/utils.d.ts"
@@ -140,9 +123,13 @@ export default function useMonacoCustomizations({
if (!monaco) return;
try {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
JSON.parse(stringifiedDiagnosticsOptions)
);
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
...JSON.parse(stringifiedDiagnosticsOptions),
diagnosticCodesToIgnore: [
1323, // remove dynamic import error
2307, // silence type declarations not found for dynamic import
],
});
} catch (error) {
console.error("Could not set diagnostics options: ", error);
}

View File

@@ -7,6 +7,7 @@ import {
ListItemIcon,
ListItemText,
Typography,
Divider,
} from "@mui/material";
import FilterIcon from "@mui/icons-material/FilterList";
import LockOpenIcon from "@mui/icons-material/LockOpen";
@@ -28,17 +29,16 @@ import SettingsIcon from "@mui/icons-material/SettingsOutlined";
import EvalIcon from "@mui/icons-material/PlayCircleOutline";
import MenuContents, { IMenuContentsProps } from "./MenuContents";
import ColumnHeader from "@src/components/Table/Column";
import ColumnHeader from "@src/components/Table/Mock/Column";
import {
globalScope,
userRolesAtom,
projectScope,
userSettingsAtom,
updateUserSettingsAtom,
confirmDialogAtom,
rowyRunAtom,
altPressAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import {
tableScope,
tableIdAtom,
@@ -50,12 +50,18 @@ import {
columnModalAtom,
tableFiltersPopoverAtom,
tableNextPageAtom,
tableSchemaAtom,
} from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { analytics, logEvent } from "@src/analytics";
import { formatSubTableName, getTableSchemaPath } from "@src/utils/table";
import {
formatSubTableName,
getTableBuildFunctionPathname,
getTableSchemaPath,
} from "@src/utils/table";
import { runRoutes } from "@src/constants/runRoutes";
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
export interface IMenuModalProps {
name: string;
@@ -73,12 +79,21 @@ export interface IMenuModalProps {
) => void;
}
export default function ColumnMenu() {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [userSettings] = useAtom(userSettingsAtom, globalScope);
const [updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope);
const confirm = useSetAtom(confirmDialogAtom, globalScope);
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
export interface IColumnMenuProps {
canAddColumns: boolean;
canEditColumns: boolean;
canDeleteColumns: boolean;
}
export default function ColumnMenu({
canAddColumns,
canEditColumns,
canDeleteColumns,
}: IColumnMenuProps) {
const [userSettings] = useAtom(userSettingsAtom, projectScope);
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
@@ -91,8 +106,10 @@ export default function ColumnMenu() {
tableScope
);
const [tableNextPage] = useAtom(tableNextPageAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const snackLogContext = useSnackLogContext();
const [altPress] = useAtom(altPressAtom, globalScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
if (!columnMenu) return null;
@@ -117,8 +134,42 @@ export default function ColumnMenu() {
const userDocHiddenFields =
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [];
let referencedColumns: string[] = [];
let referencedExtensions: string[] = [];
Object.entries(tableSchema?.columns ?? {}).forEach(([key, c], index) => {
if (
c.config?.listenerFields?.includes(column.key) ||
c.config?.requiredFields?.includes(column.key)
) {
referencedColumns.push(c.name);
}
});
tableSchema?.extensionObjects?.forEach((extension) => {
if (extension.requiredFields.includes(column.key)) {
referencedExtensions.push(extension.name);
}
});
const requireRebuild =
referencedColumns.length || referencedExtensions.length;
const handleDeleteColumn = () => {
deleteColumn(column.key);
if (requireRebuild) {
snackLogContext.requestSnackLog();
rowyRun({
route: runRoutes.buildFunction,
body: {
tablePath: tableSettings.collection,
// pathname must match old URL format
pathname: getTableBuildFunctionPathname(
tableSettings.id,
tableSettings.tableType
),
tableConfigPath: getTableSchemaPath(tableSettings),
},
});
logEvent(analytics, "deployed_extensions");
}
logEvent(analytics, "delete_column", { type: column.type });
handleClose();
};
@@ -205,7 +256,7 @@ export default function ColumnMenu() {
});
handleClose();
},
active: !column.editable,
active: column.editable === false,
},
{
label: "Disable resize",
@@ -329,6 +380,7 @@ export default function ColumnMenu() {
</>
),
handleConfirm: handleEvaluateAll,
confirm: "Evaluate",
}),
},
];
@@ -343,6 +395,7 @@ export default function ColumnMenu() {
openColumnModal({ type: "new", index: column.index - 1 });
handleClose();
},
disabled: !canAddColumns,
},
{
label: "Insert to the right…",
@@ -352,6 +405,7 @@ export default function ColumnMenu() {
openColumnModal({ type: "new", index: column.index + 1 });
handleClose();
},
disabled: !canAddColumns,
},
{
label: `Delete column${altPress ? "" : "…"}`,
@@ -359,8 +413,8 @@ export default function ColumnMenu() {
icon: <ColumnRemoveIcon />,
onClick: altPress
? handleDeleteColumn
: () =>
confirm({
: () => {
return confirm({
title: "Delete column?",
body: (
<>
@@ -372,23 +426,53 @@ export default function ColumnMenu() {
<Typography sx={{ mt: 1 }}>
Key: <code style={{ userSelect: "all" }}>{column.key}</code>
</Typography>
{requireRebuild ? (
<>
<Divider sx={{ my: 2 }} />
{referencedColumns.length ? (
<Typography sx={{ mt: 1 }}>
This column will be removed as a dependency of the
following columns:{" "}
<Typography fontWeight="bold" component="span">
{referencedColumns.join(", ")}
</Typography>
</Typography>
) : null}
{referencedExtensions.length ? (
<Typography sx={{ mt: 1 }}>
This column will be removed as a dependency from the
following Extensions:{" "}
<Typography fontWeight="bold" component="span">
{referencedExtensions.join(", ")}
</Typography>
</Typography>
) : null}
<Typography sx={{ mt: 1, fontWeight: "bold" }}>
You need to re-deploy this tables cloud function.
</Typography>
</>
) : null}
</>
),
confirm: "Delete",
confirm: requireRebuild ? "Delete & re-deploy" : "Delete",
confirmColor: "error",
handleConfirm: handleDeleteColumn,
}),
});
},
color: "error" as "error",
disabled: !canDeleteColumns,
},
];
let menuItems = [...localViewActions];
if (userRoles.includes("ADMIN") || userRoles.includes("OPS")) {
if (canEditColumns) {
menuItems.push.apply(menuItems, configActions);
if (column.type === FieldType.derivative) {
menuItems.push.apply(menuItems, derivativeActions);
}
}
if (canAddColumns || canDeleteColumns) {
menuItems.push.apply(menuItems, columnActions);
}

View File

@@ -13,10 +13,10 @@ import { InlineErrorFallback } from "@src/components/ErrorFallback";
import Loading from "@src/components/Loading";
import {
globalScope,
projectScope,
rowyRunAtom,
confirmDialogAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import {
tableScope,
tableSettingsAtom,
@@ -35,10 +35,10 @@ export default function ColumnConfigModal({
onClose,
column,
}: IColumnModalProps) {
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
const confirm = useSetAtom(confirmDialogAtom, globalScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const snackLogContext = useSnackLogContext();

View File

@@ -14,10 +14,10 @@ import { WIKI_LINKS } from "@src/constants/externalLinks";
/* eslint-disable import/no-webpack-loader-syntax */
import defaultValueDefs from "!!raw-loader!./defaultValue.d.ts";
import {
globalScope,
projectScope,
compatibleRowyRunVersionAtom,
projectSettingsAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { ColumnConfig } from "@src/types/table";
const CodeEditorComponent = lazy(
@@ -40,7 +40,7 @@ interface ICodeEditorProps {
function CodeEditor({ type, column, handleChange }: ICodeEditorProps) {
const [compatibleRowyRunVersion] = useAtom(
compatibleRowyRunVersionAtom,
globalScope
projectScope
);
const functionBodyOnly = compatibleRowyRunVersion!({ maxVersion: "1.3.10" });
@@ -92,7 +92,7 @@ export default function DefaultValueInput({
handleChange,
column,
}: IDefaultValueInputProps) {
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
const _type =
column.type !== FieldType.derivative
@@ -204,7 +204,19 @@ export default function DefaultValueInput({
{column.config?.defaultValue?.type === "dynamic" && (
<>
<CodeEditorHelper docLink={WIKI_LINKS.howToDefaultValues} />
<CodeEditorHelper
docLink={WIKI_LINKS.howToDefaultValues}
additionalVariables={[
{
key: "row",
description: `row has the value of doc.data() it has type definitions using this table's schema, but you can access any field in the document.`,
},
{
key: "ref",
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
},
]}
/>
<Suspense fallback={<FieldSkeleton height={100} />}>
<CodeEditor
column={column}

View File

@@ -8,7 +8,7 @@ import { TextField, Typography, Button } from "@mui/material";
import Modal from "@src/components/Modal";
import FieldsDropdown from "./FieldsDropdown";
import { globalScope, updateTableAtom } from "@src/atoms/globalScope";
import { projectScope, updateTableAtom } from "@src/atoms/projectScope";
import {
tableScope,
tableSettingsAtom,
@@ -29,14 +29,14 @@ const AUDIT_FIELD_TYPES = [
export default function NewColumnModal({
onClose,
}: Pick<IColumnModalProps, "onClose">) {
const [updateTable] = useAtom(updateTableAtom, globalScope);
const [updateTable] = useAtom(updateTableAtom, projectScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const addColumn = useSetAtom(addColumnAtom, tableScope);
const [columnModal, setColumnModal] = useAtom(columnModalAtom, tableScope);
const [columnLabel, setColumnLabel] = useState("");
const [fieldKey, setFieldKey] = useState("");
const [type, setType] = useState(FieldType.shortText);
const [type, setType] = useState("" as any);
const requireConfiguration = getFieldProp("requireConfiguration", type);
const isAuditField = AUDIT_FIELD_TYPES.includes(type);

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useAtom } from "jotai";
import {
@@ -10,15 +10,21 @@ import {
TextField,
Button,
} from "@mui/material";
import MemoizedText from "@src/components/Modal/MemoizedText";
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope";
export interface IConfirmDialogProps {
scope?: Parameters<typeof useAtom>[1];
}
/**
* Display a confirm dialog using `confirmDialogAtom` in `globalState`
* @see {@link confirmDialogAtom | Usage example}
*/
export default function ConfirmDialog() {
export default function ConfirmDialog({
scope = projectScope,
}: IConfirmDialogProps) {
const [
{
open,
@@ -36,12 +42,22 @@ export default function ConfirmDialog() {
hideCancel,
maxWidth = "xs",
buttonLayout = "horizontal",
},
setState,
] = useAtom(confirmDialogAtom, globalScope);
const handleClose = () => setState({ open: false });
] = useAtom(confirmDialogAtom, scope);
const [dryText, setDryText] = useState("");
const handleClose = () => {
setState({ open: false });
setDisableConfirm(false);
};
const [disableConfirm, setDisableConfirm] = useState(
Boolean(confirmationCommand)
);
useEffect(() => {
setDisableConfirm(Boolean(confirmationCommand));
}, [confirmationCommand]);
return (
<Dialog
@@ -51,57 +67,74 @@ export default function ConfirmDialog() {
else handleClose();
}}
maxWidth={maxWidth}
TransitionComponent={SlideTransitionMui}
sx={{ cursor: "default", zIndex: (theme) => theme.zIndex.modal + 50 }}
>
<DialogTitle>{title}</DialogTitle>
<>
<MemoizedText>
<DialogTitle>{title}</DialogTitle>
</MemoizedText>
<DialogContent>
{typeof body === "string" ? (
<DialogContentText>{body}</DialogContentText>
) : (
body
)}
{confirmationCommand && (
<TextField
value={dryText}
onChange={(e) => setDryText(e.target.value)}
autoFocus
label={`Type ${confirmationCommand} below to continue:`}
placeholder={confirmationCommand}
fullWidth
id="dryText"
sx={{ mt: 3 }}
/>
)}
</DialogContent>
<MemoizedText>
<DialogContent>
{typeof body === "string" ? (
<DialogContentText>{body}</DialogContentText>
) : (
body
)}
<DialogActions>
{!hideCancel && (
<Button
onClick={() => {
if (handleCancel) handleCancel();
handleClose();
}}
>
{cancel}
</Button>
)}
<Button
onClick={() => {
if (handleConfirm) handleConfirm();
handleClose();
}}
color={confirmColor || "primary"}
variant="contained"
autoFocus
disabled={
confirmationCommand ? dryText !== confirmationCommand : false
}
{confirmationCommand && (
<TextField
onChange={(e) =>
setDisableConfirm(e.target.value !== confirmationCommand)
}
label={`Type “${confirmationCommand}” below to continue:`}
placeholder={confirmationCommand}
fullWidth
id="dryText"
sx={{ mt: 3 }}
/>
)}
</DialogContent>
</MemoizedText>
<DialogActions
sx={[
buttonLayout === "vertical" && {
flexDirection: "column",
alignItems: "stretch",
"& > :not(:first-of-type)": { ml: 0, mt: 1 },
},
]}
>
{confirm}
</Button>
</DialogActions>
<MemoizedText>
{!hideCancel && (
<Button
onClick={() => {
if (handleCancel) handleCancel();
handleClose();
}}
>
{cancel}
</Button>
)}
</MemoizedText>
<MemoizedText key={disableConfirm.toString()}>
<Button
onClick={() => {
if (handleConfirm) handleConfirm();
handleClose();
}}
color={confirmColor || "primary"}
variant="contained"
autoFocus
disabled={disableConfirm}
>
{confirm}
</Button>
</MemoizedText>
</DialogActions>
</>
</Dialog>
);
}

View File

@@ -0,0 +1,93 @@
import seedrandom from "seedrandom";
import { colord } from "colord";
import { useTheme, Avatar, AvatarProps } from "@mui/material";
import { spreadSx } from "@src/utils/ui";
// https://www.stefanjudis.com/snippets/how-to-detect-emojis-in-javascript-strings/
const emojiRegex = /\p{Emoji}/u;
export const EMOJI_AVATAR_L_LIGHT = 90;
export const EMOJI_AVATAR_L_DARK = 30;
export const EMOJI_AVATAR_C_LIGHT = 15;
export const EMOJI_AVATAR_C_DARK = 20;
export interface IEmojiAvatarProps extends Partial<AvatarProps> {
/** CSS color string or a number (as a string). If number, used as hue */
bgColor?: string;
emoji?: string;
fallback: string;
uid?: string;
size?: number;
}
export default function EmojiAvatar({
bgColor: bgColorProp,
emoji,
fallback,
uid,
children,
size = 40,
...props
}: IEmojiAvatarProps) {
const theme = useTheme();
const darkMode = theme.palette.mode === "dark";
let bgcolor: string;
if (bgColorProp && !Number.isNaN(Number(bgColorProp))) {
bgcolor = colord({
l: darkMode ? EMOJI_AVATAR_L_DARK : EMOJI_AVATAR_L_LIGHT,
c: darkMode ? EMOJI_AVATAR_C_DARK : EMOJI_AVATAR_C_LIGHT,
h: Number(bgColorProp),
}).toHslString();
} else if (bgColorProp) {
bgcolor = bgColorProp;
} else {
bgcolor = generateRandomColor(`${fallback}__${uid}`, darkMode);
}
const bgcolorLch = colord(bgcolor).toLch();
const textColor = colord({
l:
bgcolorLch.l > 50
? Math.max(bgcolorLch.l - 50, 0)
: Math.min(bgcolorLch.l + 50, 100),
c: 30,
h: bgcolorLch.h,
}).toHslString();
return (
<Avatar
{...props}
sx={[
{
bgcolor,
color: textColor,
width: size,
height: size,
fontSize: size * (emojiRegex.test(emoji || "") ? 0.67 : 0.45),
},
props.variant === "rounded" && { borderRadius: size / 40 },
...spreadSx(props.sx),
]}
>
{children ||
emoji ||
fallback
.split(" ")
.slice(0, 2)
.map((s) => s.slice(0, 1))
.join("")}
</Avatar>
);
}
const generateRandomColor = (seed: string, darkMode: boolean) => {
const rng = seedrandom(seed);
const color = colord({
l: darkMode ? EMOJI_AVATAR_L_DARK : EMOJI_AVATAR_L_LIGHT,
c: darkMode ? EMOJI_AVATAR_C_DARK : EMOJI_AVATAR_C_LIGHT,
h: rng() * 360,
});
return color.toHslString();
};

View File

@@ -1,4 +1,5 @@
import { use100vh } from "react-div-100vh";
import clsx from "clsx";
import {
Grid,
@@ -40,7 +41,13 @@ export default function EmptyState({
if (basic)
return (
<Grid container alignItems="center" spacing={1} {...props}>
<Grid
container
alignItems="center"
spacing={1}
{...props}
className={clsx("empty-state", "empty-state--basic", props.className)}
>
<Grid item>
<Icon style={{ display: "block" }} />
</Grid>
@@ -66,6 +73,11 @@ export default function EmptyState({
textAlign: "center",
...props.style,
}}
className={clsx(
"empty-state",
"empty-state--full-screen",
props.className
)}
>
<Grid
item

View File

@@ -1,33 +1,26 @@
import { useState, useEffect } from "react";
import { FallbackProps } from "react-error-boundary";
import { useLocation, Link } from "react-router-dom";
import { useLocation } from "react-router-dom";
import useOffline from "@src/hooks/useOffline";
import { Button } from "@mui/material";
import { Typography, Button } from "@mui/material";
import ReloadIcon from "@mui/icons-material/Refresh";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { Tables as TablesIcon } from "@src/assets/icons";
import OfflineIcon from "@mui/icons-material/CloudOff";
import EmptyState, { IEmptyStateProps } from "@src/components/EmptyState";
import AccessDenied from "@src/components/AccessDenied";
import { ROUTES } from "@src/constants/routes";
import meta from "@root/package.json";
export const ERROR_TABLE_NOT_FOUND = "Table not found";
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
export interface IErrorFallbackProps extends FallbackProps, IEmptyStateProps {}
export default function ErrorFallback({
export function ErrorFallbackContents({
error,
resetErrorBoundary,
...props
}: IErrorFallbackProps) {
// Reset error boundary when navigating away from the page
const location = useLocation();
const [errorPathname] = useState(location.pathname);
useEffect(() => {
if (errorPathname !== location.pathname) resetErrorBoundary();
}, [errorPathname, location.pathname, resetErrorBoundary]);
const isOffline = useOffline();
if ((error as any).code === "permission-denied")
return (
@@ -38,13 +31,31 @@ export default function ErrorFallback({
message: "Something went wrong",
description: (
<>
<span>
<Typography variant="inherit" style={{ whiteSpace: "pre-line" }}>
{(error as any).code && <b>{(error as any).code}: </b>}
{(error as any).status && <b>{(error as any).status}: </b>}
{error.message}
</span>
</Typography>
<Button
size={props.basic ? "small" : "medium"}
href={meta.repository.url.replace(".git", "") + "/issues/new/choose"}
href={
EXTERNAL_LINKS.gitHub +
"/discussions/new?" +
new URLSearchParams({
labels: "bug",
category: "support-q-a",
title: [
"Error",
(error as any).code,
(error as any).status,
error.message,
]
.filter(Boolean)
.join(": ")
.replace(/\n/g, " "),
body: "👉 **Please describe the steps that you took that led to this bug.**",
}).toString()
}
target="_blank"
rel="noopener noreferrer"
>
@@ -55,56 +66,67 @@ export default function ErrorFallback({
),
};
if (error.message.startsWith(ERROR_TABLE_NOT_FOUND)) {
renderProps = {
message: ERROR_TABLE_NOT_FOUND,
description: (
<>
<span>Make sure you have the right ID</span>
<code>{error.message.replace(ERROR_TABLE_NOT_FOUND + ": ", "")}</code>
<Button
size={props.basic ? "small" : "medium"}
variant="outlined"
color="secondary"
component={Link}
to={ROUTES.tables}
startIcon={<TablesIcon />}
onClick={() => resetErrorBoundary()}
>
All tables
</Button>
</>
),
};
}
if (error.message.startsWith("Loading chunk")) {
renderProps = {
Icon: ReloadIcon,
message: "New update available",
description: (
<>
<span>Reload this page to get the latest update</span>
if (isOffline) {
renderProps = { Icon: OfflineIcon, message: "Youre offline" };
} else {
renderProps = {
Icon: ReloadIcon,
message: "Update available",
description: (
<Button
size={props.basic ? "small" : "medium"}
variant="outlined"
color="secondary"
startIcon={<ReloadIcon />}
onClick={() => window.location.reload()}
sx={{ mt: 1 }}
>
Reload
</Button>
</>
),
};
}
}
if (error.message.includes("Failed to fetch")) {
renderProps = {
Icon: OfflineIcon,
message: "Youre offline",
description: isOffline ? null : (
<Button
size={props.basic ? "small" : "medium"}
variant="outlined"
color="secondary"
startIcon={<ReloadIcon />}
onClick={() => window.location.reload()}
sx={{ mt: 1 }}
>
Reload
</Button>
),
};
}
return <EmptyState fullScreen {...renderProps} {...props} />;
return <EmptyState role="alert" fullScreen {...renderProps} {...props} />;
}
export default function ErrorFallback(props: IErrorFallbackProps) {
const { resetErrorBoundary } = props;
// Reset error boundary when navigating away from the page
const location = useLocation();
const [errorPathname] = useState(location.pathname);
useEffect(() => {
if (errorPathname !== location.pathname) resetErrorBoundary();
}, [errorPathname, location.pathname, resetErrorBoundary]);
return <ErrorFallbackContents {...props} />;
}
export function InlineErrorFallback(props: IErrorFallbackProps) {
return (
<ErrorFallback
<ErrorFallbackContents
{...props}
fullScreen={false}
basic
@@ -115,5 +137,5 @@ export function InlineErrorFallback(props: IErrorFallbackProps) {
}
export function NonFullScreenErrorFallback(props: IErrorFallbackProps) {
return <ErrorFallback {...props} fullScreen={false} />;
return <ErrorFallbackContents {...props} fullScreen={false} />;
}

View File

@@ -9,7 +9,7 @@ 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 { projectScope, publicSettingsAtom } from "@src/atoms/projectScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
import { defaultUiConfig, getSignInOptions } from "@src/config/firebaseui";
@@ -29,9 +29,20 @@ const useStyles = makeStyles()((theme) => ({
color: theme.palette.text.secondary,
fontFamily: theme.typography.fontFamily,
},
"& .firebaseui-tos": {
"& .firebaseui-provider-sign-in-footer > .firebaseui-tos": {
...(theme.typography.caption as any),
color: theme.palette.text.disabled,
textAlign: "left",
marginTop: theme.spacing(1),
marginBottom: 0,
"& .firebaseui-link": {
textDecorationColor: theme.palette.divider,
"&:hover": { textDecorationColor: "currentcolor" },
},
},
"& .firebaseui-link": {
color: "inherit",
textDecoration: "underline",
},
"& .firebaseui-country-selector": {
color: theme.palette.text.primary,
@@ -206,8 +217,8 @@ export interface IFirebaseUiProps {
export default function FirebaseUi(props: IFirebaseUiProps) {
const { classes, cx } = useStyles();
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
const [publicSettings] = useAtom(publicSettingsAtom, globalScope);
const [firebaseAuth] = useAtom(firebaseAuthAtom, projectScope);
const [publicSettings] = useAtom(publicSettingsAtom, projectScope);
const signInOptions: typeof publicSettings.signInOptions = useMemo(
() =>
@@ -259,9 +270,10 @@ export default function FirebaseUi(props: IFirebaseUiProps) {
<Typography
variant="button"
display="block"
textAlign="center"
color="textSecondary"
sx={{ mt: -1, mb: -3 }}
sx={{
"&&": { mt: -1, mb: -3, textAlign: "center", alignSelf: "center" },
}}
>
Continue with
</Typography>

View File

@@ -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/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
export interface IFloatingSearchProps extends Partial<FilledTextFieldProps> {
label: string;
@@ -26,7 +26,7 @@ export default function FloatingSearch({
});
const docked = useScrollTrigger({
disableHysteresis: true,
threshold: APP_BAR_HEIGHT,
threshold: TOP_BAR_HEIGHT,
});
return (

View File

@@ -0,0 +1,197 @@
import { useAtom, useSetAtom } from "jotai";
import { Link } from "react-router-dom";
import { Typography, Button } from "@mui/material";
import UncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
import CheckedIcon from "@mui/icons-material/CheckCircleOutline";
import AddIcon from "@mui/icons-material/Add";
import MembersIcon from "@mui/icons-material/AccountCircleOutlined";
import Modal, { IModalProps } from "@src/components/Modal";
import SteppedAccordion from "@src/components/SteppedAccordion";
import GetStartedProgress from "./GetStartedProgress";
import {
projectScope,
getStartedChecklistAtom,
tableSettingsDialogAtom,
} from "@src/atoms/projectScope";
import {
NAV_DRAWER_WIDTH,
NAV_DRAWER_COLLAPSED_WIDTH,
} from "@src/layouts/Navigation/NavDrawer";
import useGetStartedCompletion from "./useGetStartedCompletion";
import { ROUTES } from "@src/constants/routes";
export interface IGetStartedChecklistProps extends Partial<IModalProps> {
navOpen: boolean;
navPermanent: boolean;
}
export default function GetStartedChecklist({
navOpen,
navPermanent,
...props
}: IGetStartedChecklistProps) {
const [open, setOpen] = useAtom(getStartedChecklistAtom, projectScope);
const openTableSettingsDialog = useSetAtom(
tableSettingsDialogAtom,
projectScope
);
const [completedSteps] = useGetStartedCompletion();
if (!open) return null;
const incompleteIcon = <UncheckedIcon color="action" />;
const completeIcon = (
<CheckedIcon color="success" sx={{ color: "success.light" }} />
);
return (
<Modal
{...props}
onClose={() => setOpen(false)}
title="Get started"
hideBackdrop
maxWidth="xs"
PaperProps={{ elevation: 8 }}
fullScreen={false}
sx={[
{
"& .MuiDialog-container": {
justifyContent: "flex-start",
alignItems: "flex-end",
transformOrigin: "0% calc(100% - 160px)",
},
"& .MuiDialog-paper": {
marginLeft: {
xs: `max(env(safe-area-inset-left), 8px)`,
sm: `max(env(safe-area-inset-left), ${
(navPermanent
? navOpen
? NAV_DRAWER_WIDTH
: NAV_DRAWER_COLLAPSED_WIDTH
: 0) + 8
}px)`,
},
marginBottom: `max(env(safe-area-inset-bottom), 8px)`,
marginRight: `max(env(safe-area-inset-right), 8px)`,
width: 360,
},
},
]}
>
<GetStartedProgress sx={{ mb: 2 }} />
<SteppedAccordion
steps={[
{
id: "project",
title: "Create a project",
labelButtonProps: {
icon: completedSteps.project ? completeIcon : incompleteIcon,
},
content: (
<Typography paragraph>
Youve created a project and connected it to a data source.
</Typography>
),
},
{
id: "tutorial",
title: "Complete the table tutorial",
labelButtonProps: {
icon: completedSteps.tutorial ? completeIcon : incompleteIcon,
},
content: (
<>
<Typography>
Learn the basic features and functions of Rowy before creating
a table.
</Typography>
<Button
variant={completedSteps.tutorial ? "outlined" : "contained"}
color="primary"
component={Link}
to={ROUTES.tableTutorial}
onClick={() => setOpen(false)}
>
{completedSteps.tutorial ? "Redo" : "Begin"} tutorial
</Button>
</>
),
},
{
id: "table",
title: "Create a table",
labelButtonProps: {
icon: completedSteps.table ? completeIcon : incompleteIcon,
},
content: (
<>
<Typography>
Use tables to manage the data from your database in a
spreadsheet UI.
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={() => {
openTableSettingsDialog({ open: true });
setOpen(false);
}}
>
Create table
</Button>
</>
),
},
{
id: "members",
title: "Invite team members",
labelButtonProps: {
icon: completedSteps.members ? completeIcon : incompleteIcon,
},
content: (
<>
<Typography>
Collaborate on workspace projects by inviting your team
members. You can control their roles and access.
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<MembersIcon />}
component={Link}
to={ROUTES.members}
onClick={() => setOpen(false)}
>
Members
</Button>
</>
),
},
]}
sx={{
"& .MuiStepConnector-root": {
my: -10 / 8,
},
"& .Mui-active + .MuiStep-root:not(:last-of-type) .MuiStepContent-root":
{
mt: -10 / 8,
pt: 10 / 8,
mb: 10 / 8,
pb: 2,
},
"& .MuiStepContent-root .MuiCollapse-wrapperInner > * + *": {
mt: 1,
},
}}
/>
</Modal>
);
}

View File

@@ -0,0 +1,18 @@
import StepsProgress, {
IStepsProgressProps,
} from "@src/components/StepsProgress";
import useGetStartedCompletion from "./useGetStartedCompletion";
export default function GetStartedProgress(
props: Omit<IStepsProgressProps, "value" | "steps">
) {
const [completedSteps, count] = useGetStartedCompletion();
return (
<StepsProgress
{...props}
value={count}
steps={Object.keys(completedSteps).length}
/>
);
}

View File

@@ -0,0 +1,2 @@
export * from "./GetStartedChecklist";
export { default } from "./GetStartedChecklist";

View File

@@ -0,0 +1,25 @@
import { useAtom } from "jotai";
import {
projectScope,
tablesAtom,
userSettingsAtom,
allUsersAtom,
} from "@src/atoms/projectScope";
export default function useGetStartedCompletion() {
const [tables] = useAtom(tablesAtom, projectScope);
const [userSettings] = useAtom(userSettingsAtom, projectScope);
const [allUsers] = useAtom(allUsersAtom, projectScope);
const completedSteps = {
project: true,
tutorial: Boolean(userSettings.tableTutorialComplete),
table: tables.length > 0,
members: allUsers.length > 0,
};
return [
completedSteps,
Object.values(completedSteps).reduce((a, c) => (c ? a + 1 : a), 0),
] as const;
}

View File

@@ -0,0 +1,14 @@
import { memo, PropsWithChildren } from "react";
/**
* Used for global Modals that can have customizable text
* so that the default text doesnt appear as the modal closes.
*/
const MemoizedText = memo(
function MemoizedTextComponent({ children }: PropsWithChildren<{}>) {
return <>{children}</>;
},
() => true
);
export default MemoizedText;

View File

@@ -11,12 +11,10 @@ import {
DialogActions,
Button,
ButtonProps,
Slide,
} from "@mui/material";
import LoadingButton, { LoadingButtonProps } from "@mui/lab/LoadingButton";
import CloseIcon from "@mui/icons-material/Close";
import { SlideTransitionMui } from "./SlideTransition";
import ScrollableDialogContent, {
IScrollableDialogContentProps,
} from "./ScrollableDialogContent";
@@ -76,7 +74,7 @@ export default function Modal({
setOpen(false);
setEmphasizeCloseButton(false);
setTimeout(() => onClose(setOpen), 300);
setTimeout(() => onClose(setOpen), 195 * 2);
};
return (
@@ -86,8 +84,6 @@ export default function Modal({
onClose={handleClose}
fullWidth
fullScreen={fullScreen}
TransitionComponent={fullScreen ? Slide : SlideTransitionMui}
TransitionProps={fullScreen ? ({ direction: "up" } as any) : undefined}
aria-labelledby="modal-title"
{...props}
sx={

View File

@@ -0,0 +1,91 @@
import { forwardRef, cloneElement } from "react";
import { useTheme, Slide } from "@mui/material";
import { Transition } from "react-transition-group";
import { TransitionProps } from "react-transition-group/Transition";
import { TransitionProps as MuiTransitionProps } from "@mui/material/transitions";
export const ModalTransition: React.ForwardRefExoticComponent<
Pick<TransitionProps, React.ReactText> & React.RefAttributes<any>
> = forwardRef(function ModalTransition(
{ children, ...props }: TransitionProps,
ref: React.Ref<any>
) {
const theme = useTheme();
if (!children) return null;
const isFullScreenDialog = (
Array.isArray(children) ? children[0] : children
).props?.children?.props?.className?.includes("MuiDialog-paperFullScreen");
if (isFullScreenDialog)
return (
<Slide direction="up" appear {...props}>
{children as any}
</Slide>
);
const defaultStyle = {
opacity: 0,
transform: "scale(0.8)",
transition: theme.transitions.create(["transform", "opacity"], {
duration: theme.transitions.duration.enteringScreen,
easing: theme.transitions.easing.strong,
}),
};
const transitionStyles = {
entering: {
willChange: "transform, opacity",
},
entered: {
opacity: 1,
transform: "none",
},
exiting: {
opacity: 0,
transform: "scale(0.8)",
transitionDuration: theme.transitions.duration.leavingScreen,
},
exited: {
opacity: 0,
transform: "none",
transition: "none",
},
unmounted: {},
};
return (
<Transition
appear
timeout={{
enter: theme.transitions.duration.enteringScreen,
exit: theme.transitions.duration.leavingScreen,
}}
{...props}
>
{(state) =>
cloneElement(children as any, {
style: { ...defaultStyle, ...transitionStyles[state] },
tabIndex: -1,
ref,
})
}
</Transition>
);
});
export default ModalTransition;
export const ModalTransitionMui = forwardRef(function Transition(
props: MuiTransitionProps & { children?: React.ReactElement<any, any> },
ref: React.Ref<unknown>
) {
return <ModalTransition ref={ref} {...props} />;
});

View File

@@ -28,6 +28,7 @@ import "tinymce/plugins/image";
import "tinymce/plugins/paste";
import "tinymce/plugins/help";
import "tinymce/plugins/code";
import "tinymce/plugins/fullscreen";
const Styles = styled("div", {
shouldForwardProp: (prop) => prop !== "focus",
@@ -48,6 +49,11 @@ const Styles = styled("div", {
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
"&.tox-fullscreen": {
zIndex: theme.zIndex.modal,
backgroundColor: theme.palette.background.paper,
},
},
"& .tox-toolbar-overlord, & .tox-edit-area__iframe, & .tox-toolbar__primary":
@@ -242,10 +248,16 @@ export default function RichTextEditor({
].join("\n"),
minHeight: 300,
menubar: false,
plugins: ["autoresize", "lists link image", "paste help", "code"],
plugins: [
"fullscreen",
"autoresize",
"lists link image",
"paste help",
"code",
],
statusbar: false,
toolbar:
"formatselect | bold italic forecolor | link | bullist numlist outdent indent | removeformat code | help",
"formatselect | bold italic forecolor | link | fullscreen | bullist numlist outdent indent | removeformat code | help",
body_id: id,
}}
value={value}

View File

@@ -6,18 +6,20 @@ import {
Button,
DialogContentText,
Link as MuiLink,
Box,
} from "@mui/material";
import CheckIcon from "@mui/icons-material/CheckCircle";
import Modal from "@src/components/Modal";
import Logo from "@src/assets/LogoRowyRun";
import MemoizedText from "@src/components/Modal/MemoizedText";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import {
globalScope,
projectScope,
userRolesAtom,
projectSettingsAtom,
rowyRunModalAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { ROUTES } from "@src/constants/routes";
import { WIKI_LINKS } from "@src/constants/externalLinks";
@@ -27,11 +29,11 @@ import { WIKI_LINKS } from "@src/constants/externalLinks";
* @see {@link rowyRunModalAtom | Usage example}
*/
export default function RowyRunModal() {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
const [rowyRunModal, setRowyRunModal] = useAtom(
rowyRunModalAtom,
globalScope
projectScope
);
const handleClose = () => setRowyRunModal({ ...rowyRunModal, open: false });
@@ -43,45 +45,73 @@ export default function RowyRunModal() {
open={rowyRunModal.open}
onClose={handleClose}
title={
<Logo
size={2}
style={{
margin: "16px auto",
display: "block",
position: "relative",
right: 44 / -2,
}}
/>
<MemoizedText>
{rowyRunModal.feature
? `${
showUpdateModal ? "Update" : "Set up"
} Cloud Functions to use ${rowyRunModal.feature}`
: `Your Cloud isnt set up`}
</MemoizedText>
}
maxWidth="xs"
body={
<>
<Typography variant="h5" paragraph align="center">
{showUpdateModal ? "Update" : "Set up"} Rowy Run to use{" "}
{rowyRunModal.feature || "this feature"}
</Typography>
{showUpdateModal && (
<DialogContentText variant="body1" paragraph textAlign="center">
<DialogContentText variant="button" paragraph>
{rowyRunModal.feature || "This feature"} requires Rowy Run v
{rowyRunModal.version} or later.
</DialogContentText>
)}
<DialogContentText variant="body1" paragraph textAlign="center">
Rowy Run is a Cloud Run instance that provides backend
functionality, such as table action scripts, user management, and
easy Cloud Function deployment.{" "}
<MuiLink
href={WIKI_LINKS.rowyRun}
target="_blank"
rel="noopener noreferrer"
>
Learn more
<InlineOpenInNewIcon />
</MuiLink>
<DialogContentText paragraph>
Cloud Functions are free to use in our Base plan, you just need to
set a few things up first. Enable Cloud Functions for:
</DialogContentText>
<Box
component="ol"
sx={{
margin: 0,
padding: 0,
alignSelf: "stretch",
"& li": {
listStyleType: "none",
display: "flex",
gap: 1,
marginBottom: 2,
"& svg": {
display: "flex",
fontSize: "1.25rem",
color: "action.active",
},
},
}}
>
<li>
<CheckIcon />
Derivative fields, Extensions, Webhooks
</li>
<li>
<CheckIcon />
Table and Action scripts
</li>
<li>
<CheckIcon />
Easy Cloud Function deployment
</li>
</Box>
<MuiLink
href={WIKI_LINKS.rowyRun}
target="_blank"
rel="noopener noreferrer"
sx={{ display: "flex", mb: 3 }}
>
Learn more
<InlineOpenInNewIcon />
</MuiLink>
<Button
component={Link}
to={ROUTES.projectSettings + "#rowyRun"}
@@ -92,7 +122,7 @@ export default function RowyRunModal() {
style={{ display: "flex" }}
disabled={!userRoles.includes("ADMIN")}
>
Set up Rowy Run
Set up Cloud Functions
</Button>
{!userRoles.includes("ADMIN") && (
@@ -102,7 +132,7 @@ export default function RowyRunModal() {
color="error"
sx={{ mt: 1 }}
>
Contact the project owner to set up Rowy&nbsp;Run
Only admins can set up Cloud Functions
</Typography>
)}
</>

View File

@@ -5,7 +5,7 @@ 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/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
export interface ISectionHeadingProps extends Omit<StackProps, "children"> {
children: string;
@@ -35,7 +35,7 @@ export const SectionHeading = forwardRef(function SectionHeading_(
opacity: 1,
},
scrollMarginTop: (theme) => theme.spacing(APP_BAR_HEIGHT / 8 + 3.5),
scrollMarginTop: (theme) => theme.spacing(TOP_BAR_HEIGHT / 8 + 3.5),
scrollBehavior: "smooth",
}}
>

View File

@@ -10,12 +10,12 @@ import Logo from "@src/assets/Logo";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import meta from "@root/package.json";
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
import { projectScope, projectIdAtom } from "@src/atoms/projectScope";
import useUpdateCheck from "@src/hooks/useUpdateCheck";
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
export default function About() {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
const [latestUpdate, checkForUpdates, loading] = useUpdateCheck();

View File

@@ -12,7 +12,9 @@ export default function Authentication({
publicSettings,
updatePublicSettings,
}: IProjectSettingsChildProps) {
const [signInOptions, setSignInOptions] = useState(
const [signInOptions, setSignInOptions] = useState<
NonNullable<typeof publicSettings["signInOptions"]>
>(
Array.isArray(publicSettings?.signInOptions)
? publicSettings.signInOptions
: ["google"]
@@ -23,10 +25,12 @@ export default function Authentication({
<MultiSelect
label="Sign-in options"
value={signInOptions}
options={Object.keys(authOptions).map((option) => ({
value: option,
label: startCase(option).replace("Github", "GitHub"),
}))}
options={
Object.keys(authOptions).map((option) => ({
value: option,
label: startCase(option).replace("Github", "GitHub"),
})) as any
}
onChange={setSignInOptions}
onClose={() => updatePublicSettings({ signInOptions })}
multiple

View File

@@ -13,8 +13,8 @@ export default function Customization({
updatePublicSettings,
}: IProjectSettingsChildProps) {
const [customizedThemeColor, setCustomizedThemeColor] = useState(
publicSettings.theme?.light?.palette?.primary?.main ||
publicSettings.theme?.dark?.palette?.primary?.main
(publicSettings.theme?.light?.palette?.primary as any)?.main ||
(publicSettings.theme?.dark?.palette?.primary as any)?.main
);
const handleSave = ({ light, dark }: { light: string; dark: string }) => {
@@ -50,8 +50,12 @@ export default function Customization({
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
<Suspense fallback={<Loading />}>
<ThemeColorPicker
currentLight={publicSettings.theme?.light?.palette?.primary?.main}
currentDark={publicSettings.theme?.dark?.palette?.primary?.main}
currentLight={
(publicSettings.theme?.light?.palette?.primary as any)?.main
}
currentDark={
(publicSettings.theme?.dark?.palette?.primary as any)?.main
}
handleSave={handleSave}
/>
</Suspense>

View File

@@ -16,20 +16,20 @@ import MultiSelect from "@rowy/multiselect";
import Modal from "@src/components/Modal";
import {
globalScope,
projectScope,
projectRolesAtom,
projectSettingsAtom,
rowyRunAtom,
rowyRunModalAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { ROUTES } from "@src/constants/routes";
import { runRoutes } from "@src/constants/runRoutes";
export default function InviteUser() {
const [projectRoles] = useAtom(projectRolesAtom, globalScope);
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
const openRowyRunModal = useSetAtom(rowyRunModalAtom, globalScope);
const [projectRoles] = useAtom(projectRolesAtom, projectScope);
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
const { enqueueSnackbar } = useSnackbar();
const [open, setOpen] = useState(false);

View File

@@ -15,19 +15,20 @@ import { Copy as CopyIcon } from "@src/assets/icons";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import MultiSelect from "@rowy/multiselect";
import EmojiAvatar from "@src/components/EmojiAvatar";
import {
globalScope,
projectScope,
projectRolesAtom,
projectSettingsAtom,
rowyRunAtom,
rowyRunModalAtom,
UserSettings,
updateUserAtom,
confirmDialogAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { runRoutes } from "@src/constants/runRoutes";
import { USERS } from "@src/config/dbPaths";
import type { UserSettings } from "@src/types/settings";
export default function UserItem({
_rowy_ref,
@@ -35,13 +36,13 @@ export default function UserItem({
roles: rolesProp,
}: UserSettings) {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const confirm = useSetAtom(confirmDialogAtom, globalScope);
const openRowyRunModal = useSetAtom(rowyRunModalAtom, globalScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
const [projectRoles] = useAtom(projectRolesAtom, globalScope);
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
const [updateUser] = useAtom(updateUserAtom, globalScope);
const [projectRoles] = useAtom(projectRolesAtom, projectScope);
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
const [updateUser] = useAtom(updateUserAtom, projectScope);
const [value, setValue] = useState(Array.isArray(rolesProp) ? rolesProp : []);
const allRoles = new Set(["ADMIN", ...(projectRoles ?? []), ...value]);
@@ -59,7 +60,7 @@ export default function UserItem({
});
if (res.success) {
if (!updateUser) throw new Error("Could not update user document");
await updateUser(_rowy_ref!.id, { roles: value });
await updateUser(_rowy_ref!.path, { roles: value });
closeSnackbar(loadingSnackbarId);
enqueueSnackbar(`Set roles for ${user!.email}: ${value.join(", ")}`);
}
@@ -72,9 +73,10 @@ export default function UserItem({
const listItemChildren = (
<>
<ListItemAvatar>
<Avatar src={user?.photoURL}>
{user?.displayName ? user.displayName[0] : undefined}
</Avatar>
<EmojiAvatar
src={user?.photoURL}
fallback={user?.displayName || _rowy_ref?.id || "?"}
/>
</ListItemAvatar>
<ListItemText
primary={user?.displayName}

View File

@@ -9,19 +9,19 @@ export default function Account({ settings }: IUserSettingsChildProps) {
return (
<Grid container spacing={2} alignItems="center">
<Grid item>
<Avatar src={settings.user.photoURL} />
<Avatar src={settings.user?.photoURL} />
</Grid>
<Grid item xs>
<Typography variant="body1" style={{ userSelect: "all" }}>
{settings.user.displayName}
{settings.user?.displayName}
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{ userSelect: "all" }}
>
{settings.user.email}
{settings.user?.email}
</Typography>
</Grid>

View File

@@ -14,8 +14,8 @@ export default function Personalization({
}: IUserSettingsChildProps) {
const [customizedThemeColor, setCustomizedThemeColor] = useState(
Boolean(
settings.theme?.light?.palette?.primary?.main ||
settings.theme?.dark?.palette?.primary?.main
(settings.theme?.light?.palette?.primary as any)?.main ||
(settings.theme?.dark?.palette?.primary as any)?.main
)
);
@@ -52,8 +52,10 @@ export default function Personalization({
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
<Suspense fallback={<Loading style={{ height: "auto" }} />}>
<ThemeColorPicker
currentLight={settings.theme?.light?.palette?.primary?.main}
currentDark={settings.theme?.dark?.palette?.primary?.main}
currentLight={
(settings.theme?.light?.palette?.primary as any)?.main
}
currentDark={(settings.theme?.dark?.palette?.primary as any)?.main}
handleSave={handleSave}
/>
</Suspense>

View File

@@ -12,19 +12,19 @@ import {
} from "@mui/material";
import {
globalScope,
projectScope,
themeAtom,
themeOverriddenAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
export default function Theme({
settings,
updateSettings,
}: IUserSettingsChildProps) {
const [theme, setTheme] = useAtom(themeAtom, globalScope);
const [theme, setTheme] = useAtom(themeAtom, projectScope);
const [themeOverridden, setThemeOverridden] = useAtom(
themeOverriddenAtom,
globalScope
projectScope
);
return (
@@ -62,7 +62,9 @@ export default function Theme({
<FormControlLabel
control={
<Checkbox
defaultChecked={Boolean(settings.theme?.dark?.palette?.darker)}
defaultChecked={Boolean(
(settings.theme?.dark?.palette as any)?.darker
)}
onChange={(e) => {
updateSettings({
theme: merge(settings.theme, {

View File

@@ -5,7 +5,7 @@ import { signInWithPopup, GoogleAuthProvider, signOut } from "firebase/auth";
import { Typography } from "@mui/material";
import LoadingButton, { LoadingButtonProps } from "@mui/lab/LoadingButton";
import { globalScope } from "@src/atoms/globalScope";
import { projectScope } from "@src/atoms/projectScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
const googleProvider = new GoogleAuthProvider();
@@ -19,7 +19,7 @@ export default function SignInWithGoogle({
matchEmail,
...props
}: ISignInWithGoogleProps) {
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
const [firebaseAuth] = useAtom(firebaseAuthAtom, projectScope);
const [status, setStatus] = useState<"IDLE" | "LOADING" | string>("IDLE");
const handleSignIn = async () => {

View File

@@ -19,7 +19,7 @@ import ThumbDownIcon from "@mui/icons-material/ThumbDownAlt";
import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
import { analytics, logEvent } from "@src/analytics";
import { globalScope } from "@src/atoms/globalScope";
import { projectScope } from "@src/atoms/projectScope";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
import { ROUTES } from "@src/constants/routes";
import { SETTINGS } from "config/dbPaths";
@@ -35,7 +35,7 @@ export default {
} as ISetupStep;
function StepFinish() {
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {

View File

@@ -19,7 +19,7 @@ import DoneIcon from "@mui/icons-material/Done";
import SetupItem from "@src/components/Setup/SetupItem";
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
import { projectScope, projectIdAtom } from "@src/atoms/projectScope";
import { CONFIG } from "@src/config/dbPaths";
import {
RULES_START,
@@ -44,7 +44,7 @@ export default {
} as ISetupStep;
function StepRules({ isComplete, setComplete }: ISetupStepBodyProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
const { enqueueSnackbar } = useSnackbar();
const [adminRule, setAdminRule] = useState(true);

View File

@@ -12,7 +12,7 @@ import DoneIcon from "@mui/icons-material/Done";
import SetupItem from "@src/components/Setup/SetupItem";
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
import { projectScope, projectIdAtom } from "@src/atoms/projectScope";
import {
RULES_START,
RULES_END,
@@ -31,7 +31,7 @@ export default {
const rules = RULES_START + REQUIRED_RULES + RULES_END;
function StepStorageRules({ isComplete, setComplete }: ISetupStepBodyProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
const { enqueueSnackbar } = useSnackbar();
return (

View File

@@ -13,7 +13,7 @@ import {
} from "@mui/material";
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
import { projectScope, projectIdAtom } from "@src/atoms/projectScope";
export default {
id: "welcome",
@@ -33,7 +33,7 @@ export default {
} as ISetupStep;
function StepWelcome({ isComplete, setComplete }: ISetupStepBodyProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
return (
<>

View File

@@ -11,6 +11,7 @@ import {
} from "@mui/material";
import { DocumentPath as DocumentPathIcon } from "@src/assets/icons";
import LaunchIcon from "@mui/icons-material/Launch";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import LockIcon from "@mui/icons-material/LockOutlined";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined";
@@ -18,13 +19,15 @@ import { InlineErrorFallback } from "@src/components/ErrorFallback";
import FieldSkeleton from "./FieldSkeleton";
import {
globalScope,
projectScope,
projectIdAtom,
altPressAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { getLabelId, getFieldId } from "./utils";
import { useSnackbar } from "notistack";
import { copyToClipboard } from "@src/utils/ui";
export interface IFieldWrapperProps {
children?: React.ReactNode;
@@ -47,9 +50,9 @@ export default function FieldWrapper({
hidden,
index,
}: IFieldWrapperProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [altPress] = useAtom(altPressAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const { enqueueSnackbar } = useSnackbar();
return (
<div>
<Stack
@@ -126,7 +129,14 @@ export default function FieldWrapper({
>
{debugText}
</Typography>
<IconButton
onClick={() => {
copyToClipboard(debugText as string);
enqueueSnackbar("Copied!");
}}
>
<ContentCopyIcon />
</IconButton>
<IconButton
href={`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${(
debugText as string

View File

@@ -53,7 +53,7 @@ export const MemoizedField = memo(
// Should not reach this state
if (isEmpty(fieldComponent)) {
// console.error('Could not find SideDrawerField component', field);
console.error("Could not find SideDrawerField component", field);
return null;
}
@@ -78,10 +78,6 @@ export const MemoizedField = memo(
},
onSubmit: handleSubmit,
disabled,
// TODO: Remove
control: {} as any,
useFormMethods: {} as any,
docRef: _rowy_ref,
})}
</FieldWrapper>
);

View File

@@ -1,10 +1,8 @@
import { useEffect } from "react";
import useMemoValue from "use-memo-value";
import clsx from "clsx";
import { useAtom } from "jotai";
import { find, findIndex, isEqual } from "lodash-es";
import { find, findIndex } from "lodash-es";
import { ErrorBoundary } from "react-error-boundary";
import { DataGridHandle } from "react-data-grid";
import { TransitionGroup } from "react-transition-group";
import { Fab, Fade } from "@mui/material";
@@ -16,34 +14,20 @@ import ErrorFallback from "@src/components/ErrorFallback";
import StyledDrawer from "./StyledDrawer";
import SideDrawerFields from "./SideDrawerFields";
import { globalScope, userSettingsAtom } from "@src/atoms/globalScope";
import {
tableScope,
tableIdAtom,
tableColumnsOrderedAtom,
tableRowsAtom,
sideDrawerOpenAtom,
selectedCellAtom,
} from "@src/atoms/tableScope";
import { analytics, logEvent } from "@src/analytics";
import { formatSubTableName } from "@src/utils/table";
export const DRAWER_WIDTH = 512;
export const DRAWER_COLLAPSED_WIDTH = 36;
export default function SideDrawer({
dataGridRef,
}: {
dataGridRef?: React.MutableRefObject<DataGridHandle | null>;
}) {
const [userSettings] = useAtom(userSettingsAtom, globalScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
export default function SideDrawer() {
const [tableRows] = useAtom(tableRowsAtom, tableScope);
const userDocHiddenFields =
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [];
const [cell, setCell] = useAtom(selectedCellAtom, tableScope);
const [open, setOpen] = useAtom(sideDrawerOpenAtom, tableScope);
const selectedRow = find(tableRows, ["_rowy_ref.path", cell?.path]);
@@ -51,26 +35,6 @@ export default function SideDrawer({
"_rowy_ref.path",
cell?.path,
]);
// Memo a list of visible column keys for useEffect dependencies
const visibleColumnKeys = useMemoValue(
tableColumnsOrdered
.filter((col) => !userDocHiddenFields.includes(col.key))
.map((col) => col.key),
isEqual
);
// When side drawer is opened, select the cell in the table
// in case weve scrolled and selected cell was reset
useEffect(() => {
if (open) {
const columnIndex = visibleColumnKeys.indexOf(cell?.columnKey || "");
if (columnIndex === -1 || selectedCellRowIndex === -1) return;
dataGridRef?.current?.selectCell(
{ rowIdx: selectedCellRowIndex, idx: columnIndex },
false
);
}
}, [open, visibleColumnKeys, selectedCellRowIndex, cell, dataGridRef]);
const handleNavigate = (direction: "up" | "down") => () => {
if (!tableRows || !cell) return;
@@ -79,13 +43,11 @@ export default function SideDrawer({
if (direction === "down" && rowIndex < tableRows.length - 1) rowIndex += 1;
const newPath = tableRows[rowIndex]._rowy_ref.path;
setCell((cell) => ({ columnKey: cell!.columnKey, path: newPath }));
const columnIndex = visibleColumnKeys.indexOf(cell!.columnKey || "");
dataGridRef?.current?.selectCell(
{ rowIdx: rowIndex, idx: columnIndex },
false
);
setCell((cell) => ({
columnKey: cell!.columnKey,
path: newPath,
focusInside: false,
}));
};
// const [urlDocState, dispatchUrlDoc] = useDoc({});
@@ -109,7 +71,7 @@ export default function SideDrawer({
// }
// }, [cell]);
const disabled = !open && !cell; // && !urlDocState.doc;
const disabled = (!open && !cell) || selectedCellRowIndex <= -1; // && !urlDocState.doc;
useEffect(() => {
if (disabled && setOpen) setOpen(false);
}, [disabled, setOpen]);
@@ -122,7 +84,9 @@ export default function SideDrawer({
)}
variant="permanent"
anchor="right"
PaperProps={{ elevation: 4, component: "aside" } as any}
PaperProps={
{ elevation: 4, component: "aside", "aria-label": "Side drawer" } as any
}
>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<div className="sidedrawer-contents">
@@ -141,6 +105,7 @@ export default function SideDrawer({
{!!cell && (
<div className="sidedrawer-nav-fab-container">
<Fab
aria-label="Previous row"
style={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }}
size="small"
disabled={disabled || !cell || selectedCellRowIndex <= 0}
@@ -150,6 +115,7 @@ export default function SideDrawer({
</Fab>
<Fab
aria-label="Next row"
style={{ borderTopLeftRadius: 0, borderTopRightRadius: 0 }}
size="small"
disabled={
@@ -164,6 +130,7 @@ export default function SideDrawer({
<div className="sidedrawer-open-fab-container">
<Fab
aria-label={open ? "Close side drawer" : "Open side drawer"}
disabled={disabled}
onClick={() => {
if (setOpen)

View File

@@ -9,10 +9,10 @@ import MemoizedField from "./MemoizedField";
import SaveState from "./SaveState";
import {
globalScope,
projectScope,
userRolesAtom,
userSettingsAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import {
tableScope,
tableIdAtom,
@@ -30,8 +30,8 @@ export interface ISideDrawerFieldsProps {
}
export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [userSettings] = useAtom(userSettingsAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [userSettings] = useAtom(userSettingsAtom, projectScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);

View File

@@ -1,6 +1,6 @@
import { styled, Drawer, drawerClasses } from "@mui/material";
import { DRAWER_WIDTH, DRAWER_COLLAPSED_WIDTH } from "./index";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
export const StyledDrawer = styled(Drawer)(({ theme }) => ({
@@ -32,15 +32,15 @@ export const StyledDrawer = styled(Drawer)(({ theme }) => ({
boxSizing: "content-box",
top: APP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT,
height: `calc(100% - ${APP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT}px)`,
top: TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT,
height: `calc(100% - ${TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT}px)`,
".MuiDialog-paperFullScreen &": {
top:
APP_BAR_HEIGHT +
TOP_BAR_HEIGHT +
TABLE_TOOLBAR_HEIGHT +
Number(theme.spacing(2).replace("px", "")),
height: `calc(100% - ${
APP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT
TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT
}px - ${theme.spacing(2)})`,
},

View File

@@ -0,0 +1,52 @@
import { Box, BoxProps, SvgIcon } from "@mui/material";
import { Discord as DiscordIcon } from "@src/assets/icons";
import GitHubIcon from "@mui/icons-material/GitHub";
import TwitterIcon from "@mui/icons-material/Twitter";
import { spreadSx } from "@src/utils/ui";
const SOCIAL_PLATFORMS = Object.freeze({
discord: { bg: "#5865F2", fg: "#fff", icon: <DiscordIcon /> },
gitHub: { bg: "#24292E", fg: "#fff", icon: <GitHubIcon /> },
twitter: { bg: "#1d9bf0", fg: "#fff", icon: <TwitterIcon /> },
productHunt: {
bg: "#fff",
fg: "#DA552F",
icon: (
<SvgIcon viewBox="0 0 240 240">
<path d="M240 120c0 66.274-53.726 120-120 120S0 186.274 0 120 53.726 0 120 0s120 53.726 120 120m-104 0h-34V84h34c9.941 0 18 8.059 18 18 0 9.94-8.059 18-18 18m0-60H78v120h24v-36h34c23.196 0 42-18.804 42-42s-18.804-42-42-42" />
</SvgIcon>
),
},
});
export interface ISocialLogoProps extends Partial<BoxProps> {
platform: keyof typeof SOCIAL_PLATFORMS;
}
export default function SocialLogo({ platform, ...props }: ISocialLogoProps) {
return (
<Box
{...props}
sx={[
{
borderRadius: 1.5,
p: 0.5,
bgcolor: SOCIAL_PLATFORMS[platform].bg,
color: SOCIAL_PLATFORMS[platform].fg,
boxShadow: (theme) => `0 0 0 1px ${
theme.palette.action.inputOutline
} inset,
0 ${theme.palette.mode === "dark" ? "" : "-"}1px 0 0 ${
theme.palette.action.inputOutline
} inset`,
"& svg": { display: "block" },
},
...spreadSx(props.sx),
]}
>
{SOCIAL_PLATFORMS[platform].icon}
</Box>
);
}

View File

@@ -94,18 +94,20 @@ export default function SteppedAccordion({
>
<StepLabel error={error} {...labelProps}>
{title}
<ExpandIcon sx={{ mr: -0.5 }} />
{content && <ExpandIcon sx={{ mr: -0.5 }} />}
</StepLabel>
</StepButton>
<StepContent
TransitionProps={
disableUnmount ? { unmountOnExit: false } : undefined
}
{...contentProps}
>
{content}
</StepContent>
{content && (
<StepContent
TransitionProps={
disableUnmount ? { unmountOnExit: false } : undefined
}
{...contentProps}
>
{content}
</StepContent>
)}
</Step>
)
)}

View File

@@ -0,0 +1,44 @@
import { Box, BoxProps, Typography } from "@mui/material";
import { spreadSx } from "@src/utils/ui";
export interface IStepsProgressProps extends Partial<BoxProps> {
steps: number;
value: number;
}
export default function StepsProgress({
steps,
value,
sx,
...props
}: IStepsProgressProps) {
return (
<Box
{...props}
sx={[
{ display: "flex", alignItems: "center", gap: 0.5 },
...spreadSx(sx),
]}
>
<Typography
className="steps-progress__label"
sx={{ flex: 3, fontVariantNumeric: "tabular-nums" }}
>
{Math.min(Math.max(value, 0), steps)}/{steps}
</Typography>
{new Array(steps).fill(undefined).map((_, i) => (
<Box
key={i + 1}
sx={{
flex: 1,
borderRadius: 1,
height: 8,
bgcolor: i + 1 <= value ? "success.light" : "divider",
transition: (theme) => theme.transitions.create("background-color"),
}}
/>
))}
</Box>
);
}

View File

@@ -6,7 +6,7 @@ import { Stack, Breadcrumbs, Link, Typography, Tooltip } from "@mui/material";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined";
import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
import { projectScope, userRolesAtom } from "@src/atoms/projectScope";
import { ROUTES } from "@src/constants/routes";
import { TableSettings } from "@src/types/table";
@@ -23,7 +23,7 @@ export default function BreadcrumbsSubTable({
rootTableLink,
parentLabel,
}: IBreadcrumbsSubTableProps) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [searchParams] = useSearchParams();
const splitSubTableId = subTableSettings.id.split("/");

View File

@@ -1,5 +1,5 @@
import { useAtom } from "jotai";
import { useLocation, useParams, Link as RouterLink } from "react-router-dom";
import { useParams, Link as RouterLink } from "react-router-dom";
import { find, camelCase, uniq } from "lodash-es";
import {
@@ -16,11 +16,11 @@ import InfoTooltip from "@src/components/InfoTooltip";
import RenderedMarkdown from "@src/components/RenderedMarkdown";
import {
globalScope,
projectScope,
userRolesAtom,
tableDescriptionDismissedAtom,
tablesAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import { ROUTES } from "@src/constants/routes";
/**
@@ -30,12 +30,12 @@ import { ROUTES } from "@src/constants/routes";
export default function BreadcrumbsTableRoot(props: StackProps) {
const { id } = useParams();
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [dismissed, setDismissed] = useAtom(
tableDescriptionDismissedAtom,
globalScope
projectScope
);
const [tables] = useAtom(tablesAtom, globalScope);
const [tables] = useAtom(tablesAtom, projectScope);
const tableSettings = find(tables, ["id", id]);
if (!tableSettings) return null;

View File

@@ -1,99 +0,0 @@
import { createElement } from "react";
import { styled } from "@mui/material";
import EmptyState from "@src/components/EmptyState";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
const Root = styled("div")(({ theme }) => ({
width: "100%",
height: 43,
position: "relative",
overflow: "hidden",
whiteSpace: "nowrap",
pointerEvents: "none",
border: `1px solid ${theme.palette.divider}`,
borderTopWidth: 0,
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
padding: theme.spacing(0, 1.25),
...theme.typography.body2,
fontSize: "0.75rem",
lineHeight: "inherit",
color: theme.palette.text.secondary,
"& .cell-collapse-padding": {
margin: theme.spacing(0, -1.5),
width: `calc(100% + ${theme.spacing(3)})`,
},
}));
const Value = styled("div")(({ theme }) => ({
width: "100%",
height: 43,
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
}));
export interface ICellProps
extends Partial<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>
> {
field: string;
type: FieldType;
value: any;
name?: string;
}
export default function Cell({
field,
type,
value,
name,
...props
}: ICellProps) {
const formatter = type ? getFieldProp("TableCell", type) : null;
return (
<Root {...props}>
<Value>
{formatter ? (
createElement(formatter, {
value,
rowIdx: 0,
column: {
type,
key: field,
name,
config: { options: [] },
editable: false,
} as any,
row: { [field]: value },
isRowSelected: false,
onRowSelectionChange: () => {},
isSummaryRow: false,
} as any)
) : typeof value === "string" ||
typeof value === "number" ||
value === undefined ||
value === null ? (
value
) : typeof value === "boolean" ? (
value.toString()
) : (
<EmptyState basic wrap="nowrap" message="Invalid column type" />
)}
</Value>
</Root>
);
}

View File

@@ -1,98 +0,0 @@
import { styled } from "@mui/material/styles";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
import WarningIcon from "@mui/icons-material/WarningAmber";
import RichTooltip from "@src/components/RichTooltip";
const Root = styled("div", { shouldForwardProp: (prop) => prop !== "error" })(
({ theme, ...props }) => ({
width: "100%",
height: "100%",
padding: "var(--cell-padding)",
position: "relative",
overflow: "hidden",
contain: "strict",
display: "flex",
alignItems: "center",
...((props as any).error
? {
".rdg-cell:not([aria-selected=true]) &": {
boxShadow: `inset 0 0 0 2px ${theme.palette.error.main}`,
},
}
: {}),
})
);
const Dot = styled("div")(({ theme }) => ({
position: "absolute",
right: -5,
top: "50%",
transform: "translateY(-50%)",
zIndex: 1,
width: 12,
height: 12,
borderRadius: "50%",
backgroundColor: theme.palette.error.main,
boxShadow: `0 0 0 4px var(--background-color)`,
".rdg-row:hover &": {
boxShadow: `0 0 0 4px var(--row-hover-background-color)`,
},
}));
export interface ICellValidationProps
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {
value: any;
required?: boolean;
validationRegex?: string;
}
export default function CellValidation({
value,
required,
validationRegex,
children,
}: ICellValidationProps) {
const isInvalid = validationRegex && !new RegExp(validationRegex).test(value);
const isMissing = required && value === undefined;
if (isInvalid)
return (
<>
<RichTooltip
icon={<ErrorIcon fontSize="inherit" color="error" />}
title="Invalid data"
message="This row will not be saved until all the required fields contain valid data"
placement="right"
render={({ openTooltip }) => <Dot onClick={openTooltip} />}
/>
<Root {...({ error: true } as any)}>{children}</Root>
</>
);
if (isMissing)
return (
<>
<RichTooltip
icon={<WarningIcon fontSize="inherit" color="warning" />}
title="Required field"
message="This row will not be saved until all the required fields contain valid data"
placement="right"
render={({ openTooltip }) => <Dot onClick={openTooltip} />}
/>
<Root {...({ error: true } as any)}>{children}</Root>
</>
);
return <Root>{children}</Root>;
}

View File

@@ -1,82 +1,86 @@
import { useRef } from "react";
import { memo, useRef } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useDrag, useDrop } from "react-dnd";
import type { Header } from "@tanstack/react-table";
import type {
DraggableProvided,
DraggableStateSnapshot,
} from "react-beautiful-dnd";
import {
styled,
alpha,
Tooltip,
TooltipProps,
tooltipClasses,
Fade,
Grid,
StackProps,
IconButton,
Typography,
} from "@mui/material";
import DropdownIcon from "@mui/icons-material/MoreHoriz";
import LockIcon from "@mui/icons-material/LockOutlined";
import ColumnHeaderSort from "./ColumnHeaderSort";
import {
globalScope,
userRolesAtom,
altPressAtom,
} from "@src/atoms/globalScope";
StyledColumnHeader,
StyledColumnHeaderNameTooltip,
} from "@src/components/Table/Styled/StyledColumnHeader";
import ColumnHeaderSort, { SORT_STATES } from "./ColumnHeaderSort";
import ColumnHeaderDragHandle from "./ColumnHeaderDragHandle";
import ColumnHeaderResizer from "./ColumnHeaderResizer";
import { projectScope, altPressAtom } from "@src/atoms/projectScope";
import {
tableScope,
updateColumnAtom,
selectedCellAtom,
columnMenuAtom,
tableSortsAtom,
} from "@src/atoms/tableScope";
import { getFieldProp } from "@src/components/fields";
import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column";
import { ColumnConfig } from "@src/types/table";
import { FieldType } from "@src/constants/fields";
import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Mock/Column";
import type { ColumnConfig } from "@src/types/table";
import type { TableRow } from "@src/types/table";
export { COLUMN_HEADER_HEIGHT };
const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
margin: `-${COLUMN_HEADER_HEIGHT - 1 - 2}px 0 0 !important`,
padding: 0,
paddingRight: theme.spacing(1.5),
},
}));
export interface IDraggableHeaderRendererProps {
export interface IColumnHeaderProps
extends Partial<Omit<StackProps, "style" | "sx">> {
header: Header<TableRow, any>;
column: ColumnConfig;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
width: number;
isSelectedCell: boolean;
focusInsideCell: boolean;
canEditColumns: boolean;
isLastFrozen: boolean;
}
export default function DraggableHeaderRenderer({
/**
* Renders UI components for each column header, including accessibility
* attributes. Memoized to prevent re-render when resizing or reordering other
* columns.
*
* Renders:
* - Drag handle (accessible)
* - Field type icon + click to copy field key
* - Field name + hover to view full name if cut off
* - Sort button
* - Resize handle (not accessible)
*/
export const ColumnHeader = memo(function ColumnHeader({
header,
column,
}: IDraggableHeaderRendererProps) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
provided,
snapshot,
width,
isSelectedCell,
focusInsideCell,
canEditColumns,
isLastFrozen,
}: IColumnHeaderProps) {
const openColumnMenu = useSetAtom(columnMenuAtom, tableScope);
const [altPress] = useAtom(altPressAtom, globalScope);
const [{ isDragging }, dragRef] = useDrag({
type: "COLUMN_DRAG",
item: { key: column.key },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [{ isOver }, dropRef] = useDrop({
accept: "COLUMN_DRAG",
drop: ({ key }: { key: string }) => {
updateColumn({ key, config: {}, index: column.index });
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const setSelectedCell = useSetAtom(selectedCellAtom, tableScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const [tableSorts] = useAtom(tableSortsAtom, tableScope);
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -85,53 +89,69 @@ export default function DraggableHeaderRenderer({
openColumnMenu({ column, anchorEl: buttonRef.current });
};
const _sortKey = getFieldProp("sortKey", (column as any).type);
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
const currentSort: typeof SORT_STATES[number] =
tableSorts[0]?.key !== sortKey
? "none"
: tableSorts[0]?.direction || "none";
return (
<Grid
key={column.key}
<StyledColumnHeader
role="columnheader"
id={`column-header-${column.key}`}
ref={(ref) => {
dragRef(ref);
dropRef(ref);
ref={provided.innerRef}
{...provided.draggableProps}
data-row-id={"_rowy_header"}
data-col-id={header.id}
data-frozen={header.column.getIsPinned() || undefined}
data-frozen-last={isLastFrozen || undefined}
tabIndex={isSelectedCell ? 0 : -1}
aria-colindex={header.index + 1}
aria-readonly={!canEditColumns}
aria-selected={isSelectedCell}
aria-sort={
currentSort === "none"
? "none"
: currentSort === "asc"
? "ascending"
: "descending"
}
style={{
left: header.column.getIsPinned()
? header.column.getStart()
: undefined,
zIndex: header.column.getIsPinned() ? 11 : 10,
...provided.draggableProps.style,
width,
borderLeftStyle: snapshot.isDragging ? "solid" : undefined,
}}
container
alignItems="center"
wrap="nowrap"
onContextMenu={handleOpenMenu}
sx={[
{
height: "100%",
"& svg, & button": { display: "block" },
color: "text.secondary",
transition: (theme) =>
theme.transitions.create("color", {
duration: theme.transitions.duration.short,
}),
"&:hover": { color: "text.primary" },
cursor: "move",
py: 0,
pr: 0.5,
pl: 1,
width: "100%",
},
isDragging
? { opacity: 0.5 }
: isOver
? {
backgroundColor: (theme) =>
alpha(
theme.palette.primary.main,
theme.palette.action.focusOpacity
),
color: "primary.main",
}
: {},
]}
className="column-header"
onClick={(e) => {
setSelectedCell({
path: "_rowy_header",
columnKey: header.id,
focusInside: false,
});
(e.target as HTMLDivElement).focus();
}}
onDoubleClick={(e) => {
setSelectedCell({
path: "_rowy_header",
columnKey: header.id,
focusInside: true,
});
(e.target as HTMLDivElement).focus();
}}
>
{(column.width as number) > 140 && (
{provided.dragHandleProps && (
<ColumnHeaderDragHandle
dragHandleProps={provided.dragHandleProps}
tabIndex={focusInsideCell ? 0 : -1}
/>
)}
{width > 140 && (
<Tooltip
title={
<>
@@ -144,94 +164,99 @@ export default function DraggableHeaderRenderer({
placement="bottom-start"
arrow
>
<Grid
item
<div
onClick={() => {
navigator.clipboard.writeText(column.key);
}}
style={{ position: "relative", zIndex: 2 }}
>
{column.editable === false ? (
<LockIcon />
) : (
getFieldProp("icon", (column as any).type)
)}
</Grid>
</div>
</Tooltip>
)}
<Grid
item
xs
sx={{ flexShrink: 1, overflow: "hidden", my: 0, ml: 0.5, mr: -30 / 8 }}
>
<LightTooltip
title={
<Typography
sx={{
typography: "caption",
fontWeight: "fontWeightMedium",
lineHeight: `${COLUMN_HEADER_HEIGHT - 2 - 4}px`,
textOverflow: "clip",
}}
color="inherit"
>
{column.name as string}
</Typography>
}
enterDelay={1000}
placement="bottom-start"
disableInteractive
TransitionComponent={Fade}
>
<StyledColumnHeaderNameTooltip
title={
<Typography
noWrap
sx={{
typography: "caption",
fontWeight: "fontWeightMedium",
lineHeight: `${COLUMN_HEADER_HEIGHT}px`,
lineHeight: `${COLUMN_HEADER_HEIGHT - 2 - 4}px`,
textOverflow: "clip",
}}
component="div"
color="inherit"
>
{altPress ? (
<>
{column.index} <code>{column.fieldName}</code>
</>
) : (
column.name
)}
{column.name as string}
</Typography>
</LightTooltip>
</Grid>
}
enterDelay={1000}
placement="bottom-start"
disableInteractive
TransitionComponent={Fade}
sx={{ "& .MuiTooltip-tooltip": { marginTop: "-28px !important" } }}
>
<Typography
noWrap
sx={{
typography: "caption",
fontWeight: "fontWeightMedium",
textOverflow: "clip",
position: "relative",
zIndex: 1,
<Grid item>
<ColumnHeaderSort column={column as any} />
</Grid>
flexGrow: 1,
flexShrink: 1,
overflow: "hidden",
my: 0,
ml: 0.5,
mr: -30 / 8,
}}
component="div"
color="inherit"
>
{altPress ? (
<>
{column.index} <code>{column.fieldName}</code>
</>
) : (
column.name
)}
</Typography>
</StyledColumnHeaderNameTooltip>
<Grid item>
<Tooltip title="Column settings">
<IconButton
size="small"
aria-label={`Column settings for ${column.name as string}`}
id={`column-settings-${column.key}`}
color="inherit"
onClick={handleOpenMenu}
ref={buttonRef}
sx={{
transition: (theme) =>
theme.transitions.create("color", {
duration: theme.transitions.duration.short,
}),
{column.type !== FieldType.id && (
<ColumnHeaderSort
sortKey={sortKey}
currentSort={currentSort}
tabIndex={focusInsideCell ? 0 : -1}
/>
)}
color: "text.disabled",
".column-header:hover &": { color: "text.primary" },
}}
>
<DropdownIcon />
</IconButton>
</Tooltip>
</Grid>
</Grid>
<Tooltip title="Column settings">
<IconButton
size="small"
tabIndex={focusInsideCell ? 0 : -1}
id={`column-settings-${column.key}`}
onClick={handleOpenMenu}
ref={buttonRef}
>
<DropdownIcon />
</IconButton>
</Tooltip>
{header.column.getCanResize() && (
<ColumnHeaderResizer
isResizing={header.column.getIsResizing()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
/>
)}
</StyledColumnHeader>
);
}
});
export default ColumnHeader;

View File

@@ -0,0 +1,58 @@
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { DragVertical } from "@src/assets/icons";
export interface IColumnHeaderDragHandleProps {
dragHandleProps: DraggableProvidedDragHandleProps;
tabIndex: number;
}
export default function ColumnHeaderDragHandle({
dragHandleProps,
tabIndex,
}: IColumnHeaderDragHandleProps) {
return (
<div
{...dragHandleProps}
tabIndex={tabIndex}
aria-describedby={
tabIndex > -1 ? dragHandleProps["aria-describedby"] : undefined
}
style={{
position: "absolute",
inset: 0,
zIndex: 0,
display: "flex",
alignItems: "center",
outline: "none",
}}
className="column-drag-handle"
>
<DragVertical
sx={{
opacity: 0,
borderRadius: 2,
transition: (theme) => theme.transitions.create(["opacity"]),
"[role='columnheader']:hover &, [role='columnheader']:focus-within &":
{
opacity: 0.5,
},
".column-drag-handle:hover &": {
opacity: 1,
},
".column-drag-handle:active &": {
opacity: 1,
color: "primary.main",
},
".column-drag-handle:focus &": {
opacity: 1,
color: "primary.main",
outline: "2px solid",
outlineColor: "primary.main",
},
}}
style={{ width: 8 }}
preserveAspectRatio="xMidYMid slice"
/>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import { styled } from "@mui/material";
export interface IColumnHeaderResizerProps {
isResizing: boolean;
}
export const ColumnHeaderResizer = styled("div", {
name: "ColumnHeaderResizer",
shouldForwardProp: (prop) => prop !== "isResizing",
})<IColumnHeaderResizerProps>(({ theme, isResizing }) => ({
position: "absolute",
zIndex: 5,
right: 0,
top: 0,
height: "100%",
width: 10,
cursor: "col-resize",
userSelect: "none",
touchAction: "none",
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
transition: theme.transitions.create("opacity", {
duration: theme.transitions.duration.shortest,
}),
opacity: isResizing ? 1 : 0,
"[role='columnheader']:hover &": { opacity: 0.33 },
"[role='columnheader'] &:hover, [role='columnheader'] &:active": {
opacity: 1,
"&::before": { transform: "scaleY(1.25)" },
},
"&::before": {
content: "''",
display: "block",
height: "50%",
width: 4,
borderRadius: 2,
marginRight: 2,
background: isResizing
? theme.palette.primary.main
: theme.palette.action.active,
transition: theme.transitions.create("transform", {
duration: theme.transitions.duration.shortest,
}),
transform: isResizing ? "scaleY(1.5) !important" : undefined,
},
}));
ColumnHeaderResizer.displayName = "ColumnHeaderResizer";
export default ColumnHeaderResizer;

View File

@@ -1,4 +1,5 @@
import { useAtom } from "jotai";
import { memo } from "react";
import { useSetAtom } from "jotai";
import { colord } from "colord";
import { Tooltip, IconButton } from "@mui/material";
@@ -8,27 +9,26 @@ import IconSlash, {
} from "@src/components/IconSlash";
import { tableScope, tableSortsAtom } from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { ColumnConfig } from "@src/types/table";
const SORT_STATES = ["none", "desc", "asc"] as const;
export const SORT_STATES = ["none", "desc", "asc"] as const;
export interface IColumnHeaderSortProps {
column: ColumnConfig;
sortKey: string;
currentSort: typeof SORT_STATES[number];
tabIndex?: number;
}
export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope);
/**
* Renders button with current sort state.
* On click, updates `tableSortsAtom` in `tableScope`.
*/
export const ColumnHeaderSort = memo(function ColumnHeaderSort({
sortKey,
currentSort,
tabIndex,
}: IColumnHeaderSortProps) {
const setTableSorts = useSetAtom(tableSortsAtom, tableScope);
const _sortKey = getFieldProp("sortKey", (column as any).type);
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
const currentSort: typeof SORT_STATES[number] =
tableSorts[0]?.key !== sortKey
? "none"
: tableSorts[0]?.direction || "none";
const nextSort =
SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0];
@@ -37,20 +37,19 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
else setTableSorts([{ key: sortKey, direction: nextSort }]);
};
if (column.type === FieldType.id) return null;
return (
<Tooltip
title={nextSort === "none" ? "Unsort" : `Sort by ${nextSort}ending`}
title={nextSort === "none" ? "Remove sort" : `Sort by ${nextSort}ending`}
>
<IconButton
disableFocusRipple={true}
size="small"
onClick={handleSortClick}
color="inherit"
tabIndex={tabIndex}
sx={{
bgcolor: "background.default",
"&:hover": {
"&:hover, &:focus": {
backgroundColor: (theme) =>
colord(theme.palette.background.default)
.mix(
@@ -74,12 +73,6 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
position: "relative",
opacity: currentSort !== "none" ? 1 : 0,
".column-header:hover &": { opacity: 1 },
transition: (theme) =>
theme.transitions.create(["background-color", "opacity"], {
duration: theme.transitions.duration.short,
}),
"& .arrow": {
transition: (theme) =>
@@ -89,7 +82,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
transform: currentSort === "asc" ? "rotate(180deg)" : "none",
},
"&:hover .arrow": {
"&:hover .arrow, &:focus .arrow": {
transform:
currentSort === "asc" || nextSort === "asc"
? "rotate(180deg)"
@@ -100,7 +93,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
strokeDashoffset:
currentSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET,
},
"&:hover .icon-slash": {
"&:hover .icon-slash, &:focus .icon-slash": {
strokeDashoffset:
nextSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET,
},
@@ -113,4 +106,6 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
</IconButton>
</Tooltip>
);
}
});
export default ColumnHeaderSort;

View File

@@ -1,9 +1,10 @@
import { useAtom } from "jotai";
import MultiSelect, { MultiSelectProps } from "@rowy/multiselect";
import { Stack, StackProps, Typography } from "@mui/material";
import { Stack, StackProps, Typography, Chip } from "@mui/material";
import { TableColumn as TableColumnIcon } from "@src/assets/icons";
import { globalScope, altPressAtom } from "@src/atoms/globalScope";
import { projectScope, altPressAtom } from "@src/atoms/projectScope";
import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope";
import { ColumnConfig } from "@src/types/table";
import { FieldType } from "@src/constants/fields";
@@ -53,7 +54,6 @@ export default function ColumnSelect({
TextFieldProps={{
...props.TextFieldProps,
SelectProps: {
...props.TextFieldProps?.SelectProps,
renderValue: () => {
if (Array.isArray(props.value) && props.value.length > 1)
return `${props.value.length} columns`;
@@ -72,6 +72,7 @@ export default function ColumnSelect({
value
);
},
...props.TextFieldProps?.SelectProps,
},
}}
/>
@@ -90,7 +91,9 @@ export function ColumnItem({
children,
...props
}: IColumnItemProps) {
const [altPress] = useAtom(altPressAtom, globalScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const isNew = option.index === undefined && !option.type;
return (
<Stack
@@ -100,10 +103,18 @@ export function ColumnItem({
{...props}
sx={[{ color: "text.secondary", width: "100%" }, ...spreadSx(props.sx)]}
>
{getFieldProp("icon", option.type)}
{getFieldProp("icon", option.type) ?? (
<TableColumnIcon color="disabled" />
)}
<Typography color="text.primary" style={{ flexGrow: 1 }}>
{altPress ? <code>{option.value}</code> : option.label}
</Typography>
{isNew && (
<Chip label="New" color="primary" size="small" variant="outlined" />
)}
{altPress ? (
<Typography
color="text.disabled"

View File

@@ -1,3 +1,4 @@
import { useRef, useEffect } from "react";
import { useAtom } from "jotai";
import { ErrorBoundary } from "react-error-boundary";
import { NonFullScreenErrorFallback } from "@src/components/ErrorFallback";
@@ -8,10 +9,21 @@ import MenuContents from "./MenuContents";
import { tableScope, contextMenuTargetAtom } from "@src/atoms/tableScope";
export default function ContextMenu() {
const menuRef = useRef<HTMLUListElement>(null);
const [contextMenuTarget, setContextMenuTarget] = useAtom(
contextMenuTargetAtom,
tableScope
);
const open = Boolean(contextMenuTarget);
useEffect(() => {
setTimeout(() => {
if (open && menuRef.current) {
const firstMenuitem = menuRef.current.querySelector("[role=menuitem]");
(firstMenuitem as HTMLElement)?.focus();
}
});
}, [open]);
const handleClose = () => setContextMenuTarget(null);
@@ -20,11 +32,12 @@ export default function ContextMenu() {
id="cell-context-menu"
aria-label="Cell context menu"
anchorEl={contextMenuTarget as any}
open={Boolean(contextMenuTarget)}
open={open}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
sx={{ "& .MuiMenu-paper": { minWidth: 160 } }}
MenuListProps={{ ref: menuRef }}
>
<ErrorBoundary FallbackComponent={NonFullScreenErrorFallback}>
<MenuContents onClose={handleClose} />

View File

@@ -19,6 +19,7 @@ export interface IContextMenuItem extends Partial<MenuItemProps> {
disabled?: boolean;
hotkeyLabel?: string;
divider?: boolean;
subItems?: IContextMenuItem[];
}
export interface IContextMenuItemProps extends IContextMenuItem {
@@ -82,16 +83,20 @@ export default function ContextMenuItem({
</>
);
} else {
return (
<MenuItem {...props} onClick={onClick}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText>{label}</ListItemText>
{hotkeyLabel && (
<Typography variant="body2" color="text.secondary">
{hotkeyLabel}
</Typography>
)}
</MenuItem>
);
if (props.divider) {
return <Divider variant="middle" />;
} else {
return (
<MenuItem {...props} onClick={onClick}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText>{label}</ListItemText>
{hotkeyLabel && (
<Typography variant="body2" color="text.secondary">
{hotkeyLabel}
</Typography>
)}
</MenuItem>
);
}
}
}

View File

@@ -17,13 +17,13 @@ import FilterIcon from "@mui/icons-material/FilterList";
import ContextMenuItem, { IContextMenuItem } from "./ContextMenuItem";
import {
globalScope,
projectScope,
projectIdAtom,
userRolesAtom,
altPressAtom,
tableAddRowIdTypeAtom,
confirmDialogAtom,
} from "@src/atoms/globalScope";
} from "@src/atoms/projectScope";
import {
tableScope,
tableSettingsAtom,
@@ -42,11 +42,11 @@ interface IMenuContentsProps {
}
export default function MenuContents({ onClose }: IMenuContentsProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [altPress] = useAtom(altPressAtom, globalScope);
const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, globalScope);
const confirm = useSetAtom(confirmDialogAtom, globalScope);
const [projectId] = useAtom(projectIdAtom, projectScope);
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const [tableRows] = useAtom(tableRowsAtom, tableScope);
@@ -62,162 +62,188 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
if (!tableSchema.columns || !selectedCell) return null;
const selectedColumn = tableSchema.columns[selectedCell.columnKey];
const menuActions = getFieldProp("contextMenuActions", selectedColumn.type);
const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
if (!row) return null;
const actionGroups: IContextMenuItem[][] = [];
const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
// Field type actions
const fieldTypeActions = menuActions
? menuActions(selectedCell, onClose)
: [];
if (fieldTypeActions.length > 0) actionGroups.push(fieldTypeActions);
if (selectedColumn.type === FieldType.derivative) {
const renderedFieldMenuActions = getFieldProp(
"contextMenuActions",
selectedColumn.config?.renderFieldType
);
if (renderedFieldMenuActions) {
actionGroups.push(renderedFieldMenuActions(selectedCell, onClose));
}
}
// Cell actions
// TODO: Add copy and paste here
const cellValue = row?.[selectedCell.columnKey];
const handleClearValue = () =>
updateField({
path: selectedCell.path,
fieldName: selectedColumn.fieldName,
value: null,
deleteField: true,
const handleDuplicate = () => {
addRow({
row,
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
});
const columnFilters = getFieldProp("filter", selectedColumn.type);
const handleFilterValue = () => {
openTableFiltersPopover({
defaultQuery: {
key: selectedColumn.fieldName,
operator: columnFilters!.operators[0]?.value || "==",
value: cellValue,
},
});
onClose();
};
const cellActions = [
const handleDelete = () => deleteRow(row._rowy_ref.path);
const rowActions: IContextMenuItem[] = [
{
label: altPress ? "Clear value" : "Clear value…",
color: "error",
icon: <ClearIcon />,
label: "Copy ID",
icon: <CopyIcon />,
onClick: () => {
navigator.clipboard.writeText(row._rowy_ref.id);
onClose();
},
},
{
label: "Copy path",
icon: <CopyIcon />,
onClick: () => {
navigator.clipboard.writeText(row._rowy_ref.path);
onClose();
},
},
{
label: "Open in Firebase Console",
icon: <OpenIcon />,
onClick: () => {
window.open(
`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${row._rowy_ref.path.replace(
/\//g,
"~2F"
)}`
);
onClose();
},
},
{ label: "Divider", divider: true },
{
label: "Duplicate",
icon: <DuplicateIcon />,
disabled:
selectedColumn.editable === false ||
!row ||
cellValue ||
getFieldProp("group", selectedColumn.type) === "Auditing",
tableSettings.tableType === "collectionGroup" ||
(!userRoles.includes("ADMIN") && tableSettings.readOnly),
onClick: altPress
? handleClearValue
? handleDuplicate
: () => {
confirm({
title: "Clear cell value?",
body: "The cells value cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleClearValue,
title: "Duplicate row?",
body: (
<>
Row path:
<br />
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
{row._rowy_ref.path}
</code>
</>
),
confirm: "Duplicate",
handleConfirm: handleDuplicate,
});
onClose();
},
},
{
label: "Filter value",
icon: <FilterIcon />,
disabled: !columnFilters || cellValue === undefined,
onClick: handleFilterValue,
label: altPress ? "Delete" : "Delete…",
color: "error",
icon: <DeleteIcon />,
disabled: !userRoles.includes("ADMIN") && tableSettings.readOnly,
onClick: altPress
? handleDelete
: () => {
confirm({
title: "Delete row?",
body: (
<>
Row path:
<br />
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
{row._rowy_ref.path}
</code>
</>
),
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete,
});
onClose();
},
},
];
actionGroups.push(cellActions);
// Row actions
if (row) {
const handleDelete = () => deleteRow(row._rowy_ref.path);
const rowActions = [
if (selectedColumn) {
const menuActions = getFieldProp(
"contextMenuActions",
selectedColumn?.type
);
// Field type actions
const fieldTypeActions = menuActions
? menuActions(selectedCell, onClose)
: [];
if (fieldTypeActions.length > 0) actionGroups.push(fieldTypeActions);
if (selectedColumn?.type === FieldType.derivative) {
const renderedFieldMenuActions = getFieldProp(
"contextMenuActions",
selectedColumn.config?.renderFieldType
);
if (renderedFieldMenuActions) {
actionGroups.push(renderedFieldMenuActions(selectedCell, onClose));
}
}
// Cell actions
// TODO: Add copy and paste here
const cellValue = row?.[selectedCell.columnKey];
const handleClearValue = () =>
updateField({
path: selectedCell.path,
fieldName: selectedColumn.fieldName,
value: null,
deleteField: true,
});
const columnFilters = getFieldProp("filter", selectedColumn?.type);
const handleFilterValue = () => {
openTableFiltersPopover({
defaultQuery: {
key: selectedColumn.fieldName,
operator: columnFilters!.operators[0]?.value || "==",
value: cellValue,
},
});
onClose();
};
const cellActions = [
{
label: "Row",
icon: <RowIcon />,
subItems: [
{
label: "Copy ID",
icon: <CopyIcon />,
onClick: () => {
navigator.clipboard.writeText(row._rowy_ref.id);
onClose();
},
},
{
label: "Copy path",
icon: <CopyIcon />,
onClick: () => {
navigator.clipboard.writeText(row._rowy_ref.path);
onClose();
},
},
{
label: "Open in Firebase Console",
icon: <OpenIcon />,
onClick: () => {
window.open(
`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${row._rowy_ref.path.replace(
/\//g,
"~2F"
)}`
);
onClose();
},
},
{ label: "Divider", divider: true },
{
label: "Duplicate",
icon: <DuplicateIcon />,
disabled:
tableSettings.tableType === "collectionGroup" ||
(!userRoles.includes("ADMIN") && tableSettings.readOnly),
onClick: () => {
addRow({
row,
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
label: altPress ? "Clear value" : "Clear value…",
color: "error",
icon: <ClearIcon />,
disabled:
selectedColumn?.editable === false ||
!row ||
cellValue === undefined ||
getFieldProp("group", selectedColumn?.type) === "Auditing",
onClick: altPress
? handleClearValue
: () => {
confirm({
title: "Clear cell value?",
body: "The cells value cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleClearValue,
});
onClose();
},
},
{
label: altPress ? "Delete" : "Delete…",
color: "error",
icon: <DeleteIcon />,
disabled: !userRoles.includes("ADMIN") && tableSettings.readOnly,
onClick: altPress
? handleDelete
: () => {
confirm({
title: "Delete row?",
body: (
<>
Row path:
<br />
<code
style={{ userSelect: "all", wordBreak: "break-all" }}
>
{row._rowy_ref.path}
</code>
</>
),
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete,
});
onClose();
},
},
],
},
{
label: "Filter value",
icon: <FilterIcon />,
disabled: !columnFilters || cellValue === undefined,
onClick: handleFilterValue,
},
];
actionGroups.push(cellActions);
// Row actions as sub-menu
actionGroups.push([
{
label: "Row",
icon: <RowIcon />,
subItems: rowActions,
},
]);
} else {
actionGroups.push(rowActions);
}

View File

@@ -1,10 +1,15 @@
import { useAtom, useSetAtom } from "jotai";
import { Offline, Online } from "react-detect-offline";
import { Grid, Stack, Typography, Button, Divider } from "@mui/material";
import { Import as ImportIcon } from "@src/assets/icons";
import { AddColumn as AddColumnIcon } from "@src/assets/icons";
import {
Import as ImportIcon,
AddColumn as AddColumnIcon,
} from "@src/assets/icons";
import OfflineIcon from "@mui/icons-material/CloudOff";
import ImportCsv from "@src/components/TableToolbar/ImportCsv";
import EmptyState from "@src/components/EmptyState";
import ImportData from "@src/components/TableToolbar/ImportData/ImportData";
import {
tableScope,
@@ -13,7 +18,7 @@ import {
columnModalAtom,
tableModalAtom,
} from "@src/atoms/tableScope";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
export default function EmptyTable() {
const openColumnModal = useSetAtom(columnModalAtom, tableScope);
@@ -22,10 +27,14 @@ export default function EmptyTable() {
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const [tableRows] = useAtom(tableRowsAtom, tableScope);
// const { tableState, importWizardRef, columnMenuRef } = useProjectContext();
// check if theres any rows, and if rows include fields other than rowy_ref
const hasData =
tableRows.length > 0
? tableRows.some((row) => Object.keys(row).length > 1)
: false;
let contents = <></>;
if (tableRows.length > 0) {
if (hasData) {
contents = (
<>
<div>
@@ -72,10 +81,10 @@ export default function EmptyTable() {
<Grid container spacing={1}>
<Grid item xs>
<Typography paragraph>
You can import data from an external CSV file:
You can import data from an external source:
</Typography>
<ImportCsv
<ImportData
render={(onClick) => (
<Button
variant="contained"
@@ -83,7 +92,7 @@ export default function EmptyTable() {
startIcon={<ImportIcon />}
onClick={onClick}
>
Import CSV
Import data
</Button>
)}
PopoverProps={{
@@ -125,20 +134,35 @@ export default function EmptyTable() {
}
return (
<Stack
spacing={3}
justifyContent="center"
alignItems="center"
sx={{
height: `calc(100vh - ${APP_BAR_HEIGHT}px)`,
width: "100%",
p: 2,
maxWidth: 480,
margin: "0 auto",
textAlign: "center",
}}
>
{contents}
</Stack>
<>
<Offline>
<EmptyState
role="alert"
Icon={OfflineIcon}
message="Youre offline"
description="Go online to view this tables data"
style={{ height: `calc(100vh - ${TOP_BAR_HEIGHT}px)` }}
/>
</Offline>
<Online>
<Stack
spacing={3}
justifyContent="center"
alignItems="center"
sx={{
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
width: "100%",
p: 2,
maxWidth: 480,
margin: "0 auto",
textAlign: "center",
}}
id="empty-table"
>
{contents}
</Stack>
</Online>
</>
);
}

View File

@@ -0,0 +1,153 @@
import { memo } from "react";
import { useAtom, useSetAtom } from "jotai";
import type { IRenderedTableCellProps } from "@src/components/Table/TableCell/withRenderTableCell";
import { Stack, Tooltip, IconButton, alpha } from "@mui/material";
import { CopyCells as CopyCellsIcon } from "@src/assets/icons";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import MenuIcon from "@mui/icons-material/MoreHoriz";
import {
projectScope,
userRolesAtom,
tableAddRowIdTypeAtom,
altPressAtom,
confirmDialogAtom,
} from "@src/atoms/projectScope";
import {
tableScope,
tableSettingsAtom,
addRowAtom,
deleteRowAtom,
contextMenuTargetAtom,
} from "@src/atoms/tableScope";
export const FinalColumn = memo(function FinalColumn({
row,
focusInsideCell,
}: IRenderedTableCellProps) {
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const addRow = useSetAtom(addRowAtom, tableScope);
const deleteRow = useSetAtom(deleteRowAtom, tableScope);
const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope);
const [altPress] = useAtom(altPressAtom, projectScope);
const handleDelete = () => deleteRow(row.original._rowy_ref.path);
const handleDuplicate = () => {
addRow({
row: row.original,
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
});
};
if (!userRoles.includes("ADMIN") && tableSettings.readOnly === true)
return null;
return (
<Stack
direction="row"
alignItems="center"
className="cell-contents"
gap={0.5}
>
<Tooltip title="Row menu">
<IconButton
size="small"
color="inherit"
onClick={(e) => {
setContextMenuTarget(e.target as HTMLElement);
}}
className="row-hover-iconButton"
tabIndex={focusInsideCell ? 0 : -1}
>
<MenuIcon />
</IconButton>
</Tooltip>
<Tooltip title="Duplicate row">
<IconButton
size="small"
color="inherit"
disabled={tableSettings.tableType === "collectionGroup"}
onClick={
altPress
? handleDuplicate
: () => {
confirm({
title: "Duplicate row?",
body: (
<>
Row path:
<br />
<code
style={{ userSelect: "all", wordBreak: "break-all" }}
>
{row.original._rowy_ref.path}
</code>
</>
),
confirm: "Duplicate",
handleConfirm: handleDuplicate,
});
}
}
className="row-hover-iconButton"
tabIndex={focusInsideCell ? 0 : -1}
>
<CopyCellsIcon />
</IconButton>
</Tooltip>
<Tooltip title={`Delete row${altPress ? "" : "…"}`}>
<IconButton
size="small"
color="inherit"
onClick={
altPress
? handleDelete
: () => {
confirm({
title: "Delete row?",
body: (
<>
Row path:
<br />
<code
style={{ userSelect: "all", wordBreak: "break-all" }}
>
{row.original._rowy_ref.path}
</code>
</>
),
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete,
});
}
}
className="row-hover-iconButton"
tabIndex={focusInsideCell ? 0 : -1}
sx={{
"[role='row']:hover .row-hover-iconButton&&, .row-hover-iconButton&&:focus":
{
color: "error.main",
backgroundColor: (theme) =>
alpha(
theme.palette.error.main,
theme.palette.action.hoverOpacity * 2
),
},
}}
disabled={!row.original._rowy_ref.path}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Stack>
);
});
export default FinalColumn;

Some files were not shown because too many files have changed in this diff Show More