mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 03:19:52 +08:00
feat(editor): add created-time and created-by property for database block (#12156)
close: BS-3431 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added "Created By" property type and cell renderer, displaying creator's avatar and name in database blocks. - Introduced "Created Time" property type and cell renderer, showing formatted creation timestamps. - **Improvements** - Enhanced table and Kanban views with improved column and row movement, hiding, and statistics capabilities. - Streamlined property and row management with unified object handling and reactive signals for better performance and reliability. - Improved avatar display logic to handle removed or unnamed users gracefully. - Refactored property and row APIs to consolidate access patterns and support reactive updates. - Updated icon retrieval and reactive value access for improved UI responsiveness in database and Kanban cells. - Consolidated property and cell access methods to use "get or create" patterns ensuring consistent data availability. - Added locking mechanism to stabilize computed signals during locked states. - Modularized table and Kanban column and row abstractions for better encapsulation and maintainability. - **Bug Fixes** - Corrected row and column deletion, movement, and selection behaviors across table and Kanban views. - Fixed issues with property and row referencing, ensuring consistent handling of identifiers and objects. - Removed debugging logs and fixed method calls to align with updated APIs. - **Style** - Updated and simplified table column header styles for a cleaner appearance. - **Chores** - Added `@emotion/css` dependency for styling support. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { propertyType, t } from '@blocksuite/affine/blocks/database';
|
||||
import zod from 'zod';
|
||||
|
||||
export const createdByColumnType = propertyType('created-by');
|
||||
export const createdByPropertyModelConfig = createdByColumnType.modelConfig({
|
||||
name: 'Created By',
|
||||
propertyData: {
|
||||
schema: zod.object({}),
|
||||
default: () => ({}),
|
||||
},
|
||||
fixed: {
|
||||
defaultData: {},
|
||||
defaultOrder: 'end',
|
||||
defaultShow: false,
|
||||
},
|
||||
rawValue: {
|
||||
schema: zod.string().nullable(),
|
||||
default: () => null,
|
||||
toString: ({ value }) => value ?? '',
|
||||
fromString: () => {
|
||||
return { value: null };
|
||||
},
|
||||
toJson: ({ value }) => value,
|
||||
fromJson: ({ value }) => value,
|
||||
},
|
||||
jsonValue: {
|
||||
schema: zod.string().nullable(),
|
||||
isEmpty: () => false,
|
||||
type: () => t.string.instance(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Avatar, uniReactRoot } from '@affine/component';
|
||||
import {
|
||||
type CellRenderProps,
|
||||
createIcon,
|
||||
type DataViewCellLifeCycle,
|
||||
HostContextKey,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
UserProvider,
|
||||
type UserService,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
forwardRef,
|
||||
type ForwardRefRenderFunction,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import { useSignalValue } from '../../../../modules/doc-info/utils';
|
||||
import { createdByPropertyModelConfig } from './define';
|
||||
|
||||
const cellContainer = css({
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
gap: '6px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
const memberPreviewContainer = css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
const memberName = css({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: '14px',
|
||||
lineHeight: '22px',
|
||||
});
|
||||
const avatar = css({
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
const CreatedByCellComponent: ForwardRefRenderFunction<
|
||||
DataViewCellLifeCycle,
|
||||
CellRenderProps<{}, string | null, string | null>
|
||||
> = (props, ref): ReactNode => {
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
beforeEnterEditMode: () => {
|
||||
return false;
|
||||
},
|
||||
beforeExitEditingMode: () => {},
|
||||
afterEnterEditingMode: () => {},
|
||||
focusCell: () => true,
|
||||
blurCell: () => true,
|
||||
forceUpdate: () => {},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const host = props.cell.view.contextGet(HostContextKey);
|
||||
const userService = host?.std.getOptional(UserProvider);
|
||||
const memberId = useSignalValue(props.cell.value$);
|
||||
if (!memberId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div className={cellContainer}>
|
||||
<MemberPreview
|
||||
key={memberId}
|
||||
memberId={memberId}
|
||||
userService={userService}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useMemberInfo = (
|
||||
id: string,
|
||||
userService: UserService | null | undefined
|
||||
) => {
|
||||
useEffect(() => {
|
||||
userService?.revalidateUserInfo(id);
|
||||
}, [id, userService]);
|
||||
return useSignalValue(userService?.userInfo$(id));
|
||||
};
|
||||
|
||||
const MemberPreview = ({
|
||||
memberId,
|
||||
userService,
|
||||
}: {
|
||||
memberId: string;
|
||||
userService: UserService | null | undefined;
|
||||
}) => {
|
||||
const userInfo = useMemberInfo(memberId, userService);
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={memberPreviewContainer}>
|
||||
<Avatar
|
||||
name={userInfo.removed ? undefined : (userInfo.name ?? undefined)}
|
||||
className={avatar}
|
||||
url={!userInfo.removed ? userInfo.avatar : undefined}
|
||||
size={24}
|
||||
/>
|
||||
<div className={memberName}>
|
||||
{userInfo.removed ? 'Deleted user' : userInfo.name || 'Unnamed'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CreatedByCell = forwardRef(CreatedByCellComponent);
|
||||
|
||||
export const createdByPropertyConfig =
|
||||
createdByPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('UserIcon'),
|
||||
cellRenderer: {
|
||||
view: uniReactRoot.createUniComponent(CreatedByCell),
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { PropertyMetaConfig } from '@blocksuite/affine/blocks/database';
|
||||
|
||||
import { createdByPropertyConfig } from './created-by/view';
|
||||
import { filePropertyConfig } from './file/view';
|
||||
import { memberPropertyConfig } from './member/view';
|
||||
|
||||
export const propertiesPresets: PropertyMetaConfig<string, any, any, any>[] = [
|
||||
filePropertyConfig,
|
||||
memberPropertyConfig,
|
||||
createdByPropertyConfig,
|
||||
];
|
||||
|
||||
+6
-1
@@ -205,7 +205,11 @@ export const MemberListItem = (props: {
|
||||
data-selected={isSelected ? 'true' : 'false'}
|
||||
>
|
||||
<div className={styles.avatar}>
|
||||
<Avatar url={member.avatar} size={24} />
|
||||
<Avatar
|
||||
name={member.removed ? undefined : (member.name ?? undefined)}
|
||||
url={member.avatar}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.memberName}>{member.name}</div>
|
||||
</div>
|
||||
@@ -228,6 +232,7 @@ export const MemberPreview = ({
|
||||
return (
|
||||
<div className={styles.memberPreviewContainer}>
|
||||
<Avatar
|
||||
name={userInfo.removed ? undefined : (userInfo.name ?? undefined)}
|
||||
className={styles.avatar}
|
||||
url={!userInfo.removed ? userInfo.avatar : undefined}
|
||||
size={16}
|
||||
|
||||
@@ -161,6 +161,7 @@ const MemberPreview = ({
|
||||
return (
|
||||
<div className={styles.memberPreviewContainer}>
|
||||
<Avatar
|
||||
name={userInfo.removed ? undefined : (userInfo.name ?? undefined)}
|
||||
className={styles.avatar}
|
||||
url={!userInfo.removed ? userInfo.avatar : undefined}
|
||||
size={24}
|
||||
|
||||
Reference in New Issue
Block a user