mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-07 04:23:29 +00:00
New dialog system (#531)
* use new dialog system * rename + cleanup * fix util imports * remove GenericAlertDialog* * remove unnecessary setShow
This commit is contained in:
parent
0a31e7f8ce
commit
c2ab9466f5
|
@ -1,5 +1,6 @@
|
|||
import '@fontsource/inter/variable.css';
|
||||
import { LibraryContextProvider, queryClient, useDebugState } from '@sd/client';
|
||||
import { Dialogs } from '@sd/ui';
|
||||
import {
|
||||
Dedupe as DedupeIntegration,
|
||||
HttpContext as HttpContextIntegration,
|
||||
|
@ -64,6 +65,7 @@ function AppRouterWrapper() {
|
|||
return (
|
||||
<LibraryContextProvider onNoLibrary={() => navigate('/onboarding')}>
|
||||
<AppRouter />
|
||||
<Dialogs />
|
||||
</LibraryContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useCurrentLibrary } from '@sd/client';
|
||||
import { Dialogs } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Sidebar } from '~/components/layout/Sidebar';
|
||||
import { Toasts } from '~/components/primitive/Toasts';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Dialog } from '@sd/ui';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
|
||||
import { Input, useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
const schema = z.object({ path: z.string() });
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
}
|
||||
interface Props extends UseDialogProps {}
|
||||
|
||||
export default function AddLocationDialog({ open, setOpen }: Props) {
|
||||
const createLocation = useLibraryMutation('locations.create', {
|
||||
onSuccess: () => setOpen(false)
|
||||
});
|
||||
export default function AddLocationDialog(props: Props) {
|
||||
const dialog = useDialog(props);
|
||||
const createLocation = useLibraryMutation('locations.create');
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
|
@ -25,20 +21,16 @@ export default function AddLocationDialog({ open, setOpen }: Props) {
|
|||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(async ({ path }) => {
|
||||
await createLocation.mutateAsync({
|
||||
{...{ dialog, form }}
|
||||
onSubmit={form.handleSubmit(({ path }) =>
|
||||
createLocation.mutateAsync({
|
||||
path,
|
||||
indexer_rules_ids: []
|
||||
});
|
||||
})}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
})
|
||||
)}
|
||||
title="Add Location URL"
|
||||
description="As you are using the browser version of Spacedrive you will (for now) need to specify an absolute URL of a directory local to the remote node."
|
||||
loading={createLocation.isLoading}
|
||||
ctaLabel="Add"
|
||||
trigger={null}
|
||||
>
|
||||
<Input
|
||||
className="flex-grow w-full mt-3"
|
||||
|
|
|
@ -1,46 +1,25 @@
|
|||
import { Button, Dialog, Input } from '@sd/ui';
|
||||
import { Button, Dialog, Input, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { Clipboard } from 'phosphor-react';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
export const GenericAlertDialogState = {
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
value: '',
|
||||
inputBox: false
|
||||
};
|
||||
|
||||
export interface GenericAlertDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
value: string;
|
||||
inputBox: boolean;
|
||||
}
|
||||
|
||||
export interface AlertDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
export interface AlertDialogProps extends UseDialogProps {
|
||||
title: string; // dialog title
|
||||
description?: string; // description of the dialog
|
||||
value: string; // value to be displayed as text or in an input box
|
||||
label?: string; // button label
|
||||
inputBox: boolean; // whether the dialog should display the `value` in a disabled input box or as text
|
||||
inputBox?: boolean; // whether the dialog should display the `value` in a disabled input box or as text
|
||||
}
|
||||
|
||||
export const AlertDialog = (props: AlertDialogProps) => {
|
||||
const dialog = useDialog(props);
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
// maybe a copy-to-clipboard button would be beneficial too
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(() => {
|
||||
props.setOpen(false);
|
||||
})}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title={props.title}
|
||||
onSubmit={form.handleSubmit(() => {})}
|
||||
dialog={dialog}
|
||||
description={props.description}
|
||||
ctaLabel={props.label !== undefined ? props.label : 'Done'}
|
||||
>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Button, Dialog } from '@sd/ui';
|
||||
import { Button, Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { Eye, EyeSlash } from 'phosphor-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
import { usePlatform } from '../../util/Platform';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
import { useState } from 'react';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { showAlertDialog } from '~/util/dialog';
|
||||
|
||||
const { Input, useZodForm, z } = forms;
|
||||
|
||||
|
@ -15,43 +14,33 @@ const schema = z.object({
|
|||
filePath: z.string()
|
||||
});
|
||||
|
||||
export interface BackupRestorationDialogProps {
|
||||
trigger: ReactNode;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
export interface BackupRestorationDialogProps extends UseDialogProps {}
|
||||
|
||||
export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
||||
const platform = usePlatform();
|
||||
|
||||
const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore', {
|
||||
onSuccess: (total) => {
|
||||
setShow((old) => ({ ...old, backupRestoreDialog: false }));
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Import Successful',
|
||||
description: '',
|
||||
value: `${total} ${total !== 1 ? 'keys were imported.' : 'key was imported.'}`,
|
||||
inputBox: false
|
||||
value: `${total} ${total !== 1 ? 'keys were imported.' : 'key was imported.'}`
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
setShow((old) => ({ ...old, backupRestoreDialog: false }));
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Import Error',
|
||||
description: '',
|
||||
value: 'There was an error while restoring your backup.',
|
||||
inputBox: false
|
||||
value: 'There was an error while restoring your backup.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const [show, setShow] = useState({
|
||||
backupRestoreDialog: false,
|
||||
masterPassword: false,
|
||||
secretKey: false
|
||||
});
|
||||
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const MPCurrentEyeIcon = show.masterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
|
||||
|
||||
|
@ -76,13 +65,11 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
|||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={show.backupRestoreDialog}
|
||||
setOpen={(e) => setShow((old) => ({ ...old, backupRestoreDialog: e }))}
|
||||
dialog={dialog}
|
||||
title="Restore Keys"
|
||||
description="Restore keys from a backup."
|
||||
loading={restoreKeystoreMutation.isLoading}
|
||||
ctaLabel="Restore"
|
||||
trigger={props.trigger}
|
||||
>
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<Input
|
||||
|
@ -123,12 +110,9 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
|||
onClick={() => {
|
||||
if (!platform.openFilePickerDialog) {
|
||||
// TODO: Support opening locations on web
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
value: "System dialogs aren't supported on this platform."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Algorithm, useBridgeMutation } from '@sd/client';
|
||||
import { Button, Dialog, Select, SelectOption } from '@sd/ui';
|
||||
import { Button, Dialog, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { getHashingAlgorithmSettings } from '~/screens/settings/library/KeysSetting';
|
||||
|
||||
import { generatePassword } from '../key/KeyMounter';
|
||||
|
@ -21,14 +21,10 @@ const schema = z.object({
|
|||
hashing_algorithm: z.string()
|
||||
});
|
||||
|
||||
interface Props {
|
||||
onSubmit?: () => void;
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
}
|
||||
interface Props extends UseDialogProps {}
|
||||
|
||||
export default function CreateLibraryDialog(props: PropsWithChildren<Props>) {
|
||||
const queryClient = useQueryClient();
|
||||
export default function CreateLibraryDialog(props: Props) {
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
|
@ -46,24 +42,20 @@ export default function CreateLibraryDialog(props: PropsWithChildren<Props>) {
|
|||
const MP2CurrentEyeIcon = showMasterPassword2 ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const createLibrary = useBridgeMutation('library.create', {
|
||||
onSuccess: (library) => {
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => [
|
||||
...(libraries || []),
|
||||
library
|
||||
]);
|
||||
|
||||
props.onSubmit?.();
|
||||
props.setOpen(false);
|
||||
|
||||
form.reset();
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
const _onSubmit = form.handleSubmit(async (data) => {
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
if (data.password !== data.password_validate) {
|
||||
alert('Passwords are not the same');
|
||||
} else {
|
||||
|
@ -78,14 +70,12 @@ export default function CreateLibraryDialog(props: PropsWithChildren<Props>) {
|
|||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={_onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
onSubmit={onSubmit}
|
||||
dialog={dialog}
|
||||
title="Create New Library"
|
||||
description="Choose a name for your new library, you can configure this and more settings from the library settings later on."
|
||||
submitDisabled={!form.formState.isValid}
|
||||
ctaLabel="Create"
|
||||
trigger={props.children}
|
||||
>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Library name</p>
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import { RadioGroup } from '@headlessui/react';
|
||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog } from '@sd/ui';
|
||||
import { Button, Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { Eye, EyeSlash, Info } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
import { showAlertDialog } from '~/util/dialog';
|
||||
|
||||
import { usePlatform } from '../../util/Platform';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
import { Input, Switch, useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface DecryptDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
interface DecryptDialogProps extends UseDialogProps {
|
||||
location_id: number;
|
||||
path_id: number;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
|
@ -27,6 +24,7 @@ const schema = z.object({
|
|||
|
||||
export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
||||
const platform = usePlatform();
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const mountedUuids = useLibraryQuery(['keys.listMounted'], {
|
||||
onSuccess: (data) => {
|
||||
|
@ -44,22 +42,16 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
|||
|
||||
const decryptFile = useLibraryMutation('files.decryptFiles', {
|
||||
onSuccess: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Info',
|
||||
value:
|
||||
'The decryption job has started successfully. You may track the progress in the job overview panel.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
'The decryption job has started successfully. You may track the progress in the job overview panel.'
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
value: 'The decryption job failed to start.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
value: 'The decryption job failed to start.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -81,25 +73,20 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
|||
const pw = data.type === 'password' ? data.password : null;
|
||||
const save = data.type === 'password' ? data.saveToKeyManager : null;
|
||||
|
||||
props.setOpen(false);
|
||||
|
||||
decryptFile.mutate({
|
||||
return decryptFile.mutateAsync({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
output_path: output,
|
||||
password: pw,
|
||||
save_to_library: save
|
||||
});
|
||||
|
||||
form.reset();
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
dialog={dialog}
|
||||
onSubmit={onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title="Decrypt a file"
|
||||
description="Leave the output file blank for the default."
|
||||
loading={decryptFile.isLoading}
|
||||
|
@ -182,12 +169,9 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
|||
// if we allow the user to encrypt multiple files simultaneously, this should become a directory instead
|
||||
if (!platform.saveFilePickerDialog) {
|
||||
// TODO: Support opening locations on web
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
value: "System dialogs aren't supported on this platform."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,45 +1,33 @@
|
|||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Dialog } from '@sd/ui';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface DeleteDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
location_id: number | null;
|
||||
path_id: number | undefined;
|
||||
interface DeleteDialogProps extends UseDialogProps {
|
||||
location_id: number;
|
||||
path_id: number;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
// outputPath: z.string()
|
||||
});
|
||||
const schema = z.object({});
|
||||
|
||||
export const DeleteFileDialog = (props: DeleteDialogProps) => {
|
||||
const dialog = useDialog(props);
|
||||
const deleteFile = useLibraryMutation('files.deleteFiles');
|
||||
|
||||
const form = useZodForm({
|
||||
schema
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
props.setOpen(false);
|
||||
|
||||
props.location_id &&
|
||||
props.path_id &&
|
||||
deleteFile.mutate({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id
|
||||
});
|
||||
|
||||
form.reset();
|
||||
});
|
||||
const onSubmit = form.handleSubmit(() =>
|
||||
deleteFile.mutateAsync({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
dialog={dialog}
|
||||
title="Delete a file"
|
||||
description="Configure your deletion settings."
|
||||
loading={deleteFile.isLoading}
|
||||
|
|
|
@ -1,43 +1,37 @@
|
|||
import { useBridgeMutation } from '@sd/client';
|
||||
import { Dialog } from '@sd/ui';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
|
||||
const { useZodForm, z } = forms;
|
||||
|
||||
interface Props {
|
||||
interface Props extends UseDialogProps {
|
||||
libraryUuid: string;
|
||||
}
|
||||
|
||||
export default function DeleteLibraryDialog(props: PropsWithChildren<Props>) {
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
export default function DeleteLibraryDialog(props: Props) {
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const deleteLib = useBridgeMutation('library.delete', {
|
||||
onSuccess: () => {
|
||||
setOpenDeleteModal(false);
|
||||
queryClient.invalidateQueries(['library.list']);
|
||||
}
|
||||
});
|
||||
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
|
||||
const onSubmit = form.handleSubmit(() => deleteLib.mutateAsync(props.libraryUuid));
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={async () => {
|
||||
await deleteLib.mutateAsync(props.libraryUuid);
|
||||
}}
|
||||
open={openDeleteModal}
|
||||
setOpen={setOpenDeleteModal}
|
||||
onSubmit={onSubmit}
|
||||
dialog={dialog}
|
||||
title="Delete Library"
|
||||
description="Deleting a library will permanently the database, the files themselves will not be deleted."
|
||||
loading={deleteLib.isLoading}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={props.children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import { Algorithm, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog, Select, SelectOption } from '@sd/ui';
|
||||
import { Button, Dialog, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { showAlertDialog } from '~/util/dialog';
|
||||
|
||||
import { SelectOptionKeyList } from '../key/KeyList';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
import { CheckBox, useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface EncryptDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
location_id: number | null;
|
||||
path_id: number | undefined;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
interface EncryptDialogProps extends UseDialogProps {
|
||||
location_id: number;
|
||||
path_id: number;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
|
@ -25,7 +22,8 @@ const schema = z.object({
|
|||
outputPath: z.string()
|
||||
});
|
||||
|
||||
export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
||||
export const EncryptFileDialog = ({ ...props }: EncryptDialogProps) => {
|
||||
const dialog = useDialog(props);
|
||||
const platform = usePlatform();
|
||||
|
||||
const UpdateKey = (uuid: string) => {
|
||||
|
@ -45,8 +43,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
|||
|
||||
const encryptFile = useLibraryMutation('files.encryptFiles', {
|
||||
onSuccess: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Success',
|
||||
value:
|
||||
'The encryption job has started successfully. You may track the progress in the job overview panel.',
|
||||
|
@ -55,8 +52,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
|||
});
|
||||
},
|
||||
onError: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
value: 'The encryption job failed to start.',
|
||||
inputBox: false,
|
||||
|
@ -69,30 +65,23 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
|||
schema
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
props.setOpen(false);
|
||||
|
||||
props.location_id &&
|
||||
props.path_id &&
|
||||
encryptFile.mutate({
|
||||
algorithm: data.encryptionAlgo as Algorithm,
|
||||
key_uuid: data.key,
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
metadata: data.metadata,
|
||||
preview_media: data.previewMedia,
|
||||
output_path: data.outputPath || null
|
||||
});
|
||||
|
||||
form.reset();
|
||||
});
|
||||
const onSubmit = form.handleSubmit((data) =>
|
||||
encryptFile.mutateAsync({
|
||||
algorithm: data.encryptionAlgo as Algorithm,
|
||||
key_uuid: data.key,
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
metadata: data.metadata,
|
||||
preview_media: data.previewMedia,
|
||||
output_path: data.outputPath || null
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
dialog={dialog}
|
||||
title="Encrypt a file"
|
||||
description="Configure your encryption settings. Leave the output file blank for the default."
|
||||
loading={encryptFile.isLoading}
|
||||
|
@ -123,8 +112,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
|||
// if we allow the user to encrypt multiple files simultaneously, this should become a directory instead
|
||||
if (!platform.saveFilePickerDialog) {
|
||||
// TODO: Support opening locations on web
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Dialog } from '@sd/ui';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Slider from '../primitive/Slider';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface EraseDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
location_id: number | null;
|
||||
path_id: number | undefined;
|
||||
interface EraseDialogProps extends UseDialogProps {
|
||||
location_id: number;
|
||||
path_id: number;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
|
@ -18,6 +16,8 @@ const schema = z.object({
|
|||
});
|
||||
|
||||
export const EraseFileDialog = (props: EraseDialogProps) => {
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const eraseFile = useLibraryMutation('files.eraseFiles');
|
||||
|
||||
const form = useZodForm({
|
||||
|
@ -27,19 +27,13 @@ export const EraseFileDialog = (props: EraseDialogProps) => {
|
|||
}
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
props.setOpen(false);
|
||||
|
||||
props.location_id &&
|
||||
props.path_id &&
|
||||
eraseFile.mutate({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
passes: data.passes
|
||||
});
|
||||
|
||||
form.reset();
|
||||
});
|
||||
const onSubmit = form.handleSubmit((data) =>
|
||||
eraseFile.mutateAsync({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
passes: data.passes
|
||||
})
|
||||
);
|
||||
|
||||
const [passes, setPasses] = useState([4]);
|
||||
|
||||
|
@ -47,8 +41,7 @@ export const EraseFileDialog = (props: EraseDialogProps) => {
|
|||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
dialog={dialog}
|
||||
title="Erase a file"
|
||||
description="Configure your erasure settings."
|
||||
loading={eraseFile.isLoading}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import { useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog, Input, Select, SelectOption } from '@sd/ui';
|
||||
import { Button, Dialog, Input, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Clipboard } from 'phosphor-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting';
|
||||
|
||||
import { getHashingAlgorithmString } from '../../screens/settings/library/KeysSetting';
|
||||
import { SelectOptionKeyList } from '../key/KeyList';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface KeyViewerDialogProps {
|
||||
trigger: ReactNode;
|
||||
}
|
||||
interface KeyViewerDialogProps extends UseDialogProps {}
|
||||
|
||||
export const KeyUpdater = (props: {
|
||||
uuid: string;
|
||||
|
@ -40,6 +38,7 @@ const schema = z.object({});
|
|||
|
||||
export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
||||
const form = useZodForm({ schema });
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const keys = useLibraryQuery(['keys.list'], {
|
||||
onSuccess: (data) => {
|
||||
|
@ -49,7 +48,6 @@ export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
|||
}
|
||||
});
|
||||
|
||||
const [showKeyViewerDialog, setShowKeyViewerDialog] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
const [keyValue, setKeyValue] = useState('');
|
||||
const [contentSalt, setContentSalt] = useState('');
|
||||
|
@ -59,12 +57,8 @@ export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
|||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={async () => {
|
||||
setShowKeyViewerDialog(false);
|
||||
}}
|
||||
open={showKeyViewerDialog}
|
||||
setOpen={setShowKeyViewerDialog}
|
||||
trigger={props.trigger}
|
||||
onSubmit={form.handleSubmit(() => {})}
|
||||
dialog={dialog}
|
||||
title="View Key Values"
|
||||
description="Here you can view the values of your keys."
|
||||
ctaLabel="Done"
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import { Algorithm, useLibraryMutation } from '@sd/client';
|
||||
import { Button, Dialog, Input, Select, SelectOption } from '@sd/ui';
|
||||
import { Button, Dialog, Input, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { getHashingAlgorithmSettings } from '~/screens/settings/library/KeysSetting';
|
||||
import { showAlertDialog } from '~/util/dialog';
|
||||
|
||||
import { getHashingAlgorithmSettings } from '../../screens/settings/library/KeysSetting';
|
||||
import { generatePassword } from '../key/KeyMounter';
|
||||
import { PasswordMeter } from '../key/PasswordMeter';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
export interface MasterPasswordChangeDialogProps {
|
||||
trigger: ReactNode;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
export interface MasterPasswordChangeDialogProps extends UseDialogProps {}
|
||||
|
||||
const schema = z.object({
|
||||
masterPassword: z.string(),
|
||||
|
@ -27,35 +24,28 @@ const schema = z.object({
|
|||
export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => {
|
||||
const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword', {
|
||||
onSuccess: () => {
|
||||
setShow((old) => ({ ...old, masterPasswordDialog: false }));
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Success',
|
||||
description: '',
|
||||
value: 'Your master password was changed successfully',
|
||||
inputBox: false
|
||||
value: 'Your master password was changed successfully'
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
// this should never really happen
|
||||
setShow((old) => ({ ...old, masterPasswordDialog: false }));
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Master Password Change Error',
|
||||
description: '',
|
||||
value: 'There was an error while changing your master password.',
|
||||
inputBox: false
|
||||
value: 'There was an error while changing your master password.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const [show, setShow] = useState({
|
||||
masterPasswordDialog: false,
|
||||
masterPassword: false,
|
||||
masterPassword2: false,
|
||||
secretKey: false
|
||||
});
|
||||
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const MP1CurrentEyeIcon = show.masterPassword ? EyeSlash : Eye;
|
||||
const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
|
||||
|
@ -70,25 +60,20 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
|||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
if (data.masterPassword !== data.masterPassword2) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: 'Passwords are not the same, please try again.',
|
||||
inputBox: false
|
||||
value: 'Passwords are not the same, please try again.'
|
||||
});
|
||||
} else {
|
||||
const hashing_algorithm = getHashingAlgorithmSettings(data.hashingAlgo);
|
||||
const sk = data.secretKey || null;
|
||||
|
||||
changeMasterPassword.mutate({
|
||||
return changeMasterPassword.mutateAsync({
|
||||
algorithm: data.encryptionAlgo as Algorithm,
|
||||
hashing_algorithm,
|
||||
password: data.masterPassword,
|
||||
secret_key: sk
|
||||
});
|
||||
|
||||
form.reset();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -96,16 +81,11 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
|||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={show.masterPasswordDialog}
|
||||
setOpen={(e) => {
|
||||
setShow((old) => ({ ...old, masterPasswordDialog: e }));
|
||||
}}
|
||||
dialog={dialog}
|
||||
title="Change Master Password"
|
||||
description="Select a new master password for your key manager. Leave the key secret blank to disable it."
|
||||
ctaDanger={true}
|
||||
loading={changeMasterPassword.isLoading}
|
||||
ctaLabel="Change"
|
||||
trigger={props.trigger}
|
||||
>
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<Input
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { ExplorerData, rspc, useCurrentLibrary } from '@sd/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
|
||||
import { useExplorerStore } from '../../hooks/useExplorerStore';
|
||||
import { AlertDialog, GenericAlertDialogState } from '../dialog/AlertDialog';
|
||||
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
||||
import { DeleteFileDialog } from '../dialog/DeleteFileDialog';
|
||||
import { EncryptFileDialog } from '../dialog/EncryptFileDialog';
|
||||
import { EraseFileDialog } from '../dialog/EraseFileDialog';
|
||||
import { Inspector } from '../explorer/Inspector';
|
||||
import { ExplorerContextMenu } from './ExplorerContextMenu';
|
||||
import { TopBar } from './ExplorerTopBar';
|
||||
|
@ -23,13 +18,6 @@ export default function Explorer(props: Props) {
|
|||
const [scrollSegments, setScrollSegments] = useState<{ [key: string]: number }>({});
|
||||
const [separateTopBar, setSeparateTopBar] = useState<boolean>(false);
|
||||
|
||||
const [showEncryptDialog, setShowEncryptDialog] = useState(false);
|
||||
const [showDecryptDialog, setShowDecryptDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showEraseDialog, setShowEraseDialog] = useState(false);
|
||||
|
||||
const [alertDialogData, setAlertDialogData] = useState(GenericAlertDialogState);
|
||||
|
||||
useEffect(() => {
|
||||
setSeparateTopBar((oldValue) => {
|
||||
const newValue = Object.values(scrollSegments).some((val) => val >= 5);
|
||||
|
@ -45,8 +33,6 @@ export default function Explorer(props: Props) {
|
|||
}
|
||||
});
|
||||
|
||||
const selectedItem = props.data?.items[expStore.selectedRowIndex];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
|
@ -67,11 +53,6 @@ export default function Explorer(props: Props) {
|
|||
};
|
||||
});
|
||||
}}
|
||||
setShowEncryptDialog={setShowEncryptDialog}
|
||||
setShowDecryptDialog={setShowDecryptDialog}
|
||||
setShowDeleteDialog={setShowDeleteDialog}
|
||||
setShowEraseDialog={setShowEraseDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
)}
|
||||
{expStore.showInspector && (
|
||||
|
@ -96,51 +77,6 @@ export default function Explorer(props: Props) {
|
|||
</div>
|
||||
</ExplorerContextMenu>
|
||||
</div>
|
||||
<AlertDialog
|
||||
open={alertDialogData.open}
|
||||
setOpen={(e) => {
|
||||
setAlertDialogData({ ...alertDialogData, open: e });
|
||||
}}
|
||||
title={alertDialogData.title}
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
|
||||
{/* these props are all shared so could use the same prop type */}
|
||||
{selectedItem && (
|
||||
<EncryptFileDialog
|
||||
location_id={expStore.locationId}
|
||||
path_id={selectedItem.id}
|
||||
open={showEncryptDialog}
|
||||
setOpen={setShowEncryptDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
)}
|
||||
{selectedItem && expStore.locationId !== null && (
|
||||
<DecryptFileDialog
|
||||
location_id={expStore.locationId}
|
||||
path_id={selectedItem.id}
|
||||
open={showDecryptDialog}
|
||||
setOpen={setShowDecryptDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
)}
|
||||
{selectedItem && expStore.locationId !== null && (
|
||||
<DeleteFileDialog
|
||||
location_id={expStore.locationId}
|
||||
path_id={selectedItem.id}
|
||||
open={showDeleteDialog}
|
||||
setOpen={setShowDeleteDialog}
|
||||
/>
|
||||
)}
|
||||
{selectedItem && expStore.locationId !== null && (
|
||||
<EraseFileDialog
|
||||
location_id={expStore.locationId}
|
||||
path_id={selectedItem.id}
|
||||
open={showEraseDialog}
|
||||
setOpen={setShowEraseDialog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ExplorerItem, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { ContextMenu as CM } from '@sd/ui';
|
||||
import { dialogManager } from '@sd/ui';
|
||||
import {
|
||||
ArrowBendUpRight,
|
||||
Image,
|
||||
|
@ -15,11 +16,15 @@ import {
|
|||
TrashSimple
|
||||
} from 'phosphor-react';
|
||||
import { PropsWithChildren, useMemo } from 'react';
|
||||
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { showAlertDialog } from '~/util/dialog';
|
||||
|
||||
import { getExplorerStore } from '../../hooks/useExplorerStore';
|
||||
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
|
||||
import { usePlatform } from '../../util/Platform';
|
||||
import { GenericAlertDialogProps } from '../dialog/AlertDialog';
|
||||
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
||||
import { DeleteFileDialog } from '../dialog/DeleteFileDialog';
|
||||
import { EncryptFileDialog } from '../dialog/EncryptFileDialog';
|
||||
import { EraseFileDialog } from '../dialog/EraseFileDialog';
|
||||
import { isObject } from './utils';
|
||||
|
||||
const AssignTagMenuItems = (props: { objectId: number }) => {
|
||||
|
@ -64,7 +69,6 @@ const AssignTagMenuItems = (props: { objectId: number }) => {
|
|||
|
||||
function OpenInNativeExplorer() {
|
||||
const platform = usePlatform();
|
||||
const store = getExplorerStore();
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const osFileBrowserName = useMemo(() => {
|
||||
|
@ -154,14 +158,9 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
|
|||
|
||||
export interface FileItemContextMenuProps extends PropsWithChildren {
|
||||
item: ExplorerItem;
|
||||
setShowEncryptDialog: (isShowing: boolean) => void;
|
||||
setShowDecryptDialog: (isShowing: boolean) => void;
|
||||
setShowDeleteDialog: (isShowing: boolean) => void;
|
||||
setShowEraseDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
export function FileItemContextMenu(props: FileItemContextMenuProps) {
|
||||
export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) {
|
||||
const objectData = props.item ? (isObject(props.item) ? props.item : props.item.object) : null;
|
||||
|
||||
const hasMasterPasswordQuery = useLibraryQuery(['keys.hasMasterPassword']);
|
||||
|
@ -219,22 +218,22 @@ export function FileItemContextMenu(props: FileItemContextMenuProps) {
|
|||
keybind="⌘E"
|
||||
onClick={() => {
|
||||
if (hasMasterPassword && hasMountedKeys) {
|
||||
props.setShowEncryptDialog(true);
|
||||
dialogManager.create((dp) => (
|
||||
<EncryptFileDialog
|
||||
{...dp}
|
||||
location_id={useExplorerStore().locationId!}
|
||||
path_id={props.item.id}
|
||||
/>
|
||||
));
|
||||
} else if (!hasMasterPassword) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Key manager locked',
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.'
|
||||
});
|
||||
} else if (!hasMountedKeys) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'No mounted keys',
|
||||
description: '',
|
||||
value: 'No mounted keys were found. Please mount a key and try again.',
|
||||
inputBox: false
|
||||
value: 'No mounted keys were found. Please mount a key and try again.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
@ -246,14 +245,17 @@ export function FileItemContextMenu(props: FileItemContextMenuProps) {
|
|||
keybind="⌘D"
|
||||
onClick={() => {
|
||||
if (hasMasterPassword) {
|
||||
props.setShowDecryptDialog(true);
|
||||
dialogManager.create((dp) => (
|
||||
<DecryptFileDialog
|
||||
{...dp}
|
||||
location_id={useExplorerStore().locationId!}
|
||||
path_id={props.item.id}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Key manager locked',
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
@ -269,8 +271,14 @@ export function FileItemContextMenu(props: FileItemContextMenuProps) {
|
|||
variant="danger"
|
||||
label="Secure delete"
|
||||
icon={TrashSimple}
|
||||
onClick={(e) => {
|
||||
props.setShowEraseDialog(true);
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => (
|
||||
<EraseFileDialog
|
||||
{...dp}
|
||||
location_id={getExplorerStore().locationId!}
|
||||
path_id={props.item.id}
|
||||
/>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
</CM.SubMenu>
|
||||
|
@ -282,8 +290,14 @@ export function FileItemContextMenu(props: FileItemContextMenuProps) {
|
|||
label="Delete"
|
||||
variant="danger"
|
||||
keybind="⌘DEL"
|
||||
onClick={(e) => {
|
||||
props.setShowDeleteDialog(true);
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteFileDialog
|
||||
{...dp}
|
||||
location_id={getExplorerStore().locationId!}
|
||||
path_id={props.item.id}
|
||||
/>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
</CM.ContextMenu>
|
||||
|
|
|
@ -2,11 +2,8 @@ import { ExplorerItem, isVideoExt } from '@sd/client';
|
|||
import { cva, tw } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { getExplorerStore } from '~/hooks/useExplorerStore';
|
||||
|
||||
import { getExplorerStore } from '../../hooks/useExplorerStore';
|
||||
import { ObjectKind } from '../../util/kind';
|
||||
import { GenericAlertDialogProps } from '../dialog/AlertDialog';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
import { FileItemContextMenu } from './ExplorerContextMenu';
|
||||
import FileThumb from './FileThumb';
|
||||
import { isObject } from './utils';
|
||||
|
@ -28,36 +25,14 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|||
data: ExplorerItem;
|
||||
selected: boolean;
|
||||
index: number;
|
||||
setShowEncryptDialog: (isShowing: boolean) => void;
|
||||
setShowDecryptDialog: (isShowing: boolean) => void;
|
||||
setShowDeleteDialog: (isShowing: boolean) => void;
|
||||
setShowEraseDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
function FileItem({
|
||||
data,
|
||||
selected,
|
||||
index,
|
||||
setShowEncryptDialog,
|
||||
setShowDecryptDialog,
|
||||
setShowDeleteDialog,
|
||||
setShowEraseDialog,
|
||||
setAlertDialogData,
|
||||
...rest
|
||||
}: Props) {
|
||||
function FileItem({ data, selected, index, ...rest }: Props) {
|
||||
const objectData = data ? (isObject(data) ? data : data.object) : null;
|
||||
const isVid = isVideoExt(data.extension || '');
|
||||
|
||||
return (
|
||||
<FileItemContextMenu
|
||||
item={data}
|
||||
setShowEncryptDialog={setShowEncryptDialog}
|
||||
setShowDecryptDialog={setShowDecryptDialog}
|
||||
setShowDeleteDialog={setShowDeleteDialog}
|
||||
setShowEraseDialog={setShowEraseDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
>
|
||||
<FileItemContextMenu item={data}>
|
||||
<div
|
||||
onContextMenu={(e) => {
|
||||
if (index != undefined) {
|
||||
|
|
|
@ -3,13 +3,8 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
|||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKey, useOnWindowResize } from 'rooks';
|
||||
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
|
||||
import {
|
||||
ExplorerLayoutMode,
|
||||
getExplorerStore,
|
||||
useExplorerStore
|
||||
} from '../../hooks/useExplorerStore';
|
||||
import { GenericAlertDialogProps } from '../dialog/AlertDialog';
|
||||
import FileItem from './FileItem';
|
||||
import FileRow from './FileRow';
|
||||
import { isPath } from './utils';
|
||||
|
@ -21,23 +16,9 @@ interface Props {
|
|||
context: ExplorerContext;
|
||||
data: ExplorerItem[];
|
||||
onScroll?: (posY: number) => void;
|
||||
setShowEncryptDialog: (isShowing: boolean) => void;
|
||||
setShowDecryptDialog: (isShowing: boolean) => void;
|
||||
setShowDeleteDialog: (isShowing: boolean) => void;
|
||||
setShowEraseDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
export const VirtualizedList: React.FC<Props> = ({
|
||||
data,
|
||||
context,
|
||||
onScroll,
|
||||
setShowEncryptDialog,
|
||||
setShowDecryptDialog,
|
||||
setShowDeleteDialog,
|
||||
setShowEraseDialog,
|
||||
setAlertDialogData
|
||||
}) => {
|
||||
export const VirtualizedList: React.FC<Props> = ({ data, context, onScroll }) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -161,11 +142,6 @@ export const VirtualizedList: React.FC<Props> = ({
|
|||
isSelected={getExplorerStore().selectedRowIndex === virtualRow.index}
|
||||
index={virtualRow.index}
|
||||
item={data[virtualRow.index]}
|
||||
setShowEncryptDialog={setShowEncryptDialog}
|
||||
setShowDecryptDialog={setShowDecryptDialog}
|
||||
setShowDeleteDialog={setShowDeleteDialog}
|
||||
setShowEraseDialog={setShowEraseDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
) : (
|
||||
[...Array(amountOfColumns)].map((_, i) => {
|
||||
|
@ -181,11 +157,6 @@ export const VirtualizedList: React.FC<Props> = ({
|
|||
isSelected={isSelected}
|
||||
index={index}
|
||||
item={item}
|
||||
setShowEncryptDialog={setShowEncryptDialog}
|
||||
setShowDecryptDialog={setShowDecryptDialog}
|
||||
setShowDeleteDialog={setShowDeleteDialog}
|
||||
setShowEraseDialog={setShowEraseDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -206,25 +177,10 @@ interface WrappedItemProps {
|
|||
index: number;
|
||||
isSelected: boolean;
|
||||
kind: ExplorerLayoutMode;
|
||||
setShowEncryptDialog: (isShowing: boolean) => void;
|
||||
setShowDecryptDialog: (isShowing: boolean) => void;
|
||||
setShowDeleteDialog: (isShowing: boolean) => void;
|
||||
setShowEraseDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
// Wrap either list item or grid item with click logic as it is the same for both
|
||||
const WrappedItem: React.FC<WrappedItemProps> = ({
|
||||
item,
|
||||
index,
|
||||
isSelected,
|
||||
kind,
|
||||
setShowEncryptDialog,
|
||||
setShowDecryptDialog,
|
||||
setShowDeleteDialog,
|
||||
setShowEraseDialog,
|
||||
setAlertDialogData
|
||||
}) => {
|
||||
const WrappedItem: React.FC<WrappedItemProps> = ({ item, index, isSelected, kind }) => {
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
|
@ -243,11 +199,6 @@ const WrappedItem: React.FC<WrappedItemProps> = ({
|
|||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
selected={isSelected}
|
||||
setShowEncryptDialog={setShowEncryptDialog}
|
||||
setShowDecryptDialog={setShowDecryptDialog}
|
||||
setShowDeleteDialog={setShowDeleteDialog}
|
||||
setShowEraseDialog={setShowEraseDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@ import {
|
|||
SelectOption,
|
||||
Switch,
|
||||
cva,
|
||||
dialogManager,
|
||||
tw
|
||||
} from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { CheckCircle, CirclesFour, Gear, Lock, Planet, Plus, ShareNetwork } from 'phosphor-react';
|
||||
import React, { PropsWithChildren, useState } from 'react';
|
||||
import { CheckCircle, CirclesFour, Gear, Lock, Planet, Plus } from 'phosphor-react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
@ -47,7 +48,6 @@ export function Sidebar() {
|
|||
const os = useOperatingSystem();
|
||||
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
|
||||
const debugState = useDebugState();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<SidebarBody className={macOnly(os, 'bg-opacity-[0.75]')}>
|
||||
|
@ -86,7 +86,12 @@ export function Sidebar() {
|
|||
))}
|
||||
</Dropdown.Section>
|
||||
<Dropdown.Section>
|
||||
<Dropdown.Item icon={Plus} onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Dropdown.Item
|
||||
icon={Plus}
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => <CreateLibraryDialog {...dp} />);
|
||||
}}
|
||||
>
|
||||
New Library
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={Gear} to="settings/library">
|
||||
|
@ -152,8 +157,6 @@ export function Sidebar() {
|
|||
</div>
|
||||
{debugState.enabled && <DebugPanel />}
|
||||
</SidebarFooter>
|
||||
{/* Putting this within the dropdown will break the enter click handling in the modal. */}
|
||||
<CreateLibraryDialog open={isCreateDialogOpen} setOpen={setIsCreateDialogOpen} />
|
||||
</SidebarBody>
|
||||
);
|
||||
}
|
||||
|
@ -309,10 +312,10 @@ const SidebarHeadingOptionsButton: React.FC<{ to: string; icon?: React.FC }> = (
|
|||
|
||||
function LibraryScopedSection() {
|
||||
const platform = usePlatform();
|
||||
const { data: locations } = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
const { data: tags } = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
const { mutate: createLocation } = useLibraryMutation('locations.create');
|
||||
const [textLocationDialogOpen, setTextLocationDialogOpen] = useState(false);
|
||||
|
||||
const locations = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
const tags = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
const createLocation = useLibraryMutation('locations.create');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -326,7 +329,7 @@ function LibraryScopedSection() {
|
|||
</>
|
||||
}
|
||||
>
|
||||
{locations?.map((location) => {
|
||||
{locations.data?.map((location) => {
|
||||
return (
|
||||
<div key={location.id} className="flex flex-row items-center">
|
||||
<SidebarLink
|
||||
|
@ -344,11 +347,11 @@ function LibraryScopedSection() {
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{(locations?.length || 0) < 4 && (
|
||||
{(locations.data?.length || 0) < 4 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (platform.platform === 'web') {
|
||||
setTextLocationDialogOpen(true);
|
||||
dialogManager.create((dp) => <AddLocationDialog {...dp} />);
|
||||
} else {
|
||||
if (!platform.openDirectoryPickerDialog) {
|
||||
alert('Opening a dialogue is not supported on this platform!');
|
||||
|
@ -357,7 +360,7 @@ function LibraryScopedSection() {
|
|||
platform.openDirectoryPickerDialog().then((result) => {
|
||||
// TODO: Pass indexer rules ids to create location
|
||||
if (result)
|
||||
createLocation({
|
||||
createLocation.mutate({
|
||||
path: result as string,
|
||||
indexer_rules_ids: []
|
||||
} as LocationCreateArgs);
|
||||
|
@ -374,15 +377,14 @@ function LibraryScopedSection() {
|
|||
</button>
|
||||
)}
|
||||
</SidebarSection>
|
||||
<AddLocationDialog open={textLocationDialogOpen} setOpen={setTextLocationDialogOpen} />
|
||||
</div>
|
||||
{!!tags?.length && (
|
||||
{!!tags.data?.length && (
|
||||
<SidebarSection
|
||||
name="Tags"
|
||||
actionArea={<SidebarHeadingOptionsButton to="/settings/tags" />}
|
||||
>
|
||||
<div className="mt-1 mb-2">
|
||||
{tags?.slice(0, 6).map((tag, index) => (
|
||||
{tags.data?.slice(0, 6).map((tag, index) => (
|
||||
<SidebarLink key={index} to={`tag/${tag.id}`} className="">
|
||||
<div
|
||||
className="w-[12px] h-[12px] rounded-full"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Location, Node } from '@sd/client';
|
||||
import { Button, Card, Dialog } from '@sd/ui';
|
||||
import { Button, Card, Dialog, UseDialogProps, dialogManager, useDialog } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { Repeat, Trash } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
|
@ -16,20 +16,8 @@ interface LocationListItemProps {
|
|||
|
||||
export default function LocationListItem({ location }: LocationListItemProps) {
|
||||
const [hide, setHide] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutate: locRescan } = useLibraryMutation('locations.fullRescan');
|
||||
|
||||
const { mutate: deleteLoc, isLoading: locDeletePending } = useLibraryMutation(
|
||||
'locations.delete',
|
||||
{
|
||||
onSuccess: () => {
|
||||
setHide(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
const fullRescan = useLibraryMutation('locations.fullRescan');
|
||||
|
||||
if (hide) return <></>;
|
||||
|
||||
|
@ -57,32 +45,29 @@ export default function LocationListItem({ location }: LocationListItemProps) {
|
|||
{location.is_online ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</Button>
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(() => {
|
||||
deleteLoc(location.id);
|
||||
})}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title="Delete Location"
|
||||
description="Deleting a location will also remove all files associated with it from the Spacedrive database, the files themselves will not be deleted."
|
||||
loading={locDeletePending}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={
|
||||
<Button variant="gray" className="!p-1.5">
|
||||
<Tooltip label="Delete Location">
|
||||
<Trash className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="gray"
|
||||
className="!p-1.5"
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteLocationDialog
|
||||
{...dp}
|
||||
onSuccess={() => setHide(true)}
|
||||
locationId={location.id}
|
||||
/>
|
||||
));
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Delete Location">
|
||||
<Trash className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button
|
||||
variant="gray"
|
||||
className="!p-1.5"
|
||||
onClick={() => {
|
||||
// this should cause a lite directory rescan, but this will do for now, so the button does something useful
|
||||
locRescan(location.id);
|
||||
fullRescan.mutate(location.id);
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Rescan Location">
|
||||
|
@ -96,3 +81,30 @@ export default function LocationListItem({ location }: LocationListItemProps) {
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteLocationDialogProps extends UseDialogProps {
|
||||
onSuccess: () => void;
|
||||
locationId: number;
|
||||
}
|
||||
|
||||
function DeleteLocationDialog(props: DeleteLocationDialogProps) {
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
|
||||
const deleteLocation = useLibraryMutation('locations.delete', {
|
||||
onSuccess: props.onSuccess
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(() => deleteLocation.mutateAsync(props.locationId))}
|
||||
dialog={dialog}
|
||||
title="Delete Location"
|
||||
description="Deleting a location will also remove all files associated with it from the Spacedrive database, the files themselves will not be deleted."
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Button } from '@sd/ui';
|
||||
import { useState } from 'react';
|
||||
import { Button, dialogManager } from '@sd/ui';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
||||
|
@ -7,16 +6,21 @@ import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
|||
// TODO: This page requires styling for now it is just a placeholder.
|
||||
export default function OnboardingPage() {
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="h-screen p-10 flex flex-col justify-center bg-gradient-to-t from-accent to-purple-600">
|
||||
<h1 className="text-white font-bold text-center text-4xl mb-4">Welcome to Spacedrive</h1>
|
||||
<CreateLibraryDialog open={open} setOpen={setOpen} onSubmit={() => navigate('/overview')}>
|
||||
<Button variant="accent" size="md">
|
||||
Create your library
|
||||
</Button>
|
||||
</CreateLibraryDialog>
|
||||
<Button
|
||||
variant="accent"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => <CreateLibraryDialog {...dp} />, {
|
||||
onSubmit: () => navigate('/overview')
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create your library
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import {
|
||||
Algorithm,
|
||||
HashingAlgorithm,
|
||||
Params,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import { HashingAlgorithm, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Input, dialogManager } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { Eye, EyeSlash, Lock, Plus } from 'phosphor-react';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { animated, useTransition } from 'react-spring';
|
||||
import { AlertDialog, GenericAlertDialogState } from '~/components/dialog/AlertDialog';
|
||||
import { BackupRestoreDialog } from '~/components/dialog/BackupRestoreDialog';
|
||||
import { KeyViewerDialog } from '~/components/dialog/KeyViewerDialog';
|
||||
import { MasterPasswordChangeDialog } from '~/components/dialog/MasterPasswordChangeDialog';
|
||||
|
@ -21,6 +14,7 @@ import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
|||
import { SettingsHeader } from '~/components/settings/SettingsHeader';
|
||||
import { SettingsSubHeader } from '~/components/settings/SettingsSubHeader';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { showAlertDialog } from '~/util/dialog';
|
||||
|
||||
interface Props extends DropdownMenu.MenuContentProps {
|
||||
trigger: React.ReactNode;
|
||||
|
@ -31,10 +25,8 @@ interface Props extends DropdownMenu.MenuContentProps {
|
|||
export const KeyMounterDropdown = ({
|
||||
trigger,
|
||||
children,
|
||||
disabled,
|
||||
transformOrigin,
|
||||
className,
|
||||
...props
|
||||
className
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
@ -86,12 +78,9 @@ export default function KeysSettings() {
|
|||
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
|
||||
const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword', {
|
||||
onError: () => {
|
||||
setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Unlock Error',
|
||||
description: '',
|
||||
value: 'The information provided to the key manager was incorrect',
|
||||
inputBox: false
|
||||
value: 'The information provided to the key manager was incorrect'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -107,78 +96,63 @@ export default function KeysSettings() {
|
|||
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
|
||||
const [alertDialogData, setAlertDialogData] = useState(GenericAlertDialogState);
|
||||
const setShowAlertDialog = (state: boolean) => {
|
||||
setAlertDialogData({ ...alertDialogData, open: state });
|
||||
};
|
||||
|
||||
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
||||
if (!hasMasterPw?.data) {
|
||||
return (
|
||||
<>
|
||||
<div className="p-2 mr-20 ml-20 mt-10">
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={masterPassword}
|
||||
onChange={(e) => setMasterPassword(e.target.value)}
|
||||
autoFocus
|
||||
type={showMasterPassword ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Master Password"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword(!showMasterPassword)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-2 mr-20 ml-20 mt-10">
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={masterPassword}
|
||||
onChange={(e) => setMasterPassword(e.target.value)}
|
||||
autoFocus
|
||||
type={showMasterPassword ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Master Password"
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="accent"
|
||||
disabled={setMasterPasswordMutation.isLoading || isKeyManagerUnlocking.data}
|
||||
onClick={() => {
|
||||
if (masterPassword !== '') {
|
||||
const sk = secretKey || null;
|
||||
setMasterPassword('');
|
||||
setSecretKey('');
|
||||
setMasterPasswordMutation.mutate({ password: masterPassword, secret_key: sk });
|
||||
}
|
||||
}}
|
||||
onClick={() => setShowMasterPassword(!showMasterPassword)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
Unlock
|
||||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog
|
||||
open={alertDialogData.open}
|
||||
setOpen={setShowAlertDialog}
|
||||
title={alertDialogData.title}
|
||||
description={alertDialogData.description}
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
</>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="accent"
|
||||
disabled={setMasterPasswordMutation.isLoading || isKeyManagerUnlocking.data}
|
||||
onClick={() => {
|
||||
if (masterPassword !== '') {
|
||||
const sk = secretKey || null;
|
||||
setMasterPassword('');
|
||||
setSecretKey('');
|
||||
setMasterPasswordMutation.mutate({ password: masterPassword, secret_key: sk });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
|
@ -218,21 +192,23 @@ export default function KeysSettings() {
|
|||
|
||||
<SettingsSubHeader title="Password Options" />
|
||||
<div className="flex flex-row">
|
||||
<MasterPasswordChangeDialog
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2">
|
||||
Change Master Password
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<KeyViewerDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2" hidden={keys.data?.length === 0}>
|
||||
View Key Values
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="gray"
|
||||
className="mr-2"
|
||||
onClick={() => dialogManager.create((dp) => <MasterPasswordChangeDialog {...dp} />)}
|
||||
>
|
||||
Change Master Password
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="gray"
|
||||
className="mr-2"
|
||||
hidden={keys.data?.length === 0}
|
||||
onClick={() => dialogManager.create((dp) => <KeyViewerDialog {...dp} />)}
|
||||
>
|
||||
View Key Values
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SettingsSubHeader title="Data Recovery" />
|
||||
|
@ -245,12 +221,9 @@ export default function KeysSettings() {
|
|||
onClick={() => {
|
||||
if (!platform.saveFilePickerDialog) {
|
||||
// TODO: Support opening locations on web
|
||||
setAlertDialogData({
|
||||
open: true,
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
value: "System dialogs aren't supported on this platform."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -261,24 +234,16 @@ export default function KeysSettings() {
|
|||
>
|
||||
Backup
|
||||
</Button>
|
||||
<BackupRestoreDialog
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2">
|
||||
Restore
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="gray"
|
||||
className="mr-2"
|
||||
onClick={() => dialogManager.create((dp) => <BackupRestoreDialog {...dp} />)}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
<AlertDialog
|
||||
open={alertDialogData.open}
|
||||
setOpen={setShowAlertDialog}
|
||||
title={alertDialogData.title}
|
||||
description={alertDialogData.description}
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -301,7 +266,7 @@ export const getHashingAlgorithmSettings = (hashingAlgorithm: string): HashingAl
|
|||
// not sure of a suitable place for this function
|
||||
export const getHashingAlgorithmString = (hashingAlgorithm: HashingAlgorithm): string => {
|
||||
return Object.entries(table).find(
|
||||
([str, hashAlg], i) =>
|
||||
([_, hashAlg]) =>
|
||||
hashAlg.name === hashingAlgorithm.name && hashAlg.params === hashingAlgorithm.params
|
||||
)![0];
|
||||
};
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { LocationCreateArgs } from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import { Button, Input, dialogManager } from '@sd/ui';
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import AddLocationDialog from '~/components/dialog/AddLocationDialog';
|
||||
import LocationListItem from '~/components/location/LocationListItem';
|
||||
import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
||||
|
@ -12,13 +10,11 @@ import { usePlatform } from '~/util/Platform';
|
|||
|
||||
export default function LocationSettings() {
|
||||
const platform = usePlatform();
|
||||
const { data: locations } = useLibraryQuery(['locations.list']);
|
||||
const { mutate: createLocation } = useLibraryMutation('locations.create');
|
||||
const [textLocationDialogOpen, setTextLocationDialogOpen] = useState(false);
|
||||
const locations = useLibraryQuery(['locations.list']);
|
||||
const createLocation = useLibraryMutation('locations.create');
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{/*<Button size="sm">Add Location</Button>*/}
|
||||
<SettingsHeader
|
||||
title="Locations"
|
||||
description="Manage your storage locations."
|
||||
|
@ -28,13 +24,13 @@ export default function LocationSettings() {
|
|||
<MagnifyingGlass className="absolute w-[18px] h-auto top-[8px] left-[11px] text-gray-350" />
|
||||
<Input className="!p-0.5 !pl-9" placeholder="Search locations" />
|
||||
</div>
|
||||
<AddLocationDialog open={textLocationDialogOpen} setOpen={setTextLocationDialogOpen} />
|
||||
|
||||
<Button
|
||||
variant="accent"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (platform.platform === 'web') {
|
||||
setTextLocationDialogOpen(true);
|
||||
dialogManager.create((dp) => <AddLocationDialog {...dp} />);
|
||||
} else {
|
||||
if (!platform.openDirectoryPickerDialog) {
|
||||
alert('Opening a dialogue is not supported on this platform!');
|
||||
|
@ -43,7 +39,7 @@ export default function LocationSettings() {
|
|||
platform.openDirectoryPickerDialog().then((result) => {
|
||||
// TODO: Pass indexer rules ids to create location
|
||||
if (result)
|
||||
createLocation({
|
||||
createLocation.mutate({
|
||||
path: result as string,
|
||||
indexer_rules_ids: []
|
||||
} as LocationCreateArgs);
|
||||
|
@ -56,9 +52,8 @@ export default function LocationSettings() {
|
|||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid space-y-2">
|
||||
{locations?.map((location) => (
|
||||
{locations.data?.map((location) => (
|
||||
<LocationListItem key={location.id} location={location} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Tag, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Card, Dialog, Switch } from '@sd/ui';
|
||||
import { Button, Card, Dialog, Switch, UseDialogProps, dialogManager, useDialog } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
@ -13,34 +13,12 @@ import { Tooltip } from '~/components/tooltip/Tooltip';
|
|||
import { Form, Input, useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
export default function TagsSettings() {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
|
||||
const { data: tags } = useLibraryQuery(['tags.list']);
|
||||
const [selectedTag, setSelectedTag] = useState<null | Tag>(tags.data?.[0] ?? null);
|
||||
|
||||
const [selectedTag, setSelectedTag] = useState<null | Tag>(tags?.[0] ?? null);
|
||||
|
||||
const createTag = useLibraryMutation('tags.create', {
|
||||
onError: (e) => {
|
||||
console.error('error', e);
|
||||
}
|
||||
});
|
||||
const updateTag = useLibraryMutation('tags.update');
|
||||
const deleteTag = useLibraryMutation('tags.delete', {
|
||||
onSuccess: () => {
|
||||
setSelectedTag(null);
|
||||
}
|
||||
});
|
||||
|
||||
const createForm = useZodForm({
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
color: z.string()
|
||||
}),
|
||||
defaultValues: {
|
||||
color: '#A717D9'
|
||||
}
|
||||
});
|
||||
const updateForm = useZodForm({
|
||||
schema: z.object({
|
||||
id: z.number(),
|
||||
|
@ -49,7 +27,6 @@ export default function TagsSettings() {
|
|||
}),
|
||||
defaultValues: selectedTag ?? undefined
|
||||
});
|
||||
const deleteForm = useZodForm({ schema: z.object({}) });
|
||||
|
||||
const submitTagUpdate = updateForm.handleSubmit((data) => updateTag.mutateAsync(data));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -75,42 +52,21 @@ export default function TagsSettings() {
|
|||
description="Manage your tags."
|
||||
rightArea={
|
||||
<div className="flex-row space-x-2">
|
||||
<Dialog
|
||||
form={createForm}
|
||||
onSubmit={createForm.handleSubmit(async (data) => {
|
||||
await createTag.mutateAsync(data);
|
||||
setOpenCreateModal(false);
|
||||
})}
|
||||
open={openCreateModal}
|
||||
setOpen={setOpenCreateModal}
|
||||
title="Create New Tag"
|
||||
description="Choose a name and color."
|
||||
loading={createTag.isLoading}
|
||||
ctaLabel="Create"
|
||||
trigger={
|
||||
<Button variant="accent" size="sm">
|
||||
Create Tag
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
variant="accent"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => <CreateTagDialog {...dp} />);
|
||||
}}
|
||||
>
|
||||
<div className="relative mt-3 ">
|
||||
<PopoverPicker
|
||||
className="!absolute left-[9px] -top-[3px]"
|
||||
{...createForm.register('color')}
|
||||
/>
|
||||
<Input
|
||||
{...createForm.register('name', { required: true })}
|
||||
className="w-full pl-[40px]"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
Create Tag
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card className="!px-2">
|
||||
<div className="flex flex-wrap gap-2 m-1">
|
||||
{tags?.map((tag) => (
|
||||
{tags.data?.map((tag) => (
|
||||
<div
|
||||
onClick={() => setTag(tag.id === selectedTag?.id ? null : tag)}
|
||||
key={tag.id}
|
||||
|
@ -147,25 +103,23 @@ export default function TagsSettings() {
|
|||
<Input {...updateForm.register('name')} />
|
||||
</div>
|
||||
<div className="flex flex-grow" />
|
||||
<Dialog
|
||||
form={deleteForm}
|
||||
onSubmit={deleteForm.handleSubmit(async () => {
|
||||
await deleteTag.mutateAsync(selectedTag.id);
|
||||
})}
|
||||
open={openDeleteModal}
|
||||
setOpen={setOpenDeleteModal}
|
||||
title="Delete Tag"
|
||||
description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked."
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={
|
||||
<Button variant="gray" className="h-[38px] mt-[22px]">
|
||||
<Tooltip label="Delete Tag">
|
||||
<Trash className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button
|
||||
variant="gray"
|
||||
className="h-[38px] mt-[22px]"
|
||||
onClick={() =>
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteTagDialog
|
||||
{...dp}
|
||||
tagId={selectedTag.id}
|
||||
onSuccess={() => setSelectedTag(null)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Tooltip label="Delete Tag">
|
||||
<Trash className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
<InputContainer
|
||||
mini
|
||||
|
@ -181,3 +135,68 @@ export default function TagsSettings() {
|
|||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateTagDialog(props: UseDialogProps) {
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const form = useZodForm({
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
color: z.string()
|
||||
}),
|
||||
defaultValues: {
|
||||
color: '#A717D9'
|
||||
}
|
||||
});
|
||||
|
||||
const createTag = useLibraryMutation('tags.create', {
|
||||
onError: (e) => {
|
||||
console.error('error', e);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...{ dialog, form }}
|
||||
onSubmit={form.handleSubmit((data) => createTag.mutateAsync(data))}
|
||||
title="Create New Tag"
|
||||
description="Choose a name and color."
|
||||
ctaLabel="Create"
|
||||
>
|
||||
<div className="relative mt-3 ">
|
||||
<PopoverPicker className="!absolute left-[9px] -top-[3px]" {...form.register('color')} />
|
||||
<Input
|
||||
{...form.register('name', { required: true })}
|
||||
className="w-full pl-[40px]"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeleteTagDialogProps extends UseDialogProps {
|
||||
tagId: number;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
function DeleteTagDialog(props: DeleteTagDialogProps) {
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
|
||||
const deleteTag = useLibraryMutation('tags.delete', {
|
||||
onSuccess: props.onSuccess
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...{ form, dialog }}
|
||||
onSubmit={form.handleSubmit(() => deleteTag.mutateAsync(props.tagId))}
|
||||
title="Delete Tag"
|
||||
description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked."
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { useBridgeMutation, useBridgeQuery, useCurrentLibrary } from '@sd/client';
|
||||
import { useBridgeQuery, useCurrentLibrary } from '@sd/client';
|
||||
import { LibraryConfigWrapped } from '@sd/client';
|
||||
import { Button, ButtonLink, Card, tw } from '@sd/ui';
|
||||
import { Database, DotsSixVertical, Link, Pen, Pencil, Trash } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
import { Button, ButtonLink, Card, dialogManager, tw } from '@sd/ui';
|
||||
import { Database, DotsSixVertical, Pencil, Trash } from 'phosphor-react';
|
||||
import CreateLibraryDialog from '~/components/dialog/CreateLibraryDialog';
|
||||
import DeleteLibraryDialog from '~/components/dialog/DeleteLibraryDialog';
|
||||
import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
||||
|
@ -12,14 +11,6 @@ import { Tooltip } from '~/components/tooltip/Tooltip';
|
|||
const Pill = tw.span`px-1.5 ml-2 py-[2px] rounded text-xs font-medium bg-accent`;
|
||||
|
||||
function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolean }) {
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
|
||||
const deleteLibrary = useBridgeMutation('library.delete', {
|
||||
onSuccess: () => {
|
||||
setOpenDeleteModal(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<DotsSixVertical weight="bold" className="mt-[15px] mr-3 opacity-30" />
|
||||
|
@ -41,21 +32,26 @@ function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolea
|
|||
<Pencil className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</ButtonLink>
|
||||
<DeleteLibraryDialog libraryUuid={props.library.uuid}>
|
||||
<Button className="!p-1.5" variant="gray">
|
||||
<Tooltip label="Delete Library">
|
||||
<Trash className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DeleteLibraryDialog>
|
||||
<Button
|
||||
className="!p-1.5"
|
||||
variant="gray"
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteLibraryDialog {...dp} libraryUuid={props.library.uuid} />
|
||||
));
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Delete Library">
|
||||
<Trash className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LibrarySettings() {
|
||||
const { data: libraries } = useBridgeQuery(['library.list']);
|
||||
const [open, setOpen] = useState(false);
|
||||
const libraries = useBridgeQuery(['library.list']);
|
||||
|
||||
const { library: currentLibrary } = useCurrentLibrary();
|
||||
|
||||
|
@ -66,17 +62,21 @@ export default function LibrarySettings() {
|
|||
description="The database contains all library data and file metadata."
|
||||
rightArea={
|
||||
<div className="flex-row space-x-2">
|
||||
<CreateLibraryDialog open={open} setOpen={setOpen}>
|
||||
<Button variant="accent" size="sm">
|
||||
Add Library
|
||||
</Button>
|
||||
</CreateLibraryDialog>
|
||||
<Button
|
||||
variant="accent"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
dialogManager.create((dp) => <CreateLibraryDialog {...dp} />);
|
||||
}}
|
||||
>
|
||||
Add Library
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
{libraries
|
||||
{libraries.data
|
||||
?.sort((a, b) => {
|
||||
if (a.uuid === currentLibrary?.uuid) return -1;
|
||||
if (b.uuid === currentLibrary?.uuid) return 1;
|
||||
|
|
6
packages/interface/src/util/dialog.tsx
Normal file
6
packages/interface/src/util/dialog.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { dialogManager } from '@sd/ui';
|
||||
import { AlertDialog, AlertDialogProps } from '~/components/dialog/AlertDialog';
|
||||
|
||||
export function showAlertDialog(props: Omit<AlertDialogProps, 'id'>) {
|
||||
dialogManager.create((dp) => <AlertDialog {...dp} {...props} />);
|
||||
}
|
|
@ -1,17 +1,110 @@
|
|||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { ReactElement, ReactNode, useEffect } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { animated, useTransition } from 'react-spring';
|
||||
import { proxy, ref, subscribe, useSnapshot } from 'valtio';
|
||||
|
||||
import { Button, Loader } from '../';
|
||||
import { Form, FormProps } from './forms/Form';
|
||||
|
||||
export function createDialogState(open = false) {
|
||||
return proxy({
|
||||
open
|
||||
});
|
||||
}
|
||||
|
||||
export type DialogState = ReturnType<typeof createDialogState>;
|
||||
|
||||
export interface DialogOptions {
|
||||
onSubmit?(): void;
|
||||
}
|
||||
|
||||
export interface UseDialogProps extends DialogOptions {
|
||||
id: number;
|
||||
}
|
||||
|
||||
class DialogManager {
|
||||
private idGenerator = 0;
|
||||
private state: Record<string, DialogState> = {};
|
||||
|
||||
dialogs: Record<number, React.FC> = proxy({});
|
||||
|
||||
create(dialog: (props: UseDialogProps) => ReactElement, options?: DialogOptions) {
|
||||
const id = this.getId();
|
||||
|
||||
this.dialogs[id] = ref(() => dialog({ id, ...options }));
|
||||
this.state[id] = createDialogState(true);
|
||||
|
||||
return new Promise<void>((res) => {
|
||||
subscribe(this.dialogs, () => {
|
||||
if (!this.dialogs[id]) res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getId() {
|
||||
return ++this.idGenerator;
|
||||
}
|
||||
|
||||
getState(id: number) {
|
||||
return this.state[id];
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
const state = this.getState(id);
|
||||
|
||||
if (!state) {
|
||||
throw new Error(`Dialog ${id} not registered!`);
|
||||
}
|
||||
|
||||
if (state.open === false) {
|
||||
delete this.dialogs[id];
|
||||
delete this.state[id];
|
||||
console.log(`Successfully removed state ${id}`);
|
||||
} else console.log(`Tried to remove state ${id} but wasn't pending!`);
|
||||
}
|
||||
}
|
||||
|
||||
export const dialogManager = new DialogManager();
|
||||
|
||||
/**
|
||||
* Component used to detect when its parent dialog unmounts
|
||||
*/
|
||||
function Remover({ id }: { id: number }) {
|
||||
useEffect(
|
||||
() => () => {
|
||||
dialogManager.remove(id);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useDialog(props: UseDialogProps) {
|
||||
return {
|
||||
...props,
|
||||
state: dialogManager.getState(props.id)
|
||||
};
|
||||
}
|
||||
|
||||
export function Dialogs() {
|
||||
const dialogs = useSnapshot(dialogManager.dialogs);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(dialogs).map(([id, Dialog]) => (
|
||||
<Dialog key={id} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export interface DialogProps<S extends FieldValues>
|
||||
extends DialogPrimitive.DialogProps,
|
||||
FormProps<S> {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
dialog: ReturnType<typeof useDialog>;
|
||||
trigger?: ReactNode;
|
||||
ctaLabel?: string;
|
||||
ctaDanger?: boolean;
|
||||
|
@ -26,11 +119,12 @@ export interface DialogProps<S extends FieldValues>
|
|||
export function Dialog<S extends FieldValues>({
|
||||
form,
|
||||
onSubmit,
|
||||
open,
|
||||
setOpen: onOpenChange,
|
||||
dialog,
|
||||
...props
|
||||
}: DialogProps<S>) {
|
||||
const transitions = useTransition(open, {
|
||||
const stateSnap = useSnapshot(dialog.state);
|
||||
|
||||
const transitions = useTransition(stateSnap.open, {
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: `translateY(20px)`,
|
||||
|
@ -41,8 +135,10 @@ export function Dialog<S extends FieldValues>({
|
|||
config: { mass: 0.4, tension: 200, friction: 10, bounce: 0 }
|
||||
});
|
||||
|
||||
const setOpen = (v: boolean) => (dialog.state.open = v);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Root open={stateSnap.open} onOpenChange={setOpen}>
|
||||
{props.trigger && <DialogPrimitive.Trigger asChild>{props.trigger}</DialogPrimitive.Trigger>}
|
||||
{transitions((styles, show) =>
|
||||
show ? (
|
||||
|
@ -63,7 +159,11 @@ export function Dialog<S extends FieldValues>({
|
|||
>
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={async (e) => {
|
||||
await onSubmit(e);
|
||||
dialog.onSubmit?.();
|
||||
setOpen(false);
|
||||
}}
|
||||
className="min-w-[300px] max-w-[400px] rounded-md bg-app-box border border-app-line text-ink shadow-app-shade !pointer-events-auto"
|
||||
>
|
||||
<div className="p-5">
|
||||
|
@ -94,6 +194,7 @@ export function Dialog<S extends FieldValues>({
|
|||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
<Remover id={dialog.id} />
|
||||
</animated.div>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
|
|
Loading…
Reference in a new issue