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) => (
+
), }), @@ -115,6 +120,23 @@ describe('AppContentLibrary', () => { codeListMock, ); }); + + it('calls onUpdateOptionListId when onUpdateCodeListId is triggered', async () => { + const user = userEvent.setup(); + renderAppContentLibrary(optionListsMock); + await goToLibraryPage(user, 'code_lists'); + const updateCodeListIdButton = screen.getByRole('button', { + name: updateCodeListIdButtonTextMock, + }); + await user.click(updateCodeListIdButton); + expect(queriesMock.updateOptionListId).toHaveBeenCalledTimes(1); + expect(queriesMock.updateOptionListId).toHaveBeenCalledWith( + org, + app, + codeListNameMock, + newCodeListNameMock, + ); + }); }); const getLibraryPageTile = (libraryPage: string) => diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx index 1db64ff4077..663237953f8 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx @@ -6,11 +6,15 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen import { convertOptionListsToCodeLists } from './utils/convertOptionListsToCodeLists'; import { StudioPageSpinner } from '@studio/components'; import { useTranslation } from 'react-i18next'; -import { useAddOptionListMutation, useUpdateOptionListMutation } from 'app-shared/hooks/mutations'; import type { ApiError } from 'app-shared/types/api/ApiError'; import { toast } from 'react-toastify'; import type { AxiosError } from 'axios'; import { isErrorUnknown } from 'app-shared/utils/ApiErrorUtils'; +import { + useAddOptionListMutation, + useUpdateOptionListMutation, + useUpdateOptionListIdMutation, +} from 'app-shared/hooks/mutations'; export function AppContentLibrary(): React.ReactElement { const { org, app } = useStudioEnvironmentParams(); @@ -24,12 +28,17 @@ export function AppContentLibrary(): React.ReactElement { hideDefaultError: (error: AxiosError) => 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 }, );