diff --git a/frontend/README.md b/frontend/README.md index 07fbe11e..9b297eb0 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,6 +33,7 @@ elsewhere in Altinn. Should state: Preparing yarn@3.6.3 for immediate activation... > corepack yarn + This should install the dependencies listed in package.json 4. Run the Vite webserver script in package.json by @@ -40,7 +41,11 @@ This should install the dependencies listed in package.json The app should now be running in the Vite webserver, and is pseudo-available at http://localhost:5173 -(showing just a blue screen), +(showing just a blue screen), or http://localhost:5173/authfront/ui/xxx
+(showing an Error Page with a nice seagull). -and with the new /authfront/ui BasePath, we now reach the old Overview page via +and with the new /authfront/ui BasePath, we now reach the old, but edited Overview page via http://localhost:5173/authfront/ui/offered-api-delegations/overview + +while the new Authentication OverviewPage on new path is here:
+http://localhost:5173/authfront/ui/auth/overview diff --git a/frontend/src/features/overviewpage/OverviewPage.tsx b/frontend/src/features/overviewpage/OverviewPage.tsx new file mode 100644 index 00000000..a88728f4 --- /dev/null +++ b/frontend/src/features/overviewpage/OverviewPage.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from 'react-i18next'; +import * as React from 'react'; + +import { Page, PageHeader, PageContent, PageContainer } from '@/components'; +import { ReactComponent as ApiIcon } from '@/assets/Api.svg'; +import { useMediaQuery } from '@/resources/hooks'; + +import { OverviewPageContent } from './components/OverviewPageContent'; +import { LayoutState } from './components/LayoutState'; + +export const OverviewPage = () => { + const { t } = useTranslation('common'); + const isSm = useMediaQuery('(max-width: 768px)'); + + return ( + + + }>{t('api_delegation.api_delegations')} + + + + + + ); +}; diff --git a/frontend/src/features/overviewpage/components/LayoutState.tsx b/frontend/src/features/overviewpage/components/LayoutState.tsx new file mode 100644 index 00000000..136d9f41 --- /dev/null +++ b/frontend/src/features/overviewpage/components/LayoutState.tsx @@ -0,0 +1,4 @@ +export enum LayoutState { + Offered, + Received, +} diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.module.css b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.module.css new file mode 100644 index 00000000..22f923e9 --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.module.css @@ -0,0 +1,37 @@ +@media only screen and (max-width: 768px) { + .actionBarContent { + padding-left: 0.7rem; + } + + .orgDelegationActionBarTitle { + font-size: 14px; + } +} + +@media only screen and (min-width: 769px) { + .actionBarContent { + padding-left: 50px; + } +} + +.actionBarContent { + padding-right: 10px; +} + +.actionBarText__softDelete { + text-decoration: line-through; + opacity: 70%; +} + +.actionBarSubtitle__softDelete { + text-decoration: line-through; + opacity: 70%; +} + +.accordionHeaderRightText { + margin-right: 15px; +} + +.additionalText { + margin-right: 10px; +} diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.test.cy.tsx b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.test.cy.tsx new file mode 100644 index 00000000..872b1e4d --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.test.cy.tsx @@ -0,0 +1,313 @@ +import { Provider } from 'react-redux'; +import { mount } from 'cypress/react18'; +import * as React from 'react'; + +import { OrgDelegationActionBar } from '@/features/apiDelegation/components/OverviewPageContent/OrgDelegationActionBar'; +import store from '@/rtk/app/store'; + +import type { OverviewOrg } from '@/rtk/features/apiDelegation/apiDelegation/overviewOrg/overviewOrgSlice'; + +Cypress.Commands.add('mount', (component, options = {}) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { reduxStore = store, ...mountOptions } = options as any; + + const wrapped = {component}; + + return mount(wrapped, mountOptions); +}); + +describe('OrgDelegationActionBar', () => { + describe('AccordionHeader', () => { + it('should show delegateNewApi-button on render when delegateToOrgCallback is set', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: false, + orgNr: '123456789', + apiList: [ + { + id: '1', + apiName: 'Delegert API A', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + { + id: '2', + apiName: 'Delegert API B', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + ], + }; + + cy.mount( + null} + softDeleteAllCallback={() => null} + organization={overviewOrgs} + isEditable={false} + delegateToOrgCallback={() => null} + />, + ); + cy.findByRole('button', { name: /api_delegation.delegate_new_api/i }).should('exist'); + }); + + it('should not show delegateNewApi-button on render when delegateToOrgCallback is not set', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: false, + orgNr: '123456789', + apiList: [ + { + id: '1', + apiName: 'Delegert API A', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + { + id: '2', + apiName: 'Delegert API B', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + ], + }; + + cy.mount( + null} + softDeleteAllCallback={() => null} + organization={overviewOrgs} + isEditable={false} + />, + ); + cy.findByRole('button', { name: /api_delegation.delegate_new_api/i }).should('not.exist'); + }); + + it('should show delete button when state isEditable=true', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: false, + orgNr: '123456789', + apiList: [], + }; + + cy.mount( + null} + softDeleteAllCallback={() => null} + organization={overviewOrgs} + isEditable={true} + />, + ); + cy.findByRole('button', { name: /delete/i }).should('exist'); + }); + + it('should not show undo button when state is isEditable=false', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: false, + orgNr: '123456789', + apiList: [], + }; + + cy.mount( + null} + softDeleteAllCallback={() => null} + organization={overviewOrgs} + isEditable={false} + />, + ); + cy.findByRole('button', { name: /undo/i }).should('not.exist'); + }); + + it('should show an undo button and display header with line through when all apis are soft deleted', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: true, + orgNr: '123456789', + apiList: [ + { + id: '1', + apiName: 'Delegert API A', + isSoftDelete: true, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + { + id: '2', + apiName: 'Delegert API B', + isSoftDelete: true, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + ], + }; + + cy.mount( + null} + softDeleteAllCallback={() => null} + organization={overviewOrgs} + isEditable={true} + />, + ); + + cy.get('button') + .contains('Evry') + .should('have.css', 'text-decoration', 'line-through solid rgb(30, 43, 60)'); + cy.findByRole('button', { name: /undo/i }).should('exist'); + }); + + it('should call softDeleteCallback on button click and isEditable=true ', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: false, + orgNr: '123456789', + apiList: [ + { + id: '1', + apiName: 'Delegert API A', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + { + id: '2', + apiName: 'Delegert API B', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + ], + }; + + const softDeleteAll = () => { + cy.stub(); + }; + + const softDeleteAllSpy = cy.spy(softDeleteAll).as('softDeleteAllSpy'); + + cy.mount( + null} + isEditable={true} + />, + ); + + cy.findByRole('button', { name: /delete/i }).click(); + cy.get('@softDeleteAllSpy').should('have.been.called'); + }); + + it('should call softRestoreCallback on buttonclick', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: true, + orgNr: '123456789', + apiList: [ + { + id: '1', + apiName: 'Delegert API A', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + { + id: '2', + apiName: 'Delegert API B', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + ], + }; + + const softRestoreAll = () => { + cy.stub(); + }; + + const softRestoreAllSpy = cy.spy(softRestoreAll).as('softRestoreAllSpy'); + + cy.mount( + null} + softRestoreAllCallback={softRestoreAllSpy} + isEditable={true} + />, + ); + + cy.findByRole('button', { name: /undo/i }).click(); + cy.get('@softRestoreAllSpy').should('have.been.called'); + }); + + it('should call delegateToOrgCallback on buttonclick', () => { + const overviewOrgs: OverviewOrg = { + id: '1', + orgName: 'Evry', + isAllSoftDeleted: true, + orgNr: '123456789', + apiList: [ + { + id: '1', + apiName: 'Delegert API A', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + { + id: '2', + apiName: 'Delegert API B', + isSoftDelete: false, + owner: 'Accenture', + description: + 'API for forvaltningsorgan og kompetansesenter som skal styrke kommunenes, sektormyndighetenes og andre samarbeidspartneres kompetanse på integrering og', + }, + ], + }; + + const delegateToNewOrg = () => { + cy.stub(); + }; + + const delegateToNewOrgSpy = cy.spy(delegateToNewOrg).as('delegateToNewOrgSpy'); + + cy.mount( + null} + softRestoreAllCallback={() => null} + delegateToOrgCallback={delegateToNewOrgSpy} + isEditable={true} + />, + ); + + cy.findByRole('button', { name: /delegate_new_api/i }).click(); + cy.get('@delegateToNewOrgSpy').should('have.been.called'); + }); + }); +}); diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.tsx b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.tsx new file mode 100644 index 00000000..83cc5bee --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/OrgDelegationActionBar.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react'; +import { Button, List } from '@digdir/design-system-react'; +import cn from 'classnames'; +import { useTranslation } from 'react-i18next'; +import * as React from 'react'; + +import type { OverviewOrg } from '@/rtk/features/apiDelegation/overviewOrg/overviewOrgSlice'; +import { softDelete, softRestore } from '@/rtk/features/apiDelegation/overviewOrg/overviewOrgSlice'; +import { useAppDispatch } from '@/rtk/app/hooks'; +import { ReactComponent as MinusCircle } from '@/assets/MinusCircle.svg'; +import { ReactComponent as Cancel } from '@/assets/Cancel.svg'; +import { ReactComponent as AddCircle } from '@/assets/AddCircle.svg'; +import { DeletableListItem, ActionBar } from '@/components'; +import { useMediaQuery } from '@/resources/hooks'; + +import classes from './OrgDelegationActionBar.module.css'; + +export interface OrgDelegationActionBarProps { + organization: OverviewOrg; + isEditable: boolean; + softRestoreAllCallback: () => void; + softDeleteAllCallback: () => void; + delegateToOrgCallback?: () => void; +} + +export const OrgDelegationActionBar = ({ + organization, + softRestoreAllCallback, + softDeleteAllCallback, + isEditable = false, + delegateToOrgCallback, +}: OrgDelegationActionBarProps) => { + const [open, setOpen] = useState(false); + const { t } = useTranslation('common'); + const numberOfAccesses = organization.apiList.length.toString(); + const dispatch = useAppDispatch(); + const isSm = useMediaQuery('(max-width: 768px)'); + + const handleSoftDeleteAll = () => { + softDeleteAllCallback(); + setOpen(true); + }; + + const actions = ( + <> + {delegateToOrgCallback && ( + + )} + {isEditable && + (organization.isAllSoftDeleted ? ( + + ) : ( +
+ +
+ ))} + + ); + + const listItems = organization.apiList.map((item, i) => ( + dispatch(softDelete([organization.id, item.id]))} + softRestoreCallback={() => dispatch(softRestore([organization.id, item.id]))} + item={item} + isEditable={isEditable} + scopes={item.scopes} + > + )); + + return ( + { + setOpen(!open); + }} + open={open} + color={'neutral'} + actions={actions} + title={ +
+ {organization.orgName} +
+ } + subtitle={ +
+ {t('api_delegation.org_nr') + ' ' + organization.orgNr} +
+ } + additionalText={ +
+ {!isSm && ( + + {numberOfAccesses} {t('api_delegation.api_accesses')} + + )} +
+ } + > +
+ {listItems} +
+
+ ); +}; diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/index.ts b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/index.ts new file mode 100644 index 00000000..e8cf44cf --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/OrgDelegationActionBar/index.ts @@ -0,0 +1 @@ +export { OrgDelegationActionBar } from './OrgDelegationActionBar'; diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/OverviewPageContent.module.css b/frontend/src/features/overviewpage/components/OverviewPageContent/OverviewPageContent.module.css new file mode 100644 index 00000000..b3f0c825 --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/OverviewPageContent.module.css @@ -0,0 +1,107 @@ +@media only screen and (max-width: 768px) { + .overviewAccordionsContainer { + margin-top: 0rem; + } + + .delegateNewButton { + margin-bottom: 1.3rem; + margin-top: 0rem; + } + + .activeDelegationsHeader { + margin-top: 0.1rem; + } + + .editButton { + align-items: center; + justify-content: end; + margin-bottom: 0.3rem; + } +} + +@media only screen and (min-width: 769px) { + .overviewActionBarContainer { + margin-top: 3rem; + } + + .delegateNewButton { + margin-bottom: 3rem; + } + + .activeDelegationsHeader { + margin-top: 2rem; + display: flex; + align-items: center; + } + + .saveSection { + margin-right: 0.5rem; + display: flex; + justify-content: flex-end; + } + + .editButton { + justify-content: flex-end; + align-items: center; + display: flex; + flex: 1; + } + + .explanatoryContainer { + margin-top: 30px; + display: flex; + } +} + +.noActiveDelegations { + margin-top: 1.5rem; +} + +.spinnerContainer { + margin-top: 3rem; + display: flex; + justify-content: center; +} + +.saveSection { + margin-top: 2rem; +} + +.apiSubheading { + display: flex; + margin-bottom: 2rem; + font-weight: 500; +} + +.pageContentText { + margin-top: -2rem; + font-weight: 500; +} + +.link { + color: black; + text-decoration-line: underline; + text-decoration-color: #1eadf7; + display: inline-block; + background: transparent; +} + +.errorPanel { + margin-top: 2rem; +} + +.actionBarWrapper { + margin-top: 5px; + margin-bottom: 5px; +} + +.testText { + margin-right: 40px; +} + +.testContent { + margin: 1.5rem; + display: flex; + flex-direction: column; + gap: 6px; +} diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/OverviewPageContent.tsx b/frontend/src/features/overviewpage/components/OverviewPageContent/OverviewPageContent.tsx new file mode 100644 index 00000000..b6586254 --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/OverviewPageContent.tsx @@ -0,0 +1,251 @@ +import { Panel } from '@altinn/altinn-design-system'; +import { Button, Spinner } from '@digdir/design-system-react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import * as React from 'react'; + +import { useAppDispatch, useAppSelector } from '@/rtk/app/hooks'; +import { ReactComponent as Add } from '@/assets/Add.svg'; +import { ReactComponent as Edit } from '@/assets/Edit.svg'; +import { ReactComponent as Error } from '@/assets/Error.svg'; +import { resetDelegationRequests } from '@/rtk/features/apiDelegation/delegationRequest/delegationRequestSlice'; +import { resetDelegableOrgs } from '@/rtk/features/apiDelegation/delegableOrg/delegableOrgSlice'; +import { + fetchOverviewOrgsOffered, + fetchOverviewOrgsReceived, + restoreAllSoftDeletedItems, + softDeleteAll, + softRestoreAll, + deleteOfferedApiDelegation, + deleteReceivedApiDelegation, + type OverviewOrg, + type DeletionRequest, +} from '@/rtk/features/apiDelegation/overviewOrg/overviewOrgSlice'; +import { resetDelegableApis } from '@/rtk/features/apiDelegation/delegableApi/delegableApiSlice'; +import { useMediaQuery } from '@/resources/hooks'; +import { ApiDelegationPath } from '@/routes/paths'; +import { ErrorPanel } from '@/components'; + +import { LayoutState } from '../LayoutState'; + +import { OrgDelegationActionBar } from './OrgDelegationActionBar'; +import classes from './OverviewPageContent.module.css'; + +export interface OverviewPageContentInterface { + layout: LayoutState; +} + +export const OverviewPageContent = ({ + layout = LayoutState.Offered, +}: OverviewPageContentInterface) => { + const [saveDisabled, setSaveDisabled] = useState(false); + const [isEditable, setIsEditable] = useState(false); + const { t } = useTranslation('common'); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const overviewOrgs = useAppSelector((state) => state.overviewOrg.overviewOrgs); + const error = useAppSelector((state) => state.overviewOrg.error); + const loading = useAppSelector((state) => state.overviewOrg.loading); + const isSm = useMediaQuery('(max-width: 768px)'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let fetchData: () => any; + let overviewText: string; + let accessesHeader: string; + let noDelegationsInfoText: string; + + useEffect(() => { + if (loading) { + void fetchData(); + } + handleSaveDisabled(); + dispatch(resetDelegableApis()); + dispatch(resetDelegableOrgs()); + dispatch(resetDelegationRequests()); + }, [overviewOrgs, error]); + + switch (layout) { + case LayoutState.Offered: + fetchData = async () => await dispatch(fetchOverviewOrgsOffered()); + overviewText = t('api_delegation.api_overview_text'); + accessesHeader = t('api_delegation.you_have_delegated_accesses'); + noDelegationsInfoText = t('api_delegation.no_offered_delegations'); + break; + case LayoutState.Received: + fetchData = async () => await dispatch(fetchOverviewOrgsReceived()); + overviewText = t('api_delegation.api_received_overview_text'); + accessesHeader = t('api_delegation.you_have_received_accesses'); + noDelegationsInfoText = t('api_delegation.no_received_delegations'); + break; + } + + const goToStartDelegation = () => { + dispatch(restoreAllSoftDeletedItems()); + navigate('/' + ApiDelegationPath.OfferedApiDelegations + '/' + ApiDelegationPath.ChooseOrg); + }; + + const handleSaveDisabled = () => { + for (const org of overviewOrgs) { + if (org.isAllSoftDeleted) { + setSaveDisabled(false); + return; + } + for (const api of org.apiList) { + if (api.isSoftDelete) { + setSaveDisabled(false); + return; + } + } + } + setSaveDisabled(true); + }; + + const handleSetIsEditable = () => { + if (isEditable) { + dispatch(restoreAllSoftDeletedItems()); + } + setIsEditable(!isEditable); + }; + + const mapToDeletionRequest = (orgNr: string, apiId: string) => { + const deletionRequest: DeletionRequest = { + orgNr, + apiId, + }; + return deletionRequest; + }; + + const handleSave = () => { + for (const org of overviewOrgs) { + for (const item of org.apiList) { + if (item.isSoftDelete) { + if (layout === LayoutState.Offered) { + void dispatch(deleteOfferedApiDelegation(mapToDeletionRequest(org.orgNr, item.id))); + } else if (layout === LayoutState.Received) { + void dispatch(deleteReceivedApiDelegation(mapToDeletionRequest(org.orgNr, item.id))); + } + } + } + } + setIsEditable(false); + }; + + const activeDelegations = () => { + if (error.message) { + return ( +
+ +
+ ); + } else if (loading) { + return ( +
+ +
+ ); + } else if (overviewOrgs.length < 1) { + return

{noDelegationsInfoText}

; + } + return overviewOrgs.map((org: OverviewOrg) => ( +
+ dispatch(softDeleteAll(org.id))} + softRestoreAllCallback={() => dispatch(softRestoreAll(org.id))} + key={org.id} + > +
+ )); + }; + + return ( +
+ {!isSm &&

{overviewText}

} + {layout === LayoutState.Offered && ( +
+ +
+ )} + + {t('api_delegation.api_panel_content')}{' '} + + {'(se Runes issue #2) XXXX'} + + +
+ {overviewOrgs.length > 0 && ( + <> + {isSm ? ( +

{accessesHeader}

+ ) : ( +

{accessesHeader}

+ )} +
+ {!isEditable ? ( + + ) : ( + + )} +
+ + )} +
+ <>{activeDelegations()} + {isEditable && ( +
+ +
+ )} +
+ ); +}; diff --git a/frontend/src/features/overviewpage/components/OverviewPageContent/index.ts b/frontend/src/features/overviewpage/components/OverviewPageContent/index.ts new file mode 100644 index 00000000..be5c698f --- /dev/null +++ b/frontend/src/features/overviewpage/components/OverviewPageContent/index.ts @@ -0,0 +1 @@ +export { OverviewPageContent } from './OverviewPageContent'; diff --git a/frontend/src/features/overviewpage/components/SummaryPage/SummaryPage.module.css b/frontend/src/features/overviewpage/components/SummaryPage/SummaryPage.module.css new file mode 100644 index 00000000..a0990f04 --- /dev/null +++ b/frontend/src/features/overviewpage/components/SummaryPage/SummaryPage.module.css @@ -0,0 +1,63 @@ +@media only screen and (max-width: 768px) { + .receiptMainButton { + width: 100%; + } + + .listText { + font-size: 20px; + } + + .bottomListText { + font-size: 20px; + margin-top: 2.5rem; + } + + .infoText { + font-size: 16px; + } +} + +.receiptMainButton { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.navButtonContainer { + margin-top: 8rem; + display: flex; + justify-content: end; + margin-bottom: 70px; +} + +.previousButton { + display: flex; + margin-right: 3rem; +} + +.confirmButton { + display: flex; +} + +.infoText { + margin-top: 20px; + font-weight: 400; +} + +.listText { + font-weight: 500; +} + +.bottomListText { + font-weight: 500; + margin-top: 2.5rem; +} + +.restartButton { + display: flex; + justify-content: center; +} + +.receiptMainButton { + display: flex; + justify-content: flex-end; +} diff --git a/frontend/src/features/overviewpage/components/SummaryPage/SummaryPage.tsx b/frontend/src/features/overviewpage/components/SummaryPage/SummaryPage.tsx new file mode 100644 index 00000000..3420ff78 --- /dev/null +++ b/frontend/src/features/overviewpage/components/SummaryPage/SummaryPage.tsx @@ -0,0 +1,240 @@ +import { Panel, PanelVariant } from '@altinn/altinn-design-system'; +import { List, Button } from '@digdir/design-system-react'; +import type { Key } from 'react'; +import { t } from 'i18next'; +import { useNavigate } from 'react-router-dom'; +import * as React from 'react'; + +import { useAppDispatch } from '@/rtk/app/hooks'; +import { ReactComponent as OfficeIcon } from '@/assets/Office1.svg'; +import { ReactComponent as SettingsIcon } from '@/assets/Settings.svg'; +import { + CompactDeletableListItem, + NavigationButtons, + Page, + PageContent, + PageHeader, + type PageColor, +} from '@/components'; +import type { ApiDelegation } from '@/rtk/features/apiDelegation/delegationRequest/delegationRequestSlice'; +import type { DelegableOrg } from '@/rtk/features/apiDelegation/delegableOrg/delegableOrgSlice'; +import { softRemoveOrg } from '@/rtk/features/apiDelegation/delegableOrg/delegableOrgSlice'; +import { softRemoveApi } from '@/rtk/features/apiDelegation/delegableApi/delegableApiSlice'; +import type { DelegableApi } from '@/rtk/features/apiDelegation/delegableApi/delegableApiSlice'; +import { useMediaQuery } from '@/resources/hooks/useMediaQuery'; +import { ApiDelegationPath } from '@/routes/paths'; +import { setLoading as setOveviewToReload } from '@/rtk/features/apiDelegation/overviewOrg/overviewOrgSlice'; + +import { ListTextColor } from '../../../../components/CompactDeletableListItem/CompactDeletableListItem'; + +import classes from './SummaryPage.module.css'; + +export interface SummaryPageProps { + delegableApis?: DelegableApi[]; + delegableOrgs?: DelegableOrg[]; + failedDelegations?: ApiDelegation[]; + successfulDelegations?: ApiDelegation[]; + restartProcessPath: string; + pageHeaderText: string; + headerIcon: React.ReactNode; + headerColor?: PageColor; + topListText?: string; + failedDelegationText?: string; + bottomListText?: string; + bottomText?: string; + confirmationButtonClick?: () => void; + confirmationButtonDisabled?: boolean; + confirmationButtonLoading?: boolean; + showNavigationButtons?: boolean; +} + +export const SummaryPage = ({ + delegableApis, + delegableOrgs, + failedDelegations, + successfulDelegations, + pageHeaderText, + headerColor = 'dark', + headerIcon, + topListText, + failedDelegationText, + bottomListText, + bottomText, + confirmationButtonClick, + confirmationButtonDisabled = false, + confirmationButtonLoading = false, + restartProcessPath, + showNavigationButtons = true, +}: SummaryPageProps) => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const isSm = useMediaQuery('(max-width: 768px)'); + + const delegableApiListItems = delegableApis?.map( + (api: DelegableApi | ApiDelegation, index: Key) => { + return ( + } + removeCallback={delegableApis.length > 1 ? () => dispatch(softRemoveApi(api)) : null} + leftText={api.apiName} + middleText={api.orgName} + > + ); + }, + ); + + const delegableOrgListItems = delegableOrgs?.map( + (org: DelegableOrg, index: Key | null | undefined) => { + return ( + } + removeCallback={delegableOrgs.length > 1 ? () => dispatch(softRemoveOrg(org)) : null} + leftText={org.orgName} + middleText={t('api_delegation.org_nr') + ' ' + org.orgNr} + > + ); + }, + ); + + const failedDelegatedListItems = failedDelegations?.map( + (apiDelegation: ApiDelegation, index: Key | null | undefined) => { + return ( + + ); + }, + ); + + const successfulDelegatedItems = successfulDelegations?.map( + (apiDelegation: ApiDelegation, index: Key | null | undefined) => { + return ( + + ); + }, + ); + + const showTopSection = () => { + return ( + (delegableApis !== null && delegableApis !== undefined && delegableApis?.length > 0) || + (failedDelegations !== null && + failedDelegations !== undefined && + failedDelegations?.length > 0) + ); + }; + + const showBottomSection = () => { + return ( + (delegableOrgs !== null && delegableOrgs !== undefined && delegableOrgs?.length > 0) || + (successfulDelegations !== null && + successfulDelegations !== undefined && + successfulDelegations?.length > 0) + ); + }; + + const showErrorPanel = () => { + return !showTopSection() && !showBottomSection(); + }; + + const navigateToOverview = () => { + dispatch(setOveviewToReload()); + navigate('/' + ApiDelegationPath.OfferedApiDelegations + '/' + ApiDelegationPath.Overview); + }; + + return ( + + {pageHeaderText} + + {showErrorPanel() ? ( + +
+

{t('api_delegation.delegations_not_registered')}

+
+ +
+
+
+ ) : ( +
+ {showTopSection() && ( +
+

{topListText}

+ {delegableApiListItems !== undefined && ( + {delegableApiListItems} + )} + {failedDelegations !== undefined && ( + {failedDelegatedListItems} + )} +
+ )} +

{failedDelegationText}

+ {showBottomSection() && ( +
+

{bottomListText}

+ {delegableOrgs !== undefined && ( + {delegableOrgListItems} + )} + {successfulDelegations !== undefined && ( + {successfulDelegatedItems} + )} +
+ )} +

{bottomText}

+ {showNavigationButtons ? ( + + ) : ( +
+ +
+ )} +
+ )} +
+
+ ); +}; diff --git a/frontend/src/features/overviewpage/components/SummaryPage/index.ts b/frontend/src/features/overviewpage/components/SummaryPage/index.ts new file mode 100644 index 00000000..3bd5e90c --- /dev/null +++ b/frontend/src/features/overviewpage/components/SummaryPage/index.ts @@ -0,0 +1 @@ +export { SummaryPage } from './SummaryPage'; diff --git a/frontend/src/features/overviewpage/index.ts b/frontend/src/features/overviewpage/index.ts new file mode 100644 index 00000000..7a5b2424 --- /dev/null +++ b/frontend/src/features/overviewpage/index.ts @@ -0,0 +1 @@ +export { OverviewPage } from './OverviewPage'; diff --git a/frontend/src/routes/Router/Router.tsx b/frontend/src/routes/Router/Router.tsx index 247bee26..529e923d 100644 --- a/frontend/src/routes/Router/Router.tsx +++ b/frontend/src/routes/Router/Router.tsx @@ -1,17 +1,20 @@ import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom'; import * as React from 'react'; +import { OverviewPage as AuthenticationOverviewPage } from '@/features/overviewpage/OverviewPage'; + import { ChooseApiPage } from '@/features/apiDelegation/offered/ChooseApiPage'; import { OverviewPage as OfferedOverviewPage } from '@/features/apiDelegation/offered/OverviewPage'; import { OverviewPage as ReceivedOverviewPage } from '@/features/apiDelegation/received/OverviewPage'; import { ChooseOrgPage } from '@/features/apiDelegation/offered/ChooseOrgPage'; import { ReceiptPage } from '@/features/apiDelegation/offered/ReceiptPage'; import { ConfirmationPage } from '@/features/apiDelegation/offered/ConfirmationPage'; -import { NotFoundSite } from '@/sites/NotFoundSite'; import { ChooseServicePage } from '@/features/singleRight/delegate/ChooseServicePage/ChooseServicePage'; import { ChooseRightsPage } from '@/features/singleRight/delegate/ChooseRightsPage/ChooseRightsPage'; -import { GeneralPath, SingleRightPath, ApiDelegationPath } from '../paths'; +import { NotFoundSite } from '@/sites/NotFoundSite'; + +import { GeneralPath, AuthenticationPath, SingleRightPath, ApiDelegationPath } from '../paths'; // Note: there are just 8 pages: the elaborate and repetitive route tree below // maps out URLs such as /Basepath/OfferedApiDelegations/Overview @@ -46,10 +49,20 @@ export const Router = createBrowserRouter( errorElement={} > } > + } + errorElement={} + /> + + } + > } diff --git a/frontend/src/routes/paths/AuthenticationPath.tsx b/frontend/src/routes/paths/AuthenticationPath.tsx new file mode 100644 index 00000000..c20d312f --- /dev/null +++ b/frontend/src/routes/paths/AuthenticationPath.tsx @@ -0,0 +1,4 @@ +export enum AuthenticationPath { + Overview = 'overview', + Auth = 'auth', + } \ No newline at end of file diff --git a/frontend/src/routes/paths/index.ts b/frontend/src/routes/paths/index.ts index 60432134..dab6fc38 100644 --- a/frontend/src/routes/paths/index.ts +++ b/frontend/src/routes/paths/index.ts @@ -1,3 +1,4 @@ export { ApiDelegationPath } from './ApiDelegationPath'; export { GeneralPath } from './GeneralPath'; export { SingleRightPath } from './SingleRightPath'; +export { AuthenticationPath } from './AuthenticationPath';