[ENG-1007] Per-page onboarding forms (#1256)

* useMultiZodForm

* fix imports

* handle obStore.data undefined

---------

Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
Brendan AllanandUtku authored and GitHub committed 2023-08-29 10:58:39 +00:00
1 parent 08ba4f917a
commit 795bb18d18
33 files changed
+361 -264

No files matched your search

@@ -1,6 +1,5 @@
import { useLibraryMutation } from '@sd/client';
import { useLibraryMutation, useZodForm } from '@sd/client';
import { CheckBox, Dialog, Tooltip, UseDialogProps, useDialog } from '@sd/ui';
import { useZodForm } from '@sd/ui/src/forms';
interface Props extends UseDialogProps {
locationId: number;
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { FilePath, useLibraryMutation } from '@sd/client';
import { Dialog, Slider, UseDialogProps, useDialog } from '@sd/ui';
import { useZodForm, z } from '@sd/ui/src/forms';
import { FilePath, useLibraryMutation, useZodForm } from '@sd/client';
import { Dialog, Slider, UseDialogProps, useDialog, z } from '@sd/ui';
interface Props extends UseDialogProps {
locationId: number;
@@ -1,7 +1,7 @@
import clsx from 'clsx';
import { useState } from 'react';
import { Dialog, TextArea, UseDialogProps, useDialog } from '@sd/ui';
import { useZodForm, z } from '@sd/ui/src/forms';
import { useZodForm } from '@sd/client';
import { Dialog, TextArea, UseDialogProps, useDialog, z } from '@sd/ui';
import { showAlertDialog } from '~/components';
const schema = z.object({
@@ -2,8 +2,8 @@ import clsx from 'clsx';
import { useMotionValueEvent, useScroll } from 'framer-motion';
import { CheckCircle } from 'phosphor-react';
import { useEffect, useRef, useState } from 'react';
import { Themes, getThemeStore, useThemeStore } from '@sd/client';
import { Button, Form, SwitchField, useZodForm, z } from '@sd/ui';
import { Themes, getThemeStore, useThemeStore, useZodForm } from '@sd/client';
import { Button, Form, SwitchField, z } from '@sd/ui';
import { usePlatform } from '~/util/Platform';
import { Heading } from '../Layout';
import Setting from '../Setting';
@@ -1,7 +1,12 @@
import { Laptop } from '@sd/assets/icons';
import { getDebugState, useBridgeMutation, useBridgeQuery, useDebugState } from '@sd/client';
import { Button, Card, Input, Switch, tw } from '@sd/ui';
import { useZodForm, z } from '@sd/ui/src/forms';
import {
getDebugState,
useBridgeMutation,
useBridgeQuery,
useDebugState,
useZodForm
} from '@sd/client';
import { Button, Card, Input, Switch, tw, z } from '@sd/ui';
import { useDebouncedFormWatch } from '~/hooks';
import { usePlatform } from '~/util/Platform';
import { Heading } from '../Layout';
@@ -1,6 +1,5 @@
import { MaybeUndefined, useBridgeMutation, useLibraryContext } from '@sd/client';
import { Button, Input, Switch, Tooltip, dialogManager } from '@sd/ui';
import { useZodForm, z } from '@sd/ui/src/forms';
import { MaybeUndefined, useBridgeMutation, useLibraryContext, useZodForm } from '@sd/client';
import { Button, Input, Switch, Tooltip, dialogManager, z } from '@sd/ui';
import { useDebouncedFormWatch } from '~/hooks';
import { Heading } from '../Layout';
import Setting from '../Setting';
@@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react';
import { Suspense } from 'react';
import { Controller } from 'react-hook-form';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
import {
Button,
Divider,
@@ -14,7 +14,6 @@ import {
SwitchField,
Tooltip,
tw,
useZodForm,
z
} from '@sd/ui';
import ModalLayout from '~/app/$libraryId/settings/ModalLayout';
@@ -8,9 +8,10 @@ import {
extractInfoRSPCError,
useLibraryMutation,
useLibraryQuery,
usePlausibleEvent
usePlausibleEvent,
useZodForm
} from '@sd/client';
import { Dialog, ErrorMessage, InputField, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
import { Dialog, ErrorMessage, InputField, UseDialogProps, useDialog, z } from '@sd/ui';
import { showAlertDialog } from '~/components';
import { useCallbackToWatchForm } from '~/hooks';
import { Platform, usePlatform } from '~/util/Platform';
@@ -1,6 +1,5 @@
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { useLibraryMutation, usePlausibleEvent, useZodForm } from '@sd/client';
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
import { useZodForm } from '@sd/ui/src/forms';
interface Props extends UseDialogProps {
onSuccess: () => void;
@@ -8,10 +8,11 @@ import {
RuleKind,
UnionToTuple,
extractInfoRSPCError,
useLibraryMutation
useLibraryMutation,
useZodForm
} from '@sd/client';
import { Button, Card, Divider, Input, Select, SelectOption, Tooltip } from '@sd/ui';
import { ErrorMessage, Form, useZodForm, z } from '@sd/ui/src/forms';
import { ErrorMessage, Form, z } from '@sd/ui/src/forms';
import { InputKinds, RuleInput, validateInput } from './RuleInput';
const ruleKinds: UnionToTuple<RuleKind> = [
@@ -1,5 +1,5 @@
import { Object, useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { Dialog, InputField, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
import { Object, useLibraryMutation, usePlausibleEvent, useZodForm } from '@sd/client';
import { Dialog, InputField, UseDialogProps, useDialog, z } from '@sd/ui';
import { ColorPicker } from '~/components';
const schema = z.object({
@@ -1,6 +1,5 @@
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { useLibraryMutation, usePlausibleEvent, useZodForm } from '@sd/client';
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
import { useZodForm } from '@sd/ui/src/forms';
interface Props extends UseDialogProps {
tagId: number;
@@ -1,6 +1,6 @@
import { Trash } from 'phosphor-react';
import { Tag, useLibraryMutation } from '@sd/client';
import { Button, Form, InputField, Switch, Tooltip, dialogManager, useZodForm, z } from '@sd/ui';
import { Tag, useLibraryMutation, useZodForm } from '@sd/client';
import { Button, Form, InputField, Switch, Tooltip, dialogManager, z } from '@sd/ui';
import { ColorPicker } from '~/components';
import { useDebouncedFormWatch } from '~/hooks';
import Setting from '../../Setting';
@@ -1,7 +1,7 @@
import { useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { LibraryConfigWrapped, useBridgeMutation, usePlausibleEvent } from '@sd/client';
import { Dialog, InputField, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
import { LibraryConfigWrapped, useBridgeMutation, usePlausibleEvent, useZodForm } from '@sd/client';
import { Dialog, InputField, UseDialogProps, useDialog, z } from '@sd/ui';
const schema = z.object({
name: z
@@ -1,6 +1,6 @@
import { useQueryClient } from '@tanstack/react-query';
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
import { Dialog, UseDialogProps, useDialog, useZodForm } from '@sd/ui';
import { useBridgeMutation, usePlausibleEvent, useZodForm } from '@sd/client';
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
interface Props extends UseDialogProps {
libraryUuid: string;
+51 -41
View File
@@ -8,10 +8,11 @@ import {
telemetryStore,
useBridgeMutation,
useCachedLibraries,
useMultiZodForm,
useOnboardingStore,
usePlausibleEvent
} from '@sd/client';
import { RadioGroupField, useZodForm, z } from '@sd/ui';
import { RadioGroupField, z } from '@sd/ui';
export const OnboardingContext = createContext<ReturnType<typeof useContextValue> | null>(null);
@@ -46,21 +47,27 @@ export const shareTelemetry = RadioGroupField.options([
}
});
const schema = z.object({
name: z.string().min(1, 'Name is required').regex(/[\S]/g).trim(),
shareTelemetry: shareTelemetry.schema
});
const schemas = {
'new-library': z.object({
name: z.string().min(1, 'Name is required').regex(/[\S]/g).trim()
}),
'privacy': z.object({
shareTelemetry: shareTelemetry.schema
})
};
// this is a lot so it gets its own hook :)
const useFormState = () => {
const obStore = useOnboardingStore();
const form = useZodForm({
schema,
const { handleSubmit, ...forms } = useMultiZodForm({
schemas,
defaultValues: {
name: obStore.newLibraryName,
shareTelemetry: 'share-telemetry'
}
'new-library': obStore.data?.['new-library'] ?? undefined,
'privacy': obStore.data?.privacy ?? {
shareTelemetry: 'share-telemetry'
}
},
onData: (data) => (getOnboardingStore().data = data)
});
const navigate = useNavigate();
@@ -69,42 +76,45 @@ const useFormState = () => {
const createLibrary = useBridgeMutation('library.create');
const onSubmit = form.handleSubmit(async (data) => {
navigate('./creating-library', { replace: true });
const submit = handleSubmit(
async (data) => {
navigate('./creating-library', { replace: true });
// opted to place this here as users could change their mind before library creation/onboarding finalization
// it feels more fitting to configure it here (once)
telemetryStore.shareFullTelemetry = getOnboardingStore().shareFullTelemetry;
// opted to place this here as users could change their mind before library creation/onboarding finalization
// it feels more fitting to configure it here (once)
telemetryStore.shareFullTelemetry = data.privacy.shareTelemetry === 'share-telemetry';
try {
// show creation screen for a bit for smoothness
const [library] = await Promise.all([
createLibrary.mutateAsync({
name: data.name
}),
new Promise((res) => setTimeout(res, 500))
]);
try {
// show creation screen for a bit for smoothness
const [library] = await Promise.all([
createLibrary.mutateAsync({
name: data['new-library'].name
}),
new Promise((res) => setTimeout(res, 500))
]);
queryClient.setQueryData(['library.list'], (libraries: any) => [
...(libraries ?? []),
library
]);
queryClient.setQueryData(['library.list'], (libraries: any) => [
...(libraries ?? []),
library
]);
if (telemetryStore.shareFullTelemetry) {
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
if (telemetryStore.shareFullTelemetry) {
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
}
resetOnboardingStore();
navigate(`/${library.uuid}/overview`, { replace: true });
} catch (e) {
if (e instanceof Error) {
alert(`Failed to create library. Error: ${e.message}`);
}
navigate('./privacy');
}
},
(key) => navigate(`./${key}`)
);
resetOnboardingStore();
navigate(`/${library.uuid}/overview`, { replace: true });
} catch (e) {
if (e instanceof Error) {
alert(`Failed to create library. Error: ${e.message}`);
}
navigate('./privacy');
}
});
return { form, onSubmit };
return { submit, forms };
};
export const useOnboardingContext = () => {
+6 -12
View File
@@ -1,7 +1,6 @@
import { Database } from '@sd/assets/icons';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { getOnboardingStore } from '@sd/client';
import { Button, Form, InputField } from '@sd/ui';
import {
OnboardingContainer,
@@ -13,7 +12,8 @@ import { useOnboardingContext } from './context';
export default function OnboardingNewLibrary() {
const navigate = useNavigate();
const { form } = useOnboardingContext();
const form = useOnboardingContext().forms.useForm('new-library');
const [importMode, setImportMode] = useState(false);
const handleImport = () => {
@@ -23,11 +23,9 @@ export default function OnboardingNewLibrary() {
return (
<Form
form={form}
// manual onSubmit as we need to set the library name in the store
onSubmit={async () => {
getOnboardingStore().newLibraryName = form.getValues('name');
onSubmit={form.handleSubmit(() => {
navigate('../privacy', { replace: true });
}}
})}
>
<OnboardingContainer>
<OnboardingImg src={Database} />
@@ -53,17 +51,13 @@ export default function OnboardingNewLibrary() {
{...form.register('name')}
size="lg"
autoFocus
disabled={form.formState.isValid}
className="mt-6 w-[300px]"
placeholder={'e.g. "James\' Library"'}
/>
<div className="flex grow" />
<div className="mt-7 space-x-2">
<Button
type="submit"
variant="accent"
disabled={!form.formState.isValid}
size="sm"
>
<Button type="submit" variant="accent" size="sm">
New library
</Button>
{/* <span className="px-2 text-xs font-bold text-ink-faint">OR</span>
+4 -7
View File
@@ -1,19 +1,16 @@
import { Button, Form, RadioGroupField } from '@sd/ui';
import { getOnboardingStore } from '~/../packages/client/src';
import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './Layout';
import { shareTelemetry, useOnboardingContext } from './context';
export default function OnboardingPrivacy() {
const { form, onSubmit } = useOnboardingContext();
const { forms, submit } = useOnboardingContext();
const form = forms.useForm('privacy');
return (
<Form
form={form}
onSubmit={(e) => {
getOnboardingStore().shareFullTelemetry =
form.getValues('shareTelemetry') === 'share-telemetry';
return onSubmit(e);
}}
onSubmit={form.handleSubmit(submit)}
className="flex flex-col items-center"
>
<OnboardingContainer>
+1 -1
View File
@@ -5,6 +5,7 @@ import {
useDiscoveredPeers,
useFeatureFlag,
useP2PEvents,
useZodForm,
withFeatureFlag
} from '@sd/client';
import {
@@ -15,7 +16,6 @@ import {
UseDialogProps,
dialogManager,
useDialog,
useZodForm,
z
} from '@sd/ui';
import { getSpacedropState, subscribeSpacedropState } from '../../hooks/useSpacedropState';
+2 -2
View File
@@ -4,7 +4,8 @@ import {
OperatingSystem,
useBridgeMutation,
useCachedLibraries,
usePairingStatus
usePairingStatus,
useZodForm
} from '@sd/client';
import {
Button,
@@ -15,7 +16,6 @@ import {
UseDialogProps,
dialogManager,
useDialog,
useZodForm,
z
} from '@sd/ui';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Clipboard } from 'phosphor-react';
import { ReactNode } from 'react';
import { useZodForm } from '@sd/client';
import { Button, Dialog, Input, UseDialogProps, dialogManager, useDialog } from '@sd/ui';
import { useZodForm } from '@sd/ui/src/forms';
interface Props extends UseDialogProps {
title: string; // dialog title