Skip to content

feat: import for google-keep#14454

Draft
man-of-fox wants to merge 27 commits intotoeverything:canaryfrom
man-of-fox:canary-import-google-keep
Draft

feat: import for google-keep#14454
man-of-fox wants to merge 27 commits intotoeverything:canaryfrom
man-of-fox:canary-import-google-keep

Conversation

@man-of-fox
Copy link

@man-of-fox man-of-fox commented Feb 16, 2026

because I want to migrate my notes from Google Keep to affine, I wrote this missing importer.

Summary by CodeRabbit

  • New Features

    • Google Keep Takeout import: converts notes to documents with safe titles, creation/update dates, favorites, tags, attachments (images arranged in framed grids; non-images as native attachments), optional target-folder linking, per-note progress and error reporting.
    • Two-step import UI to configure options and report granular progress (documents imported, created vs. reused tags).
  • Localization

    • English and German UI strings for Google Keep import.
  • Style

    • New import dialog layout and controls styling.
  • Improvements

    • More reliable image sizing and attachment decoding for better import fidelity.

Here some screenshots of the changes:
English version:
Bildschirmfoto 2026-02-16 um 15 49 41
German version:
Bildschirmfoto 2026-02-16 um 15 44 14
Importoptions:
Bildschirmfoto 2026-02-16 um 15 49 54
Bildschirmfoto 2026-02-16 um 15 45 58
Bildschirmfoto 2026-02-16 um 15 50 11
Bildschirmfoto 2026-02-16 um 15 46 15
Import status:
Bildschirmfoto 2026-02-16 um 15 52 48
Bildschirmfoto 2026-02-16 um 15 46 51

@man-of-fox man-of-fox requested a review from a team as a code owner February 16, 2026 01:41
@github-actions github-actions bot added mod:i18n Related to i18n app:core labels Feb 16, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 45638ef8-135d-4a18-82c2-dbe346ebe382

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Google Keep Takeout import: a new transformer to unzip and parse Keep notes/attachments into AFFiNE docs, plus import dialog UI, types, styles, i18n keys, and progress/tag/favorite handling. (50 words)

Changes

Cohort / File(s) Summary
Google Keep Transformer
blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts
New transformer exporting GoogleKeepTransformer / importGoogleKeepZip: unzip Takeout, robustly decode/parse JSON, derive safe titles/metadata (created/updated/favorite), extract labels→tags, optional attachment resolution (image probing, layout), build safe HTML, import via HtmlTransformer, per-doc progress & error hooks, and helper utilities.
Transformer Exports
blocksuite/affine/widgets/linked-doc/src/transformers/index.ts
Added export for GoogleKeepTransformer (./google-keep.js).
Import Dialog & Flow
packages/frontend/core/src/desktop/dialogs/import/index.tsx
Adds googleKeep import type, two-step options UI (GoogleKeepImportOptionsPanel), new types (GoogleKeepImportOptions, ImportProgress, FolderOption), extends ImportConfig.importFunction signature to accept explorerIconService/favoriteService/tagService/googleKeepOptions/onImportProgress, wires tag creation/reuse, favorite updates, optional folder linking and progress reporting.
Import Styles
packages/frontend/core/src/desktop/dialogs/import/styles.css.ts
New style exports: importOptionsPanel, importOptionRow, importFolderSelect used by Google Keep options UI.
Internationalization
packages/frontend/i18n/src/resources/en.json, packages/frontend/i18n/src/resources/de.json
Added English and German localization keys for Google Keep import UI, options, buttons, and progress messages.
Shared Image Utility
blocksuite/affine/shared/src/utils/image.ts
readImageSize signature changed to async readImageSize(file, fallback) and updated to prefer createImageBitmap, with URL/Image fallback and robust cleanup; adds ImageSize type and validation helper.
sequenceDiagram
    participant User as User
    participant ImportDialog as Import Dialog
    participant Options as Options Panel
    participant Transformer as GoogleKeep\nTransformer
    participant Services as Services\n(Favorite/Tag/Explorer)
    participant Doc as AFFiNE Doc

    User->>ImportDialog: Open import dialog & choose "Google Keep"
    ImportDialog->>Options: Show Google Keep options panel
    User->>Options: Configure options & continue
    User->>ImportDialog: Provide Keep zip file
    ImportDialog->>Transformer: importGoogleKeepZip(zipBlob, options, onImportProgress)
    Transformer->>Transformer: Unzip archive, index files, decode JSON notes
    Transformer->>Transformer: Build safe HTML, probe/resolve attachments, layout images
    Transformer->>Doc: Import HTML content via HtmlTransformer
    Transformer->>Doc: Insert attachments, set tags, sync root title/metadata
    Transformer->>Services: Create/reuse tags, mark favorites, link to folder
    Transformer->>ImportDialog: Emit progress updates / errors
    ImportDialog->>User: Display progress and final status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I nibbled through zips with curious cheer,
Notes unfurled and images hopped near,
Titles stitched from tangled lines,
Tags and stars in tidy signs,
Now Keep's small treasures live here! 🥕📦

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: import for google-keep' clearly and concisely summarizes the main feature: adding Google Keep import functionality to AFFiNE.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use your project's `biome` configuration to improve the quality of JS/TS/CSS/JSON code reviews.

Add a configuration file to your project to customize how CodeRabbit runs biome.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 506-538: The Image object URL created in readImageSize can leak if
image decoding fails because URL.revokeObjecturl(?url=https%3A%2F%2Fgithub.com%2Ftoeverything%2FAFFiNE%2Fpull%2F14454%2FobjectUrl) is only called on
the success path; wrap the image loading block in a try/finally (or ensure a
finally-like cleanup) so that URL.revokeObjecturl(?url=https%3A%2F%2Fgithub.com%2Ftoeverything%2FAFFiNE%2Fpull%2F14454%2FobjectUrl) always runs
regardless of success or error, and keep the existing reject/resolve behavior
inside the try block while revoking in the finally for the objectUrl created in
readImageSize.

In `@packages/frontend/i18n/src/resources/de.json`:
- Around line 623-626: Replace the fragmented German value for the Google Keep
import settings title with the idiomatic compound noun: update the JSON value
for "com.affine.import.google-keep.options.title" from "Google Notizen Import
Einstellungen" to "Google Notizen Importeinstellungen" (or
"Google-Notizen-Importeinstellungen" if you prefer hyphenation) so the
translation reads naturally as a single compound word.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts (3)

802-803: toHtml is declared async but never uses await.

The function body is entirely synchronous. Dropping async and returning string directly avoids an unnecessary microtask tick on every call.

Suggested fix
-async function toHtml(
+function toHtml(
   note: GoogleKeepNote,
   fallbackTitle: string
-): Promise<string> {
+): string {

Then at the call site (line 928):

-    const html = await toHtml(note, fallbackTitle);
+    const html = toHtml(note, fallbackTitle);

880-895: Zip entries are iterated twice — can be consolidated into a single pass.

The first loop (lines 880-884) collects JSON candidates, and the second loop (lines 893-895) builds the full file map. Both iterate the same unzip entries and can be merged.

Suggested consolidation
-  const candidates: Array<{ path: string; content: Blob }> = [];
-
-  for (const entry of unzip) {
-    if (entry.path.toLowerCase().endsWith('.json')) {
-      candidates.push({ path: entry.path, content: entry.content });
-    }
-  }
-
-  const notesToImport: Array<{
-    note: GoogleKeepNote;
-    fallbackTitle: string;
-    notePath: string;
-  }> = [];
-  const allFiles = new Map<string, Blob>();
-
-  for (const entry of unzip) {
-    allFiles.set(normalizePath(entry.path), entry.content);
-  }
+  const candidates: Array<{ path: string; content: Blob }> = [];
+  const allFiles = new Map<string, Blob>();
+
+  for (const entry of unzip) {
+    allFiles.set(normalizePath(entry.path), entry.content);
+    if (entry.path.toLowerCase().endsWith('.json')) {
+      candidates.push({ path: entry.path, content: entry.content });
+    }
+  }
+
+  const notesToImport: Array<{
+    note: GoogleKeepNote;
+    fallbackTitle: string;
+    notePath: string;
+  }> = [];

927-968: Notes are processed sequentially — acceptable for import but consider error resilience.

If a single note fails mid-import (e.g., HtmlTransformer.importHTMLToDoc throws), the entire remaining batch is skipped. Consider wrapping the per-note logic in a try/catch to continue importing the remaining notes and report failures.

Suggested resilience improvement
   for (const { note, fallbackTitle, notePath } of notesToImport) {
+    try {
       const html = await toHtml(note, fallbackTitle);
       // ... existing body ...
       docIds.push(docId);
       importedDocs += 1;
       onProgress?.({ totalDocs: notesToImport.length, importedDocs });
     }
+    } catch (error) {
+      console.error(`Failed to import note "${fallbackTitle}":`, error);
+      importedDocs += 1;
+      onProgress?.({ totalDocs: notesToImport.length, importedDocs });
+    }
   }

@man-of-fox
Copy link
Author

Here some screenshots of the changes:
Bildschirmfoto 2026-02-16 um 15 49 41
Bildschirmfoto 2026-02-16 um 15 49 54
Bildschirmfoto 2026-02-16 um 15 50 11
Bildschirmfoto 2026-02-16 um 15 52 48

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts (1)

343-346: Deduplicate path candidates when baseDir is empty.

When the note JSON resides at the archive root, baseDir is '' and both candidates are identical, causing a redundant second Map.get for the same key.

♻️ Proposed fix
-  const candidates = [
-    normalizedFilePath,
-    baseDir ? `${baseDir}/${normalizedFilePath}` : normalizedFilePath,
-  ];
+  const candidates = baseDir
+    ? [normalizedFilePath, `${baseDir}/${normalizedFilePath}`]
+    : [normalizedFilePath];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts` around
lines 343 - 346, The candidates array currently contains duplicate entries when
baseDir is an empty string, causing redundant lookups; update the logic that
builds candidates (the const candidates = [...] block using normalizedFilePath
and baseDir) to only include the second entry when baseDir is non-empty/truthy
(or otherwise deduplicate entries e.g., filter unique values) so that Map.get is
not called twice for the same key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 936-962: The created doc can be orphaned if
appendAttachmentBlocksToDoc rejects, so move the metadata and title sync steps
(collection.meta.setDocMeta and syncRootTitle) and favorite handling
(onFavoriteImported) to occur immediately after HtmlTransformer.importHTMLToDoc
creates the docId, before calling appendAttachmentBlocksToDoc; then call
appendAttachmentBlocksToDoc as best-effort inside its own try/catch so any error
is caught and logged/handled without throwing (ensure docIds.push(docId),
importedDocs increment, and onProgress are executed regardless of attachment
failures).
- Around line 299-303: The current looksLikeHtml heuristic in renderTextContent
allows plain-text strings containing angle-bracket patterns to bypass escaping
(via looksLikeHtml and renderTextContent), which leads to downstream HTML
parsing removing content; fix by removing the passthrough: always treat input as
plain text and return escaped HTML using escapeHtml (i.e., stop returning raw
text when looksLikeHtml matches), or if you prefer a stricter heuristic tighten
looksLikeHtml to detect unambiguous HTML signals such as <!DOCTYPE, <html, or
multiple well-formed tag pairs before skipping escape; update renderTextContent
and/or looksLikeHtml accordingly so escapeHtml is always applied to Google Keep
JSON textContent.

---

Nitpick comments:
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 343-346: The candidates array currently contains duplicate entries
when baseDir is an empty string, causing redundant lookups; update the logic
that builds candidates (the const candidates = [...] block using
normalizedFilePath and baseDir) to only include the second entry when baseDir is
non-empty/truthy (or otherwise deduplicate entries e.g., filter unique values)
so that Map.get is not called twice for the same key.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts (2)

542-571: resolveKeepAttachments is async with no await expressions.

Every operation in the body is synchronous — resolveAttachmentBlob, toFileName, and the MIME-type derivation are all pure. Removing async makes the caller's dependency on Promise resolution explicit and avoids misleading readers about its asynchrony.

♻️ Proposed refactor
-async function resolveKeepAttachments(
+function resolveKeepAttachments(
   note: GoogleKeepNote,
   notePath: string,
   files: Map<string, Blob>
-): Promise<ResolvedKeepAttachment[]> {
+): ResolvedKeepAttachment[] {

The call site at line 911 (await resolveKeepAttachments(...)) still compiles correctly once the return type changes; the await becomes a no-op on a non-Promise value, so no other change is required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts` around
lines 542 - 571, The function resolveKeepAttachments is marked async but
contains no awaits; remove the async keyword and change its return type from
Promise<ResolvedKeepAttachment[]> to ResolvedKeepAttachment[] so it returns a
plain array; keep the internal logic as-is (calls to resolveAttachmentBlob,
toFileName, and mimeType derivation remain unchanged); you can leave callers
that currently do "await resolveKeepAttachments(...)" unchanged since awaiting a
non-Promise is a no-op.

491-532: Consider upstreaming the createImageBitmap path to the shared utility.

A readImageSize function already exists in blocksuite/affine/shared/src/utils/image.ts. The local implementation in google-keep.ts is genuinely superior (adds createImageBitmap primary path for better performance, avoids unnecessary DOMPurify.sanitize on blob URLs, uses proper try/finally for URL revocation, and returns a non-zero fallback size). This duplication means future improvements must be applied in both places.

To consolidate, consider upstreaming the createImageBitmap path and improved error handling into the shared utility, then import from there instead. The non-zero fallback value ({260, 180}) is specific to Keep attachment layouts and would need design consideration for the shared API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts` around
lines 491 - 532, The local readImageSize implementation duplicates and improves
the shared utility; refactor by merging the createImageBitmap-first logic,
try/finally URL.revokeObjectURL usage, and non-zero fallback behavior into the
shared readImageSize utility so callers (including the google-keep transformer)
import the single implementation; update the shared function to accept an
optional fallback size or remove the KEEP_ATTACHMENTS_COLUMN_WIDTH-specific
constant from the shared module and have the google-keep caller apply its {
width: KEEP_ATTACHMENTS_COLUMN_WIDTH, height: 180 } fallback after importing the
unified readImageSize, ensuring references to createImageBitmap, readImageSize,
and KEEP_ATTACHMENTS_COLUMN_WIDTH are preserved and tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 14-50: The toMeta conversion currently maps Google Keep's isPinned
to meta.favorite but never maps isTrashed to DocMeta.trash, so trashed notes are
treated as active; update the toMeta function to check GoogleKeepNote.isTrashed
(or is_trashed variant if normalized) and set meta.trash = true when present
(and ensure meta.trash remains undefined/false otherwise), and adjust any
callers that build DocMeta from toMeta to preserve this field so trashed notes
are surfaced as trashed in AFFiNE.

---

Nitpick comments:
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 542-571: The function resolveKeepAttachments is marked async but
contains no awaits; remove the async keyword and change its return type from
Promise<ResolvedKeepAttachment[]> to ResolvedKeepAttachment[] so it returns a
plain array; keep the internal logic as-is (calls to resolveAttachmentBlob,
toFileName, and mimeType derivation remain unchanged); you can leave callers
that currently do "await resolveKeepAttachments(...)" unchanged since awaiting a
non-Promise is a no-op.
- Around line 491-532: The local readImageSize implementation duplicates and
improves the shared utility; refactor by merging the createImageBitmap-first
logic, try/finally URL.revokeObjectURL usage, and non-zero fallback behavior
into the shared readImageSize utility so callers (including the google-keep
transformer) import the single implementation; update the shared function to
accept an optional fallback size or remove the
KEEP_ATTACHMENTS_COLUMN_WIDTH-specific constant from the shared module and have
the google-keep caller apply its { width: KEEP_ATTACHMENTS_COLUMN_WIDTH, height:
180 } fallback after importing the unified readImageSize, ensuring references to
createImageBitmap, readImageSize, and KEEP_ATTACHMENTS_COLUMN_WIDTH are
preserved and tested.

…d size detection; fit imported images into max width/height while preserving aspect ratio; use shared readImageSize util instead of local implementation
@man-of-fox man-of-fox changed the title feat: Canary add import for google-keep feat: import for google-keep Feb 19, 2026
@man-of-fox man-of-fox changed the title feat: import for google-keep feat: import for google-keep - with screenshots in comments Feb 19, 2026
Copy link
Author

@man-of-fox man-of-fox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

everythin commented by coderabbitai[bot] is integrated and tested with more than 350 Notes from Google Takedown Export

@man-of-fox man-of-fox changed the title feat: import for google-keep - with screenshots in comments feat: import for google-keep Feb 19, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts (1)

595-623: Isolate image-attachment failures so one bad file doesn’t drop the whole batch.

Promise.all rejects the entire image prep if any attachment fails. Consider per-attachment error handling (or Promise.allSettled) so successful images still import.

♻️ Suggested refactor (best-effort per attachment)
-      const preparedImages = await Promise.all(
-        imageAttachments.map(async attachment => {
-          const blobWithType = new File(
-            [attachment.blob],
-            attachment.fileName,
-            {
-              type: attachment.mimeType,
-            }
-          );
-          const [sourceId, naturalSize] = await Promise.all([
-            store.blobSync.set(blobWithType),
-            // Use the typed File for size detection; raw zip blobs can miss MIME type.
-            readImageSize(blobWithType, {
-              width: KEEP_ATTACHMENTS_COLUMN_WIDTH,
-              height: 180,
-            }),
-          ]);
-
-          return {
-            sourceId,
-            naturalWidth: Math.max(1, naturalSize.width),
-            naturalHeight: Math.max(1, naturalSize.height),
-          };
-        })
-      );
+      const preparedImages = (
+        await Promise.all(
+          imageAttachments.map(async attachment => {
+            try {
+              const blobWithType = new File(
+                [attachment.blob],
+                attachment.fileName,
+                {
+                  type: attachment.mimeType,
+                }
+              );
+              const [sourceId, naturalSize] = await Promise.all([
+                store.blobSync.set(blobWithType),
+                // Use the typed File for size detection; raw zip blobs can miss MIME type.
+                readImageSize(blobWithType, {
+                  width: KEEP_ATTACHMENTS_COLUMN_WIDTH,
+                  height: 180,
+                }),
+              ]);
+
+              return {
+                sourceId,
+                naturalWidth: Math.max(1, naturalSize.width),
+                naturalHeight: Math.max(1, naturalSize.height),
+              };
+            } catch (error) {
+              console.warn(
+                '[GoogleKeepTransformer] Failed to prepare image attachment:',
+                attachment.fileName,
+                error
+              );
+              return null;
+            }
+          })
+        )
+      ).filter(
+        (image): image is { sourceId: string; naturalWidth: number; naturalHeight: number } =>
+          Boolean(image)
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts` around
lines 595 - 623, The image preparation uses Promise.all over imageAttachments so
a single bad file rejects the whole batch; change to per-attachment error
isolation by mapping attachments to individual promises that catch their own
errors (or use Promise.allSettled on the mapped promises) and then filter out
failures before using preparedImages. Locate the block around
imageAttachments.map in google-keep.ts (functions/identifiers:
ensureSurfaceModel, getAttachmentsAnchorFromNote, store.blobSync.set,
readImageSize, KEEP_ATTACHMENTS_COLUMN_WIDTH, preparedImages) and update the
logic to either wrap the inner async work for each attachment in try/catch and
return null/error marker, or use Promise.allSettled and keep only settled
results with status "fulfilled" before proceeding so one bad attachment does not
drop the whole batch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 595-623: The image preparation uses Promise.all over
imageAttachments so a single bad file rejects the whole batch; change to
per-attachment error isolation by mapping attachments to individual promises
that catch their own errors (or use Promise.allSettled on the mapped promises)
and then filter out failures before using preparedImages. Locate the block
around imageAttachments.map in google-keep.ts (functions/identifiers:
ensureSurfaceModel, getAttachmentsAnchorFromNote, store.blobSync.set,
readImageSize, KEEP_ATTACHMENTS_COLUMN_WIDTH, preparedImages) and update the
logic to either wrap the inner async work for each attachment in try/catch and
return null/error marker, or use Promise.allSettled and keep only settled
results with status "fulfilled" before proceeding so one bad attachment does not
drop the whole batch.

@codecov
Copy link

codecov bot commented Feb 21, 2026

Codecov Report

❌ Patch coverage is 2.22930% with 307 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.33%. Comparing base (8f571dd) to head (1057cc4).

Files with missing lines Patch % Lines
...widgets/linked-doc/src/transformers/google-keep.ts 2.46% 277 Missing ⚠️
blocksuite/affine/shared/src/utils/image.ts 0.00% 30 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           canary   #14454      +/-   ##
==========================================
- Coverage   59.11%   58.33%   -0.78%     
==========================================
  Files        2909     2910       +1     
  Lines      160044   160342     +298     
  Branches    23361    23307      -54     
==========================================
- Hits        94607    93539    -1068     
- Misses      63278    64636    +1358     
- Partials     2159     2167       +8     
Flag Coverage Δ
server-test 75.23% <ø> (-1.23%) ⬇️
unittest 34.26% <2.22%> (-0.12%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@darkskygit
Copy link
Member

need to fix yarn typecheck error and run yarn affine i18n build

@man-of-fox man-of-fox marked this pull request as draft February 23, 2026 23:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts (1)

525-759: Make attachment import best‑effort per file.
A single failed image (Promise.all) or file attachment currently aborts all remaining attachments for that note. Consider isolating failures so the rest still import.

♻️ Possible direction (images)
-      const preparedImages = await Promise.all(
-        imageAttachments.map(async attachment => {
+      const preparedImages = (
+        await Promise.allSettled(
+          imageAttachments.map(async attachment => {
             const blobWithType = new File(
               [attachment.blob],
               attachment.fileName,
               {
                 type: attachment.mimeType,
               }
             );
             const [sourceId, naturalSize] = await Promise.all([
               store.blobSync.set(blobWithType),
               readImageSize(blobWithType, {
                 width: KEEP_ATTACHMENTS_COLUMN_WIDTH,
                 height: 180,
               }),
             ]);
             return {
               sourceId,
               naturalWidth: Math.max(1, naturalSize.width),
               naturalHeight: Math.max(1, naturalSize.height),
             };
-        })
-      );
+          })
+        )
+      )
+        .filter(
+          (result): result is PromiseFulfilledResult<{
+            sourceId: string;
+            naturalWidth: number;
+            naturalHeight: number;
+          }> => result.status === 'fulfilled'
+        )
+        .map(result => result.value);

Also consider per‑file try/catch in the non‑image loop so one bad file doesn’t drop the rest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts` around
lines 525 - 759, The image and file attachment import currently uses Promise.all
for preparedImages and has no error handling in the file loop, so one failure
aborts all attachments; update appendAttachmentBlocksToDoc to handle failures
per-file: replace the Promise.all(imageAttachments.map(...)) with either
Promise.allSettled or individual try/catch per image (referencing
imageAttachments, preparedImages, readImageSize, store.blobSync.set) and only
keep fulfilled results, and wrap the non-image loop body (creation of File,
await store.blobSync.set, and store.addBlock for 'affine:attachment') in a
try/catch that logs/skips failed files so one bad file doesn't stop the rest.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@blocksuite/affine/shared/src/utils/image.ts`:
- Around line 9-15: The current early return in readImageSize() incorrectly
rejects Blobs with empty types; update the guard so it only short‑circuits when
file.type is non‑empty and does not start with 'image/' (i.e., allow empty
file.type to proceed to probing). Locate the readImageSize function and replace
the condition that checks file.type.startsWith('image/') with a check that first
verifies file.type is truthy and then that it does not start with 'image/', so
legitimate image data in untyped Blobs still gets size-detected.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 867-875: The filter incorrectly excludes attachment-only Google
Keep notes by only checking title, textContent, and listContent; update the
check that currently inspects note (the GoogleKeepNote object) to also treat
note.attachments (or attachments.length > 0) as valid content so attachment-only
notes are not skipped—modify the conditional around note.title /
note.textContent / note.listContent to include a check for note.attachments (and
ensure it handles undefined vs empty arrays) in the same logical OR/AND
structure.

---

Nitpick comments:
In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts`:
- Around line 525-759: The image and file attachment import currently uses
Promise.all for preparedImages and has no error handling in the file loop, so
one failure aborts all attachments; update appendAttachmentBlocksToDoc to handle
failures per-file: replace the Promise.all(imageAttachments.map(...)) with
either Promise.allSettled or individual try/catch per image (referencing
imageAttachments, preparedImages, readImageSize, store.blobSync.set) and only
keep fulfilled results, and wrap the non-image loop body (creation of File,
await store.blobSync.set, and store.addBlock for 'affine:attachment') in a
try/catch that logs/skips failed files so one bad file doesn't stop the rest.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1163f96 and 5e307e9.

📒 Files selected for processing (3)
  • blocksuite/affine/shared/src/utils/image.ts
  • blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts
  • packages/frontend/i18n/src/resources/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/frontend/i18n/src/resources/en.json

Comment on lines +9 to +15
export async function readImageSize(
file: File | Blob,
fallback: ImageSize = { width: 0, height: 0 }
) {
if (!file.type.startsWith('image/')) {
return fallback;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Allow untyped image Blobs to be probed.
Blob.type can be empty (e.g., zip/fetch-created blobs). The current guard will return the fallback even if the data is an image, which can regress size detection. Consider only short‑circuiting when a non-empty type is explicitly non‑image.

🔧 Suggested tweak
-  if (!file.type.startsWith('image/')) {
+  if (file.type && !file.type.startsWith('image/')) {
     return fallback;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function readImageSize(
file: File | Blob,
fallback: ImageSize = { width: 0, height: 0 }
) {
if (!file.type.startsWith('image/')) {
return fallback;
}
export async function readImageSize(
file: File | Blob,
fallback: ImageSize = { width: 0, height: 0 }
) {
if (file.type && !file.type.startsWith('image/')) {
return fallback;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/shared/src/utils/image.ts` around lines 9 - 15, The current
early return in readImageSize() incorrectly rejects Blobs with empty types;
update the guard so it only short‑circuits when file.type is non‑empty and does
not start with 'image/' (i.e., allow empty file.type to proceed to probing).
Locate the readImageSize function and replace the condition that checks
file.type.startsWith('image/') with a check that first verifies file.type is
truthy and then that it does not start with 'image/', so legitimate image data
in untyped Blobs still gets size-detected.

Comment on lines +867 to +875
// Keep exports include additional metadata JSON files. Ignore files that
// do not look like an individual note payload.
if (
note.title === undefined &&
note.textContent === undefined &&
note.listContent === undefined
) {
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd blocksuite && find . -name "google-keep.ts" -type f

Repository: toeverything/AFFiNE

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Scan for Keep-like JSON fixtures that have attachments but no title/text/list fields.
rg -l '"attachments"' -g '*.json' -g '!**/node_modules/**' -g '!**/dist/**' |
while read -r f; do
  python - <<'PY' "$f"
import json,sys
path=sys.argv[1]
try:
    with open(path,'r',encoding='utf-8') as fh:
        data=json.load(fh)
except Exception:
    sys.exit(0)
if isinstance(data, dict) and data.get("attachments") and \
   "title" not in data and "textContent" not in data and "listContent" not in data:
    print(path)
PY
done

Repository: toeverything/AFFiNE

Length of output: 45


🏁 Script executed:

cat -n blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts | sed -n '860,885p'

Repository: toeverything/AFFiNE

Length of output: 905


🏁 Script executed:

# Search for test fixtures or sample data related to google-keep
find blocksuite -type f \( -name "*.json" -o -name "*keep*" \) | grep -E "(test|fixture|sample)" | head -20

Repository: toeverything/AFFiNE

Length of output: 243


🏁 Script executed:

rg -A 20 "interface GoogleKeepNote|type GoogleKeepNote" blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts

Repository: toeverything/AFFiNE

Length of output: 571


🏁 Script executed:

rg "attachments" blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts

Repository: toeverything/AFFiNE

Length of output: 713


🏁 Script executed:

# Search for any Google Keep documentation or schema references
rg -i "google.*keep|keep.*note" -A 5 blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts | head -40

Repository: toeverything/AFFiNE

Length of output: 1107


Don't skip attachment-only notes.
The current filter skips notes without title, textContent, or listContent, but notes with only attachments will be dropped because the condition doesn't account for them. The attachments field is a valid part of the GoogleKeepNote type and should qualify as content.

🔧 Suggested fix
     if (
       note.title === undefined &&
       note.textContent === undefined &&
       note.listContent === undefined &&
+      !note.attachments?.length
     ) {
       continue;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Keep exports include additional metadata JSON files. Ignore files that
// do not look like an individual note payload.
if (
note.title === undefined &&
note.textContent === undefined &&
note.listContent === undefined
) {
continue;
}
// Keep exports include additional metadata JSON files. Ignore files that
// do not look like an individual note payload.
if (
note.title === undefined &&
note.textContent === undefined &&
note.listContent === undefined &&
!note.attachments?.length
) {
continue;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocksuite/affine/widgets/linked-doc/src/transformers/google-keep.ts` around
lines 867 - 875, The filter incorrectly excludes attachment-only Google Keep
notes by only checking title, textContent, and listContent; update the check
that currently inspects note (the GoogleKeepNote object) to also treat
note.attachments (or attachments.length > 0) as valid content so attachment-only
notes are not skipped—modify the conditional around note.title /
note.textContent / note.listContent to include a check for note.attachments (and
ensure it handles undefined vs empty arrays) in the same logical OR/AND
structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:core mod:i18n Related to i18n

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants