Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MR-3573: Download all contracts button does not generate zip file on first click #1859

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
margin-left: .5rem
}

.buttonTextWithoutIcon {
vertical-align: middle;
}

.successButton {
background: $theme-color-success;
&:hover {
Expand Down
25 changes: 18 additions & 7 deletions services/app-web/src/components/ActionButton/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState, ComponentProps } from 'react'
import React, { useState, ComponentProps, useLayoutEffect } from 'react'
import { Button as UswdsButton } from '@trussworks/react-uswds'
import classnames from 'classnames'

Expand Down Expand Up @@ -26,8 +26,10 @@ export const ActionButton = ({
animationTimeout = 750,
onClick,
...inheritedProps
}: ActionButtonProps): React.ReactElement => {
const [showLoading, setShowLoading] = useState(false)
}: ActionButtonProps): React.ReactElement | null => {
const [showLoading, setShowLoading] = useState<boolean | undefined>(
undefined
)
const isDisabled = disabled || inheritedProps['aria-disabled']
const isLinkStyled = variant === 'linkStyle'
const isOutline = variant === 'outline'
Expand All @@ -39,14 +41,17 @@ export const ActionButton = ({
'CODING ERROR: Incompatible props on ActionButton are being used. Button should not be both loading and disabled at the same time.'
)

useEffect(() => {
if (loading) {
useLayoutEffect(() => {
// If there is no animationTimeout, do not use setTimeout else you get flickering UI.
if (animationTimeout > 0) {
const timeout = setTimeout(() => {
setShowLoading(true)
setShowLoading(loading)
}, animationTimeout)
return function cleanup() {
clearTimeout(timeout)
}
} else {
setShowLoading(loading)
}
}, [loading, animationTimeout])

Expand Down Expand Up @@ -87,7 +92,13 @@ export const ActionButton = ({
className={classes}
>
{showLoading && <Spinner size="small" />}
<span className={showLoading ? styles.buttonTextWithIcon : ''}>
<span
className={
showLoading
? styles.buttonTextWithIcon
: styles.buttonTextWithoutIcon
}
>
{showLoading ? 'Loading' : children}
</span>
</UswdsButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import styles from '../Banner.module.scss'
import { Alert } from '@trussworks/react-uswds'
import { useStringConstants } from '../../../hooks/useStringConstants'
import classnames from 'classnames'

export const DocumentWarningBanner = (): React.ReactElement => {
const stringConstants = useStringConstants()
const MAIL_TO_SUPPORT = stringConstants.MAIL_TO_SUPPORT
return (
<Alert
role="alert"
type="warning"
heading={`Document download unavailable`}
headingLevel="h4"
data-testid="warning-alert"
className={classnames(styles.bannerBodyText, 'usa-alert__text')}
>
<span>
Some documents aren’t available right now. Refresh the page to
try again. If you still see this message,&nbsp;
</span>
<a
href={`mailto: ${MAIL_TO_SUPPORT}, mc-review-team@truss.works`}
className="usa-link"
target="_blank"
rel="noreferrer"
>
email the help desk.
</a>
</Alert>
)
}
1 change: 1 addition & 0 deletions services/app-web/src/components/Banner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { SubmissionUpdatedBanner } from './SubmissionUpdatedBanner/SubmissionUpd
export { GenericApiErrorBanner } from './GenericApiErrorBanner/GenericApiErrorBanner'
export { QuestionResponseSubmitBanner } from './QuestionResponseSubmitBanner'
export { UserAccountWarningBanner } from './UserAccountWarningBanner/UserAccountWarningBanner'
export { DocumentWarningBanner } from './DocumentWarningBanner/DocumentWarningBanner'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import '../../../styles/uswdsImports.scss';
@import '../../../styles/custom.scss';

.missingInfo {
color: $theme-color-warning-darker;
font-weight: 700;
display: flex;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Icon as UswdsIcon } from '@trussworks/react-uswds'
import styles from './InlineDocumentWarning.module.scss'

interface USWDSIconProps {
focusable?: boolean
role?: string
size?: 3 | 4 | 5 | 6 | 7 | 8 | 9
className?: string
}

type IconProps = USWDSIconProps & React.JSX.IntrinsicElements['svg']

type IconType = keyof typeof UswdsIcon
export const InlineDocumentWarning = ({
message,
iconType,
}: {
message?: string
iconType?: IconType
}): React.ReactElement | null => {
const requiredFieldMissingText =
message || 'Document download is unavailable'
const type = iconType || 'Warning'
const Icon = UswdsIcon[type] as React.ComponentType<IconProps>

return (
<span className={styles.missingInfo}>
<span>
<Icon aria-label={`An ${type} icon`} size={3} />
</span>
<span>{requiredFieldMissingText}</span>
</span>
)
}
1 change: 1 addition & 0 deletions services/app-web/src/components/DocumentWarning/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InlineDocumentWarning } from './InlineDocumentWarning/InlineDocumentWarning'
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@import '../../styles/uswdsImports.scss';
@import '../../styles/custom.scss';

.disabledCursor {
cursor: not-allowed;
}

.buttonTextWithIcon {
vertical-align: middle;
margin-left: .5rem
}

.buttonTextWithoutIcon {
vertical-align: middle;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ describe('DownloadButton', () => {
zippedFilesURL="https://example.com"
/>
)
expect(screen.getByRole('link', {name: 'Download all documents'})).toHaveClass(
'usa-button usa-button--small'
)
expect(
screen.getByRole('link', { name: 'Download all documents' })
).toHaveClass('usa-button')
expect(screen.getByText('Download all documents')).toBeInTheDocument()
})
it('renders loading button', () => {
render(
<DownloadButton
text="Download all documents"
zippedFilesURL={undefined}
/>
)
expect(screen.getByText('Loading')).toBeInTheDocument()
})
})
40 changes: 29 additions & 11 deletions services/app-web/src/components/DownloadButton/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import React from 'react'
import React, { ComponentProps } from 'react'
import { Link } from '@trussworks/react-uswds'
import { Spinner } from '../Spinner'
import styles from '../ActionButton/ActionButton.module.scss'
import classnames from 'classnames'

type DownloadButtonProps = {
text: string
zippedFilesURL: string
}
zippedFilesURL: string | undefined
} & Partial<ComponentProps<typeof Link>>

export const DownloadButton = ({
text,
zippedFilesURL,
}: DownloadButtonProps): React.ReactElement => {
const classes = classnames(
{
'usa-button--active': !zippedFilesURL,
[styles.disabledCursor]: !zippedFilesURL,
},
'usa-button usa-button'
)

return (
<div>
<Link
className="usa-button usa-button--small"
variant="unstyled"
href={zippedFilesURL}
target="_blank"
>
{text}
</Link>
{zippedFilesURL ? (
<Link
className={classes}
variant="unstyled"
href={zippedFilesURL}
target="_blank"
>
<span className={styles.buttonTextWithoutIcon}>{text}</span>
</Link>
) : (
<div className={classes} aria-label="Loading">
<Spinner size="small" />
<span className={styles.buttonTextWithIcon}>Loading</span>
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mockStateSubmission,
} from '../../../testHelpers/apolloMocks'
import { UnlockedHealthPlanFormDataType } from '../../../common-code/healthPlanFormDataType'
import { testS3Client } from '../../../testHelpers/s3Helpers'

describe('ContractDetailsSummarySection', () => {
it('can render draft submission without errors (review and submit behavior)', async () => {
Expand Down Expand Up @@ -64,7 +65,7 @@ describe('ContractDetailsSummarySection', () => {
).toBeNull()
})

it('can render state submission on summary page without errors (submission summary behavior)', () => {
it('can render state submission on summary page without errors (submission summary behavior)', async () => {
renderWithProviders(
<ContractDetailsSummarySection
documentDateLookupTable={{ previousSubmissionDate: '01/01/01' }}
Expand All @@ -83,11 +84,18 @@ describe('ContractDetailsSummarySection', () => {
})
).toBeInTheDocument()
expect(screen.queryByText('Edit')).not.toBeInTheDocument()
expect(
screen.getByRole('link', {
name: 'Download all contract documents',
})
).toBeInTheDocument()

//expects loading button on component load
expect(screen.getByText('Loading')).toBeInTheDocument()

// expects download all button after loading has completed
await waitFor(() => {
expect(
screen.getByRole('link', {
name: 'Download all contract documents',
})
).toBeInTheDocument()
})
})

it('can render all contract details fields', () => {
Expand Down Expand Up @@ -336,6 +344,36 @@ describe('ContractDetailsSummarySection', () => {
await screen.queryByText('1937 Benchmark Authority')
).not.toBeInTheDocument()
})
it('renders inline error when bulk URL is unavailable', async () => {
const s3Provider = {
...testS3Client(),
getBulkDlURL: async (
keys: string[],
fileName: string
): Promise<string | Error> => {
return new Error('Error: getBulkDlURL encountered an error')
},
}
renderWithProviders(
<ContractDetailsSummarySection
documentDateLookupTable={{ previousSubmissionDate: '01/01/01' }}
submission={{
...mockStateSubmission(),
status: 'SUBMITTED',
}}
submissionName="MN-PMAP-0001"
/>,
{
s3Provider,
}
)

await waitFor(() => {
expect(
screen.getByText('Contract document download is unavailable')
).toBeInTheDocument()
})
})
describe('contract provisions', () => {
it('renders provisions and MLR references for a medicaid amendment', () => {
renderWithProviders(
Expand Down
Loading
Loading