mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 14:27:02 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user