diff --git a/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.global.css b/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.global.css index 11e202499..124c98c3b 100644 --- a/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.global.css +++ b/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.global.css @@ -33,8 +33,9 @@ .ticket-reply-submission { display: flex; + flex-direction: column; justify-content: right; - align-items: center; + align-items: end; } .ticket-model-content { diff --git a/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.test.tsx b/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.test.tsx index c3ddede69..3057581d3 100644 --- a/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.test.tsx +++ b/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.test.tsx @@ -9,6 +9,9 @@ import { vi } from 'vitest'; window.HTMLElement.prototype.scrollIntoView = vi.fn(); +const resolvedStatus = 'resolved'; +const unresolvedStatus = ''; + describe('Ticket Modal', () => { it('should render ticket history information and reply form', async () => { const { getByText, getAllByText, getByTestId } = testRender( @@ -31,7 +34,7 @@ describe('Ticket Modal', () => { ) ); const { getByLabelText, getByRole } = testRender( - + ); const reply = getByLabelText(/Reply/); await user.type(reply, 'it works!'); @@ -56,7 +59,7 @@ describe('Ticket Modal', () => { ) ); const { getByLabelText, getByText, getByRole } = testRender( - + ); const reply = getByLabelText(/Reply/); @@ -68,25 +71,142 @@ describe('Ticket Modal', () => { expect(getByText('Something went wrong.')).toBeDefined() ); }); -}); -it('should render an success message if an reply success is returned from the useMutation hook', async () => { - const user = userEvent.setup(); - server.use( - rest.post('http://localhost:8001/tickets/85411/reply', (req, res, ctx) => - res.once(ctx.status(200)) - ) - ); - const { getByLabelText, getByText, getByRole } = testRender( - - ); - - const reply = getByLabelText(/Reply/); - const submit = getByRole('button', { name: 'Reply' }); - await user.type(reply, 'success message?'); - fireEvent.click(submit); - - await waitFor(() => - expect(getByText(/Your reply has been sent/)).toBeDefined() - ); + it('should render an success message if an reply success is returned from the useMutation hook', async () => { + const user = userEvent.setup(); + server.use( + rest.post('http://localhost:8001/tickets/85411/reply', (req, res, ctx) => + res.once(ctx.status(200)) + ) + ); + const { getByLabelText, getByText, getByRole } = testRender( + + ); + + const reply = getByLabelText(/Reply/); + const submit = getByRole('button', { name: 'Reply' }); + await user.type(reply, 'success message?'); + fireEvent.click(submit); + + await waitFor(() => + expect(getByText(/Your reply has been sent/)).toBeDefined() + ); + }); + + it('should disable the reply button on load and enable it when reply is entered', async () => { + const user = userEvent.setup(); + server.use( + rest.post('http://localhost:8001/tickets/85411/reply', (req, res, ctx) => + res.once(ctx.status(200)) + ) + ); + + const { getByLabelText, getByRole } = testRender( + + ); + + //If reply is empty, the button should be disabled + const replyButton = getByRole('button', { + name: 'Reply', + }) as HTMLButtonElement; + expect(replyButton.disabled).toBe(true); + + //If reply is filled, button should be enabled + const reply = getByLabelText(/Reply/); + await user.type(reply, 'reply disabled?'); + expect(replyButton.disabled).toBe(false); + + //Changing back to empty reply + await user.clear(reply); + expect(replyButton.disabled).toBe(true); + }); + + it('should disable the reply button on load and enable it when checkbox is checked', async () => { + server.use( + rest.post('http://localhost:8001/tickets/85411/reply', (req, res, ctx) => + res.once(ctx.status(200)) + ) + ); + + const { getByRole } = testRender( + + ); + + //If reply is empty, the button should be disabled + const replyButton = getByRole('button', { + name: 'Reply', + }) as HTMLButtonElement; + expect(replyButton.disabled).toBe(true); + + //If checkbox is checked, button should be enabled + const checkbox = getByRole('checkbox', { + name: /My issue has been resolved/i, + }) as HTMLInputElement; + waitFor(() => { + fireEvent.click(checkbox); + }); + + expect(replyButton.disabled).toBe(false); + + waitFor(() => { + fireEvent.click(checkbox); + }); + + expect(replyButton.disabled).toBe(true); + }); + + it('should remove required reply when checkbox is checked', () => { + server.use( + rest.post('http://localhost:8001/tickets/85411/reply', (req, res, ctx) => + res.once(ctx.status(200)) + ) + ); + + const { getByRole, queryByText, getByText } = testRender( + + ); + + // The required label needs to be present + // Will result in error if not present - hence getbytext + getByText(/required/i); + + //If checkbox is checked, button should be enabled + waitFor(() => { + const checkbox = getByRole('checkbox', { + name: /My issue has been resolved/i, + }) as HTMLInputElement; + fireEvent.click(checkbox); + }); + + // The required label should not be there - hence querybytext + const requiredLabelAfterClick = queryByText(/required/i); + expect(requiredLabelAfterClick).toBeNull(); + }); + + it('should check the checkbox and disable it if status is resolved - replying will reopen the ticket should appear', async () => { + server.use( + rest.post('http://localhost:8001/tickets/85411/reply', (req, res, ctx) => + res.once(ctx.status(200)) + ) + ); + + const { getByRole, getByText } = testRender( + //If ticket were not resolved, this test will fail + + ); + + await waitFor(() => { + const checkbox = getByRole('checkbox', { + name: /My issue has been resolved/i, + }) as HTMLInputElement; + + // Expect the checkbox to be checked + expect(checkbox.checked).toBe(true); + + // Expect the checkbox to be disabled + expect(checkbox.disabled).toBe(true); + }); + + getByText(/replying will reopen this ticket/i); + }); }); diff --git a/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.tsx b/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.tsx index 8bae9dbb7..97daba451 100644 --- a/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.tsx +++ b/libs/tup-components/src/tickets/TicketDetailModal/TicketModal.tsx @@ -46,7 +46,10 @@ const TicketModal: React.FC<{ ticketId: string; baseRoute: string }> = ({ - + diff --git a/libs/tup-components/src/tickets/TicketDetailModal/TicketReplyForm.tsx b/libs/tup-components/src/tickets/TicketDetailModal/TicketReplyForm.tsx index 22dfdf9ab..7ccd5a88a 100644 --- a/libs/tup-components/src/tickets/TicketDetailModal/TicketReplyForm.tsx +++ b/libs/tup-components/src/tickets/TicketDetailModal/TicketReplyForm.tsx @@ -1,15 +1,10 @@ import React from 'react'; -import { Formik, Form, FormikHelpers } from 'formik'; -import { - FormikFileInput, - FormikTextarea, - FormikSelect, -} from '@tacc/core-wrappers'; +import { Formik, Form, FormikHelpers, Field } from 'formik'; +import { FormikFileInput, FormikTextarea } from '@tacc/core-wrappers'; import { FormGroup } from 'reactstrap'; import { Button, InlineMessage } from '@tacc/core-components'; import { useTicketReply } from '@tacc/tup-hooks'; -import * as Yup from 'yup'; import './TicketModal.global.css'; interface TicketReplyFormValues { @@ -18,13 +13,20 @@ interface TicketReplyFormValues { status: string; } +/* This validates the form for the first textarea in the form. +This will also show the red required text underneath the textarea. +If this is implemented, you must also add the `validationSchema={formSchema}` attribute to the Formik component. +*** const formSchema = Yup.object().shape({ - text: Yup.string().required('Required'), + text: Yup.string().required('Required'), }); +*** +*/ -export const TicketReplyForm: React.FC<{ ticketId: string }> = ({ - ticketId, -}) => { +export const TicketReplyForm: React.FC<{ + ticketId: string; + ticketStatus: string; +}> = ({ ticketId, ticketStatus }) => { const mutation = useTicketReply(ticketId); const { mutate, isSuccess, isLoading, isError } = mutation; @@ -39,9 +41,24 @@ export const TicketReplyForm: React.FC<{ ticketId: string }> = ({ { resetForm }: FormikHelpers ) => { const formData = new FormData(); - formData.append('text', values['text']); + const isReplyEmpty = values.text.length === 0; + + //if reply is empty and no checked box return + if (isReplyEmpty && !values.status) { + return; + } + + if (isReplyEmpty) { + formData.append('text', '(Resolved with no reply.)'); + } else { + formData.append('text', values['text']); + } + (values.files || []).forEach((file) => formData.append('files', file)); - if (values.status) formData.append('status', values.status); + values.status + ? formData.append('status', 'resolved') + : formData.append('status', ''); + mutate(formData, { onSuccess: () => resetForm(), onSettled: () => { @@ -55,11 +72,16 @@ export const TicketReplyForm: React.FC<{ ticketId: string }> = ({ return ( - {({ isSubmitting, isValid }) => { + {({ values }) => { + const isReplyEmpty = values.text.length === 0; + const isResolved = ticketStatus === 'resolved'; + const isChecked = values.status; + return (
= ({ label="Reply" description="" style={{ maxWidth: '100%' }} - required + required={!isChecked} /> - - - - = ({ maxSizeMessage="Max File Size: 3MB" maxSize={3145728} /> - +
+ + +
  • + +
  • +
    +
    + {isResolved + ? '* Replying will reopen this ticket' + : 'This helps us determine which users still need assistance'} +
    +
    + {isSuccess && ( Your reply has been sent. @@ -94,14 +145,6 @@ export const TicketReplyForm: React.FC<{ ticketId: string }> = ({ Something went wrong. )} - );