fix(editor): switch view is not allowed during upload (#12018)

Closes: [BS-3352](https://linear.app/affine-design/issue/BS-3352/[improvement]-附件上传时禁止切-view)

![Screenshot 2025-05-07 at 15.50.58.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/8ypiIKZXudF5a0tIgIzf/a2267752-8459-44f7-8c24-08bb50d390c0.png)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Improved the logic for enabling or disabling the embed and card actions in the attachment toolbar, ensuring actions are only available when the file is ready and conditions are met.
- **Refactor**
  - Enhanced reliability by centralizing block state checks and refining conditions for action availability in the attachment dropdown menu.
  - Centralized resource state management with a new computed signal, improving consistency in loading and error state handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-05-07 08:01:56 +00:00
parent 3afecc0605
commit 179701f587
2 changed files with 55 additions and 27 deletions

View File

@@ -94,18 +94,32 @@ export const attachmentViewDropdownMenu = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType(AttachmentBlockModel); const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
if (!model) return null; if (!block) return null;
const model = block.model;
const embedProvider = ctx.std.get(AttachmentEmbedProvider); const embedProvider = ctx.std.get(AttachmentEmbedProvider);
const actions = this.actions.map(action => ({ ...action })); const actions = computed(() => {
const viewType$ = computed(() => { const [cardAction, embedAction] = this.actions.map(action => ({
const [cardAction, embedAction] = actions; ...action,
}));
const ok = block.resourceController.resolvedState$.value.state === 'none';
const sourceId = Boolean(model.props.sourceId$.value);
const embed = model.props.embed$.value ?? false; const embed = model.props.embed$.value ?? false;
// 1. Check whether `sourceId` exists.
// 2. Check if `embedded` is allowed.
// 3. Check `blobState$`
const allowed = ok && sourceId && embedProvider.embedded(model) && !embed;
cardAction.disabled = !embed; cardAction.disabled = !embed;
embedAction.disabled = embed && embedProvider.embedded(model); embedAction.disabled = !allowed;
return [cardAction, embedAction];
});
const viewType$ = computed(() => {
const [cardAction, embedAction] = actions.value;
const embed = model.props.embed$.value ?? false;
return embed ? embedAction.label : cardAction.label; return embed ? embedAction.label : cardAction.label;
}); });
const onToggle = (e: CustomEvent<boolean>) => { const onToggle = (e: CustomEvent<boolean>) => {
@@ -123,7 +137,7 @@ export const attachmentViewDropdownMenu = {
model, model,
html`<affine-view-dropdown-menu html`<affine-view-dropdown-menu
@toggle=${onToggle} @toggle=${onToggle}
.actions=${actions} .actions=${actions.value}
.context=${ctx} .context=${ctx}
.viewType$=${viewType$} .viewType$=${viewType$}
></affine-view-dropdown-menu>` ></affine-view-dropdown-menu>`

View File

@@ -1,6 +1,11 @@
import type { Disposable } from '@blocksuite/global/disposable'; import type { Disposable } from '@blocksuite/global/disposable';
import type { BlobEngine, BlobState } from '@blocksuite/sync'; import type { BlobEngine, BlobState } from '@blocksuite/sync';
import { effect, type ReadonlySignal, signal } from '@preact/signals-core'; import {
computed,
effect,
type ReadonlySignal,
signal,
} from '@preact/signals-core';
import type { TemplateResult } from 'lit-html'; import type { TemplateResult } from 'lit-html';
export type ResourceKind = 'Blob' | 'File' | 'Image'; export type ResourceKind = 'Blob' | 'File' | 'Image';
@@ -18,17 +23,40 @@ export type StateInfo = {
description?: string; description?: string;
}; };
export type ResolvedStateInfo = StateInfo & { export type ResolvedStateInfoPart = {
loading: boolean; loading: boolean;
error: boolean; error: boolean;
state: StateKind; state: StateKind;
}; };
export type ResolvedStateInfo = StateInfo & ResolvedStateInfoPart;
export class ResourceController implements Disposable { export class ResourceController implements Disposable {
readonly blobUrl$ = signal<string | null>(null); readonly blobUrl$ = signal<string | null>(null);
readonly state$ = signal<Partial<BlobState>>({}); readonly state$ = signal<Partial<BlobState>>({});
readonly resolvedState$ = computed<ResolvedStateInfoPart>(() => {
const {
uploading = false,
downloading = false,
overSize = false,
errorMessage,
} = this.state$.value;
const hasExceeded = overSize;
const hasError = hasExceeded || Boolean(errorMessage);
const state = this.determineState(
hasExceeded,
hasError,
uploading,
downloading
);
const loading = state === 'uploading' || state === 'loading';
return { error: hasError, loading, state };
});
private engine?: BlobEngine; private engine?: BlobEngine;
constructor( constructor(
@@ -61,26 +89,12 @@ export class ResourceController implements Disposable {
errorIcon?: TemplateResult; errorIcon?: TemplateResult;
} & StateInfo } & StateInfo
): ResolvedStateInfo { ): ResolvedStateInfo {
const { const { error, loading, state } = this.resolvedState$.value;
uploading = false,
downloading = false,
overSize = false,
errorMessage,
} = this.state$.value;
const hasExceeded = overSize;
const hasError = hasExceeded || Boolean(errorMessage);
const state = this.determineState(
hasExceeded,
hasError,
uploading,
downloading
);
const loading = state === 'uploading' || state === 'loading';
const { icon, title, description, loadingIcon, errorIcon } = info; const { icon, title, description, loadingIcon, errorIcon } = info;
const result = { const result = {
error: hasError, error,
loading, loading,
state, state,
icon, icon,
@@ -91,9 +105,9 @@ export class ResourceController implements Disposable {
if (loading) { if (loading) {
result.icon = loadingIcon ?? icon; result.icon = loadingIcon ?? icon;
result.title = state === 'uploading' ? 'Uploading...' : 'Loading...'; result.title = state === 'uploading' ? 'Uploading...' : 'Loading...';
} else if (hasError) { } else if (error) {
result.icon = errorIcon ?? icon; result.icon = errorIcon ?? icon;
result.description = errorMessage ?? description; result.description = this.state$.value.errorMessage ?? description;
} }
return result; return result;