mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-07 06:43:29 +00:00
[ENG-1023] Change location path from settings page (#1301)
* update location path in db * remove + add location watcher if path is changed --------- Co-authored-by: Ericson "Fogo" Soares <ericson.ds999@gmail.com>
This commit is contained in:
parent
99ccb8f8c7
commit
4b60ff2e08
|
@ -21,6 +21,7 @@ import { type SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
displayName: z.string().nullable(),
|
displayName: z.string().nullable(),
|
||||||
|
path: z.string().min(1).nullable(),
|
||||||
localPath: z.string().nullable(),
|
localPath: z.string().nullable(),
|
||||||
indexer_rules_ids: z.array(z.string()),
|
indexer_rules_ids: z.array(z.string()),
|
||||||
generatePreviewMedia: z.boolean().nullable(),
|
generatePreviewMedia: z.boolean().nullable(),
|
||||||
|
@ -51,6 +52,7 @@ const EditLocationSettingsScreen = ({
|
||||||
updateLocation.mutateAsync({
|
updateLocation.mutateAsync({
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
name: data.displayName,
|
name: data.displayName,
|
||||||
|
path: data.path,
|
||||||
sync_preview_media: data.syncPreviewMedia,
|
sync_preview_media: data.syncPreviewMedia,
|
||||||
generate_preview_media: data.generatePreviewMedia,
|
generate_preview_media: data.generatePreviewMedia,
|
||||||
hidden: data.hidden,
|
hidden: data.hidden,
|
||||||
|
|
|
@ -182,8 +182,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||||
})
|
})
|
||||||
.procedure("update", {
|
.procedure("update", {
|
||||||
R.with2(library())
|
R.with2(library())
|
||||||
.mutation(|(_, library), args: LocationUpdateArgs| async move {
|
.mutation(|(node, library), args: LocationUpdateArgs| async move {
|
||||||
let ret = args.update(&library).await.map_err(Into::into);
|
let ret = args.update(&node, &library).await.map_err(Into::into);
|
||||||
invalidate_query!(library, "locations.list");
|
invalidate_query!(library, "locations.list");
|
||||||
ret
|
ret
|
||||||
})
|
})
|
||||||
|
|
|
@ -256,16 +256,17 @@ impl LocationCreateArgs {
|
||||||
/// Old rules that aren't in this vector will be purged.
|
/// Old rules that aren't in this vector will be purged.
|
||||||
#[derive(Type, Deserialize)]
|
#[derive(Type, Deserialize)]
|
||||||
pub struct LocationUpdateArgs {
|
pub struct LocationUpdateArgs {
|
||||||
pub id: location::id::Type,
|
id: location::id::Type,
|
||||||
pub name: Option<String>,
|
name: Option<String>,
|
||||||
pub generate_preview_media: Option<bool>,
|
generate_preview_media: Option<bool>,
|
||||||
pub sync_preview_media: Option<bool>,
|
sync_preview_media: Option<bool>,
|
||||||
pub hidden: Option<bool>,
|
hidden: Option<bool>,
|
||||||
pub indexer_rules_ids: Vec<i32>,
|
indexer_rules_ids: Vec<i32>,
|
||||||
|
path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocationUpdateArgs {
|
impl LocationUpdateArgs {
|
||||||
pub async fn update(self, library: &Arc<Library>) -> Result<(), LocationError> {
|
pub async fn update(self, node: &Node, library: &Arc<Library>) -> Result<(), LocationError> {
|
||||||
let Library { sync, db, .. } = &**library;
|
let Library { sync, db, .. } = &**library;
|
||||||
|
|
||||||
let location = find_location(library, self.id)
|
let location = find_location(library, self.id)
|
||||||
|
@ -274,9 +275,10 @@ impl LocationUpdateArgs {
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LocationError::IdNotFound(self.id))?;
|
.ok_or(LocationError::IdNotFound(self.id))?;
|
||||||
|
|
||||||
|
let name = self.name.clone();
|
||||||
|
|
||||||
let (sync_params, db_params): (Vec<_>, Vec<_>) = [
|
let (sync_params, db_params): (Vec<_>, Vec<_>) = [
|
||||||
self.name
|
self.name
|
||||||
.clone()
|
|
||||||
.filter(|name| location.name.as_ref() != Some(name))
|
.filter(|name| location.name.as_ref() != Some(name))
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
(
|
(
|
||||||
|
@ -302,6 +304,12 @@ impl LocationUpdateArgs {
|
||||||
location::hidden::set(Some(v)),
|
location::hidden::set(Some(v)),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
self.path.clone().map(|v| {
|
||||||
|
(
|
||||||
|
(location::path::NAME, json!(v)),
|
||||||
|
location::path::set(Some(v)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
@ -336,11 +344,16 @@ impl LocationUpdateArgs {
|
||||||
SpacedriveLocationMetadataFile::try_load(path).await?
|
SpacedriveLocationMetadataFile::try_load(path).await?
|
||||||
{
|
{
|
||||||
metadata
|
metadata
|
||||||
.update(library.id, maybe_missing(self.name, "location.name")?)
|
.update(library.id, maybe_missing(name, "location.name")?)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.path.is_some() {
|
||||||
|
node.locations.remove(self.id, library.clone()).await?;
|
||||||
|
node.locations.add(self.id, library.clone()).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_rules_ids = location
|
let current_rules_ids = location
|
||||||
|
|
|
@ -278,6 +278,7 @@ export const RenameLocationTextBox = (props: Omit<Props, 'renameHandler'>) => {
|
||||||
try {
|
try {
|
||||||
await renameLocation.mutateAsync({
|
await renameLocation.mutateAsync({
|
||||||
id: props.locationId,
|
id: props.locationId,
|
||||||
|
path: null,
|
||||||
name: newName,
|
name: newName,
|
||||||
generate_preview_media: null,
|
generate_preview_media: null,
|
||||||
sync_preview_media: null,
|
sync_preview_media: null,
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Pencil, Plus, Trash } from 'phosphor-react';
|
import { Pencil, Plus, Trash } from 'phosphor-react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { ContextMenu as CM, dialogManager, toast } from '@sd/ui';
|
import { ContextMenu as CM, dialogManager, toast } from '@sd/ui';
|
||||||
import {
|
import { AddLocationDialog } from '~/app/$libraryId/settings/library/locations/AddLocationDialog';
|
||||||
AddLocationDialog,
|
|
||||||
openDirectoryPickerDialog
|
|
||||||
} from '~/app/$libraryId/settings/library/locations/AddLocationDialog';
|
|
||||||
import DeleteDialog from '~/app/$libraryId/settings/library/locations/DeleteDialog';
|
import DeleteDialog from '~/app/$libraryId/settings/library/locations/DeleteDialog';
|
||||||
|
import { openDirectoryPickerDialog } from '~/app/$libraryId/settings/library/locations/openDirectoryPickerDialog';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react';
|
import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react';
|
||||||
import { Suspense } from 'react';
|
import { Suspense, useEffect } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||||
import {
|
import {
|
||||||
|
@ -21,13 +21,14 @@ import ModalLayout from '~/app/$libraryId/settings/ModalLayout';
|
||||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||||
import { useZodRouteParams } from '~/hooks';
|
import { useZodRouteParams } from '~/hooks';
|
||||||
import IndexerRuleEditor from './IndexerRuleEditor';
|
import IndexerRuleEditor from './IndexerRuleEditor';
|
||||||
|
import { LocationPathInputField } from './PathInput';
|
||||||
|
|
||||||
const FlexCol = tw.label`flex flex-col flex-1`;
|
const FlexCol = tw.label`flex flex-col flex-1`;
|
||||||
const ToggleSection = tw.label`flex flex-row w-full`;
|
const ToggleSection = tw.label`flex flex-row w-full`;
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().nullable(),
|
name: z.string().nullable(),
|
||||||
path: z.string().nullable(),
|
path: z.string().min(1).nullable(),
|
||||||
hidden: z.boolean().nullable(),
|
hidden: z.boolean().nullable(),
|
||||||
indexerRulesIds: z.array(z.number()),
|
indexerRulesIds: z.array(z.number()),
|
||||||
locationType: z.string(),
|
locationType: z.string(),
|
||||||
|
@ -51,23 +52,6 @@ const EditLocationForm = () => {
|
||||||
|
|
||||||
const locationData = useLibraryQuery(['locations.getWithRules', locationId], {
|
const locationData = useLibraryQuery(['locations.getWithRules', locationId], {
|
||||||
suspense: true
|
suspense: true
|
||||||
// onSettled: (data, error) => {
|
|
||||||
// if (isFirstLoad) {
|
|
||||||
// // @ts-expect-error // TODO: Fix the types
|
|
||||||
// if (!data && error == null) error = new Error('Failed to load location settings');
|
|
||||||
|
|
||||||
// // Return to previous page when no data is available at first load
|
|
||||||
// if (error) navigate(-1);
|
|
||||||
// else setIsFirstLoad(false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// showAlertDialog({
|
|
||||||
// title: 'Error',
|
|
||||||
// value: 'Failed to load location settings'
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
|
@ -94,17 +78,15 @@ const EditLocationForm = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isDirty } = form.formState;
|
const onSubmit = form.handleSubmit((data) =>
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(
|
|
||||||
({ name, hidden, indexerRulesIds, syncPreviewMedia, generatePreviewMedia }) =>
|
|
||||||
updateLocation.mutateAsync({
|
updateLocation.mutateAsync({
|
||||||
id: locationId,
|
id: locationId,
|
||||||
name,
|
path: data.path,
|
||||||
hidden,
|
name: data.name,
|
||||||
indexer_rules_ids: indexerRulesIds,
|
hidden: data.hidden,
|
||||||
sync_preview_media: syncPreviewMedia,
|
indexer_rules_ids: data.indexerRulesIds,
|
||||||
generate_preview_media: generatePreviewMedia
|
sync_preview_media: data.syncPreviewMedia,
|
||||||
|
generate_preview_media: data.generatePreviewMedia
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,15 +96,15 @@ const EditLocationForm = () => {
|
||||||
title="Edit Location"
|
title="Edit Location"
|
||||||
topRight={
|
topRight={
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
{isDirty && (
|
{form.formState.isDirty && (
|
||||||
<Button onClick={() => form.reset()} variant="outline" size="sm">
|
<Button onClick={() => form.reset()} variant="outline" size="sm">
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isDirty || form.formState.isSubmitting}
|
disabled={!form.formState.isDirty || form.formState.isSubmitting}
|
||||||
variant={isDirty ? 'accent' : 'outline'}
|
variant={form.formState.isDirty ? 'accent' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
|
@ -139,12 +121,7 @@ const EditLocationForm = () => {
|
||||||
</InfoText>
|
</InfoText>
|
||||||
</FlexCol>
|
</FlexCol>
|
||||||
<FlexCol>
|
<FlexCol>
|
||||||
<InputField
|
<LocationPathInputField label="Path" {...form.register('path')} />
|
||||||
label="Local Path"
|
|
||||||
readOnly={true}
|
|
||||||
className="text-ink-dull"
|
|
||||||
{...form.register('path')}
|
|
||||||
/>
|
|
||||||
<InfoText className="mt-2">
|
<InfoText className="mt-2">
|
||||||
The path to this Location, this is where the files will be stored on
|
The path to this Location, this is where the files will be stored on
|
||||||
disk.
|
disk.
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { useRef, useState } from 'react';
|
||||||
import { Button, type ButtonProps, dialogManager } from '@sd/ui';
|
import { Button, type ButtonProps, dialogManager } from '@sd/ui';
|
||||||
import { useCallbackToWatchResize } from '~/hooks';
|
import { useCallbackToWatchResize } from '~/hooks';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
import { AddLocationDialog, openDirectoryPickerDialog } from './AddLocationDialog';
|
import { AddLocationDialog } from './AddLocationDialog';
|
||||||
|
import { openDirectoryPickerDialog } from './openDirectoryPickerDialog';
|
||||||
|
|
||||||
interface AddLocationButton extends ButtonProps {
|
interface AddLocationButton extends ButtonProps {
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import clsx from 'clsx';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { CaretDown } from 'phosphor-react';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Controller, get } from 'react-hook-form';
|
import { Controller, get } from 'react-hook-form';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import {
|
import {
|
||||||
|
@ -11,11 +9,12 @@ import {
|
||||||
usePlausibleEvent,
|
usePlausibleEvent,
|
||||||
useZodForm
|
useZodForm
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
import { Dialog, ErrorMessage, InputField, UseDialogProps, toast, useDialog, z } from '@sd/ui';
|
import { Dialog, ErrorMessage, UseDialogProps, toast, useDialog, z } from '@sd/ui';
|
||||||
import Accordion from '~/components/Accordion';
|
import Accordion from '~/components/Accordion';
|
||||||
import { useCallbackToWatchForm } from '~/hooks';
|
import { useCallbackToWatchForm } from '~/hooks';
|
||||||
import { Platform, usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
import IndexerRuleEditor from './IndexerRuleEditor';
|
import IndexerRuleEditor from './IndexerRuleEditor';
|
||||||
|
import { LocationPathInputField } from './PathInput';
|
||||||
|
|
||||||
const REMOTE_ERROR_FORM_FIELD = 'root.serverError';
|
const REMOTE_ERROR_FORM_FIELD = 'root.serverError';
|
||||||
const REMOTE_ERROR_FORM_MESSAGE = {
|
const REMOTE_ERROR_FORM_MESSAGE = {
|
||||||
|
@ -39,18 +38,6 @@ const schema = z.object({
|
||||||
|
|
||||||
type SchemaType = z.infer<typeof schema>;
|
type SchemaType = z.infer<typeof schema>;
|
||||||
|
|
||||||
export const openDirectoryPickerDialog = async (platform: Platform): Promise<null | string> => {
|
|
||||||
if (!platform.openDirectoryPickerDialog) return null;
|
|
||||||
|
|
||||||
const path = await platform.openDirectoryPickerDialog();
|
|
||||||
if (!path) return '';
|
|
||||||
if (typeof path !== 'string')
|
|
||||||
// TODO: Should adding multiple locations simultaneously be implemented?
|
|
||||||
throw new Error('Adding multiple locations simultaneously is not supported');
|
|
||||||
|
|
||||||
return path;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface AddLocationDialog extends UseDialogProps {
|
export interface AddLocationDialog extends UseDialogProps {
|
||||||
path: string;
|
path: string;
|
||||||
method?: RemoteErrorFormMessage;
|
method?: RemoteErrorFormMessage;
|
||||||
|
@ -129,7 +116,7 @@ export const AddLocationDialog = ({
|
||||||
throw new Error('Unimplemented custom remote error handling');
|
throw new Error('Unimplemented custom remote error handling');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[createLocation, relinkLocation, addLocationToLibrary, addLocationToLibrary]
|
[createLocation, relinkLocation, addLocationToLibrary, submitPlausibleEvent]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAddError = useCallback(
|
const handleAddError = useCallback(
|
||||||
|
@ -218,18 +205,7 @@ export const AddLocationDialog = ({
|
||||||
>
|
>
|
||||||
<ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mb-4 mt-2" />
|
<ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mb-4 mt-2" />
|
||||||
|
|
||||||
<InputField
|
<LocationPathInputField {...form.register('path')} />
|
||||||
size="md"
|
|
||||||
label="Path:"
|
|
||||||
onClick={() =>
|
|
||||||
openDirectoryPickerDialog(platform)
|
|
||||||
.then((path) => path && form.setValue('path', path))
|
|
||||||
.catch((error) => toast.error(String(error)))
|
|
||||||
}
|
|
||||||
readOnly={platform.platform !== 'web'}
|
|
||||||
className={clsx('mb-3', platform.platform === 'web' || 'cursor-pointer')}
|
|
||||||
{...form.register('path')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input type="hidden" {...form.register('method')} />
|
<input type="hidden" {...form.register('method')} />
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ChangeEvent, ChangeEventHandler, forwardRef, memo } from 'react';
|
||||||
import { Input, toast } from '@sd/ui';
|
import { Input, toast } from '@sd/ui';
|
||||||
import { useOperatingSystem } from '~/hooks';
|
import { useOperatingSystem } from '~/hooks';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
import { openDirectoryPickerDialog } from '../AddLocationDialog';
|
import { openDirectoryPickerDialog } from '../openDirectoryPickerDialog';
|
||||||
|
|
||||||
export type InputKinds = 'Name' | 'Extension' | 'Path' | 'Advanced';
|
export type InputKinds = 'Name' | 'Extension' | 'Path' | 'Advanced';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { InputField, InputFieldProps, toast } from '@sd/ui';
|
||||||
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
import { openDirectoryPickerDialog } from './openDirectoryPickerDialog';
|
||||||
|
|
||||||
|
export const LocationPathInputField = forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
Omit<InputFieldProps, 'onClick' | 'readOnly' | 'className'>
|
||||||
|
>((props, ref) => {
|
||||||
|
const platform = usePlatform();
|
||||||
|
const form = useFormContext();
|
||||||
|
console.log(form.formState.isDirty);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputField
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
onClick={() =>
|
||||||
|
openDirectoryPickerDialog(platform)
|
||||||
|
.then(
|
||||||
|
(path) =>
|
||||||
|
path &&
|
||||||
|
form.setValue(props.name, path, {
|
||||||
|
shouldDirty: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch((error) => toast.error(String(error)))
|
||||||
|
}
|
||||||
|
readOnly={platform.platform !== 'web'}
|
||||||
|
className={clsx('mb-3', platform.platform === 'web' || 'cursor-pointer')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Platform } from '~/util/Platform';
|
||||||
|
|
||||||
|
export const openDirectoryPickerDialog = async (platform: Platform): Promise<null | string> => {
|
||||||
|
if (!platform.openDirectoryPickerDialog) return null;
|
||||||
|
|
||||||
|
const path = await platform.openDirectoryPickerDialog();
|
||||||
|
if (!path) return '';
|
||||||
|
if (typeof path !== 'string')
|
||||||
|
// TODO: Should adding multiple locations simultaneously be implemented?
|
||||||
|
throw new Error('Adding multiple locations simultaneously is not supported');
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
|
@ -267,7 +267,7 @@ export type LocationSettings = { explorer: ExplorerSettings<FilePathOrder> }
|
||||||
* It is important to note that only the indexer rule ids in this vector will be used from now on.
|
* It is important to note that only the indexer rule ids in this vector will be used from now on.
|
||||||
* Old rules that aren't in this vector will be purged.
|
* Old rules that aren't in this vector will be purged.
|
||||||
*/
|
*/
|
||||||
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] }
|
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[]; path: string | null }
|
||||||
|
|
||||||
export type LocationWithIndexerRules = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: { indexer_rule: IndexerRule }[] }
|
export type LocationWithIndexerRules = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: { indexer_rule: IndexerRule }[] }
|
||||||
|
|
||||||
|
|
|
@ -140,11 +140,11 @@ export function Label({ slug, children, className, ...props }: LabelProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PasswordInputProps extends InputProps {
|
interface Props extends InputProps {
|
||||||
buttonClassnames?: string;
|
buttonClassnames?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>((props, ref) => {
|
export const PasswordInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const CurrentEyeIcon = showPassword ? EyeSlash : Eye;
|
const CurrentEyeIcon = showPassword ? EyeSlash : Eye;
|
||||||
|
|
Loading…
Reference in a new issue