mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
@@ -15,6 +15,7 @@ import type {
|
|||||||
DatabaseRow,
|
DatabaseRow,
|
||||||
DatabaseValueCell,
|
DatabaseValueCell,
|
||||||
} from '@affine/core/modules/doc-info/types';
|
} from '@affine/core/modules/doc-info/types';
|
||||||
|
import { DocIntegrationPropertiesTable } from '@affine/core/modules/integration';
|
||||||
import { GuardService } from '@affine/core/modules/permissions';
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
@@ -448,6 +449,7 @@ const DocPropertiesTableInner = ({
|
|||||||
onOpenChange={setExpanded}
|
onOpenChange={setExpanded}
|
||||||
/>
|
/>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
|
<DocIntegrationPropertiesTable />
|
||||||
<DocWorkspacePropertiesTableBody
|
<DocWorkspacePropertiesTableBody
|
||||||
defaultOpen={
|
defaultOpen={
|
||||||
!defaultOpenProperty || defaultOpenProperty.type === 'workspace'
|
!defaultOpenProperty || defaultOpenProperty.type === 'workspace'
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { I18nString } from '@affine/i18n';
|
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 { SVGProps } from 'react';
|
||||||
|
|
||||||
import type { IntegrationProperty, IntegrationType } from './type';
|
import type { IntegrationProperty, IntegrationType } from './type';
|
||||||
@@ -16,14 +22,32 @@ export const INTEGRATION_PROPERTY_SCHEMA: {
|
|||||||
} = {
|
} = {
|
||||||
readwise: {
|
readwise: {
|
||||||
author: {
|
author: {
|
||||||
|
order: '400',
|
||||||
label: 'com.affine.integration.readwise-prop.author',
|
label: 'com.affine.integration.readwise-prop.author',
|
||||||
key: 'author',
|
key: 'author',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
icon: TextIcon,
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
|
order: '300',
|
||||||
label: 'com.affine.integration.readwise-prop.source',
|
label: 'com.affine.integration.readwise-prop.source',
|
||||||
key: 'readwise_url',
|
key: 'readwise_url',
|
||||||
type: 'source',
|
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: {},
|
zotero: {},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ReadwiseStore } from './store/readwise';
|
|||||||
|
|
||||||
export { IntegrationService };
|
export { IntegrationService };
|
||||||
export { IntegrationTypeIcon } from './views/icon';
|
export { IntegrationTypeIcon } from './views/icon';
|
||||||
|
export { DocIntegrationPropertiesTable } from './views/properties-table';
|
||||||
|
|
||||||
export function configureIntegrationModule(framework: Framework) {
|
export function configureIntegrationModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
|
|||||||
@@ -9,10 +9,18 @@ export class IntegrationPropertyService extends Service {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integrationType$ = this.docService.doc.properties$.selector(
|
||||||
|
p => p.integrationType
|
||||||
|
);
|
||||||
|
|
||||||
schema$ = this.docService.doc.properties$
|
schema$ = this.docService.doc.properties$
|
||||||
.selector(p => p.integrationType)
|
.selector(p => p.integrationType)
|
||||||
.map(type => (type ? INTEGRATION_PROPERTY_SCHEMA[type] : null));
|
.map(type => (type ? INTEGRATION_PROPERTY_SCHEMA[type] : null));
|
||||||
|
|
||||||
|
integrationProperty$(
|
||||||
|
type: IntegrationType,
|
||||||
|
key: string
|
||||||
|
): LiveData<Record<string, any> | undefined | null>;
|
||||||
integrationProperty$<
|
integrationProperty$<
|
||||||
T extends IntegrationType,
|
T extends IntegrationType,
|
||||||
Key extends keyof IntegrationDocPropertiesMap[T],
|
Key extends keyof IntegrationDocPropertiesMap[T],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { I18nString } from '@affine/i18n';
|
import type { I18nString } from '@affine/i18n';
|
||||||
|
import type { ComponentType, SVGProps } from 'react';
|
||||||
|
|
||||||
import type { DocIntegrationRef } from '../db/schema/schema';
|
import type { DocIntegrationRef } from '../db/schema/schema';
|
||||||
|
|
||||||
@@ -11,8 +12,10 @@ export type IntegrationDocPropertiesMap = {
|
|||||||
|
|
||||||
export type IntegrationProperty<T extends IntegrationType> = {
|
export type IntegrationProperty<T extends IntegrationType> = {
|
||||||
key: keyof IntegrationDocPropertiesMap[T];
|
key: keyof IntegrationDocPropertiesMap[T];
|
||||||
label?: I18nString;
|
label: I18nString;
|
||||||
type: 'link' | 'text' | 'date' | 'source';
|
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;
|
||||||
|
}
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
"ar": 95,
|
"ar": 95,
|
||||||
"ca": 4,
|
"ca": 4,
|
||||||
"da": 5,
|
"da": 5,
|
||||||
"de": 96,
|
"de": 95,
|
||||||
"el-GR": 95,
|
"el-GR": 95,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-AR": 96,
|
"es-AR": 95,
|
||||||
"es-CL": 97,
|
"es-CL": 97,
|
||||||
"es": 95,
|
"es": 95,
|
||||||
"fa": 95,
|
"fa": 95,
|
||||||
"fr": 95,
|
"fr": 95,
|
||||||
"hi": 2,
|
"hi": 2,
|
||||||
"it-IT": 96,
|
"it-IT": 95,
|
||||||
"it": 1,
|
"it": 1,
|
||||||
"ja": 95,
|
"ja": 95,
|
||||||
"ko": 60,
|
"ko": 60,
|
||||||
|
|||||||
@@ -7321,6 +7321,18 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Source`
|
* `Source`
|
||||||
*/
|
*/
|
||||||
["com.affine.integration.readwise-prop.source"](): string;
|
["com.affine.integration.readwise-prop.source"](): string;
|
||||||
|
/**
|
||||||
|
* `Created`
|
||||||
|
*/
|
||||||
|
["com.affine.integration.readwise-prop.created"](): string;
|
||||||
|
/**
|
||||||
|
* `Updated`
|
||||||
|
*/
|
||||||
|
["com.affine.integration.readwise-prop.updated"](): string;
|
||||||
|
/**
|
||||||
|
* `Integration properties`
|
||||||
|
*/
|
||||||
|
["com.affine.integration.properties"](): string;
|
||||||
/**
|
/**
|
||||||
* `Notes`
|
* `Notes`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1823,6 +1823,9 @@
|
|||||||
"com.affine.integration.readwise.import.abort-notify-desc": "Import aborted, with {{finished}} highlights processed",
|
"com.affine.integration.readwise.import.abort-notify-desc": "Import aborted, with {{finished}} highlights processed",
|
||||||
"com.affine.integration.readwise-prop.author": "Author",
|
"com.affine.integration.readwise-prop.author": "Author",
|
||||||
"com.affine.integration.readwise-prop.source": "Source",
|
"com.affine.integration.readwise-prop.source": "Source",
|
||||||
|
"com.affine.integration.readwise-prop.created": "Created",
|
||||||
|
"com.affine.integration.readwise-prop.updated": "Updated",
|
||||||
|
"com.affine.integration.properties": "Integration properties",
|
||||||
"com.affine.attachmentViewer.audio.notes": "Notes",
|
"com.affine.attachmentViewer.audio.notes": "Notes",
|
||||||
"com.affine.attachmentViewer.audio.transcribing": "Transcribing",
|
"com.affine.attachmentViewer.audio.transcribing": "Transcribing",
|
||||||
"error.INTERNAL_SERVER_ERROR": "An internal error occurred.",
|
"error.INTERNAL_SERVER_ERROR": "An internal error occurred.",
|
||||||
|
|||||||
Reference in New Issue
Block a user