1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-17 11:54:22 +01:00

(bug) prevent selecting same price category twice

This commit is contained in:
Sylvain 2023-03-02 11:35:35 +01:00
parent df725c6dbf
commit 1cfcf0a8b5
12 changed files with 157 additions and 18 deletions

View File

@ -1,7 +1,8 @@
# Changelog Fab-manager
- Fix a bug: members can't change/cancel their reservations
- Fix a bug: admin events view defaults to the list tab
- Fix a bug: admin events view should default to the list tab
- Fix a bug: event creation form should not allow setting multiple times the same price category
## v5.7.2 2023 February 24

View File

@ -71,6 +71,19 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError);
}, []);
useEffect(() => {
// When a new custom price is added to the current event, we mark it as disabled to prevent setting the same category twice
const selectedCategoriesId = output.event_price_categories_attributes
?.filter(epc => !epc._destroy && epc.price_category_id)
?.map(epc => epc.price_category_id) || [];
setPriceCategoriesOptions(priceCategoriesOptions?.map(pco => {
return {
...pco,
disabled: selectedCategoriesId.includes(pco.value)
};
}));
}, [output.event_price_categories_attributes]);
/**
* Callback triggered when the user clicks on the 'remove' button, in the additional prices area
*/
@ -278,12 +291,14 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
type="number"
tooltip={t('app.admin.event_form.seats_help')} />
<FormInput register={register}
id="amount"
formState={formState}
rules={{ required: true }}
label={t('app.admin.event_form.standard_rate')}
tooltip={t('app.admin.event_form.0_equal_free')}
addOn={FormatLib.currencySymbol()} />
type="number"
id="amount"
formState={formState}
rules={{ required: true, min: 0 }}
nullable
label={t('app.admin.event_form.standard_rate')}
tooltip={t('app.admin.event_form.0_equal_free')}
addOn={FormatLib.currencySymbol()} />
{priceCategoriesOptions && <div className="additional-prices">
{fields.map((price, index) => (
@ -293,14 +308,16 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
id={`event_price_categories_attributes.${index}.price_category_id`}
rules={{ required: true }}
formState={formState}
disabled={() => index < fields.length - 1}
label={t('app.admin.event_form.fare_class')} />
<FormInput id={`event_price_categories_attributes.${index}.amount`}
register={register}
type="number"
rules={{ required: true }}
formState={formState}
label={t('app.admin.event_form.price')}
addOn={FormatLib.currencySymbol()} />
register={register}
type="number"
rules={{ required: true, min: 0 }}
nullable
formState={formState}
label={t('app.admin.event_form.price')}
addOn={FormatLib.currencySymbol()} />
<FabButton className="remove-price is-main" onClick={() => handlePriceRemove(price, index)} icon={<Trash size={20} />} />
</div>
))}

View File

@ -66,7 +66,8 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
placeholder={placeholder}
isDisabled={isDisabled}
isClearable={clearable}
options={options} />
options={options}
isOptionDisabled={(option) => option.disabled}/>
} />
</AbstractFormItem>
);

View File

