Skip to content

Commit

Permalink
feat(core): doc database properties
Browse files Browse the repository at this point in the history
  • Loading branch information
pengx17 committed Oct 17, 2024
1 parent 700e2b5 commit 876b146
Show file tree
Hide file tree
Showing 16 changed files with 510 additions and 16 deletions.
3 changes: 2 additions & 1 deletion packages/frontend/component/src/theme/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions packages/frontend/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 19 additions & 12 deletions packages/frontend/core/src/components/doc-properties/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -443,18 +444,24 @@ DocPropertiesTableBody.displayName = 'PagePropertiesTableBody';
const DocPropertiesTableInner = () => {
const [expanded, setExpanded] = useState(false);
return (
<div className={styles.root}>
<Collapsible.Root
open={expanded}
onOpenChange={setExpanded}
className={styles.rootCentered}
>
<DocPropertiesTableHeader open={expanded} onOpenChange={setExpanded} />
<Collapsible.Content asChild>
<DocPropertiesTableBody />
</Collapsible.Content>
</Collapsible.Root>
</div>
<>
<div className={styles.root}>
<Collapsible.Root
open={expanded}
onOpenChange={setExpanded}
className={styles.rootCentered}
>
<DocPropertiesTableHeader
open={expanded}
onOpenChange={setExpanded}
/>
<Collapsible.Content asChild>
<DocPropertiesTableBody />
</Collapsible.Content>
</Collapsible.Root>
</div>
<DocDatabaseBacklinkInfo />
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DocCustomPropertyInfo } from '@toeverything/infra';

export interface PropertyValueProps {
propertyInfo: DocCustomPropertyInfo;
propertyInfo?: DocCustomPropertyInfo;
value: any;
onChange: (value: any) => void;
}
16 changes: 14 additions & 2 deletions packages/frontend/core/src/modules/doc-info/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<DatabaseValueCell | undefined>(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<DatabaseRow | undefined>(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)))
);
}
}
29 changes: 29 additions & 0 deletions packages/frontend/core/src/modules/doc-info/types.ts
Original file line number Diff line number Diff line change
@@ -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<DocCustomPropertyInfo, 'id' | 'name' | 'type' | 'additionalData'>
>;

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;
}
35 changes: 35 additions & 0 deletions packages/frontend/core/src/modules/doc-info/utils.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
subscribe: (fn: (value: T) => void) => () => void;
}

export function signalToObservable<T>(
signal: ReadonlySignal<T>
): Observable<T> {
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]);
}
Original file line number Diff line number Diff line change
@@ -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 (
<CheckboxValue
// todo(pengx17): better internal impl
value={cell.value ? 'true' : 'false'}
onChange={v => {
dataSource.cellValueChange(rowId, cell.property.id, v === 'true');
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<React.SVGProps<SVGSVGElement>>;
Renderer: React.FC<DatabaseCellRendererProps>;
name: I18nString;
}
>;

export const isSupportedDatabaseRendererType = (type?: string): boolean => {
return type ? type in DatabaseRendererTypes : false;
};
Loading

0 comments on commit 876b146

Please sign in to comment.