add atoms to update public, project, user settings

This commit is contained in:
Sidney Alcantara
2022-04-29 15:42:26 +10:00
parent 89eeaca081
commit 953652776c
13 changed files with 120 additions and 55 deletions

View File

@@ -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({
<HelmetProvider>
<Provider scope={globalScope} initialValues={initialAtomValues}>
<CacheProvider value={muiCache}>
<ThemeProvider>
<RowyThemeProvider>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<SnackbarProvider>
<Suspense fallback={<Loading fullScreen />}>
@@ -37,7 +37,7 @@ export default function Providers({
</Suspense>
</SnackbarProvider>
</ErrorBoundary>
</ThemeProvider>
</RowyThemeProvider>
</CacheProvider>
</Provider>
</HelmetProvider>

View File

@@ -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<string>("");
@@ -24,10 +25,13 @@ export type PublicSettings = Partial<{
}>;
/** Public settings are visible to unauthenticated users */
export const publicSettingsAtom = atom<PublicSettings>({});
/** Stores a function that updates public settings */
export const updatePublicSettingsAtom =
atom<UpdateFunction<PublicSettings> | null>(null);
/** Project settings are visible to authenticated users */
export type ProjectSettings = Partial<{
tables: Array<TableSettings>;
tables: TableSettings[];
setupCompleted: boolean;
@@ -42,6 +46,9 @@ export type ProjectSettings = Partial<{
}>;
/** Project settings are visible to authenticated users */
export const projectSettingsAtom = atom<ProjectSettings>({});
/** Stores a function that updates project settings */
export const updateProjectSettingsAtom =
atom<UpdateFunction<ProjectSettings> | null>(null);
/** Table settings stored in project settings */
export type TableSettings = {

1
src/atoms/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type UpdateFunction<T> = (update: Partial<T>) => Promise<any>;

View File

@@ -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<UserSettings>({});
/** Stores a function that updates user settings */
export const updateUserSettingsAtom = atom<UpdateFunction<UserSettings> | null>(
null
);
/**
* Stores which theme is currently active, based on user or OS setting.

View File

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

View File

@@ -33,7 +33,7 @@ export default function Personalization({
<FormControlLabel
control={
<Checkbox
checked={customizedThemeColor}
defaultChecked={customizedThemeColor}
onChange={(e) => {
setCustomizedThemeColor(e.target.checked);
if (!e.target.checked) {
@@ -50,7 +50,7 @@ export default function Personalization({
/>
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
<Suspense fallback={<Loading />}>
<Suspense fallback={<Loading style={{ height: "auto" }} />}>
<ThemeColorPicker
currentLight={settings.theme?.light?.palette?.primary?.main}
currentDark={settings.theme?.dark?.palette?.primary?.main}

View File

@@ -58,7 +58,7 @@ export default function Theme({
<FormControlLabel
control={
<Checkbox
checked={Boolean(settings.theme?.dark?.palette?.darker)}
defaultChecked={Boolean(settings.theme?.dark?.palette?.darker)}
onChange={(e) => {
updateSettings({
theme: merge(settings.theme, {

View File

@@ -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<T> {
disableSuspense?: boolean;
/** Optionally create the document if it doesnt exist with the following data */
createIfNonExistent?: T;
/** Set this atoms value to a function that updates the document. Uses same scope as `dataScope`. */
updateDataAtom?: PrimitiveAtom<UpdateFunction<T> | null>;
}
/**
@@ -45,11 +49,20 @@ export function useFirestoreDocWithAtom<T = DocumentData>(
) {
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<T = DocumentData>(
suspended = true;
}
const ref = doc(
firebaseDb,
path,
...((pathSegments as string[]) || [])
) as DocumentReference<T>;
const unsubscribe = onSnapshot(
doc(firebaseDb, path, ...((pathSegments as string[]) || [])),
ref,
(doc) => {
try {
if (!doc.exists() && !!createIfNonExistent) {
@@ -86,8 +105,19 @@ export function useFirestoreDocWithAtom<T = DocumentData>(
}
);
// If `options?.updateDataAtom` was passed,
// set the atoms 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 atoms value to prevent writes
if (updateDataAtom) setUpdateDataAtom(RESET);
};
}, [
firebaseDb,
@@ -98,6 +128,8 @@ export function useFirestoreDocWithAtom<T = DocumentData>(
disableSuspense,
createIfNonExistent,
handleError,
updateDataAtom,
setUpdateDataAtom,
]);
}

View File

@@ -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
</Button>
<MultiSelect
multiple={false}
onChange={console.log}
value="2"
options={new Array(10).fill(undefined).map((_, i) => i.toString())}
/>
<Button onClick={() => getIdTokenResult(currentUser!).then(console.log)}>
getIdTokenResult
</Button>

View File

@@ -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<string, any>) => {
// 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<string, any>) => {
// 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
);

View File

@@ -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<string, any>) => {
// 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(

View File

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

View File

@@ -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({
</Helmet>
)}
<MuiThemeProvider theme={customizedThemes[theme]}>
<ThemeProvider theme={customizedThemes[theme]}>
<Favicon />
<CssBaseline />
{children}
</MuiThemeProvider>
</ThemeProvider>
</>
);
}