fix(canvas): block invalid video duration confirmation
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
- The confirmation card now filters enabled server-provided video durations to the inclusive 2-6 second client boundary.
|
||||
- A legacy Quote whose selected duration is outside that boundary renders an explicit disabled `请选择时长` placeholder instead of visually selecting one allowed value while retaining a different hidden draft value.
|
||||
- Selecting 6 seconds updates the controlled draft, survives the debounced server re-quote, and is used by final confirmation.
|
||||
- Final-review P1 follow-up: a video Quote with `null`, an out-of-range duration, a disabled duration, or a duration absent from the filtered server options now disables the confirmation button and is rejected again inside the shared `handleConfirm` path used by direct, quick-reply, and text confirmation.
|
||||
- Only an explicit allowed selection followed by a successful server re-quote re-enables confirmation.
|
||||
- Existing request seams were verified: Renderer sends `durationSeconds: 6`, and the Main cloud adapter sends `duration_seconds: 6` to Works Square.
|
||||
|
||||
## Verification
|
||||
@@ -40,6 +42,9 @@
|
||||
- `corepack pnpm exec eslint src/pages/ImageCanvas/index.tsx tests/unit/image-canvas-page.test.tsx tests/unit/works-square-design-workspace.test.ts` — passed with no output.
|
||||
- `corepack pnpm run build:vite` — passed for Renderer, Electron Main, and Preload; only the existing dynamic-import and chunk-size warnings were reported.
|
||||
- `git diff --check` — passed; Git reported only the repository's LF-to-CRLF checkout warning.
|
||||
- Final-review P1 red test: the legacy 10-second Quote initially left `确认并开始生成` enabled.
|
||||
- Final-review P1 focused regression: legacy 10-second initial state disables confirmation and text confirmation sends no request; selecting 6 seconds, completing re-quote, and confirming sends 6 — passed.
|
||||
- Post-P1 focused suite: 3 files / 92 tests passed; typecheck and scoped ESLint passed; Renderer/Main/Preload `build:vite` passed with the same existing warnings.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
|
||||
@@ -268,6 +268,20 @@ type EditableGenerationParameters = Partial<Pick<
|
||||
const MIN_VIDEO_DURATION_SECONDS = 2;
|
||||
const MAX_VIDEO_DURATION_SECONDS = 6;
|
||||
|
||||
function hasSelectableVideoDuration(
|
||||
quote: DesignGenerationQuote,
|
||||
parameters: DesignGenerationParameters,
|
||||
): boolean {
|
||||
if (quote.medium !== 'video') return true;
|
||||
const durationSeconds = parameters.durationSeconds;
|
||||
return durationSeconds !== null
|
||||
&& durationSeconds >= MIN_VIDEO_DURATION_SECONDS
|
||||
&& durationSeconds <= MAX_VIDEO_DURATION_SECONDS
|
||||
&& quote.generationOptions.durations.some((option) => (
|
||||
!option.disabled && option.value === durationSeconds
|
||||
));
|
||||
}
|
||||
|
||||
function quoteDraftFromQuote(quote: DesignGenerationQuote): GenerationQuoteDraft {
|
||||
return {
|
||||
quoteId: quote.quoteId,
|
||||
@@ -740,6 +754,10 @@ function QuoteCard({
|
||||
const selectedDuration = availableDurations.some(
|
||||
(option) => option.value === draft.generationParameters.durationSeconds,
|
||||
) ? String(draft.generationParameters.durationSeconds) : '';
|
||||
const durationSelectionValid = hasSelectableVideoDuration(
|
||||
quote,
|
||||
draft.generationParameters,
|
||||
);
|
||||
const updateParameters = (next: EditableGenerationParameters) => {
|
||||
onDraftChange({
|
||||
...draft,
|
||||
@@ -867,7 +885,7 @@ function QuoteCard({
|
||||
size="sm"
|
||||
className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground"
|
||||
onClick={onConfirm}
|
||||
disabled={busy || dirty || repricing}
|
||||
disabled={busy || dirty || repricing || !durationSelectionValid}
|
||||
>
|
||||
{busy || repricing
|
||||
? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
@@ -1153,6 +1171,7 @@ export function ImageCanvas() {
|
||||
setQuoteDirty(true);
|
||||
setRepricingQuoteId(quote.quoteId);
|
||||
setQuoteUpdateError(null);
|
||||
setActionError(null);
|
||||
quoteDraftVersionRef.current += 1;
|
||||
const version = quoteDraftVersionRef.current;
|
||||
if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current);
|
||||
@@ -1189,6 +1208,11 @@ export function ImageCanvas() {
|
||||
setActionError('当前报价正在更新,请等待重新报价完成后再确认。');
|
||||
return;
|
||||
}
|
||||
if (!quote || quote.quoteId !== quoteId
|
||||
|| !hasSelectableVideoDuration(quote, draft.generationParameters)) {
|
||||
setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。');
|
||||
return;
|
||||
}
|
||||
const requestedWorkspaceId = workspace.workspaceId;
|
||||
const requestedConversationId = conversation.conversationId;
|
||||
setConfirmingQuoteId(quoteId);
|
||||
|
||||
@@ -544,6 +544,15 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
expect(durationSelect).toHaveValue('');
|
||||
expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value')))
|
||||
.toEqual(['', '2', '6']);
|
||||
expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeDisabled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('设计需求'), {
|
||||
target: { value: '确认生成' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
|
||||
|
||||
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('请选择 2 至 6 秒的视频时长');
|
||||
|
||||
fireEvent.change(durationSelect, { target: { value: '6' } });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user