diff --git a/CHANGELOG.md b/CHANGELOG.md index 476f212ef..9da3222b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 6052208c3..e8a89c29f 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -71,6 +71,19 @@ export const EventForm: React.FC = ({ 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 = ({ action, event, onError, on type="number" tooltip={t('app.admin.event_form.seats_help')} /> + 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 &&
{fields.map((price, index) => ( @@ -293,14 +308,16 @@ export const EventForm: React.FC = ({ 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')} /> + register={register} + type="number" + rules={{ required: true, min: 0 }} + nullable + formState={formState} + label={t('app.admin.event_form.price')} + addOn={FormatLib.currencySymbol()} /> handlePriceRemove(price, index)} icon={} />
))} diff --git a/app/frontend/src/javascript/components/form/form-select.tsx b/app/frontend/src/javascript/components/form/form-select.tsx index 9559e2b7a..ed62fb163 100644 --- a/app/frontend/src/javascript/components/form/form-select.tsx +++ b/app/frontend/src/javascript/components/form/form-select.tsx @@ -66,7 +66,8 @@ export const FormSelect = + options={options} + isOptionDisabled={(option) => option.disabled}/> } /> ); diff --git a/app/frontend/src/javascript/models/event.ts b/app/frontend/src/javascript/models/event.ts index 5e4fe83b5..5cca1d9e8 100644 --- a/app/frontend/src/javascript/models/event.ts +++ b/app/frontend/src/javascript/models/event.ts @@ -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; diff --git a/app/frontend/src/javascript/models/select.ts b/app/frontend/src/javascript/models/select.ts index e3dab0e1b..ce5938697 100644 --- a/app/frontend/src/javascript/models/select.ts +++ b/app/frontend/src/javascript/models/select.ts @@ -2,7 +2,7 @@ * Option format, expected by react-select * @see https://github.com/JedWatson/react-select */ -export type SelectOption = { value: TOptionValue, label: TOptionLabel } +export type SelectOption = { value: TOptionValue, label: TOptionLabel, disabled?: boolean } /** * Checklist Option format diff --git a/test/frontend/__fixtures__/age_ranges.ts b/test/frontend/__fixtures__/age_ranges.ts new file mode 100644 index 000000000..ad875b171 --- /dev/null +++ b/test/frontend/__fixtures__/age_ranges.ts @@ -0,0 +1,8 @@ +import { AgeRange } from 'models/event'; + +const ageRanges: Array = [ + { id: 1, name: 'Children' }, + { id: 2, name: 'Over 18' } +]; + +export default ageRanges; diff --git a/test/frontend/__fixtures__/event_categories.ts b/test/frontend/__fixtures__/event_categories.ts new file mode 100644 index 000000000..726889099 --- /dev/null +++ b/test/frontend/__fixtures__/event_categories.ts @@ -0,0 +1,8 @@ +import { EventCategory } from 'models/event'; + +const categories: Array = [ + { id: 1, name: 'Workshop' }, + { id: 2, name: 'Internship' } +]; + +export default categories; diff --git a/test/frontend/__fixtures__/event_price_categories.ts b/test/frontend/__fixtures__/event_price_categories.ts new file mode 100644 index 000000000..103c71b49 --- /dev/null +++ b/test/frontend/__fixtures__/event_price_categories.ts @@ -0,0 +1,8 @@ +import { EventPriceCategory } from 'models/event'; + +const categories: Array = [ + { id: 1, name: 'Students' }, + { id: 2, name: 'Partners' } +]; + +export default categories; diff --git a/test/frontend/__fixtures__/event_themes.ts b/test/frontend/__fixtures__/event_themes.ts new file mode 100644 index 000000000..229afe67f --- /dev/null +++ b/test/frontend/__fixtures__/event_themes.ts @@ -0,0 +1,8 @@ +import { EventTheme } from 'models/event'; + +const themes: Array = [ + { id: 1, name: 'Fabric week' }, + { id: 2, name: 'Everyone at the Fablab' } +]; + +export default themes; diff --git a/test/frontend/__lib__/fixtures.ts b/test/frontend/__lib__/fixtures.ts index ef7ed6663..9b9d57733 100644 --- a/test/frontend/__lib__/fixtures.ts +++ b/test/frontend/__lib__/fixtures.ts @@ -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)) }; } }; diff --git a/test/frontend/__setup__/server.js b/test/frontend/__setup__/server.js index 09e578c12..58a795bc7 100644 --- a/test/frontend/__setup__/server.js +++ b/test/frontend/__setup__/server.js @@ -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)); }) ); diff --git a/test/frontend/components/events/event-form.test.tsx b/test/frontend/components/events/event-form.test.tsx new file mode 100644 index 000000000..7b3d53b6c --- /dev/null +++ b/test/frontend/components/events/event-form.test.tsx @@ -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(); + 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(); + 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(); + 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(); + 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'); + }); +});