Commit Graph

350 Commits

Author SHA1 Message Date
fengmk2 8880cef20b test(infra): add check job to verify committed changes (#10829) 2025-03-14 02:58:27 +00:00
Saul-Mirone 05f3069efd feat(editor): add i18n support for block meta display (#10831) 2025-03-13 11:28:56 +00:00
JimmFly 0c9591f08e feat(core): add an entry for admin panel (#10813)
![CleanShot 2025-03-13 at 13 02 25@2x](https://github.com/user-attachments/assets/82f50a5b-f079-4c64-a3fa-6554735bea82)
2025-03-13 10:46:26 +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
EYHN ea07aa8607 feat(core): add notification list (#10480) 2025-03-11 06:23:33 +00:00
CatsJuice 9f3580263a feat(core): support removing journal template from right sidebar (#10571) 2025-03-07 12:32:42 +00:00
forehalo e02fb4fa94 refactor(core): standardize frontend error handling (#10667) 2025-03-06 13:10:18 +00:00
fundon 93920f9895 fix(editor): circular dependencies (#10661)
Closes: [BS-2766](https://linear.app/affine-design/issue/BS-2766/把-converttodatabase-移到-root-block-中,避免循环依赖)
2025-03-06 09:41:01 +00:00
Flrande bd62634a76 feat(editor): add callout block (#10563)
- Add `CalloutBlockModel `
- Implement `CalloutBlockComponent `
- Integrate with slash menu (/)
2025-03-05 09:28:51 +00:00
EYHN 61635aa77a feat(core): add clipper import interface (#10619) 2025-03-05 04:22:03 +00:00
Saul-Mirone fdde818ddd feat(editor): add block meta feature flag (#10548) 2025-03-03 01:45:33 +00:00
liuyi e5e5c0a8ba perf(core): only full sync before exporting (#10408) 2025-02-25 04:41:56 +00:00
CatsJuice 8b4175c44d chore(core): update free pricing plan description (#10393) 2025-02-24 07:37:30 +00:00
Cats Juice 09cc5c392b refactor(core): new workspace selector and create dialog (#10323) 2025-02-21 17:16:38 +08:00
DarkSky fa86f71853 feat(server): client version check (#9205)
Co-authored-by: forehalo <forehalo@gmail.com>
2025-02-20 15:50:22 +08:00
github-actions[bot] a0e3f9909c chore(i18n): sync translations (#10184)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-20 02:25:19 +00:00
forehalo 02f567f2c0 chore(core): better doc action error message (#10288) 2025-02-19 10:38:38 +00:00
pengx17 78346be4fe fix(electron): update settings styles (#10193) 2025-02-18 09:54:59 +00:00
L-Sun e639f08b71 chore(editor): remove page block feature flag (#10251) 2025-02-18 09:25:05 +00:00
forehalo fdddaf651f chore(core): adjust selfhost sign in ui (#10226)
close AF-2227
2025-02-18 03:38:01 +00:00
JimmFly 9048b38069 fix(core): adjust share menu (#10164)
close AF-2218 AF-2215 AF-2221
2025-02-14 10:32:12 +00:00
forehalo cc6fdef10e chore(electron): remove offline mode (#10152)
close AF-2177
2025-02-14 10:04:01 +00:00
pengx17 f20e3f6d8f feat(electron): show a warning for drop folder to split view (#10178)
fix PD-2310

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/811df8d5-f424-4135-ad90-30135b299f12.png)
2025-02-14 06:42:56 +00:00
github-actions[bot] e4e06f35bb chore(i18n): sync translations (#10026)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-12 12:18:24 +00:00
pengx17 c5c59ba00a fix(core): adjust settings names (#10061)
fix AF-2190
2025-02-11 02:14:10 +00:00
Oleg d4f0c53a0c feat(core): frame editor settings (#9970)
Co-authored-by: L-Sun <zover.v@gmail.com>
Co-authored-by: Mirone <Saul-Mirone@outlook.com>
2025-02-10 15:23:32 +08:00
EYHN 92f4f0c2d9 feat(core): guard service (#9816) 2025-02-09 23:26:38 +00:00
JimmFly 5ae5fd88f1 feat(core): add doc grant feature to share menu (#9672) 2025-02-07 13:05:58 +00:00
donteatfriedrice b38389c670 feat(core): add tooltip for import dialog snapshot item (#10013)
[PD-2132](https://linear.app/affine-design/issue/PD-2132/import-导入优化:snapshot-需要一个问号,解释文档)
2025-02-07 10:23:59 +00:00
JimmFly d5a626d9c3 feat(core): optimize team workspace member management (#9737)
close AF-2106 AF-2077 AF-2089

feat(core): handle need more seat status

feat(core): prevent invite members when team plan is canceled
2025-02-07 10:08:00 +00:00
CatsJuice b4f078b9c9 feat(core): add deleted state for sidebar journal template (#9980) 2025-02-07 09:09:58 +00:00
JimmFly e68bdbde3e feat(core): add self host team plan (#9569) 2025-02-07 03:35:24 +00:00
pengx17 86de184593 fix(electron): split view drag indicator tooltip (#9979)
fix PD-2244
2025-02-06 16:14:54 +00:00
forehalo 4ed03c9f0e feat(i18n): introduce server error i18n (#9953)
close AF-2054
2025-02-05 12:30:19 +00:00
L-Sun 3f4311ff1c chore(editor): add RTL experiement flag for editor (#9882) 2025-01-24 08:23:24 +00:00
pengx17 6ac6a8d6d6 feat(core): unused blob management in settings (#9795)
fix AF-2144, PD-2064, PD-2065, PD-2066
2025-01-23 07:12:17 +00:00
pengx17 088ae0ac0a feat(electron): backup panel (#9738)
fix PD-2071, PD-2059, PD-2069, PD-2068
2025-01-22 22:52:31 +08:00
github-actions[bot] 795c5c9a8c chore(i18n): sync translations (#9846)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: liuyi <forehalo@gmail.com>
2025-01-22 09:16:53 +00:00
forehalo 5828eb53b6 feat(core): support one time password (#9798) 2025-01-22 07:33:10 +00:00
github-actions[bot] a5025cf470 chore(i18n): sync translations (#9842)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: liuyi <forehalo@gmail.com>
2025-01-22 06:41:39 +00:00
pengx17 defb0de4dd feat(core): reorg workspace settings (#9718)
fix AF-2118
2025-01-22 03:11:27 +00:00
github-actions[bot] 24cd7e367c chore(i18n): sync translations (#9741)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: liuyi <forehalo@gmail.com>
2025-01-17 07:24:27 +00:00
EYHN a2ffdb4047 feat(core): new worker workspace engine (#9257) 2025-01-16 16:22:18 +00:00
CatsJuice ad86292f45 test(core): add basic tests for template doc setting (#9702)
close AF-2112, AF-2114, AF-2115
2025-01-16 02:28:29 +00:00
L-Sun 9df338274d chore(core): update tooltip of mode switcher (#9711)
Close [BS-2325](https://linear.app/affine-design/issue/BS-2325/tooltips-显示)

https://github.com/user-attachments/assets/bd4c6045-8de1-4e72-87ab-2d1d3262e6b7
2025-01-15 12:22:37 +00:00
L-Sun 94c9717a35 feat(editor): edgeless page block toolbar (#9707)
Close [BS-2315](https://linear.app/affine-design/issue/BS-2315/page-block-header)

### What Changes
- Add header toolbar to page block (the first note in canvas)
- Add e2e tests
- Add some edgeless e2e test utils.  **The package `@blocksuite/affine` was added to `"@affine-test/kit"`**
2025-01-15 12:04:44 +00:00
CatsJuice d82951a7c5 feat(core): sidebar journal panel template onboarding and setting (#9680)
close AF-2108
2025-01-15 11:21:18 +00:00
CatsJuice 419611f44c feat(core): sidebar template doc entrance (#9676)
close AF-2048, AF-2050, AF-2049
2025-01-15 03:45:10 +00:00
CatsJuice 1c90747899 feat(core): template doc list empty ui (#9675)
close AF-2109
2025-01-15 03:45:09 +00:00
CatsJuice af3ad4bfe9 feat(core): template doc setting ui (#9658)
close AF-2051, AF-2052
2025-01-14 02:10:35 +00:00