Fix/统一修复modal样式失效 (#122)
Project CI / Repository checks (push) Successful in 48s
Project CI / Frontend tests (push) Successful in 3m21s
Project CI / Backend tests (push) Successful in 3m53s
Project CI / Native shell tests (push) Successful in 11m16s

之前遇到几次弹窗的样式不生效,bug 现在在画布agent的删除对话弹窗又出现了,
这个pr把类似的修改集中到最下层的UnifiedModal,适用于所有的modal,
一些特殊的modal有特殊处理.

画布agent的删除对话弹窗
![shotmd-1785399342.jpg](/attachments/d63d7dc6-bf7e-4f13-bfe8-bc4716cd798f)

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/122
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #122.
This commit is contained in:
2026-08-03 17:55:47 +08:00
committed by 段舒康
parent 3c58e76a71
commit e6b3199490
35 changed files with 486 additions and 169 deletions
@@ -57,9 +57,10 @@ export function PlatformAuthModalShell({
closeOnEscape={false}
size={size}
showHeader={showHeader}
portalTheme={platformTheme}
zIndexClassName={zIndexClassName}
overlayClassName={joinClassNames(
`platform-theme platform-theme--${platformTheme} text-[var(--platform-text-strong)]`,
'text-[var(--platform-text-strong)]',
overlaySpacing === 'default' && '!px-3 !py-4 sm:!p-4',
overlayClassName,
)}
@@ -289,6 +289,7 @@ test('creative image input panel confirms before removing uploaded image', () =>
fireEvent.click(screen.getByRole('button', { name: '移除拼图图片' }));
const dialog = screen.getByRole('dialog', { name: '移除拼图图片?' });
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(within(dialog).getByText('移除后需要重新上传图片。')).toBeTruthy();
fireEvent.click(within(dialog).getByRole('button', { name: '移除' }));
expect(onMainImageRemove).toHaveBeenCalledTimes(1);
+1 -3
View File
@@ -88,9 +88,7 @@ export function LegalDocumentModal({
size="md"
closeLabel="关闭法律信息"
zIndexClassName={zIndexClassName ?? 'z-[150]'}
overlayClassName={`platform-theme ${
platformTheme ? `platform-theme--${platformTheme}` : ''
}`}
portalTheme={platformTheme ?? 'auto'}
panelClassName="platform-remap-surface rounded-t-[1.4rem] sm:rounded-[1.4rem]"
headerClassName="items-center"
bodyClassName="px-4 py-0 sm:px-5"
@@ -24,6 +24,10 @@ test('renders a standard danger confirmation with cancel and confirm actions', (
const dialog = screen.getByRole('dialog', { name: '删除作品' });
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(dialog.parentElement?.className).toContain('z-[140]');
expect(dialog.className).toContain('platform-remap-surface');
expect(dialog.className).toContain('shadow-[0_24px_80px_rgba(0,0,0,0.55)]');
expect(within(dialog).getByText('确认删除《潮雾列岛》吗?')).toBeTruthy();
expect(within(dialog).getByText('删除后不可恢复。')).toBeTruthy();
@@ -21,8 +21,13 @@ type PlatformDangerConfirmDialogProps = {
panelClassName?: string;
footerClassName?: string;
confirmClassName?: string;
zIndexClassName?: string;
};
function joinClassNames(...classNames: Array<string | undefined>) {
return classNames.filter(Boolean).join(' ');
}
/**
* 平台危险确认弹窗。
* 统一承接需要“确认 / 取消 + 危险主动作”语义的标准弹窗壳层。
@@ -46,6 +51,7 @@ export function PlatformDangerConfirmDialog({
panelClassName,
footerClassName,
confirmClassName,
zIndexClassName = 'z-[140]',
}: PlatformDangerConfirmDialogProps) {
return (
<UnifiedConfirmDialog
@@ -65,9 +71,13 @@ export function PlatformDangerConfirmDialog({
portal={portal}
size={size}
overlayClassName={overlayClassName}
panelClassName={panelClassName}
panelClassName={joinClassNames(
'platform-remap-surface shadow-[0_24px_80px_rgba(0,0,0,0.55)]',
panelClassName,
)}
footerClassName={footerClassName}
confirmClassName={confirmClassName}
zIndexClassName={zIndexClassName}
>
{children}
</UnifiedConfirmDialog>
@@ -103,6 +103,7 @@ test('renders full-screen image preview with zoom controls and dark backdrop', (
expect(
dialog.parentElement?.className.includes('!bg-black'),
).toBe(true);
expect(dialog.parentElement?.className).not.toContain('platform-theme--');
expect(screen.getByRole('button', { name: '放大图片' })).toBeTruthy();
expect(screen.getByRole('button', { name: '缩小图片' })).toBeTruthy();
expect(screen.getByRole('button', { name: '重置图片缩放' })).toBeTruthy();
@@ -243,6 +243,8 @@ export function PlatformImagePreviewModal({
showHeader={false}
showCloseButton={false}
size="fullscreen"
// 全黑自绘查看器不需要平台变量;避免 light / dark remap 改写黑底、白字和暗色控件。
portalTheme="none"
zIndexClassName={zIndexClassName}
overlayClassName="!items-stretch !justify-stretch !bg-black !p-0 !backdrop-blur-none"
panelClassName="platform-image-preview-modal !h-[100dvh] !max-h-none !max-w-none !rounded-none border-0 bg-black text-white shadow-none"
@@ -10,6 +10,10 @@ vi.mock('../../services/clipboard', () => ({
copyTextToClipboard: vi.fn(),
}));
vi.mock('../auth/AuthUiContext', () => ({
useAuthUi: () => ({ platformTheme: 'dark' }),
}));
afterEach(() => {
vi.clearAllMocks();
});
@@ -31,6 +35,8 @@ test('renders report fields and copies the joined report lines', async () => {
);
const dialog = screen.getByRole('dialog', { name: '统一报告' });
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(dialog.parentElement?.className).not.toContain('platform-theme--dark');
expect(within(dialog).getByText('拼图草稿 puzzle-session-1')).toBeTruthy();
expect(within(dialog).getByText('已完成')).toBeTruthy();
@@ -54,6 +54,8 @@ export function PlatformReportDialog({
open={open}
title={title}
onClose={onClose}
// 报告中的二维码 / 扫码区固定使用浅色背景,保证对比度和识别率。
platformTheme="light"
overlayClassName={overlayClassName}
panelClassName={panelClassName}
bodyClassName="space-y-3"
@@ -1,6 +1,5 @@
import type { ReactNode } from 'react';
import { useAuthUi } from '../auth/AuthUiContext';
import { UnifiedModal } from './UnifiedModal';
type PlatformToolModalShellProps = {
@@ -50,9 +49,6 @@ export function PlatformToolModalShell({
bodyClassName,
footerClassName,
}: PlatformToolModalShellProps) {
const resolvedPlatformTheme =
useAuthUi()?.platformTheme ?? 'light';
return (
<UnifiedModal
open={open}
@@ -68,7 +64,6 @@ export function PlatformToolModalShell({
closeOnBackdrop={closeOnBackdrop}
closeOnEscape={closeOnEscape}
zIndexClassName={zIndexClassName}
overlayClassName={`platform-theme platform-theme--${resolvedPlatformTheme}`}
panelClassName={joinClassNames(
'platform-remap-surface shadow-[0_24px_80px_rgba(0,0,0,0.55)]',
panelClassName,
@@ -1,10 +1,14 @@
/* @vitest-environment jsdom */
import { render, screen, within } from '@testing-library/react';
import { expect, test } from 'vitest';
import { expect, test, vi } from 'vitest';
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
vi.mock('../auth/AuthUiContext', () => ({
useAuthUi: () => ({ platformTheme: 'dark' }),
}));
test('renders platform utility info modal shell with default platform styling', () => {
render(
<PlatformUtilityInfoModal
@@ -19,7 +23,7 @@ test('renders platform utility info modal shell with default platform styling',
);
const dialog = screen.getByRole('dialog', { name: '工具信息' });
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(dialog.parentElement?.className).toContain('platform-theme--dark');
expect(dialog.parentElement?.className).toContain('!items-center');
expect(dialog.className).toContain('platform-remap-surface');
expect(dialog.className).toContain('rounded-[1.5rem]');
@@ -29,7 +29,7 @@ export function PlatformUtilityInfoModal({
onClose,
children,
footer,
platformTheme = 'light',
platformTheme,
overlayClassName,
panelClassName,
bodyClassName,
@@ -41,11 +41,8 @@ export function PlatformUtilityInfoModal({
title={title}
onClose={onClose}
size="sm"
overlayClassName={joinClassNames(
`platform-theme platform-theme--${platformTheme}`,
'!items-center',
overlayClassName,
)}
portalTheme={platformTheme}
overlayClassName={joinClassNames('!items-center', overlayClassName)}
panelClassName={joinClassNames('platform-remap-surface', panelClassName)}
bodyClassName={joinClassNames(
'space-y-4 px-4 py-4 sm:px-5 sm:py-5',
@@ -29,6 +29,10 @@ vi.mock('../../services/clipboard', () => ({
copyTextToClipboard: vi.fn(),
}));
vi.mock('../auth/AuthUiContext', () => ({
useAuthUi: () => ({ platformTheme: 'dark' }),
}));
const payload: PublishShareModalPayload = {
title: '暖灯猫街',
publicWorkCode: 'PZ-00000001',
@@ -119,7 +123,7 @@ describe('PublishShareModal', () => {
const dialog = screen.getByRole('dialog', { name: '分享给朋友' });
expect(dialog.parentElement?.className).toContain('!items-center');
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(dialog.parentElement?.className).toContain('platform-theme--dark');
expect(dialog.className).toContain('platform-modal-shell');
expect(dialog.className).toContain('rounded-[1.75rem]');
expect(dialog.getAttribute('style')).toBeNull();
@@ -10,7 +10,6 @@ import {
openHostShare,
openHostShareGrid,
} from '../../services/host-bridge/hostBridge';
import { useAuthUi } from '../auth/AuthUiContext';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
import { downloadPublishShareCardImage } from './publishShareCardImage';
@@ -77,7 +76,6 @@ export function PublishShareModal({
payload,
onClose,
}: PublishShareModalProps) {
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
const [copyState, setCopyState] = useState<ActionState>('idle');
const [downloadState, setDownloadState] = useState<ActionState>('idle');
const [gridState, setGridState] = useState<ActionState>('idle');
@@ -233,7 +231,6 @@ export function PublishShareModal({
open={open && Boolean(payload)}
title="分享给朋友"
onClose={onClose}
platformTheme={platformTheme}
panelClassName="rounded-[1.75rem]"
footerClassName="border-t-0 px-4 pb-5 pt-0 sm:px-5"
footer={
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import { PlatformActionButton } from './PlatformActionButton';
import { UnifiedModal } from './UnifiedModal';
import { UnifiedModal, type UnifiedModalPortalTheme } from './UnifiedModal';
type UnifiedConfirmDialogTone = 'primary' | 'danger';
@@ -24,6 +24,7 @@ type UnifiedConfirmDialogProps = {
closeOnBackdrop?: boolean;
showCloseButton?: boolean;
portal?: boolean;
portalTheme?: UnifiedModalPortalTheme;
size?: 'sm' | 'md';
overlayClassName?: string;
panelClassName?: string;
@@ -54,6 +55,7 @@ export function UnifiedConfirmDialog({
closeOnBackdrop = true,
showCloseButton = true,
portal = true,
portalTheme = 'auto',
size = 'sm',
overlayClassName,
panelClassName,
@@ -74,6 +76,7 @@ export function UnifiedConfirmDialog({
closeOnBackdrop={closeOnBackdrop && !busy}
showCloseButton={showCloseButton}
portal={portal}
portalTheme={portalTheme}
size={size}
overlayClassName={overlayClassName}
panelClassName={panelClassName}
+67 -4
View File
@@ -8,6 +8,8 @@ import {
} from 'react';
import { createPortal } from 'react-dom';
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformModalCloseButton } from './PlatformModalCloseButton';
type UnifiedModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
@@ -18,6 +20,8 @@ type UnifiedModalCloseIcon = ComponentProps<
typeof PlatformModalCloseButton
>['icon'];
export type UnifiedModalPortalTheme = PlatformTheme | 'auto' | 'none';
type UnifiedModalProps = {
open: boolean;
title: string;
@@ -37,6 +41,7 @@ type UnifiedModalProps = {
closeVariant?: UnifiedModalCloseVariant;
closeIcon?: UnifiedModalCloseIcon;
portal?: boolean;
portalTheme?: UnifiedModalPortalTheme;
zIndexClassName?: string;
overlayClassName?: string;
overlayStyle?: CSSProperties;
@@ -63,6 +68,44 @@ function joinClassNames(
return classNames.filter(Boolean).join(' ');
}
function resolvePortalOverlayClassName({
overlayClassName,
portalTheme,
contextualTheme,
}: {
overlayClassName?: string;
portalTheme: UnifiedModalPortalTheme;
contextualTheme: PlatformTheme;
}) {
if (portalTheme === 'none') {
return overlayClassName;
}
const classNames = overlayClassName?.split(/\s+/u).filter(Boolean) ?? [];
const explicitTheme = classNames.find(
(className): className is `platform-theme--${PlatformTheme}` =>
className === 'platform-theme--light' ||
className === 'platform-theme--dark',
);
const resolvedTheme =
portalTheme === 'auto'
? ((explicitTheme?.replace('platform-theme--', '') as
PlatformTheme | undefined) ?? contextualTheme)
: portalTheme;
const remainingClassNames = classNames.filter(
(className) =>
className !== 'platform-theme' &&
className !== 'platform-theme--light' &&
className !== 'platform-theme--dark',
);
return joinClassNames(
'platform-theme',
`platform-theme--${resolvedTheme}`,
...remainingClassNames,
);
}
function UnifiedModalContent({
open,
title,
@@ -91,7 +134,7 @@ function UnifiedModalContent({
bodyClassName,
footerClassName,
panelStyle,
}: Omit<UnifiedModalProps, 'portal'>) {
}: Omit<UnifiedModalProps, 'portal' | 'portalTheme'>) {
const generatedTitleId = useId();
const descriptionId = useId();
const titleId = titleIdProp ?? generatedTitleId;
@@ -231,10 +274,30 @@ function UnifiedModalContent({
* 统一模态窗口外壳。
* 业务组件只传入标题、内容和操作区;遮罩、无障碍属性、Escape 与移动端布局在这里收口。
*/
export function UnifiedModal({ portal = true, ...props }: UnifiedModalProps) {
export function UnifiedModal({
portal = true,
portalTheme = 'auto',
overlayClassName,
...props
}: UnifiedModalProps) {
const contextualTheme = useAuthUi()?.platformTheme ?? 'light';
const resolvedProps = {
...props,
overlayClassName: portal
? resolvePortalOverlayClassName({
overlayClassName,
portalTheme,
contextualTheme,
})
: overlayClassName,
};
if (!portal || typeof document === 'undefined') {
return <UnifiedModalContent {...props} />;
return <UnifiedModalContent {...resolvedProps} />;
}
return createPortal(<UnifiedModalContent {...props} />, document.body);
return createPortal(
<UnifiedModalContent {...resolvedProps} />,
document.body,
);
}
@@ -569,6 +569,10 @@ describe('CreationLandingView', () => {
);
const dialog = await screen.findByRole('dialog', { name: '角色英雄' });
expect(dialog.parentElement?.className).not.toContain('platform-theme--');
expect(dialog.parentElement?.className).toContain(
'creation-landing__showcase-modal-overlay',
);
expect(within(dialog).getByText('赞')).toBeTruthy();
expect(within(dialog).getByText('3')).toBeTruthy();
expect(within(dialog).queryByRole('button', { name: /点赞/ })).toBeNull();
@@ -409,6 +409,8 @@ function CreationShowcaseModal({
showHeader={false}
showCloseButton={false}
size="fullscreen"
// 素材舞台是独立黑底视觉;避免平台主题 remap 把它改成普通白底工具弹窗。
portalTheme="none"
zIndexClassName="z-[120]"
overlayClassName="creation-landing__showcase-modal-overlay"
panelClassName="creation-landing__showcase-modal-panel"
@@ -1174,7 +1176,8 @@ export function CreationLandingView({
showHeader={false}
showCloseButton={false}
size="sm"
overlayClassName="platform-theme platform-theme--light platform-mobile-home-welcome-overlay !items-center !p-4"
portalTheme="light"
overlayClassName="platform-mobile-home-welcome-overlay !items-center !p-4"
panelClassName="platform-remap-surface platform-mobile-home-welcome-dialog"
bodyClassName="platform-mobile-home-welcome-dialog__body"
footerClassName="platform-mobile-home-welcome-dialog__footer"
@@ -338,9 +338,7 @@ describe('EditorAgentConversationPanelView', () => {
expect(
screen.getByText('一二三四五六七八九十甲乙丙丁戊己庚辛壬癸子丑寅卯'),
).toBeTruthy();
expect(
screen.getByRole('option', { name: '角色参考' }),
).toBeTruthy();
expect(screen.getByRole('option', { name: '角色参考' })).toBeTruthy();
expect(screen.getByRole('option', { name: '新对话' })).toBeTruthy();
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
@@ -530,9 +528,9 @@ describe('EditorAgentConversationPanelView', () => {
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
).toBe('需要保留的草稿');
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
expect(
(screen.getByLabelText('当前对话') as HTMLSelectElement).value,
).toBe('conversation-1');
expect((screen.getByLabelText('当前对话') as HTMLSelectElement).value).toBe(
'conversation-1',
);
});
it('preserves newer draft edits while creating a conversation', async () => {
@@ -1989,6 +1987,14 @@ describe('EditorAgentConversationPanelView', () => {
fireEvent.click(screen.getByRole('button', { name: '删除当前对话' }));
const confirmDialog = screen.getByRole('dialog', { name: '删除对话' });
expect(confirmDialog.parentElement?.className).toContain(
'platform-theme--light',
);
expect(confirmDialog.parentElement?.className).toContain('z-[140]');
expect(confirmDialog.className).toContain('platform-remap-surface');
expect(
within(confirmDialog).getByText('确认删除这个对话吗?'),
).toBeTruthy();
fireEvent.click(
within(confirmDialog).getByRole('button', { name: '确认删除' }),
);
@@ -391,7 +391,9 @@ export function EditorAgentConversationPanelView({
setDeleteConfirmOpen(false),
);
}}
/>
>
</PlatformDangerConfirmDialog>
</>
);
}
@@ -118,9 +118,11 @@ describe('MessageBubble', () => {
clientX: 30,
clientY: 40,
});
const menu = screen.getByRole('menu', { name: '消息右键菜单' });
expect(menu.style.zIndex).toBe('60');
expect(
screen.getByRole('menu', { name: '消息右键菜单' }).style.zIndex,
).toBe('60');
menu.closest('.image-canvas-editor__portal-theme')?.className,
).toContain('platform-theme--light');
fireEvent.click(screen.getByRole('menuitem', { name: '复制文本' }));
await waitFor(() =>
@@ -1,8 +1,8 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import type { ImageCanvasActionResult } from '@/src/components/image-editor/ImageCanvasActionsContext.ts';
import { ImageCanvasEditorPortal } from '../ImageCanvasEditorPortal.tsx';
import {
contextAssetMediaSrc,
type EditorAgentContextAsset,
@@ -158,110 +158,115 @@ export function MessageBubbleRightClickMenu({
};
}, [onClose]);
if (typeof document === 'undefined') {
return null;
}
return createPortal(
<div
ref={menuRef}
className="image-canvas-editor__context-menu"
role="menu"
aria-label={target.kind === 'text' ? '消息右键菜单' : '消息素材右键菜单'}
style={{
left: position?.x ?? x,
top: position?.y ?? y,
zIndex: 60,
}}
onContextMenu={(event) => event.preventDefault()}
>
{target.kind === 'text' ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() => onAction(EditorAgentRightClickAction.CopyText)}
>
{actionLabel({
action: EditorAgentRightClickAction.CopyText,
idleLabel: '复制文本',
pendingAction,
resultAction,
result,
})}
</button>
) : (
<>
{target.asset.kind === 'generated_media' &&
target.asset.resourceId?.trim() ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() => onAction(EditorAgentRightClickAction.FocusCanvas)}
>
{actionLabel({
action: EditorAgentRightClickAction.FocusCanvas,
idleLabel: '在画布中定位',
pendingAction,
resultAction,
result,
})}
</button>
) : null}
{target.asset.mediaType === 'image' ? (
<>
{contextAssetMediaSrc(target.asset).trim() ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.ReferenceImage)
}
>
{actionLabel({
action: EditorAgentRightClickAction.ReferenceImage,
idleLabel: '引用',
pendingAction,
resultAction,
result,
})}
</button>
) : null}
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() => onAction(EditorAgentRightClickAction.CopyImage)}
>
{actionLabel({
action: EditorAgentRightClickAction.CopyImage,
idleLabel: '复制图片',
pendingAction,
resultAction,
result,
})}
</button>
</>
) : null}
return (
<ImageCanvasEditorPortal>
<div
ref={menuRef}
className="image-canvas-editor__context-menu"
role="menu"
aria-label={
target.kind === 'text' ? '消息右键菜单' : '消息素材右键菜单'
}
style={{
left: position?.x ?? x,
top: position?.y ?? y,
zIndex: 60,
}}
onContextMenu={(event) => event.preventDefault()}
>
{target.kind === 'text' ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() => onAction(EditorAgentRightClickAction.DownloadAsset)}
onClick={() => onAction(EditorAgentRightClickAction.CopyText)}
>
{actionLabel({
action: EditorAgentRightClickAction.DownloadAsset,
idleLabel: assetDownloadLabel(target.asset),
action: EditorAgentRightClickAction.CopyText,
idleLabel: '复制文本',
pendingAction,
resultAction,
result,
})}
</button>
</>
)}
</div>,
document.body,
) : (
<>
{target.asset.kind === 'generated_media' &&
target.asset.resourceId?.trim() ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.FocusCanvas)
}
>
{actionLabel({
action: EditorAgentRightClickAction.FocusCanvas,
idleLabel: '在画布中定位',
pendingAction,
resultAction,
result,
})}
</button>
) : null}
{target.asset.mediaType === 'image' ? (
<>
{contextAssetMediaSrc(target.asset).trim() ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.ReferenceImage)
}
>
{actionLabel({
action: EditorAgentRightClickAction.ReferenceImage,
idleLabel: '引用',
pendingAction,
resultAction,
result,
})}
</button>
) : null}
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.CopyImage)
}
>
{actionLabel({
action: EditorAgentRightClickAction.CopyImage,
idleLabel: '复制图片',
pendingAction,
resultAction,
result,
})}
</button>
</>
) : null}
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.DownloadAsset)
}
>
{actionLabel({
action: EditorAgentRightClickAction.DownloadAsset,
idleLabel: assetDownloadLabel(target.asset),
pendingAction,
resultAction,
result,
})}
</button>
</>
)}
</div>
</ImageCanvasEditorPortal>
);
}
@@ -1,12 +1,25 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { useState } from 'react';
import { type ComponentProps, type ReactNode, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { AuthUiContext } from '../auth/AuthUiContext';
import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView';
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
const DARK_AUTH_UI_VALUE = {
platformTheme: 'dark',
} as ComponentProps<typeof AuthUiContext.Provider>['value'];
function withDarkAuthUi(children: ReactNode) {
return (
<AuthUiContext.Provider value={DARK_AUTH_UI_VALUE}>
{children}
</AuthUiContext.Provider>
);
}
function createDialog(
patch: Partial<GenerateDialogState> = {},
): GenerateDialogState {
@@ -48,8 +61,17 @@ function EditGenerationModalHarness({
describe('ImageCanvasEditGenerationModalView', () => {
it('updates prompt and submits edit generation', () => {
const submitEdit = vi.fn();
render(<EditGenerationModalHarness onSubmit={submitEdit} />);
render(
withDarkAuthUi(<EditGenerationModalHarness onSubmit={submitEdit} />),
);
const modal = screen
.getAllByRole('dialog', { name: '修改图片' })
.find((element) => element.classList.contains('platform-modal-shell'));
expect(modal?.parentElement?.className).toContain('platform-theme--light');
expect(modal?.parentElement?.className).not.toContain(
'platform-theme--dark',
);
fireEvent.change(screen.getByLabelText('生成提示词'), {
target: { value: '新的修改提示' },
});
@@ -3,9 +3,7 @@ import { type Dispatch, type SetStateAction } from 'react';
import { UnifiedModal } from '../common/UnifiedModal';
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
import {
calculateEditorImageGenerationPrice,
} from './ImageCanvasGenerationModel';
import { calculateEditorImageGenerationPrice } from './ImageCanvasGenerationModel';
type ImageCanvasEditGenerationModalViewProps = {
dialog: GenerateDialogState | null;
@@ -27,11 +25,13 @@ export function ImageCanvasEditGenerationModalView({
imageSize: dialogImageSize,
});
// TODO: Remove this override after full dark style support.
return (
<UnifiedModal
open={isOpen}
title={dialog?.mode === 'edit' ? '修改图片' : '生成图片'}
size="sm"
portalTheme="light"
closeLabel={dialog?.mode === 'edit' ? '关闭修改图片' : '关闭生成图片'}
closeDisabled={dialog?.status === 'generating'}
onClose={() => setGenerateDialog(null)}
@@ -1,11 +1,25 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import type { ComponentProps, ReactNode } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { AuthUiContext } from '../auth/AuthUiContext';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView';
const DARK_AUTH_UI_VALUE = {
platformTheme: 'dark',
} as ComponentProps<typeof AuthUiContext.Provider>['value'];
function withDarkAuthUi(children: ReactNode) {
return (
<AuthUiContext.Provider value={DARK_AUTH_UI_VALUE}>
{children}
</AuthUiContext.Provider>
);
}
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
id: 'layer-1',
@@ -27,37 +41,45 @@ function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
describe('ImageCanvasMetadataModalView', () => {
it('renders generated layer metadata with generation inputs and references', () => {
render(
<ImageCanvasMetadataModalView
layer={createLayer({
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'gpt-image-2-task-123',
objectKey: 'generated/object.png',
generationInputs: {
fields: [{ title: '生成提示词', value: '清爽游戏按钮' }],
references: [
{
title: '参考图',
label: '角色立绘',
refType: 'project-resource',
refId: 'resource-reference',
},
],
},
})}
onClose={vi.fn()}
/>,
withDarkAuthUi(
<ImageCanvasMetadataModalView
layer={createLayer({
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'gpt-image-2-task-123',
objectKey: 'generated/object.png',
generationInputs: {
fields: [{ title: '生成提示词', value: '清爽游戏按钮' }],
references: [
{
title: '参考图',
label: '角色立绘',
refType: 'project-resource',
refId: 'resource-reference',
},
],
},
})}
onClose={vi.fn()}
/>,
),
);
const dialog = screen.getByRole('dialog', { name: '图片信息' });
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(dialog.parentElement?.className).not.toContain(
'platform-theme--dark',
);
expect(within(dialog).queryByText('生成主图')).toBeNull();
expect(within(dialog).getByText('生成图片')).toBeTruthy();
expect(within(dialog).getByText('生成提示词')).toBeTruthy();
expect(within(dialog).getByText('清爽游戏按钮')).toBeTruthy();
expect(within(dialog).getByText('参考图')).toBeTruthy();
expect(within(dialog).getByText('角色立绘')).toBeTruthy();
expect(within(dialog).getByText('项目资源 · resource-reference')).toBeTruthy();
expect(
within(dialog).getByText('项目资源 · resource-reference'),
).toBeTruthy();
expect(within(dialog).getByText('Model')).toBeTruthy();
expect(within(dialog).getByText('gpt-image-2')).toBeTruthy();
expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy();
@@ -157,7 +179,9 @@ describe('ImageCanvasMetadataModalView', () => {
expect(within(dialog).getByText('上传图片')).toBeTruthy();
expect(within(dialog).getAllByText('-').length).toBeGreaterThanOrEqual(3);
fireEvent.click(within(dialog).getByRole('button', { name: '关闭图片信息' }));
fireEvent.click(
within(dialog).getByRole('button', { name: '关闭图片信息' }),
);
expect(onClose).toHaveBeenCalledTimes(1);
});
@@ -193,7 +217,9 @@ describe('ImageCanvasMetadataModalView', () => {
expect(within(dialog).getByText('kling3.0-omni')).toBeTruthy();
expect(within(dialog).getByText('1280 x 720 px')).toBeTruthy();
fireEvent.click(within(dialog).getByRole('button', { name: '关闭视频信息' }));
fireEvent.click(
within(dialog).getByRole('button', { name: '关闭视频信息' }),
);
expect(onClose).toHaveBeenCalledTimes(1);
});

Some files were not shown because too many files have changed in this diff Show More