mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 11:36:25 +08:00
@@ -3,11 +3,16 @@ import {
|
||||
useConfirmModal,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import type {
|
||||
DatabaseRow,
|
||||
DatabaseValueCell,
|
||||
} from '@affine/core/modules/doc-info/types';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import track from '@affine/track';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
DocTitle,
|
||||
@@ -16,6 +21,7 @@ import {
|
||||
} from '@blocksuite/affine/presets';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import {
|
||||
type DocCustomPropertyInfo,
|
||||
DocService,
|
||||
DocsService,
|
||||
FeatureFlagService,
|
||||
@@ -239,6 +245,28 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
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 (
|
||||
<>
|
||||
<div className={styles.affineDocViewport} style={{ height: '100%' }}>
|
||||
@@ -248,7 +276,12 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||
)}
|
||||
{!shared && displayDocInfo ? (
|
||||
<DocPropertiesTable defaultOpenProperty={defaultOpenProperty} />
|
||||
<DocPropertiesTable
|
||||
onDatabasePropertyChange={onDatabasePropertyChange}
|
||||
onPropertyChange={onPropertyChange}
|
||||
onPropertyAdded={onPropertyAdded}
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
/>
|
||||
) : null}
|
||||
<adapted.DocEditor
|
||||
className={styles.docContainer}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { MenuItem, MenuSeparator } from '@affine/component';
|
||||
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
|
||||
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 {
|
||||
@@ -15,7 +20,7 @@ export const CreatePropertyMenuItems = ({
|
||||
onCreated,
|
||||
}: {
|
||||
at?: 'before' | 'after';
|
||||
onCreated?: (propertyId: string) => void;
|
||||
onCreated?: (property: DocCustomPropertyInfo) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
@@ -36,13 +41,13 @@ export const CreatePropertyMenuItems = ({
|
||||
? generateUniqueNameInSequence(option.name, allNames)
|
||||
: option.name;
|
||||
const uniqueId = typeDefined.uniqueId;
|
||||
const newPropertyId = propertyList.createProperty({
|
||||
const newProperty = propertyList.createProperty({
|
||||
id: uniqueId,
|
||||
name,
|
||||
type: option.type,
|
||||
index: propertyList.indexAt(at),
|
||||
});
|
||||
onCreated?.(newPropertyId);
|
||||
onCreated?.(newProperty);
|
||||
},
|
||||
[at, onCreated, propertyList, properties]
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Divider, IconButton, Tooltip } from '@affine/component';
|
||||
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
Content as CollapsibleContent,
|
||||
@@ -41,13 +42,17 @@ export const DocPropertySidebar = () => {
|
||||
const name = nameExists
|
||||
? generateUniqueNameInSequence(option.name, allNames)
|
||||
: option.name;
|
||||
const newPropertyId = propertyList.createProperty({
|
||||
const newProperty = propertyList.createProperty({
|
||||
id: typeDefined.uniqueId,
|
||||
name,
|
||||
type: option.type,
|
||||
index: propertyList.indexAt('after'),
|
||||
});
|
||||
setNewPropertyId(newPropertyId);
|
||||
setNewPropertyId(newProperty.id);
|
||||
track.doc.sidepanel.property.addProperty({
|
||||
control: 'property list',
|
||||
type: option.type,
|
||||
});
|
||||
},
|
||||
[propertyList, properties]
|
||||
);
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
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 { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
@@ -26,7 +30,6 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import type { HTMLProps } from 'react';
|
||||
import { forwardRef, useCallback, useState } from 'react';
|
||||
|
||||
import { DocPropertyIcon } from './icons/doc-property-icon';
|
||||
@@ -47,6 +50,13 @@ export type DefaultOpenProperty =
|
||||
|
||||
export interface DocPropertiesTableProps {
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
onPropertyAdded?: (property: DocCustomPropertyInfo) => void;
|
||||
onPropertyChange?: (property: DocCustomPropertyInfo, value: unknown) => void;
|
||||
onDatabasePropertyChange?: (
|
||||
row: DatabaseRow,
|
||||
cell: DatabaseValueCell,
|
||||
value: unknown
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface DocPropertiesTableHeaderProps {
|
||||
@@ -95,11 +105,13 @@ interface DocPropertyRowProps {
|
||||
propertyInfo: DocCustomPropertyInfo;
|
||||
showAll?: boolean;
|
||||
defaultOpenEditMenu?: boolean;
|
||||
onChange?: (value: unknown) => void;
|
||||
}
|
||||
|
||||
export const DocPropertyRow = ({
|
||||
propertyInfo,
|
||||
defaultOpenEditMenu,
|
||||
onChange,
|
||||
}: DocPropertyRowProps) => {
|
||||
const t = useI18n();
|
||||
const docService = useService(DocService);
|
||||
@@ -123,8 +135,9 @@ export const DocPropertyRow = ({
|
||||
throw new Error('only allow string value');
|
||||
}
|
||||
docService.doc.record.setCustomProperty(propertyInfo.id, value);
|
||||
onChange?.(value);
|
||||
},
|
||||
[docService, propertyInfo]
|
||||
[docService, onChange, propertyInfo]
|
||||
);
|
||||
|
||||
const docId = docService.doc.id;
|
||||
@@ -214,6 +227,8 @@ interface DocWorkspacePropertiesTableBodyProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
defaultOpen?: boolean;
|
||||
onChange?: (property: DocCustomPropertyInfo, value: unknown) => void;
|
||||
onPropertyAdded?: (property: DocCustomPropertyInfo) => void;
|
||||
}
|
||||
|
||||
// 🏷️ Tags (⋅ xxx) (⋅ yyy)
|
||||
@@ -221,104 +236,121 @@ interface DocWorkspacePropertiesTableBodyProps {
|
||||
// + Add a property
|
||||
const DocWorkspacePropertiesTableBody = forwardRef<
|
||||
HTMLDivElement,
|
||||
DocWorkspacePropertiesTableBodyProps & HTMLProps<HTMLDivElement>
|
||||
>(({ className, style, defaultOpen, ...props }, ref) => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
const workbenchService = useService(WorkbenchService);
|
||||
const viewService = useServiceOptional(ViewService);
|
||||
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
||||
const [propertyCollapsed, setPropertyCollapsed] = useState(true);
|
||||
DocWorkspacePropertiesTableBodyProps
|
||||
>(
|
||||
(
|
||||
{ className, style, defaultOpen, onChange, onPropertyAdded, ...props },
|
||||
ref
|
||||
) => {
|
||||
const t = useI18n();
|
||||
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 (
|
||||
<PropertyCollapsibleSection
|
||||
ref={ref}
|
||||
className={clsx(styles.tableBodyRoot, className)}
|
||||
style={style}
|
||||
title={t.t('com.affine.workspace.properties')}
|
||||
defaultCollapsed={!defaultOpen}
|
||||
{...props}
|
||||
>
|
||||
<PropertyCollapsibleContent
|
||||
collapsible
|
||||
collapsed={propertyCollapsed}
|
||||
onCollapseChange={setPropertyCollapsed}
|
||||
className={styles.tableBodySortable}
|
||||
collapseButtonText={({ hide, isCollapsed }) =>
|
||||
isCollapsed
|
||||
? 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(),
|
||||
})
|
||||
}
|
||||
const handlePropertyAdded = useCallback(
|
||||
(property: DocCustomPropertyInfo) => {
|
||||
setNewPropertyId(property.id);
|
||||
onPropertyAdded?.(property);
|
||||
},
|
||||
[onPropertyAdded]
|
||||
);
|
||||
|
||||
return (
|
||||
<PropertyCollapsibleSection
|
||||
ref={ref}
|
||||
className={clsx(styles.tableBodyRoot, className)}
|
||||
style={style}
|
||||
title={t.t('com.affine.workspace.properties')}
|
||||
defaultCollapsed={!defaultOpen}
|
||||
{...props}
|
||||
>
|
||||
{properties.map(property => (
|
||||
<DocPropertyRow
|
||||
key={property.id}
|
||||
propertyInfo={property}
|
||||
defaultOpenEditMenu={newPropertyId === property.id}
|
||||
/>
|
||||
))}
|
||||
<div className={styles.actionContainer}>
|
||||
<Menu
|
||||
items={
|
||||
<CreatePropertyMenuItems
|
||||
at="after"
|
||||
onCreated={setNewPropertyId}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
onClick(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
prefix={<PlusIcon />}
|
||||
className={styles.propertyActionButton}
|
||||
data-testid="add-property-button"
|
||||
>
|
||||
{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();
|
||||
<PropertyCollapsibleContent
|
||||
collapsible
|
||||
collapsed={propertyCollapsed}
|
||||
onCollapseChange={setPropertyCollapsed}
|
||||
className={styles.tableBodySortable}
|
||||
collapseButtonText={({ hide, isCollapsed }) =>
|
||||
isCollapsed
|
||||
? 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 => (
|
||||
<DocPropertyRow
|
||||
key={property.id}
|
||||
propertyInfo={property}
|
||||
defaultOpenEditMenu={newPropertyId === property.id}
|
||||
onChange={value => onChange?.(property, value)}
|
||||
/>
|
||||
))}
|
||||
<div className={styles.actionContainer}>
|
||||
<Menu
|
||||
items={
|
||||
<CreatePropertyMenuItems
|
||||
at="after"
|
||||
onCreated={handlePropertyAdded}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
onClick(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t['com.affine.page-properties.config-properties']()}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</PropertyCollapsibleContent>
|
||||
</PropertyCollapsibleSection>
|
||||
);
|
||||
});
|
||||
<Button
|
||||
variant="plain"
|
||||
prefix={<PlusIcon />}
|
||||
className={styles.propertyActionButton}
|
||||
data-testid="add-property-button"
|
||||
>
|
||||
{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';
|
||||
|
||||
const DocPropertiesTableInner = ({
|
||||
defaultOpenProperty,
|
||||
onPropertyAdded,
|
||||
onPropertyChange,
|
||||
onDatabasePropertyChange,
|
||||
}: DocPropertiesTableProps) => {
|
||||
const [expanded, setExpanded] = useState(!!defaultOpenProperty);
|
||||
return (
|
||||
@@ -334,9 +366,12 @@ const DocPropertiesTableInner = ({
|
||||
defaultOpen={
|
||||
!defaultOpenProperty || defaultOpenProperty.type === 'workspace'
|
||||
}
|
||||
onPropertyAdded={onPropertyAdded}
|
||||
onChange={onPropertyChange}
|
||||
/>
|
||||
<div className={styles.tableHeaderDivider} />
|
||||
<DocDatabaseBacklinkInfo
|
||||
onChange={onDatabasePropertyChange}
|
||||
defaultOpen={
|
||||
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
|
||||
// the page title
|
||||
export const DocPropertiesTable = ({
|
||||
defaultOpenProperty,
|
||||
}: DocPropertiesTableProps) => {
|
||||
return <DocPropertiesTableInner defaultOpenProperty={defaultOpenProperty} />;
|
||||
export const DocPropertiesTable = (props: DocPropertiesTableProps) => {
|
||||
return <DocPropertiesTableInner {...props} />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TagLike } from '@affine/component/ui/tags';
|
||||
import { TagsInlineEditor as TagsInlineEditorComponent } from '@affine/component/ui/tags';
|
||||
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
|
||||
import track from '@affine/track';
|
||||
import {
|
||||
LiveData,
|
||||
useLiveData,
|
||||
@@ -38,6 +39,9 @@ export const TagsInlineEditor = ({
|
||||
const onCreateTag = useCallback(
|
||||
(name: string, color: string) => {
|
||||
const newTag = tagService.tagList.createTag(name, color);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
return {
|
||||
id: newTag.id,
|
||||
value: newTag.value$.value,
|
||||
@@ -50,6 +54,9 @@ export const TagsInlineEditor = ({
|
||||
const onSelectTag = useCallback(
|
||||
(tagId: string) => {
|
||||
tagService.tagList.tagByTagId$(tagId).value?.tag(pageId);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
},
|
||||
[pageId, tagService.tagList]
|
||||
);
|
||||
@@ -57,6 +64,9 @@ export const TagsInlineEditor = ({
|
||||
const onDeselectTag = useCallback(
|
||||
(tagId: string) => {
|
||||
tagService.tagList.tagByTagId$(tagId).value?.untag(pageId);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
},
|
||||
[pageId, tagService.tagList]
|
||||
);
|
||||
@@ -68,6 +78,9 @@ export const TagsInlineEditor = ({
|
||||
} else if (property === 'color') {
|
||||
tagService.tagList.tagByTagId$(id).value?.changeColor(value);
|
||||
}
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
},
|
||||
[tagService.tagList]
|
||||
);
|
||||
@@ -77,6 +90,9 @@ export const TagsInlineEditor = ({
|
||||
const onTagDelete = useAsyncCallback(
|
||||
async (id: string) => {
|
||||
await deleteTags([id]);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
},
|
||||
[deleteTags]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user