[MOB-99] Job remove & more (#2514)

* Clear job and improve job manager design

* pullToRefresh, search adjustment, and more

* ts

* use rspc instead of query client

* Update JobManagerModal.tsx
This commit is contained in:
ameer2468 2024-05-28 22:02:50 +01:00 committed by GitHub
parent d64b21357b
commit 530e3c8ac8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 121 additions and 71 deletions

View file

@ -25,6 +25,7 @@ const SortByMenu = () => {
return (
<View style={tw`flex-row items-center gap-1.5`}>
<Menu
containerStyle={tw`max-w-44`}
trigger={<Trigger activeOption={sortOptions[searchStore.sort.by]} />}
>
@ -40,6 +41,7 @@ const SortByMenu = () => {
))}
</Menu>
<Menu
containerStyle={tw`max-w-40`}
trigger={<Trigger
triggerIcon={searchStore.sort.direction === 'Asc' ? ArrowUpIcon : ArrowDownIcon}
activeOption={searchStore.sort.direction}

View file

@ -48,7 +48,7 @@ export default function Header({ route, navBack, title, search = false }: Props)
}}
>
<MagnifyingGlass
size={24}
size={20}
weight="bold"
color={tw.color('text-zinc-300')}
/>

View file

@ -1,8 +1,8 @@
import { TextItems } from '@sd/client';
import { Image } from 'expo-image';
import { Icon } from 'phosphor-react-native';
import { Fragment } from 'react';
import { Text, View, ViewStyle } from 'react-native';
import { TextItems } from '@sd/client';
import { styled, tw, twStyle } from '~/lib/tailwind';
type JobContainerProps = {
@ -25,7 +25,7 @@ export default function JobContainer(props: JobContainerProps) {
<View
style={twStyle(
'flex flex-row justify-center',
'border-b border-app-line/50 px-8 py-4',
'border-b border-app-line/30 px-8 py-4',
isChild && 'my-1.5 border-b-0 p-2 pl-12',
restProps.containerStyle
)}

View file

@ -1,9 +1,4 @@
import { Folder } from '@sd/assets/icons';
import dayjs from 'dayjs';
import { DotsThreeVertical, Pause, Play, Stop } from 'phosphor-react-native';
import { useMemo, useState } from 'react';
import { Animated, Pressable, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import {
getJobNiceActionName,
getTotalTasks,
@ -11,13 +6,20 @@ import {
JobProgressEvent,
JobReport,
useLibraryMutation,
useRspcLibraryContext,
useTotalElapsedTimeText
} from '@sd/client';
import { tw } from '~/lib/tailwind';
import dayjs from 'dayjs';
import { DotsThreeVertical, Eye, Pause, Play, Stop, Trash } from 'phosphor-react-native';
import { SetStateAction, useMemo, useState } from 'react';
import { Animated, Pressable, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { tw, twStyle } from '~/lib/tailwind';
import { AnimatedHeight } from '../animation/layout';
import { ProgressBar } from '../animation/ProgressBar';
import { Button } from '../primitive/Button';
import { Menu, MenuItem } from '../primitive/Menu';
import { toast } from '../primitive/Toast';
import Job from './Job';
import JobContainer from './JobContainer';
@ -58,11 +60,11 @@ export default function ({ group, progress }: JobGroupProps) {
return (
<Animated.View
style={[
tw`flex flex-row items-center pr-4`,
tw`mt-5 flex flex-row items-start pr-4`,
{ transform: [{ translateX: translate }] }
]}
>
<Options activeJob={runningJob} group={group} />
<Options showChildJobs={showChildJobs} setShowChildJobs={setShowChildJobs} activeJob={runningJob} group={group} />
</Animated.View>
);
};
@ -158,8 +160,23 @@ const toastErrorSuccess = (
};
};
function Options({ activeJob, group }: { activeJob?: JobReport; group: JobGroup }) {
// const queryClient = useQueryClient();
interface OptionsProps {
activeJob?: JobReport;
group: JobGroup;
showChildJobs: boolean;
setShowChildJobs: React.Dispatch<SetStateAction<boolean>>
}
function Options({ activeJob, group, setShowChildJobs, showChildJobs }: OptionsProps) {
const rspc = useRspcLibraryContext();
const clearJob = useLibraryMutation(
['jobs.clear'], {
onSuccess: () => {
rspc.queryClient.invalidateQueries(['jobs.reports']);
}
})
const resumeJob = useLibraryMutation(
['jobs.resume'],
@ -179,32 +196,45 @@ function Options({ activeJob, group }: { activeJob?: JobReport; group: JobGroup
[group.jobs]
);
// const clearJob = useLibraryMutation(
// ['jobs.clear'],
// toastErrorSuccess('failed_to_remove_job', undefined, () => {
// queryClient.invalidateQueries(['jobs.reports']);
// })
// );
const clearJobHandler = () => {
group.jobs.forEach((job) => {
clearJob.mutate(job.id);
//only one toast for all jobs
if (job.id === group.id)
toast.success('Job has been removed');
});
};
return (
<>
{/* Resume */}
{(group.status === 'Queued' || group.status === 'Paused' || isJobPaused) && (
<Button variant="outline" size="sm" onPress={() => resumeJob.mutate(group.id)}>
<Play size={18} color="white" />
<Button style={tw`h-7 w-7`} variant="outline" size="sm" onPress={() => resumeJob.mutate(group.id)}>
<Play size={16} color="white" />
</Button>
)}
{/* TODO: This should remove the job from panel */}
{!activeJob !== undefined ? (
<Button variant="outline" size="sm">
<Menu
containerStyle={tw`max-w-25`}
trigger={
<View style={tw`flex h-7 w-7 flex-row items-center justify-center rounded-md border border-app-inputborder`}>
<DotsThreeVertical size={16} color="white" />
</Button>
</View>
}
>
<MenuItem
style={twStyle(showChildJobs ? 'rounded bg-app-screen/50' : 'bg-transparent')}
onSelect={() => setShowChildJobs(!showChildJobs)}
text="Expand" icon={Eye}/>
<MenuItem onSelect={clearJobHandler} text='Remove' icon={Trash}/>
</Menu>
) : (
<View style={tw`flex flex-row gap-2`}>
<Button variant="outline" size="sm" onPress={() => pauseJob.mutate(group.id)}>
<Button style={tw`h-7 w-7`} variant="outline" size="sm" onPress={() => pauseJob.mutate(group.id)}>
<Pause size={16} color="white" />
</Button>
<Button variant="outline" size="sm" onPress={() => cancelJob.mutate(group.id)}>
<Button style={tw`h-7 w-7`} variant="outline" size="sm" onPress={() => cancelJob.mutate(group.id)}>
<Stop size={16} color="white" />
</Button>
</View>

View file

@ -7,14 +7,14 @@ import { Icon, IconName } from '../icons/Icon';
interface Props {
description: string; //description of empty state
icon: IconName; //Spacedrive icon
icon?: IconName; //Spacedrive icon
style?: ClassInput; //Tailwind classes
iconSize?: number; //Size of the icon
textSize?: ClassInput; //Size of the text
textStyle?: ClassInput; //Size of the text
includeHeaderHeight?: boolean; //Height of the header
}
const Empty = ({ description, icon, style, includeHeaderHeight = false, textSize = 'text-sm', iconSize = 38 }: Props) => {
const Empty = ({ description, icon, style, includeHeaderHeight = false, textStyle, iconSize = 38 }: Props) => {
const headerHeight = useSafeAreaInsets().top;
return (
<View
@ -25,8 +25,8 @@ const Empty = ({ description, icon, style, includeHeaderHeight = false, textSize
style
)}
>
<Icon name={icon} size={iconSize} />
<Text style={twStyle(`mt-2 text-center font-medium text-ink-dull`, textSize)}>
{icon && <Icon name={icon} size={iconSize} />}
<Text style={twStyle(`mt-2 text-center text-sm font-medium text-ink-dull`, textStyle)}>
{description}
</Text>
</View>

View file

@ -20,7 +20,7 @@ const GridLocation: React.FC<GridLocationProps> = ({ location, modalRef }: GridL
<View style={tw`w-full flex-col justify-between gap-1`}>
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`relative`}>
<FolderIcon size={42} />
<FolderIcon size={36} />
<View
style={twStyle(
'z-5 absolute bottom-[6px] right-[2px] h-2 w-2 rounded-full',

View file

@ -27,6 +27,8 @@ const ImportModal = forwardRef<ModalRef, unknown>((_, ref) => {
//custom message handling
if (error.message.startsWith("location already exists")) {
return toast.error('This location has already been added');
} else if (error.message.startsWith("nested location currently")) {
return toast.error('Nested locations are currently not supported');
}
switch (error.message) {
case 'NEED_RELINK':

View file

@ -1,40 +1,51 @@
import { forwardRef } from 'react';
import { FlatList, Text, View } from 'react-native';
import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { useJobProgress, useLibraryQuery } from '@sd/client';
import { forwardRef, useEffect } from 'react';
import JobGroup from '~/components/job/JobGroup';
import Empty from '~/components/layout/Empty';
import { Modal, ModalRef } from '~/components/layout/Modal';
import useForwardedRef from '~/hooks/useForwardedRef';
import { tw } from '~/lib/tailwind';
// TODO:
// - When there is no job, make modal height smaller
// - Add clear all jobs button
//TODO: Handle data fetching better when modal is opened
export const JobManagerModal = forwardRef<ModalRef, unknown>((_, ref) => {
// const queryClient = useQueryClient();
// const rspc = useRspcLibraryContext();
const jobGroups = useLibraryQuery(['jobs.reports']);
const progress = useJobProgress(jobGroups.data);
const modalRef = useForwardedRef(ref);
//TODO: Add clear all jobs button
// const clearAllJobs = useLibraryMutation(['jobs.clearAll'], {
// onError: () => {
// // TODO: Show error toast
// toast.error('Failed to clear all jobs.');
// },
// onSuccess: () => {
// queryClient.invalidateQueries(['jobs.reports ']);
// }
// });
useEffect(() => {
if (jobGroups.data?.length === 0) {
modalRef.current?.snapToPosition('20');
}
}, [jobGroups, modalRef]);
return (
<Modal ref={ref} snapPoints={['60']} title="Recent Jobs" showCloseButton>
<FlatList
<Modal
ref={modalRef}
snapPoints={['60']}
title="Recent Jobs"
showCloseButton
>
<BottomSheetFlatList
data={jobGroups.data}
style={tw`flex-1`}
keyExtractor={(i) => i.id}
contentContainerStyle={tw`mt-4`}
renderItem={({ item }) => <JobGroup group={item} progress={progress} />}
ListEmptyComponent={
<View style={tw`flex h-60 items-center justify-center`}>
<Text style={tw`text-center text-base text-ink-dull`}>No jobs.</Text>
</View>
<Empty style="border-0" description='No jobs.'/>
}
/>
</Modal>

View file

@ -11,8 +11,8 @@ const button = cva(['items-center justify-center rounded-md border shadow-sm'],
gray: ['border-app-box bg-app shadow-none'],
darkgray: ['border-app-box bg-app shadow-none'],
accent: ['border-accent-deep bg-accent shadow-md shadow-app-shade/10'],
outline: ['border-app-lightborder bg-black shadow-none'],
transparent: ['border-0 bg-black shadow-none'],
outline: ['border border-app-inputborder bg-transparent shadow-none'],
transparent: ['border-0 bg-transparent shadow-none'],
dashed: ['border border-dashed border-app-line bg-transparent shadow-none']
},
size: {

View file

@ -14,13 +14,14 @@ type MenuProps = {
trigger: React.ReactNode;
children: React.ReactNode[] | React.ReactNode;
triggerStyle?: ClassInput;
containerStyle?: ClassInput;
};
// TODO: Still looks a bit off...
export const Menu = (props: MenuProps) => (
<PMenu style={twStyle(props.triggerStyle)}>
<MenuTrigger>{props.trigger}</MenuTrigger>
<MenuOptions optionsContainerStyle={tw`rounded-md border border-app-cardborder bg-app-menu p-1`}>
<MenuOptions optionsContainerStyle={twStyle(`rounded-md border border-app-cardborder bg-app-menu p-1`, props.containerStyle)}>
{props.children}
</MenuOptions>
</PMenu>
@ -28,24 +29,25 @@ export const Menu = (props: MenuProps) => (
type MenuItemProps = {
icon?: Icon;
textStyle?: ClassInput;
iconStyle?: ClassInput;
style?: ClassInput;
} & MenuOptionProps;
export const MenuItem = ({ icon, ...props }: MenuItemProps) => {
export const MenuItem = ({ icon, textStyle, iconStyle, style, ...props }: MenuItemProps) => {
const Icon = icon;
return (
<View style={tw`flex flex-1 flex-row items-center`}>
<View style={twStyle(`flex-1 flex-row items-center px-2 py-1`, style)}>
{Icon && (
<View style={tw`ml-1`}>
<Icon size={16} style={tw`text-ink`} />
</View>
<Icon size={14} style={twStyle(`text-ink-dull`, iconStyle)} />
)}
<MenuOption
{...props}
customStyles={{
optionText: tw`w-full py-1 text-sm font-medium text-ink`
optionText: twStyle(`text-sm font-medium text-ink-dull`, textStyle)
}}
style={tw`flex flex-row items-center`}
style={tw`flex flex-row`}
/>
</View>
);

View file

@ -5,13 +5,13 @@ import Toast, { ToastConfig } from 'react-native-toast-message';
import { tw } from '~/lib/tailwind';
const baseStyles = 'max-w-[340px] flex-row gap-1 items-center justify-center overflow-hidden rounded-md border p-3 shadow-lg bg-app-input border-app-inputborder';
const containerStyle = 'flex-row items-start gap-2'
const containerStyle = 'flex-row items-start gap-1.5'
const toastConfig: ToastConfig = {
success: ({ text1, ...rest }) => (
<View style={tw.style(baseStyles)}>
<View style={tw.style(containerStyle)}>
<CheckCircle size={24} weight="fill" color={tw.color("text-green-500")} />
<CheckCircle size={20} weight="fill" color={tw.color("text-green-500")} />
<Text style={tw`self-center text-left text-sm font-medium text-ink`} numberOfLines={3}>
{text1}
</Text>
@ -21,7 +21,7 @@ const toastConfig: ToastConfig = {
error: ({ text1, ...rest }) => (
<View style={tw.style(baseStyles)}>
<View style={tw.style(containerStyle)}>
<WarningCircle size={24} weight="fill" color={tw.color("text-red-500")} />
<WarningCircle size={20} weight="fill" color={tw.color("text-red-500")} />
<Text style={tw`self-center text-left text-sm font-medium text-ink`} numberOfLines={3}>
{text1}
</Text>
@ -31,7 +31,7 @@ const toastConfig: ToastConfig = {
info: ({ text1, ...rest }) => (
<View style={tw.style(baseStyles)}>
<View style={tw.style(containerStyle)}>
<Info size={24} weight="fill" color={tw.color("text-accent")} />
<Info size={20} weight="fill" color={tw.color("text-accent")} />
<Text style={tw`self-center text-left text-sm font-medium text-ink`} numberOfLines={3}>
{text1}
</Text>

View file

@ -79,7 +79,7 @@ module.exports = {
// shadow
shade: `hsla(${DARK_HUE}, 15%, 0%, ${ALPHA})`,
// menu
menu: `hsla(${DARK_HUE}, 25%, 5%, ${ALPHA})`
menu: `hsla(${DARK_HUE}, 10%, 5%, ${ALPHA})`
},
sidebar: {
box: `hsla(${DARK_HUE}, 15%, 16%, ${ALPHA})`,

View file

@ -17,7 +17,6 @@ export function useFiltersSearch(search: string) {
const locations = useLibraryQuery(['locations.list'], {
keepPreviousData: true,
enabled: (name || ext) ? true : false,
});
const filterFactory = (key: SearchFilters, value: Filters[keyof Filters]) => {

View file

@ -1,4 +1,3 @@
import BrowseCategories from '~/components/browse/BrowseCategories';
import BrowseLocations from '~/components/browse/BrowseLocations';
import BrowseTags from '~/components/browse/BrowseTags';
import ScreenContainer from '~/components/layout/ScreenContainer';
@ -6,7 +5,7 @@ import ScreenContainer from '~/components/layout/ScreenContainer';
export default function BrowseScreen() {
return (
<ScreenContainer>
<BrowseCategories />
{/* <BrowseCategories /> */}
<BrowseLocations />
<BrowseTags />
</ScreenContainer>

View file

@ -77,7 +77,6 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
includeHeaderHeight
icon={'FolderNoSpace'}
style={tw`flex-1 items-center justify-center border-0`}
textSize="text-md"
iconSize={100}
description={'No files found'}
/>}

View file

@ -60,7 +60,6 @@ export default function LocationsScreen({ viewStyle }: Props) {
<Empty
icon="Folder"
style={'border-0'}
textSize="text-md"
iconSize={84}
description="You have not added any locations"
/>

View file

@ -38,7 +38,6 @@ export default function TagScreen({ navigation, route }: BrowseStackScreenProps<
includeHeaderHeight
icon={'Tags'}
style={tw`flex-1 items-center justify-center border-0`}
textSize="text-md"
iconSize={100}
description={'No items assigned to this tag'}
/>} {...objects} />;

View file

@ -65,7 +65,6 @@ export default function TagsScreen({ viewStyle = 'list' }: Props) {
<Empty
icon="Tags"
style={'border-0'}
textSize="text-md"
iconSize={84}
description="You have not created any tags"
/>

View file

@ -1,5 +1,5 @@
import { useIsFocused } from '@react-navigation/native';
import { usePathsExplorerQuery } from '@sd/client';
import { useLibraryQuery, usePathsExplorerQuery } from '@sd/client';
import { ArrowLeft, DotsThree, FunnelSimple } from 'phosphor-react-native';
import { Suspense, useDeferredValue, useState } from 'react';
import { ActivityIndicator, Platform, Pressable, TextInput, View } from 'react-native';
@ -23,6 +23,8 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
const deferredSearch = useDeferredValue(search);
const order = useSortBy();
const locations = useLibraryQuery(['locations.list']).data ?? [];
const objects = usePathsExplorerQuery({
order,
arg: {
@ -43,6 +45,13 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
const noObjects = objects.items?.length === 0 || !objects.items;
const noSearch = deferredSearch.length === 0 && appliedFiltersLength === 0;
const searchIcon =
locations.length > 0 && noObjects && noSearch ? 'FolderNoSpace' :
noSearch && noObjects ? 'Search' : 'FolderNoSpace';
const searchDescription = locations.length === 0 ? 'You have not added any locations to search' : noObjects
|| noSearch ? 'No files found' : 'No results found for this search';
return (
<View
style={twStyle('relative z-50 flex-1 bg-app-header', {
@ -117,12 +126,12 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
emptyComponent={
<Empty
includeHeaderHeight
icon={noSearch ? 'Search' : 'FolderNoSpace'}
icon={searchIcon}
description={searchDescription}
style={tw`flex-1 items-center justify-center border-0`}
textSize="text-md"
textStyle={tw`max-w-[220px]`}
iconSize={100}
description={noSearch ? 'Add filters or type to search for files' : 'No files found'}
/>
/>
}
tabHeight={false} />
</Suspense>