Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 280 additions & 1 deletion src/api/api.generatedTypes.ts

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions src/components/dialogs/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
DialogRejectDelegatedVersionDraftProps,
DialogRevokeDelegationProps,
DialogTenantKindEserviceTemplateProps,
DialogTenantKindPurposeTemplateProps,
} from '@/types/dialog.types'
import { DialogRejectAgreement } from './DialogRejectAgreement'
import { ErrorBoundary } from '../shared/ErrorBoundary'
Expand All @@ -43,6 +44,7 @@ import { DialogRevokeDelegation } from './DialogRevokeDelegation'
import { DialogRejectDelegatedVersionDraft } from './DialogRejectDelegatedVersionDraft'
import { DialogCreateAgreementDraft } from './DialogCreateAgreementDraft/DialogCreateAgreementDraft'
import { DialogTenantKindEserviceTemplate } from './DialogTenantKindEserviceTemplate'
import { DialogTenantKindPurposeTemplate } from './DialogTenantKindPurposeTemplate'

function match<T>(
onBasic: (props: DialogBasicProps) => T,
Expand All @@ -63,7 +65,8 @@ function match<T>(
onCreateAgreementDraft: (props: DialogCreateAgreementDraftProps) => T,
onRevokeDelegation: (props: DialogRevokeDelegationProps) => T,
onRejectDelegatedVersionDraft: (props: DialogRejectDelegatedVersionDraftProps) => T,
onDialogTenantKindEserviceTemplate: (props: DialogTenantKindEserviceTemplateProps) => T
onDialogTenantKindEserviceTemplate: (props: DialogTenantKindEserviceTemplateProps) => T,
onDialogTenantKindPurposeTemplate: (props: DialogTenantKindPurposeTemplateProps) => T
) {
return (props: DialogProps) => {
switch (props.type) {
Expand Down Expand Up @@ -103,8 +106,10 @@ function match<T>(
return onRejectDelegatedVersionDraft(props)
case 'createAgreementDraft':
return onCreateAgreementDraft(props)
case 'tenantKind':
case 'tenantKindEServiceTemplate':
return onDialogTenantKindEserviceTemplate(props)
case 'tenantKindPurposeTemplate':
return onDialogTenantKindPurposeTemplate(props)
}
}
}
Expand All @@ -128,7 +133,8 @@ const _Dialog = match(
(props) => <DialogCreateAgreementDraft {...props} />,
(props) => <DialogRevokeDelegation {...props} />,
(props) => <DialogRejectDelegatedVersionDraft {...props} />,
(props) => <DialogTenantKindEserviceTemplate {...props} />
(props) => <DialogTenantKindEserviceTemplate {...props} />,
(props) => <DialogTenantKindPurposeTemplate {...props} />
)

export const Dialog: React.FC = () => {
Expand Down
112 changes: 112 additions & 0 deletions src/components/dialogs/DialogTenantKindPurposeTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react'
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
Typography,
} from '@mui/material'
import { useTranslation } from 'react-i18next'
import { useDialog } from '@/stores'
import type { DialogTenantKindPurposeTemplateProps } from '@/types/dialog.types'
import { RHFAutocompleteSingle, RHFRadioGroup } from '../shared/react-hook-form-inputs'
import { FormProvider, useForm } from 'react-hook-form'
import type { TenantKind } from '@/api/api.generatedTypes'

export const DialogTenantKindPurposeTemplate: React.FC<DialogTenantKindPurposeTemplateProps> = ({
onConfirm,
}) => {
const ariaLabelId = React.useId()

const { closeDialog } = useDialog()
const { t } = useTranslation('shared-components', {
keyPrefix: 'dialogPurposeTemplatesTenantKind',
})
const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' })

const handleCancel = () => {
closeDialog()
}

const formMethods = useForm<{ tenantKind: TenantKind; personalData: string }>({
defaultValues: {
tenantKind: 'PA',
personalData: 'true',
},
})

const onSubmit = formMethods.handleSubmit(({ tenantKind, personalData }) => {
const handlesPersonalData = personalData === 'true'
onConfirm(tenantKind, handlesPersonalData)
closeDialog()
})

const options: Array<{ label: string; value: TenantKind }> = [
{
label: t('content.options.labelPA'),
value: 'PA',
},
{
label: t('content.options.labelNotPA'),
value: 'PRIVATE',
},
]

const optionsPersonalData: Array<{ label: string; value: string }> = [
{
label: t('content.personalDataRadioBtn.options.true'),
value: 'true',
},
{
label: t('content.personalDataRadioBtn.options.false'),
value: 'false',
},
]

return (
<Dialog
open
onClose={handleCancel}
aria-labelledby={ariaLabelId}
maxWidth={'md'}
fullWidth
data-testid="create-purpose-modal"
>
<FormProvider {...formMethods}>
<Box component="form" noValidate onSubmit={onSubmit}>
<DialogTitle id={ariaLabelId}>{t('title')}</DialogTitle>
<DialogContent>
<Stack spacing={3}>
<Typography variant="body1">{t('content.description')}</Typography>
<RHFAutocompleteSingle
sx={{ my: 0 }}
name="tenantKind"
options={options}
label={t('content.label')}
rules={{ required: true }}
/>
<RHFRadioGroup
name="personalData"
options={optionsPersonalData}
label={t('content.personalDataRadioBtn.label')}
rules={{ required: true }}
row
/>
</Stack>
</DialogContent>
<DialogActions>
<Button type="button" variant="outlined" onClick={handleCancel}>
{tCommon('cancel')}
</Button>
<Button type="submit" variant="contained">
{tCommon('confirm')}
</Button>
</DialogActions>
</Box>
</FormProvider>
</Dialog>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ vi.mock('@/stores', () => ({
describe('DialogTenantKindEserviceTemplate', () => {
it('renders dialog with radio options and handles selection and confirm', async () => {
const onConfirm = vi.fn()
render(<DialogTenantKindEserviceTemplate type="tenantKind" onConfirm={onConfirm} />)
render(
<DialogTenantKindEserviceTemplate type="tenantKindEServiceTemplate" onConfirm={onConfirm} />
)

// Check dialog title and description
expect(screen.getByText('title')).toBeInTheDocument()
Expand Down Expand Up @@ -41,7 +43,9 @@ describe('DialogTenantKindEserviceTemplate', () => {

it('calls closeDialog on cancel', async () => {
const onConfirm = vi.fn()
render(<DialogTenantKindEserviceTemplate type="tenantKind" onConfirm={onConfirm} />)
render(
<DialogTenantKindEserviceTemplate type="tenantKindEServiceTemplate" onConfirm={onConfirm} />
)
const cancelButton = screen.getByRole('button', { name: 'cancel' })
await userEvent.click(cancelButton)
expect(onConfirm).not.toHaveBeenCalled()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import { DialogTenantKindPurposeTemplate } from '../DialogTenantKindPurposeTemplate'

// Mock useDialog to avoid side effects
vi.mock('@/stores', () => ({
useDialog: () => ({ closeDialog: vi.fn() }),
}))

describe('DialogTenantKindEserviceTemplate', () => {
it('renders dialog with autocomplete and handles selection and confirm', async () => {
const onConfirm = vi.fn()
render(
<DialogTenantKindPurposeTemplate type="tenantKindPurposeTemplate" onConfirm={onConfirm} />
)

// Check dialog title and description
expect(screen.getByText('title')).toBeInTheDocument()
expect(screen.getByText('content.description')).toBeInTheDocument()

const autocompleteInput = screen.getByLabelText('content.label')

// Focus / click into the autocomplete input
fireEvent.mouseDown(autocompleteInput)

// You can also simulate typing if needed
await userEvent.type(autocompleteInput, 'P')

// Wait for options to appear
await waitFor(() => {
expect(screen.getByText('content.options.labelPA')).toBeInTheDocument()

expect(screen.getByText('content.options.labelNotPA')).toBeInTheDocument()
})

// Select PRIVATE and confirm
const privateInput = screen.getByLabelText('content.options.labelNotPA')
await userEvent.click(privateInput)
await userEvent.click(screen.getByRole('button', { name: 'select' }))
expect(onConfirm).toHaveBeenCalledWith('PRIVATE')
})

it('calls closeDialog on cancel', async () => {
const onConfirm = vi.fn()
render(
<DialogTenantKindPurposeTemplate type="tenantKindPurposeTemplate" onConfirm={onConfirm} />
)
const cancelButton = screen.getByRole('button', { name: 'cancel' })
await userEvent.click(cancelButton)
expect(onConfirm).not.toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AuthHooks } from '@/api/auth'
import type { ActionItemButton } from '@/types/common.types'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import PlusOneIcon from '@mui/icons-material/PlusOne'
import { PageContainer } from '@/components/layout/containers'
import {
Expand All @@ -19,20 +18,66 @@ import {
import type { GetConsumerPurposeTemplatesParams } from '@/api/purposeTemplate/mockedResponses'
import type { CreatorPurposeTemplates } from '@/api/api.generatedTypes'
import { PurposeTemplateQueries } from '@/api/purposeTemplate/purposeTemplate.queries'
import { useDialog } from '@/stores'
import { PurposeTemplateMutations } from '@/api/purposeTemplate/purposeTemplate.mutations'
import type { TenantKind } from '@/api/api.generatedTypes'
import { useNavigate } from '@/router'
import { EServiceQueries } from '@/api/eservice'

const ConsumerPurposeTemplateListPage: React.FC = () => {
const { isAdmin, isOperatorAPI } = AuthHooks.useJwt()
const { t } = useTranslation('pages', { keyPrefix: 'consumerPurposeTemplatesList' })
const { t: tCommon } = useTranslation('common')
const { t: tPurposeTemplate } = useTranslation('purposeTemplate', { keyPrefix: 'list.filters' })
const { t: tPurposeTemplateDefaults } = useTranslation('purposeTemplate', {
keyPrefix: 'edit.defaultPurposeTemplate',
})
const navigate = useNavigate()

const [eservicesAutocompleteInput, setEServicesAutocompleteInput] = useAutocompleteTextInput()

const { mutate: createDraft } = PurposeTemplateMutations.useCreateDraft()

const { openDialog } = useDialog()

const handleCreateDraft = (tenantKind: TenantKind, _handlesPersonalData: boolean) => {
/**
* A purpose template cannot have two templates with the same title.
* To avoid this, we add the current date to the title to make it unique.
*/
const currentDateString = new Intl.DateTimeFormat('it', {
dateStyle: 'short',
timeStyle: 'short',
})
.format()
.replace(',', '')

createDraft(
{
targetDescription: tPurposeTemplateDefaults('intendedTarget'),
targetTenantKind: tenantKind,
purposeTitle: `Template finalità ${currentDateString}`,
purposeDescription: tPurposeTemplateDefaults('description'),
purposeIsFreeOfCharge: true,
purposeFreeOfChargeReason: tPurposeTemplateDefaults('freeOfChargeReason'),
purposeDailyCalls: 1,
handlesPersonalData: _handlesPersonalData,
},
{
onSuccess() {
navigate(/*'SUBSCRIBE_PURPOSE_TEMPLATE_EDIT'*/ 'NOT_FOUND') //TODO TO FIX WHEN ROUTE IS AVAILABLE
},
}
)
}

const topSideActions: Array<ActionItemButton> = [
{
action: () => navigate('PROVIDE_ESERVICE_TEMPLATE_CREATE'),
action: () =>
openDialog({
type: 'tenantKindPurposeTemplate',
onConfirm: handleCreateDraft,
}),
label: tCommon('createNewBtn'),
variant: 'contained',
icon: PlusOneIcon,
Expand All @@ -45,7 +90,6 @@ const ConsumerPurposeTemplateListPage: React.FC = () => {
states: ['PUBLISHED'],
limit: 50,
offset: 0,
isConsumerDelegable: true,
}),
placeholderData: keepPreviousData,
select: ({ results }) =>
Expand Down Expand Up @@ -107,7 +151,12 @@ const PurposeTemplateTableWrapper: React.FC<{
data: CreatorPurposeTemplates | undefined
}> = ({ data }) => {
if (!data) return <ConsumerPurposeTemplateTableSkeleton />
return <ConsumerPurposeTemplateTable purposeTemplates={data.results ?? []} />
return (
<ConsumerPurposeTemplateTable
purposeTemplates={data.results ?? []}
data-testid="purpose-template-table-component"
/>
)
}

export default ConsumerPurposeTemplateListPage
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mockUseJwt, renderWithApplicationContext } from '@/utils/testing.utils'
import ConsumerPurposeTemplateListPage from '../ConsumerPurposeTemplateList.page'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

mockUseJwt()

Expand Down Expand Up @@ -31,6 +32,13 @@ describe('ConsumerPurposeTemplateListPage', () => {
})
})

it('should open the dialog when clicking the create button', async () => {
const createButton = screen.getByText('createNewBtn')
await userEvent.click(createButton)

expect(await screen.findByTestId('create-purpose-modal')).toBeInTheDocument()
})

it('should be have four columns (intended target, purpose template, status, actions)', () => {
expect(screen.getAllByRole('columnheader')).toHaveLength(4)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const EServiceTemplateCreateStepPurposeAddPurposesTable: React.FC<{

const handleAddNewPurpose = () => {
openDialog({
type: 'tenantKind',
type: 'tenantKindEServiceTemplate',
onConfirm: onOpenAddRiskAnalysisForm,
})
}
Expand Down
8 changes: 7 additions & 1 deletion src/types/dialog.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type DialogProps =
| DialogRejectDelegatedVersionDraftProps
| DialogCreateAgreementDraftProps
| DialogTenantKindEserviceTemplateProps
| DialogTenantKindPurposeTemplateProps

export type DialogAttributeDetailsProps = {
type: 'showAttributeDetails'
Expand Down Expand Up @@ -160,6 +161,11 @@ export type DialogCreateAgreementDraftProps = {
}

export type DialogTenantKindEserviceTemplateProps = {
type: 'tenantKind'
type: 'tenantKindEServiceTemplate'
onConfirm: (tenantKind: TenantKind) => void
}

export type DialogTenantKindPurposeTemplateProps = {
type: 'tenantKindPurposeTemplate'
onConfirm: (tenantKind: TenantKind, handlesPersonalData: boolean) => void
}
Loading