chore(core): tracking events for doc properties (#8651)

fix AF-1565
This commit is contained in:
pengx17 2024-11-05 11:30:57 +00:00
parent 4977055a2e
commit ef82b9d3e7
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
18 changed files with 301 additions and 116 deletions

View File

@ -64,7 +64,7 @@ export class DocPropertiesStore extends Store {
createDocPropertyInfo( createDocPropertyInfo(
config: Omit<DocCustomPropertyInfo, 'id'> & { id?: string } config: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
) { ) {
return this.dbService.db.docCustomPropertyInfo.create(config).id; return this.dbService.db.docCustomPropertyInfo.create(config);
} }
removeDocPropertyInfo(id: string) { removeDocPropertyInfo(id: string) {

View File

@ -3,11 +3,16 @@ import {
useConfirmModal, useConfirmModal,
useLitPortalFactory, useLitPortalFactory,
} from '@affine/component'; } from '@affine/component';
import type {
DatabaseRow,
DatabaseValueCell,
} from '@affine/core/modules/doc-info/types';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { EditorSettingService } from '@affine/core/modules/editor-setting'; import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { JournalService } from '@affine/core/modules/journal'; import { JournalService } from '@affine/core/modules/journal';
import { toURLSearchParams } from '@affine/core/modules/navigation'; import { toURLSearchParams } from '@affine/core/modules/navigation';
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view'; import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
import track from '@affine/track';
import type { DocMode } from '@blocksuite/affine/blocks'; import type { DocMode } from '@blocksuite/affine/blocks';
import { import {
DocTitle, DocTitle,
@ -16,6 +21,7 @@ import {
} from '@blocksuite/affine/presets'; } from '@blocksuite/affine/presets';
import type { Doc } from '@blocksuite/affine/store'; import type { Doc } from '@blocksuite/affine/store';
import { import {
type DocCustomPropertyInfo,
DocService, DocService,
DocsService, DocsService,
FeatureFlagService, FeatureFlagService,
@ -239,6 +245,28 @@ export const BlocksuiteDocEditor = forwardRef<
editorSettingService.editorSetting.settings$.selector(s => s.displayDocInfo) editorSettingService.editorSetting.settings$.selector(s => s.displayDocInfo)
); );
const onPropertyChange = useCallback((property: DocCustomPropertyInfo) => {
track.doc.inlineDocInfo.property.editProperty({
type: property.type,
});
}, []);
const onPropertyAdded = useCallback((property: DocCustomPropertyInfo) => {
track.doc.inlineDocInfo.property.addProperty({
type: property.type,
control: 'at menu',
});
}, []);
const onDatabasePropertyChange = useCallback(
(_row: DatabaseRow, cell: DatabaseValueCell) => {
track.doc.inlineDocInfo.databaseProperty.editProperty({
type: cell.property.type$.value,
});
},
[]
);
return ( return (
<> <>
<div className={styles.affineDocViewport} style={{ height: '100%' }}> <div className={styles.affineDocViewport} style={{ height: '100%' }}>
@ -248,7 +276,12 @@ export const BlocksuiteDocEditor = forwardRef<
<BlocksuiteEditorJournalDocTitle page={page} /> <BlocksuiteEditorJournalDocTitle page={page} />
)} )}
{!shared && displayDocInfo ? ( {!shared && displayDocInfo ? (
<DocPropertiesTable defaultOpenProperty={defaultOpenProperty} /> <DocPropertiesTable
onDatabasePropertyChange={onDatabasePropertyChange}
onPropertyChange={onPropertyChange}
onPropertyAdded={onPropertyAdded}
defaultOpenProperty={defaultOpenProperty}
/>
) : null} ) : null}
<adapted.DocEditor <adapted.DocEditor
className={styles.docContainer} className={styles.docContainer}

View File

@ -1,7 +1,12 @@
import { MenuItem, MenuSeparator } from '@affine/component'; import { MenuItem, MenuSeparator } from '@affine/component';
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name'; import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { DocsService, useLiveData, useService } from '@toeverything/infra'; import {
type DocCustomPropertyInfo,
DocsService,
useLiveData,
useService,
} from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { import {
@ -15,7 +20,7 @@ export const CreatePropertyMenuItems = ({
onCreated, onCreated,
}: { }: {
at?: 'before' | 'after'; at?: 'before' | 'after';
onCreated?: (propertyId: string) => void; onCreated?: (property: DocCustomPropertyInfo) => void;
}) => { }) => {
const t = useI18n(); const t = useI18n();
const docsService = useService(DocsService); const docsService = useService(DocsService);
@ -36,13 +41,13 @@ export const CreatePropertyMenuItems = ({
? generateUniqueNameInSequence(option.name, allNames) ? generateUniqueNameInSequence(option.name, allNames)
: option.name; : option.name;
const uniqueId = typeDefined.uniqueId; const uniqueId = typeDefined.uniqueId;
const newPropertyId = propertyList.createProperty({ const newProperty = propertyList.createProperty({
id: uniqueId, id: uniqueId,
name, name,
type: option.type, type: option.type,
index: propertyList.indexAt(at), index: propertyList.indexAt(at),
}); });
onCreated?.(newPropertyId); onCreated?.(newProperty);
}, },
[at, onCreated, propertyList, properties] [at, onCreated, propertyList, properties]
); );

View File

@ -1,6 +1,7 @@
import { Divider, IconButton, Tooltip } from '@affine/component'; import { Divider, IconButton, Tooltip } from '@affine/component';
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name'; import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { import {
Content as CollapsibleContent, Content as CollapsibleContent,
@ -41,13 +42,17 @@ export const DocPropertySidebar = () => {
const name = nameExists const name = nameExists
? generateUniqueNameInSequence(option.name, allNames) ? generateUniqueNameInSequence(option.name, allNames)
: option.name; : option.name;
const newPropertyId = propertyList.createProperty({ const newProperty = propertyList.createProperty({
id: typeDefined.uniqueId, id: typeDefined.uniqueId,
name, name,
type: option.type, type: option.type,
index: propertyList.indexAt('after'), index: propertyList.indexAt('after'),
}); });
setNewPropertyId(newPropertyId); setNewPropertyId(newProperty.id);
track.doc.sidepanel.property.addProperty({
control: 'property list',
type: option.type,
});
}, },
[propertyList, properties] [propertyList, properties]
); );

