diff --git a/cypress.config.js b/cypress.config.js index 57c32a3d7c9..86edfbe5024 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -28,7 +28,6 @@ module.exports = defineConfig({ supportFile: 'test/cypress/support/index.js', }, retries: { - runMode: 2, // number of retries when running `cypress run` - openMode: 2, // number of retries when running `cypress open` + runMode: 2, }, }) diff --git a/src/apps/companies/apps/referrals/send-referral/controllers.js b/src/apps/companies/apps/referrals/send-referral/controllers.js index 41048158ccb..094385424c5 100644 --- a/src/apps/companies/apps/referrals/send-referral/controllers.js +++ b/src/apps/companies/apps/referrals/send-referral/controllers.js @@ -26,7 +26,9 @@ function renderSendReferralForm(req, res) { companyId: id, cancelUrl: urls.companies.detail(id), sendingAdviserTeamName, - flashMessages: res.locals.getMessages(), + + // TODO: This should not be necessary as flashMessages are included in global props + // flashMessages: res.locals.flashMessages, }, } ) diff --git a/src/apps/events/__test__/repos.test.js b/src/apps/events/__test__/repos.test.js index 9b59f2fac7a..e2215bcbe40 100644 --- a/src/apps/events/__test__/repos.test.js +++ b/src/apps/events/__test__/repos.test.js @@ -5,15 +5,12 @@ const proxyquire = require('proxyquire') const authorisedRequestStub = sinon.stub() const searchStub = sinon.stub() -const { saveEvent, fetchEvent, getAllEvents, getActiveEvents } = proxyquire( - '../repos', - { - '../../lib/authorised-request': { - authorisedRequest: authorisedRequestStub, - }, - '../../modules/search/services': { search: searchStub }, - } -) +const { saveEvent, getActiveEvents } = proxyquire('../repos', { + '../../lib/authorised-request': { + authorisedRequest: authorisedRequestStub, + }, + '../../modules/search/services': { search: searchStub }, +}) const config = require('../../../config') @@ -50,27 +47,6 @@ describe('Event Service', () => { }) }) - context('fetchEvent', () => { - it('should fetch single event by id', async () => { - const id = '123' - await fetchEvent(mockReq, id) - expect(authorisedRequestStub).to.have.been.calledWith( - mockReq, - `${config.apiRoot}/v3/event/${id}` - ) - }) - }) - - context('getAllEvents', () => { - it('should fetch all events with limit and offset', async () => { - await getAllEvents(mockReq) - expect(authorisedRequestStub).to.have.been.calledWith( - mockReq, - `${config.apiRoot}/v3/event?limit=100000&offset=0` - ) - }) - }) - context('getActiveEvents', () => { let clock diff --git a/src/apps/events/attendees/controllers/list.js b/src/apps/events/attendees/controllers/list.js index 4b757abaea5..37a258ad987 100644 --- a/src/apps/events/attendees/controllers/list.js +++ b/src/apps/events/attendees/controllers/list.js @@ -48,6 +48,7 @@ async function renderAttendees(req, res, next) { children: [{ value: sortby }], }) + // TODO: Can we remove this? res.breadcrumb(name).render('events/attendees/views/list', { incompleteEvent, attendees: attendeesCollection, diff --git a/src/apps/events/attendees/router.js b/src/apps/events/attendees/router.js index f00d3dd3dc0..7a592168bce 100644 --- a/src/apps/events/attendees/router.js +++ b/src/apps/events/attendees/router.js @@ -1,8 +1,9 @@ +// TODO: Remove this whole module and the whole parent folder ../ const router = require('express').Router() const { createAttendee, renderAttendees } = require('./controllers') -router.get('/', renderAttendees) +router.get('/*', renderAttendees) router.get('/create/:contactId', createAttendee) diff --git a/src/apps/events/middleware/details.js b/src/apps/events/middleware/details.js index 81d2cae30aa..546022e8a4d 100644 --- a/src/apps/events/middleware/details.js +++ b/src/apps/events/middleware/details.js @@ -1,3 +1,4 @@ +// TODO: Get rid of this whole module const { assign } = require('lodash') const { transformEventFormBodyToApiRequest } = require('../transformers') @@ -27,6 +28,7 @@ async function postDetails(req, res, next) { } } +// TODO: Get rid of this async function getEventDetails(req, res, next, eventId) { try { res.locals.event = await fetchEvent(req, eventId) diff --git a/src/apps/events/router.js b/src/apps/events/router.js index 3d996ed1731..bccdb73225a 100644 --- a/src/apps/events/router.js +++ b/src/apps/events/router.js @@ -5,7 +5,7 @@ const { APP_PERMISSIONS, LOCAL_NAV } = require('./constants') const { handleRoutePermissions, setLocalNav } = require('../middleware') const { getEventDetails } = require('./middleware/details') const { renderEventsView } = require('./controllers/events') -const attendeesRouter = require('./attendees/router') +const { createAttendee } = require('./attendees/controllers/create') router.get('/create', renderEventsView) router.use(handleRoutePermissions(APP_PERMISSIONS)) @@ -19,12 +19,9 @@ router.use( setLocalNav(LOCAL_NAV) ) -router.use('/:eventId/attendees', attendeesRouter) +// TODO: Get rid of this and fetch the event on the client router.param('eventId', getEventDetails) -// TODO: When everything in the events space is converted to react -// router.get('/*', renderEventsView) -router.get('/:eventId/edit', renderEventsView) -router.get('/:eventId', renderEventsView) -router.get('/:eventId/details', renderEventsView) +router.get('/:eventId/attendees/create/:contactId', createAttendee) +router.get('/:eventId*', renderEventsView) module.exports = router diff --git a/src/client/components/Resource/InteractionsV3.js b/src/client/components/Resource/InteractionsV3.js new file mode 100644 index 00000000000..99bef7fb5f1 --- /dev/null +++ b/src/client/components/Resource/InteractionsV3.js @@ -0,0 +1,3 @@ +import { createCollectionResource } from './Resource' + +export default createCollectionResource('InteractionsV3', 'v3/interaction') diff --git a/src/client/components/Resource/Paginated.js b/src/client/components/Resource/Paginated.js index 97b01fb8bdd..6c3608db5ae 100644 --- a/src/client/components/Resource/Paginated.js +++ b/src/client/components/Resource/Paginated.js @@ -53,6 +53,9 @@ const StyledCollectionSort = styled(CollectionSort)` * Forwarded to {qsParamName} prop of {StyledCollectionSort}. * @param {number} [props.defaultSortOptionIndex = 0] - The index of the sort option * that should be selected when there's nothing set in the query string. + * @param {string} [props.addItemUrl =] - If set, an "Add <item-name>" button + * will be displayed on the top-right corner of the header behaving as a link + * to the value of this prop. This is just forwarded to CollectionHeader. * @example * <PaginatedResource name="My task name" id="foo"> * {currentPage => @@ -100,6 +103,7 @@ const PaginatedResource = multiInstance({ result, shouldPluralize, noResults = "You don't have any results", + addItemUrl, }) => { // We know better than ESLint that we are in deed in a React component // eslint-disable-next-line react-hooks/rules-of-hooks @@ -150,6 +154,7 @@ const PaginatedResource = multiInstance({ totalItems={result.count} collectionName={heading || name} shouldPluralize={shouldPluralize} + addItemUrl={addItemUrl} /> {totalPages > 0 && ( <StyledCollectionSort diff --git a/src/client/components/Resource/tasks.js b/src/client/components/Resource/tasks.js index bc28615076d..e602fa7a79a 100644 --- a/src/client/components/Resource/tasks.js +++ b/src/client/components/Resource/tasks.js @@ -6,6 +6,7 @@ import Company from './Company' import CompanyContacts from './CompanyContacts' import Countries from './Countries' import Interactions from './Interactions' +import InteractionsV3 from './InteractionsV3' import Opportunity from './Opportunity' import OpportunityStatuses from './OpportunityStatuses' import UKRegions from './UKRegions' @@ -109,6 +110,7 @@ export default { ...ContactAuditHistory.tasks, ...Countries.tasks, ...Interactions.tasks, + ...InteractionsV3.tasks, ...Opportunity.tasks, ...OpportunityStatuses.tasks, ...CapitalInvestmentRequiredChecksConducted.tasks, diff --git a/src/client/modules/Events/CollectionList/index.jsx b/src/client/modules/Events/CollectionList/index.jsx index 4bcf5814275..5fc23cc65b1 100644 --- a/src/client/modules/Events/CollectionList/index.jsx +++ b/src/client/modules/Events/CollectionList/index.jsx @@ -69,7 +69,7 @@ const EventTemplate = (item) => { <StyledCollectionItem dataTest="data-hub-event" headingText={item.headingText} - headingUrl={item.headingUrl} + headingUrl={`/events/${item.id}/details`} metadata={item.metadata} tags={item.tags} titleRenderer={TitleRenderer} diff --git a/src/client/modules/Events/EventDetails/index.jsx b/src/client/modules/Events/EventDetails/index.jsx index 99614daf268..4234d67c941 100644 --- a/src/client/modules/Events/EventDetails/index.jsx +++ b/src/client/modules/Events/EventDetails/index.jsx @@ -3,33 +3,101 @@ import { connect } from 'react-redux' import { isEmpty } from 'lodash' import { useParams } from 'react-router-dom' -import GridRow from '@govuk-react/grid-row' -import GridCol from '@govuk-react/grid-col' import Button from '@govuk-react/button' import styled from 'styled-components' +import { H3, Link } from 'govuk-react' + import urls from '../../../../lib/urls' import { TASK_GET_EVENT_DETAILS, ID, state2props } from './state' import { EVENTS__DETAILS_LOADED } from '../../../actions' import Task from '../../../components/Task' -import { - LocalNav, - LocalNavLink, - SummaryTable, - FormActions, - DefaultLayout, -} from '../../../components' +import { SummaryTable, FormActions, DefaultLayout } from '../../../components' +import CollectionItem from '../../../components/CollectionList/CollectionItem' +import State from '../../../components/State' + +import { VerticalTabNav } from '../../../components/TabNav' +import InteractionsV3 from '../../../components/Resource/InteractionsV3' +import { formatDate, DATE_FORMAT_FULL } from '../../../utils/date-utils' +import StatusMessage from '../../../components/StatusMessage' const StyledSummaryTable = styled(SummaryTable)({ marginTop: 0, }) -const EventDetails = ({ - name = 'Event', +const Attendees = ({ eventId, isDisabled }) => ( + <div> + <H3 as="h2">Event Attendees</H3> + {isDisabled && ( + <StatusMessage> + You cannot add an event attendee because the event has been disabled. + </StatusMessage> + )} + <InteractionsV3.Paginated + id="???" + heading="attendee" + addItemUrl={!isDisabled && `/events/${eventId}/attendees/find-new`} + sortOptions={[ + { name: 'Last name: A-Z', value: 'last_name_of_first_contact' }, + { name: 'Last name: Z-A', value: '-last_name_of_first_contact' }, + { name: 'First name: A-Z', value: 'first_name_of_first_contact' }, + { name: 'First name: Z-A', value: '-first_name_of_first_contact' }, + { name: 'Company name: A-Z', value: 'company__name' }, + { name: 'Company name: Z-A', value: '-company__name' }, + { name: 'Recently added', value: '-created_on' }, + { name: 'Least recently added', value: 'created_on' }, + ]} + payload={{ + event_id: eventId, + }} + > + {(page) => ( + <ul> + {page.map( + ({ contacts: [contact], companies: [company], date, service }) => ( + <CollectionItem + headingText={contact?.name || 'Not available'} + headingUrl={contact && `/contacts/${contact?.id}`} + metadata={[ + { + label: 'Company', + value: ( + <Link href={`/companies/${company?.id}`}> + {company?.name} + </Link> + ), + }, + { + label: 'Job title', + value: contact?.job_title || 'Not available', + }, + { + label: 'Date attended', + value: formatDate(date, DATE_FORMAT_FULL), + }, + { + label: 'Service delivery', + value: ( + <Link href={`/companies/${service.id}`}> + {service.name} + </Link> + ), + }, + ]} + /> + ) + )} + </ul> + )} + </InteractionsV3.Paginated> + </div> +) + +const Details = ({ eventType, + eventDays, startDate, endDate, - eventDays, locationType, fullAddress, ukRegion, @@ -39,131 +107,139 @@ const EventDetails = ({ otherTeams, relatedProgrammes, relatedTradeAgreements, - service, disabledOn, -}) => { - const { id } = useParams() - const breadcrumbs = [ - { - link: urls.dashboard.index(), - text: 'Home', - }, - { - link: urls.events.index(), - text: 'Events', - }, - { - text: name || '', - }, - ] + service, + id, +}) => ( + <> + <StyledSummaryTable> + <SummaryTable.Row heading="Type of event" children={eventType} /> + <SummaryTable.Row + heading={eventDays == 1 ? 'Event date' : 'Event start date'} + children={startDate} + /> + {eventDays > 1 ? ( + <SummaryTable.Row heading="Event end date" children={endDate} /> + ) : null} + <SummaryTable.Row heading="Event location type"> + {isEmpty(locationType) ? 'Not set' : locationType} + </SummaryTable.Row> + <SummaryTable.Row heading="Address">{fullAddress}</SummaryTable.Row> + <SummaryTable.Row heading="Region" hideWhenEmpty={false}> + {isEmpty(ukRegion) ? 'Not set' : ukRegion} + </SummaryTable.Row> + <SummaryTable.Row heading="Notes" hideWhenEmpty={false}> + {isEmpty(notes) ? 'Not set' : notes} + </SummaryTable.Row> + <SummaryTable.Row heading="Lead team"> + {isEmpty(leadTeam) ? 'Not set' : leadTeam} + </SummaryTable.Row> + <SummaryTable.Row heading="Organiser"> + {isEmpty(organiser) ? 'Not set' : organiser} + </SummaryTable.Row> + <SummaryTable.ListRow + heading="Other teams" + value={otherTeams} + emptyValue="Not set" + hideWhenEmpty={false} + /> + <SummaryTable.ListRow + heading="Related programmes" + value={relatedProgrammes} + emptyValue="Not set" + hideWhenEmpty={false} + /> + <SummaryTable.ListRow + heading="Related Trade Agreements" + value={relatedTradeAgreements} + emptyValue="Not set" + hideWhenEmpty={false} + /> + <SummaryTable.Row heading="Service" children={service} /> + </StyledSummaryTable> + {!disabledOn && ( + <FormActions> + <Button as={'a'} href={urls.events.edit(id)}> + Edit event + </Button> + </FormActions> + )} + </> +) + +const EventDetails = ({ name, ...props }) => { + const { id, ['*']: path } = useParams() return ( - <DefaultLayout - heading="Events" - pageTitle="Events" - breadcrumbs={breadcrumbs} - useReactRouter={true} - > - <Task.Status - name={TASK_GET_EVENT_DETAILS} - id={ID} - progressMessage="loading event details" - startOnRender={{ - payload: id, - onSuccessDispatch: EVENTS__DETAILS_LOADED, - }} - > - {() => { - return ( - name && ( - <GridRow data-test="eventDetails"> - <GridCol setWidth="one-quarter"> - <LocalNav data-test="event-details-nav"> - <LocalNavLink - data-test="event-details-nav-link" - href={urls.events.details(id)} - > - Details - </LocalNavLink> - <LocalNavLink - data-test="event-details-nav-link" - href={urls.events.attendees(id)} - > - Attendees - </LocalNavLink> - </LocalNav> - </GridCol> - <GridCol setWidth="three-quarters"> - <StyledSummaryTable> - <SummaryTable.Row - heading="Type of event" - children={eventType} - /> - <SummaryTable.Row - heading={ - eventDays == 1 ? 'Event date' : 'Event start date' - } - children={startDate} - /> - {eventDays > 1 ? ( - <SummaryTable.Row - heading="Event end date" - children={endDate} - /> - ) : null} - <SummaryTable.Row heading="Event location type"> - {isEmpty(locationType) ? 'Not set' : locationType} - </SummaryTable.Row> - <SummaryTable.Row heading="Address"> - {fullAddress} - </SummaryTable.Row> - <SummaryTable.Row heading="Region" hideWhenEmpty={false}> - {isEmpty(ukRegion) ? 'Not set' : ukRegion} - </SummaryTable.Row> - <SummaryTable.Row heading="Notes" hideWhenEmpty={false}> - {isEmpty(notes) ? 'Not set' : notes} - </SummaryTable.Row> - <SummaryTable.Row heading="Lead team"> - {isEmpty(leadTeam) ? 'Not set' : leadTeam} - </SummaryTable.Row> - <SummaryTable.Row heading="Organiser"> - {isEmpty(organiser) ? 'Not set' : organiser} - </SummaryTable.Row> - <SummaryTable.ListRow - heading="Other teams" - value={otherTeams} - emptyValue="Not set" - hideWhenEmpty={false} - /> - <SummaryTable.ListRow - heading="Related programmes" - value={relatedProgrammes} - emptyValue="Not set" - hideWhenEmpty={false} - /> - <SummaryTable.ListRow - heading="Related Trade Agreements" - value={relatedTradeAgreements} - emptyValue="Not set" - hideWhenEmpty={false} - /> - <SummaryTable.Row heading="Service" children={service} /> - </StyledSummaryTable> - {!disabledOn && ( - <FormActions> - <Button as={'a'} href={urls.events.edit(id)}> - Edit event - </Button> - </FormActions> - )} - </GridCol> - </GridRow> - ) - ) - }} - </Task.Status> - </DefaultLayout> + <State> + {({ flashMessages }) => ( + <DefaultLayout + heading={name} + pageTitle="Events" + flashMessages={{ + ...flashMessages, + info: [ + ...(flashMessages.info || []), + ...(props.disabledOn + ? [ + `This event was disabled on ${formatDate(props.disabledOn, DATE_FORMAT_FULL)} and can no longer be edited.`, + ] + : []), + ], + }} + breadcrumbs={[ + { + link: urls.dashboard.index(), + text: 'Home', + }, + { + link: urls.events.index(), + text: 'Events', + }, + { + text: name, + }, + { + text: { details: 'Details', attendees: 'Attendees' }[path], + }, + ]} + useReactRouter={true} + > + <Task.Status + name={TASK_GET_EVENT_DETAILS} + id={ID} + progressMessage="loading event details" + startOnRender={{ + payload: id, + onSuccessDispatch: EVENTS__DETAILS_LOADED, + }} + > + {() => ( + <VerticalTabNav + routed={true} + id="event-details-tab-nav" + label="Event tab navigation" + selectedIndex="attendees" + tabs={{ + [`/events/${id}/details`]: { + label: 'Details', + content: <Details {...props} />, + }, + [`/events/${id}/attendees`]: { + label: 'Attendees', + content: ( + <Attendees eventId={id} isDisabled={props.disabledOn} /> + ), + }, + }} + /> + )} + </Task.Status> + </DefaultLayout> + )} + </State> ) } +// TODO: Get rid of this export default connect(state2props)(EventDetails) diff --git a/src/client/routes.js b/src/client/routes.js index 56408ae63e7..95944ffc67e 100644 --- a/src/client/routes.js +++ b/src/client/routes.js @@ -400,7 +400,7 @@ function Routes() { ), }, { - path: '/events/:id/details', + path: '/events/:id/*', element: ( <ProtectedRoute module={'datahub:events'}> <EventDetails /> diff --git a/src/middleware/__test__/user-locals.test.js b/src/middleware/__test__/user-locals.test.js index 278858b1cc3..ace496818ec 100644 --- a/src/middleware/__test__/user-locals.test.js +++ b/src/middleware/__test__/user-locals.test.js @@ -1,93 +1,29 @@ -const proxyquire = require('proxyquire') -const { faker } = require('@faker-js/faker') - -const words = faker.lorem.words -const modulePath = '../user-locals' - -describe('user-locals middleware', () => { - let req, res, next - beforeEach(() => { - res = { - locals: {}, +const { parseFlashMessages } = require('../user-locals') + +describe('parseFlashMessages', () => { + it('valid JSON in success:with-body', () => { + const BODY = { heading: 'foo bar', body: 'baz bing' } + const RAW_FLASH_MESSAGES = { + success: ['foo', '{"test":1}'], + error: ['bar', 'baz'], + 'success:with-body': [JSON.stringify(BODY)], } - next = sinon.spy() - }) - describe('#getMessages', () => { - afterEach(() => { - expect(next).to.have.been.called + expect(parseFlashMessages(RAW_FLASH_MESSAGES)).to.deep.equal({ + ...RAW_FLASH_MESSAGES, + 'success:with-body': [BODY], }) + }) - context('With no messages in the flash', () => { - it('returns the values', () => { - const userLocals = require(modulePath) - const flash = sinon.stub().returns({}) - req = { - path: '', - flash, - } - userLocals(req, res, next) - - const messages = res.locals.getMessages() - - expect(flash).to.have.been.calledWith() - expect(messages).to.deep.equal({}) - }) - }) - - context('With valid messages in the flash', () => { - it('returns the values', () => { - const userLocals = require(modulePath) - const flashData = { - success: [words(5), '{"test":1}'], - error: [words(5), words(5)], - 'success:with-body': [ - JSON.stringify({ heading: words(2), body: words(10) }), - ], - } - const flash = sinon.stub().returns(flashData) - req = { - path: '', - flash, - } - - userLocals(req, res, next) - - const messages = res.locals.getMessages() - - expect(messages).to.equal(flashData) - expect(typeof messages['success:with-body'][0]).to.equal('object') - expect(typeof messages.success[1]).to.equal('string') - expect(flash).to.have.been.calledWith() - }) - }) - - context('With invalid JSON messages in the flash', () => { - it('returns the string and reports the error', () => { - const captureException = sinon.spy() - const userLocals = proxyquire(modulePath, { - '../lib/reporter': { - captureException, - }, - }) - const flashData = { - 'success:with-body': [`heading: ${words(2)}`], - } - const flash = sinon.stub().returns(flashData) - req = { - path: '', - flash, - } - - userLocals(req, res, next) - - const messages = res.locals.getMessages() + it('invalid JSON in success:with-body', () => { + const RAW_FLASH_MESSAGES = { + success: ['foo', '{"test":1}'], + error: ['bar', 'baz'], + 'success:with-body': ["I'm no valid JSON"], + } - expect(messages).to.equal(flashData) - expect(typeof messages['success:with-body'][0]).to.equal('string') - expect(flash).to.have.been.calledWith() - expect(captureException).to.have.been.called - }) - }) + expect(parseFlashMessages(RAW_FLASH_MESSAGES)).to.deep.equal( + RAW_FLASH_MESSAGES + ) }) }) diff --git a/src/middleware/react-global-props.js b/src/middleware/react-global-props.js index b87abe16727..c6737c35bdb 100644 --- a/src/middleware/react-global-props.js +++ b/src/middleware/react-global-props.js @@ -9,6 +9,7 @@ const transformModulePermissions = (modules) => module.exports = () => { return function reactGlobalProps(req, res, next) { res.locals.globalProps = { + flashMessages: res.locals.flashMessages, sentryDsn: config.sentryDsn, sentryEnvironment: config.sentryEnvironment, csrfToken: req.csrfToken(), diff --git a/src/middleware/user-locals.js b/src/middleware/user-locals.js index ec46d8ce1ed..f9ba51c4388 100644 --- a/src/middleware/user-locals.js +++ b/src/middleware/user-locals.js @@ -27,7 +27,15 @@ function convertValueToJson(value) { } } -module.exports = (req, res, next) => { +const parseFlashMessages = (rawFlashMessages) => + Object.fromEntries( + Object.entries(rawFlashMessages).map(([k, v]) => [ + k, + k.endsWith(':with-body') ? v.map(convertValueToJson) : v, + ]) + ) + +const userLocals = (req, res, next) => { const userPermissions = get(res, 'locals.user.permissions') const userProfile = config.oauth.bypassSSO ? null @@ -37,28 +45,22 @@ module.exports = (req, res, next) => { filterNonPermittedItem(userPermissions) ) - Object.assign(res.locals, { - PERMITTED_APPLICATIONS: config.oauth.bypassSSO - ? [{ key: 'datahub-crm' }] - : permittedApplications, - ALLOWED_APPS: permittedNavItems.reduce((apps, { headerKey }) => { - headerKey && apps.push(headerKey) - return apps - }, []), - ACTIVE_KEY: getActiveHeaderKey(req.path, permittedNavItems), - - getMessages() { - const items = req.flash() + res.locals.PERMITTED_APPLICATIONS = config.oauth.bypassSSO + ? [{ key: 'datahub-crm' }] + : permittedApplications - for (const [key, values] of Object.entries(items)) { - if (key.endsWith(':with-body')) { - items[key] = values.map(convertValueToJson) - } - } + res.locals.ALLOWED_APPS = permittedNavItems.reduce((apps, { headerKey }) => { + headerKey && apps.push(headerKey) + return apps + }, []) - return items - }, - }) + res.locals.ACTIVE_KEY = getActiveHeaderKey(req.path, permittedNavItems) + res.locals.flashMessages = parseFlashMessages(req.flash()) next() } + +module.exports = { + parseFlashMessages, + userLocals, +} diff --git a/src/server.js b/src/server.js index 4c161763695..0962944400c 100644 --- a/src/server.js +++ b/src/server.js @@ -20,7 +20,7 @@ const currentJourney = require('./modules/form/current-journey') const nunjucks = require('./config/nunjucks') const headers = require('./middleware/headers') const locals = require('./middleware/locals') -const userLocals = require('./middleware/user-locals') +const { userLocals } = require('./middleware/user-locals') const user = require('./middleware/user') const auth = require('./middleware/auth') const store = require('./middleware/store') diff --git a/src/templates/_macros/common/local-header.njk b/src/templates/_macros/common/local-header.njk index 15517e18401..437d64c621f 100644 --- a/src/templates/_macros/common/local-header.njk +++ b/src/templates/_macros/common/local-header.njk @@ -14,7 +14,7 @@ #} {% macro LocalHeader(props) %} {% set breadcrumbs = props.breadcrumbs | default(getBreadcrumbs()) %} - {% set messages = props.messages | default(getMessages() if getMessages) %} + {% set messages = props.messages | default(flashMessages) %} {% set modifier = props.modifier | concat('') | reverse | join(' c-local-header--') if props.modifier %} {% if props %} diff --git a/test/end-to-end/cypress/specs/DIT/event-spec.js b/test/end-to-end/cypress/specs/DIT/event-spec.js index bd1a507a119..eb6e3caf2f5 100644 --- a/test/end-to-end/cypress/specs/DIT/event-spec.js +++ b/test/end-to-end/cypress/specs/DIT/event-spec.js @@ -123,22 +123,15 @@ describe('Event', () => { cy.visit(urls.events.index()) cy.contains(eventName).click() cy.contains('Attendees').click() - cy.get(selectors.entityCollection.addAttendee).click() + cy.contains('Add attendee').click() - cy.get('[data-test="contact-name-filter"]') - .type('dean cox') - .type('{enter}') + cy.get('input[name="name"]').type('dean cox').type('{enter}') cy.contains('Dean Cox').click() - cy.get(selectors.message.flashMessages) - .should( - 'contain', - 'Event attendee added - This has created a service delivery record.' - ) - .and( - 'contain', + cy.contains( + 'Event attendee added - This has created a service delivery record. ' + 'If required, you can view or edit the service delivery directly from the attendee record.' - ) + ) }) }) @@ -159,7 +152,7 @@ describe('Event', () => { .invoke('text') .should('contain', 'Account management') cy.contains(eventName).click() - cy.get(selectors.entityCollection.editEvent).click() + cy.contains('a', 'Edit event') fillEventType('Exhibition') clickSaveAndReturnButton() @@ -182,9 +175,7 @@ describe('Event', () => { cy.visit(urls.events.details(event.pk)) cy.contains('a', 'Attendees').click() cy.contains('Add attendee').click() - cy.get('[data-test="contact-name-filter"]') - .type('Attendee') - .type('{enter}') + cy.get('input[name="name"]').type('Attendee').type('{enter}') cy.contains('Joe Attendee').click() cy.contains('Event attendee added') }) @@ -193,17 +184,12 @@ describe('Event', () => { cy.visit(urls.events.details(event.pk)) cy.contains('a', 'Attendees').click() cy.contains('Add attendee').click() - cy.get('[data-test="contact-name-filter"]') - .type('Attendee') - .type('{enter}') + cy.get('input[name="name"]').type('Attendee').type('{enter}') cy.contains('Joe Attendee').click() cy.contains('Add attendee').click() - cy.get('[data-test="contact-name-filter"]') - .type('Attendee') - .type('{enter}') + cy.get('input[name="name"]').type('Attendee').type('{enter}') cy.contains('Joe Attendee').click() - cy.get(selectors.message.flashMessages).should( - 'contain', + cy.contains( 'Event attendee not added - This contact has already been added as an event attendee' ) }) diff --git a/test/end-to-end/cypress/specs/DIT/local-nav-spec.js b/test/end-to-end/cypress/specs/DIT/local-nav-spec.js index 0c14e265066..883ec59d719 100644 --- a/test/end-to-end/cypress/specs/DIT/local-nav-spec.js +++ b/test/end-to-end/cypress/specs/DIT/local-nav-spec.js @@ -108,17 +108,4 @@ describe('DBT Permission', () => { ]) }) }) - - describe('event', () => { - beforeEach(() => { - const event = fixtures.event.create.defaultEvent() - cy.loadFixture([event]) - cy.visit(urls.events.details(event.pk)) - }) - - it('should display DBT only side navs', () => { - const navSelector = '[data-test="event-details-nav-link"]' - assertLocalNav(navSelector, ['Details', 'Attendee']) - }) - }) }) diff --git a/test/functional/cypress/specs/events/attendees-search-spec.js b/test/functional/cypress/specs/events/attendees-search-spec.js index 012730c3854..03f9904e16e 100644 --- a/test/functional/cypress/specs/events/attendees-search-spec.js +++ b/test/functional/cypress/specs/events/attendees-search-spec.js @@ -174,12 +174,9 @@ describe('Event attendee search', () => { it('should add an attendee to the event', () => { cy.contains('Hanna Reinger').click() - cy.get('.c-message') - .should('exist') - .should( - 'have.text', - 'Event attendee added - This has created a service delivery record. If required, you can view or edit the service delivery directly from the attendee record.Dismiss' - ) + cy.contains( + 'Event attendee added - This has created a service delivery record. If required, you can view or edit the service delivery directly from the attendee record.' + ) }) }) }) diff --git a/test/functional/cypress/specs/events/attendees-spec.js b/test/functional/cypress/specs/events/attendees-spec.js index 240d26608ef..992b74e5d60 100644 --- a/test/functional/cypress/specs/events/attendees-spec.js +++ b/test/functional/cypress/specs/events/attendees-spec.js @@ -1,5 +1,4 @@ const fixtures = require('../../fixtures') -const selectors = require('../../../../selectors') const urls = require('../../../../../src/lib/urls') const { @@ -8,101 +7,35 @@ const { } = require('../../support/assertions') describe('Event Attendees', () => { - context('Enabled events', () => { - beforeEach(() => { - cy.visit(`/events/${fixtures.event.oneDayExhibition.id}/attendees`) - cy.get('.c-collection').as('collectionList') - }) - - it('should render breadcrumbs', () => { - assertBreadcrumbs({ - Home: urls.dashboard.index(), - Events: urls.events.index(), - 'One-day exhibition': null, - }) - }) - - it('should render the header', () => { - assertLocalHeader('One-day exhibition') - }) - - it('should display a collection header', () => { - cy.get('main article h2').should('contain', 'Event Attendees') - }) - - it('should display attendee collection list', () => { - cy.get(selectors.entityCollection.collection) - .find('header') - .should('contain', '1,233') - .parent() - .find('span') - .should('contain', 'Page 1 of 13') - .parents('header') - .next() - .find('li') - .should('have.length', 100) - }) - - it('should be able to use the pagination', () => { - cy.get(selectors.entityCollection.collection) - .find('nav ul li') - .as('pageLinks') - .eq(1) - .click() - - cy.get(selectors.entityCollection.collection) - .find('header') - .should('contain', 'Page 2 of 13') - - cy.get('@pageLinks').eq(0).click() - - cy.get(selectors.entityCollection.collection) - .find('header') - .should('contain', 'Page 1 of 13') - }) + it('Enabled events', () => { + cy.visit(`/events/${fixtures.event.oneDayExhibition.id}/attendees`) + assertBreadcrumbs({ + Home: urls.dashboard.index(), + Events: urls.events.index(), + 'One-day exhibition': null, + Attendees: null, + }) + assertLocalHeader('One-day exhibition') + cy.contains('h2', 'Event Attendees') + cy.contains('h2', '1,233 attendees') + cy.contains('Page 1 of 124') }) - context('Disabled events', () => { - beforeEach(() => { - cy.visit(`/events/${fixtures.event.teddyBearExpo.id}/attendees`) - }) - - it('should render breadcrumbs', () => { - assertBreadcrumbs({ - Home: urls.dashboard.index(), - Events: urls.events.index(), - 'Teddy bear expo': null, - }) - }) - - it('should render the header', () => { - assertLocalHeader('Teddy bear expo') - }) - - it('should display a message indicating when the event was disabled', () => { - assertLocalHeader( - 'This event was disabled on 5 September 2017 and can no longer be edited' - ) - }) - it('should not display add attendees button for disabled events', () => { - cy.get(selectors.entityCollection.addAttendee).should('not.exist') - }) - - it('should display a message indicating that you cannot add an event attendee', () => { - cy.get('main article h2') - .next() - .should( - 'contain', - 'You cannot add an event attendee because the event has been disabled.' - ) - }) - - it('should display a collection header', () => { - cy.get('main article h2').should('contain', 'Event Attendees') - }) - - it('should display attendee collection list', () => { - cy.get(selectors.collection.headerCount).should('contain', '1,233') - }) + it('Disabled events', () => { + cy.visit(`/events/${fixtures.event.teddyBearExpo.id}/attendees`) + assertBreadcrumbs({ + Home: urls.dashboard.index(), + Events: urls.events.index(), + 'Teddy bear expo': null, + Attendees: null, + }) + assertLocalHeader('Teddy bear expo') + assertLocalHeader( + 'This event was disabled on 5 September 2017 and can no longer be edited' + ) + cy.contains( + 'You cannot add an event attendee because the event has been disabled.' + ) + cy.contains('Add attendee').should('not.exist') }) }) diff --git a/test/functional/cypress/specs/events/details-spec.js b/test/functional/cypress/specs/events/details-spec.js index 673d253ccc6..9dd1fddc70f 100644 --- a/test/functional/cypress/specs/events/details-spec.js +++ b/test/functional/cypress/specs/events/details-spec.js @@ -1,141 +1,120 @@ import urls from '../../../../../src/lib/urls' const { - assertKeyValueTable, assertBreadcrumbs, + assertSummaryTableStrict, } = require('../../support/assertions') const fixtures = require('../../fixtures') const event = require('../../../../sandbox/fixtures/v3/event/single-event-missing-teams.json') -const selectors = require('../../../../selectors') + +const EVENT_DETAILS_ROWS = { + 'Type of event': 'Exhibition', + 'Event date': '1 January 2021', + 'Event location type': 'HQ', + Address: 'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China', + Region: 'Not set', + Notes: 'This is a dummy event for testing.', + 'Lead team': 'CBBC Hangzhou', + Organiser: 'John Rogers', + 'Other teams': 'CBBC HangzhouCBBC North West', + 'Related programmes': 'Grown in Britain', + 'Related Trade Agreements': 'UK - Japan', + Service: 'Events : UK based', +} describe('Event Details', () => { it('should display one day event details', () => { cy.visit(urls.events.details(fixtures.event.oneDayExhibition.id)) - cy.get(selectors.entityCollection.editEvent).should('be.visible') - assertBreadcrumbs({ Home: urls.dashboard.index.route, Events: urls.events.index(), 'One-day exhibition': null, + Details: null, }) - assertKeyValueTable('eventDetails', { - 'Type of event': 'Exhibition', - 'Event date': '1 January 2021', - 'Event location type': 'HQ', - Address: - 'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China', - Region: 'Not set', - Notes: 'This is a dummy event for testing.', - 'Lead team': 'CBBC Hangzhou', - Organiser: 'John Rogers', - 'Other teams': 'CBBC HangzhouCBBC North West', - 'Related programmes': 'Grown in Britain', - 'Related Trade Agreements': 'UK - Japan', - Service: 'Events : UK based', + assertSummaryTableStrict({ + rows: EVENT_DETAILS_ROWS, }) + + cy.contains('a', 'Edit event') }) it('should display one day event details with no teams', () => { cy.visit(urls.events.details(event.id)) - cy.get(selectors.entityCollection.editEvent).should('be.visible') - - assertKeyValueTable('eventDetails', { - 'Type of event': 'Exhibition', - 'Event date': '1 January 2021', - 'Event location type': 'HQ', - Address: - 'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China', - Region: 'Not set', - Notes: 'This is a dummy event for testing.', - 'Lead team': 'Not set', - Organiser: 'John Rogers', - 'Other teams': 'Not set', - 'Related programmes': 'Grown in Britain', - 'Related Trade Agreements': 'UK - Japan', - Service: 'Events : UK based', + assertSummaryTableStrict({ + rows: { + ...EVENT_DETAILS_ROWS, + 'Lead team': 'Not set', + 'Other teams': 'Not set', + }, }) + + cy.contains('a', 'Edit event') }) - describe('Disabled event with no document', () => { - beforeEach(() => { - cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id)) + it('should display no document link details', () => { + cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id)) + + assertBreadcrumbs({ + Home: urls.dashboard.index.route, + Events: urls.events.index(), + 'Teddy bear expo': null, + Details: null, }) - it('should display no document link details', () => { - assertBreadcrumbs({ - Home: urls.dashboard.index.route, - Events: urls.events.index(), - 'Teddy bear expo': null, - }) - - assertKeyValueTable('eventDetails', { - 'Type of event': 'Exhibition', - 'Event date': '1 January 2021', - 'Event location type': 'HQ', - Address: - 'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China', - Region: 'Not set', - Notes: 'This is a dummy event for testing.', - 'Lead team': 'CBBC Hangzhou', - Organiser: 'John Rogers', - 'Other teams': 'CBBC HangzhouCBBC North West', - 'Related programmes': 'Grown in Britain', + assertSummaryTableStrict({ + rows: { + ...EVENT_DETAILS_ROWS, 'Related Trade Agreements': 'Not set', - Service: 'Events : UK based', - }) + }, }) - it('should hide edit event button for disabled events', () => { - cy.get(selectors.entityCollection.editEvent).should('not.exist') - }) + cy.contains('a', 'Edit event').should('not.exist') }) - describe('Certain fields that are null', () => { - before(() => { - cy.intercept( - 'GET', - `/api-proxy/v4/event/${fixtures.event.teddyBearExpo.id}`, - { - address_1: '16 Grande Parade', - address_2: '', - address_country: { - name: 'China', - id: '63af72a6-5d95-e211-a939-e4115bead28a', - }, - address_county: '', - address_postcode: '', - address_town: 'Shanghai', - end_date: '2016-03-16', - event_type: { - name: 'Exhibition', - id: '2fade471-e868-4ea9-b125-945eb90ae5d4', - }, - lead_team: { - name: 'UK Fashion and Textile Association Ltd (UKFT)', - id: '23f12898-9698-e211-a939-e4115bead28a', - }, - name: 'Shanghai Fashion Week including The Hub, Chic-Pure-Intertextile March 2016', - notes: null, - organiser: null, - start_date: '2016-03-16', - service: { - name: 'DBT export service or funding : UK Tradeshow Programme (UKTP) – exhibitor', - id: '380bba2b-3499-e211-a939-e4115bead28a', - }, - uk_region: null, - } - ).as('apiRequest') - cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id)) - cy.wait('@apiRequest') - }) + it('should set fields that are null to "Not set"', () => { + cy.intercept( + 'GET', + `/api-proxy/v4/event/${fixtures.event.teddyBearExpo.id}`, + { + address_1: '16 Grande Parade', + address_2: '', + address_country: { + name: 'China', + id: '63af72a6-5d95-e211-a939-e4115bead28a', + }, + address_county: '', + address_postcode: '', + address_town: 'Shanghai', + end_date: '2016-03-16', + event_type: { + name: 'Exhibition', + id: '2fade471-e868-4ea9-b125-945eb90ae5d4', + }, + lead_team: { + name: 'UK Fashion and Textile Association Ltd (UKFT)', + id: '23f12898-9698-e211-a939-e4115bead28a', + }, + name: 'Shanghai Fashion Week including The Hub, Chic-Pure-Intertextile March 2016', + notes: null, + organiser: null, + start_date: '2016-03-16', + service: { + name: 'DBT export service or funding : UK Tradeshow Programme (UKTP) – exhibitor', + id: '380bba2b-3499-e211-a939-e4115bead28a', + }, + uk_region: null, + } + ) + + cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id)) - it('should set fields that are null to "Not set"', () => { - assertKeyValueTable('eventDetails', { - 'Type of event': 'Exhibition', + assertSummaryTableStrict({ + rows: { + ...EVENT_DETAILS_ROWS, 'Event date': '16 March 2016', 'Event location type': 'Not set', Address: '16 Grande Parade, Shanghai, China', @@ -148,7 +127,7 @@ describe('Event Details', () => { 'Related Trade Agreements': 'Not set', Service: 'DBT export service or funding : UK Tradeshow Programme (UKTP) – exhibitor', - }) + }, }) }) }) diff --git a/test/functional/cypress/support/assertions.js b/test/functional/cypress/support/assertions.js index 6ea7c95dfb5..97057aec34d 100644 --- a/test/functional/cypress/support/assertions.js +++ b/test/functional/cypress/support/assertions.js @@ -76,13 +76,15 @@ const assertSummaryTable = ({ } /** - * @param {{rows: [string, string | number][], caption?: string}} options + * @param {{rows: [string, string | number][] | Record<string, string>, caption?: string}} options */ const assertSummaryTableStrict = ({ caption, rows }) => { + const _rows = Array.isArray(rows) ? rows : Object.entries(rows) + const assertRows = (el) => { - cy.wrap(el).find('tr').as('rows').should('have.length', rows.length) + cy.wrap(el).find('tr').as('rows').should('have.length', _rows.length) - rows.forEach(([key, val], i) => { + _rows.forEach(([key, val], i) => { cy.get('@rows') .eq(i) .within(() => { diff --git a/test/functional/cypress/support/utils.js b/test/functional/cypress/support/utils.js deleted file mode 100644 index be6451a1257..00000000000 --- a/test/functional/cypress/support/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - randomString: () => { - return Math.random().toString(36).substring(7) - }, -} diff --git a/test/selectors/entity-collection.js b/test/selectors/entity-collection.js index b046069ac36..98f03103776 100644 --- a/test/selectors/entity-collection.js +++ b/test/selectors/entity-collection.js @@ -1,6 +1,4 @@ module.exports = { - addAttendee: '[data-test="Add attendee"]', - editEvent: 'a:contains("Edit event")', addProposition: '[data-test="add-collection-item-button"]', collection: '.c-collection', sortBy: '[name="sortBy"] > select',