Files
AFFiNE-Mirror/blocksuite/playground/apps/_common/sync/blob/mock-server.ts
T
fundon 5590cdd8f1 fix(editor): improve status display of attachments and images (#12573)
Closes: [BS-3564](https://linear.app/affine-design/issue/BS-3564/ui-embed-view-报错-ui-加-title)
Closes: [BS-3454](https://linear.app/affine-design/issue/BS-3454/点击-reload-后应该隐藏-attachment-embed-view-左下角-status(待新状态))

<img width="807" alt="Screenshot 2025-05-28 at 17 23 26" src="https://github.com/user-attachments/assets/9ecc29f8-73c6-4441-bc38-dfe9bd876542" />

<img width="820" alt="Screenshot 2025-05-28 at 17 45 37" src="https://github.com/user-attachments/assets/68e6db17-a814-4df4-a9fa-067ca03dec30" />

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

## Summary by CodeRabbit

- **New Features**
  - Added support for retrying failed uploads of attachments and images, allowing users to re-upload files directly from the error status interface.
  - The error status dialog now dynamically displays "Retry" for upload failures and "Reload" for download failures, with appropriate actions for each.
- **Enhancements**
  - Improved clarity and consistency in file type display and icon usage for attachments and citations.
  - Button labels in the attachment interface now have capitalized text for better readability.
- **Bug Fixes**
  - Streamlined error handling and status updates for attachment and image uploads/downloads, reducing redundant UI elements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 02:18:51 +00:00

130 lines
2.9 KiB
TypeScript

import type { BlobSource, BlobState } from '@blocksuite/sync';
import { BehaviorSubject, ReplaySubject, share, throttleTime } from 'rxjs';
/**
* @internal just for test
*
* API: /api/collection/:id/blob/:key
* GET: get blob
* PUT: set blob
* DELETE: delete blob
*/
export class MockServerBlobSource implements BlobSource {
private readonly _cache = new Map<string, Blob>();
private readonly _states = new Map<string, BehaviorSubject<BlobState>>();
readonly = false;
constructor(readonly name: string) {}
async delete(key: string) {
this._cache.delete(key);
this._states.delete(key);
await fetch(`/api/collection/${this.name}/blob/${key}`, {
method: 'DELETE',
});
}
async get(key: string) {
if (this._cache.has(key)) return this._cache.get(key)!;
let state$ = this._states.get(key);
if (!state$) {
state$ = new BehaviorSubject<BlobState>(defaultState());
this._states.set(key, state$);
}
let blob: Blob | null = null;
nextState(state$, { downloading: true });
try {
const resp = await fetch(`/api/collection/${this.name}/blob/${key}`);
if (!resp.ok) throw new Error(`Failed to fetch blob ${key}`);
blob = await resp.blob();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
nextState(state$, { errorMessage });
} finally {
nextState(state$, { downloading: false });
if (blob) {
this._cache.set(key, blob);
}
}
return blob;
}
async list() {
return Array.from(this._cache.keys());
}
async set(key: string, value: Blob) {
let state$ = this._states.get(key);
if (!state$) {
state$ = new BehaviorSubject<BlobState>(defaultState());
this._states.set(key, state$);
}
this._cache.set(key, value);
nextState(state$, { uploading: true });
try {
await fetch(`/api/collection/${this.name}/blob/${key}`, {
method: 'PUT',
body: value,
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
nextState(state$, { errorMessage });
} finally {
nextState(state$, { uploading: false });
}
return key;
}
blobState$(key: string) {
let state$ = this._states.get(key);
if (!state$) {
state$ = new BehaviorSubject<BlobState>(defaultState());
this._states.set(key, state$);
nextState(state$, { errorMessage: 'Blob not found' });
}
return state$.pipe(
throttleTime(1000, undefined, { leading: true, trailing: true }),
share({
connector: () => new ReplaySubject(1),
})
);
}
}
function defaultState(): BlobState {
return {
uploading: false,
downloading: false,
overSize: false,
needDownload: false,
needUpload: false,
};
}
function nextState(
state$: BehaviorSubject<BlobState>,
state?: Partial<BlobState>
) {
state$.next({ ...state$.value, ...state });
}