Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {QualifiedId} from '@wireapp/api-client/lib/user';
import {CellsNewNodeForm} from 'Components/Conversation/ConversationCells/common/CellsNewNodeForm/CellsNewNodeForm';
import {
CellsFileType,
getFileExtensionByType,
useCellsNewFileForm,
} from 'Components/Conversation/ConversationCells/common/useCellsNewNodeForm/useCellsNewFileForm';
import {CellsRepository} from 'Repositories/cells/cellsRepository';
Expand Down Expand Up @@ -50,24 +51,28 @@ export const CellsNewFileModal = ({
onSuccess,
currentPath,
}: CellsNewFileModalProps) => {
const {name, error, isSubmitting, handleSubmit, handleChange} = useCellsNewFileForm({
const {name, error, isSubmitting, handleSubmit, handleChange, handleClear} = useCellsNewFileForm({
fileType,
cellsRepository,
conversationQualifiedId,
onSuccess,
currentPath,
isOpen,
});
const fileExtension = getFileExtensionByType(fileType);
const headline = `${t('cells.newItemMenuModal.headlineFile', {fileType})} (.${fileExtension})`;

return (
<CellsModal isOpen={isOpen} onClose={onClose} size="large">
<CellsModal.Header>{t('cells.newItemMenuModal.headlineFile', {fileType})}</CellsModal.Header>
<CellsModal.Header>{headline}</CellsModal.Header>
<p css={descriptionStyles}>{t('cells.newItemMenuModal.descriptionFile')}</p>
<CellsNewNodeForm
label={t('cells.newItemMenuModal.labelFile')}
placeholder={t('cells.newItemMenuModal.placeholderFile')}
onSubmit={handleSubmit}
inputValue={name}
onChange={handleChange}
onClear={handleClear}
error={error}
isOpen={isOpen}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ export const CellsNewFolderModal = ({
onSuccess,
currentPath,
}: CellsNewFolderModalProps) => {
const {name, error, isSubmitting, handleSubmit, handleChange} = useCellsNewFolderForm({
const {name, error, isSubmitting, handleSubmit, handleChange, handleClear} = useCellsNewFolderForm({
cellsRepository,
conversationQualifiedId,
onSuccess,
currentPath,
isOpen,
});

return (
Expand All @@ -62,6 +63,7 @@ export const CellsNewFolderModal = ({
onSubmit={handleSubmit}
inputValue={name}
onChange={handleChange}
onClear={handleClear}
error={error}
isOpen={isOpen}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const CellsMoveNodeModal = ({
isSubmitting,
handleSubmit: handleCreateNewFolder,
handleChange,
handleClear,
} = useCellsNewFolderForm({
cellsRepository,
conversationQualifiedId,
Expand All @@ -87,6 +88,7 @@ export const CellsMoveNodeModal = ({
setActiveModalContent('move');
},
currentPath,
isOpen,
});

useEffect(() => {
Expand Down Expand Up @@ -134,6 +136,7 @@ export const CellsMoveNodeModal = ({
onSubmit={handleCreateNewFolder}
inputValue={name}
onChange={handleChange}
onClear={handleClear}
error={error}
isOpen={isOpen}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import {ChangeEvent, FormEvent} from 'react';

import {ErrorMessage, Input, Label} from '@wireapp/react-ui-kit';
import {TextInput} from 'Components/TextInput';

import {inputWrapperStyles} from './CellsNewNodeForm.styles';

Expand All @@ -31,6 +31,7 @@ interface CellsNewNodeFormProps {
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
inputValue: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
onClear: () => void;
error: string | null;
isOpen: boolean;
}
Expand All @@ -41,6 +42,7 @@ export const CellsNewNodeForm = ({
onSubmit,
inputValue,
onChange,
onClear,
error,
isOpen,
}: CellsNewNodeFormProps) => {
Expand All @@ -49,14 +51,18 @@ export const CellsNewNodeForm = ({
return (
<form onSubmit={onSubmit}>
<div css={inputWrapperStyles}>
<Label htmlFor="cells-new-item-name">{label}</Label>
<Input
id="cells-new-item-name"
<TextInput
label={label}
name="cells-new-item-name"
value={inputValue}
ref={inputRef}
placeholder={placeholder}
onChange={onChange}
error={error ? <ErrorMessage>{error}</ErrorMessage> : undefined}
onCancel={onClear}
isError={Boolean(error)}
errorMessage={error ?? undefined}
uieName="cells-new-item-name"
errorUieName="cells-new-item-name-error"
/>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,38 @@
*
*/

import {getClientSideNodeNameError, getErrorStatus, isClientSideNodeNameError, NODE_NAME_MAX_LENGTH} from './cellsNodeFormUtils';
import {t} from 'Util/localizerUtil';

jest.mock('Util/localizerUtil', () => ({
t: (key: string) => key,
}));
import {getClientSideNodeNameError, getErrorStatus, isClientSideNodeNameError, NODE_NAME_MAX_LENGTH} from './cellsNodeFormUtils';

describe('cellsNodeFormUtils', () => {
describe('getClientSideNodeNameError', () => {
it('returns required error for empty name', () => {
expect(getClientSideNodeNameError('').unwrapOr(null)).toBe('cells.newItemMenuModalForm.nameRequired');
expect(getClientSideNodeNameError('').unwrapOr(null)).toBe(t('cells.newItemMenuModalForm.nameRequired'));
});

it('returns max length error for names longer than the maximum', () => {
expect(getClientSideNodeNameError('a'.repeat(NODE_NAME_MAX_LENGTH + 1)).unwrapOr(null)).toBe(
'cells.newItemMenuModalForm.maxLengthError',
t('cells.newItemMenuModalForm.maxLengthError'),
);
});

it('returns max length error before invalid character error', () => {
const tooLongNameWithInvalidCharacter = `${'a'.repeat(NODE_NAME_MAX_LENGTH)}/`;
expect(getClientSideNodeNameError(tooLongNameWithInvalidCharacter).unwrapOr(null)).toBe(
'cells.newItemMenuModalForm.maxLengthError',
t('cells.newItemMenuModalForm.maxLengthError'),
);
});

it('returns invalid character error for names starting with "."', () => {
expect(getClientSideNodeNameError('.hidden').unwrapOr(null)).toBe('cells.newItemMenuModalForm.invalidCharactersError');
expect(getClientSideNodeNameError('.').unwrapOr(null)).toBe('cells.newItemMenuModalForm.invalidCharactersError');
expect(getClientSideNodeNameError('.hidden').unwrapOr(null)).toBe(t('cells.newItemMenuModalForm.invalidCharactersError'));
expect(getClientSideNodeNameError('.').unwrapOr(null)).toBe(t('cells.newItemMenuModalForm.invalidCharactersError'));
});

it('returns invalid character error for forbidden characters', () => {
expect(getClientSideNodeNameError('report/name').unwrapOr(null)).toBe('cells.newItemMenuModalForm.invalidCharactersError');
expect(getClientSideNodeNameError('report\\name').unwrapOr(null)).toBe('cells.newItemMenuModalForm.invalidCharactersError');
expect(getClientSideNodeNameError('report"name').unwrapOr(null)).toBe('cells.newItemMenuModalForm.invalidCharactersError');
expect(getClientSideNodeNameError('report/name').unwrapOr(null)).toBe(t('cells.newItemMenuModalForm.invalidCharactersError'));
expect(getClientSideNodeNameError('report\\name').unwrapOr(null)).toBe(t('cells.newItemMenuModalForm.invalidCharactersError'));
expect(getClientSideNodeNameError('report"name').unwrapOr(null)).toBe(t('cells.newItemMenuModalForm.invalidCharactersError'));
});

it('accepts names containing dots when dot is not at the beginning', () => {
Expand All @@ -62,14 +60,14 @@ describe('cellsNodeFormUtils', () => {

describe('isClientSideNodeNameError', () => {
it('returns true for node name validation errors', () => {
expect(isClientSideNodeNameError('cells.newItemMenuModalForm.nameRequired').unwrapOr(false)).toBe(true);
expect(isClientSideNodeNameError('cells.newItemMenuModalForm.maxLengthError').unwrapOr(false)).toBe(true);
expect(isClientSideNodeNameError('cells.newItemMenuModalForm.invalidCharactersError').unwrapOr(false)).toBe(true);
expect(isClientSideNodeNameError(t('cells.newItemMenuModalForm.nameRequired')).unwrapOr(false)).toBe(true);
expect(isClientSideNodeNameError(t('cells.newItemMenuModalForm.maxLengthError')).unwrapOr(false)).toBe(true);
expect(isClientSideNodeNameError(t('cells.newItemMenuModalForm.invalidCharactersError')).unwrapOr(false)).toBe(true);
});

it('returns false for non-validation errors', () => {
expect(isClientSideNodeNameError(null).isNothing).toBe(true);
expect(isClientSideNodeNameError('cells.newItemMenuModalForm.genericError').unwrapOr(false)).toBe(false);
expect(isClientSideNodeNameError(t('cells.newItemMenuModalForm.genericError')).unwrapOr(false)).toBe(false);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,97 @@ import {ChangeEvent, FormEvent} from 'react';
import {act, renderHook} from '@testing-library/react';

import {CellsRepository} from 'Repositories/cells/cellsRepository';
import {t} from 'Util/localizerUtil';

import {useCellsNewFileForm} from './useCellsNewFileForm';

jest.mock('Util/localizerUtil', () => ({
t: (key: string) => key,
}));
import type {CellsFileType} from './useCellsNewFileForm';

describe('useCellsNewFileForm', () => {
let mockCellsRepository: jest.Mocked<CellsRepository>;
let onSuccess: jest.Mock;

const createEvent = () => ({preventDefault: jest.fn()}) as unknown as FormEvent<HTMLFormElement>;

beforeEach(() => {
jest.clearAllMocks();
mockCellsRepository = {
checkFileAlreadyExists: jest.fn().mockResolvedValue(false),
createFile: jest.fn().mockResolvedValue(undefined),
} as unknown as jest.Mocked<CellsRepository>;
onSuccess = jest.fn();
});

const setup = () => {
const {result} = renderHook(() =>
const renderUseCellsNewFileForm = (fileType: CellsFileType = 'document') =>
renderHook(() =>
useCellsNewFileForm({
fileType: 'document',
fileType,
cellsRepository: mockCellsRepository,
conversationQualifiedId: {id: 'conversation-id', domain: 'wire.com'},
onSuccess,
currentPath: '/wire-cells-web/path',
isOpen: true,
}),
);

return {
result,
createNodeMock: mockCellsRepository.createFile,
onSuccess,
};
};
it('adds extension and template UUID for document files', async () => {
const {result} = renderUseCellsNewFileForm();

it('appends selected file type extension when a mismatched extension is provided', async () => {
const {result} = setup();
const createEvent = () => ({preventDefault: jest.fn()}) as unknown as FormEvent<HTMLFormElement>;
act(() => {
result.current.handleChange({currentTarget: {value: 'New file'}} as ChangeEvent<HTMLInputElement>);
});

await act(async () => {
await result.current.handleSubmit(createEvent());
});

expect(mockCellsRepository.checkFileAlreadyExists).toHaveBeenCalledWith(
expect.objectContaining({
name: 'New file.docx',
}),
);
expect(mockCellsRepository.createFile).toHaveBeenCalledWith(
expect.objectContaining({
name: 'New file.docx',
templateUuid: '01-Microsoft Word.docx',
}),
);
expect(onSuccess).toHaveBeenCalledTimes(1);
});

it('uses matching extension and template UUID for spreadsheet files', async () => {
const {result} = renderUseCellsNewFileForm('spreadsheet');

act(() => {
result.current.handleChange({currentTarget: {value: 'doc124.ppt'}} as ChangeEvent<HTMLInputElement>);
result.current.handleChange({currentTarget: {value: 'Budget'}} as ChangeEvent<HTMLInputElement>);
});

await act(async () => {
await result.current.handleSubmit(createEvent());
});

expect(mockCellsRepository.createFile).toHaveBeenCalledWith(expect.objectContaining({name: 'doc124.ppt.docx'}));
expect(mockCellsRepository.createFile).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Budget.xlsx',
templateUuid: '02-Microsoft Excel.xlsx',
}),
);
expect(onSuccess).toHaveBeenCalledTimes(1);
});

it('shows already-exists error when precheck reports a duplicate', async () => {
mockCellsRepository.checkFileAlreadyExists.mockResolvedValueOnce(true);
const {result} = renderUseCellsNewFileForm();

act(() => {
result.current.handleChange({currentTarget: {value: 'New file'}} as ChangeEvent<HTMLInputElement>);
});

await act(async () => {
await result.current.handleSubmit(createEvent());
});

expect(result.current.error).toBe(t('cells.newItemMenuModalForm.alreadyExistsError'));
expect(mockCellsRepository.createFile).not.toHaveBeenCalled();
expect(onSuccess).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {QualifiedId} from '@wireapp/api-client/lib/user';

import {CellsRepository} from 'Repositories/cells/cellsRepository';

import {ITEM_ALREADY_EXISTS_ERROR} from './cellsNodeFormUtils';
import {useCellsNewNodeFormBase} from './useCellsNewNodeFormBase';

import {getCellsApiPath} from '../getCellsApiPath/getCellsApiPath';
Expand All @@ -33,12 +34,34 @@ const FILE_EXTENSION_BY_TYPE: Record<CellsFileType, string> = {
presentation: 'pptx',
};

const FILE_TEMPLATE_ID_BY_TYPE: Record<CellsFileType, string> = {
document: '01-Microsoft Word.docx',
spreadsheet: '02-Microsoft Excel.xlsx',
presentation: '03-Microsoft PowerPoint.pptx',
};

// TO-DO Replace hard coded values with server values when GET /templates endpoint ready
const getTemplateUuidByType = (fileType: CellsFileType): string => {
return FILE_TEMPLATE_ID_BY_TYPE[fileType];
};

const createAlreadyExistsError = () => {
const error = new Error('File already exists') as Error & {response: {status: number}};
error.response = {status: ITEM_ALREADY_EXISTS_ERROR};
return error;
};

export const getFileExtensionByType = (fileType: CellsFileType): string => {
return FILE_EXTENSION_BY_TYPE[fileType];
};

interface UseCellsNewFileFormProps {
fileType: CellsFileType;
cellsRepository: CellsRepository;
conversationQualifiedId: QualifiedId;
onSuccess: () => void;
currentPath: string;
isOpen: boolean;
}

export const useCellsNewFileForm = ({
Expand All @@ -47,17 +70,24 @@ export const useCellsNewFileForm = ({
conversationQualifiedId,
onSuccess,
currentPath,
isOpen,
}: UseCellsNewFileFormProps) => {
const normalizeNameForCreation = (rawName: string): string => {
const extension = FILE_EXTENSION_BY_TYPE[fileType];
const extension = getFileExtensionByType(fileType);
return `${rawName}.${extension}`;
};

const createFile = async (name: string) => {
const path = getCellsApiPath({conversationQualifiedId, currentPath});
await cellsRepository.createFile({path, name});
const templateUuid = getTemplateUuidByType(fileType);
const fileAlreadyExists = await cellsRepository.checkFileAlreadyExists({path, name});
if (fileAlreadyExists) {
throw createAlreadyExistsError();
}

await cellsRepository.createFile({path, name, templateUuid});
onSuccess();
};

return useCellsNewNodeFormBase({createNode: createFile, normalizeNameForCreation});
return useCellsNewNodeFormBase({createNode: createFile, normalizeNameForCreation, isOpen});
};
Loading
Loading