Merge branch 'develop' into feature/rowy-285-cms

This commit is contained in:
Sidney Alcantara
2022-03-03 17:41:52 +11:00
committed by GitHub
11 changed files with 234 additions and 77 deletions

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Support & questions
url: https://discord.com/invite/fjBugmvzZP
about: Chat with us for live support on discord.
- name: 🙌 Want to join our team?
url: https://www.rowy.io/jobs
about: Get in touch to contribute & work with Rowy

View File

@@ -1,6 +1,6 @@
{
"name": "rowy",
"version": "2.3.2",
"version": "2.4.0-rc.0",
"homepage": "https://rowy.io",
"repository": {
"type": "git",

View File

@@ -1,32 +1,95 @@
/**
* utility functions
*/
declare namespace rowy {
type RowyFile = {
downloadURL: string;
name: string;
type: string;
lastModifiedTS: number;
};
type RowyUser = {
email: any;
emailVerified: boolean;
displayName: string;
photoURL: string;
uid: string;
timestamp: number;
};
type uploadOptions = {
bucket?: string;
folderPath?: string;
fileName?: string;
};
interface Rowy {
metadata: {
/**
* The project ID of the project running this function.
*/
projectId: () => Promise<string>;
/**
* The numeric project ID of the project running this function.
*/
projectNumber: () => Promise<string>;
/**
* The email address of service account running this function.
* This is the service account that is used to call other APIs.
* Ensure that the service account has the correct permissions.
*/
serviceAccountEmail: () => Promise<string>;
/**
* a user object of the service account running this function.
* Compatible with Rowy audit fields
* Can be used to add createdBy or updatedBy fields to a document.
*/
serviceAccountUser: () => Promise<RowyUser>;
};
/**
* uploads a file to the cloud storage from a url
* Gives access to the Secret Manager.
* manage your secrets in the Google Cloud Console.
*/
function url2storage(
url: string,
storagePath: string,
fileName?: string
): Promise<{
downloadURL: string;
name: string;
type: string;
lastModifiedTS: Date;
}>;
secrets: {
/**
* Get an existing secret from the secret manager.
*/
get: (name: string, version?: string) => Promise<string | undefined>;
};
/**
* Gets the secret defined in Google Cloud Secret
* Gives access to the Cloud Storage.
*/
async function getSecret(name: string, v?: string): Promise<string | null>;
async function getServiceAccountUser(): Promise<{
email: string;
emailVerified: boolean;
displayName: string;
photoURL: string;
uid: string;
timestamp: number;
}>;
storage: {
upload: {
/**
* uploads a file to storage bucket from an external url.
*/
url: (
url: string,
options: uploadOptions
) => Promise<RowyFile | undefined>;
/**
* uploads a file to storage bucket from a buffer or string
*/
data: (
data: Buffer | string,
options: uploadOptions
) => Promise<RowyFile | undefined>;
};
};
/**
* @deprecated will be removed in version 2.0.
* use rowy.secrets.get instead.
* Get an existing secret from the secret manager.
*/
getSecret: (name: string, version?: string) => Promise<string | undefined>;
/**
* @deprecated will be removed in version 2.0.
* use rowy.metadata.serviceAccountUser instead.
* Compatible with Rowy audit fields
* Can be used to add createdBy or updatedBy fields to a document.
*/
getServiceAccountUser: () => Promise<RowyUser>;
/**
* @deprecated will be removed in version 2.0.
* use rowy.storage.upload.url instead.
* uploads a file to storage bucket from an external url.
*/
url2storage: (url: string) => Promise<RowyFile | undefined>;
}
declare const rowy: Rowy;

View File

@@ -2,6 +2,7 @@ import { createElement, useEffect } from "react";
import { useForm } from "react-hook-form";
import _sortBy from "lodash/sortBy";
import _isEmpty from "lodash/isEmpty";
import _set from "lodash/set";
import createPersistedState from "use-persisted-state";
import { Stack, FormControlLabel, Switch } from "@mui/material";
@@ -44,7 +45,16 @@ export default function Form({ values }: IFormProps) {
// Get initial values from fields config. This wont be written to the db
// when the SideDrawer is opened. Only dirty fields will be written
const initialValues = fields.reduce(
(a, { key, type }) => ({ ...a, [key]: getFieldProp("initialValue", type) }),
(a, { key, type }) => {
const initialValue = getFieldProp("initialValue", type);
const nextValues = { ...a };
if (key.indexOf('.') !== -1) {
_set(nextValues, key, initialValue);
} else {
nextValues[key] = initialValue;
}
return nextValues;
},
{}
);
const { ref: docRef, ...rowValues } = values;

View File

@@ -19,6 +19,7 @@ import FiltersPopover from "./FiltersPopover";
import FilterInputs from "./FilterInputs";
import { useFilterInputs, INITIAL_QUERY } from "./useFilterInputs";
import { analytics } from "@src/analytics";
import type { TableFilter } from "@src/hooks/useTable";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { useAppContext } from "@src/contexts/AppContext";
@@ -30,6 +31,11 @@ const shouldDisableApplyButton = (value: any) =>
typeof value !== "number" &&
typeof value !== "object";
enum FilterType {
yourFilter = "local_filter",
tableFilter = "table_filter",
}
export default function Filters() {
const { table, tableState, tableActions } = useProjectContext();
const { userDoc, userClaims } = useAppContext();
@@ -109,12 +115,14 @@ export default function Filters() {
// Save table filters to table schema document
const setTableFilters = (filters: TableFilter[]) => {
analytics.logEvent(FilterType.tableFilter);
tableActions?.table.updateConfig("filters", filters);
tableActions?.table.updateConfig("filtersOverridable", canOverrideCheckbox);
};
// Save user filters to user document
// null overrides table filters
const setUserFilters = (filters: TableFilter[] | null) => {
analytics.logEvent(FilterType.yourFilter);
userDoc.dispatch({
action: DocActions.update,
data: {

View File

@@ -1,8 +1,9 @@
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useRef } from "react";
import clsx from "clsx";
import parse from "csv-parse";
import { useDropzone } from "react-dropzone";
import { useDebouncedCallback } from "use-debounce";
import { useSnackbar } from "notistack";
import { makeStyles, createStyles } from "@mui/styles";
import {
@@ -27,6 +28,7 @@ import ImportIcon from "@src/assets/icons/Import";
import FileUploadIcon from "@src/assets/icons/Upload";
import CheckIcon from "@mui/icons-material/CheckCircle";
import { analytics } from "@src/analytics";
import ImportCsvWizard, {
IImportCsvWizardProps,
} from "@src/components/Wizards/ImportCsvWizard";
@@ -79,6 +81,17 @@ const useStyles = makeStyles((theme) =>
})
);
export enum ImportType {
csv = "csv",
tsv = "tsv",
}
export enum ImportMethod {
paste = "paste",
upload = "upload",
url = "url",
}
export interface IImportCsvProps {
render?: (
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
@@ -90,7 +103,10 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
const classes = useStyles();
const { userClaims } = useAppContext();
const { table } = useProjectContext();
const { enqueueSnackbar } = useSnackbar();
const importTypeRef = useRef(ImportType.csv);
const importMethodRef = useRef(ImportMethod.upload);
const [open, setOpen] = useState<HTMLButtonElement | null>(null);
const [tab, setTab] = useState("upload");
const [csvData, setCsvData] =
@@ -128,21 +144,53 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
});
const onDrop = useCallback(async (acceptedFiles) => {
const file = acceptedFiles[0];
const reader = new FileReader();
reader.onload = (event: any) => parseCsv(event.target.result);
reader.readAsText(file);
try {
const file = acceptedFiles[0];
const reader = new FileReader();
reader.onload = (event: any) => parseCsv(event.target.result);
reader.readAsText(file);
importTypeRef.current =
file.type === "text/tab-separated-values"
? ImportType.tsv
: ImportType.csv;
} catch (error) {
enqueueSnackbar(`Please import a .tsv or .csv file`, {
variant: "error",
anchorOrigin: {
vertical: "top",
horizontal: "center",
},
});
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
accept: ["text/csv", "text/tab-separated-values"],
});
const [handlePaste] = useDebouncedCallback(
(value: string) => parseCsv(value),
1000
);
function setDataTypeRef(data: string) {
const getFirstLine = data?.match(/^(.*)/)?.[0];
/**
* Catching edge case with regex
* EG: "hello\tworld"\tFirst
* - find \t between quotes, and replace with '\s'
* - w/ the \t pattern test it against the formatted string
*/
const strInQuotes = /"(.*?)"/;
const tabsWithSpace = (str: string) => str.replace("\t", "s");
const formatString =
getFirstLine?.replace(strInQuotes, tabsWithSpace) ?? "";
const tabPattern = /\t/;
return tabPattern.test(formatString)
? (importTypeRef.current = ImportType.tsv)
: (importTypeRef.current = ImportType.csv);
}
const [handlePaste] = useDebouncedCallback((value: string) => {
parseCsv(value);
setDataTypeRef(value);
}, 1000);
const [loading, setLoading] = useState(false);
const [handleUrl] = useDebouncedCallback((value: string) => {
@@ -152,6 +200,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
.then((res) => res.text())
.then((data) => {
parseCsv(data);
setDataTypeRef(data);
setLoading(false);
})
.catch((e) => {
@@ -204,9 +253,21 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
}
variant="fullWidth"
>
<Tab label="Upload" value="upload" />
<Tab label="Paste" value="paste" />
<Tab label="URL" value="url" />
<Tab
label="Upload"
value="upload"
onClick={() => (importMethodRef.current = ImportMethod.upload)}
/>
<Tab
label="Paste"
value="paste"
onClick={() => (importMethodRef.current = ImportMethod.paste)}
/>
<Tab
label="URL"
value="url"
onClick={() => (importMethodRef.current = ImportMethod.url)}
/>
</TabList>
<Divider style={{ marginTop: -1 }} />
@@ -295,7 +356,12 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
color="primary"
disabled={!validCsv}
className={classes.continueButton}
onClick={() => setOpenWizard(true)}
onClick={() => {
setOpenWizard(true);
analytics.logEvent(`import_${importMethodRef.current}`, {
type: importTypeRef.current,
});
}}
>
Continue
</Button>
@@ -303,6 +369,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
{openWizard && csvData && (
<ImportCsvWizard
importType={importTypeRef.current}
handleClose={() => setOpenWizard(false)}
csvData={csvData}
/>

View File

@@ -6,6 +6,7 @@ import { useSnackbar } from "notistack";
import { MenuItem, DialogContentText, LinearProgress } from "@mui/material";
import { analytics } from "@src/analytics";
import Modal from "@src/components/Modal";
import CodeEditor from "@src/components/CodeEditor";
@@ -49,6 +50,7 @@ export default function ExportSettings({
const { enqueueSnackbar } = useSnackbar();
const handleExport = () => {
analytics.logEvent("export_tableSettings");
navigator.clipboard.writeText(formattedJson);
enqueueSnackbar("Copied to clipboard");
handleClose();

View File

@@ -8,6 +8,7 @@ import { useSnackbar } from "notistack";
import { MenuItem, DialogContentText, FormHelperText } from "@mui/material";
import { analytics } from "@src/analytics";
import Modal from "@src/components/Modal";
import DiffEditor from "@src/components/CodeEditor/DiffEditor";
@@ -59,8 +60,8 @@ export default function ImportSettings({
const { enqueueSnackbar } = useSnackbar();
const { setValue } = useFormMethods;
const handleImport = () => {
analytics.logEvent("import_tableSettings");
const { id, collection, ...newValues } = JSON.parse(newSettings);
for (const key in newValues) {
setValue(key, newValues[key], {
shouldDirty: true,

View File

@@ -21,6 +21,7 @@ import { ColumnConfig } from "@src/hooks/useTable/useTableConfig";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { getFieldProp } from "@src/components/fields";
import { analytics } from "@src/analytics";
import { ImportType } from "@src/components/TableHeader/ImportCsv";
export type CsvConfig = {
pairs: { csvKey: string; columnKey: string }[];
@@ -36,6 +37,7 @@ export interface IStepProps {
}
export interface IImportCsvWizardProps {
importType: ImportType;
handleClose: () => void;
csvData: {
columns: string[];
@@ -44,6 +46,7 @@ export interface IImportCsvWizardProps {
}
export default function ImportCsvWizard({
importType,
handleClose,
csvData,
}: IImportCsvWizardProps) {
@@ -96,7 +99,7 @@ export default function ImportCsvWizard({
for (const col of config.newColumns) {
tableActions.column.add(col.name, col.type, col);
}
analytics.logEvent("import_csv");
analytics.logEvent("import_success", { type: importType }); //change this import_success
// Close wizard
setOpen(false);
setTimeout(handleClose, 300);

View File

@@ -31,7 +31,7 @@ import Json from "./Json";
import Code from "./Code";
import Action from "./Action";
import Derivative from "./Derivative";
import Aggregate from "./Aggregate";
// import Aggregate from "./Aggregate";
import CreatedBy from "./CreatedBy";
import UpdatedBy from "./UpdatedBy";
import CreatedAt from "./CreatedAt";
@@ -77,7 +77,7 @@ export const FIELDS: IFieldConfig[] = [
// CLOUD FUNCTION
Action,
Derivative,
Aggregate,
// Aggregate,
Status,
// AUDITING
CreatedBy,

View File

@@ -3753,25 +3753,25 @@ acorn-walk@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn-walk@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^6.4.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^7.1.0, acorn@^7.4.0:
acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
acorn@^8.2.4:
version "8.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
acorn@^8.2.4, acorn@^8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
address@1.1.2, address@^1.0.1:
version "1.1.2"
@@ -8011,9 +8011,9 @@ fn.name@1.x.x:
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.0.0:
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
for-in@^1.0.2:
version "1.0.2"
@@ -14052,9 +14052,9 @@ querystring@^0.2.0:
integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==
querystringify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
queue-microtask@^1.2.2:
version "1.2.3"
@@ -17108,18 +17108,10 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
url-parse@^1.4.3:
version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"
url-parse@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b"
integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==
url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"
@@ -17320,9 +17312,12 @@ vm-browserify@^1.0.1:
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vm2@^3.9.3:
version "3.9.5"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496"
integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==
version "3.9.7"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.7.tgz#bb87aa677c97c61e23a6cb6547e44e990517a6f6"
integrity sha512-g/GZ7V0Mlmch3eDVOATvAXr1GsJNg6kQ5PjvYy3HbJMCRn5slNbo/u73Uy7r5yUej1cRa3ZjtoVwcWSQuQ/fow==
dependencies:
acorn "^8.7.0"
acorn-walk "^8.2.0"
w3c-hr-time@^1.0.2:
version "1.0.2"