{}}
+ disableBackdropClick
+ disableEscapeKeyDown
+ hideCloseButton
+ maxWidth="xs"
+ body={
+ <>
+
+
+ It looks like your Firestore database is configured for Firetable.
+ You can migrate your configuration, including your{" "}
+ {requiresMigration} tables, to {name}.
+
+
+
+ Alternatively, you can{" "}
+ {
+ requestConfirmation({
+ title: "Continue without migrating?",
+ body: `You will start a new ${name} project without your existing config or tables.`,
+ handleConfirm: freshStart,
+ });
+ }}
+ component="button"
+ color="inherit"
+ variant="body2"
+ display="inline"
+ style={{ verticalAlign: "baseline" }}
+ >
+ skip migration
+
+ .
+
+
+
+
+
+ 1. Update your Firestore Security Rules
+
+
+
+ Add the required rules to your Firestore Security Rules.
+
+
+
+ }
+ fullWidth
+ sx={{ mt: 1 }}
+ >
+ Copy Required Rules
+
+
+ }
+ fullWidth
+ sx={{ mt: 1 }}
+ >
+ Set Rules in Firebase Console
+
+
+
+
+ 2. Migrate your config and tables
+
+
+ {
+ setMigrationStatus("MIGRATING");
+ migrate()
+ .then(() => {
+ setMigrationStatus("COMPLETE");
+ setRequiresMigration(false);
+ })
+ .catch((e) => {
+ setMigrationStatus("IDLE");
+ alert(
+ "Failed to migrate. Please check you have set the Firestore Security Rules correctly.\n\n" +
+ e.message
+ );
+ });
+ }}
+ >
+ Migrate
+
+
+ >
+ }
+ />
+ );
+}
diff --git a/src/components/Settings/ProjectSettings/About.tsx b/src/components/Settings/ProjectSettings/About.tsx
new file mode 100644
index 00000000..e208bd84
--- /dev/null
+++ b/src/components/Settings/ProjectSettings/About.tsx
@@ -0,0 +1,208 @@
+import { useState, useCallback, useEffect } from "react";
+import createPersistedState from "use-persisted-state";
+import { differenceInDays } from "date-fns";
+
+import { Grid, Typography, Button, Link, Divider } from "@material-ui/core";
+import LoadingButton from "@material-ui/lab/LoadingButton";
+import Logo from "assets/Logo";
+import OpenInNewIcon from "@material-ui/icons/OpenInNew";
+
+import { name, version, repository } from "@root/package.json";
+import { projectId } from "@src/firebase";
+import WIKI_LINKS from "constants/wikiLinks";
+
+const useLastCheckedUpdateState = createPersistedState(
+ "__ROWY__LAST_CHECKED_UPDATE"
+);
+export const useLatestUpdateState = createPersistedState(
+ "__ROWY__LATEST_UPDATE"
+);
+
+export default function About() {
+ const [lastCheckedUpdate, setLastCheckedUpdate] =
+ useLastCheckedUpdateState();
+ const [latestUpdate, setLatestUpdate] = useLatestUpdateState>(null);
+
+ const [checkState, setCheckState] = useState(
+ null
+ );
+
+ const checkForUpdate = useCallback(async () => {
+ setCheckState("LOADING");
+
+ // https://docs.github.com/en/rest/reference/repos#get-the-latest-release
+ const endpoint = repository.url
+ .replace("github.com", "api.github.com/repos")
+ .replace(/.git$/, "/releases/latest");
+ try {
+ const res = await fetch(endpoint, {
+ headers: {
+ Accept: "application/vnd.github.v3+json",
+ },
+ });
+ const json = await res.json();
+
+ if (json.tag_name > "v" + version) {
+ setLatestUpdate(json);
+ setCheckState(null);
+ } else {
+ setCheckState("NO_UPDATE");
+ }
+
+ setLastCheckedUpdate(new Date().toISOString());
+ } catch (e) {
+ console.error(e);
+ setLatestUpdate(null);
+ setCheckState("NO_UPDATE");
+ }
+ }, [setLastCheckedUpdate, setLatestUpdate]);
+
+ // Check for new updates on page load, if last check was more than 7 days ago
+ useEffect(() => {
+ if (!lastCheckedUpdate) checkForUpdate();
+ else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7)
+ checkForUpdate();
+ }, [lastCheckedUpdate, checkForUpdate]);
+
+ // Verify latest update is not installed yet
+ useEffect(() => {
+ if (latestUpdate?.tag_name <= "v" + version) setLatestUpdate(null);
+ }, [latestUpdate, setLatestUpdate]);
+
+ return (
+ <>
+
+
+
+
+
+
+ GitHub
+
+
+
+
+ Release notes
+
+
+
+
+
+
+
+
+
+ {name} v{version}
+
+ {latestUpdate === null ? (
+
+ Up to date
+
+ ) : (
+
+ Update available: {latestUpdate.tag_name}
+
+
+ )}
+
+
+
+ {latestUpdate === null ? (
+
+ Check for Updates
+
+ ) : (
+ }
+ >
+ How to Update
+
+ )}
+
+
+
+
+
+
+
+
+
+ Firebase Project: {projectId}
+
+
+
+
+ Firebase Console
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/Settings/ProjectSettings/Authentication.tsx b/src/components/Settings/ProjectSettings/Authentication.tsx
new file mode 100644
index 00000000..bc63bf71
--- /dev/null
+++ b/src/components/Settings/ProjectSettings/Authentication.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import { authOptions } from "firebase/firebaseui";
+import _startCase from "lodash/startCase";
+
+import MultiSelect from "@antlerengineering/multiselect";
+import { Typography, Link } from "@material-ui/core";
+import OpenInNewIcon from "@material-ui/icons/OpenInNew";
+
+import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
+
+export default function Authentication({
+ publicSettings,
+ updatePublicSettings,
+}: IProjectSettingsChildProps) {
+ const [signInOptions, setSignInOptions] = useState(
+ Array.isArray(publicSettings?.signInOptions)
+ ? publicSettings.signInOptions
+ : ["google"]
+ );
+
+ return (
+ <>
+ ({
+ value: option,
+ label: _startCase(option).replace("Github", "GitHub"),
+ }))}
+ onChange={setSignInOptions}
+ onClose={() => updatePublicSettings({ signInOptions })}
+ multiple
+ TextFieldProps={{ id: "signInOptions" }}
+ />
+
+
+ Before enabling a new sign-in option, make sure it’s configured in your
+ Firebase project.{" "}
+
+ How to configure sign-in options
+
+
+
+ >
+ );
+}
diff --git a/src/components/Settings/ProjectSettings/FunctionsBuilder.tsx b/src/components/Settings/ProjectSettings/FunctionsBuilder.tsx
new file mode 100644
index 00000000..66a10981
--- /dev/null
+++ b/src/components/Settings/ProjectSettings/FunctionsBuilder.tsx
@@ -0,0 +1,81 @@
+import { Typography, Link, Grid, TextField } from "@material-ui/core";
+import LoadingButton from "@material-ui/lab/LoadingButton";
+import OpenInNewIcon from "@material-ui/icons/OpenInNew";
+
+import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
+import WIKI_LINKS from "constants/wikiLinks";
+import { repository } from "@root/package.json";
+
+export default function FunctionsBuilder({
+ settings,
+ updateSettings,
+}: IProjectSettingsChildProps) {
+ return (
+ <>
+
+ Functions Builder is a Cloud Run instance that deploys this project’s
+ Cloud Functions.{" "}
+
+ Learn more
+
+
+
+
+
+
+
+
+ If you have not yet deployed Functions Builder, click this button
+ and follow the prompts on Cloud Shell.
+
+
+
+
+ }
+ loading={
+ settings.buildStatus === "BUILDING" ||
+ settings.buildStatus === "COMPLETE"
+ }
+ loadingIndicator={
+ settings.buildStatus === "COMPLETE" ? "Deployed" : undefined
+ }
+ >
+ Deploy to Cloud Run
+
+
+
+
+
+ updateSettings({ buildUrl: e.target.value })}
+ fullWidth
+ placeholder="https://.run.app"
+ type="url"
+ autoComplete="url"
+ />
+ >
+ );
+}
diff --git a/src/components/Settings/SettingsSection.tsx b/src/components/Settings/SettingsSection.tsx
new file mode 100644
index 00000000..751d4a7f
--- /dev/null
+++ b/src/components/Settings/SettingsSection.tsx
@@ -0,0 +1,36 @@
+import { Typography, Paper } from "@material-ui/core";
+
+export interface ISettingsSectionProps {
+ children: React.ReactNode;
+ title: string;
+}
+
+export default function SettingsSection({
+ children,
+ title,
+}: ISettingsSectionProps) {
+ return (
+
+
+ {title}
+
+ :not(style) + :not(style)": {
+ m: 0,
+ mt: { xs: 2, sm: 3 },
+ },
+ }}
+ >
+ {children}
+
+
+ );
+}
diff --git a/src/components/Settings/SettingsSkeleton.tsx b/src/components/Settings/SettingsSkeleton.tsx
new file mode 100644
index 00000000..4f6a87d6
--- /dev/null
+++ b/src/components/Settings/SettingsSkeleton.tsx
@@ -0,0 +1,59 @@
+import { Typography, Paper, Skeleton, Stack, Divider } from "@material-ui/core";
+
+export default function SettingsSkeleton() {
+ return (
+
+
+
+
+ :not(style) + :not(style)": {
+ m: 0,
+ mt: { xs: 2, sm: 3 },
+ },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Table/ColumnMenu/FieldSettings/index.tsx b/src/components/Table/ColumnMenu/FieldSettings/index.tsx
index 2867264a..290b8a6c 100644
--- a/src/components/Table/ColumnMenu/FieldSettings/index.tsx
+++ b/src/components/Table/ColumnMenu/FieldSettings/index.tsx
@@ -19,19 +19,13 @@ import { FieldType } from "constants/fields";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
-import Subheading from "components/Table/ColumnMenu/Subheading";
import Button from "@material-ui/core/Button";
import routes from "constants/routes";
+import { SETTINGS } from "config/dbPaths";
+
export default function FieldSettings(props: IMenuModalProps) {
- const {
- name,
- fieldName,
- type,
- open,
- config,
- handleClose,
- handleSave,
- } = props;
+ const { name, fieldName, type, open, config, handleClose, handleSave } =
+ props;
const [showRebuildPrompt, setShowRebuildPrompt] = useState(false);
const [newConfig, setNewConfig] = useState(config ?? {});
@@ -125,12 +119,11 @@ export default function FieldSettings(props: IMenuModalProps) {
if (showRebuildPrompt) {
requestConfirmation({
title: "Deploy Changes",
- body:
- "You have made changes that affect the behavior of the cloud function of this table, Would you like to redeploy it now?",
+ body: "You have made changes that affect the behavior of the cloud function of this table, Would you like to redeploy it now?",
confirm: "Deploy",
cancel: "Later",
handleConfirm: async () => {
- const settingsDoc = await db.doc("/_rowy_/settings").get();
+ const settingsDoc = await db.doc(SETTINGS).get();
const buildUrl = settingsDoc.get("buildUrl");
if (!buildUrl) {
snack.open({
@@ -149,7 +142,8 @@ export default function FieldSettings(props: IMenuModalProps) {
),
});
}
- const userTokenInfo = await appContext?.currentUser?.getIdTokenResult();
+ const userTokenInfo =
+ await appContext?.currentUser?.getIdTokenResult();
const userToken = userTokenInfo?.token;
try {
snackLog.requestSnackLog();
diff --git a/src/components/Table/TableHeader/Extensions/index.tsx b/src/components/Table/TableHeader/Extensions/index.tsx
index 55c8d27e..e75366d5 100644
--- a/src/components/Table/TableHeader/Extensions/index.tsx
+++ b/src/components/Table/TableHeader/Extensions/index.tsx
@@ -23,6 +23,7 @@ import {
IExtension,
IExtensionType,
} from "./utils";
+import { SETTINGS } from "config/dbPaths";
import WIKI_LINKS from "constants/wikiLinks";
export default function ExtensionsEditor() {
@@ -92,7 +93,7 @@ export default function ExtensionsEditor() {
const serialisedExtension = serialiseExtension(localExtensionsObjects);
tableActions?.table.updateConfig("extensions", serialisedExtension);
- const settingsDoc = await db.doc("/_rowy_/settings").get();
+ const settingsDoc = await db.doc(SETTINGS).get();
const buildUrl = settingsDoc.get("buildUrl");
if (!buildUrl) {
snack.open({
diff --git a/src/components/Table/TableHeader/TableLogs.tsx b/src/components/Table/TableHeader/TableLogs.tsx
index cdb6da9b..3c3740d6 100644
--- a/src/components/Table/TableHeader/TableLogs.tsx
+++ b/src/components/Table/TableHeader/TableLogs.tsx
@@ -38,6 +38,7 @@ import EmptyState from "components/EmptyState";
import PropTypes from "prop-types";
import routes from "constants/routes";
import { DATE_TIME_FORMAT } from "constants/dates";
+import { SETTINGS, TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "config/dbPaths";
function a11yProps(index) {
return {
@@ -183,9 +184,8 @@ function LogPanel(props) {
// useStateRef is necessary to resolve the state syncing issue
// https://stackoverflow.com/a/63039797/12208834
- const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = useStateRef(
- true
- );
+ const [liveStreaming, setLiveStreaming, liveStreamingStateRef] =
+ useStateRef(true);
const liveStreamingRef = useRef();
const isActive = value === index;
@@ -264,9 +264,8 @@ function SnackLog({ log, onClose, onOpenPanel }) {
const status = log?.status;
const classes = useStyles();
const [expanded, setExpanded] = useState(false);
- const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = useStateRef(
- true
- );
+ const [liveStreaming, setLiveStreaming, liveStreamingStateRef] =
+ useStateRef(true);
const liveStreamingRef = useRef();
const handleScroll = _throttle(() => {
@@ -403,7 +402,7 @@ export default function TableLogs() {
}, []);
const checkBuildURL = async () => {
- const settingsDoc = await db.doc("/_rowy_/settings").get();
+ const settingsDoc = await db.doc(SETTINGS).get();
const buildUrl = settingsDoc.get("buildUrl");
if (!buildUrl) {
setBuildURLConfigured(false);
@@ -412,8 +411,8 @@ export default function TableLogs() {
const tableCollection = decodeURIComponent(router.match.params.id);
const buildStreamID =
- "_rowy_/settings/" +
- `${isCollectionGroup() ? "groupSchema/" : "schema/"}` +
+ (isCollectionGroup() ? TABLE_GROUP_SCHEMAS : TABLE_SCHEMAS) +
+ "/" +
tableCollection
.split("/")
.filter(function (_, i) {
diff --git a/src/components/TableSettings/index.tsx b/src/components/TableSettings/index.tsx
index 7b7cabba..8edf9928 100644
--- a/src/components/TableSettings/index.tsx
+++ b/src/components/TableSettings/index.tsx
@@ -14,6 +14,7 @@ import { useRowyContext } from "contexts/RowyContext";
import useRouter from "../../hooks/useRouter";
import { db } from "../../firebase";
import { name } from "@root/package.json";
+import { SETTINGS, TABLE_SCHEMAS } from "config/dbPaths";
export enum TableSettingsDialogModes {
create,
@@ -123,13 +124,13 @@ export default function TableSettingsDialog({
};
const handleResetStructure = async () => {
- const schemaDocRef = db.doc(`_rowy_/settings/table/${data!.collection}`);
+ const schemaDocRef = db.doc(`${TABLE_SCHEMAS}/${data!.collection}`);
await schemaDocRef.update({ columns: {} });
handleClose();
};
const handleDelete = async () => {
- const tablesDocRef = db.doc(`_rowy_/settings`);
+ const tablesDocRef = db.doc(SETTINGS);
const tableData = (await tablesDocRef.get()).data();
const updatedTables = tableData?.tables.filter(
(table) =>
diff --git a/src/components/fields/ConnectTable/Settings.tsx b/src/components/fields/ConnectTable/Settings.tsx
index 5e5889c8..31479beb 100644
--- a/src/components/fields/ConnectTable/Settings.tsx
+++ b/src/components/fields/ConnectTable/Settings.tsx
@@ -9,6 +9,7 @@ import MultiSelect from "@antlerengineering/multiselect";
import { FieldType } from "constants/fields";
import { db } from "../../../firebase";
import { useRowyContext } from "contexts/RowyContext";
+import { TABLE_SCHEMAS } from "config/dbPaths";
export default function Settings({ handleChange, config }: ISettingsProps) {
const { tables } = useRowyContext();
@@ -24,7 +25,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
{ value: string; label: string; type: FieldType }[]
>([]);
const getColumns = async (table) => {
- const tableConfigDoc = await db.doc(`_rowy_/settings/table/${table}`).get();
+ const tableConfigDoc = await db.doc(`${TABLE_SCHEMAS}/${table}`).get();
const tableConfig = tableConfigDoc.data();
if (tableConfig && tableConfig.columns)
setColumns(
diff --git a/src/config/dbPaths.ts b/src/config/dbPaths.ts
new file mode 100644
index 00000000..661c6ff1
--- /dev/null
+++ b/src/config/dbPaths.ts
@@ -0,0 +1,7 @@
+export const SETTINGS = "_rowy_/settings";
+export const PUBLIC_SETTINGS = "_rowy_/publicSettings";
+
+export const TABLE_SCHEMAS = SETTINGS + "/schema";
+export const TABLE_GROUP_SCHEMAS = SETTINGS + "/groupSchema";
+
+export const USERS = SETTINGS + "/users";
diff --git a/src/constants/routes.ts b/src/constants/routes.ts
index 015d2044..6c323b4d 100644
--- a/src/constants/routes.ts
+++ b/src/constants/routes.ts
@@ -18,6 +18,7 @@ export enum routes {
settings = "/settings",
userSettings = "/settings/user",
projectSettings = "/settings/project",
+ userManagement = "/settings/userManagement",
}
export default routes;
diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx
index de60cc70..3dfd566e 100644
--- a/src/contexts/AppContext.tsx
+++ b/src/contexts/AppContext.tsx
@@ -14,6 +14,7 @@ import Themes from "Themes";
import ErrorBoundary from "components/ErrorBoundary";
import { name } from "@root/package.json";
+import { USERS } from "config/dbPaths";
const useThemeState = createPersistedState("__ROWY__THEME");
const useThemeOverriddenState = createPersistedState(
@@ -62,7 +63,7 @@ export const AppProvider: React.FC = ({ children }) => {
if (currentUser) {
analytics.setUserId(currentUser.uid);
analytics.setUserProperties({ instance: window.location.hostname });
- dispatchUserDoc({ path: `_rowy_/settings/users/${currentUser.uid}` });
+ dispatchUserDoc({ path: `${USERS}/${currentUser.uid}` });
}
}, [currentUser]);
diff --git a/src/hooks/useDoc.ts b/src/hooks/useDoc.ts
index 0b2cc1aa..0bd7e1d7 100644
--- a/src/hooks/useDoc.ts
+++ b/src/hooks/useDoc.ts
@@ -15,7 +15,10 @@ const documentInitialState = {
error: null,
};
-const useDoc = (initialOverrides: any) => {
+const useDoc = (
+ initialOverrides: any,
+ options: { createIfMissing?: boolean } = {}
+) => {
const documentReducer = (prevState: any, newProps: any) => {
switch (newProps.action) {
case DocActions.clear:
@@ -43,7 +46,7 @@ const useDoc = (initialOverrides: any) => {
...initialOverrides,
});
- const setDocumentListner = () => {
+ const setDocumentListener = () => {
documentDispatch({ prevPath: documentState.path });
const unsubscribe = db.doc(documentState.path).onSnapshot(
(snapshot) => {
@@ -58,9 +61,17 @@ const useDoc = (initialOverrides: any) => {
loading: false,
});
} else {
- documentDispatch({
- loading: false,
- });
+ if (options.createIfMissing)
+ try {
+ db.doc(documentState.path).set({}, { merge: true });
+ } catch (e) {
+ console.error(
+ `Could not create ${documentState.path}`,
+ e.message
+ );
+ }
+
+ documentDispatch({ loading: false });
}
},
(error) => {
@@ -76,7 +87,7 @@ const useDoc = (initialOverrides: any) => {
const { path, prevPath, unsubscribe } = documentState;
if (path && path !== prevPath) {
if (unsubscribe) unsubscribe();
- setDocumentListner();
+ setDocumentListener();
}
}, [documentState]);
useEffect(
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 7cce5672..7b7191e8 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -1,11 +1,10 @@
import { useEffect } from "react";
import useDoc from "./useDoc";
import { db } from "../firebase";
+import { SETTINGS, TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "config/dbPaths";
const useSettings = () => {
- const [settingsState, documentDispatch] = useDoc({
- path: "_rowy_/settings",
- });
+ const [settingsState, documentDispatch] = useDoc({ path: SETTINGS });
useEffect(() => {
//updates tables data on document change
const { doc, tables } = settingsState;
@@ -31,22 +30,26 @@ const useSettings = () => {
}) => {
const { tables } = settingsState;
const { schemaSource, ...tableSettings } = data;
- const tableSchemaPath = `_rowy_/settings/${
- tableSettings.tableType !== "collectionGroup" ? "schema" : "groupSchema"
+ const tableSchemaPath = `${
+ tableSettings.tableType !== "collectionGroup"
+ ? TABLE_SCHEMAS
+ : TABLE_GROUP_SCHEMAS
}/${tableSettings.collection}`;
const tableSchemaDocRef = db.doc(tableSchemaPath);
let columns = {};
if (schemaSource) {
- const schemaSourcePath = `_rowy_/settings/${
- schemaSource.tableType !== "collectionGroup" ? "schema" : "groupSchema"
+ const schemaSourcePath = `${
+ tableSettings.tableType !== "collectionGroup"
+ ? TABLE_SCHEMAS
+ : TABLE_GROUP_SCHEMAS
}/${schemaSource.collection}`;
const sourceDoc = await db.doc(schemaSourcePath).get();
columns = sourceDoc.get("columns");
}
// updates the setting doc
await db
- .doc("_rowy_/settings")
+ .doc(SETTINGS)
.set(
{ tables: tables ? [...tables, tableSettings] : [tableSettings] },
{ merge: true }
@@ -65,7 +68,7 @@ const useSettings = () => {
const { tables } = settingsState;
const table = tables.filter((t) => t.collection === data.collection)[0];
return Promise.all([
- db.doc("_rowy_/settings").set(
+ db.doc(SETTINGS).set(
{
tables: tables
? [
@@ -80,7 +83,7 @@ const useSettings = () => {
),
//update the rowy collection doc with empty columns
db
- .collection("_rowy_/settings/table")
+ .collection(TABLE_SCHEMAS)
.doc(data.collection)
.set({ ...data }, { merge: true }),
]);
@@ -88,10 +91,10 @@ const useSettings = () => {
const deleteTable = (collection: string) => {
const { tables } = settingsState;
- db.doc("_rowy_/settings").update({
+ db.doc(SETTINGS).update({
tables: tables.filter((table) => table.collection !== collection),
});
- db.collection("_rowy_/settings/table").doc(collection).delete();
+ db.collection(TABLE_SCHEMAS).doc(collection).delete();
};
const settingsActions = { createTable, updateTable, deleteTable };
return [settingsState, settingsActions];
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 533676c6..569c00ed 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
+import queryString from "query-string";
import _find from "lodash/find";
import { makeStyles, createStyles } from "@material-ui/styles";
@@ -11,17 +12,17 @@ import {
Checkbox,
Tooltip,
IconButton,
+ Link,
} from "@material-ui/core";
-
import AddIcon from "@material-ui/icons/Add";
-// import SettingsIcon from "@material-ui/icons/Settings";
import EditIcon from "@material-ui/icons/Edit";
import Favorite from "@material-ui/icons/Favorite";
import FavoriteBorder from "@material-ui/icons/FavoriteBorder";
+import SecurityIcon from "@material-ui/icons/SecurityOutlined";
-import Navigation from "components/Navigation";
-import Logo from "assets/Logo";
import StyledCard from "components/StyledCard";
+import HomeWelcomePrompt from "components/HomeWelcomePrompt";
+import EmptyState from "components/EmptyState";
import routes from "constants/routes";
import { useRowyContext } from "contexts/RowyContext";
@@ -31,12 +32,8 @@ import TableSettingsDialog, {
TableSettingsDialogModes,
} from "components/TableSettings";
-import queryString from "query-string";
-import ProjectSettings from "components/ProjectSettings";
-import EmptyState from "components/EmptyState";
import WIKI_LINKS from "constants/wikiLinks";
-import BuilderInstaller from "../components/BuilderInstaller";
-import HomeWelcomePrompt from "components/HomeWelcomePrompt";
+import { SETTINGS } from "config/dbPaths";
const useStyles = makeStyles((theme) =>
createStyles({
@@ -136,9 +133,7 @@ export default function HomePage() {
const [openProjectSettings, setOpenProjectSettings] = useState(false);
const [openBuilderInstaller, setOpenBuilderInstaller] = useState(false);
- const [settingsDocState, settingsDocDispatch] = useDoc({
- path: "_rowy_/settings",
- });
+ const [settingsDocState, settingsDocDispatch] = useDoc({ path: SETTINGS });
useEffect(() => {
if (!settingsDocState.loading && !settingsDocState.doc) {
settingsDocDispatch({
@@ -151,22 +146,23 @@ export default function HomePage() {
return (
-
+
You do not have access to this project. Please contact the project
owner.
-
+
If you are the project owner, please follow{" "}
-
- the instructions
- {" "}
+ these instructions
+ {" "}
to set up the project rules.
>
@@ -228,125 +224,95 @@ export default function HomePage() {
};
return (
-
-
-
- }
- >
-
- {sections && Object.keys(sections).length > 0 ? (
-
- {favs.length !== 0 && (
-
+
+ {sections && Object.keys(sections).length > 0 ? (
+
+ {favs.length !== 0 && (
+
+
+ Favorites
+
+
+
+ {favs.map((table) => (
+
+ ))}
+
+
+ )}
+
+ {sections &&
+ Object.keys(sections).length > 0 &&
+ Object.keys(sections).map((sectionName) => (
+
- Favorites
+ {sectionName === "undefined" ? "Other" : sectionName}
+
+
- {favs.map((table) => (
-
+ {sections[sectionName].map((table, i) => (
+
))}
- )}
+ ))}
- {sections &&
- Object.keys(sections).length > 0 &&
- Object.keys(sections).map((sectionName) => (
-
-
- {sectionName === "undefined" ? "Other" : sectionName}
-
-
-
-
-
- {sections[sectionName].map((table, i) => (
-
- ))}
-
-
- ))}
-
-
-
-
-
-
-
- {/*
+
+
setOpenProjectSettings(true)}
+ onClick={handleCreateTable}
>
-
+
- */}
-
-
- ) : (
-
-
-
-
-
-
- )}
-
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
- {openProjectSettings && (
- setOpenProjectSettings(false)}
- handleOpenBuilderInstaller={() => setOpenBuilderInstaller(true)}
- />
- )}
- {openBuilderInstaller && (
- setOpenBuilderInstaller(false)} />
- )}
-
+
);
}
diff --git a/src/pages/Settings/ProjectSettings.tsx b/src/pages/Settings/ProjectSettings.tsx
index f88bc070..97aa2ce8 100644
--- a/src/pages/Settings/ProjectSettings.tsx
+++ b/src/pages/Settings/ProjectSettings.tsx
@@ -1,15 +1,95 @@
-import { Container, Typography } from "@material-ui/core";
+import { Container, Stack } from "@material-ui/core";
-import Navigation from "components/Navigation";
+import SettingsSkeleton from "components/Settings/SettingsSkeleton";
+import SettingsSection from "components/Settings/SettingsSection";
+import About from "components/Settings/ProjectSettings/About";
+import Authentication from "components/Settings/ProjectSettings/Authentication";
+import FunctionsBuilder from "components/Settings/ProjectSettings/FunctionsBuilder";
+
+import { SETTINGS, PUBLIC_SETTINGS } from "config/dbPaths";
+import useDoc from "hooks/useDoc";
+import { db } from "@src/firebase";
+import { useSnackContext } from "contexts/SnackContext";
+import { useDebouncedCallback } from "use-debounce";
+import { useEffect } from "react";
+
+export interface IProjectSettingsChildProps {
+ settings: Record;
+ updateSettings: (data: Record) => void;
+ publicSettings: Record;
+ updatePublicSettings: (data: Record) => void;
+}
export default function ProjectSettings() {
+ const snack = useSnackContext();
+
+ const [settingsState] = useDoc({ path: SETTINGS }, { createIfMissing: true });
+ const settings = settingsState.doc;
+ const [updateSettings, , callPending] = useDebouncedCallback(
+ (data: Record) =>
+ db
+ .doc(SETTINGS)
+ .update(data)
+ .then(() =>
+ snack.open({ message: "Saved", variant: "success", duration: 3000 })
+ ),
+ 1000
+ );
+
+ const [publicSettingsState] = useDoc(
+ { path: PUBLIC_SETTINGS },
+ { createIfMissing: true }
+ );
+ const publicSettings = publicSettingsState.doc;
+ const [updatePublicSettings, , callPendingPublic] = useDebouncedCallback(
+ (data: Record) =>
+ db
+ .doc(PUBLIC_SETTINGS)
+ .update(data)
+ .then(() =>
+ snack.open({ message: "Saved", variant: "success", duration: 3000 })
+ ),
+ 1000
+ );
+
+ const childProps: IProjectSettingsChildProps = {
+ settings,
+ updateSettings,
+ publicSettings,
+ updatePublicSettings,
+ };
+
+ useEffect(
+ () => () => {
+ callPending();
+ callPendingPublic();
+ },
+ []
+ );
+
return (
-
-
-
- Project Settings
-
-
-
+
+ {settingsState.loading || publicSettingsState.loading ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
);
}
diff --git a/src/pages/Settings/UserManagement.tsx b/src/pages/Settings/UserManagement.tsx
new file mode 100644
index 00000000..ab2099a0
--- /dev/null
+++ b/src/pages/Settings/UserManagement.tsx
@@ -0,0 +1,13 @@
+import { Container, Stack } from "@material-ui/core";
+
+import SettingsSection from "components/Settings/SettingsSection";
+
+export default function UserManagement() {
+ return (
+
+
+ TODO:
+
+
+ );
+}
diff --git a/src/pages/Settings/UserSettings.tsx b/src/pages/Settings/UserSettings.tsx
index a860977f..ca359bb2 100644
--- a/src/pages/Settings/UserSettings.tsx
+++ b/src/pages/Settings/UserSettings.tsx
@@ -1,15 +1,13 @@
-import { Container, Typography } from "@material-ui/core";
+import { Container, Stack } from "@material-ui/core";
-import Navigation from "components/Navigation";
+import SettingsSection from "components/Settings/SettingsSection";
export default function UserSettings() {
return (
-
-
-
- Settings
-
-
-
+
+
+ TODO:
+
+
);
}
diff --git a/src/pages/Table.tsx b/src/pages/Table.tsx
index deb1b53b..87a2ba5b 100644
--- a/src/pages/Table.tsx
+++ b/src/pages/Table.tsx
@@ -76,7 +76,11 @@ export default function TablePage() {
if (!tableState) return null;
return (
- }>
+ }
+ currentSection={currentSection}
+ currentTable={currentTable}
+ >
{tableState.loadingColumns && (
<>
diff --git a/src/theme/components.tsx b/src/theme/components.tsx
index 147606f9..a7e7ca14 100644
--- a/src/theme/components.tsx
+++ b/src/theme/components.tsx
@@ -64,7 +64,9 @@ export const components = (theme: Theme): ThemeOptions => {
padding: "0 var(--dialog-spacing)",
},
- paperWidthXs: { maxWidth: 360 },
+ paperWidthXs: {
+ [theme.breakpoints.up("sm")]: { maxWidth: 360 },
+ },
paperFullScreen: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
@@ -130,10 +132,6 @@ export const components = (theme: Theme): ThemeOptions => {
},
},
- MuiTypography: {
- defaultProps: { variant: "body2" },
- },
-
MuiTextField: {
defaultProps: {
variant: "filled",
@@ -208,6 +206,7 @@ export const components = (theme: Theme): ThemeOptions => {
...theme.typography.caption,
fontWeight: 500,
+ color: theme.palette.text.primary,
},
},
},
diff --git a/src/theme/typography.ts b/src/theme/typography.ts
index 0eab38fa..46a0bde7 100644
--- a/src/theme/typography.ts
+++ b/src/theme/typography.ts
@@ -144,5 +144,10 @@ export const typography = ({
lineHeight: 20 / 12,
},
},
+ components: {
+ MuiTypography: {
+ defaultProps: { variant: "body2" },
+ },
+ },
};
};
diff --git a/src/utils/fns.ts b/src/utils/fns.ts
index 144b9f6a..e83580f8 100644
--- a/src/utils/fns.ts
+++ b/src/utils/fns.ts
@@ -1,4 +1,6 @@
import _get from "lodash/get";
+import { TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "config/dbPaths";
+
/**
* reposition an element in an array
* @param arr array
@@ -26,14 +28,12 @@ export const arrayMover = (
return arr; // for testing purposes
};
-export const missingFieldsReducer = (data: any) => (
- acc: string[],
- curr: string
-) => {
- if (data[curr] === undefined) {
- return [...acc, curr];
- } else return acc;
-};
+export const missingFieldsReducer =
+ (data: any) => (acc: string[], curr: string) => {
+ if (data[curr] === undefined) {
+ return [...acc, curr];
+ } else return acc;
+ };
export const sanitiseCallableName = (name: string) => {
if (!name || typeof name !== "string") return "";
@@ -96,8 +96,8 @@ export const generateBiggerId = (id: string) => {
const formatPathRegex = /\/[^\/]+\/([^\/]+)/g;
export const formatPath = (tablePath: string) => {
- return `_rowy_/settings/${
- isCollectionGroup() ? "groupSchema" : "schema"
+ return `${
+ isCollectionGroup() ? TABLE_GROUP_SCHEMAS : TABLE_SCHEMAS
}/${tablePath.replace(formatPathRegex, "/subTables/$1")}`;
};