mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
Begin form abstraction (#515)
This commit is contained in:
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -12,19 +12,19 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "1e30449d2981cbe0bb9252511493474eb35b2696", features = [
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.4", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "1e30449d2981cbe0bb9252511493474eb35b2696", features = [
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.4", features = [
|
||||
"rspc",
|
||||
"sqlite-create-many",
|
||||
"migrations",
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "1e30449d2981cbe0bb9252511493474eb35b2696", features = [
|
||||
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust.git", tag = "0.6.4", features = [
|
||||
"sqlite",
|
||||
], default-features = false }
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@types/babel-core": "^6.25.7",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/tailwindcss": "^3.1.0",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.55.0",
|
||||
|
||||
@@ -4,11 +4,11 @@ mod crdt;
|
||||
pub use crdt::*;
|
||||
// pub use db::*;
|
||||
|
||||
use prisma_client_rust::ModelActions;
|
||||
use prisma_client_rust::ModelTypes;
|
||||
use serde_value::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub trait CreateCRDTMutation<T: ModelActions> {
|
||||
pub trait CreateCRDTMutation<T: ModelTypes> {
|
||||
fn operation_from_data(
|
||||
d: &BTreeMap<String, Value>,
|
||||
typ: CreateOperationType,
|
||||
@@ -22,7 +22,7 @@ pub enum CreateOperationType {
|
||||
Relation,
|
||||
}
|
||||
|
||||
impl<T: ModelActions> CreateCRDTMutation<T> for prisma_client_rust::Create<'_, T> {
|
||||
impl<T: ModelTypes> CreateCRDTMutation<T> for prisma_client_rust::Create<'_, T> {
|
||||
fn operation_from_data(
|
||||
_: &BTreeMap<String, Value>,
|
||||
typ: CreateOperationType,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.13",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@loadable/component": "^5.15.2",
|
||||
"@radix-ui/react-progress": "^1.0.1",
|
||||
"@radix-ui/react-slider": "^1.1.0",
|
||||
@@ -41,6 +42,7 @@
|
||||
"@zxcvbn-ts/language-en": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"byte-size": "^8.1.0",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"dayjs": "^1.11.5",
|
||||
@@ -58,7 +60,8 @@
|
||||
"tailwindcss": "^3.1.8",
|
||||
"use-count-up": "^3.0.1",
|
||||
"use-debounce": "^8.0.4",
|
||||
"valtio": "^1.7.4"
|
||||
"valtio": "^1.7.4",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
@@ -69,7 +72,6 @@
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/tailwindcss": "^3.1.0",
|
||||
"@vitejs/plugin-react": "^1.3.1",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.8.4",
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
import { LocationCreateArgs, useBridgeMutation, useLibraryMutation } from '@sd/client';
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Input } from '@sd/ui';
|
||||
import { Dialog } from '@sd/ui';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { forms } from '@sd/ui';
|
||||
|
||||
export default function AddLocationDialog({
|
||||
children,
|
||||
onSubmit,
|
||||
open,
|
||||
setOpen
|
||||
}: PropsWithChildren<{ onSubmit?: () => void; open: boolean; setOpen: (state: boolean) => void }>) {
|
||||
// BEFORE MERGE: Remove default value
|
||||
const [locationUrl, setLocationUrl] = useState(
|
||||
'/Users/jamie/Projects/spacedrive/packages/test-files/files'
|
||||
);
|
||||
const { useZodForm, z } = forms;
|
||||
|
||||
const schema = z.object({ path: z.string() });
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export default function AddLocationDialog({ open, setOpen }: Props) {
|
||||
const createLocation = useLibraryMutation('locations.create', {
|
||||
onSuccess: () => setOpen(false)
|
||||
});
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: {
|
||||
// BEFORE MERGE: Remove default value
|
||||
path: '/Users/jamie/Projects/spacedrive/packages/test-files/files'
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(async ({ path }) => {
|
||||
await 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."
|
||||
ctaAction={() =>
|
||||
createLocation.mutate({
|
||||
path: locationUrl,
|
||||
indexer_rules_ids: []
|
||||
} as LocationCreateArgs)
|
||||
}
|
||||
loading={createLocation.isLoading}
|
||||
submitDisabled={!locationUrl}
|
||||
ctaLabel="Add"
|
||||
trigger={null}
|
||||
>
|
||||
<Input
|
||||
className="flex-grow w-full mt-3"
|
||||
value={locationUrl}
|
||||
placeholder="/Users/jamie/Movies"
|
||||
onChange={(e) => setLocationUrl(e.target.value)}
|
||||
required
|
||||
{...form.register('path')}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Button, Dialog, Input } from '@sd/ui';
|
||||
import { Clipboard } from 'phosphor-react';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
export const GenericAlertDialogState = {
|
||||
open: false,
|
||||
title: '',
|
||||
@@ -28,16 +30,18 @@ export interface AlertDialogProps {
|
||||
}
|
||||
|
||||
export const AlertDialog = (props: AlertDialogProps) => {
|
||||
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}
|
||||
description={props.description}
|
||||
ctaAction={() => {
|
||||
props.setOpen(false);
|
||||
}}
|
||||
ctaLabel={props.label !== undefined ? props.label : 'Done'}
|
||||
>
|
||||
{props.inputBox && (
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Button, Dialog, Input } from '@sd/ui';
|
||||
import { Button, Dialog } from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { Eye, EyeSlash } from 'phosphor-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { usePlatform } from '../../util/Platform';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
type FormValues = {
|
||||
masterPassword: string;
|
||||
secretKey: string;
|
||||
filePath: string;
|
||||
};
|
||||
const { Input, useZodForm, z } = forms;
|
||||
|
||||
const schema = z.object({
|
||||
masterPassword: z.string(),
|
||||
secretKey: z.string(),
|
||||
filePath: z.string()
|
||||
});
|
||||
|
||||
export interface BackupRestorationDialogProps {
|
||||
trigger: ReactNode;
|
||||
@@ -53,12 +55,8 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
||||
const MPCurrentEyeIcon = show.masterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
masterPassword: '',
|
||||
secretKey: '',
|
||||
filePath: ''
|
||||
}
|
||||
const form = useZodForm({
|
||||
schema
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
@@ -75,75 +73,73 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Dialog
|
||||
open={show.backupRestoreDialog}
|
||||
setOpen={(e) => setShow((old) => ({ ...old, backupRestoreDialog: e }))}
|
||||
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
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Master Password"
|
||||
required
|
||||
type={show.masterPassword ? 'text' : 'password'}
|
||||
{...form.register('masterPassword', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-3">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
{...form.register('secretKey', { required: false })}
|
||||
type={show.secretKey ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={form.watch('filePath') !== '' ? 'accent' : 'gray'}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!platform.openFilePickerDialog) {
|
||||
// TODO: Support opening locations on web
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
platform.openFilePickerDialog().then((result) => {
|
||||
if (result) form.setValue('filePath', result as string);
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={show.backupRestoreDialog}
|
||||
setOpen={(e) => setShow((old) => ({ ...old, backupRestoreDialog: e }))}
|
||||
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
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Master Password"
|
||||
type={show.masterPassword ? 'text' : 'password'}
|
||||
{...form.register('masterPassword', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-3">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
type={show.secretKey ? 'text' : 'password'}
|
||||
{...form.register('secretKey')}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={form.watch('filePath') !== '' ? 'accent' : 'gray'}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!platform.openFilePickerDialog) {
|
||||
// TODO: Support opening locations on web
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</form>
|
||||
return;
|
||||
}
|
||||
platform.openFilePickerDialog().then((result) => {
|
||||
if (result) form.setValue('filePath', result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
import { Algorithm, useBridgeMutation } from '@sd/client';
|
||||
import { Button, Dialog, Input, Select, SelectOption } from '@sd/ui';
|
||||
import { Button, Dialog, Select, SelectOption } 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 { useForm } from 'react-hook-form';
|
||||
import { getHashingAlgorithmSettings } from '~/screens/settings/library/KeysSetting';
|
||||
|
||||
import { getHashingAlgorithmSettings } from '../../screens/settings/library/KeysSetting';
|
||||
import { generatePassword } from '../key/KeyMounter';
|
||||
import { PasswordMeter } from '../key/PasswordMeter';
|
||||
|
||||
export default function CreateLibraryDialog({
|
||||
children,
|
||||
onSubmit,
|
||||
open,
|
||||
setOpen
|
||||
}: PropsWithChildren<{ onSubmit?: () => void; open: boolean; setOpen: (state: boolean) => void }>) {
|
||||
const { Input, z, useZodForm } = forms;
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
password: z.string(),
|
||||
password_validate: z.string(),
|
||||
secret_key: z.string(),
|
||||
algorithm: z.string(),
|
||||
hashing_algorithm: z.string()
|
||||
});
|
||||
|
||||
interface Props {
|
||||
onSubmit?: () => void;
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export default function CreateLibraryDialog(props: PropsWithChildren<Props>) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm({
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: {
|
||||
name: '',
|
||||
password: '' as string,
|
||||
password_validate: '' as string,
|
||||
secret_key: '' as string | null,
|
||||
password: '',
|
||||
algorithm: 'XChaCha20Poly1305',
|
||||
hashing_algorithm: 'Argon2id-s'
|
||||
}
|
||||
@@ -43,23 +53,21 @@ export default function CreateLibraryDialog({
|
||||
library
|
||||
]);
|
||||
|
||||
if (onSubmit) onSubmit();
|
||||
setOpen(false);
|
||||
props.onSubmit?.();
|
||||
props.setOpen(false);
|
||||
|
||||
form.reset();
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
const doSubmit = form.handleSubmit((data) => {
|
||||
if (data.secret_key === '') {
|
||||
data.secret_key = null;
|
||||
}
|
||||
|
||||
const _onSubmit = form.handleSubmit(async (data) => {
|
||||
if (data.password !== data.password_validate) {
|
||||
alert('Passwords are not the same');
|
||||
} else {
|
||||
return createLibrary.mutateAsync({
|
||||
await createLibrary.mutateAsync({
|
||||
...data,
|
||||
algorithm: data.algorithm as Algorithm,
|
||||
hashing_algorithm: getHashingAlgorithmSettings(data.hashing_algorithm)
|
||||
@@ -69,170 +77,159 @@ export default function CreateLibraryDialog({
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
form={form}
|
||||
onSubmit={_onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
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."
|
||||
ctaAction={doSubmit}
|
||||
loading={form.formState.isSubmitting}
|
||||
submitDisabled={!form.formState.isValid}
|
||||
ctaLabel="Create"
|
||||
trigger={children}
|
||||
trigger={props.children}
|
||||
>
|
||||
<form onSubmit={doSubmit}>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Library name</p>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Library name</p>
|
||||
<Input
|
||||
className="flex-grow w-full"
|
||||
placeholder="My Cool Library"
|
||||
{...form.register('name', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* TODO: Proper UI for this. Maybe checkbox for encrypted or not and then reveal these fields. Select encrypted by default. */}
|
||||
{/* <span className="text-sm">Make the secret key field empty to skip key setup.</span> */}
|
||||
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-center mt-2 mb-1 text-[0.95rem] font-bold">Key Manager</p>
|
||||
<div className="w-full my-1 h-[2px] bg-gray-500" />
|
||||
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Master password</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow w-full"
|
||||
placeholder="My Cool Library"
|
||||
disabled={form.formState.isSubmitting}
|
||||
{...form.register('name', { required: true })}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Password"
|
||||
type={showMasterPassword1 ? 'text' : 'password'}
|
||||
{...form.register('password')}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const password = generatePassword(32);
|
||||
|
||||
{/* TODO: Proper UI for this. Maybe checkbox for encrypted or not and then reveal these fields. Select encrypted by default. */}
|
||||
{/* <span className="text-sm">Make the secret key field empty to skip key setup.</span> */}
|
||||
form.setValue('password', password);
|
||||
form.setValue('password_validate', password);
|
||||
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-center mt-2 mb-1 text-[0.95rem] font-bold">Key Manager</p>
|
||||
<div className="w-full my-1 h-[2px] bg-gray-500" />
|
||||
setShowMasterPassword1(true);
|
||||
setShowMasterPassword2(true);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('password') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword1(!showMasterPassword1)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<MP1CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Master password (again)</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Password"
|
||||
type={showMasterPassword2 ? 'text' : 'password'}
|
||||
{...form.register('password_validate')}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword2(!showMasterPassword2)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<MP2CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Key secret (optional)</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret"
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
{...form.register('secret_key', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.setValue('secret_key', cryptoRandomString({ length: 24 }));
|
||||
setShowSecretKey(true);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('secret_key') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Master password</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
disabled={form.formState.isSubmitting}
|
||||
{...form.register('password')}
|
||||
placeholder="Password"
|
||||
type={showMasterPassword1 ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const password = generatePassword(32);
|
||||
form.setValue('password', password);
|
||||
form.setValue('password_validate', password);
|
||||
setShowMasterPassword1(true);
|
||||
setShowMasterPassword2(true);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('password') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword1(!showMasterPassword1)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MP1CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('algorithm')}
|
||||
onChange={(e) => form.setValue('algorithm', e)}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Master password (again)</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
disabled={form.formState.isSubmitting}
|
||||
{...form.register('password_validate')}
|
||||
placeholder="Password"
|
||||
type={showMasterPassword2 ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword2(!showMasterPassword2)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MP2CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Key secret (optional)</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret"
|
||||
disabled={form.formState.isSubmitting}
|
||||
{...form.register('secret_key')}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.setValue('secret_key', cryptoRandomString({ length: 24 }));
|
||||
setShowSecretKey(true);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('secret_key') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('hashing_algorithm')}
|
||||
onChange={(e) => form.setValue('hashing_algorithm', e)}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('algorithm')}
|
||||
onChange={(e) => form.setValue('algorithm', e)}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('hashing_algorithm')}
|
||||
onChange={(e) => form.setValue('hashing_algorithm', e)}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PasswordMeter password={form.watch('password')} />
|
||||
</form>
|
||||
<PasswordMeter password={form.watch('password')} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { RadioGroup } from '@headlessui/react';
|
||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog, Input, Switch } from '@sd/ui';
|
||||
import { Button, Dialog } from '@sd/ui';
|
||||
import { Eye, EyeSlash, Info } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
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;
|
||||
location_id: number | null;
|
||||
path_id: number | undefined;
|
||||
location_id: number;
|
||||
path_id: number;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
type FormValues = {
|
||||
type: 'password' | 'key';
|
||||
outputPath: string;
|
||||
password: string;
|
||||
saveToKeyManager: boolean;
|
||||
};
|
||||
const schema = z.object({
|
||||
type: z.union([z.literal('password'), z.literal('key')]),
|
||||
outputPath: z.string(),
|
||||
password: z.string(),
|
||||
saveToKeyManager: z.boolean()
|
||||
});
|
||||
|
||||
export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
||||
const platform = usePlatform();
|
||||
@@ -67,11 +68,10 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
||||
|
||||
const PasswordCurrentEyeIcon = show.password ? EyeSlash : Eye;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: {
|
||||
type: hasMountedKeys ? 'key' : 'password',
|
||||
outputPath: '',
|
||||
password: '',
|
||||
saveToKeyManager: true
|
||||
}
|
||||
});
|
||||
@@ -83,127 +83,123 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
||||
|
||||
props.setOpen(false);
|
||||
|
||||
props.location_id &&
|
||||
props.path_id &&
|
||||
decryptFile.mutate({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
output_path: output,
|
||||
password: pw,
|
||||
save_to_library: save
|
||||
});
|
||||
decryptFile.mutate({
|
||||
location_id: props.location_id,
|
||||
path_id: props.path_id,
|
||||
output_path: output,
|
||||
password: pw,
|
||||
save_to_library: save
|
||||
});
|
||||
|
||||
form.reset();
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title="Decrypt a file"
|
||||
description="Leave the output file blank for the default."
|
||||
loading={decryptFile.isLoading}
|
||||
ctaLabel="Decrypt"
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title="Decrypt a file"
|
||||
description="Leave the output file blank for the default."
|
||||
loading={decryptFile.isLoading}
|
||||
ctaLabel="Decrypt"
|
||||
>
|
||||
<RadioGroup
|
||||
value={form.watch('type')}
|
||||
onChange={(e: 'key' | 'password') => form.setValue('type', e)}
|
||||
className="mt-2"
|
||||
>
|
||||
<RadioGroup
|
||||
value={form.watch('type')}
|
||||
onChange={(e: 'key' | 'password') => form.setValue('type', e)}
|
||||
className="mt-2"
|
||||
>
|
||||
<span className="text-xs font-bold">Key Type</span>
|
||||
<div className="flex flex-row gap-2 mt-2">
|
||||
<RadioGroup.Option disabled={!hasMountedKeys} value="key">
|
||||
{({ checked }) => (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!hasMountedKeys}
|
||||
size="sm"
|
||||
variant={checked ? 'accent' : 'gray'}
|
||||
>
|
||||
Key Manager
|
||||
</Button>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
<RadioGroup.Option value="password">
|
||||
{({ checked }) => (
|
||||
<Button type="button" size="sm" variant={checked ? 'accent' : 'gray'}>
|
||||
Password
|
||||
</Button>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{form.watch('type') === 'password' && (
|
||||
<>
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<Input
|
||||
className={`flex-grow w-max !py-0.5`}
|
||||
placeholder="Password"
|
||||
{...form.register('password', { required: false })}
|
||||
type={show.password ? 'text' : 'password'}
|
||||
required
|
||||
/>
|
||||
<span className="text-xs font-bold">Key Type</span>
|
||||
<div className="flex flex-row gap-2 mt-2">
|
||||
<RadioGroup.Option disabled={!hasMountedKeys} value="key">
|
||||
{({ checked }) => (
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, password: !old.password }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
disabled={!hasMountedKeys}
|
||||
size="sm"
|
||||
variant={checked ? 'accent' : 'gray'}
|
||||
>
|
||||
<PasswordCurrentEyeIcon className="w-4 h-4" />
|
||||
Key Manager
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<div className="space-x-2">
|
||||
<Switch
|
||||
className="bg-app-selected"
|
||||
size="sm"
|
||||
checked={form.watch('saveToKeyManager')}
|
||||
onCheckedChange={(e) => form.setValue('saveToKeyManager', e)}
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-3 text-xs font-medium mt-0.5">Save to Key Manager</span>
|
||||
<Tooltip label="This key will be saved to the key manager">
|
||||
<Info className="w-4 h-4 ml-1.5 text-ink-faint mt-0.5" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Output file</span>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
<RadioGroup.Option value="password">
|
||||
{({ checked }) => (
|
||||
<Button type="button" size="sm" variant={checked ? 'accent' : 'gray'}>
|
||||
Password
|
||||
</Button>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{form.watch('type') === 'password' && (
|
||||
<>
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<Input
|
||||
className={`flex-grow w-max !py-0.5`}
|
||||
placeholder="Password"
|
||||
type={show.password ? 'text' : 'password'}
|
||||
{...form.register('password', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={form.watch('outputPath') !== '' ? 'accent' : 'gray'}
|
||||
className="h-[23px] text-xs leading-3 mt-2"
|
||||
onClick={() => setShow((old) => ({ ...old, password: !old.password }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 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,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
platform.saveFilePickerDialog().then((result) => {
|
||||
if (result) form.setValue('outputPath', result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select
|
||||
<PasswordCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<div className="space-x-2">
|
||||
<Switch
|
||||
className="bg-app-selected"
|
||||
size="sm"
|
||||
{...form.register('saveToKeyManager')}
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-3 text-xs font-medium mt-0.5">Save to Key Manager</span>
|
||||
<Tooltip label="This key will be saved to the key manager">
|
||||
<Info className="w-4 h-4 ml-1.5 text-ink-faint mt-0.5" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Output file</span>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant={form.watch('outputPath') !== '' ? 'accent' : 'gray'}
|
||||
className="h-[23px] text-xs leading-3 mt-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 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,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
platform.saveFilePickerDialog().then((result) => {
|
||||
if (result) form.setValue('outputPath', result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
import { useBridgeMutation } from '@sd/client';
|
||||
import { Dialog } from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
|
||||
export default function DeleteLibraryDialog(
|
||||
props: PropsWithChildren<{
|
||||
libraryUuid: string;
|
||||
}>
|
||||
) {
|
||||
const { useZodForm, z } = forms;
|
||||
|
||||
interface Props {
|
||||
libraryUuid: string;
|
||||
}
|
||||
|
||||
export default function DeleteLibraryDialog(props: PropsWithChildren<Props>) {
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteLib, isLoading: libDeletePending } = useBridgeMutation('library.delete', {
|
||||
const deleteLib = useBridgeMutation('library.delete', {
|
||||
onSuccess: () => {
|
||||
setOpenDeleteModal(false);
|
||||
queryClient.invalidateQueries(['library.list']);
|
||||
}
|
||||
});
|
||||
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={async () => {
|
||||
await deleteLib.mutateAsync(props.libraryUuid);
|
||||
}}
|
||||
open={openDeleteModal}
|
||||
setOpen={setOpenDeleteModal}
|
||||
title="Delete Library"
|
||||
description="Deleting a library will permanently the database, the files themselves will not be deleted."
|
||||
ctaAction={() => {
|
||||
deleteLib(props.libraryUuid);
|
||||
}}
|
||||
loading={libDeletePending}
|
||||
loading={deleteLib.isLoading}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={props.children}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Algorithm, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog, Select, SelectOption } from '@sd/ui';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import { getHashingAlgorithmString } from '../../screens/settings/library/KeysSetting';
|
||||
import { usePlatform } from '../../util/Platform';
|
||||
import { SelectOptionKeyList } from '../key/KeyList';
|
||||
import { Checkbox } from '../primitive/Checkbox';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
import { CheckBox, useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface EncryptDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
@@ -17,14 +16,14 @@ interface EncryptDialogProps {
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
encryptionAlgo: string;
|
||||
hashingAlgo: string;
|
||||
metadata: boolean;
|
||||
previewMedia: boolean;
|
||||
outputPath: string;
|
||||
};
|
||||
const schema = z.object({
|
||||
key: z.string(),
|
||||
encryptionAlgo: z.string(),
|
||||
hashingAlgo: z.string(),
|
||||
metadata: z.boolean(),
|
||||
previewMedia: z.boolean(),
|
||||
outputPath: z.string()
|
||||
});
|
||||
|
||||
export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
||||
const platform = usePlatform();
|
||||
@@ -66,19 +65,11 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
key: '',
|
||||
encryptionAlgo: 'XChaCha20Poly1305',
|
||||
hashingAlgo: 'Argon2id-s',
|
||||
metadata: false,
|
||||
previewMedia: false,
|
||||
outputPath: ''
|
||||
}
|
||||
const form = useZodForm({
|
||||
schema
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
const output = data.outputPath !== '' ? data.outputPath : null;
|
||||
props.setOpen(false);
|
||||
|
||||
props.location_id &&
|
||||
@@ -90,113 +81,107 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
||||
path_id: props.path_id,
|
||||
metadata: data.metadata,
|
||||
preview_media: data.previewMedia,
|
||||
output_path: output
|
||||
output_path: data.outputPath || null
|
||||
});
|
||||
|
||||
form.reset();
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title="Encrypt a file"
|
||||
description="Configure your encryption settings. Leave the output file blank for the default."
|
||||
loading={encryptFile.isLoading}
|
||||
ctaLabel="Encrypt"
|
||||
>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Key</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('key')}
|
||||
onChange={(e) => {
|
||||
UpdateKey(e);
|
||||
}}
|
||||
>
|
||||
{mountedUuids.data && <SelectOptionKeyList keys={mountedUuids.data} />}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Output file</span>
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title="Encrypt a file"
|
||||
description="Configure your encryption settings. Leave the output file blank for the default."
|
||||
loading={encryptFile.isLoading}
|
||||
ctaLabel="Encrypt"
|
||||
>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Key</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('key')}
|
||||
onChange={(e) => {
|
||||
UpdateKey(e);
|
||||
}}
|
||||
>
|
||||
{mountedUuids.data && <SelectOptionKeyList keys={mountedUuids.data} />}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Output file</span>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant={form.watch('outputPath') !== '' ? 'accent' : 'gray'}
|
||||
className="h-[23px] text-xs leading-3 mt-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 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,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
platform.saveFilePickerDialog().then((result) => {
|
||||
if (result) form.setValue('outputPath', result as string);
|
||||
<Button
|
||||
size="sm"
|
||||
variant={form.watch('outputPath') !== '' ? 'accent' : 'gray'}
|
||||
className="h-[23px] text-xs leading-3 mt-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 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,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: "System dialogs aren't supported on this platform.",
|
||||
inputBox: false
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
return;
|
||||
}
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('encryptionAlgo')}
|
||||
onChange={(e) => form.setValue('encryptionAlgo', e)}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2 text-gray-400/80"
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
value={form.watch('hashingAlgo')}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
platform.saveFilePickerDialog().then((result) => {
|
||||
if (result) form.setValue('outputPath', result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex">
|
||||
<span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Metadata</span>
|
||||
<Checkbox
|
||||
checked={form.watch('metadata')}
|
||||
onChange={(e) => form.setValue('metadata', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Preview Media</span>
|
||||
<Checkbox
|
||||
checked={form.watch('previewMedia')}
|
||||
onChange={(e) => form.setValue('previewMedia', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('encryptionAlgo')}
|
||||
onChange={(e) => form.setValue('encryptionAlgo', e)}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</Dialog>
|
||||
</form>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2 text-gray-400/80"
|
||||
onChange={() => {}}
|
||||
value={form.watch('hashingAlgo')}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex">
|
||||
<span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Metadata</span>
|
||||
<CheckBox {...form.register('metadata')} />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Preview Media</span>
|
||||
<CheckBox {...form.register('previewMedia')} />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ReactNode, useState } from 'react';
|
||||
import { getHashingAlgorithmString } from '../../screens/settings/library/KeysSetting';
|
||||
import { SelectOptionKeyList } from '../key/KeyList';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface KeyViewerDialogProps {
|
||||
trigger: ReactNode;
|
||||
}
|
||||
@@ -34,7 +36,11 @@ export const KeyUpdater = (props: {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const schema = z.object({});
|
||||
|
||||
export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
||||
const form = useZodForm({ schema });
|
||||
|
||||
const keys = useLibraryQuery(['keys.list'], {
|
||||
onSuccess: (data) => {
|
||||
if (key === '' && data.length !== 0) {
|
||||
@@ -51,102 +57,101 @@ export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
||||
const [hashingAlgo, setHashingAlgo] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={showKeyViewerDialog}
|
||||
setOpen={setShowKeyViewerDialog}
|
||||
trigger={props.trigger}
|
||||
title="View Key Values"
|
||||
description="Here you can view the values of your keys."
|
||||
ctaLabel="Done"
|
||||
ctaAction={() => {
|
||||
setShowKeyViewerDialog(false);
|
||||
}}
|
||||
>
|
||||
<KeyUpdater
|
||||
uuid={key}
|
||||
setKey={setKeyValue}
|
||||
setEncryptionAlgo={setEncryptionAlgo}
|
||||
setHashingAlgo={setHashingAlgo}
|
||||
setContentSalt={setContentSalt}
|
||||
/>
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={async () => {
|
||||
setShowKeyViewerDialog(false);
|
||||
}}
|
||||
open={showKeyViewerDialog}
|
||||
setOpen={setShowKeyViewerDialog}
|
||||
trigger={props.trigger}
|
||||
title="View Key Values"
|
||||
description="Here you can view the values of your keys."
|
||||
ctaLabel="Done"
|
||||
>
|
||||
<KeyUpdater
|
||||
uuid={key}
|
||||
setKey={setKeyValue}
|
||||
setEncryptionAlgo={setEncryptionAlgo}
|
||||
setHashingAlgo={setHashingAlgo}
|
||||
setContentSalt={setContentSalt}
|
||||
/>
|
||||
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Key</span>
|
||||
<Select
|
||||
className="mt-2 flex-grow"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
setKey(e);
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Key</span>
|
||||
<Select
|
||||
className="mt-2 flex-grow"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
setKey(e);
|
||||
}}
|
||||
>
|
||||
{keys.data && <SelectOptionKeyList keys={keys.data.map((key) => key.uuid)} />}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2 w-[150px] text-gray-300"
|
||||
value={encryptionAlgo}
|
||||
disabled
|
||||
onChange={() => {}}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select className="mt-2 text-gray-300" value={hashingAlgo} disabled onChange={() => {}}>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold mb-2">Content Salt (hex)</span>
|
||||
<div className="relative flex flex-grow">
|
||||
<Input value={contentSalt} disabled className="flex-grow !py-0.5" />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(contentSalt);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
{keys.data && <SelectOptionKeyList keys={keys.data.map((key) => key.uuid)} />}
|
||||
</Select>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2 w-[150px] text-gray-300"
|
||||
value={encryptionAlgo}
|
||||
disabled
|
||||
onChange={() => {}}
|
||||
</div>
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold mb-2">Key Value</span>
|
||||
<div className="relative flex flex-grow">
|
||||
<Input value={keyValue} disabled className="flex-grow !py-0.5" />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(keyValue);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select className="mt-2 text-gray-300" value={hashingAlgo} disabled onChange={() => {}}>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold mb-2">Content Salt (hex)</span>
|
||||
<div className="relative flex flex-grow">
|
||||
<Input value={contentSalt} disabled className="flex-grow !py-0.5" />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(contentSalt);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold mb-2">Key Value</span>
|
||||
<div className="relative flex flex-grow">
|
||||
<Input value={keyValue} disabled className="flex-grow !py-0.5" />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(keyValue);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,25 +3,26 @@ import { Button, Dialog, Input, Select, SelectOption } from '@sd/ui';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
type FormValues = {
|
||||
masterPassword: string;
|
||||
masterPassword2: string;
|
||||
secretKey: string | null;
|
||||
encryptionAlgo: string;
|
||||
hashingAlgo: string;
|
||||
};
|
||||
const schema = z.object({
|
||||
masterPassword: z.string(),
|
||||
masterPassword2: z.string(),
|
||||
secretKey: z.string().nullable(),
|
||||
encryptionAlgo: z.string(),
|
||||
hashingAlgo: z.string()
|
||||
});
|
||||
|
||||
export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => {
|
||||
const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword', {
|
||||
@@ -59,11 +60,9 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
||||
const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: {
|
||||
masterPassword: '',
|
||||
masterPassword2: '',
|
||||
secretKey: '',
|
||||
encryptionAlgo: 'XChaCha20Poly1305',
|
||||
hashingAlgo: 'Argon2id-s'
|
||||
}
|
||||
@@ -94,151 +93,148 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Dialog
|
||||
open={show.masterPasswordDialog}
|
||||
setOpen={(e) => {
|
||||
setShow((old) => ({ ...old, masterPasswordDialog: e }));
|
||||
}}
|
||||
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
|
||||
className={`flex-grow w-max !py-0.5`}
|
||||
placeholder="New password"
|
||||
required
|
||||
{...form.register('masterPassword', { required: true })}
|
||||
type={show.masterPassword ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const password = generatePassword(32);
|
||||
form.setValue('masterPassword', password);
|
||||
form.setValue('masterPassword2', password);
|
||||
setShow((old) => ({
|
||||
...old,
|
||||
masterPassword: true,
|
||||
masterPassword2: true
|
||||
}));
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('masterPassword') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MP1CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className={`flex-grow !py-0.5}`}
|
||||
placeholder="New password (again)"
|
||||
required
|
||||
{...form.register('masterPassword2', { required: true })}
|
||||
type={show.masterPassword2 ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, masterPassword2: !old.masterPassword2 }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MP2CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
open={show.masterPasswordDialog}
|
||||
setOpen={(e) => {
|
||||
setShow((old) => ({ ...old, masterPasswordDialog: e }));
|
||||
}}
|
||||
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
|
||||
className={`flex-grow w-max !py-0.5`}
|
||||
placeholder="New password"
|
||||
type={show.masterPassword ? 'text' : 'password'}
|
||||
{...form.register('masterPassword', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const password = generatePassword(32);
|
||||
form.setValue('masterPassword', password);
|
||||
form.setValue('masterPassword2', password);
|
||||
setShow((old) => ({
|
||||
...old,
|
||||
masterPassword: true,
|
||||
masterPassword2: true
|
||||
}));
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('masterPassword') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MP1CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className={`flex-grow !py-0.5}`}
|
||||
placeholder="New password (again)"
|
||||
type={show.masterPassword2 ? 'text' : 'password'}
|
||||
{...form.register('masterPassword2', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, masterPassword2: !old.masterPassword2 }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<MP2CurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className={`flex-grow !py-0.5}`}
|
||||
placeholder="Key secret"
|
||||
{...form.register('secretKey', { required: false })}
|
||||
type={show.secretKey ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
// onClick={() => setmasterPassword2(!masterPassword2)}
|
||||
onClick={() => {
|
||||
form.setValue('secretKey', cryptoRandomString({ length: 24 }));
|
||||
setShow((old) => ({ ...old, secretKey: true }));
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('secretKey') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className={`flex-grow !py-0.5}`}
|
||||
placeholder="Key secret"
|
||||
type={show.secretKey ? 'text' : 'password'}
|
||||
{...form.register('secretKey', { required: false })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.setValue('secretKey', cryptoRandomString({ length: 24 }));
|
||||
setShow((old) => ({ ...old, secretKey: true }));
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('secretKey') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PasswordMeter password={form.watch('masterPassword')} />
|
||||
<PasswordMeter password={form.watch('masterPassword')} />
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('encryptionAlgo')}
|
||||
onChange={(e) => form.setValue('encryptionAlgo', e)}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('hashingAlgo')}
|
||||
onChange={(e) => form.setValue('hashingAlgo', e)}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('encryptionAlgo')}
|
||||
onChange={(e) => form.setValue('encryptionAlgo', e)}
|
||||
>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</Dialog>
|
||||
</form>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2"
|
||||
value={form.watch('hashingAlgo')}
|
||||
onChange={(e) => form.setValue('hashingAlgo', e)}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,6 +41,8 @@ export default function Explorer(props: Props) {
|
||||
}
|
||||
});
|
||||
|
||||
const selectedItem = props.data?.items[expStore.selectedRowIndex];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
@@ -97,19 +99,19 @@ export default function Explorer(props: Props) {
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
{props.data && props.data.items[expStore.selectedRowIndex] && (
|
||||
{selectedItem && (
|
||||
<EncryptFileDialog
|
||||
location_id={expStore.locationId}
|
||||
path_id={props.data?.items[expStore.selectedRowIndex].id}
|
||||
path_id={selectedItem.id}
|
||||
open={showEncryptDialog}
|
||||
setOpen={setShowEncryptDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
)}
|
||||
{props.data && props.data.items[expStore.selectedRowIndex] && (
|
||||
{selectedItem && expStore.locationId !== null && (
|
||||
<DecryptFileDialog
|
||||
location_id={expStore.locationId}
|
||||
path_id={props.data?.items[expStore.selectedRowIndex].id}
|
||||
path_id={selectedItem.id}
|
||||
open={showDecryptDialog}
|
||||
setOpen={setShowDecryptDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { Button, Input, OverlayPanel, cva, tw } from '@sd/ui';
|
||||
import { Button, Input, OverlayPanel, cva } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
ArrowsClockwise,
|
||||
CaretLeft,
|
||||
CaretRight,
|
||||
ClockCounterClockwise,
|
||||
Columns,
|
||||
HourglassSimple,
|
||||
IconProps,
|
||||
Key,
|
||||
List,
|
||||
MonitorPlay,
|
||||
@@ -64,13 +60,15 @@ const topBarButtonStyle = cva(
|
||||
|
||||
const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
|
||||
|
||||
const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>((props, ref) => {
|
||||
return (
|
||||
<Button {...props} ref={ref} className={clsx(topBarButtonStyle(props), props.className)}>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>(
|
||||
({ active, rounding, className, ...props }, ref) => {
|
||||
return (
|
||||
<Button {...props} ref={ref} className={topBarButtonStyle({ active, rounding, className })}>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRef) => {
|
||||
const {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useState } from 'react';
|
||||
|
||||
import { Folder } from '../icons/Folder';
|
||||
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
interface LocationListItemProps {
|
||||
location: Location & { node: Node };
|
||||
}
|
||||
@@ -26,6 +28,8 @@ export default function LocationListItem({ location }: LocationListItemProps) {
|
||||
}
|
||||
);
|
||||
|
||||
const form = useZodForm({ schema: z.object({}) });
|
||||
|
||||
if (hide) return <></>;
|
||||
|
||||
return (
|
||||
@@ -53,13 +57,14 @@ export default function LocationListItem({ location }: LocationListItemProps) {
|
||||
</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."
|
||||
ctaAction={() => {
|
||||
deleteLoc(location.id);
|
||||
}}
|
||||
loading={locDeletePending}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@sd/ui';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { Button } from '@sd/ui';
|
||||
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
||||
|
||||
// TODO: This page requires styling for now it is just a placeholder.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
primaryColor?: string;
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = (props) => {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
type="checkbox"
|
||||
style={{}}
|
||||
className={clsx(
|
||||
`
|
||||
form-check-input appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2
|
||||
`,
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { UseControllerProps, useController } from 'react-hook-form';
|
||||
|
||||
import useClickOutside from '../../hooks/useClickOutside';
|
||||
|
||||
interface PopoverPickerProps {
|
||||
value: string;
|
||||
onChange: (color: string) => void;
|
||||
interface PopoverPickerProps extends UseControllerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PopoverPicker = ({ value, onChange, className }: PopoverPickerProps) => {
|
||||
export const PopoverPicker = ({ className, ...props }: PopoverPickerProps) => {
|
||||
const { field } = useController(props);
|
||||
const popover = useRef<HTMLDivElement | null>(null);
|
||||
const [isOpen, toggle] = useState(false);
|
||||
|
||||
@@ -21,7 +21,7 @@ export const PopoverPicker = ({ value, onChange, className }: PopoverPickerProps
|
||||
<div className={clsx('relative flex items-center mt-3', className)}>
|
||||
<div
|
||||
className={clsx('w-5 h-5 rounded-full shadow ', isOpen && 'dark:border-gray-500')}
|
||||
style={{ backgroundColor: value }}
|
||||
style={{ backgroundColor: field.value }}
|
||||
onClick={() => toggle(true)}
|
||||
/>
|
||||
{/* <span className="inline ml-2 text-sm text-gray-200">Pick Color</span> */}
|
||||
@@ -32,7 +32,7 @@ export const PopoverPicker = ({ value, onChange, className }: PopoverPickerProps
|
||||
className="absolute left-0 rounded-md shadow"
|
||||
ref={popover}
|
||||
>
|
||||
<HexColorPicker color={value} onChange={onChange} />
|
||||
<HexColorPicker color={field.value} onChange={field.onChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,65 +1,71 @@
|
||||
import { Tag, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { TagUpdateArgs } from '@sd/client';
|
||||
import { Button, Card, Dialog, Input, Switch } from '@sd/ui';
|
||||
import { Button, Card, Dialog, Switch } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useDebounce } from 'rooks';
|
||||
|
||||
import { InputContainer } from '~/components/primitive/InputContainer';
|
||||
import { PopoverPicker } from '~/components/primitive/PopoverPicker';
|
||||
import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '~/components/settings/SettingsHeader';
|
||||
|
||||
import { Form, Input, useZodForm, z } from '@sd/ui/src/forms';
|
||||
|
||||
export default function TagsSettings() {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
// creating new tag state
|
||||
const [newColor, setNewColor] = useState('#A717D9');
|
||||
const [newName, setNewName] = useState('');
|
||||
|
||||
const { data: tags } = useLibraryQuery(['tags.list']);
|
||||
|
||||
const [selectedTag, setSelectedTag] = useState<null | Tag>(tags?.[0] ?? null);
|
||||
|
||||
const { mutate: createTag, isLoading } = useLibraryMutation('tags.create', {
|
||||
const createTag = useLibraryMutation('tags.create', {
|
||||
onError: (e) => {
|
||||
console.error('error', e);
|
||||
},
|
||||
onSuccess: (_) => {
|
||||
setOpenCreateModal(false);
|
||||
}
|
||||
});
|
||||
|
||||
const updateTag = useLibraryMutation('tags.update');
|
||||
|
||||
const deleteTag = useLibraryMutation('tags.delete', {
|
||||
onSuccess: () => {
|
||||
setSelectedTag(null);
|
||||
}
|
||||
});
|
||||
|
||||
const { register, handleSubmit, watch, reset, control } = useForm({
|
||||
defaultValues: selectedTag as TagUpdateArgs
|
||||
const createForm = useZodForm({
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
color: z.string()
|
||||
}),
|
||||
defaultValues: {
|
||||
color: '#A717D9'
|
||||
}
|
||||
});
|
||||
const updateForm = useZodForm({
|
||||
schema: z.object({
|
||||
id: z.number(),
|
||||
name: z.string().nullable(),
|
||||
color: z.string().nullable()
|
||||
}),
|
||||
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
|
||||
const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), [submitTagUpdate]);
|
||||
|
||||
const setTag = useCallback(
|
||||
(tag: Tag | null) => {
|
||||
if (tag) reset(tag);
|
||||
if (tag) updateForm.reset(tag);
|
||||
setSelectedTag(tag);
|
||||
},
|
||||
[setSelectedTag, reset]
|
||||
[setSelectedTag, updateForm]
|
||||
);
|
||||
|
||||
const submitTagUpdate = handleSubmit((data) => updateTag.mutate(data));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), []);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => autoUpdateTag());
|
||||
const subscription = updateForm.watch(() => autoUpdateTag());
|
||||
return () => subscription.unsubscribe();
|
||||
});
|
||||
}, [updateForm, autoUpdateTag]);
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
@@ -69,17 +75,16 @@ export default function TagsSettings() {
|
||||
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."
|
||||
ctaAction={() => {
|
||||
createTag({
|
||||
name: newName,
|
||||
color: newColor
|
||||
});
|
||||
}}
|
||||
loading={isLoading}
|
||||
loading={createTag.isLoading}
|
||||
ctaLabel="Create"
|
||||
trigger={
|
||||
<Button variant="accent" size="sm">
|
||||
@@ -90,12 +95,10 @@ export default function TagsSettings() {
|
||||
<div className="relative mt-3 ">
|
||||
<PopoverPicker
|
||||
className="!absolute left-[9px] -top-[3px]"
|
||||
value={newColor}
|
||||
onChange={setNewColor}
|
||||
{...createForm.register('color')}
|
||||
/>
|
||||
<Input
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
{...createForm.register('name', { required: true })}
|
||||
className="w-full pl-[40px]"
|
||||
placeholder="Name"
|
||||
/>
|
||||
@@ -122,43 +125,36 @@ export default function TagsSettings() {
|
||||
</div>
|
||||
</Card>
|
||||
{selectedTag ? (
|
||||
<form onSubmit={submitTagUpdate}>
|
||||
<Form form={updateForm} onSubmit={submitTagUpdate}>
|
||||
<div className="flex flex-row mb-10 space-x-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">
|
||||
Color
|
||||
</span>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<PopoverPicker
|
||||
className="!absolute left-[9px] -top-[3px]"
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
<PopoverPicker
|
||||
className="!absolute left-[9px] -top-[3px]"
|
||||
{...updateForm.register('color')}
|
||||
/>
|
||||
<Input className="w-28 pl-[40px]" {...register('color')} />
|
||||
<Input className="w-28 pl-[40px]" {...updateForm.register('color')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">
|
||||
Name
|
||||
</span>
|
||||
<Input {...register('name')} />
|
||||
<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."
|
||||
ctaAction={() => {
|
||||
deleteTag.mutate(selectedTag.id);
|
||||
}}
|
||||
loading={deleteTag.isLoading}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={
|
||||
@@ -175,7 +171,7 @@ export default function TagsSettings() {
|
||||
>
|
||||
<Switch checked />
|
||||
</InputContainer>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<div className="text-sm font-medium text-gray-400">No Tag Selected</div>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./src/forms": "./src/forms/index.ts",
|
||||
"./postcss": "./style/postcss.config.js",
|
||||
"./tailwind": "./style/tailwind.js",
|
||||
"./style": "./style/index.js",
|
||||
@@ -29,7 +30,7 @@
|
||||
"@radix-ui/react-tabs": "^1.0.1",
|
||||
"@sd/assets": "workspace:*",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"class-variance-authority": "^0.2.3",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"postcss": "^8.4.17",
|
||||
|
||||
@@ -22,15 +22,16 @@ type Button = {
|
||||
const hasHref = (props: ButtonProps | LinkButtonProps): props is LinkButtonProps => 'href' in props;
|
||||
|
||||
const styles = cva(
|
||||
'border rounded-md items-center transition-colors duration-100 cursor-default disabled:opacity-50 outline-none ring-offset-app-box focus:ring-2 focus:ring-accent focus:ring-offset-2',
|
||||
[
|
||||
'border rounded-md items-center transition-colors duration-100 cursor-default outline-none',
|
||||
'disabled:opacity-70 disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||
'ring-offset-app-box focus:ring-2 focus:ring-accent focus:ring-offset-2'
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
pressEffect: {
|
||||
true: 'active:translate-y-[1px]'
|
||||
},
|
||||
disabled: {
|
||||
true: 'opacity-70 pointer-events-none cursor-not-allowed'
|
||||
},
|
||||
size: {
|
||||
icon: '!p-1',
|
||||
md: 'py-1 px-3 text-md font-medium',
|
||||
@@ -70,7 +71,7 @@ export const Button = forwardRef<
|
||||
return hasHref(props) ? (
|
||||
<a {...props} ref={ref as any} className={cx(className, 'no-underline inline-block')} />
|
||||
) : (
|
||||
<button {...(props as ButtonProps)} ref={ref as any} className={className} />
|
||||
<button type="button" {...(props as ButtonProps)} ref={ref as any} className={className} />
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
18
packages/ui/src/CheckBox.tsx
Normal file
18
packages/ui/src/CheckBox.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { ComponentProps, forwardRef } from 'react';
|
||||
|
||||
const styles = cva(
|
||||
[
|
||||
'form-check-input appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2',
|
||||
'checked:bg-blue-600 checked:border-blue-600 focus:outline-none '
|
||||
],
|
||||
{ variants: {} }
|
||||
);
|
||||
|
||||
export interface CheckBoxProps extends ComponentProps<'input'>, VariantProps<typeof styles> {}
|
||||
|
||||
export const CheckBox = forwardRef<HTMLInputElement, CheckBoxProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<input {...props} type="checkbox" ref={ref} className={styles({ className })} />
|
||||
)
|
||||
);
|
||||
@@ -1,17 +1,20 @@
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import { animated, useTransition } from 'react-spring';
|
||||
|
||||
import { Button, Loader } from '../';
|
||||
import { Form, FormProps } from './forms/Form';
|
||||
|
||||
export interface DialogProps extends DialogPrimitive.DialogProps {
|
||||
export interface DialogProps<S extends FieldValues>
|
||||
extends DialogPrimitive.DialogProps,
|
||||
FormProps<S> {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
trigger?: ReactNode;
|
||||
ctaLabel?: string;
|
||||
ctaDanger?: boolean;
|
||||
ctaAction?: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children?: ReactNode;
|
||||
@@ -20,7 +23,13 @@ export interface DialogProps extends DialogPrimitive.DialogProps {
|
||||
submitDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function Dialog({ open, setOpen: onOpenChange, ...props }: DialogProps) {
|
||||
export function Dialog<S extends FieldValues>({
|
||||
form,
|
||||
onSubmit,
|
||||
open,
|
||||
setOpen: onOpenChange,
|
||||
...props
|
||||
}: DialogProps<S>) {
|
||||
const transitions = useTransition(open, {
|
||||
from: {
|
||||
opacity: 0,
|
||||
@@ -44,49 +53,7 @@ export function Dialog({ open, setOpen: onOpenChange, ...props }: DialogProps) {
|
||||
style={{
|
||||
opacity: styles.opacity
|
||||
}}
|
||||
>
|
||||
<DialogPrimitive.Content forceMount asChild>
|
||||
<animated.div
|
||||
style={styles}
|
||||
className="min-w-[300px] max-w-[400px] rounded-md bg-app-box border border-app-line text-ink shadow-2xl shadow-app-shade/230"
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.ctaAction) props.ctaAction();
|
||||
}}
|
||||
>
|
||||
<div className="p-5">
|
||||
<DialogPrimitive.Title className="mb-2 font-bold">
|
||||
{props.title}
|
||||
</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Description className="text-sm text-ink-dull">
|
||||
{props.description}
|
||||
</DialogPrimitive.Description>
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end px-3 py-3 space-x-2 border-t bg-app/20 border-app-line">
|
||||
{props.loading && <Loader />}
|
||||
<div className="flex-grow" />
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button disabled={props.loading} size="sm" variant="gray">
|
||||
Close
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={props.loading || props.submitDisabled}
|
||||
variant={props.ctaDanger ? 'colored' : 'accent'}
|
||||
className={clsx(props.ctaDanger && 'bg-red-500 border-red-500')}
|
||||
>
|
||||
{props.ctaLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</animated.div>
|
||||
</DialogPrimitive.Content>
|
||||
</animated.div>
|
||||
/>
|
||||
</DialogPrimitive.Overlay>
|
||||
|
||||
<DialogPrimitive.Content asChild forceMount>
|
||||
@@ -94,12 +61,10 @@ export function Dialog({ open, setOpen: onOpenChange, ...props }: DialogProps) {
|
||||
className="z-50 fixed top-0 bottom-0 left-0 right-0 grid place-items-center !pointer-events-none"
|
||||
style={styles}
|
||||
>
|
||||
<form
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
className="min-w-[300px] max-w-[400px] rounded-md bg-app-box border border-app-line text-ink shadow-app-shade !pointer-events-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.ctaAction) props.ctaAction();
|
||||
}}
|
||||
>
|
||||
<div className="p-5">
|
||||
<DialogPrimitive.Title className="mb-2 font-bold">
|
||||
@@ -111,7 +76,7 @@ export function Dialog({ open, setOpen: onOpenChange, ...props }: DialogProps) {
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end px-3 py-3 space-x-2 border-t bg-app-selected border-app-line">
|
||||
{props.loading && <Loader />}
|
||||
{form.formState.isSubmitting && <Loader />}
|
||||
<div className="flex-grow" />
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button disabled={props.loading} size="sm" variant="gray">
|
||||
@@ -121,14 +86,14 @@ export function Dialog({ open, setOpen: onOpenChange, ...props }: DialogProps) {
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={props.loading || props.submitDisabled}
|
||||
disabled={form.formState.isSubmitting || props.submitDisabled}
|
||||
variant={props.ctaDanger ? 'colored' : 'accent'}
|
||||
className={clsx(props.ctaDanger && 'bg-red-500 border-red-500')}
|
||||
>
|
||||
{props.ctaLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</animated.div>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { VariantProps, cva } from 'class-variance-authority';
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren, forwardRef } from 'react';
|
||||
|
||||
export interface InputBaseProps extends VariantProps<typeof inputStyles> {}
|
||||
export interface InputBaseProps extends VariantProps<typeof styles> {}
|
||||
|
||||
export type InputProps = InputBaseProps & React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export type TextareaProps = InputBaseProps & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
const inputStyles = cva(
|
||||
const styles = cva(
|
||||
[
|
||||
'px-3 py-1 text-sm rounded-md border leading-7',
|
||||
'outline-none shadow-sm focus:ring-2 transition-all'
|
||||
@@ -33,19 +33,13 @@ const inputStyles = cva(
|
||||
);
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ size, variant, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clsx(inputStyles({ size, variant }), props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
({ variant, size, className, ...props }, ref) => (
|
||||
<input {...props} ref={ref} className={styles({ variant, size, className })} />
|
||||
)
|
||||
);
|
||||
|
||||
export const TextArea = ({ size, variant, ...props }: TextareaProps) => {
|
||||
return <textarea {...props} className={clsx(inputStyles({ size, variant }), props.className)} />;
|
||||
return <textarea {...props} className={clsx(styles({ size, variant }), props.className)} />;
|
||||
};
|
||||
|
||||
export function Label(props: PropsWithChildren<{ slug?: string }>) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||
import { VariantProps, cva, cx } from 'class-variance-authority';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export type SwitchProps = VariantProps<typeof switchStyles> &
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
SwitchPrimitive.SwitchProps;
|
||||
export interface SwitchProps
|
||||
extends VariantProps<typeof switchStyles>,
|
||||
SwitchPrimitive.SwitchProps {}
|
||||
|
||||
const switchStyles = cva(
|
||||
[
|
||||
@@ -45,13 +45,10 @@ const thumbStyles = cva(
|
||||
}
|
||||
);
|
||||
|
||||
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch(
|
||||
props,
|
||||
forwardedRef
|
||||
) {
|
||||
return (
|
||||
<SwitchPrimitive.Root {...props} ref={forwardedRef} className={cx(switchStyles(props))}>
|
||||
<SwitchPrimitive.Thumb className={cx(thumbStyles(props))} />
|
||||
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(
|
||||
({ size, className, ...props }, ref) => (
|
||||
<SwitchPrimitive.Root {...props} ref={ref} className={switchStyles({ size, className })}>
|
||||
<SwitchPrimitive.Thumb className={thumbStyles({ size, className })} />
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
16
packages/ui/src/forms/CheckBox.tsx
Normal file
16
packages/ui/src/forms/CheckBox.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { CheckBox as Root } from '../CheckBox';
|
||||
import { FormField, UseFormFieldProps, useFormField } from './FormField';
|
||||
|
||||
export interface CheckBoxProps extends UseFormFieldProps {}
|
||||
|
||||
export const CheckBox = forwardRef<HTMLInputElement, CheckBoxProps>((props, ref) => {
|
||||
const { formFieldProps, childProps } = useFormField(props);
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps}>
|
||||
<Root {...childProps} ref={ref} />
|
||||
</FormField>
|
||||
);
|
||||
});
|
||||
50
packages/ui/src/forms/Form.tsx
Normal file
50
packages/ui/src/forms/Form.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ComponentProps } from 'react';
|
||||
import {
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
UseFormHandleSubmit,
|
||||
UseFormProps,
|
||||
UseFormReturn,
|
||||
useForm
|
||||
} from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface FormProps<T extends FieldValues> extends Omit<ComponentProps<'form'>, 'onSubmit'> {
|
||||
form: UseFormReturn<T>;
|
||||
onSubmit: ReturnType<UseFormHandleSubmit<T>>;
|
||||
}
|
||||
|
||||
export const Form = <T extends FieldValues>({
|
||||
form,
|
||||
onSubmit,
|
||||
children,
|
||||
...props
|
||||
}: FormProps<T>) => (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.stopPropagation();
|
||||
return onSubmit(e);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{/* <fieldset> passes the form's 'disabled' state to all of its elements,
|
||||
allowing us to handle disabled style variants with just css */}
|
||||
<fieldset disabled={form.formState.isSubmitting}>{children}</fieldset>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
|
||||
interface UseZodFormProps<S extends z.ZodSchema>
|
||||
extends Exclude<UseFormProps<z.infer<S>>, 'resolver'> {
|
||||
schema: S;
|
||||
}
|
||||
|
||||
export const useZodForm = <S extends z.ZodSchema>({ schema, ...formProps }: UseZodFormProps<S>) =>
|
||||
useForm({
|
||||
...formProps,
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
export { z } from 'zod';
|
||||
28
packages/ui/src/forms/FormField.tsx
Normal file
28
packages/ui/src/forms/FormField.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { PropsWithChildren, useId } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface UseFormFieldProps extends PropsWithChildren {
|
||||
name: string;
|
||||
// label: string;
|
||||
}
|
||||
|
||||
export const useFormField = <P extends UseFormFieldProps>(props: P) => {
|
||||
const { name, ...otherProps } = props;
|
||||
const id = useId();
|
||||
|
||||
return {
|
||||
formFieldProps: { id, name },
|
||||
childProps: { ...otherProps, id, name }
|
||||
};
|
||||
};
|
||||
|
||||
interface FormFieldProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const FormField = ({ name, children }: PropsWithChildren<FormFieldProps>) => {
|
||||
const ctx = useFormContext();
|
||||
const _ = ctx.getFieldState(name);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
18
packages/ui/src/forms/Input.tsx
Normal file
18
packages/ui/src/forms/Input.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import * as Root from '../Input';
|
||||
import { FormField, UseFormFieldProps, useFormField } from './FormField';
|
||||
|
||||
export interface InputProps extends UseFormFieldProps, Root.InputProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const { formFieldProps, childProps } = useFormField(props);
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps}>
|
||||
<Root.Input {...childProps} ref={ref} />
|
||||
</FormField>
|
||||
);
|
||||
});
|
||||
20
packages/ui/src/forms/Switch.tsx
Normal file
20
packages/ui/src/forms/Switch.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { useController } from 'react-hook-form';
|
||||
|
||||
import * as Root from '../Switch';
|
||||
import { FormField, UseFormFieldProps, useFormField } from './FormField';
|
||||
|
||||
export interface SwitchProps extends UseFormFieldProps, Root.SwitchProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
|
||||
const { field } = useController(props);
|
||||
const { formFieldProps, childProps } = useFormField(props);
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps}>
|
||||
<Root.Switch {...childProps} value={field.value} onCheckedChange={field.value} ref={ref} />
|
||||
</FormField>
|
||||
);
|
||||
});
|
||||
5
packages/ui/src/forms/index.ts
Normal file
5
packages/ui/src/forms/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './Form';
|
||||
export * from './FormField';
|
||||
export * from './CheckBox';
|
||||
export * from './Input';
|
||||
export * from './Switch';
|
||||
@@ -11,4 +11,6 @@ export * from './Switch';
|
||||
export * as Tabs from './Tabs';
|
||||
export * from './Typography';
|
||||
export * from './utils';
|
||||
export * from './CheckBox';
|
||||
export * as forms from './forms';
|
||||
export { cva, cx } from 'class-variance-authority';
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user