diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.module.css b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.module.css new file mode 100644 index 00000000000..afca290346f --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.module.css @@ -0,0 +1,11 @@ +.savingContainer { + display: flex; + align-items: center; + gap: var(--fds-spacing-1); + margin-top: var(--fds-spacing-4); +} + +.savedIcon { + font-size: var(--fds-sizing-8); + color: var(--fds-semantic-text-neutral-subtle); +} diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.test.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.test.tsx new file mode 100644 index 00000000000..786272e101f --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { SaveStatus } from './SaveStatus'; +import { textMock } from '@studio/testing/mocks/i18nMock'; + +describe('SaveStatus', () => { + it('should display a spinner while pending', () => { + render(); + + expect( + screen.getByText(textMock('settings_modal.maskinporten_tab_save_scopes_pending')), + ).toBeInTheDocument(); + + expect( + screen.getByTitle(textMock('settings_modal.maskinporten_tab_save_scopes_pending_spinner')), + ).toBeInTheDocument(); + }); + + it('should render saved status with checkmark icon', () => { + render(); + + expect( + screen.getByText(textMock('settings_modal.maskinporten_tab_save_scopes_complete')), + ).toBeInTheDocument(); + }); + + it('should render nothing when neither pending nor saved', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.tsx new file mode 100644 index 00000000000..99047940f7c --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/SaveStatus.tsx @@ -0,0 +1,50 @@ +import React, { type ReactElement } from 'react'; +import classes from './SaveStatus.module.css'; +import { StudioParagraph, StudioSpinner } from '@studio/components'; +import { useTranslation } from 'react-i18next'; +import { CheckmarkIcon } from '@studio/icons'; + +type SaveStatusProps = { + isPending: boolean; + isSaved: boolean; +}; + +export const SaveStatus = ({ isPending, isSaved }: SaveStatusProps): ReactElement => { + const { t } = useTranslation(); + + if (isPending) { + return ( + + ); + } + if (isSaved) { + return ; + } + return null; +}; + +type SaveStatusContentProps = { + text: string; + isPending?: boolean; +}; + +const SaveStatusContent = ({ text, isPending = false }: SaveStatusContentProps): ReactElement => { + const { t } = useTranslation(); + + return ( +
+ {text} + {isPending ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/index.ts b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/index.ts new file mode 100644 index 00000000000..e7dcaec9432 --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/SaveStatus/index.ts @@ -0,0 +1 @@ +export { SaveStatus } from './SaveStatus'; diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/ScopeList/ScopeList.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/ScopeList/ScopeList.tsx index 2860c815503..3f150ac7a52 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/ScopeList/ScopeList.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/components/Tabs/Maskinporten/ScopeListContainer/ScopeList/ScopeList.tsx @@ -1,6 +1,5 @@ import React, { type ChangeEvent, type ReactElement } from 'react'; import classes from './ScopeList.module.css'; - import { StudioCheckboxTable, type StudioCheckboxTableRowElement, @@ -25,6 +24,7 @@ import { GetInTouchWith } from 'app-shared/getInTouch'; import { EmailContactProvider } from 'app-shared/getInTouch/providers'; import { LoggedInTitle } from '../LoggedInTitle'; import { useUpdateSelectedMaskinportenScopesMutation } from 'app-development/hooks/mutations/useUpdateSelectedMaskinportenScopesMutation'; +import { SaveStatus } from '../SaveStatus'; export type ScopeListProps = { maskinPortenScopes: MaskinportenScope[]; @@ -33,8 +33,11 @@ export type ScopeListProps = { export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps): ReactElement => { const { t } = useTranslation(); - const { mutate: mutateSelectedMaskinportenScopes } = - useUpdateSelectedMaskinportenScopesMutation(); + const { + mutate: mutateSelectedMaskinportenScopes, + isPending: isPendingSaveScopes, + isSuccess: scopesSaved, + } = useUpdateSelectedMaskinportenScopesMutation(); const checkboxTableRowElements: StudioCheckboxTableRowElement[] = mapScopesToRowElements( maskinPortenScopes, @@ -103,6 +106,7 @@ export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps ))} + ); }; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index e836948c1f2..bfd5859b960 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1023,9 +1023,12 @@ "settings_modal.maskinporten_tab_available_scopes_description_help": "Hvis du trenger tilgang til flere scopes i denne listen, <0> tar du kontakt med Altinn servicedesk.", "settings_modal.maskinporten_tab_available_scopes_description_help_link": "tar du kontakt med Altinn servicedesk.", "settings_modal.maskinporten_tab_available_scopes_title": "Scopes for denne virksomheten", - "settings_modal.maskinporten_tab_description": "Maskinporten autentiserer og autoriserer API-er som skal brukes i apper. I Maskinporten kan du lage hvilke scopes en app skal ha fra og til andre systemer, for eksempel tilgang til persondata.", + "settings_modal.maskinporten_tab_description": "Maskinporten autentiserer og autoriserer API-er som skal brukes i apper. I Maskinporten kan du sette opp hvilke scopes en app skal ha fra og til andre systemer, for eksempel tilgang til persondata.", "settings_modal.maskinporten_tab_login_with_ansattporten": "Logg inn med Ansattporten", "settings_modal.maskinporten_tab_login_with_description": "Med Ansattporten logger du inn på vegne av virksomheten. Her kan du se og velge scopes.", + "settings_modal.maskinporten_tab_save_scopes_complete": "Lagret", + "settings_modal.maskinporten_tab_save_scopes_pending": "Lagrer...", + "settings_modal.maskinporten_tab_save_scopes_pending_spinner": "Lagrer scopes", "settings_modal.maskinporten_tab_title": "Velg scopes fra Maskinporten", "settings_modal.policy_tab_heading": "Tilganger", "settings_modal.setup_tab_heading": "Oppsett",