diff --git a/frontend/app-development/features/textEditor/TextEditor.test.tsx b/frontend/app-development/features/textEditor/TextEditor.test.tsx index aa1242afc12..963e305a824 100644 --- a/frontend/app-development/features/textEditor/TextEditor.test.tsx +++ b/frontend/app-development/features/textEditor/TextEditor.test.tsx @@ -36,6 +36,9 @@ jest.mock('react-router-dom', () => ({ }, })); +// Need to mock the scrollIntoView function +window.HTMLElement.prototype.scrollIntoView = jest.fn(); + describe('TextEditor', () => { it('renders the component', async () => { await render(); diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 8ef7c55ac55..bd0bc97ad33 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -163,7 +163,7 @@ "dashboard.search": "Søk etter applikasjon", "dashboard.search_result": "Søkeresultat", "dashboard.select_datamodelling_format": "Velg datamodelleringsformat", - "dashboard.service_name_has_illegal_characters": "Navnet er ugyldig. Det kan kun inneholde små bokstaver, sifre og bindestreker (-), og må begynne på en bokstav og ende med en bokstav eller et tall.", + "dashboard.service_name_has_illegal_characters": "Navnet er ugyldig. Det må være minimum tre tegn, kan kun inneholde små bokstaver (utenom æ, ø, og å), sifre og bindestreker (-), og må begynne på en bokstav og ende med en bokstav eller et tall.", "dashboard.service_name_is_too_long": "Repository-navnet kan maksimalt bestå av 30 tegn", "dashboard.service_saved_name_description": "Unik identifikator for appen. Brukes som navn på repository samt i alle URLer og i Altinn sine APIer. Bør derfor være både kort og beskrivende, f.eks. \"sykemelding\" eller \"skattemelding-2019\".", "dashboard.service_saved_name_description_cannot_be_changed": "Dette navnet kan ikke endres.", diff --git a/frontend/packages/text-editor/src/TextEditor.test.tsx b/frontend/packages/text-editor/src/TextEditor.test.tsx index aa774d5627d..97b26aa2cc9 100644 --- a/frontend/packages/text-editor/src/TextEditor.test.tsx +++ b/frontend/packages/text-editor/src/TextEditor.test.tsx @@ -8,6 +8,7 @@ import { ITextResource, ITextResources } from 'app-shared/types/global'; import * as testids from '../../../testing/testids'; const user = userEvent.setup(); +let mockScrollIntoView = jest.fn(); describe('TextEditor', () => { const nb: ITextResource[] = [ @@ -37,6 +38,11 @@ describe('TextEditor', () => { }; return rtlRender(); }; + beforeEach(() => { + // Need to mock the scrollIntoView function + mockScrollIntoView = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + }); it('fires upsertTextResource when Add new is clicked', async () => { jest.spyOn(global.Math, 'random').mockReturnValue(0); @@ -72,8 +78,8 @@ describe('TextEditor', () => { user.click( screen.getByRole('button', { name: textMock('schema_editor.language_confirm_deletion'), - }) - ) + }), + ), ); expect(handleDeleteLang).toHaveBeenCalledWith('en'); @@ -95,8 +101,8 @@ describe('TextEditor', () => { user.click( screen.getByRole('button', { name: textMock('schema_editor.language_confirm_deletion'), - }) - ) + }), + ), ); expect(handleDeleteLang).toHaveBeenCalledWith('en'); @@ -122,6 +128,19 @@ describe('TextEditor', () => { expect(setSelectedLangCodes).toHaveBeenCalledWith(['nb', 'en']); }); + it('Calls ScrollIntoView when a new languages is selected', async () => { + renderTextEditor({ + availableLanguages: ['nb', 'en', 'tw', 'ku'], + selectedLangCodes: ['nb', 'en', 'tw'], + }); + const kurdishCheckbox = screen.getByRole('checkbox', { + name: /kurdisk/i, + }); + await act(() => user.click(kurdishCheckbox)); + + expect(mockScrollIntoView).toHaveBeenCalledTimes(1); + }); + it('signals correctly when a translation is changed', async () => { const upsertTextResource = jest.fn(); renderTextEditor({ @@ -158,8 +177,8 @@ describe('TextEditor', () => { user.click( screen.getByRole('button', { name: textMock('schema_editor.textRow-deletion-confirm'), - }) - ) + }), + ), ); await expect(onTextIdChange).toHaveBeenCalledWith({ oldId: nb[0].id }); diff --git a/frontend/packages/text-editor/src/TextEditor.tsx b/frontend/packages/text-editor/src/TextEditor.tsx index 0d2a6528b44..3e49dd7669b 100644 --- a/frontend/packages/text-editor/src/TextEditor.tsx +++ b/frontend/packages/text-editor/src/TextEditor.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import classes from './TextEditor.module.css'; import type { LangCode, @@ -43,11 +43,28 @@ export const TextEditor = ({ }: TextEditorProps) => { const { t } = useTranslation(); const resourceRows = mapResourceFilesToTableRows(textResourceFiles); + + const previousSelectedLanguages = useRef([]); + const availableLangCodesFiltered = useMemo( () => availableLanguages?.filter((code) => ISO6391.validate(code)), [availableLanguages], ); + useEffect(() => { + const addedLanguage = selectedLangCodes.find( + (lang) => !previousSelectedLanguages.current.includes(lang), + ); + + if (addedLanguage) { + const elementToFocus: HTMLElement = document.getElementById('header-lang' + addedLanguage); + if (elementToFocus) { + elementToFocus.scrollIntoView({ behavior: 'smooth', inline: 'center' }); + } + } + previousSelectedLanguages.current = selectedLangCodes; + }, [selectedLangCodes.length, selectedLangCodes]); + const handleAddNewEntryClick = () => { const textId = `id_${getRandNumber()}`; availableLangCodesFiltered.forEach((language) => diff --git a/frontend/packages/text-editor/src/TextList.tsx b/frontend/packages/text-editor/src/TextList.tsx index 09bd15a7313..808cee35296 100644 --- a/frontend/packages/text-editor/src/TextList.tsx +++ b/frontend/packages/text-editor/src/TextList.tsx @@ -26,6 +26,7 @@ export const TextList = ({ }: TextListProps) => { const textIds = useMemo(() => resourceRows.map((row) => row.textKey), [resourceRows]); const idExists = (textId: string): boolean => textIds.includes(textId); + const getTableHeaderCellId = (language: string): string => `header-lang${language}`; return ( @@ -33,7 +34,9 @@ export const TextList = ({ {selectedLanguages.map((language) => ( - {getLangName({ code: language })} + + {getLangName({ code: language })} + ))} Tekstnøkkel Variabler diff --git a/frontend/packages/ux-editor/src/containers/DesignView/DesignView.tsx b/frontend/packages/ux-editor/src/containers/DesignView/DesignView.tsx index fadd6f02ad4..0821cfaf25d 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/DesignView.tsx +++ b/frontend/packages/ux-editor/src/containers/DesignView/DesignView.tsx @@ -39,7 +39,11 @@ export const DesignView = (): ReactNode => { const dispatch = useDispatch(); const { org, app } = useStudioUrlParams(); const { selectedLayoutSet } = useAppContext(); - const addLayoutMutation = useAddLayoutMutation(org, app, selectedLayoutSet); + const { mutate: addLayoutMutation, isPending } = useAddLayoutMutation( + org, + app, + selectedLayoutSet, + ); const { data: layouts } = useFormLayoutsQuery(org, app, selectedLayoutSet); const { data: instanceId } = useInstanceIdQuery(org, app); const { data: formLayoutSettings } = useFormLayoutSettingsQuery(org, app, selectedLayoutSet); @@ -99,23 +103,25 @@ export const DesignView = (): ReactNode => { */ const handleAddPage = (isReceipt: boolean) => { if (isReceipt) { - addLayoutMutation.mutate({ layoutName: 'Kvittering', isReceiptPage: true }); + addLayoutMutation({ layoutName: 'Kvittering', isReceiptPage: true }); setSearchParams((prevParams) => ({ ...prevParams, layout: 'Kvittering' })); setOpenAccordion('Kvittering'); } else { - let newNum = 1; - let newLayoutName = `${t('ux_editor.page')}${layoutOrder.length + newNum}`; + if (!isPending) { + let newNum = 1; + let newLayoutName = `${t('ux_editor.page')}${layoutOrder.length + newNum}`; - while (layoutOrder.indexOf(newLayoutName) > -1) { - newNum += 1; - newLayoutName = `${t('ux_editor.page')}${newNum}`; - } + while (layoutOrder.indexOf(newLayoutName) > -1) { + newNum += 1; + newLayoutName = `${t('ux_editor.page')}${newNum}`; + } - addLayoutMutation.mutate({ layoutName: newLayoutName, isReceiptPage: false }); - setSearchParams((prevParams) => ({ ...prevParams, layout: newLayoutName })); - setSelectedLayoutInLocalStorage(instanceId, newLayoutName); - dispatch(FormLayoutActions.updateSelectedLayout(newLayoutName)); - setOpenAccordion(newLayoutName); + addLayoutMutation({ layoutName: newLayoutName, isReceiptPage: false }); + setSearchParams((prevParams) => ({ ...prevParams, layout: newLayoutName })); + setSelectedLayoutInLocalStorage(instanceId, newLayoutName); + dispatch(FormLayoutActions.updateSelectedLayout(newLayoutName)); + setOpenAccordion(newLayoutName); + } } };