mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 11:13: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({
|
||||
displayName: z.string().nullable(),
|
||||
path: z.string().min(1).nullable(),
|
||||
localPath: z.string().nullable(),
|
||||
indexer_rules_ids: z.array(z.string()),
|
||||
generatePreviewMedia: z.boolean().nullable(),
|
||||
|
@ -51,6 +52,7 @@ const EditLocationSettingsScreen = ({
|
|||
updateLocation.mutateAsync({
|
||||
id: Number(id),
|
||||
name: data.displayName,
|
||||
path: data.path,
|
||||
sync_preview_media: data.syncPreviewMedia,
|
||||
generate_preview_media: data.generatePreviewMedia,
|
||||
hidden: data.hidden,
|
||||
|
|
|
@ -182,8 +182,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
.procedure("update", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), args: LocationUpdateArgs| async move {
|
||||
let ret = args.update(&library).await.map_err(Into::into);
|
||||
.mutation(|(node, library), args: LocationUpdateArgs| async move {
|
||||
let ret = args.update(&node, &library).await.map_err(Into::into);
|
||||
invalidate_query!(library, "locations.list");
|
||||
ret
|
||||
})
|
||||
|
|
|
@ -256,16 +256,17 @@ impl LocationCreateArgs {
|
|||
/// Old rules that aren't in this vector will be purged.
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct LocationUpdateArgs {
|
||||
pub id: location::id::Type,
|
||||
pub name: Option<String>,
|
||||
pub generate_preview_media: Option<bool>,
|
||||
pub sync_preview_media: Option<bool>,
|
||||
pub hidden: Option<bool>,
|
||||
pub indexer_rules_ids: Vec<i32>,
|
||||
id: location::id::Type,
|
||||
name: Option<String>,
|
||||
generate_preview_media: Option<bool>,
|
||||
sync_preview_media: Option<bool>,
|
||||
hidden: Option<bool>,
|
||||
indexer_rules_ids: Vec<i32>,
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
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 location = find_location(library, self.id)
|
||||
|
@ -274,9 +275,10 @@ impl LocationUpdateArgs {
|
|||
.await?
|
||||
.ok_or(LocationError::IdNotFound(self.id))?;
|
||||
|
||||
let name = self.name.clone();
|
||||
|
||||
let (sync_params, db_params): (Vec<_>, Vec<_>) = [
|
||||
self.name
|
||||
.clone()
|
||||
.filter(|name| location.name.as_ref() != Some(name))
|
||||
.map(|v| {
|
||||
(
|
||||
|
@ -302,6 +304,12 @@ impl LocationUpdateArgs {
|
|||
location::hidden::set(Some(v)),
|
||||
)
|
||||
}),
|
||||
self.path.clone().map(|v| {
|
||||
(
|
||||
(location::path::NAME, json!(v)),
|
||||
location::path::set(Some(v)),
|
||||
)
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
@ -336,11 +344,16 @@ impl LocationUpdateArgs {
|
|||
SpacedriveLocationMetadataFile::try_load(path).await?
|
||||
{
|
||||
metadata
|
||||
.update(library.id, maybe_missing(self.name, "location.name")?)
|
||||
.update(library.id, maybe_missing(name, "location.name")?)
|
||||
.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
|
||||
|
|
|
@ -278,6 +278,7 @@ export const RenameLocationTextBox = (props: Omit<Props, 'renameHandler'>) => {
|
|||
try {
|
||||
await renameLocation.mutateAsync({
|
||||
id: props.locationId,
|
||||
path: null,
|
||||
name: newName,
|
||||
generate_preview_media: null,
|
||||
sync_preview_media: null,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { Pencil, Plus, Trash } from 'phosphor-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { ContextMenu as CM, dialogManager, toast } from '@sd/ui';
|
||||
import {
|
||||
AddLocationDialog,
|
||||
openDirectoryPickerDialog
|
||||
} from '~/app/$libraryId/settings/library/locations/AddLocationDialog';
|
||||
import { AddLocationDialog } from '~/app/$libraryId/settings/library/locations/AddLocationDialog';
|
||||
import DeleteDialog from '~/app/$libraryId/settings/library/locations/DeleteDialog';
|
||||
import { openDirectoryPickerDialog } from '~/app/$libraryId/settings/library/locations/openDirectoryPickerDialog';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react';
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||
import {
|
||||
|
@ -21,13 +21,14 @@ import ModalLayout from '~/app/$libraryId/settings/ModalLayout';
|
|||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { useZodRouteParams } from '~/hooks';
|
||||
import IndexerRuleEditor from './IndexerRuleEditor';
|
||||
import { LocationPathInputField } from './PathInput';
|
||||
|
||||
const FlexCol = tw.label`flex flex-col flex-1`;
|
||||
const ToggleSection = tw.label`flex flex-row w-full`;
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().nullable(),
|
||||
path: z.string().nullable(),
|
||||
path: z.string().min(1).nullable(),
|
||||
hidden: z.boolean().nullable(),
|
||||
indexerRulesIds: z.array(z.number()),
|
||||
locationType: z.string(),
|
||||
|
@ -51,23 +52,6 @@ const EditLocationForm = () => {
|
|||
|
||||
const locationData = useLibraryQuery(['locations.getWithRules', locationId], {
|
||||
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({
|
||||
|
@ -94,18 +78,16 @@ const EditLocationForm = () => {
|
|||
}
|
||||
});
|
||||
|
||||
const { isDirty } = form.formState;
|
||||
|
||||
const onSubmit = form.handleSubmit(
|
||||
({ name, hidden, indexerRulesIds, syncPreviewMedia, generatePreviewMedia }) =>
|
||||
updateLocation.mutateAsync({
|
||||
id: locationId,
|
||||
name,
|
||||
hidden,
|
||||
indexer_rules_ids: indexerRulesIds,
|
||||
sync_preview_media: syncPreviewMedia,
|
||||
generate_preview_media: generatePreviewMedia
|
||||
})
|
||||
const onSubmit = form.handleSubmit((data) =>
|
||||
updateLocation.mutateAsync({
|
||||
id: locationId,
|
||||
path: data.path,
|
||||
name: data.name,
|
||||
hidden: data.hidden,
|
||||
indexer_rules_ids: data.indexerRulesIds,
|
||||
sync_preview_media: data.syncPreviewMedia,
|
||||
generate_preview_media: data.generatePreviewMedia
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -114,15 +96,15 @@ const EditLocationForm = () => {
|
|||
title="Edit Location"
|
||||
topRight={
|
||||
<div className="flex flex-row space-x-3">
|
||||
{isDirty && (
|
||||
{form.formState.isDirty && (
|
||||
<Button onClick={() => form.reset()} variant="outline" size="sm">
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isDirty || form.formState.isSubmitting}
|
||||
variant={isDirty ? 'accent' : 'outline'}
|
||||
disabled={!form.formState.isDirty || form.formState.isSubmitting}
|
||||
variant={form.formState.isDirty ? 'accent' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
Save Changes
|
||||
|
@ -139,12 +121,7 @@ const EditLocationForm = () => {
|
|||
</InfoText>
|
||||
</FlexCol>
|
||||
<FlexCol>
|
||||
<InputField
|
||||
label="Local Path"
|
||||
readOnly={true}
|
||||
className="text-ink-dull"
|
||||
{...form.register('path')}
|
||||
/>
|
||||
<LocationPathInputField label="Path" {...form.register('path')} />
|
||||
<InfoText className="mt-2">
|
||||
The path to this Location, this is where the files will be stored on
|
||||
disk.
|
||||
|
|
|
@ -5,7 +5,8 @@ import { useRef, useState } from 'react';
|
|||
import { Button, type ButtonProps, dialogManager } from '@sd/ui';
|
||||
import { useCallbackToWatchResize } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { AddLocationDialog, openDirectoryPickerDialog } from './AddLocationDialog';
|
||||
import { AddLocationDialog } from './AddLocationDialog';
|
||||
import { openDirectoryPickerDialog } from './openDirectoryPickerDialog';
|
||||
|
||||
interface AddLocationButton extends ButtonProps {
|
||||
path?: string;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import { CaretDown } from 'phosphor-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, get } from 'react-hook-form';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
|
@ -11,11 +9,12 @@ import {
|
|||
usePlausibleEvent,
|
||||
useZodForm
|
||||
} 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 { useCallbackToWatchForm } from '~/hooks';
|
||||
import { Platform, usePlatform } from '~/util/Platform';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import IndexerRuleEditor from './IndexerRuleEditor';
|
||||
import { LocationPathInputField } from './PathInput';
|
||||
|
||||
const REMOTE_ERROR_FORM_FIELD = 'root.serverError';
|
||||
const REMOTE_ERROR_FORM_MESSAGE = {
|
||||
|
@ -39,18 +38,6 @@ const schema = z.object({
|
|||
|
||||
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 {
|
||||
path: string;
|
||||
method?: RemoteErrorFormMessage;
|
||||
|
@ -129,7 +116,7 @@ export const AddLocationDialog = ({
|
|||
throw new Error('Unimplemented custom remote error handling');
|
||||
}
|
||||
},
|
||||
[createLocation, relinkLocation, addLocationToLibrary, addLocationToLibrary]
|
||||
[createLocation, relinkLocation, addLocationToLibrary, submitPlausibleEvent]
|
||||
);
|
||||
|
||||
const handleAddError = useCallback(
|
||||
|
@ -218,18 +205,7 @@ export const AddLocationDialog = ({
|
|||
>
|
||||
<ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mb-4 mt-2" />
|
||||
|
||||
<InputField
|
||||
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')}
|
||||
/>
|
||||
<LocationPathInputField {...form.register('path')} />
|
||||
|
||||
<input type="hidden" {...form.register('method')} />
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ChangeEvent, ChangeEventHandler, forwardRef, memo } from 'react';
|
|||
import { Input, toast } from '@sd/ui';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { openDirectoryPickerDialog } from '../AddLocationDialog';
|
||||
import { openDirectoryPickerDialog } from '../openDirectoryPickerDialog';
|
||||
|
||||
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.
|
||||
* 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 }[] }
|
||||
|
||||
|
|
|
@ -140,11 +140,11 @@ export function Label({ slug, children, className, ...props }: LabelProps) {
|
|||
);
|
||||
}
|
||||
|
||||
interface PasswordInputProps extends InputProps {
|
||||
interface Props extends InputProps {
|
||||
buttonClassnames?: string;
|
||||
}
|
||||
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>((props, ref) => {
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const CurrentEyeIcon = showPassword ? EyeSlash : Eye;
|
||||
|
|
Loading…
Reference in a new issue