@ -69,7 +69,7 @@ export interface Event {
export interface EventDecoration {
id?: number,
name: string,
related_to?: number
related_to?: number // report the count of events related to the given decoration
}
export type EventTheme = EventDecoration;

View File

@ -2,7 +2,7 @@
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
export type SelectOption<TOptionValue, TOptionLabel = string> = { value: TOptionValue, label: TOptionLabel }
export type SelectOption<TOptionValue, TOptionLabel = string> = { value: TOptionValue, label: TOptionLabel, disabled?: boolean }
/**
* Checklist Option format

View File

@ -0,0 +1,8 @@
import { AgeRange } from 'models/event';
const ageRanges: Array<AgeRange> = [
{ id: 1, name: 'Children' },
{ id: 2, name: 'Over 18' }
];
export default ageRanges;

View File

@ -0,0 +1,8 @@
import { EventCategory } from 'models/event';
const categories: Array<EventCategory> = [
{ id: 1, name: 'Workshop' },
{ id: 2, name: 'Internship' }
];
export default categories;

View File

@ -0,0 +1,8 @@
import { EventPriceCategory } from 'models/event';
const categories: Array<EventPriceCategory> = [
{ id: 1, name: 'Students' },
{ id: 2, name: 'Partners' }
];
export default categories;

View File

@ -0,0 +1,8 @@
import { EventTheme } from 'models/event';
const themes: Array<EventTheme> = [
{ id: 1, name: 'Fabric week' },
{ id: 2, name: 'Everyone at the Fablab' }
];
export default themes;

View File

@ -13,6 +13,10 @@ import spaces from '../__fixtures__/spaces';
import statuses from '../__fixtures__/statuses';
import notificationTypes from '../__fixtures__/notification_types';
import notifications from '../__fixtures__/notifications';
import eventCategories from '../__fixtures__/event_categories';
import eventThemes from '../__fixtures__/event_themes';
import ageRanges from '../__fixtures__/age_ranges';
import eventPriceCategories from '../__fixtures__/event_price_categories';
const FixturesLib = {
init: () => {
@ -33,7 +37,11 @@ const FixturesLib = {
spaces: JSON.parse(JSON.stringify(spaces)),
statuses: JSON.parse(JSON.stringify(statuses)),
notificationTypes: JSON.parse(JSON.stringify(notificationTypes)),
notifications: JSON.parse(JSON.stringify(notifications))
notifications: JSON.parse(JSON.stringify(notifications)),
eventCategories: JSON.parse(JSON.stringify(eventCategories)),
eventThemes: JSON.parse(JSON.stringify(eventThemes)),
ageRanges: JSON.parse(JSON.stringify(ageRanges)),
eventPriceCategories: JSON.parse(JSON.stringify(eventPriceCategories))
};
}
};

View File

@ -113,10 +113,22 @@ export const server = setupServer(
return res(ctx.json({ status }));
}),
rest.get('/api/notification_types', (req, res, ctx) => {
return res(ctx.json(fixtures.notification_types));
return res(ctx.json(fixtures.notificationTypes));
}),
rest.get('/api/notifications', (req, res, ctx) => {
return res(ctx.json(fixtures.notifications));
}),
rest.get('/api/categories', (req, res, ctx) => {
return res(ctx.json(fixtures.eventCategories));
}),
rest.get('/api/event_themes', (req, res, ctx) => {
return res(ctx.json(fixtures.eventThemes));
}),
rest.get('/api/age_ranges', (req, res, ctx) => {
return res(ctx.json(fixtures.ageRanges));
}),
rest.get('/api/price_categories', (req, res, ctx) => {
return res(ctx.json(fixtures.eventPriceCategories));
})
);

View File

@ -0,0 +1,68 @@
import { EventForm } from 'components/events/event-form';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import selectEvent from 'react-select-event';
import eventPriceCategories from '../../__fixtures__/event_price_categories';
describe('EventForm', () => {
const onError = jest.fn();
const onSuccess = jest.fn();
test('render create EventForm', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
expect(screen.getByLabelText(/app.admin.event_form.title/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.matching_visual/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.description/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.event_category/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.event_themes/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.age_range/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.start_date/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.end_date/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.all_day/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.start_time/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.end_time/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.recurrence/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.seats_available/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.standard_rate/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /app.admin.event_form.add_price/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /app.admin.event_form.add_a_new_file/ })).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.advanced_accounting_form.code/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /app.admin.event_form.save/ })).toBeInTheDocument();
});
test('all day event hides the time inputs', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
const user = userEvent.setup();
await user.click(screen.getByLabelText(/app.admin.event_form.all_day/));
expect(screen.queryByLabelText(/app.admin.event_form.start_time/)).toBeNull();
expect(screen.queryByLabelText(/app.admin.event_form.end_time/)).toBeNull();
});
test('recurrent event requires end date', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
await selectEvent.select(screen.getByLabelText(/app.admin.event_form.recurrence/), 'app.admin.event_form.recurring.every_week');
expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/).closest('label')).toHaveClass('is-required');
});
test('adding a second custom rate', async () => {
render(<EventForm action="create" onError={onError} onSuccess={onSuccess} />);
await waitFor(() => screen.getByRole('combobox', { name: /app.admin.event_form.event_category/ }));
// add a first category
fireEvent.click(screen.getByRole('button', { name: /app.admin.event_form.add_price/ }));
expect(screen.getByLabelText(/app.admin.event_form.fare_class/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.event_form.price/)).toBeInTheDocument();
await selectEvent.select(screen.getByLabelText(/app.admin.event_form.fare_class/), eventPriceCategories[0].name);
fireEvent.change(screen.getByLabelText(/app.admin.event_form.price/), { target: { value: 10 } });
// add a second category
fireEvent.click(screen.getByRole('button', { name: /app.admin.event_form.add_price/ }));
expect(screen.getAllByLabelText(/app.admin.event_form.fare_class/)[0]).toBeDisabled();
await selectEvent.openMenu(screen.getAllByLabelText(/app.admin.event_form.fare_class/)[1]);
expect(screen.getAllByText(eventPriceCategories[0].name).find(element => element.classList.contains('rs__option'))).toHaveAttribute('aria-disabled', 'true');
});
});