diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx
index ac0c6d30212..8fdf92d0c90 100644
--- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx
+++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx
@@ -15,12 +15,14 @@ import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
const uploadCodeListButtonTextMock = 'Upload Code List';
const updateCodeListButtonTextMock = 'Update Code List';
+const updateCodeListIdButtonTextMock = 'Update Code List Id';
const codeListNameMock = 'codeListNameMock';
+const newCodeListNameMock = 'newCodeListNameMock';
const codeListMock: CodeList = [{ value: '', label: '' }];
jest.mock(
'../../../libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage',
() => ({
- CodeListPage: ({ onUpdateCodeList, onUploadCodeList }: any) => (
+ CodeListPage: ({ onUpdateCodeList, onUploadCodeList, onUpdateCodeListId }: any) => (
) => isErrorUnknown(error),
});
const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app);
+ const { mutate: updateOptionListId } = useUpdateOptionListIdMutation(org, app);
if (optionListsPending)
return ;
const codeLists = convertOptionListsToCodeLists(optionLists);
+ const handleUpdateCodeListId = (optionListId: string, newOptionListId: string) => {
+ updateOptionListId({ optionListId, newOptionListId });
+ };
+
const handleUpload = (file: File) => {
uploadOptionList(file, {
onSuccess: () => {
@@ -52,6 +61,7 @@ export function AppContentLibrary(): React.ReactElement {
codeList: {
props: {
codeLists: codeLists,
+ onUpdateCodeListId: handleUpdateCodeListId,
onUpdateCodeList: handleUpdate,
onUploadCodeList: handleUpload,
fetchDataError: optionListsError,
diff --git a/frontend/app-development/features/appPublish/components/DeployDropdown.tsx b/frontend/app-development/features/appPublish/components/DeployDropdown.tsx
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/DeleteWrapper/DeleteWrapper.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/DeleteWrapper/DeleteWrapper.tsx
index 1c0c12c5d01..c8d5f57e4d9 100644
--- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/DeleteWrapper/DeleteWrapper.tsx
+++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/DeleteWrapper/DeleteWrapper.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
-import { StudioButton } from '@studio/components';
import { TrashIcon } from '@studio/icons';
import { useDeleteDataModelMutation } from '../../../../../hooks/mutations';
import type { MetadataOption } from '../../../../../types/MetadataOption';
@@ -43,18 +42,15 @@ export function DeleteWrapper({ selectedOption }: DeleteWrapperProps) {
confirmText={t('schema_editor.confirm_deletion')}
onConfirm={onDeleteConfirmClick}
onClose={() => setDialogOpen(false)}
- trigger={
- }
- variant='tertiary'
- >
- {t('schema_editor.delete_data_model')}
-
- }
+ triggerProps={{
+ id: 'delete-model-button',
+ disabled: !schemaName,
+ onClick: onDeleteClick,
+ color: 'danger',
+ icon: ,
+ variant: 'tertiary',
+ children: t('schema_editor.delete_data_model'),
+ }}
>
{{codeListsCount}} kodelister i biblioteket.",
"app_content_library.code_lists.code_lists_count_info_single": "Det finnes 1 kodeliste i biblioteket.",
diff --git a/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/StudioPageHeaderPopoverTrigger.test.tsx b/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/StudioPageHeaderPopoverTrigger.test.tsx
new file mode 100644
index 00000000000..d0199a13972
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/StudioPageHeaderPopoverTrigger.test.tsx
@@ -0,0 +1,66 @@
+import type { ForwardedRef } from 'react';
+import React from 'react';
+import type { RenderResult } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
+import { StudioPageHeader } from '../';
+import { StudioPopover } from '../../';
+import userEvent from '@testing-library/user-event';
+import { testRefForwarding } from '../../../test-utils/testRefForwarding';
+import type { StudioPageHeaderPopoverTriggerProps } from './StudioPageHeaderPopoverTrigger';
+import { testRootClassNameAppending } from '../../../test-utils/testRootClassNameAppending';
+import { testCustomAttributes } from '../../../test-utils/testCustomAttributes';
+
+// Test data:
+const triggerText = 'Trigger';
+const contentText = 'Content';
+const defaultProps: StudioPageHeaderPopoverTriggerProps = {
+ children: triggerText,
+};
+
+describe('StudioPageHeader.PopoverTrigger', () => {
+ it('Renders the trigger button', () => {
+ renderPopover();
+ expect(screen.getByRole('button', { name: triggerText })).toBeInTheDocument();
+ });
+
+ it('Does not display the popover by default', () => {
+ renderPopover();
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+
+ it('Opens the popover when the user clicks the trigger', async () => {
+ const user = userEvent.setup();
+ renderPopover();
+ await user.click(screen.getByRole('button', { name: triggerText }));
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByRole('dialog')).toHaveTextContent(contentText);
+ });
+
+ it('Forwards the ref to the trigger button', () => {
+ testRefForwarding((ref) => renderPopoverTrigger({}, ref));
+ });
+
+ it('Appends the given className to the trigger button', () => {
+ testRootClassNameAppending((className) => renderPopoverTrigger({ className }));
+ });
+
+ it('Accepts custom attributes', () => {
+ testCustomAttributes(renderPopoverTrigger);
+ });
+});
+
+function renderPopover(): RenderResult {
+ return render(
+
+
+ {contentText}
+ ,
+ );
+}
+
+function renderPopoverTrigger(
+ props: StudioPageHeaderPopoverTriggerProps,
+ ref?: ForwardedRef,
+): RenderResult {
+ return render();
+}
diff --git a/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/StudioPageHeaderPopoverTrigger.tsx b/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/StudioPageHeaderPopoverTrigger.tsx
new file mode 100644
index 00000000000..7215430d83c
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/StudioPageHeaderPopoverTrigger.tsx
@@ -0,0 +1,27 @@
+import React, { forwardRef } from 'react';
+import type { StudioPopoverTriggerProps } from '../../StudioPopover';
+import { StudioPopover } from '../../StudioPopover';
+import cn from 'classnames';
+import classes from '../common.module.css';
+import type { StudioPageHeaderColor } from '../types/StudioPageHeaderColor';
+import type { StudioPageHeaderVariant } from '../types/StudioPageHeaderVariant';
+
+export type StudioPageHeaderPopoverTriggerProps = {
+ color?: StudioPageHeaderColor;
+ variant?: StudioPageHeaderVariant;
+} & Omit;
+
+export const StudioPageHeaderPopoverTrigger = forwardRef<
+ HTMLButtonElement,
+ StudioPageHeaderPopoverTriggerProps
+>(
+ (
+ { color = 'dark', variant = 'regular', className: givenClass, ...rest },
+ ref,
+ ): React.ReactElement => {
+ const className = cn(classes.linkOrButton, classes[variant], classes[color], givenClass);
+ return ;
+ },
+);
+
+StudioPageHeaderPopoverTrigger.displayName = 'StudioPageHeader.PopoverTrigger';
diff --git a/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/index.ts b/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/index.ts
new file mode 100644
index 00000000000..2f97077da3c
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPageHeader/StudioPageHeaderPopoverTrigger/index.ts
@@ -0,0 +1 @@
+export * from './StudioPageHeaderPopoverTrigger';
diff --git a/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts b/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts
index 3da6a9f5c87..042decf25eb 100644
--- a/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts
+++ b/frontend/libs/studio-components/src/components/StudioPageHeader/index.ts
@@ -14,12 +14,14 @@ import { StudioPageHeaderMain } from './StudioPageHeaderMain';
import { StudioPageHeaderRight } from './StudioPageHeaderRight';
import { StudioPageHeaderSub } from './StudioPageHeaderSub';
import { StudioPageHeaderHeaderLink } from './StudioPageHeaderHeaderLink';
+import { StudioPageHeaderPopoverTrigger } from './StudioPageHeaderPopoverTrigger';
type StudioPageHeaderComponent = typeof StudioPageHeaderParent & {
Main: typeof StudioPageHeaderMain;
Left: typeof StudioPageHeaderLeft;
Center: typeof StudioPageHeaderCenter;
Right: typeof StudioPageHeaderRight;
+ PopoverTrigger: typeof StudioPageHeaderPopoverTrigger;
Sub: typeof StudioPageHeaderSub;
HeaderButton: typeof StudioPageHeaderHeaderButton;
HeaderLink: typeof StudioPageHeaderHeaderLink;
@@ -32,6 +34,7 @@ StudioPageHeader.Main = StudioPageHeaderMain;
StudioPageHeader.Left = StudioPageHeaderLeft;
StudioPageHeader.Center = StudioPageHeaderCenter;
StudioPageHeader.Right = StudioPageHeaderRight;
+StudioPageHeader.PopoverTrigger = StudioPageHeaderPopoverTrigger;
StudioPageHeader.Sub = StudioPageHeaderSub;
StudioPageHeader.HeaderButton = StudioPageHeaderHeaderButton;
StudioPageHeader.HeaderLink = StudioPageHeaderHeaderLink;
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx
index 313827d59d6..101f548287f 100644
--- a/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx
+++ b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx
@@ -1,24 +1,34 @@
-import React from 'react';
-import {
- type PopoverProps,
- Popover,
- type PopoverTriggerProps,
- type PopoverContentProps,
-} from '@digdir/designsystemet-react';
-
-const StudioPopoverTrigger = ({ ...rest }: PopoverTriggerProps): React.ReactElement => {
- return ;
-};
+import React, { forwardRef } from 'react';
+import { type PopoverProps, Popover, type PopoverContentProps } from '@digdir/designsystemet-react';
+import type { WithoutAsChild } from '../../types/WithoutAsChild';
+import type { StudioButtonProps } from '../StudioButton';
+import { StudioButton } from '../StudioButton';
-const StudioPopoverContent = ({ ...rest }: PopoverContentProps): React.ReactElement => {
- return ;
-};
+export type StudioPopoverTriggerProps = StudioButtonProps;
-export type StudioPopoverProps = PopoverProps;
+const StudioPopoverTrigger = forwardRef(
+ (props, ref): React.ReactElement => (
+
+
+
+ ),
+);
-const StudioPopoverRoot = ({ ...rest }: StudioPopoverProps): React.ReactElement => {
- return ;
-};
+StudioPopoverTrigger.displayName = 'StudioPopover.Trigger';
+
+export type StudioPopoverContentProps = WithoutAsChild;
+
+const StudioPopoverContent = forwardRef(
+ (props, ref): React.ReactElement => ,
+);
+
+StudioPopoverContent.displayName = 'StudioPopover.Content';
+
+export type StudioPopoverProps = WithoutAsChild;
+
+function StudioPopoverRoot(props: StudioPopoverProps): React.ReactElement {
+ return ;
+}
type StudioPopoverComponent = typeof StudioPopoverRoot & {
Trigger: typeof StudioPopoverTrigger;
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/index.ts b/frontend/libs/studio-components/src/components/StudioPopover/index.ts
index 1419f61c659..2591a8a6ae6 100644
--- a/frontend/libs/studio-components/src/components/StudioPopover/index.ts
+++ b/frontend/libs/studio-components/src/components/StudioPopover/index.ts
@@ -1,2 +1 @@
-export { StudioPopover } from './StudioPopover';
-export type { StudioPopoverProps } from './StudioPopover';
+export * from './StudioPopover';
diff --git a/frontend/libs/studio-components/src/types/WithoutAsChild.ts b/frontend/libs/studio-components/src/types/WithoutAsChild.ts
new file mode 100644
index 00000000000..bfbdee4457f
--- /dev/null
+++ b/frontend/libs/studio-components/src/types/WithoutAsChild.ts
@@ -0,0 +1 @@
+export type WithoutAsChild = Omit;
diff --git a/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts b/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts
index 73ea05e2aaf..58690a66167 100644
--- a/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts
+++ b/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts
@@ -7,6 +7,7 @@ export const mockPagesConfig: PagesConfig = {
{ title: 'CodeList1', codeList: [] },
{ title: 'CodeList2', codeList: [] },
],
+ onUpdateCodeListId: () => {},
onUpdateCodeList: () => {},
onUploadCodeList: () => {},
fetchDataError: false,
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx
index be1931673aa..a966897efe4 100644
--- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx
@@ -2,19 +2,31 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import type { CodeListPageProps, CodeListWithMetadata } from './CodeListPage';
import { CodeListPage } from './CodeListPage';
+import userEvent from '@testing-library/user-event';
+import type { UserEvent } from '@testing-library/user-event';
import { textMock } from '@studio/testing/mocks/i18nMock';
+import type { CodeList as StudioComponentCodeList } from '@studio/components';
+const onUpdateCodeListIdMock = jest.fn();
const onUpdateCodeListMock = jest.fn();
const onUploadCodeListMock = jest.fn();
const codeListName = 'codeList';
-const codeListMock: CodeListWithMetadata = {
+const codeListMock: StudioComponentCodeList = [{ value: 'value', label: 'label' }];
+const codeListWithMetadataMock: CodeListWithMetadata = {
title: codeListName,
- codeList: [{ value: 'value', label: 'label' }],
+ codeList: codeListMock,
};
+const uploadedCodeListName = 'uploadedCodeListName';
describe('CodeListPage', () => {
+ afterEach(() => {
+ defaultCodeListPageProps.codeLists = [codeListWithMetadataMock];
+ defaultCodeListPageProps.fetchDataError = false;
+ jest.clearAllMocks();
+ });
+
it('renders the codeList heading', () => {
- renderCodeList();
+ renderCodeListPage();
const codeListHeading = screen.getByRole('heading', {
name: textMock('app_content_library.code_lists.page_name'),
});
@@ -22,7 +34,7 @@ describe('CodeListPage', () => {
});
it('renders a code list counter message', () => {
- renderCodeList();
+ renderCodeListPage();
const codeListCounterMessage = screen.getByText(
textMock('app_content_library.code_lists.code_lists_count_info_single'),
);
@@ -30,7 +42,7 @@ describe('CodeListPage', () => {
});
it('renders code list actions', () => {
- renderCodeList();
+ renderCodeListPage();
const codeListSearchField = screen.getByRole('searchbox');
const codeListCreatButton = screen.getByRole('button', {
name: textMock('app_content_library.code_lists.create_new_code_list'),
@@ -43,37 +55,127 @@ describe('CodeListPage', () => {
expect(codeListUploadButton).toBeInTheDocument();
});
- it('renders the code list as a clickable element', () => {
- renderCodeList();
- const codeListAccordion = screen.getByRole('button', { name: codeListName });
+ it('renders the code list accordion', () => {
+ renderCodeListPage();
+ const codeListAccordion = screen.getByTitle(
+ textMock('app_content_library.code_lists.code_list_accordion_title', {
+ codeListTitle: codeListName,
+ }),
+ );
expect(codeListAccordion).toBeInTheDocument();
});
+ it('render the code list accordion as default open when uploading a code list', async () => {
+ const user = userEvent.setup();
+ const { rerender } = renderCodeListPage();
+ const codeListAccordionClosed = screen.getByRole('button', {
+ name: codeListName,
+ expanded: false,
+ });
+ expect(codeListAccordionClosed).toHaveAttribute('aria-expanded', 'false');
+ await uploadCodeList(user, uploadedCodeListName);
+ defaultCodeListPageProps.codeLists.push({
+ title: uploadedCodeListName,
+ codeList: codeListMock,
+ });
+ rerender(
+ ,
+ );
+ const codeListAccordionOpen = screen.getByRole('button', {
+ name: uploadedCodeListName,
+ expanded: true,
+ });
+ expect(codeListAccordionOpen).toHaveAttribute('aria-expanded', 'true');
+ });
+
it('renders error message if error fetching option lists occurred', () => {
- renderCodeList({ fetchDataError: true });
+ renderCodeListPage({ fetchDataError: true });
const errorMessage = screen.getByText(textMock('app_content_library.code_lists.fetch_error'));
expect(errorMessage).toBeInTheDocument();
});
+
+ it('calls onUpdateCodeListId when Id is changed', async () => {
+ const user = userEvent.setup();
+ renderCodeListPage();
+ await changeCodeListId(user, codeListName);
+ expect(onUpdateCodeListIdMock).toHaveBeenCalledTimes(1);
+ expect(onUpdateCodeListIdMock).toHaveBeenCalledWith(codeListName, codeListName + '2');
+ });
+
+ it('calls onUpdateCodeList when code list is changed', async () => {
+ const user = userEvent.setup();
+ const newValueText = 'newValueText';
+ renderCodeListPage();
+ await changeCodeListContent(user, newValueText);
+ expect(onUpdateCodeListMock).toHaveBeenCalledTimes(1);
+ expect(onUpdateCodeListMock).toHaveBeenLastCalledWith({
+ ...codeListWithMetadataMock,
+ codeList: [{ ...codeListWithMetadataMock.codeList[0], value: newValueText }],
+ });
+ });
+
+ it('calls onUploadCodeList when uploading a code list', async () => {
+ const user = userEvent.setup();
+ renderCodeListPage();
+ await uploadCodeList(user);
+ expect(onUploadCodeListMock).toHaveBeenCalledTimes(1);
+ expect(onUploadCodeListMock).toHaveBeenCalledWith(expect.any(Object));
+ });
});
-const defaultCodeListProps: CodeListPageProps = {
- codeLists: [codeListMock],
- onUpdateCodeList: onUpdateCodeListMock,
- onUploadCodeList: onUploadCodeListMock,
+const changeCodeListId = async (user: UserEvent, codeListNameToChange: string) => {
+ const codeListIdToggleTextfield = screen.getByTitle(
+ textMock('app_content_library.code_lists.code_list_view_id_title', {
+ codeListName: codeListNameToChange,
+ }),
+ );
+ await user.click(codeListIdToggleTextfield);
+ const codeListIdInput = screen.getByTitle(
+ textMock('app_content_library.code_lists.code_list_edit_id_title', {
+ codeListName: codeListNameToChange,
+ }),
+ );
+ await user.type(codeListIdInput, '2');
+ await user.tab();
+};
+
+const changeCodeListContent = async (user: UserEvent, newValueText: string) => {
+ const codeListFirstItemValue = screen.getByLabelText(
+ textMock('code_list_editor.value_item', { number: 1 }),
+ );
+ await user.type(codeListFirstItemValue, newValueText);
+ await user.tab();
+};
+
+const uploadCodeList = async (user: UserEvent, fileName: string = uploadedCodeListName) => {
+ const fileUploaderButton = screen.getByLabelText(
+ textMock('app_content_library.code_lists.upload_code_list'),
+ );
+ const file = new File(['test'], `${fileName}.json`, { type: 'application/json' });
+ await user.upload(fileUploaderButton, file);
+};
+
+const defaultCodeListPageProps: Partial = {
+ codeLists: [codeListWithMetadataMock],
fetchDataError: false,
};
-const renderCodeList = ({
+const renderCodeListPage = ({
codeLists,
- onUpdateCodeList,
- onUploadCodeList,
fetchDataError,
-}: Partial = defaultCodeListProps) => {
- render(
+}: Partial = defaultCodeListPageProps) => {
+ return render(
,
);
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx
index 224aac3d3dd..db9744acf9f 100644
--- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import { StudioHeading, StudioPageError } from '@studio/components';
import type { CodeList as StudioComponentCodeList } from '@studio/components';
import { useTranslation } from 'react-i18next';
@@ -6,7 +6,7 @@ import { CodeListsActionsBar } from './CodeListsActionsBar';
import { CodeLists } from './CodeLists';
import { CodeListsCounterMessage } from './CodeListsCounterMessage';
import classes from './CodeListPage.module.css';
-import { ArrayUtils } from '@studio/pure-functions';
+import { ArrayUtils, FileNameUtils } from '@studio/pure-functions';
export type CodeListWithMetadata = {
codeList: StudioComponentCodeList;
@@ -15,33 +15,52 @@ export type CodeListWithMetadata = {
export type CodeListPageProps = {
codeLists: CodeListWithMetadata[];
+ onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
onUploadCodeList: (uploadedCodeList: File) => void;
fetchDataError: boolean;
};
export function CodeListPage({
codeLists,
+ onUpdateCodeListId,
onUpdateCodeList,
onUploadCodeList,
fetchDataError,
}: CodeListPageProps): React.ReactElement {
const { t } = useTranslation();
+ const [codeListInEditMode, setCodeListInEditMode] = useState(undefined);
if (fetchDataError)
return ;
const codeListTitles = ArrayUtils.mapByKey(codeLists, 'title');
+ const handleUploadCodeList = (uploadedCodeList: File) => {
+ setCodeListInEditMode(FileNameUtils.removeExtension(uploadedCodeList.name));
+ onUploadCodeList(uploadedCodeList);
+ };
+
+ const handleUpdateCodeListId = (codeListId: string, newCodeListId: string) => {
+ setCodeListInEditMode(newCodeListId);
+ onUpdateCodeListId(codeListId, newCodeListId);
+ };
+
return (
{t('app_content_library.code_lists.page_name')}
+
-
);
}
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.test.tsx
index 3ee864fd806..d549a24b490 100644
--- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.test.tsx
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.test.tsx
@@ -4,8 +4,8 @@ import type { CodeListsProps } from './CodeLists';
import { updateCodeListWithMetadata, CodeLists } from './CodeLists';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { CodeListWithMetadata } from '../CodeListPage';
-import type { UserEvent } from '@testing-library/user-event';
import type { RenderResult } from '@testing-library/react';
+import type { UserEvent } from '@testing-library/user-event';
import userEvent from '@testing-library/user-event';
import type { CodeList as StudioComponentsCodeList } from '@studio/components';
@@ -14,28 +14,35 @@ const codeListWithMetadataMock: CodeListWithMetadata = {
title: codeListName,
codeList: [{ value: 'value', label: 'label' }],
};
+const onUpdateCodeListIdMock = jest.fn();
const onUpdateCodeListMock = jest.fn();
describe('CodeLists', () => {
- it('renders the code list', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('renders the code list accordion closed by default', () => {
renderCodeLists();
- const codeListAccordion = screen.getByRole('button', { name: codeListName });
+ const codeListAccordion = screen.getByRole('button', { name: codeListName, expanded: false });
expect(codeListAccordion).toBeInTheDocument();
+ expect(codeListAccordion).toHaveAttribute('aria-expanded', 'false');
});
- it('renders the code list editor when opening the accordion', async () => {
- const user = userEvent.setup();
+ it('renders the code list accordion open by default if code list title is equal to codeListInEditMode', () => {
+ renderCodeLists({ codeListInEditMode: codeListName });
+ const codeListAccordion = screen.getByRole('button', { name: codeListName, expanded: true });
+ expect(codeListAccordion).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('renders the code list editor', () => {
renderCodeLists();
- await openCodeList(user);
const codeListEditor = screen.getByText(textMock('code_list_editor.legend'));
- expect(codeListEditor).toBeVisible();
+ expect(codeListEditor).toBeInTheDocument();
});
it('calls onUpdateCodeList when changing a code list', async () => {
const user = userEvent.setup();
const codeListValueText = 'codeListValueText';
renderCodeLists();
- await openCodeList(user);
const codeListFirstItemValue = screen.getByLabelText(
textMock('code_list_editor.value_item', { number: 1 }),
);
@@ -48,16 +55,64 @@ describe('CodeLists', () => {
title: codeListName,
});
});
+
+ it('renders the code list title label', () => {
+ renderCodeLists();
+ const codeListTitleLabel = screen.getByText(
+ textMock('app_content_library.code_lists.code_list_edit_id_label'),
+ );
+ expect(codeListTitleLabel).toBeInTheDocument();
+ });
+
+ it('calls onUpdateCodeListId when changing the code list id', async () => {
+ const user = userEvent.setup();
+ renderCodeLists();
+ await changeCodeListId(user, codeListName, codeListName + '2');
+ expect(onUpdateCodeListIdMock).toHaveBeenCalledTimes(1);
+ expect(onUpdateCodeListIdMock).toHaveBeenLastCalledWith(codeListName, codeListName + '2');
+ });
+
+ it('shows error message when assigning an invalid id to the code list', async () => {
+ const user = userEvent.setup();
+ const invalidCodeListName = 'invalidCodeListName';
+ renderCodeLists({ codeListNames: [invalidCodeListName] });
+ await changeCodeListId(user, codeListName, invalidCodeListName);
+ const errorMessage = screen.getByText(textMock('validation_errors.file_name_occupied'));
+ expect(errorMessage).toBeInTheDocument();
+ });
+
+ it('does not call onUpdateCodeListId when assigning an invalid id to the code list', async () => {
+ const user = userEvent.setup();
+ const invalidCodeListName = 'invalidCodeListName';
+ renderCodeLists({ codeListNames: [invalidCodeListName] });
+ await changeCodeListId(user, codeListName, invalidCodeListName);
+ expect(onUpdateCodeListIdMock).not.toHaveBeenCalled();
+ });
});
-const openCodeList = async (user: UserEvent) => {
- const codeListAccordion = screen.getByRole('button', { name: codeListName });
- await user.click(codeListAccordion);
+const changeCodeListId = async (user: UserEvent, oldCodeListId: string, newCodeListId: string) => {
+ const codeListIdToggleTextfield = screen.getByTitle(
+ textMock('app_content_library.code_lists.code_list_view_id_title', {
+ codeListName: oldCodeListId,
+ }),
+ );
+ await user.click(codeListIdToggleTextfield);
+ const codeListIdInput = screen.getByTitle(
+ textMock('app_content_library.code_lists.code_list_edit_id_title', {
+ codeListName: oldCodeListId,
+ }),
+ );
+ await user.clear(codeListIdInput);
+ await user.type(codeListIdInput, newCodeListId);
+ await user.tab();
};
const defaultProps: CodeListsProps = {
codeLists: [codeListWithMetadataMock],
+ onUpdateCodeListId: onUpdateCodeListIdMock,
onUpdateCodeList: onUpdateCodeListMock,
+ codeListInEditMode: undefined,
+ codeListNames: [],
};
const renderCodeLists = (props: Partial = {}): RenderResult => {
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx
index 9eff18b4932..f49e7b55ef7 100644
--- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx
@@ -1,43 +1,69 @@
import React from 'react';
import type { CodeListWithMetadata } from '../CodeListPage';
import { Accordion } from '@digdir/designsystemet-react';
-import { StudioCodeListEditor } from '@studio/components';
-import type { CodeList as StudioComponentsCodeList, CodeListEditorTexts } from '@studio/components';
-import { useOptionListEditorTexts } from '../hooks/useCodeListEditorTexts';
+import type { CodeList as StudioComponentsCodeList } from '@studio/components';
+import { EditCodeList } from './EditCodeList/EditCodeList';
+import { useTranslation } from 'react-i18next';
export type CodeListsProps = {
codeLists: CodeListWithMetadata[];
+ onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
+ codeListInEditMode: string | undefined;
+ codeListNames: string[];
};
-export function CodeLists({ codeLists, onUpdateCodeList }: CodeListsProps) {
+export function CodeLists({
+ codeLists,
+ onUpdateCodeListId,
+ onUpdateCodeList,
+ codeListInEditMode,
+ codeListNames,
+}: CodeListsProps) {
return codeLists.map((codeList) => (
-
+
));
}
type CodeListProps = {
codeList: CodeListWithMetadata;
+ onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
+ codeListInEditMode: string | undefined;
+ codeListNames: string[];
};
-function CodeList({ codeList, onUpdateCodeList }: CodeListProps) {
- const editorTexts: CodeListEditorTexts = useOptionListEditorTexts();
-
- const handleBlurAny = (updatedCodeList: StudioComponentsCodeList): void => {
- const updatedCodeListWithMetadata = updateCodeListWithMetadata(codeList, updatedCodeList);
- onUpdateCodeList(updatedCodeListWithMetadata);
- };
+function CodeList({
+ codeList,
+ onUpdateCodeListId,
+ onUpdateCodeList,
+ codeListInEditMode,
+ codeListNames,
+}: CodeListProps) {
+ const { t } = useTranslation();
return (
-
-
+
+
{codeList.title}
-
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.module.css b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.module.css
new file mode 100644
index 00000000000..91cf92bab9c
--- /dev/null
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.module.css
@@ -0,0 +1,5 @@
+.editCodeList {
+ display: flex;
+ flex-direction: column;
+ gap: var(--fds-spacing-2);
+}
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx
new file mode 100644
index 00000000000..67b4b19c6b0
--- /dev/null
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx
@@ -0,0 +1,74 @@
+import type { CodeList, CodeListEditorTexts } from '@studio/components';
+import { StudioCodeListEditor, StudioToggleableTextfield } from '@studio/components';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import type { CodeListWithMetadata } from '../../CodeListPage';
+import { useOptionListEditorTexts } from '../../hooks/useCodeListEditorTexts';
+import { KeyVerticalIcon } from '@studio/icons';
+import { updateCodeListWithMetadata } from '../CodeLists';
+import { FileNameUtils } from '@studio/pure-functions';
+import { useInputCodeListNameErrorMessage } from '../../hooks/useInputCodeListNameErrorMessage';
+import classes from './EditCodeList.module.css';
+
+export type EditCodeListProps = {
+ codeList: CodeListWithMetadata;
+ onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void;
+ onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
+ codeListNames: string[];
+};
+
+export function EditCodeList({
+ codeList,
+ onUpdateCodeListId,
+ onUpdateCodeList,
+ codeListNames,
+}: EditCodeListProps): React.ReactElement {
+ const { t } = useTranslation();
+ const editorTexts: CodeListEditorTexts = useOptionListEditorTexts();
+ const getInvalidInputFileNameErrorMessage = useInputCodeListNameErrorMessage();
+
+ const handleUpdateCodeListId = (newCodeListId: string) => {
+ if (newCodeListId !== codeList.title) onUpdateCodeListId(codeList.title, newCodeListId);
+ };
+
+ const handleBlurAny = (updatedCodeList: CodeList): void => {
+ const updatedCodeListWithMetadata = updateCodeListWithMetadata(codeList, updatedCodeList);
+ onUpdateCodeList(updatedCodeListWithMetadata);
+ };
+
+ const handleValidateCodeListId = (newCodeListId: string) => {
+ const fileNameError = FileNameUtils.findFileNameError(newCodeListId, codeListNames);
+ return getInvalidInputFileNameErrorMessage(fileNameError);
+ };
+
+ return (
+
+ ,
+ title: t('app_content_library.code_lists.code_list_edit_id_title', {
+ codeListName: codeList.title,
+ }),
+ value: codeList.title,
+ onBlur: (event) => handleUpdateCodeListId(event.target.value),
+ size: 'small',
+ }}
+ viewProps={{
+ label: t('app_content_library.code_lists.code_list_edit_id_label'),
+ children: codeList.title,
+ variant: 'tertiary',
+ title: t('app_content_library.code_lists.code_list_view_id_title', {
+ codeListName: codeList.title,
+ }),
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx
index 6cc658ea431..b761f36de0f 100644
--- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx
+++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx
@@ -24,7 +24,10 @@ export function CodeListsActionsBar({
const getInvalidUploadFileNameErrorMessage = useUploadCodeListNameErrorMessage();
const onSubmit = (file: File) => {
- const fileNameError = getFileNameError(file.name, codeListNames);
+ const fileNameError = FileNameUtils.findFileNameError(
+ FileNameUtils.removeExtension(file.name),
+ codeListNames,
+ );
if (fileNameError) {
return toast.error(getInvalidUploadFileNameErrorMessage(fileNameError));
}
@@ -49,6 +52,3 @@ export function CodeListsActionsBar({
);
}
-
-const getFileNameError = (fileName: string, invalidFileNames: string[]) =>
- FileNameUtils.findFileNameError(FileNameUtils.removeExtension(fileName), invalidFileNames);
diff --git a/frontend/libs/studio-content-library/src/config/ContentResourceLibraryImpl.test.tsx b/frontend/libs/studio-content-library/src/config/ContentResourceLibraryImpl.test.tsx
index 180209adb4b..1adfeaabbdd 100644
--- a/frontend/libs/studio-content-library/src/config/ContentResourceLibraryImpl.test.tsx
+++ b/frontend/libs/studio-content-library/src/config/ContentResourceLibraryImpl.test.tsx
@@ -3,24 +3,13 @@ import type { PagesConfig } from '../types/PagesProps';
import { ResourceContentLibraryImpl } from './ContentResourceLibraryImpl';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { renderWithProviders } from '../../test-utils/renderWithProviders';
+import { mockPagesConfig } from '../../mocks/mockPagesConfig';
describe('ContentResourceLibraryImpl', () => {
it('renders ContentResourceLibraryImpl with given pages', () => {
const pagesConfig: PagesConfig = {
- codeList: {
- props: {
- codeLists: [],
- onUpdateCodeList: () => {},
- onUploadCodeList: () => {},
- fetchDataError: false,
- },
- },
- images: {
- props: {
- images: [],
- onUpdateImage: () => {},
- },
- },
+ codeList: mockPagesConfig.codeList,
+ images: mockPagesConfig.images,
};
renderContentResourceLibraryImpl(pagesConfig);
const libraryTitle = screen.getByRole('heading', {
diff --git a/frontend/libs/studio-content-library/src/utils/router/RouterRouteMapper.test.ts b/frontend/libs/studio-content-library/src/utils/router/RouterRouteMapper.test.ts
index 439f99b18da..9e98716b65c 100644
--- a/frontend/libs/studio-content-library/src/utils/router/RouterRouteMapper.test.ts
+++ b/frontend/libs/studio-content-library/src/utils/router/RouterRouteMapper.test.ts
@@ -23,14 +23,7 @@ describe('RouterRouteMapperImpl', () => {
it('should include configured routes only', () => {
const routerMapper = new RouterRouteMapperImpl({
- codeList: {
- props: {
- codeLists: [],
- onUpdateCodeList: () => {},
- onUploadCodeList: () => {},
- fetchDataError: false,
- },
- },
+ codeList: mockPagesConfig.codeList,
});
const routes = routerMapper.configuredRoutes;
expect(routes.has('codeList')).toBeTruthy();
diff --git a/frontend/packages/schema-editor/src/components/SchemaInspector/ItemFieldsTab/ItemFieldsTable/ItemFieldsTableRow.tsx b/frontend/packages/schema-editor/src/components/SchemaInspector/ItemFieldsTab/ItemFieldsTable/ItemFieldsTableRow.tsx
index c3261451f89..3edf0ac9ff0 100644
--- a/frontend/packages/schema-editor/src/components/SchemaInspector/ItemFieldsTab/ItemFieldsTable/ItemFieldsTableRow.tsx
+++ b/frontend/packages/schema-editor/src/components/SchemaInspector/ItemFieldsTab/ItemFieldsTable/ItemFieldsTableRow.tsx
@@ -9,7 +9,7 @@ import { Switch } from '@digdir/designsystemet-react';
import { AltinnConfirmDialog } from 'app-shared/components';
import { useTranslation } from 'react-i18next';
import { TrashIcon } from '@studio/icons';
-import { StudioButton, StudioCenter } from '@studio/components';
+import { StudioCenter } from '@studio/components';
import { nameFieldClass } from '@altinn/schema-editor/components/SchemaInspector/ItemFieldsTab/domUtils';
import { ItemFieldType } from './ItemFieldType';
@@ -96,15 +96,13 @@ export const ItemFieldsTableRow = ({
confirmText={t('schema_editor.data_model_field_deletion_confirm')}
onConfirm={deleteHandler}
onClose={() => setIsConfirmDeleteDialogOpen(false)}
- trigger={
- }
- onClick={() => setIsConfirmDeleteDialogOpen((prevState) => !prevState)}
- color='danger'
- variant='tertiary'
- />
- }
+ triggerProps={{
+ title: t('schema_editor.delete_field'),
+ icon: ,
+ onClick: () => setIsConfirmDeleteDialogOpen((prevState) => !prevState),
+ color: 'danger',
+ variant: 'tertiary',
+ }}
>
{t('schema_editor.data_model_field_deletion_text')}
{t('schema_editor.data_model_field_deletion_info')}
diff --git a/frontend/packages/shared/src/components/AltinnConfirmDialog.tsx b/frontend/packages/shared/src/components/AltinnConfirmDialog.tsx
index f345831d279..d82c7dede0a 100644
--- a/frontend/packages/shared/src/components/AltinnConfirmDialog.tsx
+++ b/frontend/packages/shared/src/components/AltinnConfirmDialog.tsx
@@ -3,7 +3,12 @@ import classes from './AltinnConfirmDialog.module.css';
import { useTranslation } from 'react-i18next';
import cn from 'classnames';
import { StudioButton, StudioPopover } from '@studio/components';
-import type { StudioButtonProps, StudioPopoverProps } from '@studio/components';
+import type {
+ StudioButtonProps,
+ StudioPopoverProps,
+ StudioPopoverTriggerProps,
+} from '@studio/components';
+import type { WithDataAttributes } from 'app-shared/types/WithDataAttributes';
export type AltinnConfirmDialogProps = {
confirmText?: string;
@@ -11,7 +16,7 @@ export type AltinnConfirmDialogProps = {
cancelText?: string;
onConfirm: (event: React.MouseEvent) => void;
onClose: (event: React.MouseEvent | MouseEvent) => void;
- trigger?: React.ReactNode;
+ triggerProps?: WithDataAttributes;
className?: string;
} & Partial>;
@@ -23,7 +28,7 @@ export function AltinnConfirmDialog({
onClose,
placement,
children,
- trigger = ,
+ triggerProps,
open = false,
className,
}: AltinnConfirmDialogProps) {
@@ -48,7 +53,7 @@ export function AltinnConfirmDialog({
return (
- {trigger}
+
{children}
diff --git a/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx b/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx
index acf6a0d02f7..ed98bcdf9be 100644
--- a/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx
+++ b/frontend/packages/shared/src/components/GiteaHeader/ThreeDotsMenu/ThreeDotsMenu.tsx
@@ -21,14 +21,12 @@ export const ThreeDotsMenu = ({ isClonePossible = false }: ThreeDotsMenuProps) =
return (
-
- }
- title={t('sync_header.gitea_menu')}
- color='light'
- variant='regular'
- />
-
+ }
+ title={t('sync_header.gitea_menu')}
+ color='light'
+ variant='regular'
+ />
{isClonePossible && (
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 884dfbb81a3..f0f86db4317 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
@@ -57,19 +57,17 @@ export const FetchChangesPopover = (): React.ReactElement => {
return (
-
- }
- color='light'
- variant='regular'
- aria-label={t('sync_header.fetch_changes')}
- >
- {shouldDisplayText && t('sync_header.fetch_changes')}
- {displayNotification && }
-
-
+ }
+ color='light'
+ variant='regular'
+ aria-label={t('sync_header.fetch_changes')}
+ >
+ {shouldDisplayText && t('sync_header.fetch_changes')}
+ {displayNotification && }
+
{isLoading && }
{!isLoading && }
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 2729ca61fdc..274d9da0242 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
@@ -60,20 +60,18 @@ export const ShareChangesPopover = () => {
return (
-
- }
- color='light'
- variant='regular'
- aria-label={t('sync_header.changes_to_share')}
- >
- {shouldDisplayText && t('sync_header.changes_to_share')}
- {displayNotification && }
-
-
+ }
+ color='light'
+ variant='regular'
+ aria-label={t('sync_header.changes_to_share')}
+ >
+ {shouldDisplayText && t('sync_header.changes_to_share')}
+ {displayNotification && }
+
diff --git a/frontend/packages/shared/src/components/PreviewLimitationsInfo/PreviewLimitationsInfo.tsx b/frontend/packages/shared/src/components/PreviewLimitationsInfo/PreviewLimitationsInfo.tsx
index cd7c25c56cb..1c4ce46e9f5 100644
--- a/frontend/packages/shared/src/components/PreviewLimitationsInfo/PreviewLimitationsInfo.tsx
+++ b/frontend/packages/shared/src/components/PreviewLimitationsInfo/PreviewLimitationsInfo.tsx
@@ -34,13 +34,11 @@ export const PreviewLimitationsInfo = () => {
{t('preview.limitations_info')}
-
- setOpenShowSaveChoiceInSession(!openSaveChoiceInSession)}
- variant='tertiary'
- icon={}
- />
-
+ setOpenShowSaveChoiceInSession(!openSaveChoiceInSession)}
+ variant='tertiary'
+ icon={}
+ />
{t('session.reminder')}
= Props & DataAttributes;
+
+type DataAttributes = Record;
+
+type DataAttribute = `data-${string}`;
diff --git a/frontend/packages/text-editor/src/RightMenu.tsx b/frontend/packages/text-editor/src/RightMenu.tsx
index 233e023e0df..68ea9bbe296 100644
--- a/frontend/packages/text-editor/src/RightMenu.tsx
+++ b/frontend/packages/text-editor/src/RightMenu.tsx
@@ -8,7 +8,6 @@ import { defaultLangCode } from './constants';
import { useTranslation } from 'react-i18next';
import { AltinnConfirmDialog } from 'app-shared/components';
import { deleteButtonId } from '@studio/testing/testids';
-import { StudioButton } from '@studio/components';
import { ArrayUtils } from '@studio/pure-functions';
export interface RightMenuProps {
@@ -70,22 +69,18 @@ export const RightMenu = ({
confirmText={t('schema_editor.language_confirm_deletion')}
onConfirm={() => handleDeleteLanguage(langCode)}
onClose={() => setLangCodeToDelete(undefined)}
- trigger={
-
- setLangCodeToDelete((prevState) =>
- prevState === langCode ? undefined : langCode,
- )
- }
- disabled={!canDeleteLang(langCode)}
- aria-label={t('schema_editor.language_delete_button')}
- >
- {t('schema_editor.language_delete_button')}
-
- }
+ triggerProps={{
+ variant: canDeleteLang(langCode) ? 'primary' : 'secondary',
+ 'data-testid': deleteButtonId(langCode),
+ color: 'danger',
+ onClick: () =>
+ setLangCodeToDelete((prevState) =>
+ prevState === langCode ? undefined : langCode,
+ ),
+ disabled: !canDeleteLang(langCode),
+ 'aria-label': t('schema_editor.language_delete_button'),
+ children: t('schema_editor.language_delete_button'),
+ }}
>
{t('schema_editor.language_display_confirm_delete')}
diff --git a/frontend/packages/text-editor/src/TextRow.tsx b/frontend/packages/text-editor/src/TextRow.tsx
index 97130d399c2..ed5dcee9bcc 100644
--- a/frontend/packages/text-editor/src/TextRow.tsx
+++ b/frontend/packages/text-editor/src/TextRow.tsx
@@ -79,17 +79,14 @@ export const TextRow = ({
confirmText={t('schema_editor.textRow-deletion-confirm')}
onConfirm={handleDeleteClick}
onClose={() => setIsConfirmDeleteDialogOpen(false)}
- trigger={
- }
- variant='tertiary'
- onClick={() => setIsConfirmDeleteDialogOpen((prevState) => !prevState)}
- aria-label={t('schema_editor.delete')}
- >
- {t('schema_editor.delete')}
-
- }
+ triggerProps={{
+ className: classes.deleteButton,
+ icon: ,
+ variant: 'tertiary',
+ onClick: () => setIsConfirmDeleteDialogOpen((prevState) => !prevState),
+ 'aria-label': t('schema_editor.delete'),
+ children: t('schema_editor.delete'),
+ }}
>
{t('schema_editor.textRow-deletion-text')}
diff --git a/frontend/packages/ux-editor-v3/src/components/TextResource.tsx b/frontend/packages/ux-editor-v3/src/components/TextResource.tsx
index 05bb3ca70d8..e9a9cef9bc0 100644
--- a/frontend/packages/ux-editor-v3/src/components/TextResource.tsx
+++ b/frontend/packages/ux-editor-v3/src/components/TextResource.tsx
@@ -174,21 +174,18 @@ export const TextResource = ({
confirmText={t('ux_editor.text_resource_bindings.delete_confirm')}
onConfirm={handleDeleteButtonClick}
onClose={() => setIsConfirmDeleteDialogOpen(false)}
- trigger={
- }
- onClick={() => setIsConfirmDeleteDialogOpen(true)}
- title={t(getTextKeyForButton('delete', generateIdOptions?.textResourceKey))}
- variant='tertiary'
- />
- }
+ triggerProps={{
+ 'aria-label': t(getTextKeyForButton('delete', generateIdOptions?.textResourceKey)),
+ className: classes.button,
+ color: 'second',
+ disabled:
+ !handleRemoveTextResource ||
+ !(!!textResourceId || shouldDisplayFeature(FeatureFlag.ComponentConfigBeta)),
+ icon: ,
+ onClick: () => setIsConfirmDeleteDialogOpen(true),
+ title: t(getTextKeyForButton('delete', generateIdOptions?.textResourceKey)),
+ variant: 'tertiary',
+ }}
>
{t('ux_editor.text_resource_bindings.delete_confirm_question')}
diff --git a/frontend/packages/ux-editor-v3/src/containers/DesignView/PageAccordion/NavigationMenu/InputPopover/InputPopover.tsx b/frontend/packages/ux-editor-v3/src/containers/DesignView/PageAccordion/NavigationMenu/InputPopover/InputPopover.tsx
index 8307c48fc6e..98b74d730d4 100644
--- a/frontend/packages/ux-editor-v3/src/containers/DesignView/PageAccordion/NavigationMenu/InputPopover/InputPopover.tsx
+++ b/frontend/packages/ux-editor-v3/src/containers/DesignView/PageAccordion/NavigationMenu/InputPopover/InputPopover.tsx
@@ -41,6 +41,7 @@ export const InputPopover = ({
const [errorMessage, setErrorMessage] = useState
(null);
const [newName, setNewName] = useState(oldName);
+
const shouldSavingBeEnabled = errorMessage === null && newName !== oldName;
/**
@@ -68,18 +69,16 @@ export const InputPopover = ({
return (
-
- setIsEditDialogOpen(true)}
- id='edit-page-button'
- disabled={disabled}
- ref={newNameRef}
- aria-expanded={isEditDialogOpen}
- >
-
- {t('ux_editor.page_menu_edit')}
-
-
+ setIsEditDialogOpen(true)}
+ id='edit-page-button'
+ disabled={disabled}
+ ref={newNameRef}
+ aria-expanded={isEditDialogOpen}
+ >
+
+ {t('ux_editor.page_menu_edit')}
+
{
expect(dataModels).toEqual([defaultDataModel, secondDataModel]);
});
+ it('should return default data model when current data model is not provided', async () => {
+ const { result } = setupUseValidDataModelsHook('');
+
+ expect(result.current.isLoadingDataModels).toBe(true);
+
+ await waitFor(() => {
+ expect(result.current.isLoadingDataModels).toBe(false);
+ });
+
+ const { selectedDataModel, isDataModelValid } = result.current;
+ expect(isDataModelValid).toBe(true);
+ expect(selectedDataModel).toEqual(defaultDataModel);
+ });
+
it('should return the default data model from metadata when the current selected data model no longer exists', async () => {
const { result } = setupUseValidDataModelsHook('invalidModel');
diff --git a/frontend/packages/ux-editor/src/hooks/useValidDataModels.ts b/frontend/packages/ux-editor/src/hooks/useValidDataModels.ts
index 8e2b05c6665..5d76888350f 100644
--- a/frontend/packages/ux-editor/src/hooks/useValidDataModels.ts
+++ b/frontend/packages/ux-editor/src/hooks/useValidDataModels.ts
@@ -3,10 +3,12 @@ import { useDataModelMetadataQuery } from './queries/useDataModelMetadataQuery';
import { useAppContext } from './useAppContext';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { getDataModel, validateSelectedDataModel } from '../utils/dataModelUtils';
+import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery';
export const useValidDataModels = (currentDataModel: string) => {
const { selectedFormLayoutSetName } = useAppContext();
const { org, app } = useStudioEnvironmentParams();
+ const { data: layoutSets } = useLayoutSetsQuery(org, app);
const {
data: dataModels,
@@ -14,15 +16,19 @@ export const useValidDataModels = (currentDataModel: string) => {
isRefetching: isFetchingDataModels,
} = useAppMetadataModelIdsQuery(org, app, false);
- const isDataModelValid = validateSelectedDataModel(currentDataModel, dataModels);
+ const dataModel = Boolean(currentDataModel)
+ ? currentDataModel
+ : (layoutSets?.sets.find((layoutSet) => layoutSet.id === selectedFormLayoutSetName)?.dataType ??
+ dataModels?.[0]);
+ const isDataModelValid = validateSelectedDataModel(dataModel, dataModels);
const { data: dataModelMetadata, isPending: isPendingDataModelMetadata } =
useDataModelMetadataQuery(
{
org,
app,
layoutSetName: selectedFormLayoutSetName,
- dataModelName: isDataModelValid && currentDataModel ? currentDataModel : dataModels?.[0],
+ dataModelName: isDataModelValid ? dataModel : dataModels?.[0],
},
{ enabled: !isPendingDataModels && !isFetchingDataModels },
);