View File

@ -9,6 +9,10 @@ import {
useDropTarget, useDropTarget,
} from '@affine/component'; } from '@affine/component';
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info'; import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
import type {
DatabaseRow,
DatabaseValueCell,
} from '@affine/core/modules/doc-info/types';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view'; import { ViewService } from '@affine/core/modules/workbench/services/view';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
@ -26,7 +30,6 @@ import {
} from '@toeverything/infra'; } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import type React from 'react'; import type React from 'react';
import type { HTMLProps } from 'react';
import { forwardRef, useCallback, useState } from 'react'; import { forwardRef, useCallback, useState } from 'react';
import { DocPropertyIcon } from './icons/doc-property-icon'; import { DocPropertyIcon } from './icons/doc-property-icon';
@ -47,6 +50,13 @@ export type DefaultOpenProperty =
export interface DocPropertiesTableProps { export interface DocPropertiesTableProps {
defaultOpenProperty?: DefaultOpenProperty; defaultOpenProperty?: DefaultOpenProperty;
onPropertyAdded?: (property: DocCustomPropertyInfo) => void;
onPropertyChange?: (property: DocCustomPropertyInfo, value: unknown) => void;
onDatabasePropertyChange?: (
row: DatabaseRow,
cell: DatabaseValueCell,
value: unknown
) => void;
} }
interface DocPropertiesTableHeaderProps { interface DocPropertiesTableHeaderProps {
@ -95,11 +105,13 @@ interface DocPropertyRowProps {
propertyInfo: DocCustomPropertyInfo; propertyInfo: DocCustomPropertyInfo;
showAll?: boolean; showAll?: boolean;
defaultOpenEditMenu?: boolean; defaultOpenEditMenu?: boolean;
onChange?: (value: unknown) => void;
} }
export const DocPropertyRow = ({ export const DocPropertyRow = ({
propertyInfo, propertyInfo,
defaultOpenEditMenu, defaultOpenEditMenu,
onChange,
}: DocPropertyRowProps) => { }: DocPropertyRowProps) => {
const t = useI18n(); const t = useI18n();
const docService = useService(DocService); const docService = useService(DocService);
@ -123,8 +135,9 @@ export const DocPropertyRow = ({
throw new Error('only allow string value'); throw new Error('only allow string value');
} }
docService.doc.record.setCustomProperty(propertyInfo.id, value); docService.doc.record.setCustomProperty(propertyInfo.id, value);
onChange?.(value);
}, },
[docService, propertyInfo] [docService, onChange, propertyInfo]
); );
const docId = docService.doc.id; const docId = docService.doc.id;
@ -214,6 +227,8 @@ interface DocWorkspacePropertiesTableBodyProps {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
defaultOpen?: boolean; defaultOpen?: boolean;
onChange?: (property: DocCustomPropertyInfo, value: unknown) => void;
onPropertyAdded?: (property: DocCustomPropertyInfo) => void;
} }
// 🏷️ Tags (⋅ xxx) (⋅ yyy) // 🏷️ Tags (⋅ xxx) (⋅ yyy)
@ -221,104 +236,121 @@ interface DocWorkspacePropertiesTableBodyProps {
// + Add a property // + Add a property
const DocWorkspacePropertiesTableBody = forwardRef< const DocWorkspacePropertiesTableBody = forwardRef<
HTMLDivElement, HTMLDivElement,
DocWorkspacePropertiesTableBodyProps & HTMLProps<HTMLDivElement> DocWorkspacePropertiesTableBodyProps
>(({ className, style, defaultOpen, ...props }, ref) => { >(
const t = useI18n(); (
const docsService = useService(DocsService); { className, style, defaultOpen, onChange, onPropertyAdded, ...props },
const workbenchService = useService(WorkbenchService); ref
const viewService = useServiceOptional(ViewService); ) => {
const properties = useLiveData(docsService.propertyList.sortedProperties$); const t = useI18n();
const [propertyCollapsed, setPropertyCollapsed] = useState(true); const docsService = useService(DocsService);
const workbenchService = useService(WorkbenchService);
const viewService = useServiceOptional(ViewService);
const properties = useLiveData(docsService.propertyList.sortedProperties$);
const [propertyCollapsed, setPropertyCollapsed] = useState(true);
const [newPropertyId, setNewPropertyId] = useState<string | null>(null); const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
return ( const handlePropertyAdded = useCallback(
<PropertyCollapsibleSection (property: DocCustomPropertyInfo) => {
ref={ref} setNewPropertyId(property.id);
className={clsx(styles.tableBodyRoot, className)} onPropertyAdded?.(property);
style={style} },
title={t.t('com.affine.workspace.properties')} [onPropertyAdded]
defaultCollapsed={!defaultOpen} );
{...props}
> return (
<PropertyCollapsibleContent <PropertyCollapsibleSection
collapsible ref={ref}
collapsed={propertyCollapsed} className={clsx(styles.tableBodyRoot, className)}
onCollapseChange={setPropertyCollapsed} style={style}
className={styles.tableBodySortable} title={t.t('com.affine.workspace.properties')}
collapseButtonText={({ hide, isCollapsed }) => defaultCollapsed={!defaultOpen}
isCollapsed {...props}
? hide === 1
? t['com.affine.page-properties.more-property.one']({
count: hide.toString(),
})
: t['com.affine.page-properties.more-property.more']({
count: hide.toString(),
})
: hide === 1
? t['com.affine.page-properties.hide-property.one']({
count: hide.toString(),
})
: t['com.affine.page-properties.hide-property.more']({
count: hide.toString(),
})
}
> >
{properties.map(property => ( <PropertyCollapsibleContent
<DocPropertyRow collapsible
key={property.id} collapsed={propertyCollapsed}
propertyInfo={property} onCollapseChange={setPropertyCollapsed}
defaultOpenEditMenu={newPropertyId === property.id} className={styles.tableBodySortable}
/> collapseButtonText={({ hide, isCollapsed }) =>
))} isCollapsed
<div className={styles.actionContainer}> ? hide === 1
<Menu ? t['com.affine.page-properties.more-property.one']({
items={ count: hide.toString(),
<CreatePropertyMenuItems })
at="after" : t['com.affine.page-properties.more-property.more']({
onCreated={setNewPropertyId} count: hide.toString(),
/> })
} : hide === 1
contentOptions={{ ? t['com.affine.page-properties.hide-property.one']({
onClick(e) { count: hide.toString(),
e.stopPropagation(); })
}, : t['com.affine.page-properties.hide-property.more']({
}} count: hide.toString(),
> })
<Button }
variant="plain" >
prefix={<PlusIcon />} {properties.map(property => (
className={styles.propertyActionButton} <DocPropertyRow
data-testid="add-property-button" key={property.id}
> propertyInfo={property}
{t['com.affine.page-properties.add-property']()} defaultOpenEditMenu={newPropertyId === property.id}
</Button> onChange={value => onChange?.(property, value)}
</Menu> />
{viewService ? ( ))}
<Button <div className={styles.actionContainer}>
variant="plain" <Menu
prefix={<PropertyIcon />} items={
className={clsx( <CreatePropertyMenuItems
styles.propertyActionButton, at="after"
styles.propertyConfigButton onCreated={handlePropertyAdded}
)} />
onClick={() => { }
viewService.view.activeSidebarTab('properties'); contentOptions={{
workbenchService.workbench.openSidebar(); onClick(e) {
e.stopPropagation();
},
}} }}
> >
{t['com.affine.page-properties.config-properties']()} <Button
</Button> variant="plain"
) : null} prefix={<PlusIcon />}
</div> className={styles.propertyActionButton}
</PropertyCollapsibleContent> data-testid="add-property-button"
</PropertyCollapsibleSection> >
); {t['com.affine.page-properties.add-property']()}
}); </Button>
</Menu>
{viewService ? (
<Button
variant="plain"
prefix={<PropertyIcon />}
className={clsx(
styles.propertyActionButton,
styles.propertyConfigButton
)}
onClick={() => {
viewService.view.activeSidebarTab('properties');
workbenchService.workbench.openSidebar();
}}
>
{t['com.affine.page-properties.config-properties']()}
</Button>
) : null}
</div>
</PropertyCollapsibleContent>
</PropertyCollapsibleSection>
);
}
);
DocWorkspacePropertiesTableBody.displayName = 'PagePropertiesTableBody'; DocWorkspacePropertiesTableBody.displayName = 'PagePropertiesTableBody';
const DocPropertiesTableInner = ({ const DocPropertiesTableInner = ({
defaultOpenProperty, defaultOpenProperty,
onPropertyAdded,
onPropertyChange,
onDatabasePropertyChange,
}: DocPropertiesTableProps) => { }: DocPropertiesTableProps) => {
const [expanded, setExpanded] = useState(!!defaultOpenProperty); const [expanded, setExpanded] = useState(!!defaultOpenProperty);
return ( return (
@ -334,9 +366,12 @@ const DocPropertiesTableInner = ({
defaultOpen={ defaultOpen={
!defaultOpenProperty || defaultOpenProperty.type === 'workspace' !defaultOpenProperty || defaultOpenProperty.type === 'workspace'
} }
onPropertyAdded={onPropertyAdded}
onChange={onPropertyChange}
/> />
<div className={styles.tableHeaderDivider} /> <div className={styles.tableHeaderDivider} />
<DocDatabaseBacklinkInfo <DocDatabaseBacklinkInfo
onChange={onDatabasePropertyChange}
defaultOpen={ defaultOpen={
defaultOpenProperty?.type === 'database' defaultOpenProperty?.type === 'database'
? [ ? [
@ -356,8 +391,6 @@ const DocPropertiesTableInner = ({
// this is the main component that renders the page properties table at the top of the page below // this is the main component that renders the page properties table at the top of the page below
// the page title // the page title
export const DocPropertiesTable = ({ export const DocPropertiesTable = (props: DocPropertiesTableProps) => {
defaultOpenProperty, return <DocPropertiesTableInner {...props} />;
}: DocPropertiesTableProps) => {
return <DocPropertiesTableInner defaultOpenProperty={defaultOpenProperty} />;
}; };

View File

@ -1,6 +1,7 @@
import type { TagLike } from '@affine/component/ui/tags'; import type { TagLike } from '@affine/component/ui/tags';
import { TagsInlineEditor as TagsInlineEditorComponent } from '@affine/component/ui/tags'; import { TagsInlineEditor as TagsInlineEditorComponent } from '@affine/component/ui/tags';
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag'; import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
import track from '@affine/track';
import { import {
LiveData, LiveData,
useLiveData, useLiveData,
@ -38,6 +39,9 @@ export const TagsInlineEditor = ({
const onCreateTag = useCallback( const onCreateTag = useCallback(
(name: string, color: string) => { (name: string, color: string) => {
const newTag = tagService.tagList.createTag(name, color); const newTag = tagService.tagList.createTag(name, color);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
return { return {
id: newTag.id, id: newTag.id,
value: newTag.value$.value, value: newTag.value$.value,
@ -50,6 +54,9 @@ export const TagsInlineEditor = ({
const onSelectTag = useCallback( const onSelectTag = useCallback(
(tagId: string) => { (tagId: string) => {
tagService.tagList.tagByTagId$(tagId).value?.tag(pageId); tagService.tagList.tagByTagId$(tagId).value?.tag(pageId);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
}, },
[pageId, tagService.tagList] [pageId, tagService.tagList]
); );
@ -57,6 +64,9 @@ export const TagsInlineEditor = ({
const onDeselectTag = useCallback( const onDeselectTag = useCallback(
(tagId: string) => { (tagId: string) => {
tagService.tagList.tagByTagId$(tagId).value?.untag(pageId); tagService.tagList.tagByTagId$(tagId).value?.untag(pageId);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
}, },
[pageId, tagService.tagList] [pageId, tagService.tagList]
); );
@ -68,6 +78,9 @@ export const TagsInlineEditor = ({
} else if (property === 'color') { } else if (property === 'color') {
tagService.tagList.tagByTagId$(id).value?.changeColor(value); tagService.tagList.tagByTagId$(id).value?.changeColor(value);
} }
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
}, },
[tagService.tagList] [tagService.tagList]
); );
@ -77,6 +90,9 @@ export const TagsInlineEditor = ({
const onTagDelete = useAsyncCallback( const onTagDelete = useAsyncCallback(
async (id: string) => { async (id: string) => {
await deleteTags([id]); await deleteTags([id]);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
}, },
[deleteTags] [deleteTags]
); );

View File

@ -8,16 +8,22 @@ import {
import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property'; import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property';
import { DocPropertyRow } from '@affine/core/components/doc-properties/table'; import { DocPropertyRow } from '@affine/core/components/doc-properties/table';
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info'; import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
import type {
DatabaseRow,
DatabaseValueCell,
} from '@affine/core/modules/doc-info/types';
import { DocsSearchService } from '@affine/core/modules/docs-search'; import { DocsSearchService } from '@affine/core/modules/docs-search';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { import {
type DocCustomPropertyInfo,
DocsService, DocsService,
LiveData, LiveData,
useLiveData, useLiveData,
useServices, useServices,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import * as styles from './info-modal.css'; import * as styles from './info-modal.css';
import { LinksRow } from './links-row'; import { LinksRow } from './links-row';
@ -49,6 +55,23 @@ export const InfoTable = ({
) )
); );
const onBacklinkPropertyChange = useCallback(
(_row: DatabaseRow, cell: DatabaseValueCell, _value: unknown) => {
track.$.docInfoPanel.databaseProperty.editProperty({
type: cell.property.type$.value,
});
},
[]
);
const onPropertyAdded = useCallback((property: DocCustomPropertyInfo) => {
setNewPropertyId(property.id);
track.$.docInfoPanel.property.addProperty({
type: property.type,
control: 'at menu',
});
}, []);
return ( return (
<> <>
{backlinks && backlinks.length > 0 ? ( {backlinks && backlinks.length > 0 ? (
@ -102,7 +125,7 @@ export const InfoTable = ({
/> />
))} ))}
<Menu <Menu
items={<CreatePropertyMenuItems onCreated={setNewPropertyId} />} items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
contentOptions={{ contentOptions={{
onClick(e) { onClick(e) {
e.stopPropagation(); e.stopPropagation();
@ -120,7 +143,7 @@ export const InfoTable = ({
</PropertyCollapsibleContent> </PropertyCollapsibleContent>
</PropertyCollapsibleSection> </PropertyCollapsibleSection>
<Divider size="thinner" /> <Divider size="thinner" />
<DocDatabaseBacklinkInfo /> <DocDatabaseBacklinkInfo onChange={onBacklinkPropertyChange} />
</> </>
); );
}; };

View File

@ -4,7 +4,13 @@ import { DocPropertyManager } from '@affine/core/components/doc-properties/manag
import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property'; import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property';
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info'; import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { FrameworkScope, type WorkspaceMetadata } from '@toeverything/infra'; import track from '@affine/track';
import {
type DocCustomPropertyInfo,
FrameworkScope,
type WorkspaceMetadata,
} from '@toeverything/infra';
import { useCallback } from 'react';
import { useWorkspace } from '../../../../../components/hooks/use-workspace'; import { useWorkspace } from '../../../../../components/hooks/use-workspace';
import * as styles from './styles.css'; import * as styles from './styles.css';
@ -12,10 +18,17 @@ import * as styles from './styles.css';
const WorkspaceSettingPropertiesMain = () => { const WorkspaceSettingPropertiesMain = () => {
const t = useI18n(); const t = useI18n();
const onCreated = useCallback((property: DocCustomPropertyInfo) => {
track.$.settingsPanel.workspace.addProperty({
type: property.type,
control: 'at menu',
});
}, []);
return ( return (
<div className={styles.main}> <div className={styles.main}>
<div className={styles.listHeader}> <div className={styles.listHeader}>
<Menu items={<CreatePropertyMenuItems />}> <Menu items={<CreatePropertyMenuItems onCreated={onCreated} />}>
<Button variant="primary"> <Button variant="primary">
{t['com.affine.settings.workspace.properties.add_property']()} {t['com.affine.settings.workspace.properties.add_property']()}
</Button> </Button>

View File

@ -32,4 +32,5 @@ export interface DatabaseCellRendererProps {
rowId: string; rowId: string;
cell: DatabaseValueCell; cell: DatabaseValueCell;
dataSource: DatabaseBlockDataSource; dataSource: DatabaseBlockDataSource;
onChange: (value: unknown) => void;
} }

View File

@ -8,6 +8,7 @@ export const CheckboxCell = ({
cell, cell,
rowId, rowId,
dataSource, dataSource,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const value = useLiveData(cell.value$ as LiveData<boolean>); const value = useLiveData(cell.value$ as LiveData<boolean>);
return ( return (
@ -16,6 +17,7 @@ export const CheckboxCell = ({
value={value ? 'true' : 'false'} value={value ? 'true' : 'false'}
onChange={v => { onChange={v => {
dataSource.cellValueChange(rowId, cell.property.id, v === 'true'); dataSource.cellValueChange(rowId, cell.property.id, v === 'true');
onChange?.(v === 'true');
}} }}
/> />
); );

View File

@ -20,6 +20,7 @@ export const DateCell = ({
cell, cell,
rowId, rowId,
dataSource, dataSource,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const value = useLiveData( const value = useLiveData(
cell.value$ as LiveData<number | string | undefined> cell.value$ as LiveData<number | string | undefined>
@ -34,6 +35,7 @@ export const DateCell = ({
cell.property.id, cell.property.id,
fromInternalDateString(v) fromInternalDateString(v)
); );
onChange?.(fromInternalDateString(v));
}} }}
/> />
); );

View File

@ -21,6 +21,7 @@ export const LinkCell = ({
cell, cell,
dataSource, dataSource,
rowId, rowId,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const isEmpty = useLiveData( const isEmpty = useLiveData(
cell.value$.map(value => typeof value !== 'string' || !value) cell.value$.map(value => typeof value !== 'string' || !value)
@ -35,7 +36,8 @@ export const LinkCell = ({
dataSource.cellValueChange(rowId, cell.id, tempValue.trim()); dataSource.cellValueChange(rowId, cell.id, tempValue.trim());
setEditing(false); setEditing(false);
setTempValue(tempValue.trim()); setTempValue(tempValue.trim());
}, [dataSource, rowId, cell.id, tempValue]); onChange?.(tempValue.trim());
}, [dataSource, rowId, cell.id, onChange, tempValue]);
const handleOnChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback( const handleOnChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
e => { e => {

View File

@ -7,6 +7,7 @@ export const NumberCell = ({
cell, cell,
rowId, rowId,
dataSource, dataSource,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const value = useLiveData(cell.value$); const value = useLiveData(cell.value$);
return ( return (
@ -14,6 +15,7 @@ export const NumberCell = ({
value={value} value={value}
onChange={v => { onChange={v => {
dataSource.cellValueChange(rowId, cell.property.id, v); dataSource.cellValueChange(rowId, cell.property.id, v);
onChange?.(v);
}} }}
/> />
); );

View File

@ -9,6 +9,7 @@ export const ProgressCell = ({
cell, cell,
dataSource, dataSource,
rowId, rowId,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const value = useLiveData(cell.value$ as LiveData<number>); const value = useLiveData(cell.value$ as LiveData<number>);
const isEmpty = value === undefined; const isEmpty = value === undefined;
@ -27,6 +28,7 @@ export const ProgressCell = ({
}} }}
onBlur={() => { onBlur={() => {
dataSource.cellValueChange(rowId, cell.id, localValue); dataSource.cellValueChange(rowId, cell.id, localValue);
onChange?.(localValue);
}} }}
/> />
</PropertyValue> </PropertyValue>

View File

@ -40,6 +40,7 @@ const renderRichText = ({
export const RichTextCell = ({ export const RichTextCell = ({
cell, cell,
dataSource, dataSource,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const std = useBlockStdScope(dataSource.doc); const std = useBlockStdScope(dataSource.doc);
const text = useLiveData(cell.value$ as LiveData<Y.Text>); const text = useLiveData(cell.value$ as LiveData<Y.Text>);
@ -49,14 +50,19 @@ export const RichTextCell = ({
if (ref.current) { if (ref.current) {
ref.current.innerHTML = ''; ref.current.innerHTML = '';
const richText = renderRichText({ doc: dataSource.doc, std, text }); const richText = renderRichText({ doc: dataSource.doc, std, text });
const listener = () => {
onChange(text);
};
if (richText) { if (richText) {
richText.addEventListener('change', listener);
ref.current.append(richText); ref.current.append(richText);
return () => { return () => {
richText.removeEventListener('change', listener);
richText.remove(); richText.remove();
}; };
} }
} }
return () => {}; return () => {};
}, [dataSource.doc, std, text]); }, [dataSource.doc, onChange, std, text]);
return <PropertyValue ref={ref}></PropertyValue>; return <PropertyValue ref={ref}></PropertyValue>;
}; };

View File

@ -147,6 +147,7 @@ const BlocksuiteDatabaseSelector = ({
dataSource, dataSource,
rowId, rowId,
multiple, multiple,
onChange,
}: DatabaseCellRendererProps & { multiple: boolean }) => { }: DatabaseCellRendererProps & { multiple: boolean }) => {
const tagService = useService(TagService); const tagService = useService(TagService);
const selectCell = cell as any as SingleSelectCell | MultiSelectCell; const selectCell = cell as any as SingleSelectCell | MultiSelectCell;
@ -177,21 +178,24 @@ const BlocksuiteDatabaseSelector = ({
const onDeleteTag = useCallback( const onDeleteTag = useCallback(
(tagId: string) => { (tagId: string) => {
adapter.deleteTag(selectCell, dataSource, tagId); adapter.deleteTag(selectCell, dataSource, tagId);
onChange?.(selectCell.value$.value);
}, },
[dataSource, selectCell] [dataSource, selectCell, onChange]
); );
const onDeselectTag = useCallback( const onDeselectTag = useCallback(
(tagId: string) => { (tagId: string) => {
adapter.deselectTag(rowId, selectCell, dataSource, tagId, multiple); adapter.deselectTag(rowId, selectCell, dataSource, tagId, multiple);
onChange?.(selectCell.value$.value);
}, },
[selectCell, dataSource, rowId, multiple] [rowId, selectCell, dataSource, multiple, onChange]
); );
const onSelectTag = useCallback( const onSelectTag = useCallback(
(tagId: string) => { (tagId: string) => {
adapter.selectTag(rowId, selectCell, dataSource, tagId, multiple); adapter.selectTag(rowId, selectCell, dataSource, tagId, multiple);
onChange?.(selectCell.value$.value);
}, },
[rowId, selectCell, dataSource, multiple] [rowId, selectCell, dataSource, multiple, onChange]
); );
const tagColors = useMemo(() => { const tagColors = useMemo(() => {
@ -237,6 +241,7 @@ export const SelectCell = ({
cell, cell,
dataSource, dataSource,
rowId, rowId,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const isEmpty = useLiveData( const isEmpty = useLiveData(
cell.value$.map(value => Array.isArray(value) && value.length === 0) cell.value$.map(value => Array.isArray(value) && value.length === 0)
@ -248,6 +253,7 @@ export const SelectCell = ({
dataSource={dataSource} dataSource={dataSource}
rowId={rowId} rowId={rowId}
multiple={false} multiple={false}
onChange={onChange}
/> />
</PropertyValue> </PropertyValue>
); );
@ -257,6 +263,7 @@ export const MultiSelectCell = ({
cell, cell,
dataSource, dataSource,
rowId, rowId,
onChange,
}: DatabaseCellRendererProps) => { }: DatabaseCellRendererProps) => {
const isEmpty = useLiveData( const isEmpty = useLiveData(
cell.value$.map(value => Array.isArray(value) && value.length === 0) cell.value$.map(value => Array.isArray(value) && value.length === 0)
@ -268,6 +275,7 @@ export const MultiSelectCell = ({
dataSource={dataSource} dataSource={dataSource}
rowId={rowId} rowId={rowId}
multiple={true} multiple={true}
onChange={onChange}
/> />
</PropertyValue> </PropertyValue>
); );

View File

@ -46,10 +46,12 @@ const DatabaseBacklinkCell = ({
cell, cell,
dataSource, dataSource,
rowId, rowId,
onChange,
}: { }: {
cell: DatabaseValueCell; cell: DatabaseValueCell;
dataSource: DatabaseBlockDataSource; dataSource: DatabaseBlockDataSource;
rowId: string; rowId: string;
onChange: (value: unknown) => void;
}) => { }) => {
const cellType = useLiveData(cell.property.type$); const cellType = useLiveData(cell.property.type$);
@ -67,7 +69,12 @@ const DatabaseBacklinkCell = ({
data-testid="database-backlink-cell" data-testid="database-backlink-cell"
> >
<DatabaseBacklinkCellName cell={cell} config={config} /> <DatabaseBacklinkCellName cell={cell} config={config} />
<config.Renderer cell={cell} dataSource={dataSource} rowId={rowId} /> <config.Renderer
cell={cell}
dataSource={dataSource}
rowId={rowId}
onChange={onChange}
/>
</li> </li>
); );
}; };
@ -79,9 +86,15 @@ const DatabaseBacklinkCell = ({
const DatabaseBacklinkRow = ({ const DatabaseBacklinkRow = ({
defaultOpen = false, defaultOpen = false,
row$, row$,
onChange,
}: { }: {
defaultOpen: boolean; defaultOpen: boolean;
row$: Observable<DatabaseRow | undefined>; row$: Observable<DatabaseRow | undefined>;
onChange?: (
row: DatabaseRow,
cell: DatabaseValueCell,
value: unknown
) => void;
}) => { }) => {
const row = useLiveData( const row = useLiveData(
useMemo(() => LiveData.from(row$, undefined), [row$]) useMemo(() => LiveData.from(row$, undefined), [row$])
@ -132,6 +145,7 @@ const DatabaseBacklinkRow = ({
cell={cell} cell={cell}
dataSource={row.dataSource} dataSource={row.dataSource}
rowId={row.id} rowId={row.id}
onChange={value => onChange?.(row, cell, value)}
/> />
); );
})} })}
@ -142,11 +156,17 @@ const DatabaseBacklinkRow = ({
export const DocDatabaseBacklinkInfo = ({ export const DocDatabaseBacklinkInfo = ({
defaultOpen = [], defaultOpen = [],
onChange,
}: { }: {
defaultOpen?: { defaultOpen?: {
databaseId: string; databaseId: string;
rowId: string; rowId: string;
}[]; }[];
onChange?: (
row: DatabaseRow,
cell: DatabaseValueCell,
value: unknown
) => void;
}) => { }) => {
const doc = useService(DocService).doc; const doc = useService(DocService).doc;
const docDatabaseBacklinks = useService(DocDatabaseBacklinksService); const docDatabaseBacklinks = useService(DocDatabaseBacklinksService);
@ -173,6 +193,7 @@ export const DocDatabaseBacklinkInfo = ({
backlink.rowId === rowId backlink.rowId === rowId
)} )}
row$={row$} row$={row$}
onChange={onChange}
/> />
<Divider size="thinner" className={styles.divider} /> <Divider size="thinner" className={styles.divider} />
</Fragment> </Fragment>

View File

@ -46,7 +46,9 @@ type DocEvents =
| 'openDocOptionsMenu' | 'openDocOptionsMenu'
| 'openDocInfo' | 'openDocInfo'
| 'copyBlockToLink' | 'copyBlockToLink'
| 'bookmark'; | 'bookmark'
| 'editProperty'
| 'addProperty';
type EditorEvents = 'bold' | 'italic' | 'underline' | 'strikeThrough'; type EditorEvents = 'bold' | 'italic' | 'underline' | 'strikeThrough';
// END SECTION // END SECTION
@ -148,10 +150,12 @@ const PageEvents = {
}, },
docInfoPanel: { docInfoPanel: {
$: ['open'], $: ['open'],
property: ['editProperty', 'addProperty'],
databaseProperty: ['editProperty'],
}, },
settingsPanel: { settingsPanel: {
menu: ['openSettings'], menu: ['openSettings'],
workspace: ['viewPlans', 'export'], workspace: ['viewPlans', 'export', 'addProperty'],
profileAndBadge: ['viewPlans'], profileAndBadge: ['viewPlans'],
accountUsage: ['viewPlans'], accountUsage: ['viewPlans'],
accountSettings: ['uploadAvatar', 'removeAvatar', 'updateUserName'], accountSettings: ['uploadAvatar', 'removeAvatar', 'updateUserName'],
@ -277,6 +281,11 @@ const PageEvents = {
}, },
inlineDocInfo: { inlineDocInfo: {
$: ['toggle'], $: ['toggle'],
property: ['editProperty', 'addProperty'],
databaseProperty: ['editProperty'],
},
sidepanel: {
property: ['addProperty'],
}, },
}, },
// remove when type added // remove when type added
@ -413,6 +422,8 @@ export type EventArgs = {
copyBlockToLink: { copyBlockToLink: {
type: string; type: string;
}; };
editProperty: { type: string };
addProperty: { type: string; control: 'at menu' | 'property list' };
}; };
// for type checking // for type checking