Skip to content

Commit

Permalink
chore: Add PDF config for layoutset (#13512)
Browse files Browse the repository at this point in the history
Co-authored-by: Erling Hauan <148075168+ErlingHauan@users.noreply.github.com>
  • Loading branch information
standeren and ErlingHauan committed Oct 4, 2024
1 parent 9b8ebe5 commit 8292aeb
Show file tree
Hide file tree
Showing 33 changed files with 717 additions and 105 deletions.
14 changes: 12 additions & 2 deletions backend/src/Designer/Controllers/AppDevelopmentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class AppDevelopmentController : Controller
private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory;
private readonly ApplicationInsightsSettings _applicationInsightsSettings;
private readonly IMediator _mediator;
private readonly IUserRequestsSynchronizationService _userRequestsSynchronizationService;


/// <summary>
Expand All @@ -45,14 +46,17 @@ public class AppDevelopmentController : Controller
/// <param name="sourceControl">The source control service.</param>
/// <param name="altinnGitRepositoryFactory"></param>
/// <param name="applicationInsightsSettings">An <see cref="ApplicationInsightsSettings"/></param>
public AppDevelopmentController(IAppDevelopmentService appDevelopmentService, IRepository repositoryService, ISourceControl sourceControl, IAltinnGitRepositoryFactory altinnGitRepositoryFactory, ApplicationInsightsSettings applicationInsightsSettings, IMediator mediator)
/// <param name="mediator"></param>
/// <param name="userRequestsSynchronizationService">An <see cref="IUserRequestsSynchronizationService"/> used to control parallel execution of user requests.</param>
public AppDevelopmentController(IAppDevelopmentService appDevelopmentService, IRepository repositoryService, ISourceControl sourceControl, IAltinnGitRepositoryFactory altinnGitRepositoryFactory, ApplicationInsightsSettings applicationInsightsSettings, IMediator mediator, IUserRequestsSynchronizationService userRequestsSynchronizationService)
{
_appDevelopmentService = appDevelopmentService;
_repository = repositoryService;
_sourceControl = sourceControl;
_altinnGitRepositoryFactory = altinnGitRepositoryFactory;
_applicationInsightsSettings = applicationInsightsSettings;
_mediator = mediator;
_userRequestsSynchronizationService = userRequestsSynchronizationService;
}

/// <summary>
Expand Down Expand Up @@ -213,9 +217,11 @@ public ActionResult UpdateFormLayoutName(string org, string app, [FromQuery] str
[Route("layout-settings")]
public async Task<ActionResult> SaveLayoutSettings(string org, string app, [FromQuery] string layoutSetName, [FromBody] JsonNode layoutSettings, CancellationToken cancellationToken)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
SemaphoreSlim semaphore = _userRequestsSynchronizationService.GetRequestsSemaphore(org, app, developer);
await semaphore.WaitAsync();
try
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
await _appDevelopmentService.SaveLayoutSettings(editingContext, layoutSettings, layoutSetName, cancellationToken);
return Ok();
Expand All @@ -224,6 +230,10 @@ public async Task<ActionResult> SaveLayoutSettings(string org, string app, [From
{
return NotFound(exception.Message);
}
finally
{
semaphore.Release();
}
}

/// <summary>
Expand Down
8 changes: 8 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,7 @@
"right_menu.expressions_property_preview_required": "<textElement>Sett </textElement><componentName>{{componentName}}</componentName><textElement> som påkrevd hvis …</textElement>",
"right_menu.expressions_property_read_only": "Sett felt som skrivebeskyttet dersom",
"right_menu.expressions_property_required": "Sett felt som påkrevd dersom",
"right_menu.pdf": "PDF",
"right_menu.read_more_about_expressions": "Les mer om dynamiske uttrykk i dokumentasjonen.",
"right_menu.rules_calculations": "Regel for beregninger",
"right_menu.rules_calculations_add_alt": "Legg til regel for beregninger",
Expand Down Expand Up @@ -1600,6 +1601,13 @@
"ux_editor.options_text_help_text": "Hjelpetekst",
"ux_editor.options_text_label": "Ledetekst",
"ux_editor.page": "Side",
"ux_editor.page_config_pdf_abort_converting_page_to_pdf": "Avbryt å gjøre om siden til PDF",
"ux_editor.page_config_pdf_convert_existing_pdf": "Konverter eksisterende PDF til skjemaside",
"ux_editor.page_config_pdf_convert_info_when_custom_pdf_exists": "Du har allerede en egendefinert PDF. Dersom du fortsatt ønsker å konvertere denne siden til PDF, kan du velge om du vil slette den eksisterende PDF-en for godt eller om du vil konvertere den tilbake til en vanlig skjemaside.",
"ux_editor.page_config_pdf_convert_page_to_pdf": "Gjør om siden til PDF",
"ux_editor.page_config_pdf_delete_existing_pdf": "Slett eksisterende PDF",
"ux_editor.page_config_pdf_exclude_components_from_default_pdf": "Velg hvilke komponenter fra siden som skal skjules i standard PDF",
"ux_editor.page_config_pdf_exclude_page_from_default_pdf": "Ekskluder siden fra standard PDF",
"ux_editor.page_delete_text": "Er du sikker på at du vil slette denne siden?\nAlt innholdet på siden vil bli fjernet.",
"ux_editor.page_menu_down": "Flytt ned",
"ux_editor.page_menu_edit": "Gi nytt navn",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const useSelectedFormLayoutName = (

const isValidLayout = (layoutName: string): boolean => {
const layoutPagesOrder = formLayoutSettings?.pages?.order;
return layoutPagesOrder?.includes(layoutName);
const isExistingLayout = layoutPagesOrder?.includes(layoutName);
const isPdf = formLayoutSettings?.pages?.pdfLayoutName === layoutName;
return isExistingLayout || isPdf;
};

const [selectedFormLayoutName, setSelectedFormLayoutName] = useSearchParamsState<string>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const ReceiptContent = ({
return (
<div className={classes.wrapper}>
<div className={classes.accordionWrapper}>
<Accordion color='neutral' className={classes.accordion}>
<Accordion color='neutral'>
<PageAccordion
pageName={receiptName}
isOpen={receiptName === selectedAccordion}
Expand Down
45 changes: 44 additions & 1 deletion frontend/packages/ux-editor/src/classes/FormLayoutSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,59 @@ export class FormLayoutSettings {
this.layoutSettings = layoutSettings;
}

public getFormLayoutSettings(): FormLayoutSettings {
return this;
}

public getLayoutSettings(): ILayoutSettings {
return this.layoutSettings;
}

public getLayoutsOrder(): string[] {
return this.layoutSettings.pages.order;
}

public isLayoutInOrder(layoutName: string): boolean {
return this.layoutSettings.pages.order.includes(layoutName);
}

public deleteLayoutFromOrder(layoutName: string): FormLayoutSettings {
const indexOfLayout = this.layoutSettings.pages.order.indexOf(layoutName);
this.layoutSettings.pages.order.splice(indexOfLayout, 1);
return this;
}

public setPdfLayoutName(layoutName: string): FormLayoutSettings {
this.layoutSettings.pages.pdfLayoutName = layoutName;
return this;
}

public getPdfLayoutName(): string {
public deletePdfLayoutName(): FormLayoutSettings {
delete this.layoutSettings.pages.pdfLayoutName;
return this;
}

public getPdfLayoutName(): string | undefined {
return this.layoutSettings.pages.pdfLayoutName;
}

public addPageToOrder(layoutName: string): void {
this.layoutSettings.pages.order.push(layoutName);
}

public deletePageFromOrder(layoutName: string): void {
const indexOfPage = this.layoutSettings.pages.order.indexOf(layoutName);
this.layoutSettings.pages.order.splice(indexOfPage, 1);
}

public deleteLayoutByName(layoutName: string): FormLayoutSettings {
if (this.isLayoutInOrder(layoutName)) {
this.deleteLayoutFromOrder(layoutName);
}

if (this.getPdfLayoutName() === layoutName) {
this.deletePdfLayoutName();
}
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ const saveFormLayoutSettings = jest.fn();

describe('SavableFormLayoutSettings', () => {
afterEach(() => jest.clearAllMocks());
it('saves layoutSettings when pdfLayoutName is updated', () => {
it('saves layoutSettings when save function is called', () => {
const savableLayoutSettings = setupLayoutSettings();
const newPdfLayoutName = 'newPdfLayoutName';
savableLayoutSettings.setPdfLayoutName(newPdfLayoutName);
savableLayoutSettings.save();
expect(saveFormLayoutSettings).toHaveBeenCalledTimes(1);
expect(saveFormLayoutSettings).toHaveBeenCalledWith(savableLayoutSettings.getLayoutSettings());
const actualNewPdfLayoutSetName = savableLayoutSettings.getPdfLayoutName();
expect(actualNewPdfLayoutSetName).toBe(newPdfLayoutName);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,4 @@ export class SavableFormLayoutSettings extends FormLayoutSettings {
this.saveFormLayoutSettings(this.getLayoutSettings());
return this;
}

public setPdfLayoutName(layoutName: string): SavableFormLayoutSettings {
super.setPdfLayoutName(layoutName);
return this.save();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.pdf,
.text {
padding: var(--fds-spacing-5) 0;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { renderWithProviders } from '../../../testing/mocks';
import { formLayoutSettingsMock, renderWithProviders } from '../../../testing/mocks';
import { PageConfigPanel } from './PageConfigPanel';
import { QueryKey } from 'app-shared/types/QueryKey';
import { queryClientMock } from 'app-shared/mocks/queryClientMock';
Expand Down Expand Up @@ -124,6 +124,10 @@ const renderPageConfigPanel = (
) => {
queryClientMock.setQueryData([QueryKey.TextResources, org, app], textResources);
queryClientMock.setQueryData([QueryKey.FormLayouts, org, app, layoutSet], layouts);
queryClientMock.setQueryData(
[QueryKey.FormLayoutSettings, org, app, layoutSet],
formLayoutSettingsMock,
);
queryClientMock.setQueryData(
[QueryKey.DataModelMetadata, org, app, layoutSet, dataModelName],
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PageConfigWarning } from './PageConfigWarning';
import classes from './PageConfigPanel.module.css';
import { PageConfigWarningModal } from './PageConfigWarningModal';
import type { IInternalLayout } from '@altinn/ux-editor/types/global';
import { PdfConfig } from '@altinn/ux-editor/components/Properties/PageConfigPanel/PdfConfig';

export const PageConfigPanel = () => {
const { selectedFormLayoutName } = useAppContext();
Expand Down Expand Up @@ -83,6 +84,12 @@ export const PageConfigPanel = () => {
<HiddenExpressionOnLayout />
</Accordion.Content>
</Accordion.Item>
<Accordion.Item>
<Accordion.Header>{t('right_menu.pdf')}</Accordion.Header>
<Accordion.Content className={classes.pdf}>
<PdfConfig />
</Accordion.Content>
</Accordion.Item>
</Accordion>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { createRef } from 'react';
import { formLayoutSettingsMock, renderWithProviders } from '@altinn/ux-editor/testing/mocks';
import { ConvertChoicesModal } from '@altinn/ux-editor/components/Properties/PageConfigPanel/PdfConfig/ConvertPageToPdfWhenExistingModal/ConvertChoicesModal';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { app, org } from '@studio/testing/testids';
import { layoutSet1NameMock } from '@altinn/ux-editor/testing/layoutSetsMock';
import { layout1NameMock } from '@altinn/ux-editor/testing/layoutMock';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import type { ILayoutSettings } from 'app-shared/types/global';
import type { AppContextProps } from '@altinn/ux-editor/AppContext';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { QueryKey } from 'app-shared/types/QueryKey';

const selectedLayoutSet = layoutSet1NameMock;
const handleModalActionMock = jest.fn();

describe('ConvertChoicesModal', () => {
afterEach(() => jest.clearAllMocks());
it('converts existing pdf back to formLayout when clicking convert in conversion choices modal', async () => {
const user = userEvent.setup();
const pdfLayoutNameMock = 'pdfLayoutNameMock';
const mutateLayoutSettingsMock = jest.fn();
await renderConvertChoicesModal(
{ pages: { order: [layout1NameMock], pdfLayoutName: pdfLayoutNameMock } },
{},
{ saveFormLayoutSettings: mutateLayoutSettingsMock },
);
const convertExistingPdfToFormLayout = screen.getByRole('button', {
name: textMock('ux_editor.page_config_pdf_convert_existing_pdf'),
});
await user.click(convertExistingPdfToFormLayout);
expect(mutateLayoutSettingsMock).toHaveBeenCalledTimes(1);
expect(mutateLayoutSettingsMock).toHaveBeenCalledWith(org, app, layoutSet1NameMock, {
pages: { order: [pdfLayoutNameMock], pdfLayoutName: layout1NameMock },
});
});

it('deletes existing pdf when clicking delete in conversion choices modal', async () => {
const user = userEvent.setup();
const pdfLayoutNameMock = 'pdfLayoutNameMock';
const mutateLayoutSettingsMock = jest.fn();
const deleteLayoutMock = jest.fn();
await renderConvertChoicesModal(
{ pages: { order: [layout1NameMock], pdfLayoutName: pdfLayoutNameMock } },
{},
{ saveFormLayoutSettings: mutateLayoutSettingsMock, deleteFormLayout: deleteLayoutMock },
);
const deleteExistingPdf = screen.getByRole('button', {
name: textMock('ux_editor.page_config_pdf_delete_existing_pdf'),
});
await user.click(deleteExistingPdf);
expect(mutateLayoutSettingsMock).toHaveBeenCalledTimes(2); // Once from pdfConfig and another from deleteLayout
expect(mutateLayoutSettingsMock).toHaveBeenCalledWith(org, app, layoutSet1NameMock, {
pages: { order: [], pdfLayoutName: layout1NameMock },
});
expect(deleteLayoutMock).toHaveBeenCalledTimes(1);
expect(deleteLayoutMock).toHaveBeenCalledWith(org, app, pdfLayoutNameMock, selectedLayoutSet);
});

it('calls handleModalAction when converting existing pdf', async () => {
const user = userEvent.setup();
const pdfLayoutNameMock = 'pdfLayoutNameMock';
await renderConvertChoicesModal({
pages: { order: [layout1NameMock], pdfLayoutName: pdfLayoutNameMock },
});
const convertExistingPdfToFormLayout = screen.getByRole('button', {
name: textMock('ux_editor.page_config_pdf_convert_existing_pdf'),
});
await user.click(convertExistingPdfToFormLayout);
expect(handleModalActionMock).toHaveBeenCalledTimes(1);
});

it('calls handleModalAction when deleting existing pdf', async () => {
const user = userEvent.setup();
const pdfLayoutNameMock = 'pdfLayoutNameMock';
await renderConvertChoicesModal({
pages: { order: [layout1NameMock], pdfLayoutName: pdfLayoutNameMock },
});
const deleteExistingPdf = screen.getByRole('button', {
name: textMock('ux_editor.page_config_pdf_delete_existing_pdf'),
});
await user.click(deleteExistingPdf);
expect(handleModalActionMock).toHaveBeenCalledTimes(1);
});
});

const renderConvertChoicesModal = async (
layoutSettings: Partial<ILayoutSettings> = {},
appContextProps: Partial<AppContextProps> = {},
queries: Partial<ServicesContextProps> = {},
) => {
const ref = createRef<HTMLDialogElement>();
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.FormLayoutSettings, org, app, selectedLayoutSet], {
...formLayoutSettingsMock,
...layoutSettings,
});
renderWithProviders(<ConvertChoicesModal handleModalAction={handleModalActionMock} ref={ref} />, {
queries,
queryClient,
appContextProps,
});
ref.current?.showModal();
await screen.findByRole('dialog');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { forwardRef } from 'react';
import { StudioModal } from '@studio/components';
import { useForwardedRef } from '@studio/hooks';
import { OverrideCurrentPdfByConversionChoices } from './OverrideCurrentPdfByConversionChoices';
import { useTranslation } from 'react-i18next';
import { usePdf } from '@altinn/ux-editor/hooks/usePdf/usePdf';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useAppContext } from '@altinn/ux-editor/hooks';
import { useDeleteLayoutMutation } from '@altinn/ux-editor/hooks/mutations/useDeleteLayoutMutation';
import { useSavableFormLayoutSettings } from '@altinn/ux-editor/hooks/useSavableFormLayoutSettings';

type ConvertChoicesModalProps = {
handleModalAction: () => void;
};
export const ConvertChoicesModal = forwardRef<HTMLDialogElement, ConvertChoicesModalProps>(
({ handleModalAction }, ref): JSX.Element => {
const { org, app } = useStudioEnvironmentParams();
const { selectedFormLayoutSetName } = useAppContext();
const { t } = useTranslation();
const { mutate: deleteLayout } = useDeleteLayoutMutation(org, app, selectedFormLayoutSetName);
const { getPdfLayoutName, convertCurrentPageToPdf, convertExistingPdfToPage } = usePdf();
const savableLayoutSettings = useSavableFormLayoutSettings();
const dialogRef = useForwardedRef<HTMLDialogElement>(ref);

const handleConvertPageToPdfAndConvertCurrent = () => {
convertExistingPdfToPage();
convertCurrentPageToPdf();
savableLayoutSettings.save();
handleModalAction();
dialogRef.current?.close();
};

const handleConvertPageToPdfAndDeleteCurrent = () => {
const currentPdfLayoutName = getPdfLayoutName();
convertCurrentPageToPdf();
deleteLayout(currentPdfLayoutName);
savableLayoutSettings.save();
handleModalAction();
dialogRef.current?.close();
};

return (
<StudioModal.Dialog
closeButtonTitle={t('ux_editor.page_config_pdf_abort_converting_page_to_pdf')}
heading={t('ux_editor.page_config_pdf_convert_page_to_pdf')}
ref={dialogRef}
>
<OverrideCurrentPdfByConversionChoices
onConvertPageToPdfAndConvertCurrent={handleConvertPageToPdfAndConvertCurrent}
onConvertPageToPdfAndDeleteCurrent={handleConvertPageToPdfAndDeleteCurrent}
/>
</StudioModal.Dialog>
);
},
);

ConvertChoicesModal.displayName = 'ConvertChoicesModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.buttonContainer {
display: flex;
flex-direction: row;
gap: var(--fds-spacing-2);
padding-top: var(--fds-spacing-10);
}
Loading

0 comments on commit 8292aeb

Please sign in to comment.