Merge branch 'develop' into feat/formula-esm

This commit is contained in:
shamsmosowi
2023-06-10 17:21:38 +02:00
38 changed files with 1686 additions and 4804 deletions

View File

@@ -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
View File

@@ -7,6 +7,7 @@
# production
/build
/dist
cloud_functions/functions/lib
# firebase

View File

@@ -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

View File

@@ -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=).

View File

@@ -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}`
);
};

View File

@@ -1,6 +1,6 @@
{
"hosting": {
"public": "build",
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{

View File

@@ -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.

View File

@@ -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"

View File

@@ -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";

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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 (

View File

@@ -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);

View File

@@ -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="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>
</>
);
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="Youre offline"
description="Go online to view this tables data"
style={{ height: `calc(100vh - ${TOP_BAR_HEIGHT}px)` }}
/>
);
}
}

View File

@@ -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`,
});

View File

@@ -234,9 +234,7 @@ export default function Step1Columns({
/>
}
label={
selectedFields.length === fieldKeys.length
? "Clear all"
: "Select all"
}

View File

@@ -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;

View File

@@ -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>
);
}
}

View 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>
);
}

View 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>
</>
);
}

View File

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

View File

@@ -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 */}

View File

@@ -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";

View File

@@ -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" */)

View File

@@ -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(
() =>

View File

@@ -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";

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -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
View 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"],
},
},
});

5690
yarn.lock

File diff suppressed because it is too large Load Diff