diff --git a/app/frontend/src/javascript/components/projects/projects-setting-option-form.tsx b/app/frontend/src/javascript/components/projects/projects-setting-option-form.tsx new file mode 100644 index 000000000..b931dd3b6 --- /dev/null +++ b/app/frontend/src/javascript/components/projects/projects-setting-option-form.tsx @@ -0,0 +1,69 @@ +import React, { useState, useRef } from 'react'; +import { FabButton } from '../base/fab-button'; +import { ProjectSettingOption } from '../../models/project-setting-option'; +import { Check, X } from 'phosphor-react'; +import { useTranslation } from 'react-i18next'; + +interface ProjectsSettingOptionFormProps { + hasDescription?: boolean, + handleSave: (option: ProjectSettingOption) => void, + handleExit: () => void, + option?: ProjectSettingOption +} + +/** +* Provides a inline form for a Projects Setting Option +**/ +export const ProjectsSettingOptionForm: React.FC = ({ option = { name: '', description: '' }, handleSave, handleExit, hasDescription }) => { + const { t } = useTranslation('admin'); + + const enteredOptionName = useRef(null); + const enteredOptionDescription = useRef(null); + + const [errorMessage, setErrorMessage] = useState(''); + + /** + * Builds up new or updated option based on User input and provides it to parent component. + * The option property :name should not be blank and triggers an error message for the user. + **/ + const saveOption = () => { + if (enteredOptionName.current.value === '') { + setErrorMessage(t('app.admin.projects_setting_option_form.name_cannot_be_blank')); + return; + } else if (hasDescription) { + handleSave({ + id: option.id, + name: enteredOptionName.current.value, + description: enteredOptionDescription.current.value + }); + } else { + handleSave({ id: option.id, name: enteredOptionName.current.value }); + } + setErrorMessage(''); + handleExit(); + }; + + return ( + + + + {errorMessage &&

{errorMessage}

} + + + {hasDescription && } + + + + + + + + + + + ); +}; diff --git a/app/frontend/src/javascript/components/projects/projects-setting-option.tsx b/app/frontend/src/javascript/components/projects/projects-setting-option.tsx new file mode 100644 index 000000000..fdd6f9856 --- /dev/null +++ b/app/frontend/src/javascript/components/projects/projects-setting-option.tsx @@ -0,0 +1,64 @@ +import React, { Fragment, useState } from 'react'; +import { FabButton } from '../base/fab-button'; +import { Pencil, Trash } from 'phosphor-react'; +import { useTranslation } from 'react-i18next'; +import { ProjectSettingOption } from '../../models/project-setting-option'; +import { ProjectsSettingOptionForm } from './projects-setting-option-form'; + +interface ProjectsSettingOptionProps { + hasDescription?: boolean, + handleDelete: (id: number) => void, + handleUpdate: (option: ProjectSettingOption) => void, + option: ProjectSettingOption +} + +/** + * This component is used by the component ProjectsSetting + * It provides the display of each option and the update/delete features, all inline. +*/ +export const ProjectsSettingOption: React.FC = ({ option, handleDelete, handleUpdate, hasDescription }) => { + const { t } = useTranslation('admin'); + + const [isEditing, setIsEditing] = useState(false); + + // If option is in display mode, sets it in editing mode, and vice-versa. + const toggleIsEditing = () => setIsEditing(prevState => !prevState); + + // Provides the id of the deleted option to parent component, ProjectSetting. + const deleteOptionLine = () => { handleDelete(option.id); }; + + // Provides the updated option to parent component, ProjectSetting. + const updateOptionLine = (option) => { handleUpdate(option); }; + + // UI for displaying an option, when editing mode is off. + const displayingOptionLine = ( + + {option.name} + {hasDescription && option.description} + + + + {t('app.admin.projects_setting_option.edit')} + + + + + + + ); + + return ( + + {!isEditing && displayingOptionLine} + {isEditing && + } + + ); +}; diff --git a/app/frontend/src/javascript/components/projects/projects-setting.tsx b/app/frontend/src/javascript/components/projects/projects-setting.tsx new file mode 100644 index 000000000..69696792d --- /dev/null +++ b/app/frontend/src/javascript/components/projects/projects-setting.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { FabButton } from '../base/fab-button'; +import { ProjectsSettingOption } from './projects-setting-option'; +import { useTranslation } from 'react-i18next'; +import { ProjectSettingOption } from '../../models/project-setting-option'; +import { ProjectsSettingOptionForm } from './projects-setting-option-form'; + +interface ProjectsSettingProps { + hasDescription?: boolean, + handleAdd: (option: ProjectSettingOption) => void, + handleDelete: (id: number) => void, + handleUpdate: (option: ProjectSettingOption) => void, + optionType: string, + options: ProjectSettingOption[] +} + +/** + * This component is used in Projects Settings (admin part) + * It provides add / update / delete features for any setting of a project (status, themes, licences...) + * If a Setting has a description property, this component should be called with the prop hasDescription={true} +*/ +export const ProjectsSetting: React.FC = ({ hasDescription = false, handleAdd, handleUpdate, handleDelete, options, optionType }) => { + const { t } = useTranslation('admin'); + const [isAdding, setIsAdding] = useState(false); + + // Shows form to add an option if it's not already here. Else, removes it. + const toggleIsAdding = () => setIsAdding((prevState) => !prevState); + + // Pass on the newly created option to parent component. + const addOption = (option) => { handleAdd(option); }; + + return ( +
+ + {`${t('app.admin.projects_setting.add')} a ${optionType}`} + + + + + + {hasDescription && + + } + {!hasDescription && + + } + + + + + {options.map((option) => { + return ( + + ); + })} + {isAdding && + + } + +
{t('app.admin.projects_setting.name')}{t('app.admin.projects_setting.description')}
+
+ ); +}; diff --git a/app/frontend/src/javascript/components/projects/status-filter.tsx b/app/frontend/src/javascript/components/projects/status/status-filter.tsx similarity index 87% rename from app/frontend/src/javascript/components/projects/status-filter.tsx rename to app/frontend/src/javascript/components/projects/status/status-filter.tsx index ae395914c..74bc0119c 100644 --- a/app/frontend/src/javascript/components/projects/status-filter.tsx +++ b/app/frontend/src/javascript/components/projects/status/status-filter.tsx @@ -2,11 +2,11 @@ import React, { useState, useEffect } from 'react'; import { react2angular } from 'react2angular'; import Select from 'react-select'; import { useTranslation } from 'react-i18next'; -import StatusAPI from '../../api/status'; -import { IApplication } from '../../models/application'; -import { SelectOption } from '../../models/select'; -import { Loader } from '../base/loader'; -import { Status } from '../../models/status'; +import StatusAPI from '../../../api/status'; +import { IApplication } from '../../../models/application'; +import { SelectOption } from '../../../models/select'; +import { Loader } from '../../base/loader'; +import { Status } from '../../../models/status'; declare const Application: IApplication; @@ -40,14 +40,14 @@ export const StatusFilter: React.FC = ({ currentStatusIndex, */ useEffect(() => { StatusAPI.index() - .then(data => setStatusesList(data)) - .catch(e => onError(e)); + .then(setStatusesList) + .catch(onError); }, []); // If currentStatusIndex is provided, set currentOption accordingly useEffect(() => { const selectedOption = statusesList.find((status) => status.id === currentStatusIndex); - setCurrentOption(selectedOption); + setCurrentOption(selectedOption || defaultValue); }, [currentStatusIndex, statusesList]); /** @@ -76,6 +76,7 @@ export const StatusFilter: React.FC = ({ currentStatusIndex, return (
+ {statusesList.length !== 0 &&