From 55f2170aaf7f461d0e5f47f5b6babed5d21d631a Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 9 Oct 2024 15:26:03 +0200 Subject: [PATCH 01/11] feat: deleting subform layoutset (#13680) --- .../Controllers/AppDevelopmentController.cs | 6 ++ .../LayoutSetDeletedComponentRefHandler.cs | 73 +++++++++++++++++++ .../Designer/Events/LayoutSetDeletedEvent.cs | 10 +++ .../Designer/Hubs/SyncHub/SyncErrorCodes.cs | 1 + .../DeleteLayoutSetTests.cs | 41 ++++++++++- .../layoutSet2/layouts/layoutFile1InSet2.json | 28 ++++--- .../SyncSuccessQueriesInvalidator.test.ts | 2 +- .../SyncSuccessQueriesInvalidator.ts | 2 +- 8 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs create mode 100644 backend/src/Designer/Events/LayoutSetDeletedEvent.cs diff --git a/backend/src/Designer/Controllers/AppDevelopmentController.cs b/backend/src/Designer/Controllers/AppDevelopmentController.cs index 274a9ccfa38..b9833d8894c 100644 --- a/backend/src/Designer/Controllers/AppDevelopmentController.cs +++ b/backend/src/Designer/Controllers/AppDevelopmentController.cs @@ -393,6 +393,12 @@ public async Task DeleteLayoutSet(string org, string app, [FromRou string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer); LayoutSets layoutSets = await _appDevelopmentService.DeleteLayoutSet(editingContext, layoutSetIdToUpdate, cancellationToken); + + await _mediator.Publish(new LayoutSetDeletedEvent + { + EditingContext = editingContext, + LayoutSetId = layoutSetIdToUpdate + }, cancellationToken); return Ok(layoutSets); } diff --git a/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs b/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs new file mode 100644 index 00000000000..95b38685a47 --- /dev/null +++ b/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Altinn.App.Core.Helpers; +using Altinn.Studio.Designer.Events; +using Altinn.Studio.Designer.Hubs.SyncHub; +using Altinn.Studio.Designer.Infrastructure.GitRepository; +using Altinn.Studio.Designer.Services.Interfaces; +using MediatR; + +namespace Altinn.Studio.Designer.EventHandlers.LayoutSetDeleted; + +public class LayoutSetDeletedComponentRefHandler(IAltinnGitRepositoryFactory altinnGitRepositoryFactory, IFileSyncHandlerExecutor fileSyncHandlerExecutor) : INotificationHandler +{ + public async Task Handle(LayoutSetDeletedEvent notification, CancellationToken cancellationToken) + { + AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository( + notification.EditingContext.Org, + notification.EditingContext.Repo, + notification.EditingContext.Developer); + + string[] layoutSetNames = altinnAppGitRepository.GetLayoutSetNames(); + + await fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotification( + notification.EditingContext, + SyncErrorCodes.LayoutSetSubLayoutSyncError, + "layouts", + async () => + { + bool hasChanges = false; + foreach (string layoutSetName in layoutSetNames) + { + Dictionary formLayouts = await altinnAppGitRepository.GetFormLayouts(layoutSetName, cancellationToken); + foreach (var formLayout in formLayouts) + { + hasChanges |= await RemoveComponentsReferencingLayoutSet( + notification, + altinnAppGitRepository, + layoutSetName, + formLayout, + cancellationToken); + } + } + return hasChanges; + }); + } + + private static async Task RemoveComponentsReferencingLayoutSet(LayoutSetDeletedEvent notification, AltinnAppGitRepository altinnAppGitRepository, string layoutSetName, KeyValuePair formLayout, CancellationToken cancellationToken) + { + if (formLayout.Value["data"] is not JsonObject data || data["layout"] is not JsonArray layoutArray) + { + return false; + } + + bool hasChanges = false; + layoutArray.RemoveAll(jsonNode => + { + if (jsonNode["layoutSet"]?.GetValue() == notification.LayoutSetId) + { + hasChanges = true; + return true; + } + return false; + }); + + if (hasChanges) + { + await altinnAppGitRepository.SaveLayout(layoutSetName, $"{formLayout.Key}.json", formLayout.Value, cancellationToken); + } + return hasChanges; + } +} diff --git a/backend/src/Designer/Events/LayoutSetDeletedEvent.cs b/backend/src/Designer/Events/LayoutSetDeletedEvent.cs new file mode 100644 index 00000000000..aadbd44a4c0 --- /dev/null +++ b/backend/src/Designer/Events/LayoutSetDeletedEvent.cs @@ -0,0 +1,10 @@ +using Altinn.Studio.Designer.Models; +using MediatR; + +namespace Altinn.Studio.Designer.Events; + +public class LayoutSetDeletedEvent : INotification +{ + public string LayoutSetId { get; set; } + public AltinnRepoEditingContext EditingContext { get; set; } +} diff --git a/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs b/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs index 3c0337ee0b2..82d51fb6c24 100644 --- a/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs +++ b/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs @@ -8,6 +8,7 @@ public static class SyncErrorCodes public const string ApplicationMetadataDataTypeSyncError = nameof(ApplicationMetadataDataTypeSyncError); public const string LayoutSetsDataTypeSyncError = nameof(LayoutSetsDataTypeSyncError); public const string LayoutSetComponentIdSyncError = nameof(LayoutSetComponentIdSyncError); + public const string LayoutSetSubLayoutSyncError = nameof(LayoutSetSubLayoutSyncError); public const string SettingsComponentIdSyncError = nameof(SettingsComponentIdSyncError); public const string LayoutPageAddSyncError = nameof(LayoutPageAddSyncError); } diff --git a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs index cb15df89f79..8acf823a8a2 100644 --- a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs +++ b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs @@ -1,7 +1,8 @@ -using System.IO; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; -using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Altinn.Platform.Storage.Interface.Models; using Altinn.Studio.Designer.Factories; @@ -133,6 +134,32 @@ public async Task DeleteLayoutSet_AppWithoutLayoutSets_ReturnsNotFound(string or response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + [Theory] + [InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet3", + "layoutSet2", "layoutFile1InSet2", "subform-component-id")] + public async Task DeleteLayoutSet_RemovesComponentsReferencingLayoutSet(string org, string app, string developer, string layoutSetToDeleteId, + string layoutSetWithRef, string layoutSetFile, string deletedComponentId) + { + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, app, developer, targetRepository); + + string url = $"{VersionPrefix(org, targetRepository)}/layout-set/{layoutSetToDeleteId}"; + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Delete, url); + + using var response = await HttpClient.SendAsync(httpRequestMessage); + response.StatusCode.Should().Be(HttpStatusCode.OK, await response.Content.ReadAsStringAsync()); + + JsonNode formLayout = (await GetFormLayouts(org, targetRepository, developer, layoutSetWithRef))[layoutSetFile]; + JsonArray layout = formLayout["data"]?["layout"] as JsonArray; + + layout.Should().NotBeNull(); + layout + .Where(jsonNode => jsonNode["layoutSet"] != null) + .Should() + .NotContain(jsonNode => jsonNode["layoutSet"].GetValue() == deletedComponentId, + $"No components should reference the deleted layout set {deletedComponentId}"); + } + private async Task GetLayoutSetsFile(string org, string app, string developer) { AltinnGitRepositoryFactory altinnGitRepositoryFactory = @@ -143,6 +170,16 @@ private async Task GetLayoutSetsFile(string org, string app, string return await altinnAppGitRepository.GetLayoutSetsFile(); } + private async Task> GetFormLayouts(string org, string app, string developer, string layoutSetName) + { + AltinnGitRepositoryFactory altinnGitRepositoryFactory = + new(TestDataHelper.GetTestDataRepositoriesRootDirectory()); + AltinnAppGitRepository altinnAppGitRepository = + altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer); + Dictionary formLayouts = await altinnAppGitRepository.GetFormLayouts(layoutSetName); + return formLayouts; + } + private async Task GetApplicationMetadataFile(string org, string app, string developer) { AltinnGitRepositoryFactory altinnGitRepositoryFactory = diff --git a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json index 2e97608b86d..32c4c651167 100644 --- a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json +++ b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json @@ -1,13 +1,19 @@ { - "schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", - "data": { - "layout": [{ - "id": "component-id", - "type": "Header", - "textResourceBindings": { - "title": "some-old-id", - "body": "another-key" - } - }] - } + "schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "component-id", + "type": "Header", + "textResourceBindings": { + "title": "some-old-id", + "body": "another-key" + } + }, + { + "id": "subform-component-id", + "layoutSet": "layoutSet3" + } + ] + } } diff --git a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts index e2611f292f1..87de0c60acd 100644 --- a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts +++ b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts @@ -67,7 +67,7 @@ describe('SyncSuccessQueriesInvalidator', () => { await waitFor(() => expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ - queryKey: [QueryKey.FormLayouts, org, app, selectedLayoutSet], + queryKey: [QueryKey.FormLayouts, org, app], }), ); expect(queryClientMock.invalidateQueries).toHaveBeenCalledTimes(1); diff --git a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts index 3bfd79198d4..0e240ae8590 100644 --- a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts +++ b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts @@ -30,7 +30,7 @@ export class SyncSuccessQueriesInvalidator extends Queue { // Maps folder names to their cache keys for invalidation upon sync success - can be extended to include more folders private readonly folderNameCacheKeyMap: Record> = { - layouts: [QueryKey.FormLayouts, '[org]', '[app]', '[layoutSetName]'], + layouts: [QueryKey.FormLayouts, '[org]', '[app]'], }; public set layoutSetName(layoutSetName: string) { From 7ca2d6083284a2918368436d58c580f3f49a46fe Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:49:23 +0200 Subject: [PATCH 02/11] refactor: adding new header component to three dots menu (#13746) --- .../GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx b/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx index 77ad759d28d..e89d26363ad 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx @@ -4,7 +4,7 @@ import { TabsIcon, MenuElipsisVerticalIcon, GiteaIcon } from '@studio/icons'; import { useTranslation } from 'react-i18next'; import { repositoryPath } from 'app-shared/api/paths'; import { Link } from '@digdir/designsystemet-react'; -import { StudioButton, StudioPopover } from '@studio/components'; +import { StudioButton, StudioPageHeader, StudioPopover } from '@studio/components'; import { LocalChangesModal } from './LocalChangesModal'; import { ClonePopoverContent } from './ClonePopoverContent'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; @@ -23,11 +23,11 @@ export const ThreeDotsMenu = ({ isClonePossible = false }: ThreeDotsMenuProps) = return ( - } title={t('sync_header.gitea_menu')} - variant='tertiary' + color='light' + variant='regular' /> From 120cd9d8da8a7f6e83b7bd455d890135f4b00af9 Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:49:55 +0200 Subject: [PATCH 03/11] refactor: move use user name and org hook (#13744) --- frontend/app-development/layout/PageHeader/PageHeader.tsx | 2 +- .../layout/PageHeader/SmallHeaderMenu/SmallHeaderMenu.tsx | 2 +- .../src/components/UserProfileMenu/UserProfileMenu.tsx | 2 +- .../src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx | 2 +- .../AltinnHeaderProfile => }/hooks/useUserNameAndOrg.test.ts | 0 .../AltinnHeaderProfile => }/hooks/useUserNameAndOrg.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename frontend/packages/shared/src/{components/AltinnHeaderProfile => }/hooks/useUserNameAndOrg.test.ts (100%) rename frontend/packages/shared/src/{components/AltinnHeaderProfile => }/hooks/useUserNameAndOrg.ts (88%) diff --git a/frontend/app-development/layout/PageHeader/PageHeader.tsx b/frontend/app-development/layout/PageHeader/PageHeader.tsx index 1b84744b5e1..a137d55fcec 100644 --- a/frontend/app-development/layout/PageHeader/PageHeader.tsx +++ b/frontend/app-development/layout/PageHeader/PageHeader.tsx @@ -9,7 +9,7 @@ import { type HeaderMenuItem } from 'app-development/types/HeaderMenu/HeaderMenu import { useTranslation } from 'react-i18next'; import { LargeNavigationMenu } from './LargeNavigationMenu'; import { usePageHeaderContext } from 'app-development/contexts/PageHeaderContext'; -import { useUserNameAndOrg } from 'app-shared/components/AltinnHeaderProfile/hooks/useUserNameAndOrg'; +import { useUserNameAndOrg } from 'app-shared/hooks/useUserNameAndOrg'; export type PageHeaderProps = { showSubMenu: boolean; diff --git a/frontend/app-development/layout/PageHeader/SmallHeaderMenu/SmallHeaderMenu.tsx b/frontend/app-development/layout/PageHeader/SmallHeaderMenu/SmallHeaderMenu.tsx index e322d9a0639..a658fb9dc86 100644 --- a/frontend/app-development/layout/PageHeader/SmallHeaderMenu/SmallHeaderMenu.tsx +++ b/frontend/app-development/layout/PageHeader/SmallHeaderMenu/SmallHeaderMenu.tsx @@ -15,7 +15,7 @@ import { SmallHeaderMenuItem } from './SmallHeaderMenuItem'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { useRepoMetadataQuery } from 'app-shared/hooks/queries'; import { usePageHeaderContext } from 'app-development/contexts/PageHeaderContext'; -import { useUserNameAndOrg } from 'app-shared/components/AltinnHeaderProfile/hooks/useUserNameAndOrg'; +import { useUserNameAndOrg } from 'app-shared/hooks/useUserNameAndOrg'; import { type HeaderMenuGroup } from 'app-development/types/HeaderMenu/HeaderMenuGroup'; import { groupMenuItemsByGroup, diff --git a/frontend/app-preview/src/components/UserProfileMenu/UserProfileMenu.tsx b/frontend/app-preview/src/components/UserProfileMenu/UserProfileMenu.tsx index 9f2686562b4..92b499306f5 100644 --- a/frontend/app-preview/src/components/UserProfileMenu/UserProfileMenu.tsx +++ b/frontend/app-preview/src/components/UserProfileMenu/UserProfileMenu.tsx @@ -1,7 +1,7 @@ import React, { type ReactElement } from 'react'; import { type Repository, type User } from 'app-shared/types/Repository'; import { useTranslation } from 'react-i18next'; -import { useUserNameAndOrg } from 'app-shared/components/AltinnHeaderProfile/hooks/useUserNameAndOrg'; +import { useUserNameAndOrg } from 'app-shared/hooks/useUserNameAndOrg'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { useMediaQuery, diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx b/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx index 2c235a6a917..7dc03fa57fd 100644 --- a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx +++ b/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx @@ -4,7 +4,7 @@ import type { Repository, User } from 'app-shared/types/Repository'; import type { ReactNode } from 'react'; import React from 'react'; import classes from './AltinnHeaderProfile.module.css'; -import { useUserNameAndOrg } from './hooks/useUserNameAndOrg'; +import { useUserNameAndOrg } from 'app-shared/hooks/useUserNameAndOrg'; export interface AltinnHeaderProfileProps { user: User; diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/hooks/useUserNameAndOrg.test.ts b/frontend/packages/shared/src/hooks/useUserNameAndOrg.test.ts similarity index 100% rename from frontend/packages/shared/src/components/AltinnHeaderProfile/hooks/useUserNameAndOrg.test.ts rename to frontend/packages/shared/src/hooks/useUserNameAndOrg.test.ts diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/hooks/useUserNameAndOrg.ts b/frontend/packages/shared/src/hooks/useUserNameAndOrg.ts similarity index 88% rename from frontend/packages/shared/src/components/AltinnHeaderProfile/hooks/useUserNameAndOrg.ts rename to frontend/packages/shared/src/hooks/useUserNameAndOrg.ts index 26e11a71038..a1066c77fb5 100644 --- a/frontend/packages/shared/src/components/AltinnHeaderProfile/hooks/useUserNameAndOrg.ts +++ b/frontend/packages/shared/src/hooks/useUserNameAndOrg.ts @@ -23,4 +23,4 @@ export const useUserNameAndOrg = (user: User, org: string, repository: Repositor return getUsername(user); }; -const getUsername = (user: User | Repository['owner']) => user?.full_name || user?.login; +const getUsername = (user: User | Repository['owner']) => user.full_name || user.login; From f4db101afdd73906388c4dbf7c4590276555d352 Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:54:46 +0200 Subject: [PATCH 04/11] refactor: adding new header component to fetch changes popover (#13747) --- .../VersionControlButtons.module.css | 1 + .../FetchChangesPopover.test.tsx | 22 +++++++++++++++++++ .../FetchChangesPopover.tsx | 15 ++++++++----- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/VersionControlButtons.module.css b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/VersionControlButtons.module.css index 1998511754f..6e7043d211d 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/VersionControlButtons.module.css +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/VersionControlButtons.module.css @@ -6,4 +6,5 @@ justify-content: flex-end; display: flex; flex-direction: row; + gap: var(--fds-spacing-2); } diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.test.tsx b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.test.tsx index af5486e9abc..79c47bb00ca 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.test.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.test.tsx @@ -14,6 +14,9 @@ import { type VersionControlButtonsContextProps, } from '../../context'; import { mockVersionControlButtonsContextValue } from '../../test/mocks/versionControlContextMock'; +import { useMediaQuery } from '@studio/components'; + +jest.mock('@studio/components/src/hooks/useMediaQuery'); const mockGetRepoPull = jest.fn(); @@ -121,6 +124,25 @@ describe('fetchChanges', () => { expect(mockVersionControlButtonsContextValue.commitAndPushChanges).toHaveBeenCalledWith(''); }); }); + + it('should render the button with text on a large screen', () => { + renderFetchChangesPopover(); + + expect(screen.getByText(textMock('sync_header.fetch_changes'))).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: textMock('sync_header.fetch_changes') }), + ).toBeInTheDocument(); + }); + + it('should not render the button text on a small screen', () => { + (useMediaQuery as jest.Mock).mockReturnValue(true); + renderFetchChangesPopover(); + + expect(screen.queryByText(textMock('sync_header.fetch_changes'))).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: textMock('sync_header.fetch_changes') }), + ).toBeInTheDocument(); + }); }); type Props = { diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.tsx b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.tsx index 83b1018ad56..884dfbb81a3 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/FetchChangesPopover/FetchChangesPopover.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { StudioButton, StudioPopover } from '@studio/components'; +import { StudioPageHeader, StudioPopover, useMediaQuery } from '@studio/components'; import { DownloadIcon } from '@studio/icons'; import classes from './FetchChangesPopover.module.css'; import { useTranslation } from 'react-i18next'; @@ -10,6 +10,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { GiteaFetchCompleted } from '../GiteaFetchCompleted'; import { useVersionControlButtonsContext } from '../../context'; import { SyncLoadingIndicator } from '../SyncLoadingIndicator'; +import { MEDIA_QUERY_MAX_WIDTH } from 'app-shared/constants'; export const FetchChangesPopover = (): React.ReactElement => { const { @@ -22,6 +23,7 @@ export const FetchChangesPopover = (): React.ReactElement => { } = useVersionControlButtonsContext(); const { t } = useTranslation(); + const shouldDisplayText = !useMediaQuery(MEDIA_QUERY_MAX_WIDTH); const { org, app } = useStudioEnvironmentParams(); const { refetch: fetchPullData } = useRepoPullQuery(org, app, true); const queryClient = useQueryClient(); @@ -56,16 +58,17 @@ export const FetchChangesPopover = (): React.ReactElement => { return ( - } + color='light' + variant='regular' + aria-label={t('sync_header.fetch_changes')} > - {t('sync_header.fetch_changes')} + {shouldDisplayText && t('sync_header.fetch_changes')} {displayNotification && } - + {isLoading && } From 85c907d3747114c6064a87faf2ceb7a8b228a540 Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:56:27 +0200 Subject: [PATCH 05/11] refactor: add new header to shared changes popover (#13748) --- .../CommitAndPushContent.tsx | 2 +- .../FileChangesInfoModal/index.ts | 1 + .../ShareChangesPopover.test.tsx | 22 +++++++++++++++++++ .../ShareChangesPopover.tsx | 16 +++++++++----- 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/index.ts diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/CommitAndPushContent.tsx b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/CommitAndPushContent.tsx index 338920bd709..d0b436f5519 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/CommitAndPushContent.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/CommitAndPushContent.tsx @@ -5,7 +5,7 @@ import { useVersionControlButtonsContext } from '../../../context'; import { Heading, Paragraph } from '@digdir/designsystemet-react'; import { StudioButton, StudioTextarea } from '@studio/components'; import type { RepoContentStatus } from 'app-shared/types/RepoStatus'; -import { FileChangesInfoModal } from './FileChangesInfoModal/FileChangesInfoModal'; +import { FileChangesInfoModal } from './FileChangesInfoModal'; export type CommitAndPushContentProps = { onClosePopover: () => void; diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/index.ts b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/index.ts new file mode 100644 index 00000000000..bedf3c953bd --- /dev/null +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/index.ts @@ -0,0 +1 @@ +export { FileChangesInfoModal } from './FileChangesInfoModal'; diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.test.tsx b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.test.tsx index b6c0c8a79d8..9a7ea345f42 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.test.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.test.tsx @@ -18,6 +18,9 @@ import { MemoryRouter } from 'react-router-dom'; import { app, org } from '@studio/testing/testids'; import { QueryKey } from 'app-shared/types/QueryKey'; import { repository } from 'app-shared/mocks/mocks'; +import { useMediaQuery } from '@studio/components'; + +jest.mock('@studio/components/src/hooks/useMediaQuery'); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -183,6 +186,25 @@ describe('shareChanges', () => { expect(screen.queryByText(textMock('sync_header.nothing_to_push'))).not.toBeInTheDocument(); expect(screen.getByText(textMock('sync_header.changes_to_share'))).toBeInTheDocument(); }); + + it('should render the button with text on a large screen', () => { + renderShareChangesPopover(); + + expect(screen.getByText(textMock('sync_header.changes_to_share'))).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: textMock('sync_header.changes_to_share') }), + ).toBeInTheDocument(); + }); + + it('should not render the button text on a small screen', () => { + (useMediaQuery as jest.Mock).mockReturnValue(true); + renderShareChangesPopover(); + + expect(screen.queryByText(textMock('sync_header.changes_to_share'))).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: textMock('sync_header.changes_to_share') }), + ).toBeInTheDocument(); + }); }); type Props = { diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.tsx b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.tsx index 9f0d86a1dd6..2729ca61fdc 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/ShareChangesPopover.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { StudioButton, StudioPopover } from '@studio/components'; +import { StudioPageHeader, StudioPopover, useMediaQuery } from '@studio/components'; import { UploadIcon } from '@studio/icons'; import classes from './ShareChangesPopover.module.css'; import { useTranslation } from 'react-i18next'; @@ -12,12 +12,15 @@ import { SyncLoadingIndicator } from '../SyncLoadingIndicator'; import type { IContentStatus, IGitStatus } from 'app-shared/types/global'; import { CommitAndPushContent } from './CommitAndPushContent'; import type { RepoContentStatus } from 'app-shared/types/RepoStatus'; +import { MEDIA_QUERY_MAX_WIDTH } from 'app-shared/constants'; export const ShareChangesPopover = () => { const { isLoading, setIsLoading, hasPushRights, hasMergeConflict, repoStatus } = useVersionControlButtonsContext(); const { t } = useTranslation(); + const shouldDisplayText = !useMediaQuery(MEDIA_QUERY_MAX_WIDTH); + const { org, app } = useStudioEnvironmentParams(); const { refetch: refetchRepoStatus } = useRepoStatusQuery(org, app); @@ -58,17 +61,18 @@ export const ShareChangesPopover = () => { return ( - } + color='light' + variant='regular' + aria-label={t('sync_header.changes_to_share')} > - {t('sync_header.changes_to_share')} + {shouldDisplayText && t('sync_header.changes_to_share')} {displayNotification && } - + Date: Thu, 10 Oct 2024 09:48:58 +0200 Subject: [PATCH 06/11] refactor: delete old unused Altinn header (#13745) --- .../AltinnHeaderProfile.module.css | 5 - .../AltinnHeaderProfile.test.tsx | 99 ---------- .../AltinnHeaderProfile.tsx | 44 ----- .../components/AltinnHeaderProfile/index.ts | 1 - .../altinnHeader/AltinnHeader.module.css | 74 ------- .../altinnHeader/AltinnHeader.test.tsx | 187 ------------------ .../components/altinnHeader/AltinnHeader.tsx | 88 --------- .../src/components/altinnHeader/index.ts | 1 - .../src/components/altinnHeader/types.ts | 9 - .../AltinnHeaderButton.module.css | 8 - .../AltinnHeaderButton.test.tsx | 37 ---- .../AltinnHeaderButton.tsx | 31 --- .../components/altinnHeaderButtons/index.ts | 1 - .../AltinnHeaderMenu.module.css | 36 ---- .../AltinnHeaderMenu.test.tsx | 72 ------- .../altinnHeaderMenu/AltinnHeaderMenu.tsx | 33 ---- .../src/components/altinnHeaderMenu/index.ts | 1 - .../altinnSubHeader/AltinnSubMenu.module.css | 15 -- .../altinnSubHeader/AltinnSubMenu.test.tsx | 21 -- .../altinnSubHeader/AltinnSubMenu.tsx | 17 -- .../src/components/altinnSubHeader/index.ts | 1 - .../packages/shared/src/components/index.ts | 1 - .../molecules/AltinnMenu.module.css | 7 - .../src/components/molecules/AltinnMenu.tsx | 28 --- .../packages/shared/src/enums/TopBarMenu.ts | 11 -- .../main-header/AltinnStudioLogo.tsx | 68 ------- .../navigation/main-header/Header.module.css | 17 -- .../navigation/main-header/Header.test.tsx | 121 ------------ .../src/navigation/main-header/Header.tsx | 75 ------- .../main-header/HeaderMenu.module.css | 13 -- .../main-header/HeaderMenu.test.tsx | 138 ------------- .../src/navigation/main-header/HeaderMenu.tsx | 127 ------------ .../ProfileMenu/ProfileMenu.module.css | 29 --- .../ProfileMenu/ProfileMenu.test.tsx | 86 -------- .../main-header/ProfileMenu/ProfileMenu.tsx | 129 ------------ .../main-header/ProfileMenu/index.ts | 1 - .../shared/src/types/TopBarMenuItem.ts | 12 -- .../components/config/FormComponentConfig.tsx | 3 +- 38 files changed, 1 insertion(+), 1646 deletions(-) delete mode 100644 frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.module.css delete mode 100644 frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.test.tsx delete mode 100644 frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx delete mode 100644 frontend/packages/shared/src/components/AltinnHeaderProfile/index.ts delete mode 100644 frontend/packages/shared/src/components/altinnHeader/AltinnHeader.module.css delete mode 100644 frontend/packages/shared/src/components/altinnHeader/AltinnHeader.test.tsx delete mode 100644 frontend/packages/shared/src/components/altinnHeader/AltinnHeader.tsx delete mode 100644 frontend/packages/shared/src/components/altinnHeader/index.ts delete mode 100644 frontend/packages/shared/src/components/altinnHeader/types.ts delete mode 100644 frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.module.css delete mode 100644 frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.test.tsx delete mode 100644 frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.tsx delete mode 100644 frontend/packages/shared/src/components/altinnHeaderButtons/index.ts delete mode 100644 frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.module.css delete mode 100644 frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.test.tsx delete mode 100644 frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.tsx delete mode 100644 frontend/packages/shared/src/components/altinnHeaderMenu/index.ts delete mode 100644 frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.module.css delete mode 100644 frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.test.tsx delete mode 100644 frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.tsx delete mode 100644 frontend/packages/shared/src/components/altinnSubHeader/index.ts delete mode 100644 frontend/packages/shared/src/components/molecules/AltinnMenu.module.css delete mode 100644 frontend/packages/shared/src/components/molecules/AltinnMenu.tsx delete mode 100644 frontend/packages/shared/src/enums/TopBarMenu.ts delete mode 100644 frontend/packages/shared/src/navigation/main-header/AltinnStudioLogo.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/Header.module.css delete mode 100644 frontend/packages/shared/src/navigation/main-header/Header.test.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/Header.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/HeaderMenu.module.css delete mode 100644 frontend/packages/shared/src/navigation/main-header/HeaderMenu.test.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/HeaderMenu.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.module.css delete mode 100644 frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.test.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.tsx delete mode 100644 frontend/packages/shared/src/navigation/main-header/ProfileMenu/index.ts delete mode 100644 frontend/packages/shared/src/types/TopBarMenuItem.ts diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.module.css b/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.module.css deleted file mode 100644 index 82e13bb62a0..00000000000 --- a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.profileMenuWrapper { - display: flex; - justify-content: flex-end; - align-items: center; -} diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.test.tsx b/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.test.tsx deleted file mode 100644 index 6d187d2251a..00000000000 --- a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.test.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; -import type { AltinnHeaderProfileProps } from './AltinnHeaderProfile'; -import { AltinnHeaderProfile } from './AltinnHeaderProfile'; -import { textMock } from '@studio/testing/mocks/i18nMock'; -import { app, org } from '@studio/testing/testids'; -import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; -import { queriesMock } from 'app-shared/mocks/queriesMock'; -import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; - -describe('AltinnHeaderProfile', () => { - it('should render users name if user and org are the same', () => { - render({ org: 'test-user' }); - expect(screen.getByText('test-user')).toBeInTheDocument(); - }); - - it('should render users full name if it exists', () => { - render({ - org: 'test-user', - user: { - avatar_url: 'avatar_url', - email: 'test@email.com', - full_name: 'Test User', - id: 1, - login: 'test-user', - userType: 0, - }, - }); - expect(screen.queryByText('test-user')).not.toBeInTheDocument(); - expect(screen.getByText('Test User')).toBeInTheDocument(); - }); - - it('should render users name and name of org the user represents', () => { - render({ org }); - expect( - screen.getByText( - textMock('shared.header_user_for_org', { user: 'test-user', org: 'Test Org' }), - ), - ).toBeInTheDocument(); - }); -}); - -export const render = (props?: Partial) => { - const defaultProps: AltinnHeaderProfileProps = { - org, - repository: { - clone_url: 'clone_url', - description: 'description', - full_name: 'Test App', - html_url: 'html_url', - id: 1, - is_cloned_to_local: false, - name: app, - owner: { - avatar_url: 'avatar_url', - full_name: 'Test Org', - login: org, - email: 'test-email', - id: 1, - userType: 1, - }, - updated_at: 'never', - created_at: 'now', - permissions: { - pull: true, - push: true, - admin: true, - }, - default_branch: 'master', - private: false, - empty: false, - size: 1, - fork: false, - forks_count: 0, - mirror: false, - open_issues_count: 0, - watchers_count: 0, - repositoryCreatedStatus: 1, - ssh_url: 'ssh_url', - stars_count: 0, - website: 'website', - }, - user: { - avatar_url: 'avatar_url', - email: 'test@email.com', - full_name: undefined, - id: 1, - login: 'test-user', - userType: 0, - }, - }; - - // This code will be replaced in issue: #11611 which is being split up into smaller chunks now. - return rtlRender( - - - , - ); -}; diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx b/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx deleted file mode 100644 index 7dc03fa57fd..00000000000 --- a/frontend/packages/shared/src/components/AltinnHeaderProfile/AltinnHeaderProfile.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { ProfileMenu } from 'app-shared/navigation/main-header/ProfileMenu'; -import type { Repository, User } from 'app-shared/types/Repository'; - -import type { ReactNode } from 'react'; -import React from 'react'; -import classes from './AltinnHeaderProfile.module.css'; -import { useUserNameAndOrg } from 'app-shared/hooks/useUserNameAndOrg'; - -export interface AltinnHeaderProfileProps { - user: User; - org: string; - repository: Repository; -} - -/** - * @component - * Dispalys the Heacer Profile in the Altinn header - * - * @property {User}[user] - the user - * @property {string}[org] - the org - * @property {Repository}[repository] - the repository - * - * @returns {ReactNode} - The Rendered component - */ -export const AltinnHeaderProfile = ({ - user, - repository, - org, -}: AltinnHeaderProfileProps): ReactNode => { - const userNameAndOrg = useUserNameAndOrg(user, org, repository); - - return ( -
- {user && ( - - )} -
- ); -}; diff --git a/frontend/packages/shared/src/components/AltinnHeaderProfile/index.ts b/frontend/packages/shared/src/components/AltinnHeaderProfile/index.ts deleted file mode 100644 index 8cb8b6b31ee..00000000000 --- a/frontend/packages/shared/src/components/AltinnHeaderProfile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AltinnHeaderProfile } from './AltinnHeaderProfile'; diff --git a/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.module.css b/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.module.css deleted file mode 100644 index 02e33637a34..00000000000 --- a/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.module.css +++ /dev/null @@ -1,74 +0,0 @@ -.altinnHeaderBar { - display: flex; - align-items: center; - height: var(--toolbar-height); - padding: 0 2rem; - flex: 2; -} - -.regular { - background-color: #022f51; - color: #fff; -} - -.preview { - background-color: #118849; - color: #fff; -} - -.altinnHeaderBar .leftContent { - align-items: center; - border-right: 1px solid #fff2; - display: flex; - flex: 2; - line-height: 1; - overflow: hidden; - padding-right: 1rem; -} - -.appName { - font-size: 1.125rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.bigSlash { - font-size: 2rem; - padding: 0 0.5rem; -} - -.headingMenuItem { - padding-left: var(--fds-spacing-8); - color: var(--fds-semantic-background-default); -} - -.altinnHeaderBar .rightContent { - align-items: center; - display: flex; - flex: 3; - gap: 1rem; - justify-content: flex-end; -} - -.profileMenuWrapper { - flex: 3; - display: flex; - justify-content: flex-end; - align-items: center; -} - -.userOrgNames { - text-align: right; - white-space: pre-line; - line-height: 25px; - font-size: 1rem; - margin-right: 1px; -} - -.rightContentButtons { - align-items: center; - display: flex; - gap: 1rem; - padding: 15px; -} diff --git a/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.test.tsx b/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.test.tsx deleted file mode 100644 index a62f12c269b..00000000000 --- a/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.test.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; -import type { AltinnHeaderProps } from './AltinnHeader'; -import { AltinnHeader } from './AltinnHeader'; -import { Button } from '@digdir/designsystemet-react'; -import { textMock } from '@studio/testing/mocks/i18nMock'; -import { RepositoryType } from 'app-shared/types/global'; -import { TopBarMenu } from 'app-shared/enums/TopBarMenu'; -import { MemoryRouter } from 'react-router-dom'; -import type { AltinnButtonActionItem } from './types'; -import { app, org } from '@studio/testing/testids'; -import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; -import { queriesMock } from 'app-shared/mocks/queriesMock'; -import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; - -const mockTo: string = '/test'; -const mockMenuKey: TopBarMenu = TopBarMenu.About; -const mockAction: AltinnButtonActionItem = { - menuKey: mockMenuKey, - to: mockTo, -}; - -describe('AltinnHeader', () => { - afterEach(jest.clearAllMocks); - - it('should render heading when provided', () => { - const heading: string = 'Test heading'; - render({ heading }); - expect(screen.getByText(heading)).toBeInTheDocument(); - }); - - it('should not render heading when not provided', () => { - render(); - expect(screen.queryByRole('heading')).not.toBeInTheDocument(); - }); - - it('should render AltinnHeaderMenu when provided', () => { - render({ - menuItems: [ - { - key: TopBarMenu.Preview, - link: 'Link1', - repositoryTypes: [RepositoryType.App, RepositoryType.DataModels], - }, - ], - }); - expect(screen.getByRole('link', { name: textMock('top_menu.preview') })).toBeInTheDocument(); - }); - - it('should not render AltinnHeaderMenu when not provided', () => { - render(); - expect(screen.getAllByRole('link').length).toBe(1); // Only dashboard link - }); - - it('should render AltinnHeaderMenu with only data models menu item when repositoryType is data models', () => { - render({ - menuItems: [ - { - key: TopBarMenu.About, - link: 'Link1', - repositoryTypes: [RepositoryType.App, RepositoryType.DataModels], - }, - { - key: TopBarMenu.DataModel, - link: 'Link2', - repositoryTypes: [RepositoryType.DataModels], - }, - ], - }); - expect(screen.getByRole('link', { name: textMock(TopBarMenu.DataModel) })).toBeInTheDocument(); - expect(screen.queryByText(textMock('about'))).not.toBeInTheDocument(); - }); - - it('should render AltinnHeaderButtons when buttonActions are provided', () => { - render({ - buttonActions: [mockAction], - }); - expect(screen.getByRole('link', { name: textMock(mockMenuKey) })).toBeInTheDocument(); - expect(screen.getAllByRole('button').length).toEqual(1); // Profile Menu - }); - - it('should not render AltinnHeaderButtons when buttonActions are not provided', () => { - render(); - expect(screen.getAllByRole('button').length).toEqual(1); // Only profile menu - }); - - it('should render subMenu with provided subMenuContent when showSubMenu is true', () => { - const subMenuContentText = 'subMenuContent'; - render({ - showSubMenu: true, - subMenuContent: , - }); - expect(screen.getByRole('button', { name: subMenuContentText })).toBeInTheDocument(); - }); - - it('should not render AltinnSubMenu when showSubMenu is false', () => { - const subMenuContentText = 'subMenuContent'; - render({ - showSubMenu: false, - subMenuContent: , - }); - expect(screen.queryByRole('button', { name: subMenuContentText })).not.toBeInTheDocument(); - }); - - it('should render Deploy header button when repo is owned by an org', () => { - render({ - repoOwnerIsOrg: true, - buttonActions: [mockAction], - }); - expect(screen.getByRole('link', { name: textMock(mockMenuKey) })).toBeInTheDocument(); - }); - - it('should not render Deploy header button when repo is owned by a private person', () => { - render({ - repoOwnerIsOrg: false, - buttonActions: [mockAction], - }); - expect( - screen.queryByRole('button', { name: textMock(TopBarMenu.Deploy) }), - ).not.toBeInTheDocument(); - }); -}); - -const render = (props: Partial = {}) => { - const defaultProps: AltinnHeaderProps = { - menuItems: [], - showSubMenu: true, - subMenuContent: null, - app, - org, - user: { - avatar_url: '', - email: 'test@email.com', - full_name: 'Test Testesen', - id: 1, - login: 'username', - userType: 0, - }, - repository: { - clone_url: 'clone_url', - description: 'description', - full_name: 'Test App', - html_url: 'html_url', - id: 1, - is_cloned_to_local: false, - name: app, - owner: { - avatar_url: 'avatar_url', - full_name: 'Test Org', - login: org, - email: 'test-email', - id: 1, - userType: 1, - }, - updated_at: 'never', - created_at: 'now', - permissions: { - pull: true, - push: true, - admin: true, - }, - default_branch: 'master', - private: false, - empty: false, - size: 1, - fork: false, - forks_count: 0, - mirror: false, - open_issues_count: 0, - watchers_count: 0, - repositoryCreatedStatus: 1, - ssh_url: 'ssh_url', - stars_count: 0, - website: 'website', - }, - buttonActions: [], - }; - - // This code will be replaced in issue: #11611 which is being split up into smaller chunks now. - return rtlRender( - - - - - , - ); -}; diff --git a/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.tsx b/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.tsx deleted file mode 100644 index 4b6aa2189d5..00000000000 --- a/frontend/packages/shared/src/components/altinnHeader/AltinnHeader.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import AltinnStudioLogo from 'app-shared/navigation/main-header/AltinnStudioLogo'; -import React from 'react'; -import classes from './AltinnHeader.module.css'; -import { AltinnSubMenu } from '../altinnSubHeader'; -import { AltinnHeaderMenu } from '../altinnHeaderMenu'; -import { AltinnHeaderButton } from '../altinnHeaderButtons/AltinnHeaderButton'; -import { AltinnHeaderProfile } from '../AltinnHeaderProfile'; -import type { User, Repository } from 'app-shared/types/Repository'; -import classnames from 'classnames'; -import type { AltinnButtonActionItem, AltinnHeaderVariant } from './types'; - -import type { TopBarMenuItem } from 'app-shared/types/TopBarMenuItem'; -import { getRepositoryType } from 'app-shared/utils/repository'; -import { RepositoryType } from 'app-shared/types/global'; -import { TopBarMenu } from 'app-shared/enums/TopBarMenu'; -import { useTranslation } from 'react-i18next'; -import { Paragraph } from '@digdir/designsystemet-react'; - -export interface AltinnHeaderProps { - heading?: string; - menuItems?: TopBarMenuItem[]; - showSubMenu: boolean; - subMenuContent?: JSX.Element; - repository: Repository; - user: User; - org: string; - app: string; - variant?: AltinnHeaderVariant; - repoOwnerIsOrg?: boolean; - buttonActions: AltinnButtonActionItem[]; -} - -export const AltinnHeader = ({ - heading, - menuItems, - showSubMenu, - repository, - org, - app, - user, - subMenuContent, - buttonActions, - variant = 'regular', - repoOwnerIsOrg, -}: AltinnHeaderProps) => { - const { t } = useTranslation(); - - const repositoryType = getRepositoryType(org, app); - - return ( -
-
-
- - - - {app && ( - <> - / - {app} - - )} -
- {heading && ( - - {heading} - - )} - {menuItems && } -
- {buttonActions && ( -
- {buttonActions.map((action) => - !repoOwnerIsOrg && action.menuKey === TopBarMenu.Deploy - ? null - : repositoryType !== RepositoryType.DataModels && ( - - ), - )} -
- )} - -
-
- {showSubMenu && {subMenuContent}} -
- ); -}; diff --git a/frontend/packages/shared/src/components/altinnHeader/index.ts b/frontend/packages/shared/src/components/altinnHeader/index.ts deleted file mode 100644 index 4a65eee605f..00000000000 --- a/frontend/packages/shared/src/components/altinnHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AltinnHeader } from './AltinnHeader'; diff --git a/frontend/packages/shared/src/components/altinnHeader/types.ts b/frontend/packages/shared/src/components/altinnHeader/types.ts deleted file mode 100644 index d83802236d4..00000000000 --- a/frontend/packages/shared/src/components/altinnHeader/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TopBarMenu } from 'app-shared/enums/TopBarMenu'; - -export type AltinnHeaderVariant = 'regular' | 'preview'; - -export interface AltinnButtonActionItem { - menuKey: TopBarMenu; - to: string; - isInverted?: boolean; -} diff --git a/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.module.css b/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.module.css deleted file mode 100644 index 80485cc9869..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.inverted { - color: var(--fds-semantic-text-neutral-on_inverted); - border-color: var(--fds-semantic-text-neutral-on_inverted); -} - -.inverted:hover { - background: var(--fds-semantic-surface-on_inverted-no_fill-hover); -} diff --git a/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.test.tsx b/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.test.tsx deleted file mode 100644 index bbd2d708c06..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; -import type { AltinnHeaderButtonProps } from './AltinnHeaderButton'; -import { AltinnHeaderButton } from './AltinnHeaderButton'; -import { textMock } from '@studio/testing/mocks/i18nMock'; -import type { AltinnButtonActionItem } from '../altinnHeader/types'; -import { TopBarMenu } from 'app-shared/enums/TopBarMenu'; - -const mockTo: string = '/test'; -const mockMenuKey: TopBarMenu = TopBarMenu.About; -const mockAction: AltinnButtonActionItem = { - menuKey: mockMenuKey, - to: mockTo, -}; - -describe('AltinnHeaderbuttons', () => { - afterEach(jest.clearAllMocks); - - it('should render nothing if action is undefined', () => { - render({ action: undefined }); - expect(screen.queryByRole('button')).toBeNull(); - }); - - it('should render the button for the provided action with the correct href', () => { - render(); - - const link = screen.getByRole('link', { name: textMock(mockMenuKey) }); - expect(link).toHaveAttribute('href', mockTo); - }); -}); - -const render = (props?: Partial) => { - const defaultProps: AltinnHeaderButtonProps = { - action: mockAction, - }; - rtlRender(, { wrapper: undefined }); -}; diff --git a/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.tsx b/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.tsx deleted file mode 100644 index 639a9970b17..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderButtons/AltinnHeaderButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { useTranslation } from 'react-i18next'; -import { StudioButton } from '@studio/components'; -import classes from './AltinnHeaderButton.module.css'; -import type { AltinnButtonActionItem } from '../altinnHeader/types'; - -export interface AltinnHeaderButtonProps { - action: AltinnButtonActionItem; -} - -export const AltinnHeaderButton = ({ action }: AltinnHeaderButtonProps) => { - const { t } = useTranslation(); - - if (!action) return null; - - return ( - - - {t(action.menuKey)} - - - ); -}; diff --git a/frontend/packages/shared/src/components/altinnHeaderButtons/index.ts b/frontend/packages/shared/src/components/altinnHeaderButtons/index.ts deleted file mode 100644 index f2cfe29f3ad..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderButtons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AltinnHeaderButton } from './AltinnHeaderButton'; diff --git a/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.module.css b/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.module.css deleted file mode 100644 index cce78c6b2b4..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.menu { - display: flex; - align-items: center; - justify-content: flex-start; - list-style-type: none; - flex: 2; - padding: 0 1rem; - gap: 2rem; - font-size: 1rem; -} - -.menuItem { - color: #fff; - font-size: 1rem; - display: flex; - align-items: center; - gap: 0.25rem; -} - -.menuItem a:hover { - border-bottom: 2px solid #fff; -} - -.menuItem a { - color: #fff; - text-decoration: none; -} - -.menuItem .active { - border-bottom: 2px solid #fff; -} - -.betaTag { - min-height: min-content; - padding: 0 var(--fds-spacing-1); -} diff --git a/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.test.tsx b/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.test.tsx deleted file mode 100644 index e8a4c38b3ca..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; -import type { IAltinnHeaderMenuProps } from './AltinnHeaderMenu'; -import { AltinnHeaderMenu } from './AltinnHeaderMenu'; -import { MemoryRouter } from 'react-router-dom'; -import type { TopBarMenuItem } from 'app-shared/types/TopBarMenuItem'; -import { TopBarMenu } from 'app-shared/enums/TopBarMenu'; -import { RepositoryType } from 'app-shared/types/global'; -import { textMock } from '@studio/testing/mocks/i18nMock'; - -const mockMenuItems: TopBarMenuItem[] = [ - { - key: TopBarMenu.About, - link: 'Link1', - repositoryTypes: [RepositoryType.App, RepositoryType.DataModels], - }, - { - key: TopBarMenu.Create, - link: 'Link2', - repositoryTypes: [RepositoryType.App], - }, - { - key: TopBarMenu.DataModel, - link: 'Link3', - repositoryTypes: [RepositoryType.App, RepositoryType.DataModels], - }, -]; - -const mockMenuItem4: TopBarMenuItem = { - key: TopBarMenu.ProcessEditor, - link: 'Link4', - repositoryTypes: [RepositoryType.App], - isBeta: true, -}; - -describe('AltinnHeaderMenu', () => { - afterEach(jest.clearAllMocks); - - it('Should render nothing if there are no provided meny items', () => { - render(); - expect(screen.queryByTestId('altinn-header-menu')).not.toBeInTheDocument(); - }); - - it('should render all provided menu items', () => { - render({ menuItems: mockMenuItems }); - expect(screen.queryAllByRole('link')).toHaveLength(3); - }); - - it('shouldm not render the beta tag when there is no beta', () => { - render({ menuItems: mockMenuItems }); - - expect(screen.queryByText(textMock('general.beta'))).not.toBeInTheDocument(); - }); - - it('should render the beta tag when an item is beta', () => { - render({ menuItems: [...mockMenuItems, mockMenuItem4] }); - - expect(screen.getByText(textMock('general.beta'))).toBeInTheDocument(); - }); -}); - -const render = (props: Partial = {}) => { - const defaultProps: IAltinnHeaderMenuProps = { - menuItems: [], - }; - - return rtlRender( - - - , - ); -}; diff --git a/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.tsx b/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.tsx deleted file mode 100644 index 83557b16deb..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderMenu/AltinnHeaderMenu.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import classes from './AltinnHeaderMenu.module.css'; -import { NavLink } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import type { TopBarMenuItem } from 'app-shared/types/TopBarMenuItem'; -import { Tag } from '@digdir/designsystemet-react'; - -export interface IAltinnHeaderMenuProps { - menuItems: TopBarMenuItem[]; -} - -export const AltinnHeaderMenu = ({ menuItems }: IAltinnHeaderMenuProps) => { - const { t } = useTranslation(); - - if (!menuItems?.length) return null; - - return ( -
    - {menuItems.map((item) => ( -
  • - (isActive ? classes.active : '')}> - {t(item.key)} - - {item.isBeta && ( - - {t('general.beta')} - - )} -
  • - ))} -
