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:
parent
d80cc4769a
commit
96ddb40c59
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -0,0 +1,6 @@
|
||||
// Type model used in ProjectSettings and its child components
|
||||
export interface ProjectSettingOption {
|
||||
name: string,
|
||||
id?: number,
|
||||
description?: string
|
||||
}
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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"
|
||||
|
37
test/frontend/__lib__/fixtures.ts
Normal file
37
test/frontend/__lib__/fixtures.ts
Normal 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;
|
@ -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());
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
43
test/frontend/components/projects/status-settings.test.tsx
Normal file
43
test/frontend/components/projects/status-settings.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user