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
@@ -13,6 +13,12 @@ export const card = style({
flexDirection: 'column', flexDirection: 'column',
background: cssVarV2.layer.background.overlayPanel, background: cssVarV2.layer.background.overlayPanel,
cursor: 'pointer', cursor: 'pointer',
color: 'unset',
selectors: {
'&:visited': {
color: 'unset',
},
},
}); });
export const cardHeader = style({ export const cardHeader = style({
display: 'flex', display: 'flex',
@@ -14,9 +14,22 @@ import {
export const IntegrationCard = ({ export const IntegrationCard = ({
className, className,
link,
...props ...props
}: HTMLAttributes<HTMLDivElement>) => { }: HTMLAttributes<HTMLElement> & {
return <div className={clsx(className, card)} {...props} />; 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 = ({ export const IntegrationCardIcon = ({
@@ -1,20 +1,26 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { IntegrationTypeIcon } from '@affine/core/modules/integration'; import { IntegrationTypeIcon } from '@affine/core/modules/integration';
import type { I18nString } from '@affine/i18n'; 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 { LiveData } from '@toeverything/infra';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { CalendarSettingPanel } from './calendar/setting-panel'; import { CalendarSettingPanel } from './calendar/setting-panel';
import { ReadwiseSettingPanel } from './readwise/setting-panel'; import { ReadwiseSettingPanel } from './readwise/setting-panel';
interface IntegrationCard { type IntegrationCard = {
id: string; id: string;
name: I18nString; name: I18nString;
desc: I18nString; desc: I18nString;
icon: ReactNode; icon: ReactNode;
setting: ReactNode; } & (
} | {
setting: ReactNode;
}
| {
link: string;
}
);
const INTEGRATION_LIST = [ const INTEGRATION_LIST = [
{ {
@@ -31,6 +37,13 @@ const INTEGRATION_LIST = [
icon: <TodayIcon />, icon: <TodayIcon />,
setting: <CalendarSettingPanel />, 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)[]; ] satisfies (IntegrationCard | false)[];
type IntegrationId = Exclude< type IntegrationId = Exclude<
@@ -2,7 +2,7 @@ import { SettingHeader } from '@affine/component/setting-components';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; 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 { SubPageProvider, useSubPageIsland } from '../../sub-page';
import { import {
@@ -11,6 +11,7 @@ import {
IntegrationCardHeader, IntegrationCardHeader,
} from './card'; } from './card';
import { getAllowedIntegrationList$ } from './constants'; import { getAllowedIntegrationList$ } from './constants';
import { type IntegrationItem } from './constants';
import { list } from './index.css'; import { list } from './index.css';
export const IntegrationSetting = () => { 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 ( return (
<> <>
<SettingHeader <SettingHeader
@@ -49,16 +56,22 @@ export const IntegrationSetting = () => {
: t[item.desc.i18nKey](); : t[item.desc.i18nKey]();
return ( return (
<li key={item.id}> <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} /> <IntegrationCardHeader icon={item.icon} title={title} />
<IntegrationCardContent desc={desc} /> <IntegrationCardContent desc={desc} />
</IntegrationCard> </IntegrationCard>
<IntegrationSettingPage
open={opened === item.id} {'setting' in item && item.setting ? (
onClose={() => setOpened(null)} <IntegrationSettingPage
> open={opened === item.id}
{item.setting} onClose={() => setOpened(null)}
</IntegrationSettingPage> >
{item.setting}
</IntegrationSettingPage>
) : null}
</li> </li>
); );
})} })}
+8
View File
@@ -7415,6 +7415,14 @@ export function useAFFiNEI18N(): {
* `Integrations` * `Integrations`
*/ */
["com.affine.integration.integrations"](): string; ["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.` * `Elevate your AFFiNE experience with diverse add-ons and seamless integrations.`
*/ */
@@ -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.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.name.readwise": "Readwise",
"com.affine.integration.integrations": "Integrations", "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.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.setting.learn": "Learn how to develop a integration for AFFiNE",
"com.affine.integration.readwise.name": "Readwise", "com.affine.integration.readwise.name": "Readwise",