- ); -}; diff --git a/frontend/packages/shared/src/components/altinnHeaderMenu/index.ts b/frontend/packages/shared/src/components/altinnHeaderMenu/index.ts deleted file mode 100644 index b8ed0a1407b..00000000000 --- a/frontend/packages/shared/src/components/altinnHeaderMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AltinnHeaderMenu } from './AltinnHeaderMenu'; diff --git a/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.module.css b/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.module.css deleted file mode 100644 index 847f9bc4987..00000000000 --- a/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.subToolbar { - align-items: center; - display: flex; - height: var(--subtoolbar-height); - justify-content: space-between; - padding: 0 2rem; -} - -.regular { - background-color: #284e6a; -} - -.preview { - background-color: #0c6536; -} diff --git a/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.test.tsx b/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.test.tsx deleted file mode 100644 index 58067e4c90d..00000000000 --- a/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; -import type { AltinnSubMenuProps } from './AltinnSubMenu'; -import { AltinnSubMenu } from './AltinnSubMenu'; - -describe('AltinnSubMenu', () => { - it('should render provided child components', () => { - render({ - children: , - }); - expect(screen.getByRole('button', { name: 'test-button' })).toBeInTheDocument(); - }); -}); - -const render = (props?: Partial) => { - const defaultProps: AltinnSubMenuProps = { - variant: 'regular', - children: null, - }; - return rtlRender(); -}; diff --git a/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.tsx b/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.tsx deleted file mode 100644 index 7ac202226ae..00000000000 --- a/frontend/packages/shared/src/components/altinnSubHeader/AltinnSubMenu.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import classes from './AltinnSubMenu.module.css'; -import React from 'react'; -import type { AltinnHeaderVariant } from '../altinnHeader/types'; -import classnames from 'classnames'; - -export interface AltinnSubMenuProps { - variant: AltinnHeaderVariant; - children?: JSX.Element; -} - -export const AltinnSubMenu = ({ variant, children }: AltinnSubMenuProps) => { - return ( -
-
{children}
-
- ); -}; diff --git a/frontend/packages/shared/src/components/altinnSubHeader/index.ts b/frontend/packages/shared/src/components/altinnSubHeader/index.ts deleted file mode 100644 index 6bb7beceeaf..00000000000 --- a/frontend/packages/shared/src/components/altinnSubHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AltinnSubMenu } from './AltinnSubMenu'; diff --git a/frontend/packages/shared/src/components/index.ts b/frontend/packages/shared/src/components/index.ts index c388c92995e..9e4cfa30f43 100644 --- a/frontend/packages/shared/src/components/index.ts +++ b/frontend/packages/shared/src/components/index.ts @@ -1,2 +1 @@ -export { default as AltinnMenu } from './molecules/AltinnMenu'; export { AltinnConfirmDialog } from './AltinnConfirmDialog'; diff --git a/frontend/packages/shared/src/components/molecules/AltinnMenu.module.css b/frontend/packages/shared/src/components/molecules/AltinnMenu.module.css deleted file mode 100644 index 71937e0e7d5..00000000000 --- a/frontend/packages/shared/src/components/molecules/AltinnMenu.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.paper { - border: 1px solid #d3d4d5; -} - -.list { - padding: 0; -} diff --git a/frontend/packages/shared/src/components/molecules/AltinnMenu.tsx b/frontend/packages/shared/src/components/molecules/AltinnMenu.tsx deleted file mode 100644 index a9738880f7d..00000000000 --- a/frontend/packages/shared/src/components/molecules/AltinnMenu.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import type { MenuProps } from '@mui/material'; -import { Menu } from '@mui/material'; -import classes from './AltinnMenu.module.css'; - -function AltinnMenu(props: MenuProps) { - return ( - - ); -} - -export default AltinnMenu; diff --git a/frontend/packages/shared/src/enums/TopBarMenu.ts b/frontend/packages/shared/src/enums/TopBarMenu.ts deleted file mode 100644 index 1a9e33818cf..00000000000 --- a/frontend/packages/shared/src/enums/TopBarMenu.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum TopBarMenu { - About = 'top_menu.about', - Create = 'top_menu.create', - DataModel = 'top_menu.data_model', - Text = 'top_menu.texts', - Preview = 'top_menu.preview', - PreviewBackToEditing = 'top_menu.preview_back_to_editing', - Deploy = 'top_menu.deploy', - ProcessEditor = 'top_menu.process_editor', - None = '', -} diff --git a/frontend/packages/shared/src/navigation/main-header/AltinnStudioLogo.tsx b/frontend/packages/shared/src/navigation/main-header/AltinnStudioLogo.tsx deleted file mode 100644 index 95cf6c4bce6..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/AltinnStudioLogo.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; - -export default function AltinnStudioLogo() { - return ( - - Altinn logo - - - - - - - - - - - - - - - ); -} diff --git a/frontend/packages/shared/src/navigation/main-header/Header.module.css b/frontend/packages/shared/src/navigation/main-header/Header.module.css deleted file mode 100644 index f65e28c6e93..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/Header.module.css +++ /dev/null @@ -1,17 +0,0 @@ -header.appBar { - background-color: #022f51; - box-shadow: none; - z-index: 1; -} - -.toolbar { - height: 96px; - padding-left: 48px; - padding-right: 48px; -} - -.divider { - font-size: 2rem; - margin-left: 10px; - margin-right: 10px; -} diff --git a/frontend/packages/shared/src/navigation/main-header/Header.test.tsx b/frontend/packages/shared/src/navigation/main-header/Header.test.tsx deleted file mode 100644 index 48fa3b8de77..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/Header.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; - -import { render as rtlRender, screen } from '@testing-library/react'; -import Router from 'react-router-dom'; - -import type { IHeaderContext } from './Header'; -import { getOrgNameByUsername, Header, HeaderContext, SelectedContextType } from './Header'; - -const orgUsername = 'username1'; -const orgFullName = 'Organization 1'; - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(), - useNavigate: jest.fn(), - useLocation: jest.fn(), -})); - -describe('Header', () => { - const orgProps = { - avatar_url: 'avatar_url', - description: 'description', - id: 1, - location: 'location', - username: 'username1', - website: 'website', - full_name: 'full_name', - }; - - it(`should render org name when selected context is a org username`, () => { - render({ selectedContext: orgUsername }); - expect(screen.getByText(orgFullName)).toBeInTheDocument(); - }); - - Object.values(SelectedContextType).forEach((context) => { - it(`should not render org name when selected context is ${context}`, () => { - render({ selectedContext: context }); - expect(screen.queryByText(orgFullName)).not.toBeInTheDocument(); - }); - }); - - describe('getOrgNameByUsername', () => { - it('should return org name by username', () => { - const orgs = [ - { - ...orgProps, - id: 1, - full_name: 'full_name 1', - username: 'username1', - }, - { - ...orgProps, - id: 2, - full_name: 'full_name 2', - username: 'username2', - }, - ]; - - expect(getOrgNameByUsername('username1', orgs)).toBe('full_name 1'); - expect(getOrgNameByUsername('username2', orgs)).toBe('full_name 2'); - }); - - it('should return username by username', () => { - const orgs = [ - { - ...orgProps, - id: 1, - full_name: 'full_name 1', - }, - { - ...orgProps, - id: 2, - full_name: undefined, - username: 'username2', - }, - ]; - - expect(getOrgNameByUsername('username2', orgs)).toBe('username2'); - }); - - it('should return undefined when no orgs are defined', () => { - expect(getOrgNameByUsername('username2', undefined)).toBe(undefined); - }); - }); -}); - -const render = ({ - selectedContext = SelectedContextType.Self, -}: { - selectedContext: string | SelectedContextType; -}) => { - jest.spyOn(Router, 'useParams').mockReturnValue({ selectedContext }); - - const orgProps = { - avatar_url: 'avatar_url', - description: 'description', - id: 1, - location: 'location', - username: orgUsername, - website: 'website', - full_name: orgFullName, - }; - - const headerContextValue: IHeaderContext = { - selectableOrgs: [{ ...orgProps }], - user: { - full_name: 'John Smith', - avatar_url: 'avatar_url', - login: 'login', - email: '', - id: 0, - userType: 0, - }, - }; - - return rtlRender( - -
- , - ); -}; diff --git a/frontend/packages/shared/src/navigation/main-header/Header.tsx b/frontend/packages/shared/src/navigation/main-header/Header.tsx deleted file mode 100644 index f361ebf9b89..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/Header.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { AppBar, Grid, Toolbar, Typography } from '@mui/material'; -import type { Organization } from '../../types/Organization'; -import type { User } from '../../types/Repository'; -import AltinnStudioLogo from './AltinnStudioLogo'; -import { HeaderMenu } from './HeaderMenu'; -import classes from './Header.module.css'; -import { useSelectedContext } from 'dashboard/hooks/useSelectedContext'; - -export enum SelectedContextType { - All = 'all', - Self = 'self', - None = 'none', -} - -export interface IHeaderContext { - selectableOrgs?: Organization[]; - user: User; -} - -export const HeaderContext = React.createContext({ - selectableOrgs: undefined, - user: undefined, -}); - -export const getOrgNameByUsername = (username: string, orgs: Organization[]) => { - const org = orgs?.find((o) => o.username === username); - return org?.full_name || org?.username; -}; - -export const getOrgUsernameByUsername = (username: string, orgs: Organization[]) => { - const org = orgs?.find((o) => o.username === username); - return org?.username; -}; - -export type HeaderProps = { - showMenu?: boolean; -}; - -export function Header({ showMenu = true }: HeaderProps) { - const { selectableOrgs } = React.useContext(HeaderContext); - const selectedContext = useSelectedContext(); - - return ( - - - - - - - - - - {selectedContext !== SelectedContextType.All && - selectedContext !== SelectedContextType.Self && ( - - - / - {getOrgNameByUsername(selectedContext, selectableOrgs)} - - - )} - - {showMenu && ( - - - - )} - - - - ); -} - -export default Header; diff --git a/frontend/packages/shared/src/navigation/main-header/HeaderMenu.module.css b/frontend/packages/shared/src/navigation/main-header/HeaderMenu.module.css deleted file mode 100644 index befb66b5d0c..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/HeaderMenu.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.avatar { - height: 60px; - width: 60px; -} - -.typography { - text-align: right; -} - -.iconButton:hover, -.iconButton:focus { - background-color: #193d61; -} diff --git a/frontend/packages/shared/src/navigation/main-header/HeaderMenu.test.tsx b/frontend/packages/shared/src/navigation/main-header/HeaderMenu.test.tsx deleted file mode 100644 index 771b7fff81f..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/HeaderMenu.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; -import * as networking from '../../utils/networking'; -import type { IHeaderContext } from './Header'; -import { HeaderContext, SelectedContextType } from './Header'; -import type { HeaderMenuProps } from './HeaderMenu'; -import { HeaderMenu } from './HeaderMenu'; -import { render as rtlRender, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -const originalLocation = window.location; -jest.mock('../../utils/networking', () => ({ - __esModule: true, - ...jest.requireActual('../../utils/networking'), -})); - -const mockedNavigate = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockedNavigate, - useLocation: () => ({ - search: '', - }), -})); - -describe('HeaderMenu', () => { - beforeEach(() => { - delete window.location; - - window.location = { - ...originalLocation, - assign: jest.fn(), - }; - }); - - afterEach(() => { - window.location = originalLocation; - jest.restoreAllMocks(); - }); - - it('should call gitea logout api when clicking log out', async () => { - const postSpy = jest.spyOn(networking, 'post').mockResolvedValue(null); - render(); - - await openMenu(); - const logoutButton = screen.getByRole('menuitem', { - name: /shared\.header_logout/i, - }); - - await userEvent.click(logoutButton); // eslint-disable-line testing-library/no-unnecessary-act - - expect(postSpy).toHaveBeenCalledWith(`${window.location.origin}/repos/user/logout`); - }); - - it('should call setSelectedContext with all keyword when clicking All item in menu', async () => { - render(); - await openMenu(); - - const allItem = screen.getByRole('menuitem', { - name: /shared\.header_all/i, - }); - - await userEvent.click(allItem); // eslint-disable-line testing-library/no-unnecessary-act - - expect(mockedNavigate).toHaveBeenCalledWith('/' + SelectedContextType.All); - }); - - it('should call setSelectedContext with self keyword when clicking Self item in menu', async () => { - render(); - await openMenu(); - - const selfItem = screen.getByRole('menuitem', { - name: /john smith/i, - }); - - await userEvent.click(selfItem); // eslint-disable-line testing-library/no-unnecessary-act - - expect(mockedNavigate).toHaveBeenCalledWith('/self'); - }); - - it('should call setSelectedContext with org-id when selecting org as context', async () => { - render(); - await openMenu(); - - const orgItem = screen.getByRole('menuitem', { - name: /organization 1/i, - }); - - await userEvent.click(orgItem); // eslint-disable-line testing-library/no-unnecessary-act - expect(mockedNavigate).toHaveBeenCalledWith('/username'); - }); -}); - -const openMenu = async () => { - const user = userEvent.setup(); - const menuButton = screen.getByRole('button', { - name: /shared\.header_button_alt/i, - }); - await user.click(menuButton); -}; - -const render = (props: Partial = {}) => { - const handleSetSelectedContext = jest.fn(); - const headerContextValue: IHeaderContext = { - selectableOrgs: [ - { - avatar_url: 'avatar_url', - description: 'description', - id: 1, - location: 'location', - username: 'username', - website: 'website', - full_name: 'Organization 1', - }, - ], - user: { - full_name: 'John Smith', - avatar_url: 'avatar_url', - login: 'login', - email: '', - id: 0, - userType: 0, - }, - }; - const allProps = { - language: {}, - org: 'username', - ...props, - }; - - return { - rendered: rtlRender( - - - , - ), - handleSetSelectedContext, - }; -}; diff --git a/frontend/packages/shared/src/navigation/main-header/HeaderMenu.tsx b/frontend/packages/shared/src/navigation/main-header/HeaderMenu.tsx deleted file mode 100644 index 9bb74a686a0..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/HeaderMenu.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { Avatar, Divider, Grid, IconButton, MenuItem, Typography } from '@mui/material'; -import { AltinnMenu } from '../../components'; -import { post } from '../../utils/networking'; -import { getOrgNameByUsername, HeaderContext, SelectedContextType } from './Header'; -import { repositoryBasePath, repositoryOwnerPath, repositoryPath } from '../../api/paths'; -import classes from './HeaderMenu.module.css'; -import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useSelectedContext } from 'dashboard/hooks/useSelectedContext'; -import { orgMenuItemId, userMenuItemId } from '@studio/testing/testids'; - -export type HeaderMenuProps = { - org: string; - repo?: string; -}; - -export function HeaderMenu({ org, repo }: HeaderMenuProps) { - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const { user, selectableOrgs } = useContext(HeaderContext); - const { t } = useTranslation(); - const selectedContext = useSelectedContext(); - const navigate = useNavigate(); - const location = useLocation(); - - const openMenu = (e: React.MouseEvent) => { - e.stopPropagation(); - setMenuAnchorEl(e.currentTarget); - }; - - const closeMenu = (e: React.MouseEvent) => { - e.stopPropagation(); - setMenuAnchorEl(null); - }; - - const handleLogout = () => { - const altinnWindow: Window = window; - const url = `${altinnWindow.location.origin}/repos/user/logout`; - post(url).then(() => { - window.location.assign(`${altinnWindow.location.origin}/Home/Logout`); - }); - sessionStorage.clear(); - return true; - }; - - const handleSetSelectedContext = (context: string | SelectedContextType) => { - navigate('/' + context + location.search); - setMenuAnchorEl(null); - }; - - const getRepoPath = () => { - const owner = org || user?.login; - if (owner && repo) { - return repositoryPath(owner, repo); - } - if (owner) { - return repositoryOwnerPath(owner); - } - return repositoryBasePath(); - }; - - return ( - <> - - - - {user?.full_name || user?.login}{' '} - {selectedContext !== SelectedContextType.All && - selectedContext !== SelectedContextType.Self && ( - <> -
{t('shared.header_for')}{' '} - {getOrgNameByUsername(selectedContext, selectableOrgs)} - - )} -
-
- - - - - -
- - handleSetSelectedContext(SelectedContextType.All)} - > - {t('shared.header_all')} - - {selectableOrgs?.map((selectableOrg) => { - return ( - handleSetSelectedContext(selectableOrg.username)} - > - {selectableOrg.full_name || selectableOrg.username} - - ); - })} - handleSetSelectedContext(SelectedContextType.Self)} - > - {user?.full_name || user?.login} - - - - - - {t('shared.header_go_to_gitea')} - - - - {t('shared.header_logout')} - - - - ); -} diff --git a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.module.css b/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.module.css deleted file mode 100644 index 4c9cb27b24e..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.paperStyle { - border-radius: 1px; - min-width: 150px; - padding: 0; - top: 50px; - right: 25px; -} - -.menuItem { - font-size: 16px; - justify-content: flex-end; - padding-right: 25px; -} - -.userAvatar { - height: 40px; - width: 40px; - border-radius: 30px; - padding: 5px; -} - -.userOrgNames { - text-align: right; - white-space: pre-line; - line-height: 25px; - font-size: 1rem; - margin-right: 10px; - color: inherit; -} diff --git a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.test.tsx b/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.test.tsx deleted file mode 100644 index 401487beb74..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import { render as rtlRender, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type { IProfileMenuComponentProps } from './ProfileMenu'; -import { ProfileMenu } from './ProfileMenu'; -import { textMock } from '@studio/testing/mocks/i18nMock'; -import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; -import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; -import { queriesMock } from 'app-shared/mocks/queriesMock'; - -const user = userEvent.setup(); - -const render = (props: Partial = {}) => { - const allProps = { - showlogout: false, - user: false, - ...props, - } as IProfileMenuComponentProps; - return rtlRender( - - - , - ); -}; - -// This code will be replaced in issue: #11611 which is being split up into smaller chunks now. Thats why some tests are delted -describe('ProfileMenu', () => { - it('should show menu with link to documentation when clicking profile button', async () => { - render(); - - expect( - screen.queryByRole('menuitem', { name: textMock('sync_header.documentation') }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('menuitem', { name: textMock('dashboard.open_repository') }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('menuitem', { name: textMock('shared.header_logout') }), - ).not.toBeInTheDocument(); - - const profileBtn = screen.getByRole('button', { - name: textMock('general.profile_icon'), - }); - expect(profileBtn).toBeInTheDocument(); - await user.click(profileBtn); - - expect( - screen.getByRole('menuitem', { name: textMock('sync_header.documentation') }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: textMock('dashboard.open_repository') }), - ).toBeInTheDocument(); - expect( - screen.queryByRole('menuitem', { name: textMock('shared.header_logout') }), - ).not.toBeInTheDocument(); - }); - - it('should show menu with link to documentation, logout and open repository when showlogout is true, window object has org and repo properties, and clicking profile button', async () => { - delete window.location; - window.location = new URL('https://www.example.com/editor/org/app') as unknown as Location; - render({ showlogout: true }); - - expect( - screen.queryByRole('link', { name: textMock('sync_header.documentation') }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('link', { name: textMock('dashboard.open_repository') }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('menuitem', { name: textMock('shared.header_logout') }), - ).not.toBeInTheDocument(); - - const profileBtn = screen.getByRole('img', { name: textMock('general.profile_icon') }); - await user.click(profileBtn); - - expect( - screen.getByRole('link', { name: textMock('sync_header.documentation') }), - ).toBeInTheDocument(); - expect( - screen.getByRole('link', { name: textMock('dashboard.open_repository') }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: textMock('shared.header_logout') }), - ).toBeInTheDocument(); - }); -}); diff --git a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.tsx b/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.tsx deleted file mode 100644 index 2a4d7a7b66d..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/ProfileMenu.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import type { ReactNode } from 'react'; -import React, { useEffect, useRef, useState } from 'react'; -import classes from './ProfileMenu.module.css'; -import { Menu, MenuItem } from '@mui/material'; -import { altinnDocsUrl } from 'app-shared/ext-urls'; -import { repositoryPath } from '../../../api/paths'; -import { useTranslation } from 'react-i18next'; -import type { User } from 'app-shared/types/Repository'; -import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; -import { StudioButton } from '@studio/components'; -import { profileButtonId } from '@studio/testing/testids'; -import { useLogoutMutation } from 'app-shared/hooks/mutations/useLogoutMutation'; - -export interface IProfileMenuComponentProps { - showlogout?: boolean; - user: User; - userNameAndOrg: string; - repositoryError?: boolean; -} - -/** - * @component - * Displays the menu in the Altinn Header - * - * @property {boolean}[showlogout] - Optional flag for if logout button should be shown - * @property {User}[user] - The user - * @property {string}[userNameAndOrg] - The username and org string to display in the header - * - * @returns {ReactNode} - The rendered component - */ -export const ProfileMenu = ({ - showlogout, - user, - userNameAndOrg, - repositoryError, -}: IProfileMenuComponentProps): ReactNode => { - const menuRef = useRef(null); - const [menuOpen, setMenuOpen] = useState(false); - const { mutate: logout } = useLogoutMutation(); - - const { org, app } = useStudioEnvironmentParams(); - - const handleClick = (event: any) => setMenuOpen(true); - const handleClose = () => setMenuOpen(false); - const { t } = useTranslation(); - - /** - * Closes the menu when clicking outside the menu - */ - const handleClickOutside = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target)) { - handleClose(); - } - }; - - /** - * Closes the menu when clicking the ESCAPE key - */ - const handleEscapeKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') handleClose(); - }; - - /** - * Listens to the events of clicking outside or using the keys on the menu - */ - useEffect(() => { - const handleOutsideClick = (e: MouseEvent) => handleClickOutside(e); - const handleKeydown = (e: KeyboardEvent) => handleEscapeKey(e); - - document.addEventListener('click', handleOutsideClick); - document.addEventListener('keydown', handleKeydown); - - return () => { - document.removeEventListener('click', handleOutsideClick); - document.removeEventListener('keydown', handleKeydown); - }; - }); - - return ( - - {userNameAndOrg} - {t('general.profile_icon')} - - - { - // workaround for highlighted menu item not changing. - // https://github.com/mui-org/material-ui/issues/5186#issuecomment-337278330 - } - {org && app && !repositoryError && ( - - - {t('dashboard.open_repository')} - - - )} - - - {t('sync_header.documentation')} - - - {showlogout && ( - logout()} className={classes.menuItem}> - {t('shared.header_logout')} - - )} - - - ); -}; diff --git a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/index.ts b/frontend/packages/shared/src/navigation/main-header/ProfileMenu/index.ts deleted file mode 100644 index 6c275f637fa..00000000000 --- a/frontend/packages/shared/src/navigation/main-header/ProfileMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ProfileMenu } from './ProfileMenu'; diff --git a/frontend/packages/shared/src/types/TopBarMenuItem.ts b/frontend/packages/shared/src/types/TopBarMenuItem.ts deleted file mode 100644 index 2252390cbb3..00000000000 --- a/frontend/packages/shared/src/types/TopBarMenuItem.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TopBarMenu } from 'app-shared/enums/TopBarMenu'; -import type { RepositoryType } from './global'; -import type { SupportedFeatureFlags } from 'app-shared/utils/featureToggleUtils'; - -export interface TopBarMenuItem { - key: TopBarMenu; - link: string; - icon?: React.FC>; - repositoryTypes: RepositoryType[]; - featureFlagName?: SupportedFeatureFlags; - isBeta?: boolean; -} diff --git a/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx index b968d8f0194..30d06552541 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx @@ -14,7 +14,6 @@ import { useText } from '../../hooks'; import { getComponentPropertyLabel } from '../../utils/language'; import { getUnsupportedPropertyTypes } from '../../utils/component'; import { EditGrid } from './editModal/EditGrid'; -import { TopBarMenu } from 'app-shared/enums/TopBarMenu'; export interface IEditFormComponentProps { editFormId: string; @@ -85,7 +84,7 @@ export const FormComponentConfig = ({ {dataModelBindings?.properties && ( <> - {t(TopBarMenu.DataModel)} + {t('top_menu.data_model')} {Object.keys(dataModelBindings?.properties).map((propertyKey: any) => { return ( From 38eb673df60936bdffc41a7c9a6702f8acd3c1c1 Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:21:18 +0200 Subject: [PATCH 07/11] fix: fixing margin bug in app-preview (#13755) --- frontend/app-preview/App.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app-preview/App.css b/frontend/app-preview/App.css index b894a9b85f7..d4c242b2091 100644 --- a/frontend/app-preview/App.css +++ b/frontend/app-preview/App.css @@ -1,3 +1,7 @@ :root { font-family: Roboto, 'San Fransisco', 'Helvetica Neue', Helvetica, Arial, sans-serif; } + +body { + margin: 0; +} From a68dc4e8989dcb1baed8a416925bd1a49b9e175c Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:23:45 +0200 Subject: [PATCH 08/11] refactor: remove unused text key (#13756) --- frontend/language/src/en.json | 1 - frontend/language/src/nb.json | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/language/src/en.json b/frontend/language/src/en.json index f6cc6bd14c1..99155229286 100644 --- a/frontend/language/src/en.json +++ b/frontend/language/src/en.json @@ -771,7 +771,6 @@ "testing.testing_in_testenv_title": "Testing in test environment", "top_menu.about": "About", "top_menu.create": "Create", - "top_menu.dashboard": "Dashboard", "top_menu.data_model": "Data model", "top_menu.deploy": "Deploy", "top_menu.policy_editor": "Policy", diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 85512e48ccf..4872aacf45e 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1038,7 +1038,6 @@ "top_bar.group_tools": "Verktøy", "top_menu.about": "Oversikt", "top_menu.create": "Utforming", - "top_menu.dashboard": "Dashboard", "top_menu.data_model": "Datamodell", "top_menu.deploy": "Publiser", "top_menu.menu": "Meny", From 593a6eb6446a9ae8afd7a01054867a732120c9f0 Mon Sep 17 00:00:00 2001 From: andreastanderen <71079896+standeren@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:43:42 +0300 Subject: [PATCH 09/11] feat: add layout set config for sub form (#13653) Co-authored-by: Erling Hauan <148075168+ErlingHauan@users.noreply.github.com> Co-authored-by: Jonas Dyrlie Co-authored-by: davidovrelid.com --- frontend/language/src/nb.json | 7 + .../ConfigContent/EditPolicy/EditPolicy.tsx | 3 +- .../RedirectToCreatePageButton.tsx | 4 +- .../RedirectBox/RedirectBox.module.css | 0 .../components/RedirectBox/RedirectBox.tsx | 11 +- .../src/components/RedirectBox/index.ts | 0 .../src/types/api/LayoutSetsResponse.ts | 4 +- .../shared/src/utils/featureToggleUtils.ts | 2 +- .../ux-editor/src/AppContext.test.tsx | 2 +- .../ux-editor/src/classes/SubFormUtils.ts | 26 +++ .../src/classes/Subformutils.test.ts | 36 ++++ .../Elements/LayoutSetsContainer.test.tsx | 20 +- .../Elements/LayoutSetsContainer.tsx | 2 +- .../Elements/SubForm/SubFormUtils.test.ts | 6 +- .../DefinedLayoutSet.module.css | 5 + .../DefinedLayoutSet/DefinedLayoutSet.tsx | 37 ++++ .../EditLayoutSet/EditLayoutSet.tsx | 45 +++++ .../EditLayoutSetButton.test.tsx | 46 +++++ .../EditLayoutSetButtons.module.css | 4 + .../EditLayoutSetButtons.tsx | 29 +++ .../SelectLayoutSet.module.css | 6 + .../SelectLayoutSet/SelectLayoutSet.tsx | 69 +++++++ .../UndefinedLayoutSet.test.tsx | 42 ++++ .../UndefinedLayoutSet/UndefinedLayoutSet.tsx | 12 ++ .../EditLayoutSet/index.ts | 1 + .../EditLayoutSetForSubform.test.tsx | 180 ++++++++++++++++++ .../EditLayoutSetForSubform.tsx | 34 ++++ .../NoSubformLayoutsExist.module.css | 3 + .../NoSubformLayoutsExist.tsx | 14 ++ .../NoSubformLayoutsExist/index.ts | 1 + .../EditLayoutSetForSubform/index.ts | 1 + .../PropertiesHeader.module.css | 4 + .../PropertiesHeader.test.tsx | 22 ++- .../PropertiesHeader/PropertiesHeader.tsx | 8 + .../config/FormComponentConfig.test.tsx | 21 ++ .../components/config/FormComponentConfig.tsx | 11 +- .../RedirectToLayoutSet.module.css | 3 + .../RedirectToLayoutSet.test.tsx | 43 +++++ .../RedirectToLayoutSet.tsx | 40 ++++ .../editModal/RedirectToLayoutSet/index.ts | 1 + .../ux-editor/src/data/formItemConfig.ts | 2 +- .../src/hooks/useAppContext.test.tsx | 19 ++ .../ux-editor/src/hooks/useAppContext.ts | 9 +- .../ux-editor/src/testing/componentMocks.ts | 4 + 44 files changed, 808 insertions(+), 31 deletions(-) rename frontend/packages/{process-editor => shared}/src/components/RedirectBox/RedirectBox.module.css (100%) rename frontend/packages/{process-editor => shared}/src/components/RedirectBox/RedirectBox.tsx (59%) rename frontend/packages/{process-editor => shared}/src/components/RedirectBox/index.ts (100%) create mode 100644 frontend/packages/ux-editor/src/classes/SubFormUtils.ts create mode 100644 frontend/packages/ux-editor/src/classes/Subformutils.test.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButton.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/index.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/index.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/index.ts create mode 100644 frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.module.css create mode 100644 frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.tsx create mode 100644 frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/index.ts create mode 100644 frontend/packages/ux-editor/src/hooks/useAppContext.test.tsx diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 4872aacf45e..cb90e9c62ba 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1284,6 +1284,13 @@ "ux_editor.component_properties.stickyHeader": "Fest tittelraden", "ux_editor.component_properties.style": "Stil", "ux_editor.component_properties.subdomains": "Subdomener (kommaseparert)", + "ux_editor.component_properties.subform": "Sidegruppe for underskjema", + "ux_editor.component_properties.subform.choose_layout_set": "Velg sidegruppe...", + "ux_editor.component_properties.subform.choose_layout_set_label": "Velg sidegruppe å knytte til underskjema", + "ux_editor.component_properties.subform.go_to_layout_set": "Gå til utforming av underskjemaet", + "ux_editor.component_properties.subform.no_layout_sets_acting_as_subform": "Det finnes ingen sidegrupper i løsningen som kan brukes som et underskjema", + "ux_editor.component_properties.subform.selected_layout_set_label": "Underskjema", + "ux_editor.component_properties.subform.selected_layout_set_title": "Endre underskjemakobling til {{subform}}", "ux_editor.component_properties.summary.add_override": "Legg til overstyring", "ux_editor.component_properties.summary.override.component_id": "ID på komponenten", "ux_editor.component_properties.summary.override.empty_field_text": "Tekst for tomme felter", diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditPolicy/EditPolicy.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditPolicy/EditPolicy.tsx index b9bbf028d28..f7dc1c30f93 100644 --- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditPolicy/EditPolicy.tsx +++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigContent/EditPolicy/EditPolicy.tsx @@ -5,7 +5,7 @@ import { useBpmnApiContext } from '../../../../contexts/BpmnApiContext'; import { useTranslation } from 'react-i18next'; import { ShieldLockIcon } from '@studio/icons'; import classes from './EditPolicy.module.css'; -import { RedirectBox } from '@altinn/process-editor/components/RedirectBox'; +import { RedirectBox } from 'app-shared/components/RedirectBox'; export const EditPolicy = () => { const { t } = useTranslation(); @@ -25,7 +25,6 @@ export const EditPolicy = () => { color='second' icon={} iconPlacement='left' - className={classes.policyEditorButton} > {t('process_editor.configuration_panel.edit_policy_open_policy_editor_button')} diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.tsx index 72ea3552064..9ec141e333c 100644 --- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.tsx +++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.tsx @@ -7,7 +7,7 @@ import { StudioButton } from '@studio/components'; import { useLocalStorage } from '@studio/components/src/hooks/useLocalStorage'; import { useTranslation } from 'react-i18next'; import { useBpmnApiContext } from '../../../../../contexts/BpmnApiContext'; -import { RedirectBox } from '../../../../RedirectBox'; +import { RedirectBox } from 'app-shared/components/RedirectBox'; import { Link } from '@digdir/designsystemet-react'; export const RedirectToCreatePageButton = (): React.ReactElement => { @@ -16,7 +16,7 @@ export const RedirectToCreatePageButton = (): React.ReactElement => { const packagesRouter = new PackagesRouter({ org, app }); const { existingCustomReceiptLayoutSetId } = useBpmnApiContext(); - const [, setSelectedLayoutSet] = useLocalStorage('layoutSet/' + app, null); + const [, setSelectedLayoutSet] = useLocalStorage('layoutSet/' + app); const handleClick = () => { setSelectedLayoutSet(existingCustomReceiptLayoutSetId); diff --git a/frontend/packages/process-editor/src/components/RedirectBox/RedirectBox.module.css b/frontend/packages/shared/src/components/RedirectBox/RedirectBox.module.css similarity index 100% rename from frontend/packages/process-editor/src/components/RedirectBox/RedirectBox.module.css rename to frontend/packages/shared/src/components/RedirectBox/RedirectBox.module.css diff --git a/frontend/packages/process-editor/src/components/RedirectBox/RedirectBox.tsx b/frontend/packages/shared/src/components/RedirectBox/RedirectBox.tsx similarity index 59% rename from frontend/packages/process-editor/src/components/RedirectBox/RedirectBox.tsx rename to frontend/packages/shared/src/components/RedirectBox/RedirectBox.tsx index 6a4b792f7ce..f60f3cb07cc 100644 --- a/frontend/packages/process-editor/src/components/RedirectBox/RedirectBox.tsx +++ b/frontend/packages/shared/src/components/RedirectBox/RedirectBox.tsx @@ -1,15 +1,22 @@ import React, { type ReactNode } from 'react'; +import cn from 'classnames'; import classes from './RedirectBox.module.css'; import { StudioLabelAsParagraph } from '@studio/components'; export type RedirectBoxProps = { title: string; children: ReactNode; + className?: string; }; -export const RedirectBox = ({ title, children }: RedirectBoxProps): React.ReactElement => { +export const RedirectBox = ({ + title, + children, + className: givenClassName, +}: RedirectBoxProps): React.ReactElement => { + const className = cn(classes.wrapper, givenClassName); return ( -
+
{title}
{children}
diff --git a/frontend/packages/process-editor/src/components/RedirectBox/index.ts b/frontend/packages/shared/src/components/RedirectBox/index.ts similarity index 100% rename from frontend/packages/process-editor/src/components/RedirectBox/index.ts rename to frontend/packages/shared/src/components/RedirectBox/index.ts diff --git a/frontend/packages/shared/src/types/api/LayoutSetsResponse.ts b/frontend/packages/shared/src/types/api/LayoutSetsResponse.ts index d0bbce4f2c9..ae6539e57e3 100644 --- a/frontend/packages/shared/src/types/api/LayoutSetsResponse.ts +++ b/frontend/packages/shared/src/types/api/LayoutSetsResponse.ts @@ -6,7 +6,9 @@ export type LayoutSet = { id: string; dataType?: string; tasks?: string[]; - type?: string; + type?: LayoutSetType; }; export type LayoutSetConfig = LayoutSet; + +export type LayoutSetType = 'subform'; diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index ed1761f68b0..8f3dc9010f2 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -10,7 +10,7 @@ export type SupportedFeatureFlags = | 'resourceMigration' | 'multipleDataModelsPerTask' | 'exportForm' - | 'subForm' + | 'subform' | 'summary2'; /* diff --git a/frontend/packages/ux-editor/src/AppContext.test.tsx b/frontend/packages/ux-editor/src/AppContext.test.tsx index 4cbca1bb139..10f7fc5ffa9 100644 --- a/frontend/packages/ux-editor/src/AppContext.test.tsx +++ b/frontend/packages/ux-editor/src/AppContext.test.tsx @@ -23,7 +23,7 @@ const TestComponent = ({ children, }: { queryClient: QueryClient; - children: (appContext: AppContextProps) => React.ReactNode; + children: (appContext: Partial) => React.ReactNode; }) => { const appContext = useAppContext(); useEffect(() => { diff --git a/frontend/packages/ux-editor/src/classes/SubFormUtils.ts b/frontend/packages/ux-editor/src/classes/SubFormUtils.ts new file mode 100644 index 00000000000..c916dcbb7cb --- /dev/null +++ b/frontend/packages/ux-editor/src/classes/SubFormUtils.ts @@ -0,0 +1,26 @@ +import type { LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse'; + +type SubFormLayoutSet = LayoutSetConfig & { + type: 'subform'; +}; + +interface SubFormUtils { + hasSubforms: boolean; + subformLayoutSetsIds: Array; +} + +export class SubFormUtilsImpl implements SubFormUtils { + constructor(private readonly layoutSets: Array) {} + + public get hasSubforms(): boolean { + return this.getSubformLayoutSets.length > 0; + } + + public get subformLayoutSetsIds(): Array { + return this.getSubformLayoutSets.map((set: SubFormLayoutSet) => set.id); + } + + private get getSubformLayoutSets(): Array { + return this.layoutSets.filter((set) => set.type === 'subform') as Array; + } +} diff --git a/frontend/packages/ux-editor/src/classes/Subformutils.test.ts b/frontend/packages/ux-editor/src/classes/Subformutils.test.ts new file mode 100644 index 00000000000..a9e33d4dbca --- /dev/null +++ b/frontend/packages/ux-editor/src/classes/Subformutils.test.ts @@ -0,0 +1,36 @@ +import { SubFormUtilsImpl } from './SubFormUtils'; +import type { LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse'; + +describe('SubFormUtilsImpl', () => { + describe('hasSubform', () => { + it('should return false for hasSubforms when there are no subform layout sets', () => { + const layoutSets: Array = [{ id: '1' }]; + const subFormUtils = new SubFormUtilsImpl(layoutSets); + expect(subFormUtils.hasSubforms).toBe(false); + }); + + it('should return true for hasSubforms when there are subform layout sets', () => { + const layoutSets: Array = [{ id: '1', type: 'subform' }]; + const subFormUtils = new SubFormUtilsImpl(layoutSets); + expect(subFormUtils.hasSubforms).toBe(true); + }); + }); + + describe('subformLayoutSetsIds', () => { + it('should return an empty array for subformLayoutSetsIds when there are no subform layout sets', () => { + const layoutSets: Array = [{ id: '1' }]; + const subFormUtils = new SubFormUtilsImpl(layoutSets); + expect(subFormUtils.subformLayoutSetsIds).toEqual([]); + }); + + it('should return the correct subform layout set IDs', () => { + const layoutSets: Array = [ + { id: '1', type: 'subform' }, + { id: '2' }, + { id: '3', type: 'subform' }, + ]; + const subFormUtils = new SubFormUtilsImpl(layoutSets); + expect(subFormUtils.subformLayoutSetsIds).toEqual(['1', '3']); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx index ff6d6eb0a1e..7d1097e8149 100644 --- a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx +++ b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx @@ -52,32 +52,32 @@ describe('LayoutSetsContainer', () => { }); it('should render add and delete subform buttons when feature is enabled', () => { - addFeatureFlagToLocalStorage('subForm'); + addFeatureFlagToLocalStorage('subform'); render(); - const createSubFormButton = screen.getByRole('button', { + const createSubformButton = screen.getByRole('button', { name: textMock('ux_editor.create.subform'), }); - expect(createSubFormButton).toBeInTheDocument(); + expect(createSubformButton).toBeInTheDocument(); - const deleteSubFormButton = screen.getByRole('button', { + const deleteSubformButton = screen.getByRole('button', { name: textMock('ux_editor.delete.subform'), }); - expect(deleteSubFormButton).toBeInTheDocument(); - removeFeatureFlagFromLocalStorage('subForm'); + expect(deleteSubformButton).toBeInTheDocument(); + removeFeatureFlagFromLocalStorage('subform'); }); it('should not render add and delete subform buttons when feature is disabled', () => { render(); - const createSubFormButton = screen.queryByRole('button', { + const createSubformButton = screen.queryByRole('button', { name: textMock('ux_editor.create.subform'), }); - expect(createSubFormButton).not.toBeInTheDocument(); + expect(createSubformButton).not.toBeInTheDocument(); - const deleteSubFormButton = screen.queryByRole('button', { + const deleteSubformButton = screen.queryByRole('button', { name: textMock('ux_editor.delete.subform'), }); - expect(deleteSubFormButton).not.toBeInTheDocument(); + expect(deleteSubformButton).not.toBeInTheDocument(); }); }); diff --git a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx index c1c3ca9ca03..ded81d7fc39 100644 --- a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx +++ b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx @@ -58,7 +58,7 @@ export function LayoutSetsContainer() { ))} {shouldDisplayFeature('exportForm') && } - {shouldDisplayFeature('subForm') && ( + {shouldDisplayFeature('subform') && ( { describe('findSubFormById', () => { - const layoutSets: Array = [ - { id: '1', type: 'form' }, - { id: '2', type: 'subform' }, - { id: '3', type: 'custom' }, - ]; + const layoutSets: Array = [{ id: '1' }, { id: '2', type: 'subform' }, { id: '3' }]; it('should return the layout set when it is a subform', () => { const result = SubFormUtils.findSubFormById(layoutSets, '2'); diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.module.css new file mode 100644 index 00000000000..934a4372f8e --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.module.css @@ -0,0 +1,5 @@ +.selectedLayoutSet { + align-items: center; + display: flex; + gap: var(--fds-spacing-1); +} diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx new file mode 100644 index 00000000000..1b78a5ff4a0 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { ClipboardIcon } from '@studio/icons'; +import { StudioProperty } from '@studio/components'; +import { useTranslation } from 'react-i18next'; +import classes from './DefinedLayoutSet.module.css'; + +type DefinedLayoutSetProps = { + existingLayoutSetForSubForm: string; + onClick: () => void; +}; + +export const DefinedLayoutSet = ({ + existingLayoutSetForSubForm, + onClick, +}: DefinedLayoutSetProps) => { + const { t } = useTranslation(); + + const value = ( + + {existingLayoutSetForSubForm} + + ); + + return ( + + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx new file mode 100644 index 00000000000..4bd920fbe96 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DefinedLayoutSet } from './DefinedLayoutSet/DefinedLayoutSet'; +import { UndefinedLayoutSet } from './UndefinedLayoutSet/UndefinedLayoutSet'; +import { SelectLayoutSet } from './SelectLayoutSet/SelectLayoutSet'; + +type EditLayoutSetProps = { + existingLayoutSetForSubform: string; + onUpdateLayoutSet: (layoutSetId: string) => void; +}; + +export const EditLayoutSet = ({ + existingLayoutSetForSubform, + onUpdateLayoutSet, +}: EditLayoutSetProps): React.ReactElement => { + const { t } = useTranslation(); + const [isLayoutSetSelectorVisible, setIsLayoutSetSelectorVisible] = useState(false); + + if (isLayoutSetSelectorVisible) { + return ( + + ); + } + + const layoutSetIsUndefined = !existingLayoutSetForSubform; + if (layoutSetIsUndefined) { + return ( + setIsLayoutSetSelectorVisible(true)} + /> + ); + } + + return ( + setIsLayoutSetSelectorVisible(true)} + /> + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButton.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButton.test.tsx new file mode 100644 index 00000000000..6fde6c2a852 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButton.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { EditLayoutSetButtons, type EditLayoutSetButtonsProps } from './EditLayoutSetButtons'; +import userEvent from '@testing-library/user-event'; +import { textMock } from '@studio/testing/mocks/i18nMock'; + +const defaultEditLayoutSetButtonsProps: EditLayoutSetButtonsProps = { + onClose: jest.fn(), + onDelete: jest.fn(), +}; + +describe('EditLayoutSetButtons', () => { + it('should trigger onClose callback when close button is clicked', async () => { + const user = userEvent.setup(); + const onCloseMock = jest.fn(); + + renderEditLayoutSetButtons({ + ...defaultEditLayoutSetButtonsProps, + onClose: onCloseMock, + }); + + const closeButton = screen.getByRole('button', { name: textMock('general.close') }); + await user.click(closeButton); + + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); + + it('should trigger onDelete callback when close button is clicked', async () => { + const user = userEvent.setup(); + const onDeleteMock = jest.fn(); + + renderEditLayoutSetButtons({ + ...defaultEditLayoutSetButtonsProps, + onDelete: onDeleteMock, + }); + + const deleteButton = screen.getByRole('button', { name: textMock('general.delete') }); + await user.click(deleteButton); + + expect(onDeleteMock).toHaveBeenCalledTimes(1); + }); +}); + +const renderEditLayoutSetButtons = (props?: EditLayoutSetButtonsProps) => { + return render(); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.module.css new file mode 100644 index 00000000000..070293f3385 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.module.css @@ -0,0 +1,4 @@ +.buttons { + display: flex; + gap: var(--studio-property-button-vertical-spacing); +} diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.tsx new file mode 100644 index 00000000000..4eea4c21233 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/EditLayoutSetButtons/EditLayoutSetButtons.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { StudioButton, StudioDeleteButton } from '@studio/components'; +import { CheckmarkIcon } from '@studio/icons'; +import classes from './EditLayoutSetButtons.module.css'; + +export type EditLayoutSetButtonsProps = { + onClose: () => void; + onDelete: () => void; +}; + +export const EditLayoutSetButtons = ({ + onClose, + onDelete, +}: EditLayoutSetButtonsProps): React.ReactElement => { + const { t } = useTranslation(); + + return ( +
+ } + onClick={onClose} + title={t('general.close')} + variant='secondary' + /> + +
+ ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css new file mode 100644 index 00000000000..d3243791006 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css @@ -0,0 +1,6 @@ +.selectLayoutSet { + display: flex; + flex-direction: column; + gap: var(--fds-spacing-2); + padding: 0 var(--fds-spacing-5); +} diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx new file mode 100644 index 00000000000..3c4a190e817 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx @@ -0,0 +1,69 @@ +import React, { type ChangeEvent } from 'react'; +import { StudioNativeSelect } from '@studio/components'; +import { useTranslation } from 'react-i18next'; +import classes from './SelectLayoutSet.module.css'; +import { EditLayoutSetButtons } from './EditLayoutSetButtons/EditLayoutSetButtons'; +import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; +import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery'; +import { SubFormUtilsImpl } from '../../../../../../classes/SubFormUtils'; + +type SelectLayoutSetProps = { + existingLayoutSetForSubForm: string; + onUpdateLayoutSet: (layoutSetId: string) => void; + onSetLayoutSetSelectorVisible: (visible: boolean) => void; +}; + +export const SelectLayoutSet = ({ + existingLayoutSetForSubForm, + onUpdateLayoutSet, + onSetLayoutSetSelectorVisible, +}: SelectLayoutSetProps) => { + const { t } = useTranslation(); + const { org, app } = useStudioEnvironmentParams(); + const { data: layoutSets } = useLayoutSetsQuery(org, app); + const subFormUtils = new SubFormUtilsImpl(layoutSets.sets); + + const addLinkToLayoutSet = (layoutSetId: string): void => { + onUpdateLayoutSet(layoutSetId); + }; + + const deleteLinkToLayoutSet = (): void => { + onUpdateLayoutSet(undefined); + closeLayoutSetSelector(); + }; + + const closeLayoutSetSelector = (): void => { + onSetLayoutSetSelectorVisible(false); + }; + + const handleLayoutSetChange = (event: ChangeEvent): void => { + const selectedLayoutSetId = event.target.value; + + if (selectedLayoutSetId === '') { + deleteLinkToLayoutSet(); + return; + } + + addLinkToLayoutSet(selectedLayoutSetId); + }; + + return ( +
+ + + {subFormUtils.subformLayoutSetsIds.map((option) => ( + + ))} + + +
+ ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.test.tsx new file mode 100644 index 00000000000..07d98ebf7dd --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { UndefinedLayoutSet, type UndefinedLayoutSetProps } from './UndefinedLayoutSet'; +import userEvent from '@testing-library/user-event'; + +const defaultUndefinedLayoutSetProps = { + onClick: jest.fn(), + label: '', +}; + +describe('UndefinedLayoutSet', () => { + it('it should render add layout-set with given label', () => { + const label = 'Add link to layout set'; + renderUndefinedLayoutSet({ + ...defaultUndefinedLayoutSetProps, + label, + }); + + const addLinkToLayoutSetButton = screen.getByRole('button', { name: label }); + expect(addLinkToLayoutSetButton).toBeInTheDocument(); + }); + + it('should invoke onClick callback when button is clicked', async () => { + const user = userEvent.setup(); + const label = 'add'; + const onClickMock = jest.fn(); + + renderUndefinedLayoutSet({ + onClick: onClickMock, + label, + }); + + const button = screen.getByRole('button', { name: label }); + await user.click(button); + + expect(onClickMock).toHaveBeenCalledTimes(1); + }); +}); + +const renderUndefinedLayoutSet = (props?: UndefinedLayoutSetProps): void => { + render(); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.tsx new file mode 100644 index 00000000000..9c3ec829372 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/UndefinedLayoutSet/UndefinedLayoutSet.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { StudioProperty } from '@studio/components'; +import { LinkIcon } from '@studio/icons'; + +export type UndefinedLayoutSetProps = { + onClick: () => void; + label: string; +}; + +export const UndefinedLayoutSet = ({ onClick, label }: UndefinedLayoutSetProps) => ( + } onClick={onClick} property={label} /> +); diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/index.ts b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/index.ts new file mode 100644 index 00000000000..2fe3ded97ea --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/index.ts @@ -0,0 +1 @@ +export { EditLayoutSet } from './EditLayoutSet'; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx new file mode 100644 index 00000000000..cbbbcac4006 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { renderWithProviders } from '../../../../testing/mocks'; +import { EditLayoutSetForSubform } from './EditLayoutSetForSubform'; +import { ComponentType } from 'app-shared/types/ComponentType'; +import { componentMocks } from '../../../../testing/componentMocks'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { screen, within } from '@testing-library/react'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { app, org } from '@studio/testing/testids'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { layoutSets } from 'app-shared/mocks/mocks'; +import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse'; +import type { UserEvent } from '@testing-library/user-event'; +import userEvent from '@testing-library/user-event'; +import type { FormComponent } from '../../../../types/FormComponent'; +import { AppContext } from '../../../../AppContext'; +import { appContextMock } from '../../../../testing/appContextMock'; + +const handleComponentChangeMock = jest.fn(); +const setSelectedFormLayoutSetMock = jest.fn(); + +describe('EditLayoutSetForSubForm', () => { + afterEach(jest.clearAllMocks); + + it('displays "no existing subform layout sets" message if no subform layout set exist', () => { + renderEditLayoutSetForSubForm(); + const noExistingSubFormForLayoutSet = screen.getByText( + textMock('ux_editor.component_properties.subform.no_layout_sets_acting_as_subform'), + ); + expect(noExistingSubFormForLayoutSet).toBeInTheDocument(); + }); + + it('displays a button to set subform if subform layout sets exists', () => { + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + const setLayoutSetButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + }); + expect(setLayoutSetButton).toBeInTheDocument(); + }); + + it('displays a select to choose a layout set for the subform when clicking button to set', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + await openEditMode(user); + const selectLayoutSet = getSelectForLayoutSet(); + const options = within(selectLayoutSet).getAllByRole('option'); + expect(options).toHaveLength(2); + expect(options[0]).toHaveTextContent( + textMock('ux_editor.component_properties.subform.choose_layout_set'), + ); + expect(options[1]).toHaveTextContent(subformLayoutSetId); + }); + + it('calls handleComponentChange when setting a layout set for the subform', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + await openEditMode(user); + const selectLayoutSet = getSelectForLayoutSet(); + await user.selectOptions(selectLayoutSet, subformLayoutSetId); + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + expect(handleComponentChangeMock).toHaveBeenCalledWith( + expect.objectContaining({ + layoutSet: subformLayoutSetId, + }), + ); + }); + + it('calls handleComponentChange with no layout set for component if selecting the empty option', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + await openEditMode(user); + const selectLayoutSet = getSelectForLayoutSet(); + const emptyOptionText = textMock('ux_editor.component_properties.subform.choose_layout_set'); + await user.selectOptions(selectLayoutSet, emptyOptionText); + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + expect(handleComponentChangeMock).toHaveBeenCalledWith( + expect.not.objectContaining({ + layoutSet: expect.anything(), + }), + ); + }); + + it('closes the view mode when clicking close button after selecting a layout set', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + await openEditMode(user); + const closeSetLayoutSetButton = screen.getByRole('button', { + name: textMock('general.close'), + }); + await user.click(closeSetLayoutSetButton); + const setLayoutSetButtonAfterClose = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + }); + expect(setLayoutSetButtonAfterClose).toBeInTheDocument(); + }); + + it('calls handleComponentChange with no layout set for component when clicking delete button', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + await openEditMode(user); + const deleteLayoutSetConnectionButton = screen.getByRole('button', { + name: textMock('general.delete'), + }); + await user.click(deleteLayoutSetConnectionButton); + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + expect(handleComponentChangeMock).toHaveBeenCalledWith( + expect.not.objectContaining({ + layoutSet: expect.anything(), + }), + ); + }); + + it('displays a button with the existing layout set for the subform if set', () => { + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm( + { sets: [{ id: subformLayoutSetId, type: 'subform' }] }, + { layoutSet: subformLayoutSetId }, + ); + const existingLayoutSetButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_title', { + subform: subformLayoutSetId, + }), + }); + expect(existingLayoutSetButton).toBeInTheDocument(); + }); + + it('opens view mode when clicking the button when a layout set for the subform if set', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm( + { sets: [{ id: subformLayoutSetId, type: 'subform' }] }, + { layoutSet: subformLayoutSetId }, + ); + const existingLayoutSetButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_title', { + subform: subformLayoutSetId, + }), + }); + await user.click(existingLayoutSetButton); + const selectLayoutSet = getSelectForLayoutSet(); + expect(selectLayoutSet).toBeInTheDocument(); + }); +}); + +const openEditMode = async (user: UserEvent) => { + const setLayoutSetButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + }); + await user.click(setLayoutSetButton); +}; + +const getSelectForLayoutSet = () => + screen.getByRole('combobox', { + name: textMock('ux_editor.component_properties.subform.choose_layout_set_label'), + }); + +const renderEditLayoutSetForSubForm = ( + layoutSetsMock: LayoutSets = layoutSets, + componentProps: Partial> = {}, +) => { + const queryClient = createQueryClientMock(); + queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock); + return renderWithProviders( + + + , + { queryClient }, + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx new file mode 100644 index 00000000000..e158d7ffaf4 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { EditLayoutSet } from './EditLayoutSet'; +import { NoSubformLayoutsExist } from './NoSubformLayoutsExist'; +import type { ComponentType } from 'app-shared/types/ComponentType'; +import { SubFormUtilsImpl } from '../../../../classes/SubFormUtils'; +import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery'; +import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; +import type { IGenericEditComponent } from '../../../../components/config/componentConfig'; + +export const EditLayoutSetForSubform = ({ + handleComponentChange, + component, +}: IGenericEditComponent): React.ReactElement => { + const { org, app } = useStudioEnvironmentParams(); + const { data: layoutSets } = useLayoutSetsQuery(org, app); + + const subFormUtils = new SubFormUtilsImpl(layoutSets.sets); + + if (!subFormUtils.hasSubforms) { + return ; + } + + const handleUpdatedLayoutSet = (layoutSet: string): void => { + const updatedComponent = { ...component, layoutSet }; + handleComponentChange(updatedComponent); + }; + + return ( + + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.module.css new file mode 100644 index 00000000000..f9c625687da --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.module.css @@ -0,0 +1,3 @@ +.alert { + margin: 0 var(--fds-spacing-5); +} diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.tsx new file mode 100644 index 00000000000..d29a0b3ad90 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/NoSubformLayoutsExist.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Alert } from '@digdir/designsystemet-react'; +import { useTranslation } from 'react-i18next'; +import classes from './NoSubformLayoutsExist.module.css'; + +export const NoSubformLayoutsExist = () => { + const { t } = useTranslation(); + + return ( + + {t('ux_editor.component_properties.subform.no_layout_sets_acting_as_subform')} + + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/index.ts b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/index.ts new file mode 100644 index 00000000000..3aa25640eb8 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/NoSubformLayoutsExist/index.ts @@ -0,0 +1 @@ +export { NoSubformLayoutsExist } from './NoSubformLayoutsExist'; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/index.ts b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/index.ts new file mode 100644 index 00000000000..d982b8a37b3 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/index.ts @@ -0,0 +1 @@ +export { EditLayoutSetForSubform } from './EditLayoutSetForSubform'; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.module.css index f1642324b3e..72129bd41ee 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.module.css +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.module.css @@ -1,3 +1,7 @@ .content { + display: flex; + flex-direction: column; background-color: var(--fds-semantic-surface-neutral-default); + gap: var(--fds-spacing-2); + padding-bottom: var(--fds-spacing-2); } diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx index 328b282a43b..a353ea9d863 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx @@ -10,10 +10,11 @@ import { textMock } from '@studio/testing/mocks/i18nMock'; import { queryClientMock } from 'app-shared/mocks/queryClientMock'; import { QueryKey } from 'app-shared/types/QueryKey'; import { componentSchemaMocks } from '../../../testing/componentSchemaMocks'; -import { layoutSet1NameMock } from '@altinn/ux-editor/testing/layoutSetsMock'; +import { layoutSet1NameMock, layoutSetsMock } from '@altinn/ux-editor/testing/layoutSetsMock'; import { layout1NameMock, layoutMock } from '@altinn/ux-editor/testing/layoutMock'; import type { IFormLayouts } from '@altinn/ux-editor/types/global'; import { app, org } from '@studio/testing/testids'; +import { ComponentType } from 'app-shared/types/ComponentType'; const mockHandleComponentUpdate = jest.fn(); @@ -96,6 +97,24 @@ describe('PropertiesHeader', () => { expect(containerIdInput).toHaveAttribute('aria-invalid', 'true'); expect(mockHandleComponentUpdate).toHaveBeenCalledTimes(0); }); + + it('should render subform config when component is subform', () => { + renderPropertiesHeader({ + formItem: { id: 'subformComponentId', type: ComponentType.SubForm, itemType: 'COMPONENT' }, + }); + const setLayoutSetButton = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + }); + expect(setLayoutSetButton).toBeInTheDocument(); + }); + + it('should not render subform config when component is not subform', () => { + renderPropertiesHeader(); + const setLayoutSetButton = screen.queryByRole('button', { + name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + }); + expect(setLayoutSetButton).not.toBeInTheDocument(); + }); }); const renderPropertiesHeader = (props: Partial = {}) => { const componentType = props.formItem ? props.formItem.type : defaultProps.formItem.type; @@ -104,6 +123,7 @@ const renderPropertiesHeader = (props: Partial = {}) => { componentSchemaMocks[componentType], ); queryClientMock.setQueryData([QueryKey.FormLayouts, org, app, layoutSetName], layouts); + queryClientMock.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock); return renderWithProviders(
+ {formItem.type === ComponentType.SubForm && ( + + )}
); diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx index d786a433029..e97a4e206f8 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx @@ -10,6 +10,7 @@ import { screen } from '@testing-library/react'; import { textMock } from '@studio/testing/mocks/i18nMock'; import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs'; import userEvent from '@testing-library/user-event'; +import { ComponentType } from 'app-shared/types/ComponentType'; const somePropertyName = 'somePropertyName'; const customTextMockToHandleUndefined = ( @@ -54,6 +55,26 @@ describe('FormComponentConfig', () => { } }); + it('should render "RedirectToLayoutSet"', () => { + render({ + props: { + component: { + id: 'subform-unit-test-id', + layoutSet: 'subform-unit-test-layout-set', + itemType: 'COMPONENT', + type: ComponentType.SubForm, + }, + schema: { + properties: { + layoutSet: 'subform-unit-test-layout-set', + }, + }, + }, + }); + + expect(screen.getByText(textMock('ux_editor.component_properties.subform.go_to_layout_set'))); + }); + it('should render list of unsupported properties', () => { render({ props: { diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx index 9fd56228e8d..d187ec1e3cb 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx @@ -15,6 +15,7 @@ import type { FormItem } from '../../types/FormItem'; import type { UpdateFormMutateOptions } from '../../containers/FormItemContext'; import { useComponentPropertyDescription } from '../../hooks/useComponentPropertyDescription'; import classes from './FormComponentConfig.module.css'; +import { RedirectToLayoutSet } from './editModal/RedirectToLayoutSet'; export interface IEditFormComponentProps { editFormId: string; @@ -41,13 +42,14 @@ export const FormComponentConfig = ({ if (!schema?.properties) return null; const { properties } = schema; - const { hasCustomFileEndings, validFileEndings, grid } = properties; + const { hasCustomFileEndings, validFileEndings, grid, layoutSet } = properties; // Add any properties that have a custom implementation to this list so they are not duplicated in the generic view const customProperties = [ 'hasCustomFileEndings', 'validFileEndings', 'grid', + 'layoutSet', 'children', 'dataTypeIds', 'target', @@ -93,8 +95,11 @@ export const FormComponentConfig = ({ return ( <> + {layoutSet && component['layoutSet'] && ( + + )} {grid && ( -
+ <> {t('ux_editor.component_properties.grid')} @@ -103,7 +108,7 @@ export const FormComponentConfig = ({ component={component} handleComponentChange={handleComponentUpdate} /> -
+ )} {!hideUnsupported && ( diff --git a/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.module.css b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.module.css new file mode 100644 index 00000000000..c42db18581c --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.module.css @@ -0,0 +1,3 @@ +.redirectContainer { + background-color: var(--fds-semantic-background-default); +} diff --git a/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.test.tsx b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.test.tsx new file mode 100644 index 00000000000..9e6c352adda --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { RedirectToLayoutSet } from './RedirectToLayoutSet'; +import { screen } from '@testing-library/react'; +import { renderWithProviders } from '../../../../testing/mocks'; +import userEvent from '@testing-library/user-event'; +import { AppContext } from '../../../../AppContext'; +import { appContextMock } from '../../../../testing/appContextMock'; + +const subformLayoutSetIdMock = 'subformLayoutSetId'; +const setSelectedFormLayoutSetMock = jest.fn(); + +describe('RedirectToLayoutSet', () => { + it('displays a redirect button to design layout set for the subform if set', () => { + renderRedirectToLayoutSet(); + const redirectBoxTitle = screen.queryByText( + textMock('ux_editor.component_properties.subform.go_to_layout_set'), + ); + expect(redirectBoxTitle).toBeInTheDocument(); + }); + + it('calls setSelectedFormLayoutSet when clicking the redirect button', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderRedirectToLayoutSet(); + const redirectButton = screen.queryByRole('button', { + name: textMock('top_menu.create'), + }); + await user.click(redirectButton); + expect(setSelectedFormLayoutSetMock).toHaveBeenCalledTimes(1); + expect(setSelectedFormLayoutSetMock).toHaveBeenCalledWith(subformLayoutSetId); + }); +}); + +const renderRedirectToLayoutSet = (selectedSubform: string = subformLayoutSetIdMock) => { + return renderWithProviders( + + + , + ); +}; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.tsx b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.tsx new file mode 100644 index 00000000000..912d5f6c39e --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/RedirectToLayoutSet.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { PencilIcon } from '@studio/icons'; +import { StudioButton } from '@studio/components'; +import { RedirectBox } from 'app-shared/components/RedirectBox'; +import classes from './RedirectToLayoutSet.module.css'; +import { useAppContext } from '../../../../hooks'; + +type RedirectToLayoutSetProps = { + selectedSubform: string; +}; + +export const RedirectToLayoutSet = ({ + selectedSubform, +}: RedirectToLayoutSetProps): React.ReactElement => { + const { setSelectedFormLayoutName, setSelectedFormLayoutSetName } = useAppContext(); + const { t } = useTranslation(); + + const handleOnRedirectClick = (): void => { + setSelectedFormLayoutSetName(selectedSubform); + setSelectedFormLayoutName(undefined); + }; + + return ( + + } + iconPlacement='left' + > + {t('top_menu.create')} + + + ); +}; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/index.ts b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/index.ts new file mode 100644 index 00000000000..ed7b76853ac --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/editModal/RedirectToLayoutSet/index.ts @@ -0,0 +1 @@ +export { RedirectToLayoutSet } from './RedirectToLayoutSet'; diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.ts b/frontend/packages/ux-editor/src/data/formItemConfig.ts index 089e4f00f93..df1cc244cc4 100644 --- a/frontend/packages/ux-editor/src/data/formItemConfig.ts +++ b/frontend/packages/ux-editor/src/data/formItemConfig.ts @@ -482,7 +482,7 @@ export const advancedItems: FormItemConfigs[ComponentType][] = [ formItemConfigs[ComponentType.Custom], formItemConfigs[ComponentType.RepeatingGroup], formItemConfigs[ComponentType.PaymentDetails], - shouldDisplayFeature('subForm') && formItemConfigs[ComponentType.SubForm], + shouldDisplayFeature('subform') && formItemConfigs[ComponentType.SubForm], ].filter(FilterUtils.filterOutDisabledFeatureItems); export const schemaComponents: FormItemConfigs[ComponentType][] = [ diff --git a/frontend/packages/ux-editor/src/hooks/useAppContext.test.tsx b/frontend/packages/ux-editor/src/hooks/useAppContext.test.tsx new file mode 100644 index 00000000000..22ff75a87bf --- /dev/null +++ b/frontend/packages/ux-editor/src/hooks/useAppContext.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { useAppContext } from './useAppContext'; + +const TestComponent = () => { + useAppContext(); + return
Test
; +}; + +describe('useAppContext', () => { + it('should throw an error when useAppContext is used outside of a AppContextProvider', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => render()).toThrow( + 'useAppContext must be used within a AppContextProvider', + ); + expect(consoleError).toHaveBeenCalled(); + }); +}); diff --git a/frontend/packages/ux-editor/src/hooks/useAppContext.ts b/frontend/packages/ux-editor/src/hooks/useAppContext.ts index 25a236d1618..9f02c604ea3 100644 --- a/frontend/packages/ux-editor/src/hooks/useAppContext.ts +++ b/frontend/packages/ux-editor/src/hooks/useAppContext.ts @@ -1,4 +1,11 @@ import { useContext } from 'react'; +import type { AppContextProps } from '../AppContext'; import { AppContext } from '../AppContext'; -export const useAppContext = () => useContext(AppContext); +export const useAppContext = (): Partial => { + const context = useContext(AppContext); + if (context === null) { + throw new Error('useAppContext must be used within a AppContextProvider'); + } + return context; +}; diff --git a/frontend/packages/ux-editor/src/testing/componentMocks.ts b/frontend/packages/ux-editor/src/testing/componentMocks.ts index 1213463c02a..2160eb3f9cb 100644 --- a/frontend/packages/ux-editor/src/testing/componentMocks.ts +++ b/frontend/packages/ux-editor/src/testing/componentMocks.ts @@ -65,6 +65,9 @@ const textareaComponent: FormComponent = { ...commonProps(ComponentType.TextArea), dataModelBindings: { simpleBinding: '' }, }; +const subFormComponent: FormComponent = { + ...commonProps(ComponentType.SubForm), +}; const fileUploadComponent: FormComponent = { ...commonProps(ComponentType.FileUpload), dataModelBindings: { simpleBinding: '' }, @@ -176,6 +179,7 @@ export const componentMocks = { [ComponentType.Paragraph]: paragraphComponent, [ComponentType.RadioButtons]: radiosComponent, [ComponentType.RepeatingGroup]: repeatingGroupContainer, + [ComponentType.SubForm]: subFormComponent, [ComponentType.TextArea]: textareaComponent, [ComponentType.Custom]: thirdPartyComponent, [ComponentType.Summary2]: summary2Component, From c198a8dccf749a9b187aaa8c4fc7d9f18b9ac10c Mon Sep 17 00:00:00 2001 From: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:14:11 +0200 Subject: [PATCH 10/11] fix: fixing the z-index bug on data model page (#13757) --- .../TopToolbar/CreateNewWrapper.module.css | 4 ++++ .../SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.module.css diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.module.css b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.module.css new file mode 100644 index 00000000000..d799729b9f7 --- /dev/null +++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.module.css @@ -0,0 +1,4 @@ +.popoverContent { + /* This is so that it goes above the header which has an z-index of 2000 */ + z-index: 2001; +} diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.tsx index 2c2084c0d63..ab21b0c3bea 100644 --- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.tsx +++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/CreateNewWrapper.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import classes from './CreateNewWrapper.module.css'; import { ErrorMessage, Textfield } from '@digdir/designsystemet-react'; import { useTranslation } from 'react-i18next'; import { PlusIcon } from '@studio/icons'; @@ -91,7 +92,7 @@ export function CreateNewWrapper({ {} {t('general.create_new')} - + Date: Thu, 10 Oct 2024 11:15:11 +0200 Subject: [PATCH 11/11] chore: Update azure identity dep (#13758) --- src/Altinn.Platform/Altinn.Platform.PDF/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml b/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml index 02bf5335a5a..d06952122c6 100644 --- a/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml +++ b/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml @@ -78,7 +78,7 @@ com.azure azure-identity - 1.13.3 + 1.14.0 org.springframework.boot