mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-13 10:44:08 +00:00
[ENG-1210] Navigate to location after adding it (#1454)
* remove annoying log * return location id on creation * add checkbox to open new location once it's been added * redirect if checkbox was true and a location id was provided, and update bindings * add `new()` for `MissingFieldError` * return location id on location relink * working, clean redirecting * accordion closing tag * navigate to location after adding * chore: remove erroneous `{' '}` * multiple location redirect support * oops missed check * cleanup * fix bad merge --------- Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
This commit is contained in:
parent
5f417e7c1b
commit
cc72f54c3d
|
@ -208,11 +208,13 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
R.with2(library())
|
||||
.mutation(|(node, library), args: LocationCreateArgs| async move {
|
||||
if let Some(location) = args.create(&node, &library).await? {
|
||||
let id = Some(location.id);
|
||||
scan_location(&node, &library, location).await?;
|
||||
invalidate_query!(library, "locations.list");
|
||||
Ok(id)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("update", {
|
||||
|
@ -244,10 +246,13 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
R.with2(library())
|
||||
.mutation(|(node, library), args: LocationCreateArgs| async move {
|
||||
if let Some(location) = args.add_library(&node, &library).await? {
|
||||
let id = location.id;
|
||||
scan_location(&node, &library, location).await?;
|
||||
invalidate_query!(library, "locations.list");
|
||||
Ok(Some(id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("fullRescan", {
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
},
|
||||
prisma::{file_path, indexer_rules_in_location, location, PrismaClient},
|
||||
util::{
|
||||
db::maybe_missing,
|
||||
db::{maybe_missing, MissingFieldError},
|
||||
error::{FileIOError, NonUtf8PathError},
|
||||
},
|
||||
Node,
|
||||
|
@ -532,7 +532,7 @@ pub async fn light_scan_location(
|
|||
pub async fn relink_location(
|
||||
Library { db, id, sync, .. }: &Library,
|
||||
location_path: impl AsRef<Path>,
|
||||
) -> Result<(), LocationError> {
|
||||
) -> Result<i32, LocationError> {
|
||||
let location_path = location_path.as_ref();
|
||||
let mut metadata = SpacedriveLocationMetadataFile::try_load(&location_path)
|
||||
.await?
|
||||
|
@ -547,7 +547,7 @@ pub async fn relink_location(
|
|||
.ok_or_else(|| NonUtf8PathError(location_path.into()))?;
|
||||
|
||||
sync.write_op(
|
||||
db,
|
||||
&db,
|
||||
sync.shared_update(
|
||||
prisma_sync::location::SyncId {
|
||||
pub_id: pub_id.clone(),
|
||||
|
@ -556,13 +556,23 @@ pub async fn relink_location(
|
|||
json!(path),
|
||||
),
|
||||
db.location().update(
|
||||
location::pub_id::equals(pub_id),
|
||||
location::pub_id::equals(pub_id.clone()),
|
||||
vec![location::path::set(Some(path))],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
let location_id = db
|
||||
.location()
|
||||
.find_unique(location::pub_id::equals(pub_id))
|
||||
.select(location::select!({ id }))
|
||||
.exec()
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
LocationError::MissingField(MissingFieldError::new("missing id of location"))
|
||||
})?;
|
||||
|
||||
Ok(location_id.id)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -69,6 +69,13 @@ pub fn inode_to_db(inode: u64) -> Vec<u8> {
|
|||
#[error("Missing field {0}")]
|
||||
pub struct MissingFieldError(&'static str);
|
||||
|
||||
impl MissingFieldError {
|
||||
#[must_use]
|
||||
pub const fn new(value: &'static str) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MissingFieldError> for rspc::Error {
|
||||
fn from(value: MissingFieldError) -> Self {
|
||||
rspc::Error::with_cause(
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
type ExplorerItem,
|
||||
type ExplorerLayout,
|
||||
type ExplorerSettings,
|
||||
type SortOrder
|
||||
type SortOrder,
|
||||
JobGroup
|
||||
} from '@sd/client';
|
||||
|
||||
export enum ExplorerKind {
|
||||
|
@ -121,6 +122,7 @@ const state = {
|
|||
tagAssignMode: false,
|
||||
showInspector: false,
|
||||
showMoreInfo: false,
|
||||
jobsToRedirect: [] as {locationId: number | null}[],
|
||||
mediaPlayerVolume: 0.7,
|
||||
newThumbnails: proxySet() as Set<string>,
|
||||
cutCopyState: { type: 'Idle' } as CutCopyState,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Pencil, Plus, Trash } from '@phosphor-icons/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useLibraryContext } from '@sd/client';
|
||||
import { ContextMenu as CM, dialogManager, toast } from '@sd/ui';
|
||||
import { AddLocationDialog } from '~/app/$libraryId/settings/library/locations/AddLocationDialog';
|
||||
import DeleteDialog from '~/app/$libraryId/settings/library/locations/DeleteDialog';
|
||||
|
@ -14,6 +15,8 @@ interface Props {
|
|||
export default ({ children, locationId }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const platform = usePlatform();
|
||||
const libraryId = useLibraryContext().library.uuid;
|
||||
|
||||
return (
|
||||
<CM.Root trigger={children}>
|
||||
<CM.Item
|
||||
|
@ -22,7 +25,11 @@ export default ({ children, locationId }: Props) => {
|
|||
const path = await openDirectoryPickerDialog(platform);
|
||||
if (path !== '') {
|
||||
dialogManager.create((dp) => (
|
||||
<AddLocationDialog path={path ?? ''} {...dp} />
|
||||
<AddLocationDialog
|
||||
path={path ?? ''}
|
||||
libraryId={libraryId}
|
||||
{...dp}
|
||||
/>
|
||||
));
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -2,6 +2,8 @@ import { FolderSimplePlus } from '@phosphor-icons/react';
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useLibraryContext } from '@sd/client';
|
||||
import { Button, dialogManager, type ButtonProps } from '@sd/ui';
|
||||
import { useCallbackToWatchResize } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
@ -16,6 +18,9 @@ interface AddLocationButton extends ButtonProps {
|
|||
|
||||
export const AddLocationButton = ({ path, className, onClick, ...props }: AddLocationButton) => {
|
||||
const platform = usePlatform();
|
||||
const libraryId = useLibraryContext().library.uuid;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const transition = {
|
||||
type: 'keyframes',
|
||||
ease: 'easeInOut',
|
||||
|
@ -49,7 +54,7 @@ export const AddLocationButton = ({ path, className, onClick, ...props }: AddLoc
|
|||
// Remember `path` will be `undefined` on web cause the user has to provide it in the modal
|
||||
if (path !== '')
|
||||
dialogManager.create((dp) => (
|
||||
<AddLocationDialog path={path ?? ''} {...dp} />
|
||||
<AddLocationDialog path={path ?? ''} libraryId={libraryId} {...dp} />
|
||||
));
|
||||
|
||||
onClick?.();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, get } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
extractInfoRSPCError,
|
||||
|
@ -9,7 +10,8 @@ import {
|
|||
usePlausibleEvent,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
import { Dialog, ErrorMessage, toast, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||
import { CheckBox, Dialog, ErrorMessage, Label, toast, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||
import { getExplorerStore, useExplorerStore } from '~/app/$libraryId/Explorer/store';
|
||||
import Accordion from '~/components/Accordion';
|
||||
import { useCallbackToWatchForm } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
@ -34,13 +36,15 @@ const isRemoteErrorFormMessage = (message: unknown): message is RemoteErrorFormM
|
|||
const schema = z.object({
|
||||
path: z.string().min(1),
|
||||
method: z.enum(Object.keys(REMOTE_ERROR_FORM_MESSAGE) as UnionToTuple<RemoteErrorFormMessage>),
|
||||
indexerRulesIds: z.array(z.number())
|
||||
indexerRulesIds: z.array(z.number()),
|
||||
shouldRedirect: z.boolean()
|
||||
});
|
||||
|
||||
type SchemaType = z.infer<typeof schema>;
|
||||
|
||||
export interface AddLocationDialog extends UseDialogProps {
|
||||
path: string;
|
||||
libraryId: string;
|
||||
method?: RemoteErrorFormMessage;
|
||||
}
|
||||
|
||||
|
@ -56,6 +60,7 @@ export const AddLocationDialog = ({
|
|||
const relinkLocation = useLibraryMutation('locations.relink');
|
||||
const listIndexerRules = useLibraryQuery(['locations.indexer_rules.list']);
|
||||
const addLocationToLibrary = useLibraryMutation('locations.addLibrary');
|
||||
const explorerStore = useExplorerStore();
|
||||
|
||||
// This is required because indexRules is undefined on first render
|
||||
const indexerRulesIds = useMemo(
|
||||
|
@ -65,7 +70,7 @@ export const AddLocationDialog = ({
|
|||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: { path, method, indexerRulesIds }
|
||||
defaultValues: { path, method, indexerRulesIds, shouldRedirect: true }
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -78,10 +83,12 @@ export const AddLocationDialog = ({
|
|||
}, [form, path, indexerRulesIds]);
|
||||
|
||||
const addLocation = useCallback(
|
||||
async ({ path, method, indexerRulesIds }: SchemaType, dryRun = false) => {
|
||||
async ({ path, method, indexerRulesIds, shouldRedirect }: SchemaType, dryRun = false) => {
|
||||
let id = null;
|
||||
|
||||
switch (method) {
|
||||
case 'CREATE':
|
||||
await createLocation.mutateAsync({
|
||||
id = await createLocation.mutateAsync({
|
||||
path,
|
||||
dry_run: dryRun,
|
||||
indexer_rules_ids: indexerRulesIds
|
||||
|
@ -91,7 +98,7 @@ export const AddLocationDialog = ({
|
|||
|
||||
break;
|
||||
case 'NEED_RELINK':
|
||||
if (!dryRun) await relinkLocation.mutateAsync(path);
|
||||
if (!dryRun) id = await relinkLocation.mutateAsync(path);
|
||||
// TODO: Update relinked location with new indexer rules, don't have a way to get location id yet though
|
||||
// await updateLocation.mutateAsync({
|
||||
// id: locationId,
|
||||
|
@ -104,7 +111,7 @@ export const AddLocationDialog = ({
|
|||
|
||||
break;
|
||||
case 'ADD_LIBRARY':
|
||||
await addLocationToLibrary.mutateAsync({
|
||||
id = await addLocationToLibrary.mutateAsync({
|
||||
path,
|
||||
dry_run: dryRun,
|
||||
indexer_rules_ids: indexerRulesIds
|
||||
|
@ -116,8 +123,14 @@ export const AddLocationDialog = ({
|
|||
default:
|
||||
throw new Error('Unimplemented custom remote error handling');
|
||||
}
|
||||
if (shouldRedirect) {
|
||||
getExplorerStore().jobsToRedirect = [
|
||||
{ locationId: id },
|
||||
...explorerStore.jobsToRedirect
|
||||
];
|
||||
}
|
||||
},
|
||||
[createLocation, relinkLocation, addLocationToLibrary, submitPlausibleEvent]
|
||||
[createLocation, relinkLocation, addLocationToLibrary, submitPlausibleEvent, explorerStore]
|
||||
);
|
||||
|
||||
const handleAddError = useCallback(
|
||||
|
@ -206,27 +219,38 @@ export const AddLocationDialog = ({
|
|||
: ''
|
||||
}
|
||||
>
|
||||
<ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mb-4 mt-2" />
|
||||
|
||||
<LocationPathInputField {...form.register('path')} />
|
||||
|
||||
<input type="hidden" {...form.register('method')} />
|
||||
|
||||
<Accordion title="Advanced settings">
|
||||
<Controller
|
||||
name="indexerRulesIds"
|
||||
render={({ field }) => (
|
||||
<IndexerRuleEditor
|
||||
field={field}
|
||||
label="File indexing rules"
|
||||
className="relative flex flex-col"
|
||||
rulesContainerClass="grid grid-cols-2 gap-2"
|
||||
ruleButtonClass="w-full"
|
||||
/>
|
||||
)}
|
||||
control={form.control}
|
||||
<div className="flex flex-col">
|
||||
<ErrorMessage
|
||||
name={REMOTE_ERROR_FORM_FIELD}
|
||||
variant="large"
|
||||
className="mb-4 mt-2"
|
||||
/>
|
||||
</Accordion>
|
||||
|
||||
<LocationPathInputField {...form.register('path')} />
|
||||
|
||||
<input type="hidden" {...form.register('method')} />
|
||||
|
||||
<div className="mb-4 flex">
|
||||
<CheckBox {...form.register('shouldRedirect')} />
|
||||
<Label className="mt-[3px] font-semibold">Open new location once added</Label>
|
||||
</div>
|
||||
|
||||
<Accordion title="Advanced settings">
|
||||
<Controller
|
||||
name="indexerRulesIds"
|
||||
render={({ field }) => (
|
||||
<IndexerRuleEditor
|
||||
field={field}
|
||||
label="File indexing rules"
|
||||
className="relative flex flex-col"
|
||||
rulesContainerClass="grid grid-cols-2 gap-2"
|
||||
ruleButtonClass="w-full"
|
||||
/>
|
||||
)}
|
||||
control={form.control}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Navigate, Outlet, useMatches, type RouteObject } from 'react-router-dom';
|
||||
import { currentLibraryCache, useCachedLibraries, useInvalidateQuery } from '@sd/client';
|
||||
import { Dialogs, toast, Toaster } from '@sd/ui';
|
||||
import { Dialogs, Toaster } from '@sd/ui';
|
||||
import { RouterErrorBoundary } from '~/ErrorFallback';
|
||||
import { useKeybindHandler, useTheme } from '~/hooks';
|
||||
import { useKeybindHandler, useShouldRedirect, useTheme } from '~/hooks';
|
||||
|
||||
import libraryRoutes from './$libraryId';
|
||||
import onboardingRoutes from './onboarding';
|
||||
|
@ -11,8 +11,6 @@ import { RootContext } from './RootContext';
|
|||
|
||||
import './style.scss';
|
||||
|
||||
import { usePlatform } from '..';
|
||||
|
||||
const Index = () => {
|
||||
const libraries = useCachedLibraries();
|
||||
|
||||
|
@ -31,6 +29,7 @@ const Wrapper = () => {
|
|||
useKeybindHandler();
|
||||
useInvalidateQuery();
|
||||
useTheme();
|
||||
useShouldRedirect();
|
||||
|
||||
const rawPath = useRawRoutePath();
|
||||
|
||||
|
|
|
@ -23,3 +23,4 @@ export * from './useIsTextTruncated';
|
|||
export * from './useKeyMatcher';
|
||||
export * from './useKeyCopyCutPaste';
|
||||
export * from './useMouseNavigate';
|
||||
export * from './useShouldRedirect';
|
||||
|
|
58
interface/hooks/useShouldRedirect.ts
Normal file
58
interface/hooks/useShouldRedirect.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { getExplorerStore, useExplorerStore } from "~/app/$libraryId/Explorer/store"
|
||||
import { useLibraryQuery} from '@sd/client';
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useZodRouteParams } from "../hooks/useZodRouteParams";
|
||||
import { LibraryIdParamsSchema } from "../app/route-schemas";
|
||||
/**
|
||||
* When a user adds a location and checks the should redirect box,
|
||||
* this hook will redirect them to the location
|
||||
* once the indexer has been invoked
|
||||
*/
|
||||
|
||||
export const useShouldRedirect = () => {
|
||||
const { jobsToRedirect } = useExplorerStore();
|
||||
const navigate = useNavigate();
|
||||
const { libraryId } = useZodRouteParams(LibraryIdParamsSchema);
|
||||
const jobGroups = useLibraryQuery(['jobs.reports'], {
|
||||
enabled: !!(jobsToRedirect.length > 0),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
//We loop all job groups and pull the first job that matches the location id from the job group
|
||||
|
||||
const pullMatchingJob = useCallback(() => {
|
||||
if (jobsToRedirect.length === 0) return;
|
||||
let jobFound
|
||||
if (jobGroups.data) {
|
||||
for (const jobGroup of jobGroups.data) {
|
||||
for (const job of jobGroup.jobs) {
|
||||
if (job.name === 'indexer') {
|
||||
const locationId = jobsToRedirect.find((l) => l.locationId === job.metadata.location.id)?.locationId
|
||||
if (job.metadata.location.id === locationId && job.completed_task_count > 0) {
|
||||
jobFound = job;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return jobFound
|
||||
}, [jobGroups.data, jobsToRedirect])
|
||||
|
||||
//Once we have a matching job, we redirect the user to the location
|
||||
|
||||
useEffect(() => {
|
||||
if (jobGroups.data) {
|
||||
const matchingJob = pullMatchingJob();
|
||||
if (matchingJob) {
|
||||
const locationId = jobsToRedirect.find((l) => l.locationId === matchingJob.metadata.location.id)?.locationId
|
||||
navigate(`/${libraryId}/location/${locationId}`);
|
||||
getExplorerStore().jobsToRedirect = jobsToRedirect.filter((l) => l.locationId !== matchingJob.metadata.location.id);
|
||||
}
|
||||
}
|
||||
}, [jobGroups.data, pullMatchingJob, navigate, libraryId, jobsToRedirect])
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -73,13 +73,13 @@ export type Procedures = {
|
|||
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
{ key: "library.edit", input: EditLibraryArgs, result: null } |
|
||||
{ key: "locations.addLibrary", input: LibraryArgs<LocationCreateArgs>, result: null } |
|
||||
{ key: "locations.create", input: LibraryArgs<LocationCreateArgs>, result: null } |
|
||||
{ key: "locations.addLibrary", input: LibraryArgs<LocationCreateArgs>, result: number | null } |
|
||||
{ key: "locations.create", input: LibraryArgs<LocationCreateArgs>, result: number | null } |
|
||||
{ key: "locations.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.fullRescan", input: LibraryArgs<FullRescanArgs>, result: null } |
|
||||
{ key: "locations.indexer_rules.create", input: LibraryArgs<IndexerRuleCreateArgs>, result: null } |
|
||||
{ key: "locations.indexer_rules.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "locations.relink", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "locations.relink", input: LibraryArgs<string>, result: number } |
|
||||
{ key: "locations.subPathRescan", input: LibraryArgs<RescanArgs>, result: null } |
|
||||
{ key: "locations.update", input: LibraryArgs<LocationUpdateArgs>, result: null } |
|
||||
{ key: "nodes.edit", input: ChangeNodeNameArgs, result: null } |
|
||||
|
|
Loading…
Reference in a new issue