From 532ff4b62e616a7493a024c6685f331e47321e17 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 27 May 2024 16:44:15 +0100 Subject: [PATCH] [MOB-98] Rename, delete, & more (#2506) * rename and delete support, browse design improvement * update toast styling * Update RenameModal.tsx * fix test * fix warning message on initial render, add-tag test, and remove solid js references * ci * Add feedback toast for location delete and tag delete --- apps/mobile/src/App.tsx | 42 ++++---- .../components/browse/BrowseCategories.tsx | 6 +- .../src/components/browse/BrowseLocations.tsx | 71 ++++++++------ .../src/components/browse/BrowseTags.tsx | 74 ++++++++------ .../src/components/drawer/DrawerContent.tsx | 2 +- .../src/components/drawer/DrawerLocations.tsx | 10 +- .../src/components/explorer/FileRow.tsx | 2 +- apps/mobile/src/components/layout/Modal.tsx | 4 +- .../src/components/locations/GridLocation.tsx | 8 +- .../src/components/locations/ListLocation.tsx | 6 +- .../src/components/locations/LocationItem.tsx | 9 +- .../src/components/modal/AddTagModal.tsx | 4 +- .../src/components/modal/ImportModal.tsx | 13 ++- .../confirmModals/DeleteLocationModal.tsx | 7 +- .../modal/confirmModals/DeleteTagModal.tsx | 4 +- .../modal/inspector/ActionsModal.tsx | 27 +++++- .../modal/inspector/RenameModal.tsx | 90 ++++++++++++++++++ .../components/modal/tag/CreateTagModal.tsx | 15 ++- .../src/components/overview/Categories.tsx | 6 +- .../src/components/overview/Locations.tsx | 6 +- .../src/components/overview/OverviewStats.tsx | 4 +- .../src/components/primitive/Button.tsx | 4 +- .../mobile/src/components/primitive/Input.tsx | 18 ++-- .../mobile/src/components/primitive/Toast.tsx | 25 +++-- apps/mobile/src/components/tags/GridTag.tsx | 2 +- apps/mobile/src/components/tags/TagItem.tsx | 8 +- apps/mobile/src/hooks/useFiltersSearch.ts | 2 +- apps/mobile/src/screens/browse/Location.tsx | 9 +- apps/mobile/tests/add-tag.yml | 6 +- pnpm-lock.yaml | Bin 1027270 -> 1024032 bytes 30 files changed, 339 insertions(+), 145 deletions(-) create mode 100644 apps/mobile/src/components/modal/inspector/RenameModal.tsx diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index ac5f5232b..54e45cb5d 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -4,6 +4,19 @@ import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; +import { + ClientContextProvider, + LibraryContextProvider, + P2PContextProvider, + RspcProvider, + initPlausible, + useBridgeQuery, + useClientContext, + useInvalidateQuery, + usePlausibleEvent, + usePlausiblePageViewMonitor, + usePlausiblePingMonitor +} from '@sd/client'; import { QueryClient } from '@tanstack/react-query'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; @@ -17,19 +30,6 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { MenuProvider } from 'react-native-popup-menu'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { useSnapshot } from 'valtio'; -import { - ClientContextProvider, - initPlausible, - LibraryContextProvider, - P2PContextProvider, - RspcProvider, - useBridgeQuery, - useClientContext, - useInvalidateQuery, - usePlausibleEvent, - usePlausiblePageViewMonitor, - usePlausiblePingMonitor -} from '@sd/client'; import { GlobalModals } from './components/modal/GlobalModals'; import { Toast, toastConfig } from './components/primitive/Toast'; @@ -55,13 +55,17 @@ function AppNavigation() { const plausibleEvent = usePlausibleEvent(); const buildInfo = useBridgeQuery(['buildInfo']); - initPlausible({ platformType: 'mobile', buildInfo: buildInfo?.data }); - const navRef = useNavigationContainerRef(); const routeNameRef = useRef(); const [currentPath, setCurrentPath] = useState('/'); + useEffect(() => { + if (buildInfo?.data) { + initPlausible({ platformType: 'mobile', buildInfo: buildInfo.data }); + } + }, [buildInfo]); + usePlausiblePageViewMonitor({ currentPath }); usePlausiblePingMonitor({ currentPath }); @@ -73,9 +77,11 @@ function AppNavigation() { return () => clearInterval(interval); }, [plausibleEvent]); - if (library === null && libraries.data) { - currentLibraryStore.id = libraries.data[0]?.uuid ?? null; - } + useEffect(() => { + if (library === null && libraries.data) { + currentLibraryStore.id = libraries.data[0]?.uuid ?? null; + } + }, [library, libraries]); return ( { const navigation = useNavigation['navigation']>(); return ( - + Library diff --git a/apps/mobile/src/components/browse/BrowseLocations.tsx b/apps/mobile/src/components/browse/BrowseLocations.tsx index 819a50e22..991404cab 100644 --- a/apps/mobile/src/components/browse/BrowseLocations.tsx +++ b/apps/mobile/src/components/browse/BrowseLocations.tsx @@ -1,14 +1,15 @@ import { useNavigation } from '@react-navigation/native'; import { useLibraryQuery } from '@sd/client'; -import { DotsThree, Plus } from 'phosphor-react-native'; -import { useRef } from 'react'; -import { Text, View } from 'react-native'; +import { useRef, useState } from 'react'; +import { FlatList, Text, View } from 'react-native'; import { ModalRef } from '~/components/layout/Modal'; -import { tw } from '~/lib/tailwind'; +import { tw, twStyle } from '~/lib/tailwind'; import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack'; import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack'; +import { Plus } from 'phosphor-react-native'; import Empty from '../layout/Empty'; +import Fade from '../layout/Fade'; import { LocationItem } from '../locations/LocationItem'; import ImportModal from '../modal/ImportModal'; import { Button } from '../primitive/Button'; @@ -20,54 +21,64 @@ const BrowseLocations = () => { >(); const modalRef = useRef(null); - + const [showAll, setShowAll] = useState(false); const result = useLibraryQuery(['locations.list'], { keepPreviousData: true }); const locations = result.data; return ( - - + + Locations - - {locations?.length === 0 ? ( - - ) : ( - <> - {locations?.slice(0, 3).map((location) => ( + + + } + numColumns={showAll ? 3 : 1} + horizontal={showAll ? false : true} + contentContainerStyle={twStyle(locations?.length === 0 && 'w-full','px-5')} + key={showAll ? '_locations' : 'alllocationcols'} + keyExtractor={(item) => item.id.toString()} + scrollEnabled={showAll ? false : true} + showsHorizontalScrollIndicator={false} + renderItem={({ item }) => { + return ( navigation.navigate('SettingsStack', { screen: 'EditLocationSettings', - params: { id: location.id }, + params: { id: item.id }, initial: false }) } - onPress={() => navigation.navigate('Location', { id: location.id })} - /> - ))} - - )} + onPress={() => navigation.navigate('Location', { id: item.id })} + /> + )} + } + /> + diff --git a/apps/mobile/src/components/browse/BrowseTags.tsx b/apps/mobile/src/components/browse/BrowseTags.tsx index 4fc8369ae..a1360362d 100644 --- a/apps/mobile/src/components/browse/BrowseTags.tsx +++ b/apps/mobile/src/components/browse/BrowseTags.tsx @@ -1,13 +1,14 @@ import { useNavigation } from '@react-navigation/native'; import { useLibraryQuery } from '@sd/client'; -import { DotsThree, Plus } from 'phosphor-react-native'; -import React, { useRef } from 'react'; -import { Text, View } from 'react-native'; +import { Plus } from 'phosphor-react-native'; +import React, { useRef, useState } from 'react'; +import { FlatList, Text, View } from 'react-native'; import { ModalRef } from '~/components/layout/Modal'; -import { tw } from '~/lib/tailwind'; +import { tw, twStyle } from '~/lib/tailwind'; import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack'; import Empty from '../layout/Empty'; +import Fade from '../layout/Fade'; import CreateTagModal from '../modal/tag/CreateTagModal'; import { Button } from '../primitive/Button'; import { TagItem } from '../tags/TagItem'; @@ -19,47 +20,58 @@ const BrowseTags = () => { const tagData = tags.data; const modalRef = useRef(null); + const [showAll, setShowAll] = useState(false); return ( - - + + Tags - - {tagData?.length === 0 ? ( - - ) : ( - tagData - ?.slice(0, 3) - .map((tag) => ( + + + } + numColumns={showAll ? 3 : 1} + contentContainerStyle={twStyle(tagData?.length === 0 && 'w-full','px-5')} + horizontal={showAll ? false : true} + key={showAll ? '_tags' : 'alltagcols'} + keyExtractor={(item) => item.id.toString()} + scrollEnabled={showAll ? false : true} + showsHorizontalScrollIndicator={false} + renderItem={({ item }) => ( - navigation.navigate('Tag', { id: tag.id, color: tag.color! }) - } - /> - )) - )} + style={twStyle(showAll && 'max-w-[31%] flex-1')} + key={item.id} + tag={item} + onPress={() => + navigation.navigate('Tag', { id: item.id, color: item.color! }) + } + /> + )} + /> + diff --git a/apps/mobile/src/components/drawer/DrawerContent.tsx b/apps/mobile/src/components/drawer/DrawerContent.tsx index 02817e080..b46099351 100644 --- a/apps/mobile/src/components/drawer/DrawerContent.tsx +++ b/apps/mobile/src/components/drawer/DrawerContent.tsx @@ -1,11 +1,11 @@ import { DrawerContentScrollView } from '@react-navigation/drawer'; import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types'; import { AppLogo } from '@sd/assets/images'; +import { JobManagerContextProvider, useLibraryQuery } from '@sd/client'; import { Image } from 'expo-image'; import { CheckCircle } from 'phosphor-react-native'; import { useRef } from 'react'; import { Platform, Pressable, Text, View } from 'react-native'; -import { JobManagerContextProvider, useLibraryQuery } from '@sd/client'; import Layout from '~/constants/Layout'; import { tw, twStyle } from '~/lib/tailwind'; diff --git a/apps/mobile/src/components/drawer/DrawerLocations.tsx b/apps/mobile/src/components/drawer/DrawerLocations.tsx index 28a367cb5..2af0da0f6 100644 --- a/apps/mobile/src/components/drawer/DrawerLocations.tsx +++ b/apps/mobile/src/components/drawer/DrawerLocations.tsx @@ -1,14 +1,14 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'; import { useNavigation } from '@react-navigation/native'; -import { useRef } from 'react'; -import { Pressable, Text, View } from 'react-native'; import { + Location, arraysEqual, humanizeSize, - Location, useLibraryQuery, useOnlineLocations } from '@sd/client'; +import { useRef } from 'react'; +import { Pressable, Text, View } from 'react-native'; import { ModalRef } from '~/components/layout/Modal'; import { tw, twStyle } from '~/lib/tailwind'; @@ -49,8 +49,8 @@ const DrawerLocationItem: React.FC = ({ {location.name ?? ''} - - + + {`${humanizeSize(location.size_in_bytes)}`} diff --git a/apps/mobile/src/components/explorer/FileRow.tsx b/apps/mobile/src/components/explorer/FileRow.tsx index d376c0177..25ac79a5f 100644 --- a/apps/mobile/src/components/explorer/FileRow.tsx +++ b/apps/mobile/src/components/explorer/FileRow.tsx @@ -27,7 +27,7 @@ const FileRow = ({ data }: FileRowProps) => { height: getExplorerStore().listItemSize })} > - + diff --git a/apps/mobile/src/components/layout/Modal.tsx b/apps/mobile/src/components/layout/Modal.tsx index 6d2b60524..ecdc86872 100644 --- a/apps/mobile/src/components/layout/Modal.tsx +++ b/apps/mobile/src/components/layout/Modal.tsx @@ -6,10 +6,10 @@ import { BottomSheetHandleProps, BottomSheetModal, BottomSheetModalProps, - BottomSheetScrollView + BottomSheetScrollView, } from '@gorhom/bottom-sheet'; import { X } from 'phosphor-react-native'; -import { forwardRef, ReactNode } from 'react'; +import { ReactNode, forwardRef } from 'react'; import { Platform, Pressable, Text, View } from 'react-native'; import useForwardedRef from '~/hooks/useForwardedRef'; import { tw, twStyle } from '~/lib/tailwind'; diff --git a/apps/mobile/src/components/locations/GridLocation.tsx b/apps/mobile/src/components/locations/GridLocation.tsx index ed40b9c25..47c8e1853 100644 --- a/apps/mobile/src/components/locations/GridLocation.tsx +++ b/apps/mobile/src/components/locations/GridLocation.tsx @@ -1,6 +1,6 @@ +import { Location, arraysEqual, humanizeSize, useOnlineLocations } from '@sd/client'; import { DotsThreeOutlineVertical } from 'phosphor-react-native'; import { Pressable, Text, View } from 'react-native'; -import { arraysEqual, humanizeSize, Location, useOnlineLocations } from '@sd/client'; import { tw, twStyle } from '~/lib/tailwind'; import FolderIcon from '../icons/FolderIcon'; @@ -16,7 +16,7 @@ const GridLocation: React.FC = ({ location, modalRef }: GridL const onlineLocations = useOnlineLocations(); const online = onlineLocations.some((l) => arraysEqual(location.pub_id, l)); return ( - + @@ -46,9 +46,11 @@ const GridLocation: React.FC = ({ location, modalRef }: GridL {location.path} - + + {`${humanizeSize(location.size_in_bytes)}`} + ); }; diff --git a/apps/mobile/src/components/locations/ListLocation.tsx b/apps/mobile/src/components/locations/ListLocation.tsx index 90fc66b8b..66b242a18 100644 --- a/apps/mobile/src/components/locations/ListLocation.tsx +++ b/apps/mobile/src/components/locations/ListLocation.tsx @@ -61,10 +61,10 @@ const ListLocation = ({ location }: ListLocationProps) => { - - + + {`${humanizeSize(location.size_in_bytes)}`} diff --git a/apps/mobile/src/components/locations/LocationItem.tsx b/apps/mobile/src/components/locations/LocationItem.tsx index fae5729db..a9f476724 100644 --- a/apps/mobile/src/components/locations/LocationItem.tsx +++ b/apps/mobile/src/components/locations/LocationItem.tsx @@ -1,8 +1,9 @@ +import { Location } from '@sd/client'; import { useRef } from 'react'; import { Pressable } from 'react-native'; -import { Location } from '@sd/client'; import { twStyle } from '~/lib/tailwind'; +import { ClassInput } from 'twrnc'; import { ModalRef } from '../layout/Modal'; import { LocationModal } from '../modal/location/LocationModal'; import GridLocation from './GridLocation'; @@ -13,19 +14,21 @@ type LocationItemProps = { onPress: () => void; viewStyle?: 'grid' | 'list'; editLocation: () => void; + style?: ClassInput; }; export const LocationItem = ({ location, onPress, editLocation, - viewStyle = 'grid' + viewStyle = 'grid', + style }: LocationItemProps) => { const modalRef = useRef(null); return ( <> {viewStyle === 'grid' ? ( diff --git a/apps/mobile/src/components/modal/AddTagModal.tsx b/apps/mobile/src/components/modal/AddTagModal.tsx index 1beee726a..e45d16502 100644 --- a/apps/mobile/src/components/modal/AddTagModal.tsx +++ b/apps/mobile/src/components/modal/AddTagModal.tsx @@ -1,4 +1,4 @@ -import { Tag, getItemObject, useLibraryMutation, useLibraryQuery, useRspcContext } from "@sd/client"; +import { Tag, getItemObject, useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from "@sd/client"; import { CaretLeft, Plus } from "phosphor-react-native"; import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FlatList, NativeScrollEvent, Pressable, Text, View } from "react-native"; @@ -24,7 +24,7 @@ const AddTagModal = forwardRef((_, ref) => { const [startedScrolling, setStartedScrolling] = useState(false); const [reachedBottom, setReachedBottom] = useState(true); // needs to be set to true for initial rendering fade to be correct - const rspc = useRspcContext(); + const rspc = useRspcLibraryContext(); const tagsQuery = useLibraryQuery(['tags.list']); const tagsObjectQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1]); const mutation = useLibraryMutation(['tags.assign'], { diff --git a/apps/mobile/src/components/modal/ImportModal.tsx b/apps/mobile/src/components/modal/ImportModal.tsx index 70e98a93f..7980284c9 100644 --- a/apps/mobile/src/components/modal/ImportModal.tsx +++ b/apps/mobile/src/components/modal/ImportModal.tsx @@ -1,14 +1,15 @@ import * as RNFS from '@dr.pogodin/react-native-fs'; +import { useLibraryMutation, useRspcLibraryContext } from '@sd/client'; import { forwardRef, useCallback } from 'react'; import { Alert, Platform, Text, View } from 'react-native'; import DocumentPicker from 'react-native-document-picker'; -import { useLibraryMutation, useRspcLibraryContext } from '@sd/client'; import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import useForwardedRef from '~/hooks/useForwardedRef'; import { tw } from '~/lib/tailwind'; import { Icon } from '../icons/Icon'; +import { toast } from '../primitive/Toast'; // import * as ML from 'expo-media-library'; @@ -22,17 +23,27 @@ const ImportModal = forwardRef((_, ref) => { const createLocation = useLibraryMutation('locations.create', { onError: (error, variables) => { + modalRef.current?.close(); + //custom message handling + if (error.message.startsWith("location already exists")) { + return toast.error('This location has already been added'); + } switch (error.message) { case 'NEED_RELINK': if (!variables.dry_run) relinkLocation.mutate(variables.path); + toast.info('Please relink the location'); break; case 'ADD_LIBRARY': addLocationToLibrary.mutate(variables); break; default: + toast.error(error.message); throw new Error('Unimplemented custom remote error handling'); } }, + onSuccess: () => { + toast.success('Location added successfully'); + }, onSettled: () => { rspc.queryClient.invalidateQueries(['locations.list']); modalRef.current?.close(); diff --git a/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx b/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx index 9dc6a59fd..8d221305e 100644 --- a/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx +++ b/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx @@ -1,6 +1,7 @@ -import { useRef } from 'react'; import { useLibraryMutation, usePlausibleEvent, useRspcLibraryContext } from '@sd/client'; +import { useRef } from 'react'; import { ConfirmModal, ModalRef } from '~/components/layout/Modal'; +import { toast } from '~/components/primitive/Toast'; type Props = { locationId: number; @@ -20,6 +21,10 @@ const DeleteLocationModal = ({ trigger, onSubmit, locationId, triggerStyle }: Pr onSuccess: () => { submitPlausibleEvent({ event: { type: 'locationDelete' } }); onSubmit?.(); + toast.success('Location deleted successfully'); + }, + onError: (error) => { + toast.error(error.message); }, onSettled: () => { modalRef.current?.close(); diff --git a/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx b/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx index 54eb5e8cf..8d68272cc 100644 --- a/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx +++ b/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx @@ -1,6 +1,7 @@ -import { useRef } from 'react'; import { useLibraryMutation, usePlausibleEvent, useRspcLibraryContext } from '@sd/client'; +import { useRef } from 'react'; import { ConfirmModal, ModalRef } from '~/components/layout/Modal'; +import { toast } from '~/components/primitive/Toast'; type Props = { tagId: number; @@ -19,6 +20,7 @@ const DeleteTagModal = ({ trigger, onSubmit, tagId, triggerStyle }: Props) => { submitPlausibleEvent({ event: { type: 'tagDelete' } }); onSubmit?.(); rspc.queryClient.invalidateQueries(['tags.list']); + toast.success('Tag deleted successfully'); }, onSettled: () => { modalRef.current?.close(); diff --git a/apps/mobile/src/components/modal/inspector/ActionsModal.tsx b/apps/mobile/src/components/modal/inspector/ActionsModal.tsx index b0dcd79ee..f310ad2c9 100644 --- a/apps/mobile/src/components/modal/inspector/ActionsModal.tsx +++ b/apps/mobile/src/components/modal/inspector/ActionsModal.tsx @@ -3,7 +3,8 @@ import { getItemObject, humanizeSize, useLibraryMutation, - useLibraryQuery + useLibraryQuery, + useRspcContext } from '@sd/client'; import dayjs from 'dayjs'; import { @@ -28,6 +29,7 @@ import { tw, twStyle } from '~/lib/tailwind'; import { useActionsModalStore } from '~/stores/modalStore'; import FileInfoModal from './FileInfoModal'; +import RenameModal from './RenameModal'; type ActionsContainerProps = PropsWithChildren<{ style?: ViewStyle; @@ -65,8 +67,10 @@ const ActionDivider = () => ; export const ActionsModal = () => { const fileInfoRef = useRef(null); + const renameRef = useRef(null); const { modalRef, data } = useActionsModalStore(); + const rspc = useRspcContext(); const objectData = data && getItemObject(data); const filePath = data && getIndexedItemFilePath(data); @@ -77,6 +81,13 @@ export const ActionsModal = () => { enabled: filePath != null }); + const deleteFile = useLibraryMutation('files.deleteFiles', { + onSuccess: () => { + rspc.queryClient.invalidateQueries(['search.paths']) + modalRef.current?.dismiss(); + } + }); + async function handleOpen() { const absolutePath = queriedFullPath.data; if (!absolutePath) return; @@ -141,7 +152,9 @@ export const ActionsModal = () => { /> - + { + renameRef.current?.present(); + }} icon={Pencil} title="Rename" /> @@ -154,11 +167,19 @@ export const ActionsModal = () => { - + { + if (filePath && filePath.location_id) { + await deleteFile.mutateAsync({ + location_id: filePath.location_id, + file_path_ids: [filePath.id] + }); + } + }} /> )} + ); diff --git a/apps/mobile/src/components/modal/inspector/RenameModal.tsx b/apps/mobile/src/components/modal/inspector/RenameModal.tsx new file mode 100644 index 000000000..4bc7f100b --- /dev/null +++ b/apps/mobile/src/components/modal/inspector/RenameModal.tsx @@ -0,0 +1,90 @@ +import { getIndexedItemFilePath, useLibraryMutation, useRspcLibraryContext } from '@sd/client'; +import React, { forwardRef, useEffect, useRef, useState } from 'react'; +import { Text, View } from 'react-native'; +import { TextInput } from 'react-native-gesture-handler'; +import { Modal, ModalRef } from '~/components/layout/Modal'; +import { Button } from '~/components/primitive/Button'; +import { ModalInput } from '~/components/primitive/Input'; +import { toast } from '~/components/primitive/Toast'; +import useForwardedRef from '~/hooks/useForwardedRef'; +import { tw } from '~/lib/tailwind'; +import { useActionsModalStore } from '~/stores/modalStore'; + +interface Props { + objectName: string; +} + +const RenameModal = forwardRef((props, ref) => { + const modalRef = useForwardedRef(ref); + const [newName, setNewName] = useState(''); + const rspc = useRspcLibraryContext(); + const { data } = useActionsModalStore(); + const inputRef = useRef(null); + + const filePathData = data && getIndexedItemFilePath(data); + + const renameFile = useLibraryMutation(['files.renameFile'], { + onSuccess: () => { + modalRef.current?.dismiss(); + rspc.queryClient.invalidateQueries(['search.paths']); + }, + onError: () => { + toast.error('Failed to rename object'); + } + }); + + // set input value to object name on initial render + useEffect(() => { + setNewName(props.objectName); + }, [props.objectName]); + + const textRenameHandler = async () => { + switch (data?.type) { + case 'Path': + case 'Object': { + if (!filePathData) throw new Error('Failed to get file path object'); + + const { id, location_id } = filePathData; + + if (!location_id) throw new Error('Missing location id'); + + await renameFile.mutateAsync({ + location_id: location_id, + kind: { + One: { + from_file_path_id: id, + to: newName + } + } + }); + break; + } + } + }; + + return ( + setNewName(props.objectName)} + enableContentPanningGesture={false} + enablePanDownToClose={false} + snapPoints={['20']} + > + + inputRef.current?.setSelection(0, newName.length)} + value={newName} + onChangeText={(t) => setNewName(t)} + /> + + + + ); +}); + +export default RenameModal; diff --git a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx index d92fc40cf..ff4f9fdd0 100644 --- a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx +++ b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx @@ -1,16 +1,17 @@ -import { forwardRef, useEffect, useState } from 'react'; -import { Pressable, Text, View } from 'react-native'; -import ColorPicker from 'react-native-wheel-color-picker'; import { ToastDefautlColor, useLibraryMutation, usePlausibleEvent, useRspcLibraryContext } from '@sd/client'; +import { forwardRef, useEffect, useState } from 'react'; +import { Pressable, Text, View } from 'react-native'; +import ColorPicker from 'react-native-wheel-color-picker'; import { FadeInAnimation } from '~/components/animation/layout'; import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import { ModalInput } from '~/components/primitive/Input'; +import { toast } from '~/components/primitive/Toast'; import useForwardedRef from '~/hooks/useForwardedRef'; import { useKeyboard } from '~/hooks/useKeyboard'; import { tw, twStyle } from '~/lib/tailwind'; @@ -36,8 +37,12 @@ const CreateTagModal = forwardRef((_, ref) => { rspc.queryClient.invalidateQueries(['tags.list']); + toast.success('Tag created successfully'); submitPlausibleEvent({ event: { type: 'tagCreate' } }); }, + onError: (error) => { + toast.error(error.message); + }, onSettled: () => { // Close modal modalRef.current?.dismiss(); @@ -57,7 +62,7 @@ const CreateTagModal = forwardRef((_, ref) => { return ( { // Resets form onDismiss @@ -94,7 +99,7 @@ const CreateTagModal = forwardRef((_, ref) => { diff --git a/apps/mobile/src/components/overview/Locations.tsx b/apps/mobile/src/components/overview/Locations.tsx index 62984ac07..fb24ca639 100644 --- a/apps/mobile/src/components/overview/Locations.tsx +++ b/apps/mobile/src/components/overview/Locations.tsx @@ -1,8 +1,8 @@ import { useNavigation } from '@react-navigation/native'; +import { useLibraryQuery } from '@sd/client'; import React, { useRef } from 'react'; import { Pressable, Text, View } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; -import { useLibraryQuery } from '@sd/client'; import { tw, twStyle } from '~/lib/tailwind'; import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack'; @@ -25,10 +25,11 @@ const Locations = () => { <> + location.id.toString()} ItemSeparatorComponent={() => } @@ -73,6 +74,7 @@ const Locations = () => { )} /> + diff --git a/apps/mobile/src/components/overview/OverviewStats.tsx b/apps/mobile/src/components/overview/OverviewStats.tsx index da1719629..d72eba4e7 100644 --- a/apps/mobile/src/components/overview/OverviewStats.tsx +++ b/apps/mobile/src/components/overview/OverviewStats.tsx @@ -37,7 +37,7 @@ const StatItem = ({ title, bytes, isLoading, style }: StatItemProps) => { hidden: isLoading })} > - {title} + {title} {count} {unit} @@ -104,7 +104,7 @@ const OverviewStats = ({ stats }: Props) => { }; return ( - + Statistics diff --git a/apps/mobile/src/components/primitive/Button.tsx b/apps/mobile/src/components/primitive/Button.tsx index 37eabd611..16b5c0958 100644 --- a/apps/mobile/src/components/primitive/Button.tsx +++ b/apps/mobile/src/components/primitive/Button.tsx @@ -8,10 +8,10 @@ const button = cva(['items-center justify-center rounded-md border shadow-sm'], variants: { variant: { danger: ['border-red-800 bg-red-600 shadow-none'], - gray: ['border-app-lightborder bg-app-button shadow-none'], + 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-sidebar-line/60 bg-black shadow-none'], + outline: ['border-app-lightborder bg-black shadow-none'], transparent: ['border-0 bg-black shadow-none'], dashed: ['border border-dashed border-app-line bg-transparent shadow-none'] }, diff --git a/apps/mobile/src/components/primitive/Input.tsx b/apps/mobile/src/components/primitive/Input.tsx index 023b5c21b..abe5b453f 100644 --- a/apps/mobile/src/components/primitive/Input.tsx +++ b/apps/mobile/src/components/primitive/Input.tsx @@ -1,7 +1,7 @@ import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; import { cva, VariantProps } from 'class-variance-authority'; import { Eye, EyeSlash } from 'phosphor-react-native'; -import { useState } from 'react'; +import { forwardRef, useState } from 'react'; import { Pressable, TextInputProps as RNTextInputProps, TextInput, View } from 'react-native'; import { tw, twStyle } from '~/lib/tailwind'; @@ -23,28 +23,32 @@ const input = cva(['rounded-md border text-sm leading-tight shadow-sm'], { type InputProps = VariantProps & RNTextInputProps; -export const Input = ({ variant, size, ...props }: InputProps) => { - const { style, ...otherProps } = props; +export const Input = forwardRef((props, ref) => { + const { style, variant, size, ...otherProps } = props; return ( ); -}; +}) // To use in modals (for keyboard handling) -export const ModalInput = ({ variant, size, ...props }: InputProps) => { - const { style, ...otherProps } = props; +export const ModalInput = forwardRef((props, ref) => { + const { style, variant, size, ...otherProps } = props; return ( ); -}; +}) // Same as Input but configured with password props & show/hide password button diff --git a/apps/mobile/src/components/primitive/Toast.tsx b/apps/mobile/src/components/primitive/Toast.tsx index a22fc0d83..0ba8d9528 100644 --- a/apps/mobile/src/components/primitive/Toast.tsx +++ b/apps/mobile/src/components/primitive/Toast.tsx @@ -1,30 +1,41 @@ /* eslint-disable no-restricted-imports */ +import { CheckCircle, Info, WarningCircle } from 'phosphor-react-native'; import { Text, View } from 'react-native'; import Toast, { ToastConfig } from 'react-native-toast-message'; import { tw } from '~/lib/tailwind'; -const baseStyles = 'w-[340px] flex-row overflow-hidden rounded-md border p-3 shadow-lg'; +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 toastConfig: ToastConfig = { success: ({ text1, ...rest }) => ( - - + + + + {text1} + ), error: ({ text1, ...rest }) => ( - - + + + + {text1} + ), info: ({ text1, ...rest }) => ( - - + + + + {text1} + ) }; diff --git a/apps/mobile/src/components/tags/GridTag.tsx b/apps/mobile/src/components/tags/GridTag.tsx index 98fb764fb..a2598126d 100644 --- a/apps/mobile/src/components/tags/GridTag.tsx +++ b/apps/mobile/src/components/tags/GridTag.tsx @@ -16,7 +16,7 @@ const GridTag = ({ tag, modalRef }: GridTagProps) => { diff --git a/apps/mobile/src/components/tags/TagItem.tsx b/apps/mobile/src/components/tags/TagItem.tsx index e131fe501..7aba00a01 100644 --- a/apps/mobile/src/components/tags/TagItem.tsx +++ b/apps/mobile/src/components/tags/TagItem.tsx @@ -1,8 +1,9 @@ +import { Tag } from '@sd/client'; import { useRef } from 'react'; import { Pressable } from 'react-native'; -import { Tag } from '@sd/client'; import { twStyle } from '~/lib/tailwind'; +import { ClassInput } from 'twrnc'; import { ModalRef } from '../layout/Modal'; import { TagModal } from '../modal/tag/TagModal'; import GridTag from './GridTag'; @@ -12,14 +13,15 @@ type TagItemProps = { tag: Tag; onPress: () => void; viewStyle?: 'grid' | 'list'; + style?: ClassInput; }; -export const TagItem = ({ tag, onPress, viewStyle = 'grid' }: TagItemProps) => { +export const TagItem = ({ tag, onPress, style, viewStyle = 'grid' }: TagItemProps) => { const modalRef = useRef(null); return ( <> diff --git a/apps/mobile/src/hooks/useFiltersSearch.ts b/apps/mobile/src/hooks/useFiltersSearch.ts index 37c17608f..a93bd0216 100644 --- a/apps/mobile/src/hooks/useFiltersSearch.ts +++ b/apps/mobile/src/hooks/useFiltersSearch.ts @@ -60,7 +60,7 @@ export function useFiltersSearch(search: string) { const filters = [] as SearchFilterArgs[]; //It's a global search if no locations have been selected - if (searchStore.filters.locations.length === 0 || !name || !ext) { + if (searchStore.filters.locations.length === 0) { const locationIds = locations.data?.map((l) => l.id); if (locationIds) filters.push({ filePath: { locations: { in: locationIds } } }); } diff --git a/apps/mobile/src/screens/browse/Location.tsx b/apps/mobile/src/screens/browse/Location.tsx index fe02529d0..39a767968 100644 --- a/apps/mobile/src/screens/browse/Location.tsx +++ b/apps/mobile/src/screens/browse/Location.tsx @@ -1,4 +1,4 @@ -import { useLibraryQuery, usePathsExplorerQuery } from '@sd/client'; +import { useLibraryQuery, useLibrarySubscription, usePathsExplorerQuery } from '@sd/client'; import { useEffect, useMemo } from 'react'; import Explorer from '~/components/explorer/Explorer'; import Empty from '~/components/layout/Empty'; @@ -19,6 +19,13 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP .pop(); }, [path]) + // makes sure that the location shows newest/modified objects + // when a location is opened + useLibrarySubscription( + ['locations.quickRescan', { sub_path: path ?? '', location_id: id }], + { onData() {} } + ); + const paths = usePathsExplorerQuery({ arg: { filters: [ diff --git a/apps/mobile/tests/add-tag.yml b/apps/mobile/tests/add-tag.yml index 893545b0e..9eb1236f6 100644 --- a/apps/mobile/tests/add-tag.yml +++ b/apps/mobile/tests/add-tag.yml @@ -5,10 +5,10 @@ appId: com.spacedrive.app id: 'browse-tab' - waitForAnimationToEnd - tapOn: - id: 'navigate-tags-screen' -- tapOn: - id: 'create-tag-modal' + id: 'create-tag-button' - inputText: 'MyTag' - tapOn: text: 'Create' +- tapOn: + id: 'show-all-tags-button' - assertVisible: 'MyTag' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d102aeed5c3d1298a509f59287249cdec4dccc7..0f94df44787bec088c7bde3fd3512d19d81c26bc 100644 GIT binary patch delta 4043 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 delta 4523 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