mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
@@ -15,6 +15,7 @@ import type {
|
||||
DatabaseRow,
|
||||
DatabaseValueCell,
|
||||
} from '@affine/core/modules/doc-info/types';
|
||||
import { DocIntegrationPropertiesTable } from '@affine/core/modules/integration';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
@@ -448,6 +449,7 @@ const DocPropertiesTableInner = ({
|
||||
onOpenChange={setExpanded}
|
||||
/>
|
||||
<Collapsible.Content>
|
||||
<DocIntegrationPropertiesTable />
|
||||
<DocWorkspacePropertiesTableBody
|
||||
defaultOpen={
|
||||
!defaultOpenProperty || defaultOpenProperty.type === 'workspace'
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { I18nString } from '@affine/i18n';
|
||||
import { ReadwiseLogoDuotoneIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
DateTimeIcon,
|
||||
HistoryIcon,
|
||||
LinkIcon,
|
||||
ReadwiseLogoDuotoneIcon,
|
||||
TextIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
import type { IntegrationProperty, IntegrationType } from './type';
|
||||
@@ -16,14 +22,32 @@ export const INTEGRATION_PROPERTY_SCHEMA: {
|
||||
} = {
|
||||
readwise: {
|
||||
author: {
|
||||
order: '400',
|
||||
label: 'com.affine.integration.readwise-prop.author',
|
||||
key: 'author',
|
||||
type: 'text',
|
||||
icon: TextIcon,
|
||||
},
|
||||
source: {
|
||||
order: '300',
|
||||
label: 'com.affine.integration.readwise-prop.source',
|
||||
key: 'readwise_url',
|
||||
type: 'source',
|
||||
icon: LinkIcon,
|
||||
},
|
||||
created: {
|
||||
order: '100',
|
||||
label: 'com.affine.integration.readwise-prop.created',
|
||||
key: 'created_at',
|
||||
type: 'date',
|
||||
icon: DateTimeIcon,
|
||||
},
|
||||
updated: {
|
||||
order: '200',
|
||||
label: 'com.affine.integration.readwise-prop.updated',
|
||||
key: 'updated_at',
|
||||
type: 'date',
|
||||
icon: HistoryIcon,
|
||||
},
|
||||
},
|
||||
zotero: {},
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ReadwiseStore } from './store/readwise';
|
||||
|
||||
export { IntegrationService };
|
||||
export { IntegrationTypeIcon } from './views/icon';
|
||||
export { DocIntegrationPropertiesTable } from './views/properties-table';
|
||||
|
||||
export function configureIntegrationModule(framework: Framework) {
|
||||
framework
|
||||
|
||||
@@ -9,10 +9,18 @@ export class IntegrationPropertyService extends Service {
|
||||
super();
|
||||
}
|
||||
|
||||
integrationType$ = this.docService.doc.properties$.selector(
|
||||
p => p.integrationType
|
||||
);
|
||||
|
||||
schema$ = this.docService.doc.properties$
|
||||
.selector(p => p.integrationType)
|
||||
.map(type => (type ? INTEGRATION_PROPERTY_SCHEMA[type] : null));
|
||||
|
||||
integrationProperty$(
|
||||
type: IntegrationType,
|
||||
key: string
|
||||
): LiveData<Record<string, any> | undefined | null>;
|
||||
integrationProperty$<
|
||||
T extends IntegrationType,
|
||||
Key extends keyof IntegrationDocPropertiesMap[T],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { I18nString } from '@affine/i18n';
|
||||
import type { ComponentType, SVGProps } from 'react';
|
||||
|
||||
import type { DocIntegrationRef } from '../db/schema/schema';
|
||||
|
||||
@@ -11,8 +12,10 @@ export type IntegrationDocPropertiesMap = {
|
||||
|
||||
export type IntegrationProperty<T extends IntegrationType> = {
|
||||
key: keyof IntegrationDocPropertiesMap[T];
|
||||
label?: I18nString;
|
||||
label: I18nString;
|
||||
type: 'link' | 'text' | 'date' | 'source';
|
||||
icon?: ComponentType<SVGProps<SVGSVGElement>>;
|
||||
order?: string;
|
||||
};
|
||||
|
||||
// ===============================
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
PropertyCollapsibleContent,
|
||||
PropertyCollapsibleSection,
|
||||
PropertyName,
|
||||
PropertyRoot,
|
||||
} from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { IntegrationPropertyService } from '../services/integration-property';
|
||||
import type { IntegrationProperty } from '../type';
|
||||
import { ValueRenderer } from './property-values';
|
||||
|
||||
export const DocIntegrationPropertiesTable = () => {
|
||||
const t = useI18n();
|
||||
const integrationPropertyService = useService(IntegrationPropertyService);
|
||||
|
||||
const integrationType = useLiveData(
|
||||
integrationPropertyService.integrationType$
|
||||
);
|
||||
const schema = useLiveData(integrationPropertyService.schema$);
|
||||
|
||||
const properties = useMemo(
|
||||
() =>
|
||||
(Object.values(schema || {}) as IntegrationProperty<any>[]).sort(
|
||||
(a, b) => {
|
||||
const aOrder = a.order ?? '9999';
|
||||
const bOrder = b.order ?? '9999';
|
||||
return aOrder.localeCompare(bOrder);
|
||||
}
|
||||
),
|
||||
[schema]
|
||||
);
|
||||
|
||||
if (!schema || !integrationType) return null;
|
||||
|
||||
return (
|
||||
<PropertyCollapsibleSection
|
||||
title={t['com.affine.integration.properties']()}
|
||||
>
|
||||
<PropertyCollapsibleContent>
|
||||
{properties.map(property => {
|
||||
const Icon = property.icon;
|
||||
const key = property.key as string;
|
||||
const label = property.label;
|
||||
const displayName =
|
||||
typeof label === 'string'
|
||||
? t[label]()
|
||||
: t.t(label?.i18nKey, label?.options);
|
||||
|
||||
return (
|
||||
<PropertyRoot key={key}>
|
||||
<PropertyName name={displayName} icon={Icon ? <Icon /> : null} />
|
||||
<ValueRenderer
|
||||
integration={integrationType}
|
||||
type={property.type}
|
||||
propertyKey={key}
|
||||
/>
|
||||
</PropertyRoot>
|
||||
);
|
||||
})}
|
||||
</PropertyCollapsibleContent>
|
||||
</PropertyCollapsibleSection>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { PropertyValue, Tooltip } from '@affine/component';
|
||||
import { i18nTime } from '@affine/i18n';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
import type { PropertyValueProps } from './type';
|
||||
|
||||
export const DateValue = ({ value }: PropertyValueProps) => {
|
||||
const accuracySecond = useMemo(() => {
|
||||
return i18nTime(value, { absolute: { accuracy: 'second' } });
|
||||
}, [value]);
|
||||
|
||||
const accuracyDay = useMemo(() => {
|
||||
return i18nTime(value, { absolute: { accuracy: 'day' } });
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<PropertyValue className={styles.value} hoverable={false}>
|
||||
<Tooltip content={accuracySecond} side="right">
|
||||
<span>{accuracyDay}</span>
|
||||
</Tooltip>
|
||||
</PropertyValue>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { type ComponentType, useMemo } from 'react';
|
||||
|
||||
import { IntegrationPropertyService } from '../../services/integration-property';
|
||||
import type { IntegrationProperty } from '../../type';
|
||||
import { DateValue } from './date-value';
|
||||
import { LinkValue } from './link-value';
|
||||
import { SourceValue } from './source-value';
|
||||
import { TextValue } from './text-value';
|
||||
import type { PropertyValueProps } from './type';
|
||||
|
||||
type IntegrationPropertyType = IntegrationProperty<any>['type'];
|
||||
|
||||
const valueRenderers: Record<
|
||||
IntegrationPropertyType,
|
||||
ComponentType<PropertyValueProps>
|
||||
> = {
|
||||
link: LinkValue,
|
||||
source: SourceValue,
|
||||
text: TextValue,
|
||||
date: DateValue,
|
||||
};
|
||||
|
||||
type ValueRendererProps = {
|
||||
type: IntegrationPropertyType;
|
||||
propertyKey: string;
|
||||
} & Pick<PropertyValueProps, 'integration'>;
|
||||
|
||||
export const ValueRenderer = ({
|
||||
integration,
|
||||
type,
|
||||
propertyKey,
|
||||
}: ValueRendererProps) => {
|
||||
const Renderer = valueRenderers[type];
|
||||
|
||||
const integrationPropertyService = useService(IntegrationPropertyService);
|
||||
|
||||
const propertyValue = useLiveData(
|
||||
useMemo(() => {
|
||||
return integrationPropertyService.integrationProperty$(
|
||||
integration,
|
||||
propertyKey
|
||||
);
|
||||
}, [integration, integrationPropertyService, propertyKey])
|
||||
);
|
||||
|
||||
if (!Renderer) {
|
||||
return null;
|
||||
}
|
||||
return <Renderer integration={integration} value={propertyValue} />;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PropertyValue } from '@affine/component';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
import type { PropertyValueProps } from './type';
|
||||
|
||||
export const LinkValue = ({ value }: PropertyValueProps) => {
|
||||
return (
|
||||
<a
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.linkWrapper}
|
||||
>
|
||||
<PropertyValue className={styles.value}>{value}</PropertyValue>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PropertyValue } from '@affine/component';
|
||||
import { DualLinkIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { IntegrationTypeIcon } from '../icon';
|
||||
import * as styles from './styles.css';
|
||||
import type { PropertyValueProps } from './type';
|
||||
|
||||
export const SourceValue = ({ value, integration }: PropertyValueProps) => {
|
||||
return (
|
||||
<a
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.linkWrapper}
|
||||
>
|
||||
<PropertyValue className={styles.sourceValue}>
|
||||
<div className={styles.sourceValueIcon}>
|
||||
<IntegrationTypeIcon type={integration} />
|
||||
</div>
|
||||
{value}
|
||||
<DualLinkIcon className={styles.sourceValueLinkIcon} />
|
||||
</PropertyValue>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const value = style({
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
lineHeight: '22px',
|
||||
color: cssVarV2.text.primary,
|
||||
});
|
||||
|
||||
export const linkWrapper = style({
|
||||
flex: 1,
|
||||
});
|
||||
export const sourceValue = style([
|
||||
value,
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
]);
|
||||
export const sourceValueIcon = style({
|
||||
fontSize: 18,
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 3,
|
||||
boxShadow: `0px 0px 0px 1px ${cssVarV2.layer.insideBorder.border}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: cssVarV2.integrations.background.iconSolid,
|
||||
});
|
||||
export const sourceValueLinkIcon = style({
|
||||
fontSize: 18,
|
||||
color: cssVarV2.icon.primary,
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PropertyValue } from '@affine/component';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
export const TextValue = ({ value }: { value: string }) => {
|
||||
return (
|
||||
<PropertyValue hoverable={false} className={styles.value}>
|
||||
{value}
|
||||
</PropertyValue>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { IntegrationType } from '../../type';
|
||||
|
||||
export interface PropertyValueProps {
|
||||
value: any;
|
||||
integration: IntegrationType;
|
||||
}
|
||||
Reference in New Issue
Block a user