fix(editor): improve image block upload and download states (#12017)

Related to: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-loading-和错误样式)

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

- **New Features**
  - Introduced a unified resource controller for managing image and attachment resources, providing improved loading, error, and state handling.
  - Added a visual loading indicator overlay to image blocks for better feedback during image loading.

- **Improvements**
  - Simplified and centralized image and attachment state management, reducing redundant properties and manual state tracking.
  - Updated fallback UI for image blocks with clearer titles, descriptions, and improved layout.
  - Enhanced batch image block creation and download handling for improved efficiency.
  - Refined image block accessibility with improved alt text and streamlined rendering logic.
  - Centralized target model selection for image insertion in AI actions.
  - Reordered CSS declarations without affecting styling.
  - Improved reactive state tracking for blob upload/download operations in mock server.

- **Bug Fixes**
  - Improved cleanup of object URLs to prevent resource leaks.
  - Adjusted toolbar logic to more accurately reflect available actions based on image state.

- **Tests**
  - Updated end-to-end tests to match new UI text and behaviors for image loading and error states.

- **Chores**
  - Refactored internal logic and updated comments for clarity and maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-05-07 05:15:57 +00:00
parent 8f6e604774
commit 93b1d6c729
17 changed files with 484 additions and 532 deletions

View File

@@ -31,7 +31,6 @@ import { BlockSelection } from '@blocksuite/std';
import { Slice } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
import { html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -55,6 +54,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
computed(() => this.model.props.sourceId$.value)
);
get blobUrl() {
return this.resourceController.blobUrl$.value;
}
protected containerStyleMap = styleMap({
position: 'relative',
width: '100%',
@@ -123,10 +126,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
this.contentEditable = 'false';
// This is a tradeoff, initializing `Blob Sync Engine`.
this.resourceController.setEngine(this.std.store.blobSync);
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.refreshData();
@@ -139,14 +142,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}
}
override disconnectedCallback() {
const blobUrl = this.blobUrl;
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
}
super.disconnectedCallback();
}
override firstUpdated() {
// lazy bindings
this.disposables.addFromEvent(this, 'click', this.onClick);
@@ -207,7 +202,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
<div class="affine-attachment-content">
<div class="affine-attachment-content-title">
<div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-text truncate">
${title}
</div>
@@ -237,7 +231,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
<div class="affine-attachment-content">
<div class="affine-attachment-content-title">
<div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-text truncate">
${title}
</div>
@@ -326,9 +319,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
`;
}
@property({ attribute: false })
accessor blobUrl: string | null = null;
override accessor selectedStyle = SelectedStyle.Border;
override accessor useCaptionEditor = true;

View File

@@ -6,9 +6,9 @@ export const styles = css`
border-radius: 8px;
box-sizing: border-box;
user-select: none;
overflow: hidden;
border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')};
background: ${unsafeCSSVarV2('layer/background/primary')};
overflow: hidden;
&.focused {
border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')};
@@ -30,6 +30,13 @@ export const styles = css`
min-width: 0;
}
.truncate {
align-self: stretch;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.affine-attachment-content-title {
display: flex;
flex-direction: row;
@@ -47,13 +54,6 @@ export const styles = css`
color: var(--affine-text-primary-color);
}
.truncate {
align-self: stretch;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.affine-attachment-content-title-text {
color: var(--affine-text-primary-color);
font-family: var(--affine-font-family);

View File

@@ -22,15 +22,13 @@ import type { BlockModel } from '@blocksuite/store';
import type { AttachmentBlockComponent } from './attachment-block';
export async function getAttachmentBlob(model: AttachmentBlockModel) {
const {
sourceId$: { value: sourceId },
type$: { value: type },
} = model.props;
const { sourceId$, type$ } = model.props;
const sourceId = sourceId$.peek();
const type = type$.peek();
if (!sourceId) return null;
const doc = model.doc;
let blob = await doc.blobSync.get(sourceId);
const blob = await doc.blobSync.get(sourceId);
if (!blob) return null;
return new Blob([blob], { type });
@@ -72,19 +70,9 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
export async function refreshData(block: AttachmentBlockComponent) {
const model = block.model;
const sourceId = model.props.sourceId$.peek();
if (!sourceId) return;
const type = model.props.type$.peek();
const url = await block.resourceController.createBlobUrlWith(type);
if (!url) return;
// Releases the previous url.
const prevUrl = block.blobUrl;
if (prevUrl) URL.revokeObjectURL(prevUrl);
block.blobUrl = url;
await block.resourceController.refreshUrlWith(type);
}
export async function getFileType(file: File) {
@@ -94,7 +82,7 @@ export async function getFileType(file: File) {
const buffer = await file.arrayBuffer();
const FileType = await import('file-type');
const fileType = await FileType.fileTypeFromBuffer(buffer);
return fileType ? fileType.mime : '';
return fileType?.mime ?? '';
}
function hasExceeded(