feat(core): web-clipper integration entrance (#11883)

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

## Summary by CodeRabbit

- **New Features**
  - Introduced a new "Web Clipper" integration, allowing users to import web pages into AFFiNE via a direct link to the Chrome Web Store.
- **Improvements**
  - Integration cards can now display as clickable links or settings panels, depending on the integration.
- **Localization**
  - Added English translations for the "Web Clipper" integration name and description.
- **Style**
  - Improved card styling to ensure consistent text color, including for visited links.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
CatsJuice
2025-04-25 02:09:20 +00:00
parent a46f1a3adf
commit 4b7fddc32c
6 changed files with 69 additions and 14 deletions

View File

@@ -13,6 +13,12 @@ export const card = style({
flexDirection: 'column',
background: cssVarV2.layer.background.overlayPanel,
cursor: 'pointer',
color: 'unset',
selectors: {
'&:visited': {
color: 'unset',
},
},
});
export const cardHeader = style({
display: 'flex',

View File

@@ -14,9 +14,22 @@ import {
export const IntegrationCard = ({
className,
link,
...props
}: HTMLAttributes<HTMLDivElement>) => {
return <div className={clsx(className, card)} {...props} />;
}: HTMLAttributes<HTMLElement> & {
link?: string;
}) => {
return link ? (
<a
className={clsx(className, card)}
{...props}
href={link}
target="_blank"
rel="noreferrer"
/>
) : (
<div className={clsx(className, card)} {...props} />
);
};
export const IntegrationCardIcon = ({

View File

@@ -1,20 +1,26 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { IntegrationTypeIcon } from '@affine/core/modules/integration';
import type { I18nString } from '@affine/i18n';
import { TodayIcon } from '@blocksuite/icons/rc';
import { Logo1Icon, TodayIcon } from '@blocksuite/icons/rc';
import { LiveData } from '@toeverything/infra';
import type { ReactNode } from 'react';
import { CalendarSettingPanel } from './calendar/setting-panel';
import { ReadwiseSettingPanel } from './readwise/setting-panel';
interface IntegrationCard {
type IntegrationCard = {
id: string;
name: I18nString;
desc: I18nString;
icon: ReactNode;
setting: ReactNode;
}
} & (
| {
setting: ReactNode;
}
| {
link: string;
}
);
const INTEGRATION_LIST = [
{
@@ -31,6 +37,13 @@ const INTEGRATION_LIST = [
icon: <TodayIcon />,
setting: <CalendarSettingPanel />,
},
{
id: 'web-clipper' as const,
name: 'com.affine.integration.web-clipper.name',
desc: 'com.affine.integration.web-clipper.desc',
icon: <Logo1Icon />,
link: 'https://chromewebstore.google.com/detail/affine-web-clipper/mpbbkmbdpleomiogkbkkpfoljjpahmoi',
},
] satisfies (IntegrationCard | false)[];
type IntegrationId = Exclude<

View File

@@ -2,7 +2,7 @@ import { SettingHeader } from '@affine/component/setting-components';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { type ReactNode, useMemo, useState } from 'react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { SubPageProvider, useSubPageIsland } from '../../sub-page';
import {
@@ -11,6 +11,7 @@ import {
IntegrationCardHeader,
} from './card';
import { getAllowedIntegrationList$ } from './constants';
import { type IntegrationItem } from './constants';
import { list } from './index.css';
export const IntegrationSetting = () => {
@@ -25,6 +26,12 @@ export const IntegrationSetting = () => {
)
);
const handleCardClick = useCallback((card: IntegrationItem) => {
if ('setting' in card && card.setting) {
setOpened(card.id);
}
}, []);
return (
<>
<SettingHeader
@@ -49,16 +56,22 @@ export const IntegrationSetting = () => {
: t[item.desc.i18nKey]();
return (
<li key={item.id}>
<IntegrationCard onClick={() => setOpened(item.id)}>
<IntegrationCard
onClick={() => handleCardClick(item)}
link={'link' in item ? item.link : undefined}
>
<IntegrationCardHeader icon={item.icon} title={title} />
<IntegrationCardContent desc={desc} />
</IntegrationCard>
<IntegrationSettingPage
open={opened === item.id}
onClose={() => setOpened(null)}
>
{item.setting}
</IntegrationSettingPage>
{'setting' in item && item.setting ? (
<IntegrationSettingPage
open={opened === item.id}
onClose={() => setOpened(null)}
>
{item.setting}
</IntegrationSettingPage>
) : null}
</li>
);
})}

View File

@@ -7415,6 +7415,14 @@ export function useAFFiNEI18N(): {
* `Integrations`
*/
["com.affine.integration.integrations"](): string;
/**
* `Web Clipper`
*/
["com.affine.integration.web-clipper.name"](): string;
/**
* `Import web pages to AFFiNE`
*/
["com.affine.integration.web-clipper.desc"](): string;
/**
* `Elevate your AFFiNE experience with diverse add-ons and seamless integrations.`
*/

View File

@@ -1854,6 +1854,8 @@
"com.affine.failed-to-send-request.description": "Unable to process your request to join <1/> <2>{{workspaceName}}</2> with <3>{{userEmail}}</3>, the workspace has reached its member limit. Please contact the workspace owner for available seats.",
"com.affine.integration.name.readwise": "Readwise",
"com.affine.integration.integrations": "Integrations",
"com.affine.integration.web-clipper.name": "Web Clipper",
"com.affine.integration.web-clipper.desc": "Import web pages to AFFiNE",
"com.affine.integration.setting.description": "Elevate your AFFiNE experience with diverse add-ons and seamless integrations.",
"com.affine.integration.setting.learn": "Learn how to develop a integration for AFFiNE",
"com.affine.integration.readwise.name": "Readwise",