diff --git a/interface/app/$libraryId/Explorer/util.ts b/interface/app/$libraryId/Explorer/util.ts index 8daccaf8e..373b27a03 100644 --- a/interface/app/$libraryId/Explorer/util.ts +++ b/interface/app/$libraryId/Explorer/util.ts @@ -185,3 +185,12 @@ export function translateKindName(kindName: string): string { return kindName; } } + +export function fetchAccessToken(): string { + const accessToken: string = + JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]') + .find((cookie: string) => cookie.startsWith('st-access-token')) + ?.split('=')[1] + .split(';')[0] || ''; + return accessToken; +} diff --git a/interface/app/$libraryId/overview/index.tsx b/interface/app/$libraryId/overview/index.tsx index 71569d20c..b4578d36c 100644 --- a/interface/app/$libraryId/overview/index.tsx +++ b/interface/app/$libraryId/overview/index.tsx @@ -1,7 +1,7 @@ import { Key } from 'react'; import { Link } from 'react-router-dom'; import { HardwareModel, useBridgeQuery, useLibraryQuery } from '@sd/client'; -import { useLocale, useOperatingSystem } from '~/hooks'; +import { useAccessToken, useLocale, useOperatingSystem } from '~/hooks'; import { useRouteTitle } from '~/hooks/useRouteTitle'; import { hardwareModelToIcon } from '~/util/hardware'; @@ -28,16 +28,14 @@ export const Component = () => { const os = useOperatingSystem(); const { t } = useLocale(); + const accessToken = useAccessToken(); const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true }); const locations = locationsQuery.data ?? []; // not sure if we'll need the node state in the future, as it should be returned with the cloud.devices.list query // const { data: node } = useBridgeQuery(['nodeState']); - const cloudDevicesList = useBridgeQuery(['cloud.devices.list'], { - suspense: true, - retry: false - }); + const cloudDevicesList = useBridgeQuery(['cloud.devices.list', { access_token: accessToken }]); const search = useSearchFromSearchParams({ defaultTarget: 'paths' }); diff --git a/interface/app/$libraryId/settings/node/libraries/DeleteDeviceDialog.tsx b/interface/app/$libraryId/settings/node/libraries/DeleteDeviceDialog.tsx new file mode 100644 index 000000000..db19933de --- /dev/null +++ b/interface/app/$libraryId/settings/node/libraries/DeleteDeviceDialog.tsx @@ -0,0 +1,94 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { HardwareModel, useBridgeMutation, useBridgeQuery, useZodForm } from '@sd/client'; +import { Dialog, ErrorMessage, useDialog, UseDialogProps } from '@sd/ui'; +import { Icon } from '~/components'; +import { useAccessToken, useLocale } from '~/hooks'; +import { hardwareModelToIcon } from '~/util/hardware'; +import { usePlatform } from '~/util/Platform'; + +interface Props extends UseDialogProps { + pubId: string; + name: string; + device_model: string; +} + +interface CorePubId { + Uuid: string; +} + +export default function DeleteLibraryDialog(props: Props) { + const { t } = useLocale(); + + const queryClient = useQueryClient(); + const platform = usePlatform(); + const navigate = useNavigate(); + const accessToken = useAccessToken(); + const { data: node } = useBridgeQuery(['nodeState']); + const deleteDevice = useBridgeMutation('cloud.devices.delete'); + const deviceAmount = useBridgeQuery(['cloud.devices.list', { access_token: accessToken }]).data + ?.length; + + const form = useZodForm(); + + // Check if the current device matches the UUID or if it's the only device + useEffect(() => { + if (deviceAmount === 1) { + form.setError('pubId', { + type: 'manual', + message: t('error_only_device') + }); + } else if ((node?.id as CorePubId).Uuid === props.pubId) { + form.setError('pubId', { + type: 'manual', + message: t('error_current_device') + }); + } + }, [form, node, props.pubId, deviceAmount, t]); + + const onSubmit = form.handleSubmit(async () => { + try { + // Check for form errors before proceeding + if (form.formState.errors.pubId) { + return; + } + + await deleteDevice.mutateAsync({ + access_token: accessToken, + pub_id: props.pubId + }); + queryClient.invalidateQueries(['library.list']); + + platform.refreshMenuBar && platform.refreshMenuBar(); + navigate('/'); + } catch (e) { + alert(`Failed to delete device: ${e}`); + } + }); + + return ( + +
+ +

{props.name}

