修复画板提示与失效项目跳转
画板参考图选择提示改为持续显示并支持手动关闭。 显式项目访问失效时同步切回项目页状态。 补充提示关闭和项目失效回退测试。
This commit is contained in:
@@ -110,6 +110,10 @@ const CANVAS_STARTUP_TOOLS: CanvasStartupTool[] = [
|
||||
'publication-cover',
|
||||
];
|
||||
|
||||
type ImageCanvasEditorViewProps = {
|
||||
onProjectAccessLost?: () => void;
|
||||
};
|
||||
|
||||
function isCanvasStartupTool(value: string | null): value is CanvasStartupTool {
|
||||
return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool);
|
||||
}
|
||||
@@ -278,7 +282,9 @@ function createAssetActionLayer(asset: EditorAsset): CanvasLayer {
|
||||
};
|
||||
}
|
||||
|
||||
export function ImageCanvasEditorView() {
|
||||
export function ImageCanvasEditorView({
|
||||
onProjectAccessLost,
|
||||
}: ImageCanvasEditorViewProps = {}) {
|
||||
const authUi = useAuthUi();
|
||||
const [, setGenerationPricingVersion] = useState(0);
|
||||
const [walletBalanceLabel, setWalletBalanceLabel] = useState<string | null>(
|
||||
@@ -996,6 +1002,7 @@ export function ImageCanvasEditorView() {
|
||||
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
||||
currentUserId: currentEditorUserId,
|
||||
openEditorLoginModal,
|
||||
onProjectAccessLost,
|
||||
});
|
||||
const applyGeneratedProjectSnapshot = useCallback(
|
||||
(project: EditorProjectSnapshot) => {
|
||||
|
||||
@@ -311,6 +311,8 @@ describe('useImageCanvasGenerationSurface', () => {
|
||||
expect(screen.getByRole('alert').textContent).toBe(
|
||||
'选择的图片不是角色规范图,请选择生成规范里的角色规范。',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭提示' }));
|
||||
expect(screen.queryByRole('alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('anchors the music menu to the music tool and closes after hover leaves', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import {
|
||||
PlatformFloatingMenu,
|
||||
@@ -481,7 +482,15 @@ export function useImageCanvasGenerationSurface({
|
||||
className="image-canvas-editor__reference-pick-toast"
|
||||
role="alert"
|
||||
>
|
||||
{generationWorkflow.referencePickWarning}
|
||||
<span>{generationWorkflow.referencePickWarning}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="image-canvas-editor__reference-pick-toast-close"
|
||||
aria-label="关闭提示"
|
||||
onClick={generationWorkflow.clearReferencePickWarning}
|
||||
>
|
||||
<X aria-hidden="true" size={14} strokeWidth={2.4} />
|
||||
</button>
|
||||
</PlatformRuntimeStatusToast>
|
||||
) : null}
|
||||
<ImageCanvasGenerationComposerView
|
||||
|
||||
@@ -1291,6 +1291,30 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('keeps reference pick warnings visible until the user takes a follow-up action', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开角色生成' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择非规范角色图' }));
|
||||
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
|
||||
'选择的图片不是角色规范图,请选择生成规范里的角色规范。',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(10_000);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
|
||||
'选择的图片不是角色规范图,请选择生成规范里的角色规范。',
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('restores the last picked character spec from local cache after remounting the editor', () => {
|
||||
const firstRender = render(<GenerationWorkflowHarness />);
|
||||
|
||||
|
||||
@@ -189,7 +189,6 @@ const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY =
|
||||
'genarrative.imageCanvas.lastCharacterSpecReference';
|
||||
const LAST_ICON_SPEC_REFERENCE_CACHE_KEY =
|
||||
'genarrative.imageCanvas.lastIconSpecReference';
|
||||
const REFERENCE_PICK_WARNING_DISMISS_MS = 2600;
|
||||
const INVALID_CHARACTER_SPEC_WARNING =
|
||||
'选择的图片不是角色规范图,请选择生成规范里的角色规范。';
|
||||
const INVALID_ICON_SPEC_WARNING =
|
||||
@@ -697,16 +696,6 @@ export function useImageCanvasGenerationWorkflow({
|
||||
generateDialog?.mode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!referencePickWarning) {
|
||||
return undefined;
|
||||
}
|
||||
const warningTimer = window.setTimeout(() => {
|
||||
setReferencePickWarning(null);
|
||||
}, REFERENCE_PICK_WARNING_DISMISS_MS);
|
||||
return () => window.clearTimeout(warningTimer);
|
||||
}, [referencePickWarning]);
|
||||
|
||||
const quickEditSourceLayer = quickEditPanel
|
||||
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
|
||||
null)
|
||||
|
||||
@@ -89,9 +89,11 @@ function createDeferred<T>() {
|
||||
function ProjectPersistenceHarness({
|
||||
canAccessProtectedData = true,
|
||||
initialGenerationDialogs = [],
|
||||
onProjectAccessLost,
|
||||
}: {
|
||||
canAccessProtectedData?: boolean;
|
||||
initialGenerationDialogs?: CanvasGenerationDialogState[];
|
||||
onProjectAccessLost?: () => void;
|
||||
}) {
|
||||
const [layers, setLayers] = useState<CanvasLayer[]>([]);
|
||||
const [generationDialogs, setGenerationDialogs] = useState<
|
||||
@@ -151,6 +153,7 @@ function ProjectPersistenceHarness({
|
||||
isViewportInteracting,
|
||||
canAccessProtectedData,
|
||||
openEditorLoginModal: openEditorLoginModalRef.current,
|
||||
onProjectAccessLost,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -1355,4 +1358,29 @@ describe('useImageCanvasProjectPersistence', () => {
|
||||
expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('notifies the shell to leave the canvas when an explicit project becomes inaccessible', async () => {
|
||||
const onProjectAccessLost = vi.fn();
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/editor/canvas?projectid=missing-project',
|
||||
);
|
||||
loadEditorProjectMock.mockRejectedValueOnce(
|
||||
new ApiClientError({
|
||||
message: '项目不存在',
|
||||
status: 404,
|
||||
code: 'not_found',
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<ProjectPersistenceHarness onProjectAccessLost={onProjectAccessLost} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onProjectAccessLost).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledWith('missing-project');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,19 @@ type ImageCanvasProjectPersistenceSetters = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ImageCanvasProjectPersistenceOptions = {
|
||||
refs: ImageCanvasProjectPersistenceRefs;
|
||||
setters: ImageCanvasProjectPersistenceSetters;
|
||||
layers: CanvasLayer[];
|
||||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||||
viewport: CanvasViewport;
|
||||
isViewportInteracting: boolean;
|
||||
canAccessProtectedData: boolean;
|
||||
currentUserId?: string | null;
|
||||
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||||
onProjectAccessLost?: () => void;
|
||||
};
|
||||
|
||||
function isEditorAuthError(error: unknown) {
|
||||
return (
|
||||
error instanceof ApiClientError &&
|
||||
@@ -284,17 +297,8 @@ export function useImageCanvasProjectPersistence({
|
||||
canAccessProtectedData,
|
||||
currentUserId,
|
||||
openEditorLoginModal,
|
||||
}: {
|
||||
refs: ImageCanvasProjectPersistenceRefs;
|
||||
setters: ImageCanvasProjectPersistenceSetters;
|
||||
layers: CanvasLayer[];
|
||||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||||
viewport: CanvasViewport;
|
||||
isViewportInteracting: boolean;
|
||||
canAccessProtectedData: boolean;
|
||||
currentUserId?: string | null;
|
||||
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||||
}) {
|
||||
onProjectAccessLost,
|
||||
}: ImageCanvasProjectPersistenceOptions) {
|
||||
const projectIdRef = useRef<string | null>(null);
|
||||
const pendingProjectResourceLayersRef = useRef<PendingProjectResourceLayer[]>(
|
||||
[],
|
||||
@@ -704,6 +708,10 @@ export function useImageCanvasProjectPersistence({
|
||||
}
|
||||
if (projectIdFromQuery && isEditorProjectAccessError(error)) {
|
||||
removeEditorProjectSessionCache(projectIdFromQuery);
|
||||
if (onProjectAccessLost) {
|
||||
onProjectAccessLost();
|
||||
return;
|
||||
}
|
||||
replaceAppHistoryPath('/project');
|
||||
}
|
||||
});
|
||||
@@ -715,6 +723,7 @@ export function useImageCanvasProjectPersistence({
|
||||
canAccessProtectedData,
|
||||
applyProjectSnapshot,
|
||||
createProjectResourceForLayer,
|
||||
onProjectAccessLost,
|
||||
openEditorLoginModal,
|
||||
]);
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ import { useHostNetworkOnline } from '../../hooks/useHostNetworkOnline';
|
||||
import {
|
||||
buildPublicWorkStagePath,
|
||||
pushAppHistoryPath,
|
||||
replaceAppHistoryPath,
|
||||
resolvePathForSelectionStage,
|
||||
} from '../../routing/appPageRoutes';
|
||||
import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFoundRecovery';
|
||||
@@ -3757,6 +3758,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSelectionStage('project');
|
||||
}, [setSelectionStage]);
|
||||
|
||||
const replaceWithProjectGallery = useCallback(() => {
|
||||
replaceAppHistoryPath('/project');
|
||||
setSelectionStage('project');
|
||||
}, [setSelectionStage]);
|
||||
|
||||
const openCreationCommunity = useCallback(() => {
|
||||
setIsCreationCommunityOpen(true);
|
||||
}, []);
|
||||
@@ -15716,7 +15722,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载编辑器..." />}
|
||||
>
|
||||
<ImageCanvasEditorView />
|
||||
<ImageCanvasEditorView
|
||||
onProjectAccessLost={replaceWithProjectGallery}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -8944,6 +8944,25 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__reference-pick-toast-close {
|
||||
display: inline-grid;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentColor 16%, transparent);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.image-canvas-editor__reference-pick-toast-close:hover,
|
||||
.image-canvas-editor__reference-pick-toast-close:focus-visible {
|
||||
background: color-mix(in srgb, currentColor 26%, transparent);
|
||||
}
|
||||
|
||||
.image-canvas-editor__generate-submit {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user