Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add PDF config for layoutset #13512

Merged
merged 12 commits into from
Oct 2, 2024
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
Loading