diff --git a/packages/frontend/component/src/theme/global.css b/packages/frontend/component/src/theme/global.css
index bf67aaeb41346..636a0cba77d03 100644
--- a/packages/frontend/component/src/theme/global.css
+++ b/packages/frontend/component/src/theme/global.css
@@ -282,7 +282,8 @@ body {
/**
* A hack to make the anchor wrapper not affect the layout of the page.
*/
-[data-lit-react-wrapper] {
+[data-lit-react-wrapper],
+affine-lit-template-wrapper {
display: contents;
}
diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json
index d1c3be3c81a5a..b8915193cfcbe 100644
--- a/packages/frontend/core/package.json
+++ b/packages/frontend/core/package.json
@@ -17,6 +17,7 @@
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "0.17.18",
+ "@blocksuite/data-view": "0.17.18",
"@blocksuite/icons": "2.1.68",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
diff --git a/packages/frontend/core/src/components/doc-properties/table.tsx b/packages/frontend/core/src/components/doc-properties/table.tsx
index c27501d363616..d3541afa88f4d 100644
--- a/packages/frontend/core/src/components/doc-properties/table.tsx
+++ b/packages/frontend/core/src/components/doc-properties/table.tsx
@@ -10,6 +10,7 @@ import {
useDraggable,
useDropTarget,
} from '@affine/component';
+import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
import { DocLinksService } from '@affine/core/modules/doc-link';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { WorkbenchService } from '@affine/core/modules/workbench';
@@ -443,18 +444,24 @@ DocPropertiesTableBody.displayName = 'PagePropertiesTableBody';
const DocPropertiesTableInner = () => {
const [expanded, setExpanded] = useState(false);
return (
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/packages/frontend/core/src/components/doc-properties/types/types.ts b/packages/frontend/core/src/components/doc-properties/types/types.ts
index 5eda8c8c89f56..93cbd7190f957 100644
--- a/packages/frontend/core/src/components/doc-properties/types/types.ts
+++ b/packages/frontend/core/src/components/doc-properties/types/types.ts
@@ -1,7 +1,7 @@
import type { DocCustomPropertyInfo } from '@toeverything/infra';
export interface PropertyValueProps {
- propertyInfo: DocCustomPropertyInfo;
+ propertyInfo?: DocCustomPropertyInfo;
value: any;
onChange: (value: any) => void;
}
diff --git a/packages/frontend/core/src/modules/doc-info/index.ts b/packages/frontend/core/src/modules/doc-info/index.ts
index 14d8afa3df581..01805077da64d 100644
--- a/packages/frontend/core/src/modules/doc-info/index.ts
+++ b/packages/frontend/core/src/modules/doc-info/index.ts
@@ -1,10 +1,22 @@
-import { type Framework, WorkspaceScope } from '@toeverything/infra';
+import {
+ DocsService,
+ type Framework,
+ WorkspaceScope,
+} from '@toeverything/infra';
+import { DocsSearchService } from '../docs-search';
import { DocInfoModal } from './entities/modal';
+import { DocDatabaseBacklinksService } from './services/doc-database-backlinks';
import { DocInfoService } from './services/doc-info';
+export { DocDatabaseBacklinkInfo } from './views/database-properties/doc-database-backlink-info';
+
export { DocInfoService };
export function configureDocInfoModule(framework: Framework) {
- framework.scope(WorkspaceScope).service(DocInfoService).entity(DocInfoModal);
+ framework
+ .scope(WorkspaceScope)
+ .service(DocInfoService)
+ .service(DocDatabaseBacklinksService, [DocsService, DocsSearchService])
+ .entity(DocInfoModal);
}
diff --git a/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts b/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts
new file mode 100644
index 0000000000000..b95effb054a89
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts
@@ -0,0 +1,136 @@
+import {
+ DatabaseBlockDataSource,
+ type DatabaseBlockModel,
+} from '@blocksuite/affine/blocks';
+import type { DocsService } from '@toeverything/infra';
+import { Service } from '@toeverything/infra';
+import { combineLatest, map, Observable, switchMap } from 'rxjs';
+
+import type { Backlink } from '../../doc-link';
+import type { DocsSearchService } from '../../docs-search';
+import type { DatabaseRow, DatabaseValueCell } from '../types';
+import { signalToObservable } from '../utils';
+
+export class DocDatabaseBacklinksService extends Service {
+ constructor(
+ private readonly docsService: DocsService,
+ private readonly docsSearchService: DocsSearchService
+ ) {
+ super();
+ }
+
+ private async ensureDocLoaded(docId: string) {
+ const docRef = this.docsService.open(docId);
+ if (!docRef.doc.blockSuiteDoc.ready) {
+ docRef.doc.blockSuiteDoc.load();
+ }
+ docRef.doc.setPriorityLoad(10);
+ await docRef.doc.waitForSyncReady();
+ return docRef;
+ }
+
+ private watchRowCells$(dbModel: DatabaseBlockModel, rowId: string) {
+ const dataSource = new DatabaseBlockDataSource(dbModel);
+
+ console.log('watchRowCells$', dataSource.propertyMetas);
+
+ const hydratedRows$ = combineLatest([
+ signalToObservable(dataSource.rows$),
+ signalToObservable(dataSource.properties$),
+ ]).pipe(
+ map(([rowIds, propertyIds]) => {
+ const rowExists = rowIds.some(id => id === rowId);
+ if (!rowExists) {
+ return undefined;
+ }
+ return propertyIds
+ .map(id => {
+ const type = dataSource.propertyTypeGet(id);
+ return {
+ value: dataSource.cellValueGet(rowId, id),
+ property: {
+ id,
+ type,
+ name: dataSource.propertyNameGet(id),
+ additionalData: dataSource.propertyDataGet(id),
+ },
+ };
+ })
+ .filter((p: any): p is DatabaseValueCell => !!p)
+ .toSorted((a, b) =>
+ (a.property.name ?? '').localeCompare(b.property.name ?? '')
+ );
+ })
+ );
+
+ return [hydratedRows$, dataSource] as const;
+ }
+
+ // for each backlink,
+ // 1. check if it is in a database block
+ // 2. if it is, return the related database row
+ // 3. if it is not, return undefined
+ private watchDatabaseRow$(backlink: Backlink) {
+ return new Observable(subscriber => {
+ let disposed = false;
+ let unsubscribe = () => {};
+ const docRef = this.docsService.open(backlink.docId);
+ const run = async () => {
+ await this.ensureDocLoaded(backlink.docId);
+ if (disposed) {
+ return;
+ }
+ const block = docRef.doc.blockSuiteDoc.getBlock(backlink.blockId);
+ const parent = block?.model.parent;
+ if (parent?.flavour === 'affine:database') {
+ const dbModel = parent as DatabaseBlockModel;
+ const [cells$, dataSource] = this.watchRowCells$(
+ dbModel,
+ backlink.blockId
+ );
+ const subscription = cells$.subscribe(cells => {
+ if (cells) {
+ subscriber.next({
+ cells,
+ id: backlink.blockId,
+ doc: docRef.doc,
+ docId: backlink.docId,
+ databaseId: dbModel.id,
+ databaseName: dbModel.title.yText.toString(),
+ dataSource: dataSource,
+ });
+ } else {
+ subscriber.next(undefined);
+ }
+ });
+ unsubscribe = () => subscription.unsubscribe();
+ }
+ };
+
+ run().catch(e => {
+ console.error(`failed to get database info:`, e);
+ docRef.release();
+ });
+
+ return () => {
+ docRef.release();
+ disposed = true;
+ unsubscribe();
+ };
+ });
+ }
+
+ // backlinks (docid:blockid) -> related db rows (DatabaseRow[])
+ watchDbBacklinkRows$(docId: string) {
+ return this.docsSearchService.watchRefsTo(docId).pipe(
+ switchMap(backlinks => {
+ return combineLatest(
+ backlinks.map(backlink => {
+ return this.watchDatabaseRow$(backlink);
+ })
+ );
+ }),
+ map(rows => rows.filter((row): row is DatabaseRow => Boolean(row)))
+ );
+ }
+}
diff --git a/packages/frontend/core/src/modules/doc-info/types.ts b/packages/frontend/core/src/modules/doc-info/types.ts
new file mode 100644
index 0000000000000..cda4ec953147d
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/types.ts
@@ -0,0 +1,29 @@
+import type { DatabaseBlockDataSource } from '@blocksuite/affine/blocks';
+import type { Doc, DocCustomPropertyInfo } from '@toeverything/infra';
+
+// make database property type to be compatible with DocCustomPropertyInfo
+export type DatabaseProperty = Required<
+ Pick
+>;
+
+export interface DatabaseValueCell {
+ property: DatabaseProperty;
+ value: unknown;
+}
+
+export interface DatabaseRow {
+ cells: DatabaseValueCell[];
+ id: string; // row id (block id)
+ doc: Doc; // the doc that contains the database. required for editing etc.
+ docId: string; // for rendering the doc reference
+ dataSource: DatabaseBlockDataSource;
+ databaseId: string;
+ databaseName: string; // the title
+}
+
+export interface DatabaseCellRendererProps {
+ rowId: string;
+ cell: DatabaseValueCell;
+ dataSource: DatabaseBlockDataSource;
+ doc: Doc;
+}
diff --git a/packages/frontend/core/src/modules/doc-info/utils.ts b/packages/frontend/core/src/modules/doc-info/utils.ts
new file mode 100644
index 0000000000000..08a90521d4020
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/utils.ts
@@ -0,0 +1,35 @@
+import { BlockStdScope } from '@blocksuite/affine/block-std';
+import { PageEditorBlockSpecs } from '@blocksuite/affine/blocks';
+import type { Doc } from '@blocksuite/affine/store';
+import { useMemo } from 'react';
+import { Observable } from 'rxjs';
+
+interface ReadonlySignal {
+ subscribe: (fn: (value: T) => void) => () => void;
+}
+
+export function signalToObservable(
+ signal: ReadonlySignal
+): Observable {
+ return new Observable(subscriber => {
+ const unsub = signal.subscribe(value => {
+ subscriber.next(value);
+ });
+ return () => {
+ unsub();
+ };
+ });
+}
+
+// todo(pengx17): use rc pool?
+export function createBlockStdScope(doc: Doc) {
+ const std = new BlockStdScope({
+ doc,
+ extensions: PageEditorBlockSpecs,
+ });
+ return std;
+}
+
+export function useBlockStdScope(doc: Doc) {
+ return useMemo(() => createBlockStdScope(doc), [doc]);
+}
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/checkbox.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/checkbox.tsx
new file mode 100644
index 0000000000000..e6acb309054d6
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/checkbox.tsx
@@ -0,0 +1,19 @@
+import { CheckboxValue } from '@affine/core/components/doc-properties/types/checkbox';
+
+import type { DatabaseCellRendererProps } from '../../../types';
+
+export const CheckboxCell = ({
+ cell,
+ rowId,
+ dataSource,
+}: DatabaseCellRendererProps) => {
+ return (
+ {
+ dataSource.cellValueChange(rowId, cell.property.id, v === 'true');
+ }}
+ />
+ );
+};
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/constant.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/constant.tsx
new file mode 100644
index 0000000000000..913e9dce682ed
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/constant.tsx
@@ -0,0 +1,75 @@
+import type { I18nString } from '@affine/i18n';
+import {
+ CheckBoxCheckLinearIcon,
+ DateTimeIcon,
+ LinkIcon,
+ NumberIcon,
+ ProgressIcon,
+ SelectIcon,
+ TextIcon,
+} from '@blocksuite/icons/rc';
+
+import type { DatabaseCellRendererProps } from '../../../types';
+import { CheckboxCell } from './checkbox';
+import { DateCell } from './date';
+import { LinkCell } from './link';
+import { NumberCell } from './number';
+import { RichTextCell } from './rich-text';
+
+export const DatabaseRendererTypes = {
+ 'rich-text': {
+ icon: TextIcon,
+ Renderer: RichTextCell,
+ name: 'com.affine.page-properties.property.text',
+ },
+ checkbox: {
+ icon: CheckBoxCheckLinearIcon,
+ Renderer: CheckboxCell,
+ name: 'com.affine.page-properties.property.checkbox',
+ },
+ date: {
+ icon: DateTimeIcon,
+ Renderer: DateCell,
+ name: 'com.affine.page-properties.property.date',
+ },
+ number: {
+ icon: NumberIcon,
+ Renderer: NumberCell,
+ name: 'com.affine.page-properties.property.number',
+ },
+ link: {
+ icon: LinkIcon,
+ Renderer: LinkCell,
+ name: 'com.affine.page-properties.property.link',
+ },
+ progress: {
+ icon: ProgressIcon,
+ Renderer: NumberCell,
+ name: 'com.affine.page-properties.property.progress',
+ },
+ select: {
+ icon: SelectIcon,
+ Renderer: ({ cell }) => {
+ return cell.value.toString();
+ },
+ name: 'com.affine.page-properties.property.select',
+ },
+ 'multi-select': {
+ icon: SelectIcon,
+ Renderer: ({ cell }) => {
+ return cell.value.toString();
+ },
+ name: 'com.affine.page-properties.property.multi-select',
+ },
+} as Record<
+ string,
+ {
+ icon: React.FC>;
+ Renderer: React.FC;
+ name: I18nString;
+ }
+>;
+
+export const isSupportedDatabaseRendererType = (type?: string): boolean => {
+ return type ? type in DatabaseRendererTypes : false;
+};
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/date.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/date.tsx
new file mode 100644
index 0000000000000..c1d29c7834fbd
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/date.tsx
@@ -0,0 +1,34 @@
+import { DateValue } from '@affine/core/components/doc-properties/types/date';
+import dayjs from 'dayjs';
+
+import type { DatabaseCellRendererProps } from '../../../types';
+
+const toInternalDateString = (date: unknown) => {
+ if (typeof date !== 'string' && typeof date !== 'number') {
+ return '';
+ }
+ return dayjs(date).format('YYYY-MM-DD');
+};
+
+const fromInternalDateString = (date: string) => {
+ return dayjs(date).toDate().getTime();
+};
+
+export const DateCell = ({
+ cell,
+ rowId,
+ dataSource,
+}: DatabaseCellRendererProps) => {
+ return (
+ {
+ dataSource.cellValueChange(
+ rowId,
+ cell.property.id,
+ fromInternalDateString(v)
+ );
+ }}
+ />
+ );
+};
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/link.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/link.tsx
new file mode 100644
index 0000000000000..054057b6d623d
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/link.tsx
@@ -0,0 +1,16 @@
+import { PropertyValue } from '@affine/component';
+
+import type { DatabaseCellRendererProps } from '../../../types';
+
+export const LinkCell = ({ cell }: DatabaseCellRendererProps) => {
+ const isEmpty = typeof cell.value !== 'string' || !cell.value;
+ const link = cell.value as string;
+ // todo(pengx17): support edit
+ return (
+
+
+ {link.replace(/^https?:\/\//, '')}
+
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/number.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/number.tsx
new file mode 100644
index 0000000000000..699171fc46032
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/number.tsx
@@ -0,0 +1,18 @@
+import { NumberValue } from '@affine/core/components/doc-properties/types/number';
+
+import type { DatabaseCellRendererProps } from '../../../types';
+
+export const NumberCell = ({
+ cell,
+ rowId,
+ dataSource,
+}: DatabaseCellRendererProps) => {
+ return (
+ {
+ dataSource.cellValueChange(rowId, cell.property.id, v);
+ }}
+ />
+ );
+};
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx
new file mode 100644
index 0000000000000..9ec8607058643
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx
@@ -0,0 +1,48 @@
+import { PropertyValue, toReactNode } from '@affine/component';
+import type { BlockStdScope } from '@blocksuite/affine/block-std';
+import { DefaultInlineManagerExtension } from '@blocksuite/affine/blocks';
+import type { Y } from '@blocksuite/store';
+import type { Doc } from '@toeverything/infra';
+import { html } from 'lit';
+
+import type {
+ DatabaseCellRendererProps,
+ DatabaseValueCell,
+} from '../../../types';
+import { useBlockStdScope } from '../../../utils';
+
+const renderRichText = ({
+ cell,
+ doc,
+ std,
+}: {
+ std: BlockStdScope;
+ cell: DatabaseValueCell;
+ doc: Doc;
+}) => {
+ const inlineManager = std.get(DefaultInlineManagerExtension.identifier);
+
+ if (!inlineManager) {
+ return null;
+ }
+
+ return html``;
+};
+
+export const RichTextCell = ({ cell, doc }: DatabaseCellRendererProps) => {
+ const std = useBlockStdScope(doc.blockSuiteDoc);
+ const template = renderRichText({ cell, doc, std });
+ return (
+
+ {template ? toReactNode(template) : null}
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/doc-database-backlink-info.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/doc-database-backlink-info.tsx
new file mode 100644
index 0000000000000..9b68d133ff4b6
--- /dev/null
+++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/doc-database-backlink-info.tsx
@@ -0,0 +1,62 @@
+import { AffinePageReference } from '@affine/core/components/affine/reference-link';
+import {
+ DocService,
+ LiveData,
+ useLiveData,
+ useService,
+} from '@toeverything/infra';
+import { useMemo } from 'react';
+
+import { DocDatabaseBacklinksService } from '../../services/doc-database-backlinks';
+import { DatabaseRendererTypes } from './cells/constant';
+
+export const DocDatabaseBacklinkInfo = () => {
+ const docService = useService(DocService).doc;
+ const docDatabaseBacklinks = useService(DocDatabaseBacklinksService);
+ const rows = useLiveData(
+ useMemo(
+ () =>
+ LiveData.from(
+ docDatabaseBacklinks.watchDbBacklinkRows$(docService.id),
+ []
+ ),
+ [docDatabaseBacklinks, docService.id]
+ )
+ );
+ console.log(rows);
+ return (
+
+
Database Backlinks
+ {rows.map(row => (
+
+ {row.databaseName}
+
+ {row.cells
+ .filter(cell => cell.property.type !== 'title')
+ .map(cell => {
+ const DatabaseRendererConfig =
+ DatabaseRendererTypes[cell.property.type];
+ return (
+ -
+ {cell.property.name}({cell.property.type}):
+ {DatabaseRendererConfig ? (
+
+ ) : (
+ JSON.stringify(cell.value)
+ )}
+
+ );
+ })}
+
+ ))}
+
+ );
+};
diff --git a/yarn.lock b/yarn.lock
index cf27ce3fb42ab..e436d5963e7ef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -365,6 +365,7 @@ __metadata:
"@affine/templates": "workspace:*"
"@affine/track": "workspace:*"
"@blocksuite/affine": "npm:0.17.18"
+ "@blocksuite/data-view": "npm:0.17.18"
"@blocksuite/icons": "npm:2.1.68"
"@dnd-kit/core": "npm:^6.1.0"
"@dnd-kit/modifiers": "npm:^7.0.0"