mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
add atoms to update public, project, user settings
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
1
src/atoms/types.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type UpdateFunction<T> = (update: Partial<T>) => Promise<any>;
|
||||
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 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<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 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<T = DocumentData>(
|
||||
disableSuspense,
|
||||
createIfNonExistent,
|
||||
handleError,
|
||||
updateDataAtom,
|
||||
setUpdateDataAtom,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user