Files
spacedrive/interface/app/$libraryId/Explorer/File/RenameTextBox.tsx
Jamie Pine e8d3ad6005 [ENG-779] Finalize UI (#986)
* [ENG-779] Finalize UI
This is one branch with a variety of UI changes

add tag select mode bar without functionality 

fix group job status 

add notice icon with info to stat icons

add WIP notice to media view 

add modal before add location with greyed out clouds

remove disappearing add location button

add WIP spacedrop page 

bring back limited key manager UI 

add options bar on search page without functionality 

Add greyed out encrypt library button or setting

See more button on locations

Show locations on node screen

Fix overview category left padding

* key manager placeholder

* stat info

* nodes screen

* location click yay

* fix size in bytes

Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com>

* small ui improvements

* sh*tty see more button

* last touches

* fix merge boo boo

* Fix mobile
 - Move `getItemObject`, `getItemFilePath`, `getItemLocation`, `getExplorerItemData` to @sd/core to allow mobile to use them

* Formatting

* Normalize displayed file size between all screens
 - Replace every use of internal formatBytes with byte-size dep

---------

Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com>
Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com>
2023-06-22 06:34:45 +00:00

260 lines
6.3 KiB
TypeScript

/* eslint-disable react-hooks/exhaustive-deps */
import clsx from 'clsx';
import { ComponentProps, forwardRef, useEffect, useRef, useState } from 'react';
import { useKey } from 'rooks';
import { useLibraryMutation, useRspcLibraryContext } from '@sd/client';
import { showAlertDialog } from '~/components';
import { useOperatingSystem } from '~/hooks';
import { useExplorerViewContext } from '../ViewContext';
type Props = ComponentProps<'div'> & {
itemId: number;
locationId: number | null;
text: string | null;
activeClassName?: string;
disabled?: boolean;
renameHandler: (name: string) => Promise<void>;
};
export const RenameTextBoxBase = forwardRef<HTMLDivElement, Props>(
({ className, activeClassName, disabled, ...props }, _ref) => {
const explorerView = useExplorerViewContext();
const os = useOperatingSystem();
const [allowRename, setAllowRename] = useState(false);
const [renamable, setRenamable] = useState(false);
const funnyRef = useRef<HTMLDivElement>(null);
const ref = typeof _ref === 'function' ? { current: funnyRef.current } : _ref;
// Highlight file name up to extension or
// fully if it's a directory or has no extension
function highlightText() {
if (ref?.current) {
const range = document.createRange();
const node = ref.current.firstChild;
if (!node) return;
range.setStart(node, 0);
range.setEnd(node, props?.text?.length || 0);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
}
// Blur field
function blur() {
if (ref?.current) {
ref.current.blur();
setAllowRename(false);
}
}
// Reset to original file name
function reset() {
if (ref?.current) {
ref.current.innerText = props.text || '';
}
}
async function handleRename() {
if (!ref?.current) return;
const newName = ref?.current.innerText.trim();
if (!newName) return reset();
if (!props.locationId) return;
const oldName = props.text;
if (!oldName || !props.locationId || newName === oldName) return;
await props.renameHandler(newName);
}
// Handle keydown events
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
switch (e.key) {
case 'Tab':
e.preventDefault();
blur();
break;
case 'Escape':
reset();
blur();
break;
case 'z':
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
reset();
highlightText();
}
break;
}
}
// Focus and highlight when renaming is allowed
useEffect(() => {
if (allowRename) {
explorerView.setIsRenaming(true);
setTimeout(() => {
if (ref?.current) {
ref.current.focus();
highlightText();
}
});
}
}, [allowRename]);
// Handle renaming when triggered from outside
useEffect(() => {
if (!disabled) {
if (explorerView.isRenaming && !allowRename) setAllowRename(true);
else if (!explorerView.isRenaming && allowRename) setAllowRename(false);
}
}, [explorerView.isRenaming]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref?.current && !ref.current.contains(event.target as Node)) {
blur();
}
}
document.addEventListener('mousedown', handleClickOutside, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
};
}, [ref]);
// Rename or blur on Enter key
useKey('Enter', (e) => {
if (allowRename) {
e.preventDefault();
blur();
} else if (!disabled) setAllowRename(true);
});
return (
<div
ref={ref}
role="textbox"
contentEditable={allowRename}
suppressContentEditableWarning
className={clsx(
'cursor-default overflow-y-auto truncate rounded-md px-1.5 py-px text-xs text-ink',
allowRename && [
'whitespace-normal bg-app outline-none ring-2 ring-accent-deep',
activeClassName
],
className
)}
onDoubleClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.button === 0 && setRenamable(!disabled)}
onMouseUp={(e) => {
if (e.button === 0) {
if (renamable) {
setAllowRename(true);
}
setRenamable(false);
}
}}
onBlur={async () => {
await handleRename();
setAllowRename(false);
explorerView.setIsRenaming(false);
}}
onKeyDown={handleKeyDown}
{...props}
>
{props.text}
</div>
);
}
);
export const RenamePathTextBox = (
props: Omit<Props, 'renameHandler'> & { isDir: boolean; extension?: string | null }
) => {
const rspc = useRspcLibraryContext();
const ref = useRef<HTMLDivElement>(null);
const renameFile = useLibraryMutation(['files.renameFile'], {
onError: () => reset(),
onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths'])
});
// Reset to original file name
function reset() {
if (ref?.current) {
ref.current.innerText = props.text || '';
}
}
const fileName =
props.isDir || !props.extension ? props.text : props.text + '.' + props.extension;
// Handle renaming
async function rename(newName: string) {
if (!props.locationId || newName === fileName) return;
try {
await renameFile.mutateAsync({
location_id: props.locationId,
kind: {
One: {
from_file_path_id: props.itemId,
to: newName
}
}
});
} catch (e) {
showAlertDialog({
title: 'Error',
value: String(e)
});
}
}
return <RenameTextBoxBase {...props} text={fileName} renameHandler={rename} ref={ref} />;
};
export const RenameLocationTextBox = (props: Omit<Props, 'renameHandler'>) => {
const rspc = useRspcLibraryContext();
const ref = useRef<HTMLDivElement>(null);
const renameLocation = useLibraryMutation(['locations.update'], {
onError: () => reset(),
onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths'])
});
// Reset to original file name
function reset() {
if (ref?.current) {
ref.current.innerText = props.text || '';
}
}
// Handle renaming
async function rename(newName: string) {
if (!props.locationId) return;
try {
await renameLocation.mutateAsync({
id: props.locationId,
name: newName,
generate_preview_media: null,
sync_preview_media: null,
hidden: null,
indexer_rules_ids: []
});
} catch (e) {
showAlertDialog({
title: 'Error',
value: String(e)
});
}
}
return <RenameTextBoxBase {...props} renameHandler={rename} ref={ref} />;
};