mirror of
https://github.com/ProtonMail/WebClients.git
synced 2026-01-11 20:06:40 +00:00
Merge branch 'DRVWEB-5047-select-in-rename-modal' into 'main'
[DRVWEB-5047] Ensure finename extension is not selected in rename modal See merge request web/clients!21674
This commit is contained in:
commit
db44606ce3
10 changed files with 167 additions and 81 deletions
|
|
@ -32,6 +32,7 @@ const RenameButton = ({ link, showRenameModal, close }: Props) => {
|
|||
isDoc: isProtonDocsDocument(link.mimeType),
|
||||
name: link.name,
|
||||
volumeId: link.volumeId,
|
||||
mediaType: link.mimeType,
|
||||
linkId: link.linkId,
|
||||
onSubmit: (formattedName) =>
|
||||
renameLink(new AbortController().signal, link.rootShareId, link.linkId, formattedName),
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const ActionsDropdown = ({ volumeId, shareId, selectedLinks, permissions, trashL
|
|||
showRenameModal({
|
||||
isFile: selectedLinks[0].isFile,
|
||||
name: selectedLinks[0].name,
|
||||
mediaType: selectedLinks[0].mimeType,
|
||||
isDoc: isProtonDocsDocument(selectedLinks[0].mimeType),
|
||||
volumeId: selectedLinks[0].volumeId,
|
||||
linkId: selectedLinks[0].linkId,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const RenameButton = ({ link, showRenameModal, close }: Props) => {
|
|||
isFile: link.isFile,
|
||||
name: link.name,
|
||||
isDoc: isProtonDocsDocument(link.mimeType),
|
||||
mediaType: link.mimeType,
|
||||
volumeId: link.volumeId,
|
||||
linkId: link.linkId,
|
||||
onSubmit: (formattedName) =>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const RenameButton = ({ selectedLinks, renameLink }: RenameButtonProps) => {
|
|||
isFile: selectedLinks[0].isFile,
|
||||
name: selectedLinks[0].name,
|
||||
isDoc: isProtonDocsDocument(selectedLinks[0].mimeType),
|
||||
mediaType: selectedLinks[0].mimeType,
|
||||
volumeId: selectedLinks[0].volumeId,
|
||||
linkId: selectedLinks[0].linkId,
|
||||
onSubmit: (formattedName) =>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ export type RenameModalViewProps = {
|
|||
isFile: boolean;
|
||||
onClose?: () => void;
|
||||
name: string;
|
||||
ignoreExtension: boolean;
|
||||
// The name (or part of it) that will need to be highlighted at
|
||||
// focus time in the modal's input.
|
||||
nameToFocus: string;
|
||||
isDoc?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -34,7 +36,7 @@ export const RenameModalView = ({
|
|||
handleSubmit,
|
||||
name: originalName,
|
||||
isFile,
|
||||
ignoreExtension,
|
||||
nameToFocus,
|
||||
// modalProps
|
||||
onClose,
|
||||
onExit,
|
||||
|
|
@ -48,11 +50,8 @@ export const RenameModalView = ({
|
|||
if (autofocusDone) {
|
||||
return;
|
||||
}
|
||||
e.target.setSelectionRange(0, nameToFocus.length);
|
||||
setAutofocusDone(true);
|
||||
if (ignoreExtension) {
|
||||
return e.target.select();
|
||||
}
|
||||
e.target.setSelectionRange(0, originalName.length);
|
||||
};
|
||||
|
||||
const onBlur = ({ target }: FocusEvent<HTMLInputElement>) => {
|
||||
|
|
|
|||
|
|
@ -1,63 +1,152 @@
|
|||
import { MemberRole, type NodeEntity, NodeType } from '@proton/drive/index';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { PROTON_DOCS_DOCUMENT_MIMETYPE, PROTON_DOCS_SPREADSHEET_MIMETYPE } from '@proton/shared/lib/helpers/mimetype';
|
||||
|
||||
import { getIgnoreExtension } from './useRenameModalState';
|
||||
import { useRenameModalState } from './useRenameModalState';
|
||||
|
||||
export const mockNode = (overrides: Partial<NodeEntity> = {}): NodeEntity => ({
|
||||
uid: 'mock-uid',
|
||||
parentUid: 'parent-uid',
|
||||
name: 'mock-file.txt',
|
||||
keyAuthor: {
|
||||
ok: true,
|
||||
value: 'key-author-1',
|
||||
},
|
||||
nameAuthor: {
|
||||
ok: true,
|
||||
value: 'name-author-1',
|
||||
},
|
||||
directRole: MemberRole.Admin,
|
||||
type: NodeType.File,
|
||||
mediaType: 'application/octet-stream',
|
||||
isShared: false,
|
||||
isSharedPublicly: false,
|
||||
creationTime: new Date('2024-01-01T00:00:00Z'),
|
||||
modificationTime: new Date('2024-01-01T00:00:00Z'),
|
||||
trashTime: undefined,
|
||||
totalStorageSize: 12345,
|
||||
activeRevision: undefined,
|
||||
folder: {
|
||||
claimedModificationTime: new Date('2024-01-01T00:00:00Z'),
|
||||
},
|
||||
treeEventScopeId: 'tree-event-scope-id',
|
||||
...overrides,
|
||||
});
|
||||
jest.mock('@proton/components', () => ({
|
||||
useNotifications: jest.fn(() => ({
|
||||
createNotification: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('getIgnoreExtension', () => {
|
||||
jest.mock('@proton/drive/index', () => ({
|
||||
useDrive: jest.fn(() => ({
|
||||
drive: {
|
||||
renameNode: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
onClose: () => {},
|
||||
onExit: () => {},
|
||||
onSubmit: (_newName: string) => Promise.resolve(),
|
||||
volumeId: 'VOLUME_ID',
|
||||
linkId: 'LINK_ID',
|
||||
open: true,
|
||||
name: 'DEFAULT_NAME',
|
||||
isFile: true,
|
||||
isDoc: false,
|
||||
mediaType: 'DEFAULT_MEDIATYPE',
|
||||
};
|
||||
|
||||
describe('useRenameModalState', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns true if node is null', () => {
|
||||
expect(getIgnoreExtension(null, 'file.docx')).toBe(true);
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns true if node is a folder', () => {
|
||||
expect(getIgnoreExtension(mockNode({ type: NodeType.Folder, mediaType: undefined }), 'folder')).toBe(true);
|
||||
});
|
||||
describe('Compute the filename to focus', () => {
|
||||
it('should focus the filename without the png extension', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'myimage.png',
|
||||
mediaType: 'image/png',
|
||||
})
|
||||
);
|
||||
|
||||
it('returns true if mediaType is Proton Docs', () => {
|
||||
expect(getIgnoreExtension(mockNode({ mediaType: PROTON_DOCS_DOCUMENT_MIMETYPE }), 'file.docx')).toBe(true);
|
||||
});
|
||||
expect(result.current.nameToFocus).toBe('myimage');
|
||||
});
|
||||
|
||||
it('returns true if mediaType is Proton Sheets', () => {
|
||||
expect(getIgnoreExtension(mockNode({ mediaType: PROTON_DOCS_SPREADSHEET_MIMETYPE }), 'file.docx')).toBe(true);
|
||||
});
|
||||
it('should focus the filename without the vcf extension', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'myvcard.vcf',
|
||||
mediaType: 'text/vcard',
|
||||
})
|
||||
);
|
||||
|
||||
it('returns true if name doesnt include an extension', () => {
|
||||
expect(getIgnoreExtension(mockNode({ mediaType: 'text/plain' }), '.txt')).toBe(true);
|
||||
});
|
||||
expect(result.current.nameToFocus).toBe('myvcard');
|
||||
});
|
||||
|
||||
it('returns false in case of file with extention', () => {
|
||||
expect(getIgnoreExtension(mockNode({ mediaType: 'text/plain' }), 'file.txt')).toBe(false);
|
||||
it('should focus the entire filename when the name is missing', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: '.txt',
|
||||
mediaType: 'plain/text',
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('.txt');
|
||||
});
|
||||
|
||||
it('should focus the filename without the docx extension', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'mydocument.docx',
|
||||
mediaType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('mydocument');
|
||||
});
|
||||
|
||||
it('should focus the entire name for folder', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'myfolder',
|
||||
isFile: false,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('myfolder');
|
||||
});
|
||||
|
||||
it('should focus the entire name for folder, even with a dot inside', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'myfolder.withdot',
|
||||
isFile: false,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('myfolder.withdot');
|
||||
});
|
||||
|
||||
it('should focus the entire name for proton documents', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'myprotondocument.with_dot_inside',
|
||||
mediaType: PROTON_DOCS_DOCUMENT_MIMETYPE,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('myprotondocument.with_dot_inside');
|
||||
});
|
||||
|
||||
it('should focus the entire name for proton sheets', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'myprotonsheets.with_dot_inside',
|
||||
mediaType: PROTON_DOCS_SPREADSHEET_MIMETYPE,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('myprotonsheets.with_dot_inside');
|
||||
});
|
||||
|
||||
it('should focus the entire name when missing the mediatype', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRenameModalState({
|
||||
...defaultProps,
|
||||
name: 'somefile.mp3',
|
||||
mediaType: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.nameToFocus).toBe('somefile.mp3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { c } from 'ttag';
|
||||
|
||||
import { type ModalStateProps, useNotifications } from '@proton/components';
|
||||
import type { NodeEntity } from '@proton/drive';
|
||||
import { generateNodeUid, useDrive } from '@proton/drive';
|
||||
import { splitExtension } from '@proton/shared/lib/helpers/file';
|
||||
import { isProtonDocsDocument, isProtonDocsSpreadsheet } from '@proton/shared/lib/helpers/mimetype';
|
||||
|
||||
import { useDriveEventManager } from '../../store';
|
||||
import { useSdkErrorHandler } from '../../utils/errorHandling/useSdkErrorHandler';
|
||||
import { getNodeEntity } from '../../utils/sdk/getNodeEntity';
|
||||
|
||||
export type RenameModalInnerProps = {
|
||||
onSuccess?: (newName: string) => Promise<void>;
|
||||
volumeId: string;
|
||||
linkId: string;
|
||||
name: string;
|
||||
mediaType: string | undefined;
|
||||
isFile: boolean; // isFile could come from getNode but it will be slow with noticeable delay in the modal
|
||||
/** @deprecated used only on legacy, it will not be handled here */
|
||||
onSubmit?: (newName: string) => Promise<void>;
|
||||
|
|
@ -36,14 +33,20 @@ const splitLinkName = (linkName: string) => {
|
|||
return splitExtension(linkName);
|
||||
};
|
||||
|
||||
export const getIgnoreExtension = (node: null | NodeEntity, name: string) => {
|
||||
const isFile = node?.type === 'file';
|
||||
let ignoreExtension = !isFile;
|
||||
if (node !== null && node.mediaType) {
|
||||
const [namePart] = splitLinkName(name);
|
||||
ignoreExtension = isProtonDocsDocument(node.mediaType) || isProtonDocsSpreadsheet(node.mediaType) || !namePart;
|
||||
const computeFilenameToFocus = (name: string, isFile: boolean, mediaType: string | undefined) => {
|
||||
if (!isFile || !mediaType) {
|
||||
return name;
|
||||
}
|
||||
return ignoreExtension;
|
||||
|
||||
if (isProtonDocsDocument(mediaType) || isProtonDocsSpreadsheet(mediaType)) {
|
||||
return name;
|
||||
}
|
||||
const [namePart] = splitLinkName(name);
|
||||
if (namePart) {
|
||||
return namePart;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const useRenameModalState = ({
|
||||
|
|
@ -52,28 +55,14 @@ export const useRenameModalState = ({
|
|||
linkId,
|
||||
name,
|
||||
isFile,
|
||||
mediaType,
|
||||
onSuccess,
|
||||
...modalProps
|
||||
}: UseRenameModalProps) => {
|
||||
const { drive } = useDrive();
|
||||
const events = useDriveEventManager();
|
||||
const { createNotification } = useNotifications();
|
||||
const [node, setNode] = useState<null | NodeEntity>(null);
|
||||
const ignoreExtension = getIgnoreExtension(node, name);
|
||||
const { handleError } = useSdkErrorHandler();
|
||||
useEffect(() => {
|
||||
const fetchNode = async () => {
|
||||
try {
|
||||
const maybeNode = await drive.getNode(generateNodeUid(volumeId, linkId));
|
||||
const { node } = getNodeEntity(maybeNode);
|
||||
setNode(node);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
void fetchNode();
|
||||
}, [volumeId, linkId, createNotification, drive, handleError, onClose]);
|
||||
|
||||
const handleSubmit = async (newName: string) => {
|
||||
const nodeUid = generateNodeUid(volumeId, linkId);
|
||||
|
|
@ -97,7 +86,7 @@ export const useRenameModalState = ({
|
|||
handleSubmit,
|
||||
onClose,
|
||||
name,
|
||||
ignoreExtension,
|
||||
nameToFocus: computeFilenameToFocus(name, isFile, mediaType),
|
||||
isFile,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export const ActionsDropdown = ({ volumeId, shareId, selectedItems, role }: Prop
|
|||
isDoc: isProtonDocsDocument(selectedItems[0].mimeType),
|
||||
volumeId: selectedItems[0].volumeId,
|
||||
linkId: selectedItems[0].linkId,
|
||||
mediaType: selectedItems[0].mimeType,
|
||||
onSubmit: (formattedName) =>
|
||||
renameLink(
|
||||
new AbortController().signal,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,10 @@ export const useFolderActions = ({ allSortedItems, selectedItems, shareId, linkI
|
|||
return;
|
||||
}
|
||||
|
||||
showRenameModal(item);
|
||||
showRenameModal({
|
||||
...item,
|
||||
mediaType: item.mimeType,
|
||||
});
|
||||
};
|
||||
|
||||
const showDetails = () => {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export const SharedByMeActions = ({
|
|||
name: singleItem.name,
|
||||
volumeId: splitedSingleItemUid.volumeId,
|
||||
linkId: splitedSingleItemUid.nodeId,
|
||||
mediaType: singleItem.mediaType,
|
||||
onSubmit: (formattedName) =>
|
||||
renameLink(
|
||||
new AbortController().signal,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue