Commit Graph

15 Commits

Author SHA1 Message Date
L-Sun 6fabc0eb1f fix(editor): adjustment of scaled and folded synced doc (#12294)
Close [BS-3418](https://linear.app/affine-design/issue/BS-3418/折叠的embed-doc调整宽度时,会出现一个最小高度,不需要这个)

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

## Summary by CodeRabbit

- **New Features**
  - Added support for dynamically scaling the height of embedded synced document blocks, including proper handling when folding and unfolding.
  - Introduced a new property to track the scaled height of folded synced document blocks.

- **Bug Fixes**
  - Improved accuracy of height calculations for synced document blocks by accounting for both viewport zoom and block scale.

- **Tests**
  - Enhanced end-to-end tests to consistently apply scaling before running size adjustment checks.
  - Added a utility function to simulate scaling elements with keyboard shortcuts during test execution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 03:56:03 +00:00
L-Sun 3ebed1d5a8 chore(editor): adjust size of synced doc (#12163)
Close [BS-3418](https://linear.app/affine-design/issue/BS-3418/折叠的embed-doc调整宽度时,会出现一个最小高度,不需要这个)

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

- **New Features**
  - Improved resizing and folding interactions for embedded synced documents in edgeless mode, including dynamic height calculation and enhanced drag-handle styling.
- **Bug Fixes**
  - Adjusted minimum height for synced document embeds to allow more compact display.
  - Ensured consistent width settings across embed cards.
- **Tests**
  - Added end-to-end tests covering folding, unfolding, and resizing behaviors for edgeless synced document embeds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-14 05:17:42 +00:00
L-Sun e0308c5815 feat(editor): make height of edgeless embed doc to fit content (#12089)
Close [BS-3388](https://linear.app/affine-design/issue/BS-3388/embed-doc-拖入后的初始高度不要超过800,不要限制用户随后的调整空间)

This PR impl a extension which initialize the height of added `affine-embed-edgeless-synced-doc-block` to fit its content

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

- **New Features**
  - Improved handling of embedded synced document block height to better fit content within edgeless mode.
- **Tests**
  - Added an end-to-end test to verify correct height adjustment for embedded synced documents in edgeless mode.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-30 18:48:54 +00:00
L-Sun 315ea00390 feat(editor): header of edgeless embed doc (#12029)
Close [BS-3268](https://linear.app/affine-design/issue/BS-3268/edgeless-下,-dark-mode-embed的配色应该更加清晰)
Close [BS-3067](https://linear.app/affine-design/issue/BS-3067/在embed上,添加split-view等相关的操作入口,基本接近page-block(见设计稿))

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

- **New Features**
  - Introduced an interactive header for embedded synced documents with fold/unfold toggle, document opening, and multiple view options.
  - Added info and copy link buttons for embedded synced documents and notes to improve document management and sharing.
- **Enhancements**
  - Updated styles for embedded synced document blocks and headers for better visual consistency.
  - Added new localization entries for header actions: "Fold", "Unfold", and "Open".
  - Disabled redundant open document actions in toolbars, centralizing controls in the header.
- **Refactor**
  - Unified header button components for notes and embedded synced documents into reusable components.
  - Simplified header components by delegating button behaviors to shared components.
- **Bug Fixes**
  - Fixed conditional rendering of editor content in embedded synced documents when folded.
- **Chores**
  - Upgraded theme dependency version from "^1.1.12" to "^1.1.14" across multiple packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-30 03:11:38 +00:00
donteatfriedrice 83670ab335 feat(editor): add experimental feature citation (#11984)
Closes: [BS-3122](https://linear.app/affine-design/issue/BS-3122/footnote-definition-adapter-适配)
Closes: [BS-3123](https://linear.app/affine-design/issue/BS-3123/几个-block-card-view-适配-footnote-态)

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

- **New Features**
  - Introduced a new citation card component and web element for displaying citations.
  - Added support for citation-style rendering in attachment, bookmark, and linked document blocks.
  - Enabled citation parsing from footnote definitions in markdown for attachments, bookmarks, and linked docs.
  - Added a feature flag to enable or disable citation features.
  - Provided new toolbar logic to disable downloads for citation-style attachments.

- **Improvements**
  - Updated block models and properties to support citation identifiers.
  - Added localization and settings for the citation experimental feature.
  - Enhanced markdown adapters to recognize and process citation footnotes.
  - Included new constants and styles for citation card display.

- **Bug Fixes**
  - Ensured readonly state is respected in block interactions and rendering for citation blocks.

- **Documentation**
  - Added exports and effects for new citation components and features.

- **Tests**
  - Updated snapshots to include citation-related properties in block data.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 06:59:27 +00:00
L-Sun a2b40fea20 feat(editor): insertion and duplication actions for embed synced doc (#11852)
Close [BS-3068](https://linear.app/affine-design/issue/BS-3068/在edgeless-的embed-doc-block上,增加-insert-into-page的选项,类似frame)
Close [BS-3069](https://linear.app/affine-design/issue/BS-3069/edgeless下的-embed,覆盖现在的edit行为-,暂定叫做duplicate-into-a-note,复制一个-note)
2025-04-21 11:01:04 +00:00
Saul-Mirone 205cd7a86d refactor(editor): rename block-std to std (#11250)
Closes: BS-2946
2025-03-28 07:20:34 +00:00
fundon 7f4b56e05c refactor(editor): edgeless external embed card toolbar config extension (#10712) 2025-03-19 04:05:36 +00:00
donteatfriedrice d2c62602a4 feat(editor): support embed iframe block (#10740)
To close:
[BS-2660](https://linear.app/affine-design/issue/BS-2660/slash-menu-支持-iframe-embed)
[BS-2661](https://linear.app/affine-design/issue/BS-2661/iframe-embed-block-model-and-block-component)
[BS-2662](https://linear.app/affine-design/issue/BS-2662/iframe-embed-block-toolbar)
[BS-2768](https://linear.app/affine-design/issue/BS-2768/iframe-embed-block-loading-和-error-态)
[BS-2670](https://linear.app/affine-design/issue/BS-2670/iframe-embed-block-导出)

# PR Description

# Add Embed Iframe Block Support

## Overview

This PR introduces a new `EmbedIframeBlock` to enhance content embedding capabilities within our editor. This block allows users to seamlessly embed external content from various providers (Google Drive, Spotify, etc.) directly into their docs.

## New Blocks

### EmbedIframeBlock

The core block that renders embedded iframe content. This block:

* Displays external content within a secure iframe
* Handles loading states with visual feedback
* Provides error handling with edit and retry options
* Supports customization of width, height, and other iframe attributes

### Supporting Components

* **EmbedIframeCreateModal**: Modal interface for creating new iframe embeds
* **EmbedIframeLinkEditPopup**: UI for editing existing embed links
* **EmbedIframeLoadingCard**: Visual feedback during content loading
* **EmbedIframeErrorCard**: Error handling with retry functionality

## New Store Extensions

### EmbedIframeConfigExtension

This extension provides configuration for different embed providers:

```typescript
/**
 * The options for the iframe
 * @example
 * {
 *   defaultWidth: '100%',
 *   defaultHeight: '152px',
 *   style: 'border-radius: 8px;',
 *   allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
 * }
 * =>
 * <iframe
 *   width="100%"
 *   height="152px"
 *   style="border-radius: 8px;"
 *   allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
 * ></iframe>
 */
export type IframeOptions = {
  defaultWidth?: string;
  defaultHeight?: string;
  style?: string;
  referrerpolicy?: string;
  scrolling?: boolean;
  allow?: string;
  allowFullscreen?: boolean;
};

/**
 * Define the config of an embed iframe block provider
 */
export type EmbedIframeConfig = {
  /**
   * The name of the embed iframe block provider
   */
  name: string;
  /**
   * The function to match the url
   */
  match: (url: string) => boolean;
  /**
   * The function to build the oEmbed URL for fetching embed data
   */
  buildOEmbedUrl: (url: string) => string | undefined;
  /**
   * Use oEmbed URL directly as iframe src without fetching oEmbed data
   */
  useOEmbedUrlDirectly: boolean;
  /**
   * The options for the iframe
   */
  options?: IframeOptions;
};

export const EmbedIframeConfigIdentifier =
  createIdentifier<EmbedIframeConfig>('EmbedIframeConfig');

export function EmbedIframeConfigExtension(
  config: EmbedIframeConfig
): ExtensionType & {
  identifier: ServiceIdentifier<EmbedIframeConfig>;
} {
  const identifier = EmbedIframeConfigIdentifier(config.name);
  return {
    setup: di => {
      di.addImpl(identifier, () => config);
    },
    identifier,
  };
}
```

**example:**

```typescript
//  blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts

const SPOTIFY_DEFAULT_WIDTH = '100%';
const SPOTIFY_DEFAULT_HEIGHT = '152px';

// https://developer.spotify.com/documentation/embeds/reference/oembed
const spotifyEndpoint = 'https://open.spotify.com/oembed';

const spotifyUrlValidationOptions: EmbedIframeUrlValidationOptions = {
  protocols: ['https:'],
  hostnames: ['open.spotify.com', 'spotify.link'],
};

const spotifyConfig = {
  name: 'spotify',
  match: (url: string) =>
    validateEmbedIframeUrl(url, spotifyUrlValidationOptions),
  buildOEmbedUrl: (url: string) => {
    const match = validateEmbedIframeUrl(url, spotifyUrlValidationOptions);
    if (!match) {
      return undefined;
    }
    const encodedUrl = encodeURIComponent(url);
    const oEmbedUrl = `${spotifyEndpoint}?url=${encodedUrl}`;
    return oEmbedUrl;
  },
  useOEmbedUrlDirectly: false,
  options: {
    defaultWidth: SPOTIFY_DEFAULT_WIDTH,
    defaultHeight: SPOTIFY_DEFAULT_HEIGHT,
    allow:
      'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
    style: 'border-radius: 12px;',
    allowFullscreen: true,
  },
};

// add the config extension to store
export const SpotifyEmbedConfig = EmbedIframeConfigExtension(spotifyConfig);
```

**Key features:**

* Provider registration and discovery
* URL pattern matching
* Provider-specific embed options (width, height, features)

### EmbedIframeService

This service provides abilities to handle URL validation, data fetching, and block creation

**Type:**

```typescript
/**
 * Service for handling embeddable URLs
 */
export interface EmbedIframeProvider {
  /**
   * Check if a URL can be embedded
   * @param url URL to check
   * @returns true if the URL can be embedded, false otherwise
   */
  canEmbed: (url: string) => boolean;

  /**
   * Build a API URL for fetching embed data
   * @param url URL to build API URL
   * @returns API URL if the URL can be embedded, undefined otherwise
   */
  buildOEmbedUrl: (url: string) => string | undefined;

  /**
   * Get the embed iframe config
   * @param url URL to get embed iframe config
   * @returns Embed iframe config if the URL can be embedded, undefined otherwise
   */
  getConfig: (url: string) => EmbedIframeConfig | undefined;

  /**
   * Get embed iframe data
   * @param url URL to get embed iframe data
   * @returns Embed iframe data if the URL can be embedded, undefined otherwise
   */
  getEmbedIframeData: (url: string) => Promise<EmbedIframeData | null>;

  /**
   * Parse an embeddable URL and add an EmbedIframeBlock to doc
   * @param url Original url to embed
   * @param parentId Parent block ID
   * @param index Optional index to insert at
   * @returns Created block id if successful, undefined if the URL cannot be embedded
   */
  addEmbedIframeBlock: (
    props: Partial<EmbedIframeBlockProps>,
    parentId: string,
    index?: number
  ) => string | undefined;
}
```

**Implemetation:**

```typescript
export class EmbedIframeService
  extends StoreExtension
  implements EmbedIframeProvider
{
  static override key = 'embed-iframe-service';

  private readonly _configs: EmbedIframeConfig[];

  constructor(store: Store) {
    super(store);
    this._configs = Array.from(
      store.provider.getAll(EmbedIframeConfigIdentifier).values()
    );
  }

  canEmbed = (url: string): boolean => {
    return this._configs.some(config => config.match(url));
  };

  buildOEmbedUrl = (url: string): string | undefined => {
    return this._configs.find(config => config.match(url))?.buildOEmbedUrl(url);
  };

  getConfig = (url: string): EmbedIframeConfig | undefined => {
    return this._configs.find(config => config.match(url));
  };

  getEmbedIframeData = async (
    url: string,
    signal?: AbortSignal
  ): Promise<EmbedIframeData | null> => {
    try {
      const config = this._configs.find(config => config.match(url));
      if (!config) {
        return null;
      }

      const oEmbedUrl = config.buildOEmbedUrl(url);
      if (!oEmbedUrl) {
        return null;
      }

      // if the config useOEmbedUrlDirectly is true, return the url directly as iframe_url
      if (config.useOEmbedUrlDirectly) {
        return {
          iframe_url: oEmbedUrl,
        };
      }

      // otherwise, fetch the oEmbed data
      const response = await fetch(oEmbedUrl, { signal });
      if (!response.ok) {
        console.warn(
          `Failed to fetch oEmbed data: ${response.status} ${response.statusText}`
        );
        return null;
      }

      const data = await response.json();
      return data as EmbedIframeData;
    } catch (error) {
      if (error instanceof Error && error.name !== 'AbortError') {
        console.error('Error fetching embed iframe data:', error);
      }
      return null;
    }
  };

  addEmbedIframeBlock = (
    props: Partial<EmbedIframeBlockProps>,
    parentId: string,
    index?: number
  ): string | undefined => {
    const blockId = this.store.addBlock(
      'affine:embed-iframe',
      props,
      parentId,
      index
    );
    return blockId;
  };
}
```

**Usage:**

```typescript
// Usage example
const embedIframeService = this.std.get(EmbedIframeService);

// Check if a URL can be embedded
const canEmbed = embedIframeService.canEmbed(url);

// Get embed data for a URL
const embedData = await embedIframeService.getEmbedIframeData(url);

// Add an embed iframe block to the document
const block = embedIframeService.addEmbedIframeBlock({
  url,
  iframeUrl: embedData.iframe_url,
  title: embedData.title,
  description: embedData.description
}, parentId, index);
```

**Key features:**

* URL validation and transformation
* Provider-specific data fetching
* Block creation and management

## Adaptations

### Toolbar Integration

Added toolbar actions for embedded content:

* Copy link
* Edit embed title and description
* Toggle between inline/card views
* Add caption
* And more

### Slash Menu Integration

Added a new slash menu option for embedding content:

* Embed item for inserting embed iframe block
* Conditional rendering based on feature flags

### Adapters

Implemented adapters for various formats:

* **HTML Adapter**: Exports embed original urls as html links
* **Markdown Adapter**: Exports embed original urls as markdown links
* **Plain Text Adapter**: Exports embed original urls as link text

## To Be Continued:
- [ ] **UI Optimization**
- [ ] **Edgeless Mode Support**
- [ ] **Mobile Support**
2025-03-13 04:11:46 +00:00
fundon ec9bd1f383 feat(editor): add toolbar registry extension (#9572)
### What's Changed!

#### Added
Manage various types of toolbars uniformly in one place.

* `affine-toolbar-widget`
* `ToolbarRegistryExtension`

The toolbar currently supports and handles several scenarios:

1.  Select blocks: `BlockSelection`
2. Select text: `TextSelection` or `NativeSelection`
3. Hover a link: `affine-link` and `affine-reference`

#### Removed
Remove redundant toolbar implementations.

* `attachment` toolbar
* `bookmark` toolbar
* `embed` toolbar
* `formatting` toolbar
* `affine-link` toolbar
* `affine-reference` toolbar

### How to migrate?

Here is an example that can help us migrate some unrefactored toolbars:

Check out the more detailed types of [`ToolbarModuleConfig`](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/blocksuite/affine/shared/src/services/toolbar-service/config.ts).

1.  Add toolbar configuration file to a block type, such as bookmark block: [`config.ts`](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/blocksuite/affine/block-bookmark/src/configs/toolbar.ts)

```ts
export const builtinToolbarConfig = {
  actions: [
    {
      id: 'a.preview',
      content(ctx) {
        const model = ctx.getCurrentModelBy(BlockSelection, BookmarkBlockModel);
        if (!model) return null;

        const { url } = model;

        return html`<affine-link-preview .url=${url}></affine-link-preview>`;
      },
    },
    {
      id: 'b.conversions',
      actions: [
        {
          id: 'inline',
          label: 'Inline view',
          run(ctx) {
          },
        },
        {
          id: 'card',
          label: 'Card view',
          disabled: true,
        },
        {
          id: 'embed',
          label: 'Embed view',
          disabled(ctx) {
          },
          run(ctx) {
          },
        },
      ],
      content(ctx) {
      },
    } satisfies ToolbarActionGroup<ToolbarAction>,
    {
      id: 'c.style',
      actions: [
        {
          id: 'horizontal',
          label: 'Large horizontal style',
        },
        {
          id: 'list',
          label: 'Small horizontal style',
        },
      ],
      content(ctx) {
      },
    } satisfies ToolbarActionGroup<ToolbarAction>,
    {
      id: 'd.caption',
      tooltip: 'Caption',
      icon: CaptionIcon(),
      run(ctx) {
      },
    },
    {
      placement: ActionPlacement.More,
      id: 'a.clipboard',
      actions: [
        {
          id: 'copy',
          label: 'Copy',
          icon: CopyIcon(),
          run(ctx) {
          },
        },
        {
          id: 'duplicate',
          label: 'Duplicate',
          icon: DuplicateIcon(),
          run(ctx) {
          },
        },
      ],
    },
    {
      placement: ActionPlacement.More,
      id: 'b.refresh',
      label: 'Reload',
      icon: ResetIcon(),
      run(ctx) {
      },
    },
    {
      placement: ActionPlacement.More,
      id: 'c.delete',
      label: 'Delete',
      icon: DeleteIcon(),
      variant: 'destructive',
      run(ctx) {
      },
    },
  ],
} as const satisfies ToolbarModuleConfig;
```

2. Add configuration extension to a block spec: [bookmark's spec](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/blocksuite/affine/block-bookmark/src/bookmark-spec.ts)

```ts
const flavour = BookmarkBlockSchema.model.flavour;

export const BookmarkBlockSpec: ExtensionType[] = [
  ...,
  ToolbarModuleExtension({
    id: BlockFlavourIdentifier(flavour),
    config: builtinToolbarConfig,
  }),
].flat();
```

3. If the bock type already has a toolbar configuration built in, we can customize it in the following ways:

Check out the [editor's config](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts#L51C4-L54C8) file.

```ts
// Defines a toolbar configuration for the bookmark block type
const customBookmarkToolbarConfig = {
  actions: [
    ...
  ]
} as const satisfies ToolbarModuleConfig;

// Adds it into the editor's config
 ToolbarModuleExtension({
    id: BlockFlavourIdentifier('custom:affine:bookmark'),
    config: customBookmarkToolbarConfig,
 }),
```

4. If we want to extend the global:

```ts
// Defines a toolbar configuration
const customWildcardToolbarConfig = {
  actions: [
    ...
  ]
} as const satisfies ToolbarModuleConfig;

// Adds it into the editor's config
 ToolbarModuleExtension({
    id: BlockFlavourIdentifier('custom:affine:*'),
    config: customWildcardToolbarConfig,
 }),
```

Currently, only most toolbars in page mode have been refactored. Next is edgeless mode.
2025-03-06 06:46:03 +00:00
Saul-Mirone ce87dcf58e feat(editor): schema extension (#10447)
1. **Major Architectural Change: Schema Management**
   - Moved from `workspace.schema` to `store.schema` throughout the codebase
   - Removed schema property from Workspace and Doc interfaces
   - Added `BlockSchemaExtension` pattern across multiple block types

2. **Block Schema Extensions Added**
   - Added new `BlockSchemaExtension` to numerous block types including:
     - DataView, Surface, Attachment, Bookmark, Code
     - Database, Divider, EdgelessText, Embed blocks (Figma, Github, HTML, etc.)
     - Frame, Image, Latex, List, Note, Paragraph
     - Root, Surface Reference, Table blocks

3. **Import/Export System Updates**
   - Updated import functions to accept `schema` parameter:
     - `importHTMLToDoc`
     - `importHTMLZip`
     - `importMarkdownToDoc`
     - `importMarkdownZip`
     - `importNotionZip`
   - Modified export functions to use new schema pattern

4. **Test Infrastructure Updates**
   - Updated test files to use new schema extensions
   - Modified test document creation to include schema extensions
   - Removed direct schema registration in favor of extensions

5. **Service Layer Changes**
   - Updated various services to use `getAFFiNEWorkspaceSchema()`
   - Modified transformer initialization to use document schema
   - Updated collection initialization patterns

6. **Version Management**
   - Removed version-related properties and methods from:
     - `WorkspaceMetaImpl`
     - `TestMeta`
     - `DocImpl`
   - Removed `blockVersions` and `workspaceVersion/pageVersion`

7. **Store and Extension Updates**
   - Added new store extensions and adapters
   - Updated store initialization patterns
   - Added new schema-related functionality in store extension

This PR represents a significant architectural shift in how schemas are managed, moving from a workspace-centric to a store-centric approach, while introducing a more extensible block schema system through `BlockSchemaExtension`. The changes touch multiple layers of the application including core functionality, services, testing infrastructure, and import/export capabilities.
2025-02-26 11:31:29 +00:00
Saul-Mirone dbf0f9dc20 refactor(editor): remove global types in edgeless (#10092)
Closes: [BS-2553](https://linear.app/affine-design/issue/BS-2553/remove-global-types-in-edgeless)
2025-02-11 12:09:44 +00:00
Saul-Mirone 39eb8625d6 refactor(editor): remove block models global type (#10086) 2025-02-11 11:00:57 +00:00
fundon d03744688b refactor(editor): move embed-card-modal to components (#10037) 2025-02-10 10:56:13 +00:00
Mirone 30200ff86d chore: merge blocksuite source code (#9213) 2024-12-20 15:38:06 +08:00