diff --git a/src/Providers.tsx b/src/Providers.tsx
index accfd85c..2093c1c5 100644
--- a/src/Providers.tsx
+++ b/src/Providers.tsx
@@ -6,7 +6,7 @@ import { Provider, Atom } from "jotai";
import { globalScope } from "@src/atoms/globalScope";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
-import ThemeProvider from "@src/theme/ThemeProvider";
+import RowyThemeProvider from "@src/theme/RowyThemeProvider";
import SnackbarProvider from "@src/contexts/SnackbarContext";
import { Suspense } from "react";
@@ -29,7 +29,7 @@ export default function Providers({
-
+
}>
@@ -37,7 +37,7 @@ export default function Providers({
-
+
diff --git a/src/atoms/project.ts b/src/atoms/project.ts
index bfe254b3..68277ed3 100644
--- a/src/atoms/project.ts
+++ b/src/atoms/project.ts
@@ -3,6 +3,7 @@ import { sortBy } from "lodash-es";
import { ThemeOptions } from "@mui/material";
import { userRolesAtom } from "./auth";
+import { UpdateFunction } from "./types";
export const projectIdAtom = atom("");
@@ -24,10 +25,13 @@ export type PublicSettings = Partial<{
}>;
/** Public settings are visible to unauthenticated users */
export const publicSettingsAtom = atom({});
+/** Stores a function that updates public settings */
+export const updatePublicSettingsAtom =
+ atom | null>(null);
/** Project settings are visible to authenticated users */
export type ProjectSettings = Partial<{
- tables: Array;
+ tables: TableSettings[];
setupCompleted: boolean;
@@ -42,6 +46,9 @@ export type ProjectSettings = Partial<{
}>;
/** Project settings are visible to authenticated users */
export const projectSettingsAtom = atom({});
+/** Stores a function that updates project settings */
+export const updateProjectSettingsAtom =
+ atom | null>(null);
/** Table settings stored in project settings */
export type TableSettings = {
diff --git a/src/atoms/types.d.ts b/src/atoms/types.d.ts
new file mode 100644
index 00000000..8d85b83e
--- /dev/null
+++ b/src/atoms/types.d.ts
@@ -0,0 +1 @@
+export type UpdateFunction = (update: Partial) => Promise;
diff --git a/src/atoms/user.ts b/src/atoms/user.ts
index bb390720..65db3bb7 100644
--- a/src/atoms/user.ts
+++ b/src/atoms/user.ts
@@ -6,6 +6,7 @@ import { ThemeOptions } from "@mui/material";
import themes from "@src/theme";
import { publicSettingsAtom } from "./project";
import { TableFilter } from "./table";
+import { UpdateFunction } from "./types";
/** User info and settings */
export type UserSettings = Partial<{
@@ -32,6 +33,10 @@ export type UserSettings = Partial<{
}>;
/** User info and settings */
export const userSettingsAtom = atom({});
+/** Stores a function that updates user settings */
+export const updateUserSettingsAtom = atom | null>(
+ null
+);
/**
* Stores which theme is currently active, based on user or OS setting.
diff --git a/src/components/Settings/ThemeColorPicker.tsx b/src/components/Settings/ThemeColorPicker.tsx
index 46bb4fb7..35e00f0c 100644
--- a/src/components/Settings/ThemeColorPicker.tsx
+++ b/src/components/Settings/ThemeColorPicker.tsx
@@ -57,7 +57,7 @@ export default function ThemeColorPicker({
width={244}
height={140}
color={toColor("hex", light)}
- onChange={(c) => () => setLight(c.hex)}
+ onChange={(c) => setLight(c.hex)}
dark={theme.palette.mode === "dark"}
/>
diff --git a/src/components/Settings/UserSettings/Personalization.tsx b/src/components/Settings/UserSettings/Personalization.tsx
index 943d33a7..f6f2fc2c 100644
--- a/src/components/Settings/UserSettings/Personalization.tsx
+++ b/src/components/Settings/UserSettings/Personalization.tsx
@@ -33,7 +33,7 @@ export default function Personalization({
{
setCustomizedThemeColor(e.target.checked);
if (!e.target.checked) {
@@ -50,7 +50,7 @@ export default function Personalization({
/>
- }>
+ }>
{
updateSettings({
theme: merge(settings.theme, {
diff --git a/src/hooks/useFirestoreDocWithAtom.ts b/src/hooks/useFirestoreDocWithAtom.ts
index 014ade85..2322f423 100644
--- a/src/hooks/useFirestoreDocWithAtom.ts
+++ b/src/hooks/useFirestoreDocWithAtom.ts
@@ -1,17 +1,19 @@
import { useEffect } from "react";
import { useAtom, PrimitiveAtom } from "jotai";
import { Scope } from "jotai/core/atom";
-import { useUpdateAtom } from "jotai/utils";
+import { useUpdateAtom, RESET } from "jotai/utils";
import {
doc,
DocumentData,
onSnapshot,
FirestoreError,
setDoc,
+ DocumentReference,
} from "firebase/firestore";
import { useErrorHandler } from "react-error-boundary";
import { globalScope } from "@src/atoms/globalScope";
+import { UpdateFunction } from "@src/atoms/types";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
/** Options for {@link useFirestoreDocWithAtom} */
@@ -24,6 +26,8 @@ interface IUseFirestoreDocWithAtomOptions {
disableSuspense?: boolean;
/** Optionally create the document if it doesn’t exist with the following data */
createIfNonExistent?: T;
+ /** Set this atom’s value to a function that updates the document. Uses same scope as `dataScope`. */
+ updateDataAtom?: PrimitiveAtom | null>;
}
/**
@@ -45,11 +49,20 @@ export function useFirestoreDocWithAtom(
) {
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
const setDataAtom = useUpdateAtom(dataAtom, dataScope);
+ const setUpdateDataAtom = useUpdateAtom(
+ options?.updateDataAtom || (dataAtom as any),
+ globalScope
+ );
const handleError = useErrorHandler();
// Destructure options so they can be used as useEffect dependencies
- const { pathSegments, onError, disableSuspense, createIfNonExistent } =
- options || {};
+ const {
+ pathSegments,
+ onError,
+ disableSuspense,
+ createIfNonExistent,
+ updateDataAtom,
+ } = options || {};
useEffect(() => {
if (!path || (Array.isArray(pathSegments) && pathSegments.some((x) => !x)))
@@ -63,8 +76,14 @@ export function useFirestoreDocWithAtom(
suspended = true;
}
+ const ref = doc(
+ firebaseDb,
+ path,
+ ...((pathSegments as string[]) || [])
+ ) as DocumentReference;
+
const unsubscribe = onSnapshot(
- doc(firebaseDb, path, ...((pathSegments as string[]) || [])),
+ ref,
(doc) => {
try {
if (!doc.exists() && !!createIfNonExistent) {
@@ -86,8 +105,19 @@ export function useFirestoreDocWithAtom(
}
);
+ // If `options?.updateDataAtom` was passed,
+ // set the atom’s value to a function that updates the document
+ if (updateDataAtom) {
+ setUpdateDataAtom(
+ () => (update: T) => setDoc(ref, update, { merge: true })
+ );
+ }
+
return () => {
unsubscribe();
+ // If `options?.updateDataAtom` was passed,
+ // reset the atom’s value to prevent writes
+ if (updateDataAtom) setUpdateDataAtom(RESET);
};
}, [
firebaseDb,
@@ -98,6 +128,8 @@ export function useFirestoreDocWithAtom(
disableSuspense,
createIfNonExistent,
handleError,
+ updateDataAtom,
+ setUpdateDataAtom,
]);
}
diff --git a/src/pages/JotaiTest.tsx b/src/pages/JotaiTest.tsx
index e9e25e27..f94cce52 100644
--- a/src/pages/JotaiTest.tsx
+++ b/src/pages/JotaiTest.tsx
@@ -12,6 +12,7 @@ import {
} from "@src/atoms/globalScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
import { Button } from "@mui/material";
+import MultiSelect from "@rowy/multiselect";
import { useSnackbar } from "notistack";
import { useFirestoreDocWithAtom } from "hooks/useFirestoreDocWithAtom";
@@ -77,6 +78,13 @@ function JotaiTest() {
Sign out
+ i.toString())}
+ />
+
diff --git a/src/pages/Settings/ProjectSettings.tsx b/src/pages/Settings/ProjectSettings.tsx
index aed6424a..77fa5921 100644
--- a/src/pages/Settings/ProjectSettings.tsx
+++ b/src/pages/Settings/ProjectSettings.tsx
@@ -14,7 +14,9 @@ import Customization from "@src/components/Settings/ProjectSettings/Customizatio
import {
globalScope,
projectSettingsAtom,
+ updateProjectSettingsAtom,
publicSettingsAtom,
+ updatePublicSettingsAtom,
} from "@src/atoms/globalScope";
export interface IProjectSettingsChildProps {
@@ -30,20 +32,21 @@ export default function ProjectSettingsPage() {
const { enqueueSnackbar } = useSnackbar();
- const updateProjectSettings = useDebouncedCallback(
- (data: Record) => {
- // TODO:
- // db
- // .doc(path)
- // .update(data)
- // .then(() =>
-
- enqueueSnackbar("Saved", { variant: "success" });
-
- // )
- },
- 1000
+ const [_updateProjectSettingsDoc] = useAtom(
+ updateProjectSettingsAtom,
+ globalScope
);
+ const updateProjectSettings = useDebouncedCallback((data) => {
+ if (_updateProjectSettingsDoc) {
+ _updateProjectSettingsDoc(data).then(() =>
+ enqueueSnackbar("Saved", { variant: "success" })
+ );
+ } else {
+ enqueueSnackbar("Could not update project settings", {
+ variant: "error",
+ });
+ }
+ }, 1000);
// When the component is to be unmounted, force update settings
useEffect(
() => () => {
@@ -52,17 +55,21 @@ export default function ProjectSettingsPage() {
[updateProjectSettings]
);
+ const [_updatePublicSettingsDoc] = useAtom(
+ updatePublicSettingsAtom,
+ globalScope
+ );
const updatePublicSettings = useDebouncedCallback(
(data: Record) => {
- // TODO:
- // db
- // .doc(path)
- // .update(data)
- // .then(() =>
-
- enqueueSnackbar("Saved", { variant: "success" });
-
- // )
+ if (_updatePublicSettingsDoc) {
+ _updatePublicSettingsDoc(data).then(() =>
+ enqueueSnackbar("Saved", { variant: "success" })
+ );
+ } else {
+ enqueueSnackbar("Could not update public settings", {
+ variant: "error",
+ });
+ }
},
1000
);
diff --git a/src/pages/Settings/UserSettings.tsx b/src/pages/Settings/UserSettings.tsx
index ac65bb75..75c1e3e3 100644
--- a/src/pages/Settings/UserSettings.tsx
+++ b/src/pages/Settings/UserSettings.tsx
@@ -15,6 +15,7 @@ import {
globalScope,
currentUserAtom,
userSettingsAtom,
+ updateUserSettingsAtom,
} from "@src/atoms/globalScope";
export interface IUserSettingsChildProps {
@@ -27,16 +28,17 @@ export default function UserSettingsPage() {
const [userSettings] = useAtom(userSettingsAtom, globalScope);
const { enqueueSnackbar } = useSnackbar();
- const updateSettings = useDebouncedCallback((data: Record) => {
- // TODO:
- // db
- // .doc(path)
- // .update(data)
- // .then(() =>
-
- enqueueSnackbar("Saved", { variant: "success" });
-
- // )
+ const [_updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope);
+ const updateSettings = useDebouncedCallback((data) => {
+ if (_updateUserSettings) {
+ _updateUserSettings(data).then(() =>
+ enqueueSnackbar("Saved", { variant: "success" })
+ );
+ } else {
+ enqueueSnackbar("Could not update project settings", {
+ variant: "error",
+ });
+ }
}, 1000);
// When the component is to be unmounted, force update settings
useEffect(
diff --git a/src/sources/ProjectSourceFirebase.tsx b/src/sources/ProjectSourceFirebase.tsx
index 48f2edea..5f15f332 100644
--- a/src/sources/ProjectSourceFirebase.tsx
+++ b/src/sources/ProjectSourceFirebase.tsx
@@ -15,10 +15,13 @@ import {
globalScope,
projectIdAtom,
projectSettingsAtom,
+ updateProjectSettingsAtom,
publicSettingsAtom,
+ updatePublicSettingsAtom,
currentUserAtom,
userRolesAtom,
userSettingsAtom,
+ updateUserSettingsAtom,
UserSettings,
} from "@src/atoms/globalScope";
import { SETTINGS, PUBLIC_SETTINGS, USERS } from "@src/config/dbPaths";
@@ -120,17 +123,20 @@ export default function ProjectSourceFirebase() {
}, [firebaseAuth, setCurrentUser, setUserRoles]);
// Store public settings in atom
- useFirestoreDocWithAtom(publicSettingsAtom, globalScope, PUBLIC_SETTINGS);
+ useFirestoreDocWithAtom(publicSettingsAtom, globalScope, PUBLIC_SETTINGS, {
+ updateDataAtom: updatePublicSettingsAtom,
+ });
// Store public settings in atom when a user is signed in
useFirestoreDocWithAtom(
projectSettingsAtom,
globalScope,
- currentUser ? SETTINGS : undefined
+ currentUser ? SETTINGS : undefined,
+ { updateDataAtom: updateProjectSettingsAtom }
);
// Store user settings in atom when a user is signed in
- useFirestoreDocWithAtom(userSettingsAtom, globalScope, USERS, {
+ useFirestoreDocWithAtom(userSettingsAtom, globalScope, USERS, {
pathSegments: [currentUser?.uid],
createIfNonExistent: currentUser
? {
@@ -142,6 +148,7 @@ export default function ProjectSourceFirebase() {
},
}
: undefined,
+ updateDataAtom: updateUserSettingsAtom,
});
return null;
diff --git a/src/theme/ThemeProvider.tsx b/src/theme/RowyThemeProvider.tsx
similarity index 87%
rename from src/theme/ThemeProvider.tsx
rename to src/theme/RowyThemeProvider.tsx
index c9bb224c..c308694a 100644
--- a/src/theme/ThemeProvider.tsx
+++ b/src/theme/RowyThemeProvider.tsx
@@ -2,11 +2,7 @@ import { useEffect } from "react";
import { useAtom } from "jotai";
import { Helmet } from "react-helmet-async";
-import {
- useMediaQuery,
- ThemeProvider as MuiThemeProvider,
- CssBaseline,
-} from "@mui/material";
+import { useMediaQuery, ThemeProvider, CssBaseline } from "@mui/material";
import Favicon from "@src/assets/Favicon";
import { globalScope } from "@src/atoms/globalScope";
@@ -20,7 +16,7 @@ import {
* Injects the MUI theme with customizations from project and user settings.
* Also adds dark mode support.
*/
-export default function ThemeProvider({
+export default function RowyThemeProvider({
children,
}: React.PropsWithChildren<{}>) {
const [theme, setTheme] = useAtom(themeAtom, globalScope);
@@ -55,11 +51,11 @@ export default function ThemeProvider({
)}
-
+
{children}
-
+
>
);
}