New dialog system (#531)

* use new dialog system

* rename + cleanup

* fix util imports

* remove GenericAlertDialog*

* remove unnecessary setShow
This commit is contained in:
Brendan Allan 2023-01-19 19:23:22 -08:00 committed by GitHub
parent 0a31e7f8ce
commit c2ab9466f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 588 additions and 740 deletions

View file

@ -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>
);
}

View file

@ -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';

View file

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

View file

@ -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'}
>

View file

@ -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;
}

View file

@ -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>

View file

@ -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;
}

View file

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

View file

@ -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}
/>
);
}

View file

@ -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.",

View file

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

View file

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

View file

@ -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

View file

@ -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}
/>
)}
</>
);
}

View file

@ -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>

View file

@ -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) {

View file

@ -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}
/>
);

View file

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

View file

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

View file

@ -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>
);
}

View file

@ -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];
};

View file

@ -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>

View file

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

View file

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

View 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} />);
}

View file

@ -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>