Rename field components + asyncify onSubmits (#1046)

rename field components + asyncify onSubmits
This commit is contained in:
Brendan Allan 2023-06-28 11:44:40 +02:00 committed by GitHub
parent 444a6d23c0
commit d8210f13f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 118 additions and 101 deletions

View file

@ -3,7 +3,7 @@ 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, Slider, forms } from '@sd/ui';
import { Button, Form, SwitchField, useZodForm, z } from '@sd/ui';
import { usePlatform } from '~/util/Platform';
import { Heading } from '../Layout';
import Setting from '../Setting';
@ -19,8 +19,6 @@ type Theme = {
type ThemeProps = Theme & { isSelected?: boolean; className?: string };
const { Form, Switch, useZodForm, z } = forms;
const schema = z.object({
uiAnimations: z.boolean(),
syncThemeWithSystem: z.boolean(),
@ -192,7 +190,11 @@ export const Component = () => {
className="opacity-30"
description="Dialogs and other UI elements will animate when opening and closing."
>
<Switch disabled {...form.register('uiAnimations')} className="m-2 ml-4" />
<SwitchField
disabled
{...form.register('uiAnimations')}
className="m-2 ml-4"
/>
</Setting>
<Setting
@ -201,7 +203,11 @@ export const Component = () => {
className="opacity-30"
description="Some components will have a blur effect applied to them."
>
<Switch disabled {...form.register('blurEffects')} className="m-2 ml-4" />
<SwitchField
disabled
{...form.register('blurEffects')}
className="m-2 ml-4"
/>
</Setting>
</div>
</Form>

View file

@ -1,5 +1,5 @@
import { telemetryStore, useTelemetryState } from '@sd/client';
import { Switch } from '@sd/ui';
import { telemetryStore, useTelemetryState } from '~/../packages/client/src';
import { Heading } from '../Layout';
import Setting from '../Setting';

View file

@ -3,8 +3,20 @@ 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 { Button, Divider, Label, Tooltip, tw } from '@sd/ui';
import { Form, InfoText, Input, RadioGroup, Switch, useZodForm, z } from '@sd/ui/src/forms';
import {
Button,
Divider,
Form,
InfoText,
InputField,
Label,
RadioGroupField,
SwitchField,
Tooltip,
tw,
useZodForm,
z
} from '@sd/ui';
import ModalLayout from '~/app/$libraryId/settings/ModalLayout';
import { LocationIdParamsSchema } from '~/app/route-schemas';
import { showAlertDialog } from '~/components';
@ -124,14 +136,14 @@ const EditLocationForm = () => {
>
<div className="flex space-x-4">
<FlexCol>
<Input label="Display Name" {...form.register('name')} />
<InputField label="Display Name" {...form.register('name')} />
<InfoText className="mt-2">
The name of this Location, this is what will be displayed in the
sidebar. Will not rename the actual folder on disk.
</InfoText>
</FlexCol>
<FlexCol>
<Input
<InputField
label="Local Path"
readOnly={true}
className="text-ink-dull"
@ -146,46 +158,46 @@ const EditLocationForm = () => {
<Divider />
<div className="space-y-2">
<Label className="grow">Location Type</Label>
<RadioGroup.Root
<RadioGroupField.Root
className="flex flex-row !space-y-0 space-x-2"
{...form.register('locationType')}
>
<RadioGroup.Item key="normal" value="normal">
<RadioGroupField.Item key="normal" value="normal">
<h1 className="font-bold">Normal</h1>
<p className="text-sm text-ink-faint">
Contents will be indexed as-is, new files will not be automatically
sorted.
</p>
</RadioGroup.Item>
</RadioGroupField.Item>
<RadioGroup.Item disabled key="managed" value="managed">
<RadioGroupField.Item disabled key="managed" value="managed">
<h1 className="font-bold">Managed</h1>
<p className="text-sm text-ink-faint">
Spacedrive will sort files for you. If Location isn't empty a
"spacedrive" folder will be created.
</p>
</RadioGroup.Item>
</RadioGroupField.Item>
<RadioGroup.Item disabled key="replica" value="replica">
<RadioGroupField.Item disabled key="replica" value="replica">
<h1 className="font-bold">Replica</h1>
<p className="text-sm text-ink-faint ">
This Location is a replica of another, its contents will be
automatically synchronized.
</p>
</RadioGroup.Item>
</RadioGroup.Root>
</RadioGroupField.Item>
</RadioGroupField.Root>
</div>
<Divider />
<div className="space-y-2">
<ToggleSection>
<Label className="grow">Generate preview media for this Location</Label>
<Switch {...form.register('generatePreviewMedia')} size="sm" />
<SwitchField {...form.register('generatePreviewMedia')} size="sm" />
</ToggleSection>
<ToggleSection>
<Label className="grow">
Sync preview media for this Location with your devices
</Label>
<Switch {...form.register('syncPreviewMedia')} size="sm" />
<SwitchField {...form.register('syncPreviewMedia')} size="sm" />
</ToggleSection>
<ToggleSection>
<Label className="grow">
@ -194,7 +206,7 @@ const EditLocationForm = () => {
<Info className="inline" />
</Tooltip>
</Label>
<Switch {...form.register('hidden')} size="sm" />
<SwitchField {...form.register('hidden')} size="sm" />
</ToggleSection>
</div>
<Divider />

View file

@ -9,8 +9,7 @@ import {
useLibraryMutation,
useLibraryQuery
} from '@sd/client';
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
import { ErrorMessage, Input, useZodForm, z } from '@sd/ui/src/forms';
import { Dialog, ErrorMessage, InputField, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
import { showAlertDialog } from '~/components';
import { useCallbackToWatchForm } from '~/hooks';
import { Platform, usePlatform } from '~/util/Platform';
@ -210,7 +209,7 @@ export const AddLocationDialog = ({
>
<ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mb-4 mt-2" />
<Input
<InputField
size="md"
label="Path:"
onClick={() =>

View file

@ -1,6 +1,5 @@
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
import { Input, useZodForm, z } from '@sd/ui/src/forms';
import { Dialog, InputField, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
import { ColorPicker } from '~/components';
const schema = z.object({
@ -16,39 +15,38 @@ export default (props: UseDialogProps & { assignToObject?: number }) => {
defaultValues: { color: '#A717D9' }
});
const createTag = useLibraryMutation('tags.create', {
onSuccess: (tag) => {
const createTag = useLibraryMutation('tags.create');
const assignTag = useLibraryMutation('tags.assign');
const onSubmit = form.handleSubmit(async (data) => {
try {
const tag = await createTag.mutateAsync(data);
submitPlausibleEvent({ event: { type: 'tagCreate' } });
if (props.assignToObject !== undefined) {
assignTag.mutate({
await assignTag.mutateAsync({
tag_id: tag.id,
object_ids: [props.assignToObject],
unassign: false
});
}
},
onError: (e) => {
} catch (e) {
console.error('error', e);
}
});
const assignTag = useLibraryMutation('tags.assign', {
onSuccess: () => {
submitPlausibleEvent({ event: { type: 'tagAssign' } });
}
});
return (
<Dialog
form={form}
onSubmit={onSubmit}
dialog={useDialog(props)}
onSubmit={form.handleSubmit((data) => createTag.mutateAsync(data))}
title="Create New Tag"
description="Choose a name and color."
ctaLabel="Create"
>
<div className="relative mt-3 ">
<Input
<InputField
{...form.register('name', { required: true })}
placeholder="Name"
maxLength={24}

View file

@ -1,7 +1,6 @@
import { Trash } from 'phosphor-react';
import { Tag, useLibraryMutation } from '@sd/client';
import { Button, Switch, Tooltip, dialogManager } from '@sd/ui';
import { Form, Input, useZodForm, z } from '@sd/ui/src/forms';
import { Button, Form, InputField, Switch, Tooltip, dialogManager, useZodForm, z } from '@sd/ui';
import { ColorPicker } from '~/components';
import { useDebouncedFormWatch } from '~/hooks';
import Setting from '../../Setting';
@ -25,12 +24,14 @@ interface Props {
export default ({ tag, onDelete }: Props) => {
const updateTag = useLibraryMutation('tags.update');
const form = useZodForm({
schema,
mode: 'onChange',
defaultValues: tag,
reValidateMode: 'onChange'
});
useDebouncedFormWatch(form, (data) => {
updateTag.mutate({
name: data.name ?? null,
@ -43,7 +44,7 @@ export default ({ tag, onDelete }: Props) => {
<Form form={form}>
<div className="flex justify-between">
<div className="mb-10 flex flex-row space-x-3">
<Input
<InputField
label="Color"
maxLength={7}
value={form.watch('color')?.trim() ?? '#ffffff'}
@ -51,7 +52,7 @@ export default ({ tag, onDelete }: Props) => {
{...form.register('color')}
/>
<Input maxLength={24} label="Name" {...form.register('name')} />
<InputField maxLength={24} label="Name" {...form.register('name')} />
</div>
<Button
variant="gray"

View file

@ -1,9 +1,7 @@
import { useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { LibraryConfigWrapped, useBridgeMutation, usePlausibleEvent } from '@sd/client';
import { Dialog, UseDialogProps, forms, useDialog } from '@sd/ui';
const { Input, z, useZodForm } = forms;
import { Dialog, InputField, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
const schema = z.object({
name: z
@ -25,18 +23,22 @@ export default (props: UseDialogProps) => {
const form = useZodForm({ schema });
const onSubmit = form.handleSubmit(async (data) => {
const library = await createLibrary.mutateAsync({ name: data.name });
try {
const library = await createLibrary.mutateAsync({ name: data.name });
queryClient.setQueryData<LibraryConfigWrapped[]>(['library.list'], (libraries) => [
...(libraries || []),
library
]);
queryClient.setQueryData<LibraryConfigWrapped[]>(['library.list'], (libraries) => [
...(libraries || []),
library
]);
submitPlausibleEvent({
event: { type: 'libraryCreate' }
});
submitPlausibleEvent({
event: { type: 'libraryCreate' }
});
navigate(`/${library.uuid}/overview`);
navigate(`/${library.uuid}/overview`);
} catch (e) {
console.error(e);
}
});
return (
@ -50,7 +52,7 @@ export default (props: UseDialogProps) => {
ctaLabel={form.formState.isSubmitting ? 'Creating library...' : 'Create library'}
>
<div className="mt-5 space-y-4">
<Input
<InputField
{...form.register('name')}
label="Library name"
placeholder={'e.g. "James\' Library"'}

View file

@ -1,8 +1,6 @@
import { useQueryClient } from '@tanstack/react-query';
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
import { Dialog, UseDialogProps, forms, useDialog } from '@sd/ui';
const { useZodForm, z } = forms;
import { Dialog, UseDialogProps, useDialog, useZodForm, z } from '@sd/ui';
interface Props extends UseDialogProps {
libraryUuid: string;
@ -11,8 +9,15 @@ interface Props extends UseDialogProps {
export default function DeleteLibraryDialog(props: Props) {
const submitPlausibleEvent = usePlausibleEvent();
const queryClient = useQueryClient();
const deleteLib = useBridgeMutation('library.delete', {
onSuccess: () => {
const deleteLib = useBridgeMutation('library.delete');
const form = useZodForm();
const onSubmit = form.handleSubmit(async () => {
try {
await deleteLib.mutateAsync(props.libraryUuid);
queryClient.invalidateQueries(['library.list']);
submitPlausibleEvent({
@ -20,18 +25,15 @@ export default function DeleteLibraryDialog(props: Props) {
type: 'libraryDelete'
}
});
},
onError: (e) => {
} catch (e) {
alert(`Failed to delete library: ${e}`);
}
});
const form = useZodForm({ schema: z.object({}) });
return (
<Dialog
form={form}
onSubmit={form.handleSubmit(() => deleteLib.mutateAsync(props.libraryUuid))}
onSubmit={onSubmit}
dialog={useDialog(props)}
title="Delete Library"
description="Deleting a library will permanently the database, the files themselves will not be deleted."

View file

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import {
PeerMetadata,
useBridgeMutation,
useBridgeSubscription,
useDiscoveredPeers,
@ -8,17 +7,17 @@ import {
} from '@sd/client';
import {
Dialog,
InputField,
Select,
SelectOption,
UseDialogProps,
dialogManager,
forms,
useDialog
useDialog,
useZodForm,
z
} from '@sd/ui';
import { getSpacedropState, subscribeSpacedropState } from '../hooks/useSpacedropState';
const { Input, useZodForm, z } = forms;
export function SpacedropUI() {
const isSpacedropEnabled = useFeatureFlag('spacedrop');
if (!isSpacedropEnabled) {
@ -126,7 +125,7 @@ function SpacedropRequestDialog(
<div className="space-y-2 py-2">
<p>File Name: {props.name}</p>
<p>Peer Id: {props.peerId}</p>
<Input
<InputField
size="sm"
placeholder="/Users/oscar/Desktop/demo.txt"
className="w-full"

View file

@ -2,8 +2,7 @@ import { Database } from '@sd/assets/icons';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { getOnboardingStore, useOnboardingStore } from '@sd/client';
import { Button } from '@sd/ui';
import { Form, Input, useZodForm, z } from '@sd/ui/src/forms';
import { Button, Form, InputField, useZodForm, z } from '@sd/ui';
import {
OnboardingContainer,
OnboardingDescription,
@ -63,7 +62,7 @@ export default function OnboardingNewLibrary() {
</div>
) : (
<>
<Input
<InputField
{...form.register('name')}
size="lg"
autoFocus

View file

@ -1,11 +1,10 @@
import { useNavigate } from 'react-router';
import { getOnboardingStore } from '@sd/client';
import { Button } from '@sd/ui';
import { Form, RadioGroup, useZodForm, z } from '@sd/ui/src/forms';
import { Button, Form, RadioGroupField, useZodForm, z } from '@sd/ui';
import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './Layout';
import { useUnlockOnboardingScreen } from './Progress';
export const shareTelemetry = RadioGroup.options([
export const shareTelemetry = RadioGroupField.options([
z.literal('share-telemetry'),
z.literal('no-telemetry')
]).details({
@ -36,7 +35,7 @@ export default function OnboardingPrivacy() {
}
});
const onSubmit = form.handleSubmit(async (data) => {
const onSubmit = form.handleSubmit((data) => {
getOnboardingStore().shareTelemetry = data.shareTelemetry === 'share-telemetry';
navigate('/onboarding/creating-library', { replace: true });
@ -51,14 +50,14 @@ export default function OnboardingPrivacy() {
So we'll make it very clear what data is shared with us.
</OnboardingDescription>
<div className="m-4">
<RadioGroup.Root {...form.register('shareTelemetry')}>
<RadioGroupField.Root {...form.register('shareTelemetry')}>
{shareTelemetry.options.map(({ value, heading, description }) => (
<RadioGroup.Item key={value} value={value}>
<RadioGroupField.Item key={value} value={value}>
<h1 className="font-bold">{heading}</h1>
<p className="text-sm text-ink-faint">{description}</p>
</RadioGroup.Item>
</RadioGroupField.Item>
))}
</RadioGroup.Root>
</RadioGroupField.Root>
</div>
<Button className="text-center" type="submit" variant="accent" size="sm">
Continue

View file

@ -2,9 +2,9 @@ import { forwardRef } from 'react';
import { CheckBox as Root } from '../CheckBox';
import { FormField, UseFormFieldProps, useFormField } from './FormField';
export interface CheckBoxProps extends UseFormFieldProps {}
export interface CheckBoxFieldProps extends UseFormFieldProps {}
export const CheckBox = forwardRef<HTMLInputElement, CheckBoxProps>((props, ref) => {
export const CheckBoxField = forwardRef<HTMLInputElement, CheckBoxFieldProps>((props, ref) => {
const { formFieldProps, childProps } = useFormField(props);
return (

View file

@ -8,11 +8,11 @@ import { useDebouncedCallback } from 'use-debounce';
import * as Root from '../Input';
import { FormField, UseFormFieldProps, useFormField } from './FormField';
export interface InputProps extends UseFormFieldProps, Root.InputProps {
export interface InputFieldProps extends UseFormFieldProps, Root.InputProps {
name: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
export const InputField = forwardRef<HTMLInputElement, InputFieldProps>((props, ref) => {
const { formFieldProps, childProps } = useFormField(props);
return (
@ -91,7 +91,7 @@ const PasswordStrengthMeter = (props: { password: string }) => {
);
};
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
export const PasswordInputField = forwardRef<HTMLInputElement, PasswordInputProps>(
({ showStrength, ...props }, ref) => {
const { formFieldProps, childProps } = useFormField(props);
const { watch } = useFormContext();

View file

@ -2,12 +2,12 @@ import { FieldValues, UseControllerProps, useController } from 'react-hook-form'
import * as Root from '../Select';
import { FormField, UseFormFieldProps, useFormField } from './FormField';
export interface SelectProps<T extends FieldValues>
export interface SelectFieldProps<T extends FieldValues>
extends Omit<UseFormFieldProps, 'name'>,
Omit<Root.SelectProps, 'value' | 'onChange'>,
UseControllerProps<T> {}
export const Select = <T extends FieldValues>(props: SelectProps<T>) => {
export const SelectField = <T extends FieldValues>(props: SelectFieldProps<T>) => {
const { formFieldProps, childProps } = useFormField(props);
const { field } = useController({ name: props.name });

View file

@ -1,20 +1,20 @@
import clsx from 'clsx';
import { forwardRef } from 'react';
import { useController } from 'react-hook-form';
import * as Root from '../Switch';
import { Switch, SwitchProps } from '../Switch';
import { FormField, UseFormFieldProps, useFormField } from './FormField';
export interface SwitchProps extends UseFormFieldProps, Root.SwitchProps {
export interface SwitchFieldProps extends UseFormFieldProps, SwitchProps {
name: string;
}
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
export const SwitchField = forwardRef<HTMLButtonElement, SwitchFieldProps>((props, ref) => {
const { field } = useController(props);
const { formFieldProps, childProps } = useFormField(props);
return (
<FormField {...formFieldProps}>
<Root.Switch
<Switch
{...childProps}
checked={field.value}
onCheckedChange={field.onChange}

View file

@ -1,7 +1,7 @@
export * from './Form';
export * from './FormField';
export * from './CheckBox';
export * from './Input';
export * from './Switch';
export * from './Select';
export * as RadioGroup from './RadioGroup';
export * from './CheckBoxField';
export * from './InputField';
export * from './SwitchField';
export * from './SelectField';
export * as RadioGroupField from './RadioGroupField';

View file

@ -14,7 +14,7 @@ export * from './Switch';
export * as Tabs from './Tabs';
export * as RadioGroup from './RadioGroup';
export * from './Typography';
export * as forms from './forms';
export * from './forms';
export * from './utils';
export * from './Tooltip';
export * from './Slider';