mirror of
https://github.com/rowyio/rowy.git
synced 2026-05-18 05:05:28 +02:00
Merge branch 'develop' into feat/formula-esm
This commit is contained in:
4
.github/workflows/deploy-preview.yml
vendored
4
.github/workflows/deploy-preview.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
# paths:
|
||||
# - "website/**"
|
||||
env:
|
||||
REACT_APP_FIREBASE_PROJECT_ID: rowyio
|
||||
REACT_APP_FIREBASE_PROJECT_WEB_API_KEY:
|
||||
VITE_APP_FIREBASE_PROJECT_ID: rowyio
|
||||
VITE_APP_FIREBASE_PROJECT_WEB_API_KEY:
|
||||
"${{ secrets.FIREBASE_WEB_API_KEY_TRYROWY }}"
|
||||
CI: ""
|
||||
jobs:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
cloud_functions/functions/lib
|
||||
|
||||
# firebase
|
||||
|
||||
@@ -17,12 +17,11 @@ Read the documentation on setting up your local development environment
|
||||
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/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) is a good place
|
||||
to start.
|
||||
[good first issues](https://github.com/rowyio/rowy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
||||
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
|
||||
@@ -39,7 +38,6 @@ 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
|
||||
@@ -47,10 +45,9 @@ 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://rowy.io/discord). This allows others in the
|
||||
community and the maintainers a chance to provide feedback and guidance before
|
||||
you spend time working on it.
|
||||
#contributions channel in Rowy's [Discord](https://rowy.io/discord). 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
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -33,7 +33,6 @@ Low-code for Firebase and Google Cloud.
|
||||
|
||||
## Features ✨
|
||||
|
||||
|
||||
<!-- <table>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -56,6 +55,7 @@ Low-code for Firebase and Google Cloud.
|
||||
</td>
|
||||
</tr>
|
||||
</table> -->
|
||||
|
||||
https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-9589-d7defbf7a63f.mp4
|
||||
|
||||
<!-- <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" />
|
||||
@@ -99,8 +99,10 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
|
||||
|
||||
## Quick guided install
|
||||
|
||||
Set up Rowy on your Google Cloud Platform project with this easy deploy button. Your
|
||||
data and cloud functions stay on your own Firestore/GCP and is managed via a cloud run instance that operates exclusively on your GCP project. So we do do not access or store any of your data on Rowy.
|
||||
Set up Rowy on your Google Cloud Platform project with this easy deploy button.
|
||||
Your data and cloud functions stay on your own Firestore/GCP and is managed via
|
||||
a cloud run instance that operates exclusively on your GCP project. So we do do
|
||||
not access or store any of your data on Rowy.
|
||||
|
||||
[<img width="200" alt="Guided quick start button" src="https://user-images.githubusercontent.com/307298/185548050-e9208fb6-fe53-4c84-bbfa-53c08e03c15f.png">](https://rowy.app/)
|
||||
|
||||
@@ -113,12 +115,17 @@ You can find the full documentation with how-to guides and templates
|
||||
|
||||
## Manual Install
|
||||
|
||||
We recommend the [quick guided install](https://github.com/rowyio/rowy#quick-guided-install) option above. Manual install option is only recommended if you want to develop and contribute to the project. Follow this [guide](https://docs.rowy.io/setup/install#option-2-manual-install) for manual setup.
|
||||
We recommend the
|
||||
[quick guided install](https://github.com/rowyio/rowy#quick-guided-install)
|
||||
option above. Manual install option is only recommended if you want to develop
|
||||
and contribute to the project. Follow this
|
||||
[guide](https://docs.rowy.io/setup/install#option-2-manual-install) for manual
|
||||
setup.
|
||||
|
||||
## Roadmap
|
||||
|
||||
[View our roadmap](https://roadmap.rowy.io/) on Rowy - Upvote,
|
||||
downvote, share your thoughts!
|
||||
[View our roadmap](https://roadmap.rowy.io/) 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=).
|
||||
|
||||
@@ -8,10 +8,10 @@ const main = (
|
||||
) => {
|
||||
return fs.writeFileSync(
|
||||
".env",
|
||||
`REACT_APP_FIREBASE_PROJECT_ID = ${projectID}
|
||||
REACT_APP_FIREBASE_PROJECT_WEB_API_KEY = ${firebaseWebApiKey}
|
||||
REACT_APP_ALGOLIA_APP_ID = ${algoliaAppId}
|
||||
REACT_APP_ALGOLIA_SEARCH_API_KEY = ${algoliaSearhApiKey}`
|
||||
`VITE_APP_FIREBASE_PROJECT_ID = ${projectID}
|
||||
VITE_APP_FIREBASE_PROJECT_WEB_API_KEY = ${firebaseWebApiKey}
|
||||
VITE_APP_ALGOLIA_APP_ID = ${algoliaAppId}
|
||||
VITE_APP_ALGOLIA_SEARCH_API_KEY = ${algoliaSearhApiKey}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "build",
|
||||
"public": "dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
|
||||
@@ -15,30 +15,30 @@
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%PUBLIC_URL%/favicon/apple-touch-icon.png"
|
||||
href="/favicon/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%PUBLIC_URL%/favicon/favicon-32x32.png"
|
||||
href="/favicon/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%PUBLIC_URL%/favicon/favicon-16x16.png"
|
||||
href="/favicon/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="%PUBLIC_URL%/favicon/icon.svg"
|
||||
href="/favicon/icon.svg"
|
||||
id="favicon-svg"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="%PUBLIC_URL%/favicon/safari-pinned-tab.svg"
|
||||
href="/favicon/safari-pinned-tab.svg"
|
||||
color="#4200FF"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#4200FF" />
|
||||
@@ -47,13 +47,13 @@
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
@@ -87,7 +87,7 @@
|
||||
property="og:description"
|
||||
content="Build on the Google Cloud Platform in minutes. Manage Firestore data in a spreadsheet-like UI, write Cloud Functions effortlessly in the browser, and connect to third-party apps. Rowy is open source!"
|
||||
/>
|
||||
<meta property="og:image" content="%PUBLIC_URL%/static/meta.png" />
|
||||
<meta property="og:image" content="/static/meta.png" />
|
||||
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://rowy.io/" />
|
||||
@@ -96,11 +96,12 @@
|
||||
property="twitter:description"
|
||||
content="Build on the Google Cloud Platform in minutes. Manage Firestore data in a spreadsheet-like UI, write Cloud Functions effortlessly in the browser, and connect to third-party apps. Rowy is open source!"
|
||||
/>
|
||||
<meta property="twitter:image" content="%PUBLIC_URL%/static/meta.png" />
|
||||
<meta property="twitter:image" content="/static/meta.png" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
28
package.json
28
package.json
@@ -10,13 +10,14 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@json2csv/plainjs": "^7.0.1",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@monaco-editor/react": "^4.4.4",
|
||||
"@mui/icons-material": "^5.10.16",
|
||||
"@mui/lab": "^5.0.0-alpha.76",
|
||||
"@mui/material": "^5.10.16",
|
||||
"@mui/x-date-pickers": "^5.0.0-alpha.4",
|
||||
"@rowy/form-builder": "^0.8.0",
|
||||
"@rowy/form-builder": "^1.0.0",
|
||||
"@rowy/multiselect": "^0.4.1",
|
||||
"@tanstack/react-table": "^8.5.15",
|
||||
"@tinymce/tinymce-react": "^3",
|
||||
@@ -24,6 +25,7 @@
|
||||
"algoliasearch": "^4.13.1",
|
||||
"ansi-to-react": "^6.1.6",
|
||||
"buffer": "^6.0.3",
|
||||
"colord": "^2.9.3",
|
||||
"compare-versions": "^4.1.3",
|
||||
"csv-parse": "^5.1.0",
|
||||
"date-fns": "^2.28.0",
|
||||
@@ -33,7 +35,6 @@
|
||||
"firebaseui": "^6.0.1",
|
||||
"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",
|
||||
@@ -47,7 +48,6 @@
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-color-palette": "^6.2.0",
|
||||
"react-detect-offline": "^2.4.5",
|
||||
"react-div-100vh": "^0.7.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
@@ -62,7 +62,6 @@
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-usestateref": "^1.0.8",
|
||||
"react-virtual": "^2.10.4",
|
||||
"remark-gfm": "^3.0.1",
|
||||
@@ -75,17 +74,18 @@
|
||||
"typescript": "^4.9.3",
|
||||
"use-algolia": "^1.5.3",
|
||||
"use-async-memo": "^1.2.4",
|
||||
"use-debounce": "^8.0.0",
|
||||
"use-debounce": "^9.0.4",
|
||||
"use-memo-value": "^1.0.1",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=7699 craco start",
|
||||
"startWithEmulators": "cross-env PORT=7699 REACT_APP_FIREBASE_EMULATORS=true craco start",
|
||||
"start": "vite --port 7699",
|
||||
"startWithEmulators": "VITE_APP_FIREBASE_EMULATORS=true vite --port 7699",
|
||||
"emulators": "firebase emulators:start --only firestore,auth --import ./emulators/ --export-on-exit",
|
||||
"test": "craco test --env ./src/test/custom-jest-env.js --verbose --detectOpenHandles",
|
||||
"build": "craco build",
|
||||
"test": "vitest",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview --port 7699",
|
||||
"analyze": "source-map-explorer ./build/static/js/*.js",
|
||||
"prepare": "husky install",
|
||||
"env": "node createDotEnv",
|
||||
@@ -155,7 +155,6 @@
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/json2csv": "^5.0.3",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/react": "^18.0.25",
|
||||
@@ -168,6 +167,8 @@
|
||||
"@types/seedrandom": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"craco-alias": "^3.0.1",
|
||||
"craco-swc": "^0.5.1",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -177,6 +178,7 @@
|
||||
"eslint-plugin-local-rules": "^1.1.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.2.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
"happy-dom": "^9.20.3",
|
||||
"husky": ">=7.0.4",
|
||||
"lint-staged": ">=12.3.7",
|
||||
"monaco-editor": "^0.33.0",
|
||||
@@ -184,7 +186,11 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"ts-jest": "^28.0.2",
|
||||
"typedoc": "^0.23.21"
|
||||
"typedoc": "^0.23.21",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.31.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18"
|
||||
|
||||
@@ -12,13 +12,12 @@ import type { languages } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { SystemStyleObject, Theme } from "@mui/system";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
|
||||
import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts";
|
||||
import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts";
|
||||
import utilsDefs from "!!raw-loader!./utils.d.ts";
|
||||
import rowyUtilsDefs from "!!raw-loader!./rowy.d.ts";
|
||||
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
||||
import firestoreDefs from "./firestore.d.ts?raw";
|
||||
import firebaseAuthDefs from "./firebaseAuth.d.ts?raw";
|
||||
import firebaseStorageDefs from "./firebaseStorage.d.ts?raw";
|
||||
import utilsDefs from "./utils.d.ts?raw";
|
||||
import rowyUtilsDefs from "./rowy.d.ts?raw";
|
||||
import extensionsDefs from "./extensions.d.ts?raw";
|
||||
import { projectScope, secretNamesAtom } from "@src/atoms/projectScope";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
|
||||
@@ -192,9 +192,10 @@ export default function ColumnMenu({
|
||||
setTableSorts(
|
||||
isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }]
|
||||
);
|
||||
if (!isSorted || isAsc) {
|
||||
triggerSaveTableSorts([{ key: sortKey, direction: "desc" }]);
|
||||
}
|
||||
|
||||
triggerSaveTableSorts(
|
||||
isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }]
|
||||
);
|
||||
handleClose();
|
||||
},
|
||||
active: isSorted && !isAsc,
|
||||
@@ -209,9 +210,9 @@ export default function ColumnMenu({
|
||||
setTableSorts(
|
||||
isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }]
|
||||
);
|
||||
if (!isSorted || !isAsc) {
|
||||
triggerSaveTableSorts([{ key: sortKey, direction: "asc" }]);
|
||||
}
|
||||
triggerSaveTableSorts(
|
||||
isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }]
|
||||
);
|
||||
handleClose();
|
||||
},
|
||||
active: isSorted && isAsc,
|
||||
|
||||
@@ -11,8 +11,7 @@ import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import defaultValueDefs from "!!raw-loader!./defaultValue.d.ts";
|
||||
import defaultValueDefs from "./defaultValue.d.ts?raw";
|
||||
import {
|
||||
projectScope,
|
||||
compatibleRowyRunVersionAtom,
|
||||
|
||||
@@ -11,14 +11,14 @@ import "tinymce/themes/silver";
|
||||
import "tinymce/icons/default";
|
||||
// Editor styles
|
||||
/* eslint import/no-webpack-loader-syntax: off */
|
||||
import skinCss from "!!raw-loader!tinymce/skins/ui/oxide/skin.min.css";
|
||||
import skinDarkCss from "!!raw-loader!tinymce/skins/ui/oxide-dark/skin.min.css";
|
||||
import skinCss from "tinymce/skins/ui/oxide/skin.min.css?inline";
|
||||
import skinDarkCss from "tinymce/skins/ui/oxide-dark/skin.min.css?inline";
|
||||
// Content styles, including inline UI like fake cursors
|
||||
/* eslint import/no-webpack-loader-syntax: off */
|
||||
import contentCss from "!!raw-loader!tinymce/skins/content/default/content.min.css";
|
||||
import contentUiCss from "!!raw-loader!tinymce/skins/ui/oxide/content.min.css";
|
||||
import contentCssDark from "!!raw-loader!tinymce/skins/content/dark/content.min.css";
|
||||
import contentUiCssDark from "!!raw-loader!tinymce/skins/ui/oxide-dark/content.min.css";
|
||||
import contentCss from "tinymce/skins/content/default/content.min.css?inline";
|
||||
import contentUiCss from "tinymce/skins/ui/oxide/content.min.css?inline";
|
||||
import contentCssDark from "tinymce/skins/content/dark/content.min.css?inline";
|
||||
import contentUiCssDark from "tinymce/skins/ui/oxide-dark/content.min.css?inline";
|
||||
|
||||
// Plugins
|
||||
import "tinymce/plugins/autoresize";
|
||||
|
||||
@@ -38,14 +38,12 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({
|
||||
const triggerSaveTableSorts = useSaveTableSorts(canEditColumns);
|
||||
|
||||
const handleSortClick = () => {
|
||||
if (nextSort === "none") setTableSorts([]);
|
||||
else setTableSorts([{ key: sortKey, direction: nextSort }]);
|
||||
triggerSaveTableSorts([
|
||||
{
|
||||
key: sortKey,
|
||||
direction: nextSort === "none" ? "asc" : nextSort,
|
||||
},
|
||||
]);
|
||||
setTableSorts(
|
||||
nextSort === "none" ? [] : [{ key: sortKey, direction: nextSort }]
|
||||
);
|
||||
triggerSaveTableSorts(
|
||||
nextSort === "none" ? [] : [{ key: sortKey, direction: nextSort }]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
userRolesAtom,
|
||||
altPressAtom,
|
||||
confirmDialogAtom,
|
||||
updateUserSettingsAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import {
|
||||
tableScope,
|
||||
@@ -34,8 +35,10 @@ import {
|
||||
updateFieldAtom,
|
||||
tableFiltersPopoverAtom,
|
||||
_updateRowDbAtom,
|
||||
tableIdAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { TableRow } from "@src/types/table";
|
||||
|
||||
interface IMenuContentsProps {
|
||||
onClose: () => void;
|
||||
@@ -58,6 +61,8 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
tableFiltersPopoverAtom,
|
||||
tableScope
|
||||
);
|
||||
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
|
||||
const addRowIdType = tableSchema.idType || "decrement";
|
||||
|
||||
@@ -241,7 +246,28 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
|
||||
// Cell actions
|
||||
// TODO: Add copy and paste here
|
||||
const cellValue = row?.[selectedCell.columnKey];
|
||||
|
||||
const selectedColumnKey = selectedCell.columnKey;
|
||||
const selectedColumnKeySplit = selectedColumnKey.split(".");
|
||||
|
||||
const getNestedFieldValue = (object: TableRow, keys: string[]) => {
|
||||
let value = object;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
if (value && typeof value === "object" && key in value) {
|
||||
value = value[key];
|
||||
} else {
|
||||
// Handle cases where the key does not exist in the nested structure
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const cellValue = getNestedFieldValue(row, selectedColumnKeySplit);
|
||||
|
||||
const columnFilters = getFieldProp(
|
||||
"filter",
|
||||
@@ -249,14 +275,18 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
? selectedColumn.config?.renderFieldType
|
||||
: selectedColumn?.type
|
||||
);
|
||||
const handleFilterValue = () => {
|
||||
openTableFiltersPopover({
|
||||
defaultQuery: {
|
||||
const handleFilterBy = () => {
|
||||
const filters = [
|
||||
{
|
||||
key: selectedColumn.fieldName,
|
||||
operator: columnFilters!.operators[0]?.value || "==",
|
||||
value: cellValue,
|
||||
},
|
||||
});
|
||||
];
|
||||
|
||||
if (updateUserSettings) {
|
||||
updateUserSettings({ tables: { [`${tableId}`]: { filters } } });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
const cellActions = [
|
||||
@@ -272,10 +302,10 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
onClick: handleClearValue,
|
||||
},
|
||||
{
|
||||
label: "Filter value",
|
||||
label: "Filter by",
|
||||
icon: <FilterIcon />,
|
||||
disabled: !columnFilters || cellValue === undefined,
|
||||
onClick: handleFilterValue,
|
||||
onClick: handleFilterBy,
|
||||
},
|
||||
];
|
||||
actionGroups.push(cellActions);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { Offline, Online } from "react-detect-offline";
|
||||
|
||||
import { Grid, Stack, Typography, Button, Divider } from "@mui/material";
|
||||
import {
|
||||
@@ -142,36 +141,34 @@ export default function EmptyTable() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Offline>
|
||||
<EmptyState
|
||||
role="alert"
|
||||
Icon={OfflineIcon}
|
||||
message="You’re offline"
|
||||
description="Go online to view this table’s 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>
|
||||
</>
|
||||
);
|
||||
if (navigator.onLine) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EmptyState
|
||||
role="alert"
|
||||
Icon={OfflineIcon}
|
||||
message="You’re offline"
|
||||
description="Go online to view this table’s data"
|
||||
style={{ height: `calc(100vh - ${TOP_BAR_HEIGHT}px)` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { parse as json2csv } from "json2csv";
|
||||
import { Parser } from "@json2csv/plainjs";
|
||||
import { saveAs } from "file-saver";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { getDocs } from "firebase/firestore";
|
||||
@@ -171,10 +171,10 @@ export default function Export({
|
||||
const csvData = docs.map((doc: any) =>
|
||||
columns.reduce(selectedColumnsCsvReducer(doc), {})
|
||||
);
|
||||
const csv = json2csv(
|
||||
csvData,
|
||||
const parser = new Parser(
|
||||
exportType === "tsv" ? { delimiter: "\t" } : undefined
|
||||
);
|
||||
const csv = parser.parse(csvData);
|
||||
const csvBlob = new Blob([csv], {
|
||||
type: `text/${exportType};charset=utf-8`,
|
||||
});
|
||||
|
||||
@@ -234,9 +234,7 @@ export default function Step1Columns({
|
||||
/>
|
||||
}
|
||||
label={
|
||||
|
||||
selectedFields.length === fieldKeys.length
|
||||
|
||||
? "Clear all"
|
||||
: "Select all"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { parse } from "csv-parse/browser/esm";
|
||||
import { parse as parseJSON } from "json2csv";
|
||||
import { Parser, ParserOptions } from "@json2csv/plainjs";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useSnackbar } from "notistack";
|
||||
@@ -78,7 +78,8 @@ function convertJSONToCSV(rawData: string): string | false {
|
||||
};
|
||||
|
||||
try {
|
||||
const csv = parseJSON(rawDataJSONified, opts);
|
||||
const parser = new Parser(opts as ParserOptions);
|
||||
const csv = parser.parse(rawDataJSONified);
|
||||
return csv;
|
||||
} catch (err) {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Suspense, forwardRef } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { Offline, Online } from "react-detect-offline";
|
||||
|
||||
import { Tooltip, Typography, TypographyProps } from "@mui/material";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
@@ -78,22 +77,20 @@ function LoadedRowsStatus() {
|
||||
}
|
||||
|
||||
export default function SuspendedLoadedRowsStatus() {
|
||||
return (
|
||||
<>
|
||||
<Online>
|
||||
<Suspense fallback={<StatusText>{loadingIcon}Loading…</StatusText>}>
|
||||
<LoadedRowsStatus />
|
||||
</Suspense>
|
||||
</Online>
|
||||
|
||||
<Offline>
|
||||
<Tooltip title="Changes will be saved when you reconnect" describeChild>
|
||||
<StatusText color="error.main">
|
||||
<OfflineIcon />
|
||||
Offline
|
||||
</StatusText>
|
||||
</Tooltip>
|
||||
</Offline>
|
||||
</>
|
||||
);
|
||||
if (navigator.onLine) {
|
||||
return (
|
||||
<Suspense fallback={<StatusText>{loadingIcon}Loading…</StatusText>}>
|
||||
<LoadedRowsStatus />
|
||||
</Suspense>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title="Changes will be saved when you reconnect" describeChild>
|
||||
<StatusText color="error.main">
|
||||
<OfflineIcon />
|
||||
Offline
|
||||
</StatusText>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
135
src/components/TableToolbar/Sort/Sort.tsx
Normal file
135
src/components/TableToolbar/Sort/Sort.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import {
|
||||
Grid,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
alpha,
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
import {
|
||||
tableColumnsOrderedAtom,
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
tableSortsAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import SortPopover from "./SortPopover";
|
||||
import ColumnSelect from "@src/components/Table/ColumnSelect";
|
||||
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import { projectScope, userRolesAtom } from "@src/atoms/projectScope";
|
||||
import useSaveTableSorts from "@src/components/Table/ColumnHeader/useSaveTableSorts";
|
||||
|
||||
export default function Sort() {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
|
||||
const canEditColumns = Boolean(
|
||||
userRoles.includes("ADMIN") ||
|
||||
tableSettings.modifiableBy?.some((r) => userRoles.includes(r))
|
||||
);
|
||||
|
||||
const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope);
|
||||
const triggerSaveTableSorts = useSaveTableSorts(canEditColumns);
|
||||
|
||||
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
|
||||
|
||||
const sortColumns = tableColumnsOrdered.map(({ key, name, type, index }) => ({
|
||||
value: key,
|
||||
label: name,
|
||||
type,
|
||||
index,
|
||||
}));
|
||||
|
||||
return (
|
||||
<SortPopover>
|
||||
{({ handleClose }) => (
|
||||
<Grid container spacing={2} sx={{ p: 3 }}>
|
||||
<Grid item xs={5.5}>
|
||||
<ColumnSelect
|
||||
multiple={false}
|
||||
label="Column"
|
||||
options={sortColumns}
|
||||
value={tableSorts[0].key}
|
||||
onChange={(value: string | null) => {
|
||||
setTableSorts(
|
||||
value === null
|
||||
? []
|
||||
: [{ key: value, direction: tableSorts[0].direction }]
|
||||
);
|
||||
triggerSaveTableSorts(
|
||||
value === null
|
||||
? []
|
||||
: [{ key: value, direction: tableSorts[0].direction }]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={5.5}>
|
||||
<TextField
|
||||
label="Sort"
|
||||
select
|
||||
variant="filled"
|
||||
fullWidth
|
||||
value={tableSorts[0].direction}
|
||||
onChange={(e) => {
|
||||
setTableSorts([
|
||||
{
|
||||
key: tableSorts[0].key,
|
||||
direction: e.target.value === "asc" ? "asc" : "desc",
|
||||
},
|
||||
]);
|
||||
triggerSaveTableSorts([
|
||||
{
|
||||
key: tableSorts[0].key,
|
||||
direction: e.target.value === "asc" ? "asc" : "desc",
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<MenuItem key="asc" value="asc">
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<ArrowUpwardIcon />
|
||||
<Typography>Sort ascending</Typography>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
<MenuItem key="desc" value="desc">
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<ArrowDownwardIcon />
|
||||
<Typography>Sort descending</Typography>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={1} alignSelf="flex-end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setTableSorts([]);
|
||||
triggerSaveTableSorts([]);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover, &:focus": {
|
||||
color: "error.main",
|
||||
backgroundColor: (theme) =>
|
||||
alpha(
|
||||
theme.palette.error.main,
|
||||
theme.palette.action.hoverOpacity * 2
|
||||
),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</SortPopover>
|
||||
);
|
||||
}
|
||||
67
src/components/TableToolbar/Sort/SortPopover.tsx
Normal file
67
src/components/TableToolbar/Sort/SortPopover.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import { Popover } from "@mui/material";
|
||||
|
||||
import ButtonWithStatus from "@src/components/ButtonWithStatus";
|
||||
|
||||
import { tableScope, tableSortsAtom } from "@src/atoms/tableScope";
|
||||
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
|
||||
export interface ISortPopoverProps {
|
||||
children: (props: { handleClose: () => void }) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function SortPopover({ children }: ISortPopoverProps) {
|
||||
const [tableSortPopoverState, setTableSortPopoverState] = useState(false);
|
||||
|
||||
const anchorEl = useRef<HTMLButtonElement>(null);
|
||||
const popoverId = tableSortPopoverState ? "sort-popover" : undefined;
|
||||
const handleClose = () => setTableSortPopoverState(false);
|
||||
|
||||
const [tableSorts] = useAtom(tableSortsAtom, tableScope);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonWithStatus
|
||||
ref={anchorEl}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => setTableSortPopoverState(true)}
|
||||
active={true}
|
||||
startIcon={
|
||||
<ArrowDownwardIcon
|
||||
sx={{
|
||||
transition: (theme) =>
|
||||
theme.transitions.create("transform", {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
|
||||
transform:
|
||||
tableSorts[0].direction === "asc" ? "rotate(180deg)" : "none",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
aria-describedby={popoverId}
|
||||
>
|
||||
Sorted: {tableSorts[0].key}
|
||||
</ButtonWithStatus>
|
||||
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={tableSortPopoverState}
|
||||
anchorEl={anchorEl.current}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "left" }}
|
||||
sx={{
|
||||
"& .MuiPaper-root": { width: 640 },
|
||||
"& .content": { p: 3 },
|
||||
}}
|
||||
>
|
||||
{children({ handleClose })}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
2
src/components/TableToolbar/Sort/index.tsx
Normal file
2
src/components/TableToolbar/Sort/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./Sort";
|
||||
export { default } from "./Sort";
|
||||
@@ -32,11 +32,15 @@ import {
|
||||
tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
tableModalAtom,
|
||||
tableSortsAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { TableToolsType } from "@src/types/table";
|
||||
import FilterIcon from "@mui/icons-material/FilterList";
|
||||
|
||||
// prettier-ignore
|
||||
const Sort = lazy(() => import("./Sort" /* webpackChunkName: "Filters" */));
|
||||
|
||||
// prettier-ignore
|
||||
const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */));
|
||||
// prettier-ignore
|
||||
@@ -62,6 +66,7 @@ export default function TableToolbar({
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const openTableModal = useSetAtom(tableModalAtom, tableScope);
|
||||
const [tableSorts] = useAtom(tableSortsAtom, tableScope);
|
||||
const hasDerivatives =
|
||||
Object.values(tableSchema.columns ?? {}).filter(
|
||||
(column) => column.type === FieldType.derivative
|
||||
@@ -116,6 +121,11 @@ export default function TableToolbar({
|
||||
<Filters />
|
||||
</Suspense>
|
||||
)}
|
||||
{tableSorts.length > 0 && tableSettings.isCollection !== false && (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<Sort />
|
||||
</Suspense>
|
||||
)}
|
||||
<div /> {/* Spacer */}
|
||||
<LoadedRowsStatus />
|
||||
<div style={{ flexGrow: 1, minWidth: 64 }} />
|
||||
@@ -134,22 +144,20 @@ export default function TableToolbar({
|
||||
</Suspense>
|
||||
)
|
||||
)}
|
||||
|
||||
{(!projectSettings.exporterRoles ||
|
||||
projectSettings.exporterRoles.length === 0 ||
|
||||
userRoles.some((role) =>
|
||||
projectSettings.exporterRoles?.includes(role)
|
||||
)) && (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<TableToolbarButton
|
||||
title="Export/Download"
|
||||
onClick={() => openTableModal("export")}
|
||||
icon={<ExportIcon />}
|
||||
disabled={disabledTools.includes("export")}
|
||||
/>
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<TableToolbarButton
|
||||
title="Export/Download"
|
||||
onClick={() => openTableModal("export")}
|
||||
icon={<ExportIcon />}
|
||||
disabled={disabledTools.includes("export")}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<>
|
||||
<div /> {/* Spacer */}
|
||||
|
||||
@@ -42,8 +42,7 @@ import {
|
||||
import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import actionDefs from "!!raw-loader!./action.d.ts";
|
||||
import actionDefs from "./action.d.ts?raw";
|
||||
import { RUN_ACTION_TEMPLATE, UNDO_ACTION_TEMPLATE } from "./templates";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import connectorDefs from "!!raw-loader!./connector.d.ts";
|
||||
import connectorDefs from "./connector.d.ts?raw";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { baseFunction } from "./utils";
|
||||
@@ -26,7 +25,7 @@ import {
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
|
||||
//import typeDefs from "!!raw-loader!./types.d.ts";
|
||||
//import typeDefs from "./types.d.ts?raw";
|
||||
const CodeEditor = lazy(
|
||||
() =>
|
||||
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
|
||||
@@ -19,8 +19,7 @@ import { FieldType } from "@src/constants/fields";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import derivativeDefs from "!!raw-loader!./derivative.d.ts";
|
||||
import derivativeDefs from "./derivative.d.ts?raw";
|
||||
|
||||
const CodeEditor = lazy(
|
||||
() =>
|
||||
|
||||
@@ -29,8 +29,7 @@ import {
|
||||
import PreviewTable from "./PreviewTable";
|
||||
import { getFieldProp } from "..";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import formulaDefs from "!!raw-loader!./formula.d.ts";
|
||||
import formulaDefs from "./formula.d.ts?raw";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import { currentUserAtom } from "@src/atoms/projectScope";
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { IDisplayCellProps } from "@src/components/fields/types";
|
||||
|
||||
import { Grid, Box } from "@mui/material";
|
||||
import { Grid, Box, useTheme } from "@mui/material";
|
||||
|
||||
import { resultColorsScale } from "@src/utils/color";
|
||||
|
||||
export default function Slider({ column, value }: IDisplayCellProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
max,
|
||||
min,
|
||||
unit,
|
||||
colors,
|
||||
}: {
|
||||
max: number;
|
||||
min: number;
|
||||
unit?: string;
|
||||
colors: any;
|
||||
} = {
|
||||
max: 10,
|
||||
min: 0,
|
||||
@@ -24,6 +28,7 @@ export default function Slider({ column, value }: IDisplayCellProps) {
|
||||
? 0
|
||||
: ((value - min) / (max - min)) * 100;
|
||||
|
||||
const percentage = progress / 100;
|
||||
return (
|
||||
<Grid container alignItems="center" wrap="nowrap" spacing={1}>
|
||||
<Grid item xs={6} style={{ fontVariantNumeric: "tabular-nums" }}>
|
||||
@@ -48,7 +53,11 @@ export default function Slider({ column, value }: IDisplayCellProps) {
|
||||
maxWidth: "100%",
|
||||
|
||||
width: `${progress}%`,
|
||||
backgroundColor: resultColorsScale(progress / 100).toHex(),
|
||||
backgroundColor: resultColorsScale(
|
||||
percentage,
|
||||
colors,
|
||||
theme.palette.background.paper
|
||||
).toHex(),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,54 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
Grid,
|
||||
InputLabel,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import ColorPickerInput from "@src/components/ColorPickerInput";
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
import { TextField, FormControlLabel, Switch } from "@mui/material";
|
||||
|
||||
import { Color, toColor } from "react-color-palette";
|
||||
import { fieldSx } from "@src/components/SideDrawer/utils";
|
||||
import { resultColorsScale, defaultColors } from "@src/utils/color";
|
||||
|
||||
const colorLabels: { [key: string]: string } = {
|
||||
0: "Start",
|
||||
1: "Middle",
|
||||
2: "End",
|
||||
};
|
||||
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
const colors: string[] = config.colors ?? defaultColors;
|
||||
|
||||
const [checkStates, setCheckStates] = useState<boolean[]>(
|
||||
colors.map(Boolean)
|
||||
);
|
||||
|
||||
const onCheckboxChange = (index: number, checked: boolean) => {
|
||||
onChange("colors")(
|
||||
colors.map((value: any, idx: number) =>
|
||||
index === idx ? (checked ? value || defaultColors[idx] : null) : value
|
||||
)
|
||||
);
|
||||
setCheckStates(
|
||||
checkStates.map((value, idx) => (index === idx ? checked : value))
|
||||
);
|
||||
};
|
||||
|
||||
const handleColorChange = (index: number, color: Color): void => {
|
||||
onChange("colors")(
|
||||
colors.map((value, idx) => (index === idx ? color.hex : value))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
@@ -47,6 +94,124 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
}
|
||||
label="Show slider steps"
|
||||
/>
|
||||
|
||||
<Grid container>
|
||||
{checkStates.map((checked: boolean, index: number) => {
|
||||
const colorHex = colors[index];
|
||||
return (
|
||||
<Grid
|
||||
xs={12}
|
||||
md={4}
|
||||
item
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "end",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
sx={[
|
||||
fieldSx,
|
||||
{
|
||||
width: "auto",
|
||||
boxShadow: "none",
|
||||
backgroundColor: "inherit",
|
||||
"&:hover": {
|
||||
backgroundColor: "inherit",
|
||||
},
|
||||
},
|
||||
]}
|
||||
onChange={() => onCheckboxChange(index, !checked)}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
label={colorLabels[index]}
|
||||
value={1}
|
||||
fullWidth
|
||||
disabled={!checkStates[index]}
|
||||
>
|
||||
<MenuItem value={1} sx={{ display: "none" }}>
|
||||
{checked && (
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: colorHex,
|
||||
width: 15,
|
||||
height: 15,
|
||||
mr: 1.5,
|
||||
boxShadow: (theme) =>
|
||||
`0 0 0 1px ${theme.palette.divider} inset`,
|
||||
borderRadius: 0.5,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
<Box>{colorHex}</Box>
|
||||
</Box>
|
||||
)}
|
||||
</MenuItem>
|
||||
{colorHex && (
|
||||
<div>
|
||||
<ColorPickerInput
|
||||
value={toColor("hex", colorHex)}
|
||||
onChangeComplete={(color) =>
|
||||
handleColorChange(index, color)
|
||||
}
|
||||
disabled={!checkStates[index]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
<Preview colors={config.colors} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Preview = ({ colors }: { colors: any }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<InputLabel>
|
||||
Preview:
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1].map((value) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
padding: "0.5rem 0",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
key={value}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: resultColorsScale(
|
||||
value,
|
||||
colors,
|
||||
theme.palette.background.paper
|
||||
).toHex(),
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
<Typography style={{ position: "relative", zIndex: 1 }}>
|
||||
{value * 100}%
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</InputLabel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,9 @@ export default function getLabel(value: any, conditions: any) {
|
||||
let _label: any = undefined;
|
||||
const isBoolean = Boolean(typeof value === "boolean");
|
||||
const notBoolean = Boolean(typeof value !== "boolean");
|
||||
const isNullOrUndefined = Boolean((value === null || value === undefined) && notBoolean);
|
||||
const isNullOrUndefined = Boolean(
|
||||
(value === null || value === undefined) && notBoolean
|
||||
);
|
||||
const isNumeric = Boolean(typeof value === "number");
|
||||
|
||||
if (isNullOrUndefined) _label = getFalseyLabelFrom(conditions, value);
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface ISideDrawerFieldProps<T = any> {
|
||||
/** Field locked. Do NOT check `column.locked` */
|
||||
disabled: boolean;
|
||||
|
||||
row: TableRow
|
||||
row: TableRow;
|
||||
}
|
||||
|
||||
export interface ISettingsProps {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useEffect } from "react";
|
||||
|
||||
const DOCUMENT_TITLE_BASE =
|
||||
"Rowy" +
|
||||
(process.env.NODE_ENV === "production"
|
||||
(import.meta.env.MODE === "production"
|
||||
? ""
|
||||
: ` (${
|
||||
process.env.REACT_APP_FIREBASE_EMULATORS ? "Emulator • " : ""
|
||||
}${process.env.NODE_ENV.replace("development", "dev")})`);
|
||||
import.meta.env.VITE_APP_FIREBASE_EMULATORS ? "Emulator • " : ""
|
||||
}${import.meta.env.MODE.replace("development", "dev")})`);
|
||||
|
||||
/**
|
||||
* Sets the document/tab title and resets when the page is changed
|
||||
|
||||
1
src/react-app-env.d.ts
vendored
1
src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -10,17 +10,19 @@ import { getStorage, connectStorageEmulator } from "firebase/storage";
|
||||
import { getFunctions } from "firebase/functions";
|
||||
|
||||
export const envConfig = {
|
||||
apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY,
|
||||
authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`,
|
||||
databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`,
|
||||
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
|
||||
storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`,
|
||||
apiKey: import.meta.env.VITE_APP_FIREBASE_PROJECT_WEB_API_KEY,
|
||||
authDomain: `${import.meta.env.VITE_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`,
|
||||
databaseURL: `https://${
|
||||
import.meta.env.VITE_APP_FIREBASE_PROJECT_ID
|
||||
}.firebaseio.com`,
|
||||
projectId: import.meta.env.VITE_APP_FIREBASE_PROJECT_ID,
|
||||
storageBucket: `${import.meta.env.VITE_APP_FIREBASE_PROJECT_ID}.appspot.com`,
|
||||
};
|
||||
|
||||
// Connect emulators based on env vars
|
||||
const envConnectEmulators =
|
||||
process.env.NODE_ENV === "test" ||
|
||||
process.env.REACT_APP_FIREBASE_EMULATORS === "true";
|
||||
import.meta.env.NODE_ENV === "test" ||
|
||||
import.meta.env.VITE_APP_FIREBASE_EMULATORS === "true";
|
||||
|
||||
/**
|
||||
* Store Firebase config here so it can be set programmatically.
|
||||
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -3,9 +3,9 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
38
vite.config.ts
Normal file
38
vite.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
||||
import svgrPlugin from "vite-plugin-svgr";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
// Explicitly setting mainFields to default value. For some reason, Vitest isn't
|
||||
// respecting the 'module' field in package.json without specifying it explicitly
|
||||
mainFields: ["module", "jsnext:main", "jsnext"],
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
"jotai/babel/plugin-react-refresh",
|
||||
"jotai/babel/plugin-debug-label",
|
||||
],
|
||||
},
|
||||
include: "**/*.tsx",
|
||||
}),
|
||||
// To enable import '@src/' type of imports
|
||||
viteTsconfigPaths(),
|
||||
// To enable import of SVG as React component
|
||||
svgrPlugin(),
|
||||
],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "happy-dom",
|
||||
setupFiles: "src/test/setupTests.ts",
|
||||
deps: {
|
||||
// According to vitest, clsx exports ES Module code in a CommonJS package.
|
||||
// This fixes it.
|
||||
inline: ["clsx"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user