1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

(feat) Add Status Settings in Projects Settings

This commit is contained in:
Karen 2023-01-20 16:01:18 +01:00 committed by Sylvain
parent d80cc4769a
commit 96ddb40c59
14 changed files with 540 additions and 48 deletions

View File

@ -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<ProjectsSettingOptionFormProps> = ({ 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 (
<tr>
<td>
<input
ref={enteredOptionName}
aria-label={t('app.admin.projects_setting_option_form.name')}
autoFocus={true}
defaultValue={option.name}/>
{errorMessage && <p className="error-msg">{errorMessage}</p>}
</td>
<td>
{hasDescription && <input ref={enteredOptionDescription} defaultValue={option.description}/>}
</td>
<td className="action-buttons">
<FabButton className="save-btn" onClick={saveOption}>
<Check size={20} weight="bold" aria-label={t('app.admin.projects_setting_option_form.save')} />
</FabButton>
<FabButton className="cancel-btn" onClick={handleExit}>
<X size={20} weight="bold"/>
</FabButton>
</td>
</tr>
);
};

View File

@ -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<ProjectsSettingOptionProps> = ({ 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 = (
<tr key={option.id}>
<td>{option.name}</td>
<td>{hasDescription && option.description}</td>
<td className="action-buttons">
<FabButton className="edit-btn" onClick={toggleIsEditing}>
<Pencil size={20} aria-label={`${t('app.admin.projects_setting_option.edit')} ${option.name}`}/>
<span>{t('app.admin.projects_setting_option.edit')}</span>
</FabButton>
<FabButton
className="delete-btn"
onClick={deleteOptionLine}
tooltip={`${t('app.admin.projects_setting_option.delete_option')} ${option.name}`}>
<Trash size={20} aria-label={`${t('app.admin.projects_setting_option.delete_option')} ${option.name}`}/>
</FabButton>
</td>
</tr>
);
return (
<Fragment>
{!isEditing && displayingOptionLine}
{isEditing &&
<ProjectsSettingOptionForm
option={option}
handleSave={updateOptionLine}
handleExit={toggleIsEditing}
hasDescription={hasDescription}/>}
</Fragment>
);
};

View File

@ -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<ProjectsSettingProps> = ({ 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 (
<div className="projects-setting">
<FabButton
className="add-btn"
onClick={toggleIsAdding}
type="button">
{`${t('app.admin.projects_setting.add')} a ${optionType}`}
</FabButton>
<table>
<thead>
<tr>
<th style={{ width: hasDescription ? '30%' : '80%' }}>{t('app.admin.projects_setting.name')}</th>
{hasDescription &&
<th style={{ width: '50%' }} aria-hidden='false'>{t('app.admin.projects_setting.description')}</th>
}
{!hasDescription &&
<th style={{ width: '0%' }} aria-hidden='true'></th>
}
<th style={{ width: '20%' }} aria-label={t('app.admin.projects_setting.actions_controls')}></th>
</tr>
</thead>
<tbody>
{options.map((option) => {
return (
<ProjectsSettingOption
key={option.id}
option={option}
hasDescription={hasDescription}
handleDelete={handleDelete}
handleUpdate={handleUpdate}/>
);
})}
{isAdding &&
<ProjectsSettingOptionForm
hasDescription={hasDescription}
handleSave={addOption}
handleExit={toggleIsAdding}/>
}
</tbody>
</table>
</div>
);
};

View File

@ -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<StatusFilterProps> = ({ 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<StatusFilterProps> = ({ currentStatusIndex,
return (
<div>
{statusesList.length !== 0 &&
<Select defaultValue={currentOption}
value={currentOption}
id="status"
@ -84,6 +85,7 @@ export const StatusFilter: React.FC<StatusFilterProps> = ({ currentStatusIndex,
options={buildOptions()}
styles={selectStyles}
aria-label={t('app.public.status_filter.select_status')}/>
}
</div>
);
};

View File

@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import { react2angular } from 'react2angular';
import StatusAPI from '../../../api/status';
import { IApplication } from '../../../models/application';
import { Loader } from '../../base/loader';
import { ProjectsSetting } from './../projects-setting';
import { ProjectSettingOption } from '../../../models/project-setting-option';
import { useTranslation } from 'react-i18next';
declare const Application: IApplication;
interface StatusSettingsProps {
onError: (message: string) => void,
onSuccess: (message: string) => void
}
/**
* Allows Admin to see, create, update and destroy the availables status on Projects.
*/
export const StatusSettings: React.FC<StatusSettingsProps> = ({ onError, onSuccess }) => {
const { t } = useTranslation('admin');
const [statusesOptions, setStatusesOptions] = useState([]);
// Async function : post new status to API, then refresh status list
const addOption = async (option: ProjectSettingOption) => {
await StatusAPI.create({ label: option.name }).catch(onError);
onSuccess(t('app.admin.status_settings.option_create_success'));
fetchStatus();
};
// Async function : send delete action to API, then refresh status list
const deleteOption = async (id: number) => {
await StatusAPI.destroy(id).catch(onError);
onSuccess(t('app.admin.status_settings.option_delete_success'));
fetchStatus();
};
// Async function : send updated status to API, then refresh status list
const updateOption = async (option: ProjectSettingOption) => {
await StatusAPI.update({ id: option.id, label: option.name }).catch(onError);
onSuccess(t('app.admin.status_settings.option_update_success'));
fetchStatus();
};
// fetch list of Status from API and sort it by id
const fetchStatus = () => {
StatusAPI.index()
.then(data => {
const options = data.map(status => {
return { id: status.id, name: status.label };
});
setStatusesOptions(options.sort((a, b) => a.id - b.id));
})
.catch(onError);
};
// fetch list of Status on component mount
useEffect(() => {
fetchStatus();
}, []);
return (
<div data-testid="status-settings">
<ProjectsSetting
options={statusesOptions}
optionType= 'status'
handleAdd={addOption}
handleDelete={deleteOption}
handleUpdate={updateOption} />
</div>
);
};
const StatusSettingsWrapper: React.FC<StatusSettingsProps> = (props) => {
return (
<Loader>
<StatusSettings {...props} />
</Loader>
);
};
Application.Components.component('statusSettings', react2angular(StatusSettingsWrapper, ['onError', 'onSuccess']));

View File

@ -0,0 +1,6 @@
// Type model used in ProjectSettings and its child components
export interface ProjectSettingOption {
name: string,
id?: number,
description?: string
}

View File

@ -103,6 +103,7 @@
@import "modules/profile-completion/completion-header-info";
@import "modules/profile-completion/profile-form-option";
@import "modules/profile-custom-fields/profile-custom-fields-list";
@import "modules/projects/projects-setting.scss";
@import "modules/select-gateway-modal";
@import "modules/settings/boolean-setting";
@import "modules/settings/check-list-setting";

View File

@ -0,0 +1,73 @@
.projects-setting {
width: 100%;
.error-msg {
color: var(--alert);
}
table {
width: inherit;
}
th {
padding: 8px;
vertical-align: bottom;
border-bottom: 2px solid #ddd;
}
tr td {
padding: 8px;
vertical-align: top;
border-top: 1px solid #ddd;
}
.action-buttons {
.delete-btn {
background-color: #cb1117;
color: white;
padding: 6px 8px;
&:hover {
background-color: var(--alert-dark);
}
}
.edit-btn {
gap: 6px;
margin-right: 4px;
}
.save-btn {
margin-right: 4px;
background-color: var(--secondary);
&:hover {
background-color: var(--secondary-dark);
}
}
}
.add-btn {
margin: 15px 0;
background-color: var(--secondary);
border: none;
&:hover {
background-color: var(--secondary-dark);
}
}
input {
border: 1px solid #c4c4c4;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
color: #555555;
padding: 6px 12px;
&:focus {
border-color: #fdde3f;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(253, 222, 63, 0.6);
outline: none;
}
}
@media (max-width: 1054px) {
.action-buttons {
span {
display: none;
}
}
}
}

View File

@ -39,7 +39,10 @@
<uib-tab heading="{{ 'app.admin.projects.licences' | translate }}" index="2">
<ng-include src="'/admin/projects/licences.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.projects.settings.title' | translate }}" index="3" class="settings-tab">
<uib-tab heading="{{ 'app.admin.projects.statuses' | translate }}" index="3">
<status-settings on-error="onError" on-success="onSuccess"/>
</uib-tab>
<uib-tab heading="{{ 'app.admin.projects.settings.title' | translate }}" index="4" class="settings-tab">
<ng-include src="'/admin/projects/settings.html'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -339,6 +339,7 @@ en:
themes: "Themes"
add_a_new_theme: "Add a new theme"
licences: "Licences"
statuses: "Statuses"
description: "Description"
add_a_new_licence: "Add a new licence"
manage_abuses: "Manage the reports"
@ -367,6 +368,23 @@ en:
open_lab_app_secret: "Secret"
openlab_default_info_html: "In the projects gallery, visitors can switch between two views: all shared projets from the whole OpenLab network, or only the projects documented in your Fab Lab.<br/>Here, you can choose which view is shown by default."
default_to_openlab: "Display OpenLab by default"
projects_setting:
add: "Add"
actions_controls: "Actions Controls"
name: "Name"
projects_setting_option:
edit: "Edit"
delete_option: "Delete Option"
projects_setting_option_form:
name: "Name"
description: "Description"
name_cannot_be_blank: "Name cannot be blank."
save: "Save"
cancel: "Cancel"
status_settings:
option_create_success: "Status was successfully created."
option_delete_success: "Status was successfully deleted."
option_update_success: "Status was successfully updated."
#track and monitor the trainings
trainings:
trainings_monitoring: "Trainings monitoring"

View File

@ -0,0 +1,37 @@
import groups from '../__fixtures__/groups';
import plans from '../__fixtures__/plans';
import planCategories from '../__fixtures__/plan_categories';
import { partners, managers, users } from '../__fixtures__/users';
import { settings } from '../__fixtures__/settings';
import products from '../__fixtures__/products';
import productCategories from '../__fixtures__/product_categories';
import productStockMovements from '../__fixtures__/product_stock_movements';
import machines from '../__fixtures__/machines';
import providers from '../__fixtures__/auth_providers';
import profileCustomFields from '../__fixtures__/profile_custom_fields';
import spaces from '../__fixtures__/spaces';
import statuses from '../__fixtures__/statuses';
const FixturesLib = {
init: () => {
return {
groups: JSON.parse(JSON.stringify(groups)),
plans: JSON.parse(JSON.stringify(plans)),
planCategories: JSON.parse(JSON.stringify(planCategories)),
partners: JSON.parse(JSON.stringify(partners)),
managers: JSON.parse(JSON.stringify(managers)),
users: JSON.parse(JSON.stringify(users)),
settings: JSON.parse(JSON.stringify(settings)),
products: JSON.parse(JSON.stringify(products)),
productCategories: JSON.parse(JSON.stringify(productCategories)),
productStockMovements: JSON.parse(JSON.stringify(productStockMovements)),
machines: JSON.parse(JSON.stringify(machines)),
providers: JSON.parse(JSON.stringify(providers)),
profileCustomFields: JSON.parse(JSON.stringify(profileCustomFields)),
spaces: JSON.parse(JSON.stringify(spaces)),
statuses: JSON.parse(JSON.stringify(statuses))
};
}
};
export default FixturesLib;

View File

@ -1,38 +1,29 @@
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import groups from '../__fixtures__/groups';
import plans from '../__fixtures__/plans';
import planCategories from '../__fixtures__/plan_categories';
import { partners, managers, users } from '../__fixtures__/users';
import { buildHistoryItem, settings } from '../__fixtures__/settings';
import products from '../__fixtures__/products';
import productCategories from '../__fixtures__/product_categories';
import productStockMovements from '../__fixtures__/product_stock_movements';
import machines from '../__fixtures__/machines';
import providers from '../__fixtures__/auth_providers';
import profileCustomFields from '../__fixtures__/profile_custom_fields';
import spaces from '../__fixtures__/spaces';
import statuses from '../__fixtures__/statuses';
import { buildHistoryItem } from '../__fixtures__/settings';
import FixturesLib from '../__lib__/fixtures';
let fixtures = null;
export const server = setupServer(
rest.get('/api/groups', (req, res, ctx) => {
return res(ctx.json(groups));
return res(ctx.json(fixtures.groups));
}),
rest.get('/api/plan_categories', (req, res, ctx) => {
return res(ctx.json(planCategories));
return res(ctx.json(fixtures.planCategories));
}),
rest.get('/api/users', (req, res, ctx) => {
switch (new URLSearchParams(req.url.search).get('role')) {
case 'partner':
return res(ctx.json(partners));
return res(ctx.json(fixtures.partners));
case 'manager':
return res(ctx.json(managers));
return res(ctx.json(fixtures.managers));
default:
return res(ctx.json(users));
return res(ctx.json(fixtures.users));
}
}),
rest.get('/api/plans', (req, res, ctx) => {
return res(ctx.json(plans));
return res(ctx.json(fixtures.plans));
}),
rest.post('/api/plans', (req, res, ctx) => {
return res(ctx.json(req.body));
@ -51,7 +42,7 @@ export const server = setupServer(
/* eslint-enable camelcase */
}),
rest.get('/api/settings/:name', (req, res, ctx) => {
const setting = settings.find(s => s.name === req.params.name);
const setting = fixtures.settings.find(s => s.name === req.params.name);
const history = new URLSearchParams(req.url.search).get('history');
const result = { setting };
if (history) {
@ -61,48 +52,75 @@ export const server = setupServer(
}),
rest.get('/api/settings', (req, res, ctx) => {
const names = new URLSearchParams(req.url.search).get('names');
const foundSettings = settings.filter(setting => names.replace(/[[\]']/g, '').split(',').includes(setting.name));
const foundSettings = fixtures.settings.filter(setting => names.replace(/[[\]']/g, '').split(',').includes(setting.name));
return res(ctx.json(Object.fromEntries(foundSettings.map(s => [s.name, s.value]))));
}),
rest.patch('/api/settings/bulk_update', (req, res, ctx) => {
return res(ctx.json(req.body));
}),
rest.get('/api/product_categories', (req, res, ctx) => {
return res(ctx.json(productCategories));
return res(ctx.json(fixtures.productCategories));
}),
rest.get('/api/products', (req, res, ctx) => {
return res(ctx.json(products));
return res(ctx.json(fixtures.products));
}),
rest.get('/api/products/:id/stock_movements', (req, res, ctx) => {
const { id } = req.params;
return res(ctx.json({
page: 1,
total_pages: Math.ceil(productStockMovements.length / 10),
total_pages: Math.ceil(fixtures.productStockMovements.length / 10),
page_size: 10,
total_count: productStockMovements.length,
data: productStockMovements.filter(m => String(m.product_id) === id)
total_count: fixtures.productStockMovements.length,
data: fixtures.productStockMovements.filter(m => String(m.product_id) === id)
}));
}),
rest.get('/api/machines', (req, res, ctx) => {
return res(ctx.json(machines));
return res(ctx.json(fixtures.machines));
}),
rest.get('/api/auth_providers/active', (req, res, ctx) => {
return res(ctx.json(providers[0]));
return res(ctx.json(fixtures.providers[0]));
}),
rest.get('/api/profile_custom_fields', (req, res, ctx) => {
return res(ctx.json(profileCustomFields));
return res(ctx.json(fixtures.profileCustomFields));
}),
rest.get('/api/members/current', (req, res, ctx) => {
return res(ctx.json(global.loggedUser));
}),
rest.get('/api/spaces', (req, res, ctx) => {
return res(ctx.json(spaces));
return res(ctx.json(fixtures.spaces));
}),
rest.get('/api/statuses', (req, res, ctx) => {
return res(ctx.json(statuses));
return res(ctx.json(fixtures.statuses));
}),
rest.delete('api/statuses/:id', (req, res, ctx) => {
const id = parseInt(req.params.id);
const statusIndex = fixtures.statuses.findIndex((status) => status.id === id);
fixtures.statuses.splice(statusIndex, 1);
return res(ctx.json());
}),
rest.patch('api/statuses/:id', async (req, res, ctx) => {
const id = parseInt(req.params.id);
const reqBody = await req.json();
const status = fixtures.statuses.find((status) => status.id === id);
status.label = reqBody.status.label;
return res(ctx.json(status));
}),
rest.post('/api/statuses', async (req, res, ctx) => {
const reqBody = await req.json();
const status = reqBody.status;
status.id = fixtures.statuses.length + 1;
fixtures.statuses.push(status);
return res(ctx.json({ status }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
beforeAll(() => {
server.listen();
fixtures = FixturesLib.init();
}
);
afterEach(() => {
server.resetHandlers();
fixtures = FixturesLib.init();
});
afterAll(() => server.close());

View File

@ -1,6 +1,7 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { StatusFilter } from '../../../../app/frontend/src/javascript/components/projects/status-filter';
import { StatusFilter } from '../../../../app/frontend/src/javascript/components/projects/status/status-filter';
// Status filter is a part of Project Gallery
describe('Status Filter', () => {
test('should call onChange with option when selecting and shifting option', async () => {
const onError = jest.fn();
@ -8,19 +9,20 @@ describe('Status Filter', () => {
render(<StatusFilter onError={onError} onFilterChange={onFilterChange}/>);
expect(onFilterChange).toHaveBeenCalledTimes(0);
// Wait for component to render with list of statuses
await waitFor(() => screen.getByLabelText(/app.public.status_filter.select_status/));
fireEvent.keyDown(screen.getByLabelText(/app.public.status_filter.all_statuses/), { key: 'ArrowDown' });
fireEvent.keyDown(screen.getByLabelText(/app.public.status_filter.select_status/), { key: 'ArrowDown' });
await waitFor(() => screen.getByText('Mocked Status 1'));
fireEvent.click(screen.getByText('Mocked Status 1'));
expect(onFilterChange).toHaveBeenCalledWith({ label: 'Mocked Status 1', value: 1 });
expect(onFilterChange).toHaveBeenCalledWith({ label: 'Mocked Status 1', id: 1 });
fireEvent.keyDown(screen.getByLabelText(/app.public.status_filter.all_statuses/), { key: 'ArrowDown' });
fireEvent.keyDown(screen.getByLabelText(/app.public.status_filter.select_status/), { key: 'ArrowDown' });
await waitFor(() => screen.getByText('Mocked Status 2'));
fireEvent.click(screen.getByText('Mocked Status 2'));
expect(onFilterChange).toHaveBeenCalledTimes(2);
expect(onFilterChange).toHaveBeenCalledWith({ label: 'Mocked Status 2', value: 2 });
expect(onFilterChange).toHaveBeenCalledWith({ label: 'Mocked Status 2', id: 2 });
});
});

View File

@ -0,0 +1,43 @@
import { render, fireEvent, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import { StatusSettings } from '../../../../app/frontend/src/javascript/components/projects/status/status-settings';
// Status Settings are a part of Project Settings in Admin Section
describe('Status Settings', () => {
const onError = jest.fn();
const onSuccess = jest.fn();
test('display all Statuses', async () => {
render(<StatusSettings onError={onError} onSuccess={onSuccess}/>);
await waitFor(() => screen.getByTestId('status-settings'));
expect(screen.getByText('Mocked Status 1')).toBeDefined();
expect(screen.getByText('Mocked Status 2')).toBeDefined();
});
test('can create a Status', async () => {
render(<StatusSettings onError={onError} onSuccess={onSuccess}/>);
await waitFor(() => screen.getByTestId('status-settings'));
fireEvent.click(screen.getByRole('button', { name: /app.admin.projects_setting.add/ }));
fireEvent.change(screen.getByLabelText(/app.admin.projects_setting_option_form.name/), { target: { value: 'My new Status' } });
fireEvent.click(screen.getByRole('button', { name: /app.admin.projects_setting_option_form.save/ }));
await waitFor(() => screen.getByText('My new Status'));
expect(screen.getByText('My new Status')).toBeDefined();
});
test('can update a Status', async () => {
render(<StatusSettings onError={onError} onSuccess={onSuccess}/>);
await waitFor(() => screen.getByTestId('status-settings'));
fireEvent.click(screen.getByRole('button', { name: /app.admin.projects_setting_option.edit Mocked Status 2/ }));
fireEvent.change(screen.getByLabelText(/app.admin.projects_setting_option_form.name/), { target: { value: 'My updated Status' } });
fireEvent.click(screen.getByRole('button', { name: /app.admin.projects_setting_option_form.save/ }));
await waitFor(() => expect(screen.getByText('My updated Status')));
});
test('can delete a Status', async () => {
render(<StatusSettings onError={onError} onSuccess={onSuccess}/>);
await waitFor(() => screen.getByTestId('status-settings'));
fireEvent.click(screen.getByRole('button', { name: /app.admin.projects_setting_option.delete_option Mocked Status 1/ }));
await waitForElementToBeRemoved(screen.getByText('Mocked Status 1'));
expect(screen.queryByText('Mocked Status 1')).toBeNull();
expect(screen.queryByText('Mocked Status 2')).toBeDefined();
});
});