diff --git a/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.test.tsx b/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.test.tsx new file mode 100644 index 0000000000..fddfc82127 --- /dev/null +++ b/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.test.tsx @@ -0,0 +1,79 @@ +/** + * This source file is available under the terms of the + * Pimcore Open Core License (POCL) + * Full copyright and license information is available in + * LICENSE.md which is distributed with this source code. + * + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) + */ + +import React from 'react' +import { render } from '@testing-library/react' +import { useAlertModal } from './use-alert-modal' + +const modal = { + info: jest.fn(), + error: jest.fn(), + warning: jest.fn(), + success: jest.fn() +} + +jest.mock('@sdk/components', () => ({ + useStudioModal: () => ({ modal }) +})) + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => `t:${key}` }) +})) + +jest.mock('@Pimcore/components/icon/icon', () => ({ + Icon: () => null +})) + +// Opens one alert through the hook and hands back the config antd was called with. +const openAlert = (open: (alert: ReturnType) => void, method: keyof typeof modal): Record => { + const Harness = (): null => { + open(useAlertModal()) + + return null + } + + render() + + return modal[method].mock.calls.at(-1)![0] +} + +describe('useAlertModal', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('translates a title it is given', () => { + const config = openAlert((alert) => { alert.error({ content: 'boom', title: 'custom.title' }) }, 'error') + + expect(config.title).toBe('t:custom.title') + }) + + /** + * The error handler reports a plain error as `{ title: null }`. antd renders its title slot for + * every value that is not undefined or null, so passing the translation of `null` through would + * leave an empty heading above the message and the icon aligned to it rather than to the text. + */ + it('falls back to the default heading when the title is null', () => { + const config = openAlert((alert) => { alert.error({ content: 'boom', title: null }) }, 'error') + + expect(config.title).toBe('t:error') + }) + + it.each([ + ['info', 't:info'], + ['warn', 't:warning'], + ['success', 't:success'] + ] as const)('falls back to the default heading for %s', (kind, expected) => { + const method = kind === 'warn' ? 'warning' : kind + const config = openAlert((alert) => { alert[kind]({ content: 'hello', title: null }) }, method) + + expect(config.title).toBe(expected) + }) +}) diff --git a/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.tsx b/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.tsx index 1360b544d0..0e02176969 100644 --- a/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.tsx +++ b/assets/js/src/core/components/modal/alert-modal/hooks/use-alert-modal.tsx @@ -11,7 +11,7 @@ import { type ModalFuncProps } from 'antd' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { isUndefined } from 'lodash' +import { isNil } from 'lodash' import { useStudioModal } from '@sdk/components' import { Icon } from '@Pimcore/components/icon/icon' @@ -19,7 +19,12 @@ type ConfigUpdate = ModalFuncProps | ((prevConfig: ModalFuncProps) => ModalFuncP interface IAlertModalProps extends Omit { content: string | React.ReactNode - title?: string + /** + * Omit it (or pass null) to get the default heading for the kind of alert. Anything defined is + * handed to antd as the title, and antd renders the title slot for any non-null value - an empty + * one leaves the icon floating above a text block it no longer lines up with. + */ + title?: string | null } export interface UseAlertModalResponse { @@ -37,7 +42,7 @@ export const useAlertModal = (): UseAlertModalResponse => { () => ({ info: ({ title, content, icon, ...rest }) => ( modal.info({ - title: !isUndefined(title) ? t(title) : t('info'), + title: !isNil(title) ? t(title) : t('info'), content, icon: icon ?? , okText: t('alert-modal.ok-text'), @@ -46,7 +51,7 @@ export const useAlertModal = (): UseAlertModalResponse => { ), error: ({ title, content, icon, ...rest }) => ( modal.error({ - title: !isUndefined(title) ? t(title) : t('error'), + title: !isNil(title) ? t(title) : t('error'), content, icon: icon ?? , okText: t('alert-modal.ok-text'), @@ -55,7 +60,7 @@ export const useAlertModal = (): UseAlertModalResponse => { ), warn: ({ title, content, icon, ...rest }) => ( modal.warning({ - title: !isUndefined(title) ? t(title) : t('warning'), + title: !isNil(title) ? t(title) : t('warning'), content, icon: icon ?? , okText: t('alert-modal.ok-text'), @@ -64,7 +69,7 @@ export const useAlertModal = (): UseAlertModalResponse => { ), success: ({ title, content, icon, ...rest }) => ( modal.success({ - title: !isUndefined(title) ? t(title) : t('success'), + title: !isNil(title) ? t(title) : t('success'), content, icon: icon ?? , okText: t('alert-modal.ok-text'), diff --git a/build-dist/build-d5f8093b0a6a.zip b/build-dist/build-1413e08fa4d3.zip similarity index 88% rename from build-dist/build-d5f8093b0a6a.zip rename to build-dist/build-1413e08fa4d3.zip index 37e140b6b9..afd10a332b 100644 Binary files a/build-dist/build-d5f8093b0a6a.zip and b/build-dist/build-1413e08fa4d3.zip differ