diff --git a/src/client/components/Form/elements/FieldInvestmentProjectTypeahead/index.jsx b/src/client/components/Form/elements/FieldInvestmentProjectTypeahead/index.jsx new file mode 100644 index 00000000000..5abc2e0fe75 --- /dev/null +++ b/src/client/components/Form/elements/FieldInvestmentProjectTypeahead/index.jsx @@ -0,0 +1,54 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { throttle } from 'lodash' + +import FieldTypeahead from '../FieldTypeahead' +import { apiProxyAxios } from '../../../Task/utils' + +const FieldInvestmentProjectTypeahead = ({ + name, + label, + required, + placeholder = 'Type to search for investment projects', + company = null, + ...props +}) => { + return ( + + apiProxyAxios + .get('/v3/investment', { + params: { + autocomplete: searchString, + investor_company_id: company, + }, + }) + .then(({ data: { results } }) => + results.map(({ id, name }) => ({ + label: name, + chipLabel: name, + value: id, + })) + ), + 500 + )} + {...props} + /> + ) +} + +FieldInvestmentProjectTypeahead.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + required: PropTypes.string, + isMulti: PropTypes.bool, + placeholder: PropTypes.string, +} + +export default FieldInvestmentProjectTypeahead diff --git a/src/client/components/Form/elements/__stories__/FieldInvestmentProjectTypeahead.stories.jsx b/src/client/components/Form/elements/__stories__/FieldInvestmentProjectTypeahead.stories.jsx new file mode 100644 index 00000000000..e74a6c0299b --- /dev/null +++ b/src/client/components/Form/elements/__stories__/FieldInvestmentProjectTypeahead.stories.jsx @@ -0,0 +1,53 @@ +import React from 'react' + +import FieldInvestmentProjectTypeahead from '../FieldInvestmentProjectTypeahead' +import Form from '../../../Form' + +const options = [ + { + value: '379f390a-e083-4a2c-9cea-e3b9a08606a7', + label: 'Project A', + }, + { + value: '8dcd2bb8-dc73-4a42-8655-4ae42d4d3c5a', + label: 'Project B', + }, +] + +export const mockLoadOptions = (query = '') => + new Promise((resolve) => + query && query.length + ? setTimeout( + resolve, + 200, + options.filter(({ label }) => + label.toLowerCase().includes(query.toLowerCase()) + ) + ) + : resolve([]) + ) + +export default { + title: 'Form/Form Elements/FieldInvestmentProjectTypeahead', + excludeStories: ['mockLoadOptions'], + parameters: { + component: FieldInvestmentProjectTypeahead, + }, +} + +export const Default = () => ( +
+ {() => ( + <> + + + )} + +) diff --git a/src/client/components/index.jsx b/src/client/components/index.jsx index e0d281711b7..324c32316bb 100644 --- a/src/client/components/index.jsx +++ b/src/client/components/index.jsx @@ -94,3 +94,4 @@ export { default as ContactLayout } from './Layout/ContactLayout' export { default as FieldCurrency } from './Form/elements/FieldCurrency' export { default as Wrap } from './Wrap' export { default as FieldCompaniesTypeahead } from './Form/elements/FieldCompaniesTypeahead' +export { default as FieldInvestmentProjectTypeahead } from './Form/elements/FieldInvestmentProjectTypeahead' diff --git a/src/client/modules/Tasks/TaskForm/TaskFormFields.jsx b/src/client/modules/Tasks/TaskForm/TaskFormFields.jsx index 9b12b554ae0..6cfe80aecfe 100644 --- a/src/client/modules/Tasks/TaskForm/TaskFormFields.jsx +++ b/src/client/modules/Tasks/TaskForm/TaskFormFields.jsx @@ -12,6 +12,7 @@ import { FieldAdvisersTypeahead, NewWindowLink, FieldCompaniesTypeahead, + FieldInvestmentProjectTypeahead, } from '../../../components' import { validateDaysRange, validateIfDateInFuture } from './validators' @@ -52,8 +53,7 @@ const TaskFormFields = ({ redirectTo={() => redirectToUrl} submissionTaskName={TASK_SAVE_TASK_DETAILS} transformPayload={(values) => ({ - //hidden fields do not get added to the values, include investment project here for now until it gets converted into a FieldSelect in future PRs - values: { ...values, investmentProject: task?.investmentProject }, + values: values, currentAdviserId, taskId: values.id, })} @@ -62,7 +62,7 @@ const TaskFormFields = ({ submitButtonLabel="Save task" cancelButtonLabel="Back" > - {() => ( + {({ values }) => ( <> - {!task?.investmentProject && ( - + {(task?.company || values.company) && ( + )} - )} diff --git a/src/client/modules/Tasks/TaskForm/state.js b/src/client/modules/Tasks/TaskForm/state.js index d629c4c4d02..5b3650c8d5a 100644 --- a/src/client/modules/Tasks/TaskForm/state.js +++ b/src/client/modules/Tasks/TaskForm/state.js @@ -87,8 +87,14 @@ export const state2props = (state) => { if (project) { const transformedProject = transformIdNameToValueLabel(project) + const transformedCompany = transformIdNameToValueLabel( + project.investorCompany + ) return { - task: { investmentProject: transformedProject }, + task: { + investmentProject: transformedProject, + company: transformedCompany, + }, currentAdviserId, breadcrumbs: getInvestmentProjectBreadcumbs(transformedProject), } diff --git a/test/component/cypress/specs/Tasks/TaskForm/TaskFormFields.cy.jsx b/test/component/cypress/specs/Tasks/TaskForm/TaskFormFields.cy.jsx index 95b4462b7f8..1c87d3d27ae 100644 --- a/test/component/cypress/specs/Tasks/TaskForm/TaskFormFields.cy.jsx +++ b/test/component/cypress/specs/Tasks/TaskForm/TaskFormFields.cy.jsx @@ -7,13 +7,20 @@ import { assertFieldError, assertFieldInput, assertFieldRadiosWithLegend, + assertFieldSingleTypeahead, assertFieldTextarea, assertLink, + assertNotExists, + assertVisible, } from '../../../../../functional/cypress/support/assertions' import { clickButton } from '../../../../../functional/cypress/support/actions' import urls from '../../../../../../src/lib/urls' import TaskFormFields from '../../../../../../src/client/modules/Tasks/TaskForm/TaskFormFields' -import { taskWithInvestmentProjectFaker } from '../../../../../functional/cypress/fakers/task' +import { + taskFaker, + taskWithCompanyFaker, + taskWithInvestmentProjectFaker, +} from '../../../../../functional/cypress/fakers/task' import { transformAPIValuesForForm } from '../../../../../../src/client/modules/Tasks/TaskForm/transformers' import advisersListFaker, { adviserFaker, @@ -82,311 +89,390 @@ describe('Task form', () => { }) }) + it('should display the company typeahead field', () => { + assertVisible('[data-test="field-company"]') + }) + + it('should hide the investment project typeahead field', () => { + assertNotExists('[data-test="field-investmentProject"') + }) + it('should render the cancel button with the correct url', () => { assertLink('cancel-button', urls.companies.index()) }) }) - context('When a task form renders with existing data', () => { - const investmentProjectTask = taskWithInvestmentProjectFaker({}) + describe('Task form validation', () => { + context('When a task is missing all mandatory fields', () => { + beforeEach(() => { + cy.mount() + clickButton('Save task') + }) - beforeEach(() => { - cy.mount( - - ) - }) + it('should display an error when for each mandatory field', () => { + assertFieldError( + cy.get('[data-test="field-title"]'), + 'Enter a task title', + false + ) - it('should display the task title field', () => { - cy.dataTest('field-title').then((element) => { - assertFieldInput({ - element, - label: 'Task title', - value: investmentProjectTask.title, - }) + assertFieldError( + cy.get('[data-test="field-assignedTo"]'), + 'Select who this task is assigned to' + ) + + assertFieldError( + cy.get('[data-test="field-dueDate"]'), + 'Select task due date' + ) + + assertFieldError( + cy.get('[data-test="field-emailRemindersEnabled"]'), + 'Select reminder' + ) }) }) - it('should display the task description field', () => { - cy.get('[data-test="field-description"]').then((element) => { - assertFieldTextarea({ - element, - label: 'Task description (optional)', - hint: 'Add details of the task, especially if you intend to assign it to someone else.', - value: investmentProjectTask.description, - }) + context('When creating a task assigned to someone else', () => { + beforeEach(() => { + cy.mount() + + cy.get('[data-test=assigned-to-someone-else]').click() }) - }) - it('should display the custom date', () => { - cy.get('[data-test="field-customDate"]').then((element) => { - assertFieldDate({ - element, - label: 'For example 28 11 2025', - value: convertDateToFieldDateObject(investmentProjectTask.dueDate), - }) + it('should display an error when no advisers are selected', () => { + clickButton('Save task') + + cy.get('[data-test="field-advisers"]').should( + 'contain.text', + 'Select an adviser' + ) }) }) - it('should display the task due date field radios', () => { - cy.get('[data-test="field-dueDate"]').then((element) => { - assertFieldRadiosWithLegend({ - element, - legend: 'Task due date', - optionsCount: 7, - }) + context('When a task is created a task with a custom date', () => { + beforeEach(() => { + cy.mount() + cy.get('[data-test=due-date-custom-date]').click() }) - }) - it('should display the task reminder field radios', () => { - cy.get('[data-test="field-emailRemindersEnabled"]').then((element) => { - assertFieldRadiosWithLegend({ - element, - legend: 'Do you want to set a reminder for this task?', - optionsCount: 3, - value: investmentProjectTask.emailRemindersEnabled - ? capitalize(OPTION_YES) - : capitalize(OPTION_NO), - }) + it('should display an error when no custom date is entered', () => { + clickButton('Save task') + + cy.get('[data-test="field-customDate"]').should( + 'contain.text', + 'Enter a date' + ) + }) + + it('should display an error when invalid date is entered', () => { + cy.get('[data-test=custom_date-day]').type(50) + cy.get('[data-test=custom_date-month]').type(50) + cy.get('[data-test=custom_date-year]').type(50) + + clickButton('Save task') + + cy.get('[data-test="field-customDate"]').should( + 'contain.text', + 'Enter a valid date' + ) + }) + + it('should display an error when date in the past is entered', () => { + cy.get('[data-test=custom_date-day]').clear().type(1) + cy.get('[data-test=custom_date-month]').clear().type(1) + cy.get('[data-test=custom_date-year]').clear().type(2000) + + clickButton('Save task') + + cy.get('[data-test="field-customDate"]').should( + 'contain.text', + 'Enter a date in the future' + ) }) }) - it('should render the cancel button with the correct url', () => { - assertLink('cancel-button', urls.companies.index()) + context('When creating a task with reminders', () => { + beforeEach(() => { + cy.mount() + + cy.get('[data-test=field-emailRemindersEnabled]').click() + }) + + it('should display an error when no task reminder days are entered', () => { + clickButton('Save task') + + cy.get('[data-test="field-emailRemindersEnabled"]').should( + 'contain.text', + 'Enter a number between 1 and 365' + ) + }) + + it('should display an error when 0 is entered', () => { + cy.get('[data-test=reminder-days-input]').type(0) + clickButton('Save task') + + cy.get('[data-test="field-emailRemindersEnabled"]').should( + 'contain.text', + 'Enter a number between 1 and 365' + ) + }) + + it('should display an error when day higher than 365 is entered', () => { + cy.get('[data-test=reminder-days-input]').type(366) + clickButton('Save task') + + cy.get('[data-test="field-emailRemindersEnabled"]').should( + 'contain.text', + 'Enter a number between 1 and 365' + ) + }) }) }) - context( - 'When a task form renders with existing data that is assigned to me', - () => { - const currentAdviser = adviserFaker() - const investmentProjectTask = taskWithInvestmentProjectFaker({ - advisers: [currentAdviser], - }) + describe('Editing a task form', () => { + context('When a generic task form renders with existing data', () => { + const task = taskFaker() beforeEach(() => { cy.mount( ) }) - it('should display the task assigned to field radios with me selected', () => { - cy.get('[data-test="field-assignedTo"]').then((element) => { + it('should display the task title field', () => { + cy.dataTest('field-title').then((element) => { + assertFieldInput({ + element, + label: 'Task title', + value: task.title, + }) + }) + }) + + it('should display the task description field', () => { + cy.get('[data-test="field-description"]').then((element) => { + assertFieldTextarea({ + element, + label: 'Task description (optional)', + hint: 'Add details of the task, especially if you intend to assign it to someone else.', + value: task.description, + }) + }) + }) + + it('should display the custom date', () => { + cy.get('[data-test="field-customDate"]').then((element) => { + assertFieldDate({ + element, + label: 'For example 28 11 2025', + value: convertDateToFieldDateObject(task.dueDate), + }) + }) + }) + + it('should display the task due date field radios', () => { + cy.get('[data-test="field-dueDate"]').then((element) => { assertFieldRadiosWithLegend({ element, - legend: 'Task assigned to', - optionsCount: 2, - value: 'Me', + legend: 'Task due date', + optionsCount: 7, }) }) }) - } - ) - context( - 'When a task form renders with existing data that is assigned to me and others', - () => { - const currentAdviser = adviserFaker() - const otherAdviser = adviserFaker() - const investmentProjectTask = taskWithInvestmentProjectFaker({ - advisers: [currentAdviser, otherAdviser], + it('should display the task reminder field radios', () => { + cy.get('[data-test="field-emailRemindersEnabled"]').then((element) => { + assertFieldRadiosWithLegend({ + element, + legend: 'Do you want to set a reminder for this task?', + optionsCount: 3, + value: task.emailRemindersEnabled + ? capitalize(OPTION_YES) + : capitalize(OPTION_NO), + }) + }) + }) + + it('should render the cancel button with the correct url', () => { + assertLink('cancel-button', urls.companies.index()) }) + }) + + context('When a company task form renders with existing data', () => { + const task = taskWithCompanyFaker() beforeEach(() => { cy.mount( ) }) - it('should display the task assigned to field radios with me selected', () => { - cy.get('[data-test="field-assignedTo"]').then((element) => { - assertFieldRadiosWithLegend({ + it('should display the company typeahead with selected value', () => { + cy.get('[data-test="field-company"]').then((element) => { + assertFieldSingleTypeahead({ element, - legend: 'Task assigned to', - optionsCount: 3, - value: 'Someone else', + label: 'Company name (optional)', + value: task.company.name, + placeholder: '', }) }) }) - } - ) - context( - 'When a task form renders with existing data that is assigned to someone else', - () => { - var adviserAssignedToTestRuns = [ - { - advisers: advisersListFaker(), - }, - { - advisers: advisersListFaker((length = 2)), - }, - ] - - adviserAssignedToTestRuns.forEach(function (run) { + it('should display the investment project typeahead without a selected value', () => { + cy.get('[data-test="field-investmentProject"]').then((element) => { + assertFieldSingleTypeahead({ + element, + label: 'Investment project (optional)', + value: '', + placeholder: 'Type to search for investment projects', + }) + }) + }) + }) + + context( + 'When an investment project task form renders with existing data', + () => { + const task = taskWithInvestmentProjectFaker() + beforeEach(() => { - const investmentProjectTask = taskWithInvestmentProjectFaker({ - advisers: run.advisers, + cy.mount( + + ) + }) + + it('should display the company typeahead with selected value', () => { + cy.get('[data-test="field-company"]').then((element) => { + assertFieldSingleTypeahead({ + element, + label: 'Company name (optional)', + value: task.company.name, + placeholder: '', + }) }) + }) + it('should display the investment project typeahead with selected value', () => { + cy.get('[data-test="field-investmentProject"]').then((element) => { + assertFieldSingleTypeahead({ + element, + label: 'Investment project (optional)', + value: task.investmentProject.name, + placeholder: '', + }) + }) + }) + } + ) + + context( + 'When a task form renders with existing data that is assigned to me', + () => { + const currentAdviser = adviserFaker() + const task = taskFaker({ + advisers: [currentAdviser], + }) + + beforeEach(() => { cy.mount( ) }) - it('should display the task assigned to field radios with someone else selected', () => { + + it('should display the task assigned to field radios with me selected', () => { cy.get('[data-test="field-assignedTo"]').then((element) => { assertFieldRadiosWithLegend({ element, legend: 'Task assigned to', - optionsCount: 3, - value: 'Someone else', + optionsCount: 2, + value: 'Me', }) }) }) - }) - } - ) - - context('When a task is missing all mandatory fields', () => { - beforeEach(() => { - cy.mount() - clickButton('Save task') - }) - - it('should display an error when for each mandatory field', () => { - assertFieldError( - cy.get('[data-test="field-title"]'), - 'Enter a task title', - false - ) - - assertFieldError( - cy.get('[data-test="field-assignedTo"]'), - 'Select who this task is assigned to' - ) - - assertFieldError( - cy.get('[data-test="field-dueDate"]'), - 'Select task due date' - ) - - assertFieldError( - cy.get('[data-test="field-emailRemindersEnabled"]'), - 'Select reminder' - ) - }) - }) - - context('When creating a task assigned to someone else', () => { - beforeEach(() => { - cy.mount() - - cy.get('[data-test=assigned-to-someone-else]').click() - }) - - it('should display an error when no advisers are selected', () => { - clickButton('Save task') - - cy.get('[data-test="field-advisers"]').should( - 'contain.text', - 'Select an adviser' - ) - }) - }) - - context('When a task is created a task with a custom date', () => { - beforeEach(() => { - cy.mount() - cy.get('[data-test=due-date-custom-date]').click() - }) - - it('should display an error when no custom date is entered', () => { - clickButton('Save task') - - cy.get('[data-test="field-customDate"]').should( - 'contain.text', - 'Enter a date' - ) - }) - - it('should display an error when invalid date is entered', () => { - cy.get('[data-test=custom_date-day]').type(50) - cy.get('[data-test=custom_date-month]').type(50) - cy.get('[data-test=custom_date-year]').type(50) - - clickButton('Save task') - - cy.get('[data-test="field-customDate"]').should( - 'contain.text', - 'Enter a valid date' - ) - }) - - it('should display an error when date in the past is entered', () => { - cy.get('[data-test=custom_date-day]').clear().type(1) - cy.get('[data-test=custom_date-month]').clear().type(1) - cy.get('[data-test=custom_date-year]').clear().type(2000) - - clickButton('Save task') - - cy.get('[data-test="field-customDate"]').should( - 'contain.text', - 'Enter a date in the future' - ) - }) - }) - - context('When creating a task with task reminders', () => { - beforeEach(() => { - cy.mount() - - cy.get('[data-test=field-emailRemindersEnabled]').click() - }) - - it('should display an error when no task reminder days are entered', () => { - clickButton('Save task') - - cy.get('[data-test="field-emailRemindersEnabled"]').should( - 'contain.text', - 'Enter a number between 1 and 365' - ) - }) - - it('should display an error when 0 is entered', () => { - cy.get('[data-test=reminder-days-input]').type(0) - clickButton('Save task') + } + ) + + context( + 'When a task form renders with existing data that is assigned to me and others', + () => { + const currentAdviser = adviserFaker() + const otherAdviser = adviserFaker() + const task = taskFaker({ + advisers: [currentAdviser, otherAdviser], + }) - cy.get('[data-test="field-emailRemindersEnabled"]').should( - 'contain.text', - 'Enter a number between 1 and 365' - ) - }) + beforeEach(() => { + cy.mount( + + ) + }) - it('should display an error when day higher than 365 is entered', () => { - cy.get('[data-test=reminder-days-input]').type(366) - clickButton('Save task') + it('should display the task assigned to field radios with me selected', () => { + cy.get('[data-test="field-assignedTo"]').then((element) => { + assertFieldRadiosWithLegend({ + element, + legend: 'Task assigned to', + optionsCount: 3, + value: 'Someone else', + }) + }) + }) + } + ) + + context( + 'When a task form renders with existing data that is assigned to someone else', + () => { + var adviserAssignedToTestRuns = [ + { + advisers: advisersListFaker(), + }, + { + advisers: advisersListFaker((length = 2)), + }, + ] + + adviserAssignedToTestRuns.forEach(function (run) { + beforeEach(() => { + const task = taskFaker({ + advisers: run.advisers, + }) - cy.get('[data-test="field-emailRemindersEnabled"]').should( - 'contain.text', - 'Enter a number between 1 and 365' - ) - }) + cy.mount( + + ) + }) + it('should display the task assigned to field radios with someone else selected', () => { + cy.get('[data-test="field-assignedTo"]').then((element) => { + assertFieldRadiosWithLegend({ + element, + legend: 'Task assigned to', + optionsCount: 3, + value: 'Someone else', + }) + }) + }) + }) + } + ) }) }) diff --git a/test/functional/cypress/specs/tasks/add-task-spec.js b/test/functional/cypress/specs/tasks/add-task-spec.js index 0f768106647..70caf697e17 100644 --- a/test/functional/cypress/specs/tasks/add-task-spec.js +++ b/test/functional/cypress/specs/tasks/add-task-spec.js @@ -8,6 +8,8 @@ import { assertPayload, assertFlashMessage, assertExactUrl, + assertSingleTypeaheadOptionSelected, + assertVisible, } from '../../support/assertions' import { fill, @@ -29,6 +31,17 @@ describe('Add generic task', () => { it('should display the header', () => { cy.get('h1').should('have.text', 'Add task') }) + + context('When a company is selected', () => { + const company = companyFaker() + + it('should display the investment project typeahead field', () => { + cy.intercept(`/api-proxy/v4/company?*`, { results: [company] }) + fillTypeahead('[data-test=field-company]', company.name) + + assertVisible('[data-test="field-investmentProject"]') + }) + }) }) context('When creating a task for me', () => { @@ -83,6 +96,20 @@ describe('Add investment project task', () => { cy.get('h1').should('have.text', `Add task for ${fixture.name}`) }) + it('should display the company typeahead with the value matching the investment project company', () => { + assertSingleTypeaheadOptionSelected({ + element: '[data-test="field-company"]', + expectedOption: fixture.investor_company.name, + }) + }) + + it('should display the investment project typeahead with the select value', () => { + assertSingleTypeaheadOptionSelected({ + element: '[data-test="field-investmentProject"]', + expectedOption: fixture.name, + }) + }) + it('add task button should send expected values to the api', () => { cy.get('[data-test=assigned-to-me]').click() diff --git a/test/functional/cypress/specs/tasks/edit-task-spec.js b/test/functional/cypress/specs/tasks/edit-task-spec.js index c3bf6ecc49f..a07251e1b45 100644 --- a/test/functional/cypress/specs/tasks/edit-task-spec.js +++ b/test/functional/cypress/specs/tasks/edit-task-spec.js @@ -78,6 +78,14 @@ describe('Edit investment project task', () => { ) }) + it('should display the company typeahead with the value matching the investment project company', () => { + assertSingleTypeaheadOptionSelected({ + element: '[data-test="field-company"]', + expectedOption: + investmentProjectTask.investmentProject.investorCompany.name, + }) + }) + it('changing field values should send new values to the api', () => { cy.intercept('PATCH', endpoint, { statusCode: 200,