refactor(core): doc property (#8465)

doc property upgraded to use orm.

The visibility of the property are simplified to three types: `always show`, `always hide`, `hide when empty`, and the default is `always show`.

![CleanShot 2024-10-14 at 15 34 52](https://github.com/user-attachments/assets/748b8b80-061f-4d6a-8579-52e59df717c2)

Added a sidebar view to manage properties
![CleanShot 2024-10-14 at 15 35 58](https://github.com/user-attachments/assets/bffa9b1a-a1a5-4708-b2e8-4963120f3af9)

new property ui in workspace settings
![CleanShot 2024-10-14 at 15 36 44](https://github.com/user-attachments/assets/572d8dcc-9b3d-462a-9bcc-5f5fa8e622da)

Property lists can be collapsed
![CleanShot 2024-10-14 at 15 37 59](https://github.com/user-attachments/assets/2b20be1a-8141-478a-8fe7-405aff6d04fd)
This commit is contained in:
EYHN
2024-10-15 10:17:11 +00:00
parent 13b24eb823
commit 24e0c5797c
88 changed files with 3151 additions and 3617 deletions
@@ -182,6 +182,11 @@ export const useDraggable = <D extends DNDData = DNDData>(
let previewPosition: DraggableDragPreviewPosition =
options.dragPreviewPosition ?? 'native';
source.element.dataset['dragPreview'] = 'true';
requestAnimationFrame(() => {
delete source.element.dataset['dragPreview'];
});
if (enableCustomDragPreview.current) {
setCustomNativeDragPreview({
getOffset: (...args) => {
@@ -49,6 +49,12 @@ export const treeLine = style({
height: 2,
right: 0,
},
selectors: {
['&[data-no-terminal="true"]::before']: {
display: 'none',
},
},
});
export const lineAboveStyles = style({
@@ -143,7 +149,7 @@ export const left = style({
export const edgeLine = style({
vars: {
[terminalSize]: '8px',
[terminalSize]: '6px',
},
display: 'block',
position: 'absolute',
@@ -156,11 +162,17 @@ export const edgeLine = style({
// Terminal
'::before': {
content: '""',
width: terminalSize,
height: terminalSize,
width: 0,
height: 0,
boxSizing: 'border-box',
position: 'absolute',
border: `${terminalSize} solid ${cssVar('--affine-primary-color')}`,
borderRadius: '50%',
},
selectors: {
['&[data-no-terminal="true"]::before']: {
display: 'none',
},
},
});
@@ -10,14 +10,17 @@ import * as styles from './drop-indicator.css';
export type DropIndicatorProps = {
instruction?: Instruction | null;
edge?: Edge | null;
noTerminal?: boolean;
};
function getTreeElement({
instruction,
isBlocked,
noTerminal,
}: {
instruction: Exclude<Instruction, { type: 'instruction-blocked' }>;
isBlocked: boolean;
noTerminal?: boolean;
}): ReactElement | null {
const style = {
[styles.horizontalIndent]: `${instruction.currentLevel * instruction.indentPerLevel}px`,
@@ -31,6 +34,7 @@ function getTreeElement({
<div
className={clsx(styles.treeLine, styles.lineAboveStyles)}
style={assignInlineVars(style)}
data-no-terminal={noTerminal}
/>
);
}
@@ -39,6 +43,7 @@ function getTreeElement({
<div
className={clsx(styles.treeLine, styles.lineBelowStyles)}
style={assignInlineVars(style)}
data-no-terminal={noTerminal}
/>
);
}
@@ -48,6 +53,7 @@ function getTreeElement({
<div
className={clsx(styles.outlineStyles)}
style={assignInlineVars(style)}
data-no-terminal={noTerminal}
/>
);
}
@@ -61,6 +67,7 @@ function getTreeElement({
<div
className={clsx(styles.treeLine, styles.lineBelowStyles)}
style={assignInlineVars(style)}
data-no-terminal={noTerminal}
/>
);
}
@@ -88,13 +95,14 @@ const edgeStyles: Record<Edge, string> = {
right: styles.right,
};
function getEdgeElement(edge: Edge, gap: number = 0) {
function getEdgeElement(edge: Edge, gap: number = 0, noTerminal?: boolean) {
const lineOffset = `calc(-0.5 * (${gap}px + 2px))`;
const orientation = edgeToOrientationMap[edge];
return (
<div
data-no-terminal={noTerminal}
className={clsx([
styles.edgeLine,
orientationStyles[orientation],
@@ -105,9 +113,13 @@ function getEdgeElement(edge: Edge, gap: number = 0) {
);
}
export function DropIndicator({ instruction, edge }: DropIndicatorProps) {
export function DropIndicator({
instruction,
edge,
noTerminal,
}: DropIndicatorProps) {
if (edge) {
return getEdgeElement(edge, 0);
return getEdgeElement(edge, 0, noTerminal);
}
if (instruction) {
if (instruction.type === 'instruction-blocked') {
@@ -0,0 +1 @@
export * from './property';
@@ -0,0 +1,163 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
export const propertyRoot = style({
display: 'flex',
minHeight: 32,
position: 'relative',
padding: '2px 0px 2px 2px',
selectors: {
'&[draggable="true"]': {
cursor: 'grab',
},
'&[draggable="true"][data-dragging="true"]': {
visibility: 'hidden',
},
'&[draggable="true"]:before': {
content: '""',
display: 'block',
position: 'absolute',
cursor: 'grab',
top: '50%',
left: 0,
borderRadius: '2px',
backgroundColor: cssVarV2('text/placeholder'),
transform: 'translate(-6px, -50%)',
transition: 'height 0.2s 0.1s, opacity 0.2s 0.1s',
opacity: 0,
height: '4px',
width: '4px',
willChange: 'height, opacity',
},
'&[draggable="true"]:after': {
content: '""',
display: 'block',
position: 'absolute',
cursor: 'grab',
top: '50%',
left: 0,
borderRadius: '2px',
backgroundColor: 'transparent',
transform: 'translate(-8px, -50%)',
height: '100%',
width: '8px',
willChange: 'height, opacity',
},
'&[draggable="true"]:hover:before': {
height: 12,
opacity: 1,
},
},
});
export const hide = style({
// Visually hide the property while maintaining its position in the layout.
// This ensures that any open menu remains in the same position when the property is hidden.
overflow: 'hidden',
height: '0px',
minHeight: '0px',
padding: '0px',
visibility: 'hidden',
pointerEvents: 'none',
});
export const propertyNameContainer = style({
display: 'flex',
flexDirection: 'column',
position: 'relative',
borderRadius: 4,
fontSize: cssVar('fontSm'),
padding: `6px 6px 6px 4px`,
flexShrink: 0,
lineHeight: '22px',
userSelect: 'none',
color: cssVarV2('text/secondary'),
width: '160px',
selectors: {
'&[data-has-menu="true"]': {
cursor: 'pointer',
},
'&[data-has-menu="true"]:hover': {
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
},
},
});
globalStyle(`[data-drag-preview] ${propertyNameContainer}:hover`, {
backgroundColor: 'transparent',
});
export const propertyNameInnerContainer = style({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 6,
});
export const propertyIconContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '2px',
fontSize: 16,
color: cssVarV2('icon/secondary'),
});
export const propertyNameContent = style({
flexGrow: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: cssVar('fontSm'),
});
export const propertyValueContainer = style({
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
position: 'relative',
borderRadius: 4,
fontSize: cssVar('fontSm'),
lineHeight: '22px',
padding: `6px`,
flex: 1,
':focus-visible': {
outline: 'none',
},
'::placeholder': {
color: cssVarV2('text/placeholder'),
},
selectors: {
'&[data-readonly="false"]': {
cursor: 'pointer',
},
'&[data-readonly="false"]:hover': {
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
},
'&[data-readonly="false"]:focus-within': {
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
},
},
});
export const tableButton = style({
alignSelf: 'flex-start',
fontSize: cssVar('fontSm'),
color: `${cssVarV2('text/secondary')}`,
padding: '0 6px',
height: 36,
fontWeight: 400,
gap: 6,
'@media': {
print: {
display: 'none',
},
},
});
globalStyle(`${tableButton} svg`, {
fontSize: 16,
color: cssVarV2('icon/secondary'),
});
globalStyle(`${tableButton}:hover svg`, {
color: cssVarV2('icon/primary'),
});
@@ -0,0 +1,144 @@
import { FrameIcon } from '@blocksuite/icons/rc';
import { useDraggable, useDropTarget } from '../dnd';
import { MenuItem } from '../menu';
import {
PropertyCollapsible,
PropertyName,
PropertyRoot,
PropertyValue,
} from './property';
export default {
title: 'UI/Property',
};
export const SingleProperty = () => {
return (
<>
<PropertyRoot>
<PropertyName name="Name" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot>
<PropertyName name="Long nameeeeeeeeeeeeeeeee" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot>
<PropertyName
name="With Menu"
icon={<FrameIcon />}
menuItems={<MenuItem>Menu</MenuItem>}
/>
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot>
<PropertyName name="Readonly" icon={<FrameIcon />} />
<PropertyValue readonly>Readonly Value</PropertyValue>
</PropertyRoot>
</>
);
};
export const DNDProperty = () => {
const { dragRef: dragRef1 } = useDraggable(
() => ({
data: { text: 'hello' },
}),
[]
);
const { dragRef: dragRef2 } = useDraggable(
() => ({
data: { text: 'hello' },
}),
[]
);
const { dropTargetRef, closestEdge } = useDropTarget(
() => ({
closestEdge: {
allowedEdges: ['top', 'bottom'],
},
}),
[]
);
return (
<>
<PropertyRoot ref={dragRef1}>
<PropertyName name="Draggable" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot ref={dragRef2}>
<PropertyName
name="Draggable Menu"
icon={<FrameIcon />}
menuItems={<MenuItem>Menu</MenuItem>}
/>
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot ref={dropTargetRef} dropIndicatorEdge={closestEdge}>
<PropertyName name="DropTarget" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
</>
);
};
export const HideEmptyProperty = () => {
return (
<>
<PropertyRoot hideEmpty>
<PropertyName name="Should not be hidden" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot hideEmpty>
<PropertyName name="Should be hidden" icon={<FrameIcon />} />
<PropertyValue isEmpty>Value</PropertyValue>
</PropertyRoot>
</>
);
};
export const BasicPropertyCollapsible = () => {
return (
<PropertyCollapsible collapsible>
<PropertyRoot>
<PropertyName name="Always show" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot hideEmpty>
<PropertyName name="Hide with empty" icon={<FrameIcon />} />
<PropertyValue isEmpty>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot hide>
<PropertyName name="Hide" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
</PropertyCollapsible>
);
};
export const PropertyCollapsibleCustomButton = () => {
return (
<PropertyCollapsible
collapsible
collapseButtonText={({ hide, isCollapsed }) =>
`${isCollapsed ? 'Show' : 'Hide'} ${hide} properties`
}
>
<PropertyRoot>
<PropertyName name="Always show" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot hideEmpty>
<PropertyName name="Hide with empty" icon={<FrameIcon />} />
<PropertyValue isEmpty>Value</PropertyValue>
</PropertyRoot>
<PropertyRoot hide>
<PropertyName name="Hide" icon={<FrameIcon />} />
<PropertyValue>Value</PropertyValue>
</PropertyRoot>
</PropertyCollapsible>
);
};
@@ -0,0 +1,276 @@
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
import { ArrowDownSmallIcon, ArrowUpSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import {
createContext,
forwardRef,
type HTMLProps,
type PropsWithChildren,
type ReactNode,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import { Button } from '../button';
import { DropIndicator } from '../dnd';
import { Menu } from '../menu';
import * as styles from './property.css';
const PropertyTableContext = createContext<{
mountProperty: (payload: { isHide: boolean }) => () => void;
showAllHide: boolean;
} | null>(null);
export const PropertyCollapsible = forwardRef<
HTMLDivElement,
PropsWithChildren<{
collapsible?: boolean;
defaultCollapsed?: boolean;
collapsed?: boolean;
onCollapseChange?: (collapsed: boolean) => void;
collapseButtonText?: (option: {
total: number;
hide: number;
isCollapsed: boolean;
}) => ReactNode;
}> &
HTMLProps<HTMLDivElement>
>(
(
{
children,
collapsible = true,
collapsed,
defaultCollapsed,
onCollapseChange,
collapseButtonText,
...props
},
ref
) => {
const [propertyCount, setPropertyCount] = useState({ total: 0, hide: 0 });
const [showAllHide, setShowAllHide] = useState(!defaultCollapsed);
const finalCollapsible = collapsible ? propertyCount.hide !== 0 : false;
const controlled = collapsed !== undefined;
const finalShowAllHide = finalCollapsible
? !controlled
? showAllHide
: !collapsed
: true;
const mountProperty = useCallback((payload: { isHide: boolean }) => {
setPropertyCount(prev => ({
total: prev.total + 1,
hide: prev.hide + (payload.isHide ? 1 : 0),
}));
return () => {
setPropertyCount(prev => ({
total: prev.total - 1,
hide: prev.hide - (payload.isHide ? 1 : 0),
}));
};
}, []);
const contextValue = useMemo(
() => ({ mountProperty, showAllHide: finalShowAllHide }),
[mountProperty, finalShowAllHide]
);
const handleShowAllHide = useCallback(() => {
setShowAllHide(!finalShowAllHide);
onCollapseChange?.(finalShowAllHide);
}, [finalShowAllHide, onCollapseChange]);
return (
<div
ref={ref}
data-property-collapsible={finalCollapsible}
data-property-collapsed={!finalShowAllHide}
{...props}
>
<PropertyTableContext.Provider value={contextValue}>
{children}
{finalCollapsible && (
<Button
variant="plain"
prefix={
!finalShowAllHide ? (
<ArrowDownSmallIcon />
) : (
<ArrowUpSmallIcon />
)
}
className={styles.tableButton}
onClick={handleShowAllHide}
data-testid="property-collapsible-button"
>
{collapseButtonText
? collapseButtonText({
total: propertyCount.total,
hide: propertyCount.hide,
isCollapsed: !finalShowAllHide,
})
: !finalShowAllHide
? 'Show All'
: 'Hide'}
</Button>
)}
</PropertyTableContext.Provider>
</div>
);
}
);
PropertyCollapsible.displayName = 'PropertyCollapsible';
const PropertyRootContext = createContext<{
mountValue: (payload: { isEmpty: boolean }) => () => void;
} | null>(null);
export const PropertyRoot = forwardRef<
HTMLDivElement,
{
dropIndicatorEdge?: Edge | null;
hideEmpty?: boolean;
hide?: boolean;
} & HTMLProps<HTMLDivElement>
>(
(
{ children, className, dropIndicatorEdge, hideEmpty, hide, ...props },
ref
) => {
const [isEmpty, setIsEmpty] = useState(false);
const context = useContext(PropertyTableContext);
const preferHide = hide || (hideEmpty && isEmpty);
const showAllHide = context?.showAllHide;
const shouldHide = preferHide && !showAllHide;
useLayoutEffect(() => {
if (context) {
return context.mountProperty({ isHide: !!preferHide });
}
return;
}, [context, preferHide]);
const contextValue = useMemo(
() => ({
mountValue: (payload: { isEmpty: boolean }) => {
setIsEmpty(payload.isEmpty);
return () => {
setIsEmpty(false);
};
},
}),
[setIsEmpty]
);
return (
<PropertyRootContext.Provider value={contextValue}>
<div
ref={ref}
className={clsx(
styles.propertyRoot,
shouldHide && styles.hide,
className
)}
{...props}
>
{children}
<DropIndicator edge={dropIndicatorEdge} />
</div>
</PropertyRootContext.Provider>
);
}
);
PropertyRoot.displayName = 'PropertyRoot';
export const PropertyName = ({
icon,
name,
className,
menuItems,
defaultOpenMenu,
...props
}: {
icon?: ReactNode;
name?: ReactNode;
menuItems?: ReactNode;
defaultOpenMenu?: boolean;
} & HTMLProps<HTMLDivElement>) => {
const [menuOpen, setMenuOpen] = useState(defaultOpenMenu);
const hasMenu = !!menuItems;
const handleClick = useCallback(() => {
if (!hasMenu) return;
setMenuOpen(true);
}, [hasMenu]);
const handleMenuClose = useCallback((open: boolean) => {
if (!open) {
setMenuOpen(false);
}
}, []);
const content = (
<div
className={clsx(styles.propertyNameContainer, className)}
data-has-menu={hasMenu}
onClick={handleClick}
{...props}
>
<div className={styles.propertyNameInnerContainer}>
{icon && <div className={styles.propertyIconContainer}>{icon}</div>}
<div className={styles.propertyNameContent}>{name}</div>
</div>
</div>
);
if (menuOpen && menuItems) {
// Do not mount <Menu /> when menuOpen is false, as <Menu /> will cause draggable to not work
return (
<Menu
items={menuItems}
rootOptions={{
open: true,
modal: true, // false will case bug
onOpenChange: handleMenuClose,
}}
>
{content}
</Menu>
);
}
return content;
};
export const PropertyValue = forwardRef<
HTMLDivElement,
{ readonly?: boolean; isEmpty?: boolean } & HTMLProps<HTMLDivElement>
>(({ children, className, readonly, isEmpty, ...props }, ref) => {
const context = useContext(PropertyRootContext);
useLayoutEffect(() => {
if (context) {
return context.mountValue({ isEmpty: !!isEmpty });
}
return;
}, [context, isEmpty]);
return (
<div
ref={ref}
className={clsx(styles.propertyValueContainer, className)}
data-readonly={readonly ? 'true' : 'false'}
data-empty={isEmpty ? 'true' : 'false'}
data-property-value
{...props}
>
{children}
</div>
);
});
PropertyValue.displayName = 'PropertyValue';