Skip to content

Commit

Permalink
add validation function in studio-pure-function and use in codeList i…
Browse files Browse the repository at this point in the history
…n library
  • Loading branch information
standeren committed Nov 22, 2024
1 parent 95c8ca9 commit d766e81
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { convertOptionListsToCodeLists } from './utils/convertOptionListsToCodeL
import { StudioPageSpinner } from '@studio/components';
import { useTranslation } from 'react-i18next';
import { useAddOptionListMutation, useUpdateOptionListMutation } from 'app-shared/hooks/mutations';
import type { ApiError } from 'app-shared/types/api/ApiError';
import { toast } from 'react-toastify';
import type { AxiosError } from 'axios';

export function AppContentLibrary(): React.ReactElement {
const { org, app } = useStudioEnvironmentParams();
Expand All @@ -25,7 +28,16 @@ export function AppContentLibrary(): React.ReactElement {
const codeLists = convertOptionListsToCodeLists(optionLists);

const handleUpload = (file: File) => {
uploadOptionList(file);
uploadOptionList(file, {
onSuccess: () => {
toast.success(t('ux_editor.modal_properties_code_list_upload_success'));
},
onError: (error: AxiosError<ApiError>) => {
if (!error.response?.data?.errorCode) {
toast.error(t('ux_editor.modal_properties_code_list_upload_generic_error'));
}
},
});
};

const handleUpdate = ({ title, codeList }: CodeListWithMetadata) => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,7 @@
"ux_editor.upload_file_error_too_large": "Kunne ikke laste opp filen. Den er for stor.",
"ux_editor.url_label": "Lenke",
"ux_editor.warning": "Advarsel",
"validation_errors.file_name_invalid": "Filnavnet er ugyldig. Du kan bruke tall, understrek, punktum, bindestrek, og store/små bokstaver fra det norske alfabetet. Filnavnet må starte med en engelsk bokstav.",
"validation_errors.length": "Antall tillatte tegn er {{0}}",
"validation_errors.max": "Største gyldig verdi er {{0}}",
"validation_errors.maxLength": "Bruk {{0}} eller færre tegn",
Expand All @@ -1801,5 +1802,6 @@
"validation_errors.numbers_only": "Kun sifre er gyldige tegn",
"validation_errors.pattern": "Feil format eller verdi",
"validation_errors.required": "Feltet må fylles ut",
"validation_errors.upload_file_name_occupied": "Opplastning feilet. Du prøvde å laste opp en fil som finnes fra før.",
"validation_errors.value_as_url": "Ugyldig lenke"
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ export function CodeList({
if (fetchDataError)
return <StudioPageError message={t('app_content_library.code_lists.fetch_error')} />;

const codeListTitles = codeLists.map((codeList) => codeList.title);

return (
<div className={classes.codeListsContainer}>
<StudioHeading size='small'>{t('app_content_library.code_lists.page_name')}</StudioHeading>
<CodeListsCounterMessage codeListsCount={codeLists.length} />
<CodeListsActionsBar
onUploadCodeList={onUploadCodeList}
onUpdateCodeList={onUpdateCodeList}
codeListNames={codeListTitles}
/>
<CodeLists codeLists={codeLists} onUpdateCodeList={onUpdateCodeList} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ describe('CodeListsActionsBar', () => {

const renderCodeListsActionsBar = () => {
render(
<CodeListsActionsBar onUploadCodeList={onUploadCodeListMock} onUpdateCodeList={jest.fn()} />,
<CodeListsActionsBar
onUploadCodeList={onUploadCodeListMock}
onUpdateCodeList={jest.fn()}
codeListNames={['codeList', 'codeList2']}
/>,
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,47 @@ import classes from './CodeListsActionsBar.module.css';
import { useTranslation } from 'react-i18next';
import type { CodeListWithMetadata } from '../CodeList';
import { CreateNewCodeListModal } from './CreateNewCodeListModal/CreateNewCodeListModal';
import { FileNameValidationResult, FileNameUtils } from '@studio/pure-functions';
import { useValidateFileName } from '../hooks/useValidateFileName';

type CodeListsActionsBarProps = {
onUploadCodeList: (updatedCodeList: File) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
codeListNames: string[];
};

export function CodeListsActionsBar({
onUploadCodeList,
onUpdateCodeList,
codeListNames,
}: CodeListsActionsBarProps) {
const { t } = useTranslation();
const { handleInvalidUploadedFileName } = useValidateFileName();

const onSubmit = (file: File) => {
const fileNameError = FileNameUtils.validateFileName(
FileNameUtils.removeExtension(file.name),
codeListNames,
);
if (fileNameError !== FileNameValidationResult.Valid)
handleInvalidUploadedFileName(fileNameError);
else onUploadCodeList(file);
};

return (
<div className={classes.actionsBar}>
<Search
className={classes.searchField}
size='sm'
placeholder={t('app_content_library.code_lists.search_placeholder')}
/>
<CreateNewCodeListModal onUpdateCodeList={onUpdateCodeList} />
<CreateNewCodeListModal onUpdateCodeList={onUpdateCodeList} codeListNames={codeListNames} />
<StudioFileUploader
accept='.json'
size='small'
variant='tertiary'
uploaderButtonText={t('app_content_library.code_lists.upload_code_list')}
onSubmit={onUploadCodeList}
onSubmit={onSubmit}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import { useOptionListEditorTexts } from '../../hooks/useCodeListEditorTexts';
import { CheckmarkIcon } from '@studio/icons';
import classes from './CreateNewCodeListModal.module.css';
import type { CodeListWithMetadata } from '../../CodeList';
import { FileNameUtils, FileNameValidationResult } from '@studio/pure-functions';
import { useValidateFileName } from '../../hooks/useValidateFileName';

type CreateNewCodeListModalProps = {
onUpdateCodeList: (codeListWithMetadata: CodeListWithMetadata) => void;
codeListNames: string[];
};

export function CreateNewCodeListModal({ onUpdateCodeList }: CreateNewCodeListModalProps) {
export function CreateNewCodeListModal({
onUpdateCodeList,
codeListNames,
}: CreateNewCodeListModalProps) {
const { t } = useTranslation();
const modalRef = createRef<HTMLDialogElement>();

Expand All @@ -39,6 +45,7 @@ export function CreateNewCodeListModal({ onUpdateCodeList }: CreateNewCodeListMo
>
<CreateNewCodeList
codeList={newCodeList}
codeListNames={codeListNames}
onUpdateCodeList={onUpdateCodeList}
onCloseModal={handleCloseModal}
/>
Expand All @@ -49,14 +56,22 @@ export function CreateNewCodeListModal({ onUpdateCodeList }: CreateNewCodeListMo

type CreateNewCodeListProps = {
codeList: CodeList;
codeListNames: string[];
onUpdateCodeList: (codeListWithMetadata: CodeListWithMetadata) => void;
onCloseModal: () => void;
};

function CreateNewCodeList({ codeList, onUpdateCodeList, onCloseModal }: CreateNewCodeListProps) {
function CreateNewCodeList({
codeList,
codeListNames,
onUpdateCodeList,
onCloseModal,
}: CreateNewCodeListProps) {
const { t } = useTranslation();
const editorTexts: CodeListEditorTexts = useOptionListEditorTexts();
const { getInvalidInputFileNameErrorMessage } = useValidateFileName();
const [isCodeListValid, setIsCodeListValid] = useState<boolean>(true);
const [codeListTitleError, setCodeListTitleError] = useState<string>('');
const [currentCodeListWithMetadata, setCurrentCodeListWithMetadata] =
useState<CodeListWithMetadata>({
title: '',
Expand All @@ -69,10 +84,14 @@ function CreateNewCodeList({ codeList, onUpdateCodeList, onCloseModal }: CreateN
};

const handleCodeListTitleChange = (codeListTitle: string) => {
setCurrentCodeListWithMetadata({
title: codeListTitle,
codeList: currentCodeListWithMetadata.codeList,
});
const fileNameError = FileNameUtils.validateFileName(codeListTitle, codeListNames);
const errorMessage = getInvalidInputFileNameErrorMessage(fileNameError);
setCodeListTitleError(errorMessage);
if (fileNameError === FileNameValidationResult.Valid)
setCurrentCodeListWithMetadata({
title: codeListTitle,
codeList: currentCodeListWithMetadata.codeList,
});
};

const handleCodeListChange = (updatedCodeList: CodeList) => {
Expand All @@ -87,7 +106,8 @@ function CreateNewCodeList({ codeList, onUpdateCodeList, onCloseModal }: CreateN
setIsCodeListValid(false);
};

const isSaveButtonDisabled = !isCodeListValid || !currentCodeListWithMetadata.title;
const isSaveButtonDisabled =
!isCodeListValid || !currentCodeListWithMetadata.title || codeListTitleError;

return (
<div className={classes.createNewCodeList}>
Expand All @@ -96,6 +116,7 @@ function CreateNewCodeList({ codeList, onUpdateCodeList, onCloseModal }: CreateN
className={classes.codeListTitle}
size='small'
onChange={(event) => handleCodeListTitleChange(event.target.value)}
error={codeListTitleError}
/>
<div className={classes.codeListEditor}>
<StudioCodeListEditor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FileNameValidationResult } from '@studio/pure-functions';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';

export function useValidateFileName() {
const { t } = useTranslation();

const handleInvalidUploadedFileName = (fileNameError: FileNameValidationResult) => {
switch (fileNameError) {
case FileNameValidationResult.NoRegExMatch:
return toast.error(t('validation_errors.file_name_invalid'));
case FileNameValidationResult.FileExists:
return toast.error(t('validation_errors.upload_file_name_occupied'));
default:
return null;
}
};

const getInvalidInputFileNameErrorMessage = (fileNameError: FileNameValidationResult) => {
switch (fileNameError) {
case FileNameValidationResult.FileNameIsEmpty:
return t('validation_errors.required');
case FileNameValidationResult.NoRegExMatch:
return t('validation_errors.file_name_invalid');
case FileNameValidationResult.FileExists:
return t('validation_errors.file_name_occupied');
default:
return '';
}
};

return { handleInvalidUploadedFileName, getInvalidInputFileNameErrorMessage };
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileNameUtils } from './FilenameUtils';
import { FileNameUtils, FileNameValidationResult } from './FileNameUtils';

describe('FileNameUtils', () => {
describe('removeExtension', () => {
Expand Down Expand Up @@ -57,13 +57,13 @@ describe('FileNameUtils', () => {

describe('extractFilename', () => {
it('Returns filename if path contains a slash', () => {
expect(FileNameUtils.extractFilename('/path/to/filename')).toEqual('filename');
expect(FileNameUtils.extractFilename('/path/to/filename.json')).toEqual('filename.json');
expect(FileNameUtils.extractFileName('/path/to/filename')).toEqual('filename');
expect(FileNameUtils.extractFileName('/path/to/filename.json')).toEqual('filename.json');
});

it('Returns path if path does not contain a slash', () => {
expect(FileNameUtils.extractFilename('filename')).toEqual('filename');
expect(FileNameUtils.extractFilename('filename.json')).toEqual('filename.json');
expect(FileNameUtils.extractFileName('filename')).toEqual('filename');
expect(FileNameUtils.extractFileName('filename.json')).toEqual('filename.json');
});
});

Expand All @@ -90,4 +90,79 @@ describe('FileNameUtils', () => {
expect(FileNameUtils.removeFileNameFromPath('filename.json', true)).toEqual('');
});
});

describe('validateFileName', () => {
it('Returns "FileNameIsEmpty" when file name is empty', () => {
const fileName: string = '';
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
[],
);
expect(fileNameValidation).toBe(FileNameValidationResult.FileNameIsEmpty);
});

it('Returns "NoRegExMatch" when file name does not match given regex', () => {
const fileName: string = 'ABC';
const fileNameRegEx: RegExp = /^[a-z]+$/;
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
[],
fileNameRegEx,
);
expect(fileNameValidation).toBe(FileNameValidationResult.NoRegExMatch);
});

it('Returns "FileExists" when file name matches regEx and exists in list', () => {
const fileName: string = 'fileName1';
const invalidFileNames: string[] = ['fileName1', 'fileName2', 'fileName3'];
const fileNameRegEx: RegExp = /^[a-zA-Z0-9]+$/;
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
invalidFileNames,
fileNameRegEx,
);
expect(fileNameValidation).toBe(FileNameValidationResult.FileExists);
});

it('Returns "FileExists" when no regEx is provided and exists in list', () => {
const fileName: string = 'fileName1';
const invalidFileNames: string[] = ['fileName1', 'fileName2', 'fileName3'];
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
invalidFileNames,
);
expect(fileNameValidation).toBe(FileNameValidationResult.FileExists);
});

it('Returns "Valid" when file name matches regEx and does not exist in list of invalid names', () => {
const fileName: string = 'fileName';
const invalidFileNames: string[] = ['fileName2', 'fileName3'];
const fileNameRegEx: RegExp = /^[a-zA-Z]+$/;
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
invalidFileNames,
fileNameRegEx,
);
expect(fileNameValidation).toBe(FileNameValidationResult.Valid);
});

it('Returns "Valid" when no regEx is provided and file name does not exist in list of invalid names', () => {
const fileName: string = 'fileName';
const invalidFileNames: string[] = ['fileName2', 'fileName3'];
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
invalidFileNames,
);
expect(fileNameValidation).toBe(FileNameValidationResult.Valid);
});

it('Returns "Valid" when no regEx is provided and list of invalid names is empty', () => {
const fileName: string = 'fileName';
const fileNameValidation: FileNameValidationResult = FileNameUtils.validateFileName(
fileName,
[],
);
expect(fileNameValidation).toBe(FileNameValidationResult.Valid);
});
});
});
Loading

0 comments on commit d766e81

Please sign in to comment.