fix(canvas): block invalid video duration confirmation
This commit is contained in:
1 parent
566f99f5ed
commit
11b19832a3
3 files changed
+39
-1
No files matched your search
@@ -29,6 +29,8 @@
|
|||||||
- The confirmation card now filters enabled server-provided video durations to the inclusive 2-6 second client boundary.
|
- 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.
|
- 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.
|
- 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.
|
- Existing request seams were verified: Renderer sends `durationSeconds: 6`, and the Main cloud adapter sends `duration_seconds: 6` to Works Square.
|
||||||
|
|
||||||
## Verification
|
## 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 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.
|
- `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.
|
- `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
|
## Follow-ups
|
||||||
|
|
||||||
|
|||||||
@@ -268,6 +268,20 @@ type EditableGenerationParameters = Partial<Pick<
|
|||||||
const MIN_VIDEO_DURATION_SECONDS = 2;
|
const MIN_VIDEO_DURATION_SECONDS = 2;
|
||||||
const MAX_VIDEO_DURATION_SECONDS = 6;
|
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 {
|
function quoteDraftFromQuote(quote: DesignGenerationQuote): GenerationQuoteDraft {
|
||||||
return {
|
return {
|
||||||
quoteId: quote.quoteId,
|
quoteId: quote.quoteId,
|
||||||
@@ -740,6 +754,10 @@ function QuoteCard({
|
|||||||
const selectedDuration = availableDurations.some(
|
const selectedDuration = availableDurations.some(
|
||||||
(option) => option.value === draft.generationParameters.durationSeconds,
|
(option) => option.value === draft.generationParameters.durationSeconds,
|
||||||
) ? String(draft.generationParameters.durationSeconds) : '';
|
) ? String(draft.generationParameters.durationSeconds) : '';
|
||||||
|
const durationSelectionValid = hasSelectableVideoDuration(
|
||||||
|
quote,
|
||||||
|
draft.generationParameters,
|
||||||
|
);
|
||||||
const updateParameters = (next: EditableGenerationParameters) => {
|
const updateParameters = (next: EditableGenerationParameters) => {
|
||||||
onDraftChange({
|
onDraftChange({
|
||||||
...draft,
|
...draft,
|
||||||
@@ -867,7 +885,7 @@ function QuoteCard({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground"
|
className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
disabled={busy || dirty || repricing}
|
disabled={busy || dirty || repricing || !durationSelectionValid}
|
||||||
>
|
>
|
||||||
{busy || repricing
|
{busy || repricing
|
||||||
? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||||
@@ -1153,6 +1171,7 @@ export function ImageCanvas() {
|
|||||||
setQuoteDirty(true);
|
setQuoteDirty(true);
|
||||||
setRepricingQuoteId(quote.quoteId);
|
setRepricingQuoteId(quote.quoteId);
|
||||||
setQuoteUpdateError(null);
|
setQuoteUpdateError(null);
|
||||||
|
setActionError(null);
|
||||||
quoteDraftVersionRef.current += 1;
|
quoteDraftVersionRef.current += 1;
|
||||||
const version = quoteDraftVersionRef.current;
|
const version = quoteDraftVersionRef.current;
|
||||||
if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current);
|
if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current);
|
||||||
@@ -1189,6 +1208,11 @@ export function ImageCanvas() {
|
|||||||
setActionError('当前报价正在更新,请等待重新报价完成后再确认。');
|
setActionError('当前报价正在更新,请等待重新报价完成后再确认。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!quote || quote.quoteId !== quoteId
|
||||||
|
|| !hasSelectableVideoDuration(quote, draft.generationParameters)) {
|
||||||
|
setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const requestedWorkspaceId = workspace.workspaceId;
|
const requestedWorkspaceId = workspace.workspaceId;
|
||||||
const requestedConversationId = conversation.conversationId;
|
const requestedConversationId = conversation.conversationId;
|
||||||
setConfirmingQuoteId(quoteId);
|
setConfirmingQuoteId(quoteId);
|
||||||
|
|||||||
@@ -544,6 +544,15 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
|||||||
expect(durationSelect).toHaveValue('');
|
expect(durationSelect).toHaveValue('');
|
||||||
expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value')))
|
expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value')))
|
||||||
.toEqual(['', '2', '6']);
|
.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' } });
|
fireEvent.change(durationSelect, { target: { value: '6' } });
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user