Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/src/pages/inside/testCaseLibraryPage/commonMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,9 @@ export const commonMessages: Record<string, MessageDescriptor> = defineMessages(
id: 'TestCaseLibraryPage.createNew',
defaultMessage: 'Create new',
},
incorrectCsvFormat: {
id: 'TestCaseLibraryPage.incorrectCsvFormat',
defaultMessage:
'Invalid CSV format: Ensure your file includes the required columns: "name", "description", "priority", "externalId".',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,43 @@
width: 16px;
height: 16px;
}

&__location-block {
background-color: var(--rp-ui-base-bg-000);
border-radius: 4px;
padding: 16px;
}

&__location-title {
font-weight: 400;
font-size: 13px;
line-height: 20px;
margin-bottom: 12px;
}

&__segmented {
display: flex;
gap: 4px;
background: var(--rp-ui-base-bg-200);
border-radius: 6px;
padding: 2px;
}

&__segmented-btn {
flex: 1 1 0;
min-width: 0;
border: 0;
background: transparent;
border-radius: 4px;
padding: 8px 12px;
font-weight: 400;
font-size: 13px;
line-height: 20px;
text-indent: 1px;
cursor: pointer;

&.is-active {
background: var(--rp-ui-base-bg-000);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { reduxForm, InjectedFormProps, FormErrors, SubmitHandler } from 'redux-form';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from 'react-intl';
import Parser from 'html-react-parser';
import Link from 'redux-first-router-link';
import { isEmpty } from 'es-toolkit/compat';
import { isString } from 'es-toolkit';
import { Modal, FileDropArea, AddCsvIcon } from '@reportportal/ui-kit';
import { format } from 'date-fns';
import { Modal, FileDropArea, AddCsvIcon, Dropdown } from '@reportportal/ui-kit';
import { MIME_TYPES, MimeType } from '@reportportal/ui-kit/fileDropArea';
import { AttachmentFile } from '@reportportal/ui-kit/fileDropArea/attachedFilesList';

Expand All @@ -19,16 +20,21 @@
import { uniqueId } from 'common/utils';
import { commonMessages } from 'pages/inside/testCaseLibraryPage/commonMessages';
import { FolderNameField } from 'pages/inside/testCaseLibraryPage/testCaseFolders/modals/folderFormFields';
import { Folder, foldersSelector } from 'controllers/testCase';

import { messages } from './messages';
import { useImportTestCase } from './useImportTestCase';

import { messages } from './messages';

import styles from './importTestCaseModal.scss';

export const IMPORT_TEST_CASE_MODAL_KEY = 'importTestCaseModalKey';
export const IMPORT_TEST_CASE_FORM_NAME = 'import-test-case-modal-form';
const DEFAULT_FOLDER_NAME = `Import ${format(new Date(), 'dd.MM.yyyy')}`;
export type ImportTarget = 'root' | 'existing';
export type ImportTestCaseFormValues = {
folderName: string;
importTarget?: ImportTarget;
};

const cx = createClassnames(styles);
Expand All @@ -51,56 +57,88 @@

export const ImportTestCaseModal = ({
handleSubmit,
change,
data,
invalid,
error,
}: InjectedFormProps<ImportTestCaseFormValues, ImportModalData> & ImportModalData) => {
const { formatMessage } = useIntl();
const folderIdFromUrl = useMemo(() => extractFolderIdFromHash(window.location.hash), []);

Check warning on line 66 in app/src/pages/inside/testCaseLibraryPage/importTestCaseModal/importTestCaseModal.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=reportportal_service-ui&issues=AZrLL7AU_lTYpb_ux-2v&open=AZrLL7AU_lTYpb_ux-2v&pullRequest=4747
const [file, setFile] = useState<File | null>(null);
const [folderIdFromUrl] = useState<number | undefined>(() =>
extractFolderIdFromHash(window.location.hash),
);
const [target, setTarget] = useState<ImportTarget>(folderIdFromUrl != null ? 'existing' : 'root');

Check warning on line 68 in app/src/pages/inside/testCaseLibraryPage/importTestCaseModal/importTestCaseModal.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=reportportal_service-ui&issues=AZrLL7AU_lTYpb_ux-2w&open=AZrLL7AU_lTYpb_ux-2w&pullRequest=4747
const [existingFolderId, setExistingFolderId] = useState<number | null>(null);
const [attachedFiles, setAttachedFiles] = useState<AttachmentFile[]>([]);
const folders = useSelector(foldersSelector);
const dispatch = useDispatch();
const selectedFolderName = data?.folderName ?? '';
const hasFolderIdFromUrl = folderIdFromUrl != null;

const { isImportingTestCases, importTestCases } = useImportTestCase();

const handleCancel = () => {
dispatch(hideModalAction());
};

const setTargetAndForm = (next: ImportTarget) => {
setTarget(next);
change('importTarget', next);
};

const existingOptions = useMemo(
() =>
(Array.isArray(folders) ? folders : []).map((folder: Folder) => ({
value: folder.id,
label: folder.name,
})),
[folders],
);

const acceptFileMimeTypes = useMemo<MimeType[]>(() => [...CSV_MIME_TYPES], []);

useEffect(() => {
change('importTarget', target);

if (folderIdFromUrl && existingOptions.some(({ value }) => Number(value) === folderIdFromUrl)) {
setExistingFolderId(folderIdFromUrl);
}
}, [folderIdFromUrl, existingOptions, change]);

const isAllowedMime = (file: File) => CSV_MIME_TYPES.includes(file.type as MimeType);

const isWithinSize = (file: File) => file.size <= MAX_FILE_SIZE_BYTES;

const isRootTarget = target === 'root';

const isExistingTarget = target === 'existing';

const hasExistingOptions = !isEmpty(existingOptions);

const hasFolderIdFromUrl = folderIdFromUrl != null;

const handleImport = async (formValues: ImportTestCaseFormValues) => {
const name = formValues.folderName?.trim() ?? '';
const hasFolderIdFromUrl = folderIdFromUrl != null;
const importTarget = formValues.importTarget ?? 'root';

if (!file || (!hasFolderIdFromUrl && !name)) {
if (!file || (importTarget === 'root' && !name)) {
return;
}

if (hasFolderIdFromUrl) {
await importTestCases({ file, testFolderId: folderIdFromUrl });
if (importTarget === 'existing') {
const resolvedId = existingFolderId ?? folderIdFromUrl;

if (!file || resolvedId == null) {
return;
}

await importTestCases({ file, testFolderId: resolvedId });
} else {
if (!file || !name) {
return;
}

await importTestCases({ file, testFolderName: name });
}
};

const attachedFiles: AttachmentFile[] = file
? [
{
id: uniqueId(),
fileName: file.name,
file,
size: toMB(file.size),
},
]
: [];

const handleFilesAdded = (incoming: FileInput) => {
const items = Array.isArray(incoming) ? incoming : [incoming];

Expand All @@ -116,10 +154,26 @@
return;
}

const attachment: AttachmentFile = {
id: uniqueId(),
fileName: selectedFile.name,
file: selectedFile,
size: toMB(selectedFile.size),
};

setFile(selectedFile);
setAttachedFiles([attachment]);
};

const handleRemove = () => setFile(null);
const handleRemove = (fileId: string) => {
setAttachedFiles((prev) => prev.filter((f) => f.id !== fileId));
setFile(null);
};

const filesWithError: AttachmentFile[] = attachedFiles.map((item) => ({
...item,
customErrorMessage: error ?? item.customErrorMessage,
}));

const handleDownload = () => {
if (!file) {
Expand All @@ -143,6 +197,49 @@

const iconMarkup = isString(ExternalLinkIcon) ? Parser(ExternalLinkIcon) : null;

const handleExistingFolderChange = (value: number) => {
setExistingFolderId(value);
};

const renderLocationControl = () => {
if (hasFolderIdFromUrl) {
return (
<>
<label className={cx('import-test-case-modal__label')}>
{formatMessage(messages.importFolderNameLabel)}
</label>
<div className={cx('import-test-case-modal__static-value')}>
<span>{selectedFolderName}</span>
</div>
</>
);
}

if (target === 'existing' && hasExistingOptions) {
return (
<>
<label className={cx('import-test-case-modal__label')}>
{formatMessage(messages.importDropdownLabel)}
</label>
<Dropdown
className={cx('import-test-case-modal__dropdown')}
options={existingOptions}
value={existingFolderId ?? undefined}
placeholder={formatMessage(messages.typeToSearchOrSelect)}
onChange={handleExistingFolderChange}
/>
</>
);
}

return (
<FolderNameField
label={formatMessage(messages.importFolderNameLabel)}
helpText={formatMessage(messages.importFolderNameDescription)}
/>
);
};

return (
<Modal
title={formatMessage(commonMessages.importTestCases)}
Expand Down Expand Up @@ -193,30 +290,47 @@
<div className={cx('import-test-case-modal__files')}>
<FileDropArea.AttachedFilesList
className={cx('import-test-case-modal__files')}
files={attachedFiles}
files={filesWithError}
onRemoveFile={handleRemove}
onDownloadFile={handleDownload}
/>
</div>
</div>
</FileDropArea>
</div>
{!hasFolderIdFromUrl && (
<section className={cx('import-test-case-modal__location-block')}>
<div className={cx('import-test-case-modal__location-title')}>
{formatMessage(messages.specifyLocation)}
</div>

<div className={cx('import-test-case-modal__segmented')}>
<button
type="button"
className={cx('import-test-case-modal__segmented-btn', {
'is-active': isRootTarget,
})}
aria-pressed={isRootTarget}
onClick={() => setTargetAndForm('root')}
>
{formatMessage(messages.createNewRootFolder)}
</button>

<button
type="button"
className={cx('import-test-case-modal__segmented-btn', {
'is-active': isExistingTarget,
})}
aria-pressed={isExistingTarget}
onClick={() => setTargetAndForm('existing')}
>
{formatMessage(messages.addToExistingFolder)}
</button>
</div>
</section>
)}
<div className={cx('import-test-case-modal__input-control')}>
{folderIdFromUrl ? (
<>
<label className={cx('import-test-case-modal__label')}>
{formatMessage(messages.importFolderNameLabel)}
</label>
<div className={cx('import-test-case-modal__static-value')}>
<span>{selectedFolderName}</span>
</div>
</>
) : (
<FolderNameField
label={formatMessage(messages.importFolderNameLabel)}
helpText={formatMessage(messages.importFolderNameDescription)}
/>
)}
{renderLocationControl()}
</div>
</div>
</form>
Expand All @@ -228,9 +342,17 @@
reduxForm<ImportTestCaseFormValues>({
form: IMPORT_TEST_CASE_FORM_NAME,
destroyOnUnmount: true,
initialValues: { folderName: '' },
validate: ({ folderName }): FormErrors<ImportTestCaseFormValues> => ({
folderName: commonValidators.requiredField(folderName),
}),
initialValues: { folderName: DEFAULT_FOLDER_NAME, importTarget: undefined },
validate: ({
importTarget,
folderName,
}: ImportTestCaseFormValues): FormErrors<ImportTestCaseFormValues> => {
const needName =
importTarget === 'root' || extractFolderIdFromHash(window.location.hash) == null;

Check warning on line 351 in app/src/pages/inside/testCaseLibraryPage/importTestCaseModal/importTestCaseModal.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=reportportal_service-ui&issues=AZrLL7AU_lTYpb_ux-2x&open=AZrLL7AU_lTYpb_ux-2x&pullRequest=4747

return {
folderName: needName ? commonValidators.requiredField(folderName) : undefined,
};
},
})(ImportTestCaseModal),
);
Loading
Loading