+ +
+
+ ); +} diff --git a/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx b/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx index 1fc9fca0e..8c0e734c4 100644 --- a/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx +++ b/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx @@ -2,11 +2,13 @@ import { Trash } from '@phosphor-icons/react'; import { iconNames } from '@sd/assets/util'; import { Key } from 'react'; import { HardwareModel, humanizeSize } from '@sd/client'; -import { Button, Card, Tooltip } from '@sd/ui'; +import { Button, Card, dialogManager, Tooltip } from '@sd/ui'; import { Icon } from '~/components'; -import { useLocale } from '~/hooks'; +import { useAccessToken, useLocale } from '~/hooks'; import { hardwareModelToIcon } from '~/util/hardware'; +import DeleteDeviceDialog from './DeleteDeviceDialog'; + interface DeviceItemProps { pub_id: Key | null | undefined; name: string; @@ -46,7 +48,19 @@ export default (props: DeviceItemProps) => { }} > - + { + dialogManager.create((dp) => ( + + )); + }} + className="size-4" + /> diff --git a/interface/app/$libraryId/settings/node/libraries/ListItem.tsx b/interface/app/$libraryId/settings/node/libraries/ListItem.tsx index b6ff11a8e..78bc8beec 100644 --- a/interface/app/$libraryId/settings/node/libraries/ListItem.tsx +++ b/interface/app/$libraryId/settings/node/libraries/ListItem.tsx @@ -4,7 +4,7 @@ import { Key, useState } from 'react'; import { LibraryConfigWrapped, useBridgeQuery } from '@sd/client'; import { Button, ButtonLink, Card, dialogManager, Tooltip } from '@sd/ui'; import { Icon } from '~/components'; -import { useLocale } from '~/hooks'; +import { useAccessToken, useLocale } from '~/hooks'; import DeleteDialog from './DeleteDialog'; import DeviceItem from './DeviceItem'; @@ -18,12 +18,8 @@ export default (props: Props) => { const { t } = useLocale(); const [isExpanded, setIsExpanded] = useState(false); - const cloudDevicesList = useBridgeQuery(['cloud.devices.list'], { - suspense: true, - retry: false - }); - console.log(cloudDevicesList); - + const accessToken = useAccessToken(); + const cloudDevicesList = useBridgeQuery(['cloud.devices.list', { access_token: accessToken }]); const toggleExpansion = () => { setIsExpanded((prev) => !prev); }; diff --git a/interface/hooks/index.ts b/interface/hooks/index.ts index 8d1b54ac1..fa9720835 100644 --- a/interface/hooks/index.ts +++ b/interface/hooks/index.ts @@ -32,3 +32,4 @@ export * from './useZodParams'; export * from './useZodRouteParams'; export * from './useZodSearchParams'; export * from './useDeeplinkEventHandler'; +export * from './useAccessToken'; diff --git a/interface/hooks/useAccessToken.ts b/interface/hooks/useAccessToken.ts new file mode 100644 index 000000000..d0a20a1e9 --- /dev/null +++ b/interface/hooks/useAccessToken.ts @@ -0,0 +1,8 @@ +export function useAccessToken(): string { + const accessToken: string = + JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]') + .find((cookie: string) => cookie.startsWith('st-access-token')) + ?.split('=')[1] + .split(';')[0] || ''; + return accessToken.trim(); +} diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json index 3f5af2450..0f97989b7 100644 --- a/interface/locales/en/common.json +++ b/interface/locales/en/common.json @@ -168,11 +168,13 @@ "default": "Default", "default_settings": "Default Settings", "delete": "Delete", + "delete_device": "Delete device", + "delete_device_description": "This is permanent! This device will lose access to its the corresponding library and be removed. ", "delete_dialog_title": "Delete {{prefix}} {{type}}", "delete_forever": "Delete Forever", "delete_info": "This will not delete the actual folder on disk. Preview media will be deleted.", "delete_library": "Delete Library", - "delete_library_description": "This is permanent! Original files will not be deleted, only the Spacedrive library.", + "delete_library_description": "This is permanent! Only the Spacedrive library will be deleted, and original files will remain untouched.", "delete_location": "Delete Location", "delete_location_description": "Deleting a location will also remove all files associated with it from the Spacedrive database, the files themselves will not be deleted.", "delete_object": "Delete object", @@ -191,7 +193,6 @@ "dialog": "Dialog", "dialog_shortcut_description": "To perform actions and operations", "direction": "Direction", - "drop_files_here_to_send_with": "Drop files here to send with Spacedrop", "directory_one": "directory", "directory_other": "directories", "disabled": "Disabled", @@ -212,6 +213,7 @@ "download": "Download", "downloading_update": "Downloading Update", "drag_to_resize": "Drag to resize", + "drop_files_here_to_send_with": "Drop files here to send with Spacedrop", "duplicate": "Duplicate", "duplicate_object": "Duplicate object", "duplicate_success": "Items duplicated", @@ -242,8 +244,10 @@ "erase_a_file": "Erase a file", "erase_a_file_description": "Configure your erasure settings.", "error": "Error", + "error_current_device": "You are currently on this device, and cannot delete the device from the library. Please use another device if you'd like to remove this device.", "error_loading_original_file": "Error loading original file", "error_message": "Error: {{error}}.", + "error_only_device": "You cannot delete this device as it is the only device that belongs to this library.", "error_unknown": "An unknown error occurred.", "executable": "Executable", "executable_one": "Executable", diff --git a/packages/ui/src/forms/Form.tsx b/packages/ui/src/forms/Form.tsx index 00165b408..cb7c7bdb0 100644 --- a/packages/ui/src/forms/Form.tsx +++ b/packages/ui/src/forms/Form.tsx @@ -50,7 +50,7 @@ export const Form = ({ }; export const errorStyles = cva( - 'flex justify-center gap-2 break-all rounded border border-red-500/40 bg-red-800/40 px-3 py-2 text-white', + 'flex justify-center gap-2 whitespace-normal break-words rounded border border-red-500/40 bg-red-800/40 px-3 py-2 text-white', { variants: { variant: { @@ -89,7 +89,7 @@ export const ErrorMessage = ({ name, variant, className }: ErrorMessageProps) => return typeof message === 'string' ? ( -

{message}

+

{message}

) : null; })}