mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 00:28:33 +00:00
feat(core): card view drag handle for doc explorer (#12431)
close AF-2624, AF-2628, AF-2581 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a draggable handle to document cards in the explorer, visible on hover in card view. - Added an option to remove grouping in the display menu. - Added contextual tooltips for user avatars indicating creation or last update. - Enabled optional tooltips on public user labels. - Extended dropdown buttons to accept custom styling classes. - Added a new masonry story showcasing item heights determined by ratios. - **Style** - Enhanced drag handle appearance and visibility for card view items. - Replaced static shadows with theme-aware, smoothly transitioning shadows on card items. - Adjusted spacing between items in the document explorer for improved layout, with increased horizontal and (in card view) vertical gaps. - Reduced top padding in workspace page styles. - Added new button background style for secondary buttons. - **Bug Fixes** - Removed duplicate internal property declarations to eliminate redundancy. - **Refactor** - Simplified layout props by removing fixed height parameters in multiple components. - Updated masonry layout logic to support ratio-based item sizing alongside fixed heights. - Removed randomized skeleton loading placeholders, replacing them with fixed or no placeholders. - Refined masonry component typings and scrollbar placement for improved styling and layout. - Improved selection logic to activate selection mode when selecting all documents. - **Localization** - Added new translation keys for grouping removal and user attribution tooltips. - Updated English locale with new strings for "Remove group" and user-created/updated tooltips. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
@@ -12,28 +13,33 @@ type DropdownButtonProps = {
|
||||
export const DropdownButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
DropdownButtonProps
|
||||
>(({ onClickDropDown, children, size = 'default', ...props }, ref) => {
|
||||
const handleClickDropDown: MouseEventHandler<HTMLElement> = e => {
|
||||
e.stopPropagation();
|
||||
onClickDropDown?.(e);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-size={size}
|
||||
className={styles.dropdownBtn}
|
||||
{...props}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<span className={styles.divider} />
|
||||
<span className={styles.dropdownWrapper} onClick={handleClickDropDown}>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.dropdownIcon}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
>(
|
||||
(
|
||||
{ onClickDropDown, children, size = 'default', className, ...props },
|
||||
ref
|
||||
) => {
|
||||
const handleClickDropDown: MouseEventHandler<HTMLElement> = e => {
|
||||
e.stopPropagation();
|
||||
onClickDropDown?.(e);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-size={size}
|
||||
className={clsx(styles.dropdownBtn, className)}
|
||||
{...props}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<span className={styles.divider} />
|
||||
<span className={styles.dropdownWrapper} onClick={handleClickDropDown}>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.dropdownIcon}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
DropdownButton.displayName = 'DropdownButton';
|
||||
|
||||
@@ -279,3 +279,34 @@ export const MultiViewTransition = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const availableRatios = [0.8, 1.2, 1.4, 1.5, 1.6, 1.7, 1.8];
|
||||
const ratioItems = Array.from({ length: 10000 }, (_, i) => {
|
||||
const ratio =
|
||||
availableRatios[Math.floor(Math.random() * availableRatios.length)];
|
||||
return {
|
||||
id: i.toString(),
|
||||
ratio,
|
||||
children: (
|
||||
<Card>
|
||||
{i} <br /> ratio: {ratio}
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
});
|
||||
export const HeightByRatio = () => {
|
||||
return (
|
||||
<ResizePanel width={800} height={600}>
|
||||
<Masonry
|
||||
gapX={10}
|
||||
gapY={10}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
paddingX={12}
|
||||
paddingY={12}
|
||||
items={ratioItems}
|
||||
virtualScroll
|
||||
itemWidthMin={120}
|
||||
/>
|
||||
</ResizePanel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -286,7 +286,6 @@ export const Masonry = ({
|
||||
})}
|
||||
<div data-masonry-placeholder style={{ height }} />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
{stickyGroup ? (
|
||||
<div
|
||||
className={clsx(styles.stickyGroupHeader, stickyGroup.className)}
|
||||
@@ -310,16 +309,16 @@ export const Masonry = ({
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<Scrollable.Scrollbar className={styles.scrollbar} />
|
||||
</Scrollable.Root>
|
||||
);
|
||||
};
|
||||
|
||||
interface MasonryItemProps
|
||||
extends MasonryItem,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'id' | 'height'> {
|
||||
locateMode?: 'transform' | 'leftTop' | 'transform3d';
|
||||
xywh?: MasonryItemXYWH;
|
||||
}
|
||||
type MasonryItemProps = MasonryItem &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'id' | 'height'> & {
|
||||
locateMode?: 'transform' | 'leftTop' | 'transform3d';
|
||||
xywh?: MasonryItemXYWH;
|
||||
};
|
||||
|
||||
const MasonryGroupHeader = memo(function MasonryGroupHeader({
|
||||
id,
|
||||
@@ -330,7 +329,6 @@ const MasonryGroupHeader = memo(function MasonryGroupHeader({
|
||||
groupId,
|
||||
itemCount,
|
||||
collapsed,
|
||||
height,
|
||||
paddingX,
|
||||
...props
|
||||
}: Omit<MasonryItemProps, 'Component'> & {
|
||||
@@ -356,7 +354,6 @@ const MasonryGroupHeader = memo(function MasonryGroupHeader({
|
||||
return (
|
||||
<MasonryItem
|
||||
id={id}
|
||||
height={height}
|
||||
style={{
|
||||
padding: `0 ${paddingX}px`,
|
||||
height: '100%',
|
||||
@@ -404,7 +401,7 @@ const MasonryItem = memo(function MasonryItem({
|
||||
className,
|
||||
style: styleProp,
|
||||
...props
|
||||
}: MasonryItemProps) {
|
||||
}: Omit<MasonryItemProps, 'height' | 'ratio'>) {
|
||||
const style = useMemo(() => {
|
||||
if (!xywh) return { display: 'none' };
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ export const stickyGroupHeader = style({
|
||||
top: 0,
|
||||
width: '100%',
|
||||
});
|
||||
export const scrollbar = style({
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const item = style({
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export interface MasonryItem extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export type MasonryItem = React.HTMLAttributes<HTMLDivElement> & {
|
||||
id: string;
|
||||
height: number;
|
||||
Component?: React.ComponentType<{ groupId: string; itemId: string }>;
|
||||
}
|
||||
} & ({ height: number } | { ratio: number });
|
||||
|
||||
export interface MasonryGroup extends React.HTMLAttributes<HTMLDivElement> {
|
||||
id: string;
|
||||
|
||||
@@ -82,6 +82,7 @@ export const calcLayout = (
|
||||
|
||||
groups.forEach((group, index) => {
|
||||
const heightStack = Array.from({ length: columns }, () => 0);
|
||||
const ratioStack = Array.from({ length: columns }, () => 0);
|
||||
if (index !== 0) {
|
||||
finalHeight += groupsGap;
|
||||
}
|
||||
@@ -101,24 +102,52 @@ export const calcLayout = (
|
||||
return;
|
||||
}
|
||||
|
||||
finalHeight += groupHeaderLayout.h + groupHeaderGapWithItems;
|
||||
finalHeight +=
|
||||
groupHeaderLayout.h +
|
||||
// if group header is empty, don't add gap
|
||||
(groupHeaderLayout.h > 0 ? groupHeaderGapWithItems : 0);
|
||||
// calculate group items
|
||||
group.items.forEach(item => {
|
||||
const itemId = group.id ? `${group.id}:${item.id}` : item.id;
|
||||
const minHeight = Math.min(...heightStack);
|
||||
const minHeightIndex = heightStack.indexOf(minHeight);
|
||||
const hasGap = heightStack[minHeightIndex] ? gapY : 0;
|
||||
const x = minHeightIndex * (width + gapX) + paddingX;
|
||||
const y = finalHeight + minHeight + hasGap;
|
||||
const ratioMode = 'ratio' in item;
|
||||
const height = ratioMode ? item.ratio * width : item.height;
|
||||
|
||||
heightStack[minHeightIndex] += item.height + hasGap;
|
||||
layout.set(itemId, {
|
||||
type: 'item',
|
||||
x,
|
||||
y,
|
||||
w: width,
|
||||
h: item.height,
|
||||
});
|
||||
if (ratioMode) {
|
||||
const minRatio = Math.min(...ratioStack);
|
||||
const minRatioIndex = ratioStack.indexOf(minRatio);
|
||||
const minHeight = heightStack[minRatioIndex];
|
||||
const hasGap = heightStack[minRatioIndex] ? gapY : 0;
|
||||
const x = minRatioIndex * (width + gapX) + paddingX;
|
||||
const y = finalHeight + minHeight + hasGap;
|
||||
|
||||
ratioStack[minRatioIndex] += item.ratio * 10000;
|
||||
heightStack[minRatioIndex] += height + hasGap;
|
||||
layout.set(itemId, {
|
||||
type: 'item',
|
||||
x,
|
||||
y,
|
||||
w: width,
|
||||
h: height,
|
||||
});
|
||||
} else {
|
||||
const minHeight = Math.min(...heightStack);
|
||||
const minHeightIndex = heightStack.indexOf(minHeight);
|
||||
const hasGap = heightStack[minHeightIndex] ? gapY : 0;
|
||||
const x = minHeightIndex * (width + gapX) + paddingX;
|
||||
const y = finalHeight + minHeight + hasGap;
|
||||
|
||||
const ratio = height / width;
|
||||
heightStack[minHeightIndex] += height + hasGap;
|
||||
ratioStack[minHeightIndex] += ratio * 10000;
|
||||
|
||||
layout.set(itemId, {
|
||||
type: 'item',
|
||||
x,
|
||||
y,
|
||||
w: width,
|
||||
h: height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const groupHeight = Math.max(...heightStack) + paddingY;
|
||||
|
||||
@@ -49,6 +49,9 @@ export const createDocExplorerContext = (
|
||||
selectedDocIds$: new LiveData<string[]>([]),
|
||||
prevCheckAnchorId$: new LiveData<string | null>(null),
|
||||
displayPreference$: displayPreference$,
|
||||
showDragHandle$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showDragHandle
|
||||
),
|
||||
view$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.view
|
||||
),
|
||||
@@ -85,9 +88,6 @@ export const createDocExplorerContext = (
|
||||
showMoreOperation$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showMoreOperation
|
||||
),
|
||||
showDragHandle$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showDragHandle
|
||||
),
|
||||
quickDeletePermanently$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickDeletePermanently
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from '@affine/component';
|
||||
import { Divider, MenuItem } from '@affine/component';
|
||||
import type { GroupByParams } from '@affine/core/modules/collection-rules/types';
|
||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -48,8 +48,9 @@ export const GroupByList = ({
|
||||
onChange,
|
||||
}: {
|
||||
groupBy?: GroupByParams;
|
||||
onChange?: (next: GroupByParams) => void;
|
||||
onChange?: (next: GroupByParams | undefined) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||
const propertyList = useLiveData(workspacePropertyService.sortedProperties$);
|
||||
const explorerPropertyList = useMemo(() => {
|
||||
@@ -66,6 +67,10 @@ export const GroupByList = ({
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
<Divider size="thinner" style={{ margin: '4px 0' }} />
|
||||
<MenuItem onClick={() => onChange?.(undefined)}>
|
||||
{t['com.affine.explorer.display-menu.grouping.remove']()}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const ExplorerDisplayMenu = ({
|
||||
const t = useI18n();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(groupBy: GroupByParams) => {
|
||||
(groupBy: GroupByParams | undefined) => {
|
||||
onDisplayPreferenceChange({ ...displayPreference, groupBy });
|
||||
},
|
||||
[displayPreference, onDisplayPreferenceChange]
|
||||
|
||||
@@ -159,6 +159,14 @@ export const listHide560 = style({
|
||||
export const cardViewRoot = style({
|
||||
vars: {
|
||||
'--ring-color': 'transparent',
|
||||
'--light-shadow':
|
||||
'0px 0px 0px 1px var(--ring-color), 0px 2px 3px rgba(0,0,0,.05)',
|
||||
'--dark-shadow':
|
||||
'0px 0px 0px 1px var(--ring-color), 0px 2px 3px rgba(0,0,0,.05)',
|
||||
'--light-shadow-hover':
|
||||
'0px 0px 0px 1px var(--ring-color), 0px 4px 6px rgba(0,0,0,.1)',
|
||||
'--dark-shadow-hover':
|
||||
'0px 0px 0px 1px var(--ring-color), 0px 4px 6px rgba(0,0,0,.1)',
|
||||
},
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@@ -170,14 +178,30 @@ export const cardViewRoot = style({
|
||||
backgroundColor: cssVarV2.layer.background.mobile.secondary,
|
||||
border: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
// TODO: use variable
|
||||
boxShadow: '0px 0px 0px 1px var(--ring-color),0px 0px 3px rgba(0,0,0,.05)',
|
||||
boxShadow: '0px 0px 0px 1px var(--ring-color), 0px 0px 3px rgba(0,0,0,.05)',
|
||||
overflow: 'hidden',
|
||||
transition: 'box-shadow 0.23s ease, border-color 0.23s ease',
|
||||
selectors: {
|
||||
[`${root}[data-selected="true"] &`]: {
|
||||
vars: {
|
||||
'--ring-color': cssVarV2.layer.insideBorder.primaryBorder,
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
borderColor: cssVarV2.pagelist.hoverBorder,
|
||||
},
|
||||
'[data-theme="light"] &': {
|
||||
boxShadow: 'var(--light-shadow)',
|
||||
},
|
||||
'[data-theme="light"] &:hover': {
|
||||
boxShadow: 'var(--light-shadow-hover)',
|
||||
},
|
||||
'[data-theme="dark"] &': {
|
||||
boxShadow: 'var(--dark-shadow)',
|
||||
},
|
||||
'[data-theme="dark"] &:hover': {
|
||||
boxShadow: 'var(--dark-shadow-hover)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const cardViewHeader = style({
|
||||
@@ -224,3 +248,17 @@ export const cardViewCheckbox = style({
|
||||
color: cssVarV2.icon.primary,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
export const cardDragHandle = style([
|
||||
dragHandle,
|
||||
{
|
||||
left: -4,
|
||||
top: 0,
|
||||
transform: 'translateX(-100%)',
|
||||
opacity: 0,
|
||||
selectors: {
|
||||
[`${cardViewRoot}:hover &`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
Checkbox,
|
||||
DragHandle as DragHandleIcon,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
useDraggable,
|
||||
Wrapper,
|
||||
} from '@affine/component';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
type SVGProps,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { PagePreview } from '../../page-list/page-content-preview';
|
||||
@@ -312,9 +311,6 @@ export const ListViewDoc = ({ docId }: DocListItemProps) => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(docId));
|
||||
const [previewSkeletonWidth] = useState(
|
||||
Math.round(100 + Math.random() * 100)
|
||||
);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
@@ -334,7 +330,7 @@ export const ListViewDoc = ({ docId }: DocListItemProps) => {
|
||||
<DocPreview
|
||||
id={docId}
|
||||
className={styles.listPreview}
|
||||
loading={<Skeleton height={20} width={previewSkeletonWidth} />}
|
||||
loading={<Wrapper height={20} width={10} />}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.listSpace} />
|
||||
@@ -360,19 +356,13 @@ const cardMoreMenuContentOptions = {
|
||||
sideOffset: 12,
|
||||
alignOffset: -4,
|
||||
} as const;
|
||||
const randomPreviewSkeleton = () => {
|
||||
return Array.from({ length: Math.floor(Math.random() * 2) + 1 }, (_, i) => ({
|
||||
id: i,
|
||||
width: Math.round(30 + Math.random() * 70),
|
||||
}));
|
||||
};
|
||||
|
||||
export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
const t = useI18n();
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(docId));
|
||||
const [previewSkeleton] = useState(randomPreviewSkeleton);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
@@ -380,6 +370,7 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
|
||||
return (
|
||||
<li className={styles.cardViewRoot}>
|
||||
<DragHandle id={docId} className={styles.cardDragHandle} />
|
||||
<header className={styles.cardViewHeader}>
|
||||
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||
<DocTitle
|
||||
@@ -404,17 +395,7 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<DocPreview
|
||||
id={docId}
|
||||
className={styles.cardPreviewContainer}
|
||||
loading={
|
||||
<div>
|
||||
{previewSkeleton.map(({ id, width }) => (
|
||||
<Skeleton key={id} height={20} width={`${width}%`} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<DocPreview id={docId} className={styles.cardPreviewContainer} />
|
||||
<CardViewProperties docId={docId} />
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
Masonry,
|
||||
type MasonryGroup,
|
||||
type MasonryItem,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
@@ -69,15 +74,13 @@ const GroupHeader = memo(function GroupHeader({
|
||||
return header;
|
||||
});
|
||||
|
||||
const calcCardHeightById = (id: string, base = 250, scale = 10) => {
|
||||
const ratios = [1.26, 1.304, 1.13, 1.391, 1.521];
|
||||
const calcCardRatioById = (id: string) => {
|
||||
if (!id) {
|
||||
return base;
|
||||
return ratios[0];
|
||||
}
|
||||
const max = 5;
|
||||
const min = 1;
|
||||
const code = id.charCodeAt(0);
|
||||
const value = Math.floor((code % (max - min)) + min);
|
||||
return base + value * scale;
|
||||
return ratios[code % ratios.length];
|
||||
};
|
||||
|
||||
export const DocListItemComponent = memo(function DocListItemComponent({
|
||||
@@ -94,16 +97,12 @@ export const DocsExplorer = ({
|
||||
className,
|
||||
disableMultiDelete,
|
||||
masonryItemWidthMin,
|
||||
heightBase,
|
||||
heightScale,
|
||||
onRestore,
|
||||
onDelete,
|
||||
}: {
|
||||
className?: string;
|
||||
disableMultiDelete?: boolean;
|
||||
masonryItemWidthMin?: number;
|
||||
heightBase?: number;
|
||||
heightScale?: number;
|
||||
onRestore?: (ids: string[]) => void;
|
||||
/** Override the default delete action */
|
||||
onDelete?: (ids: string[]) => void;
|
||||
@@ -129,23 +128,23 @@ export const DocsExplorer = ({
|
||||
height: groupBy ? 24 : 0,
|
||||
className: styles.groupHeader,
|
||||
items: group.items.map((docId: string) => {
|
||||
if (view === 'list') {
|
||||
return {
|
||||
id: docId,
|
||||
Component: DocListItemComponent,
|
||||
height: 42,
|
||||
} satisfies MasonryItem;
|
||||
}
|
||||
return {
|
||||
id: docId,
|
||||
Component: DocListItemComponent,
|
||||
height:
|
||||
view === 'list'
|
||||
? 42
|
||||
: view === 'grid'
|
||||
? 280
|
||||
: calcCardHeightById(docId, heightBase, heightScale),
|
||||
'data-view': view,
|
||||
className: styles.docItem,
|
||||
};
|
||||
ratio: view === 'grid' ? ratios[0] : calcCardRatioById(docId),
|
||||
} satisfies MasonryItem;
|
||||
}),
|
||||
} satisfies MasonryGroup;
|
||||
});
|
||||
return items;
|
||||
}, [groupBy, groups, heightBase, heightScale, view]);
|
||||
}, [groupBy, groups, view]);
|
||||
|
||||
const handleCloseFloatingToolbar = useCallback(() => {
|
||||
contextValue.selectMode$?.next(false);
|
||||
@@ -222,13 +221,18 @@ export const DocsExplorer = ({
|
||||
};
|
||||
}, [contextValue]);
|
||||
|
||||
const responsivePaddingX = useCallback(
|
||||
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Masonry
|
||||
className={className}
|
||||
items={masonryItems}
|
||||
gapY={12}
|
||||
gapX={12}
|
||||
gapY={BUILD_CONFIG.isMobileEdition ? 12 : view === 'list' ? 12 : 24}
|
||||
gapX={BUILD_CONFIG.isMobileEdition ? 12 : 24}
|
||||
groupsGap={12}
|
||||
groupHeaderGapWithItems={12}
|
||||
columns={view === 'list' ? 1 : undefined}
|
||||
@@ -237,10 +241,8 @@ export const DocsExplorer = ({
|
||||
itemWidth={'stretch'}
|
||||
virtualScroll
|
||||
collapsedGroups={collapsedGroups}
|
||||
paddingX={useCallback(
|
||||
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
||||
[]
|
||||
)}
|
||||
paddingY={BUILD_CONFIG.isMobileEdition ? 12 : 0}
|
||||
paddingX={BUILD_CONFIG.isMobileEdition ? 16 : responsivePaddingX}
|
||||
/>
|
||||
{!disableMultiDelete || onRestore ? (
|
||||
<ListFloatingToolbar
|
||||
|
||||
@@ -43,6 +43,9 @@ export const DocGroupHeader = ({
|
||||
}, [groupId, contextValue]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (!contextValue.selectMode$?.value) {
|
||||
contextValue.selectMode$?.next(true);
|
||||
}
|
||||
const prev = contextValue.selectedDocIds$.value;
|
||||
if (isGroupAllSelected) {
|
||||
contextValue.selectedDocIds$.next(
|
||||
|
||||
@@ -2,7 +2,12 @@ import { IconButton, Menu, MenuItem, MenuSeparator } from '@affine/component';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowLeftBigIcon, FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
ArrowLeftBigIcon,
|
||||
CloudWorkspaceIcon,
|
||||
FavoriteIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -57,7 +62,9 @@ export const AddFilterMenu = ({
|
||||
<span className={styles.filterTypeItemName}>{t['Favorited']()}</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
prefixIcon={<FavoriteIcon className={styles.filterTypeItemIcon} />}
|
||||
prefixIcon={
|
||||
<CloudWorkspaceIcon className={styles.filterTypeItemIcon} />
|
||||
}
|
||||
key={'shared'}
|
||||
onClick={() => {
|
||||
onAdd({
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const menuContent = style({
|
||||
backgroundColor: cssVar('backgroundOverlayPanelColor'),
|
||||
});
|
||||
export const button = style({
|
||||
backgroundColor: cssVarV2.button.secondary,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||
import type { MouseEvent, PropsWithChildren } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { menuContent } from './new-page-button.css';
|
||||
import * as styles from './new-page-button.css';
|
||||
|
||||
type NewPageButtonProps = {
|
||||
createNewDoc: (e?: MouseEvent) => void;
|
||||
@@ -120,7 +120,7 @@ export const NewPageButton = ({
|
||||
open,
|
||||
}}
|
||||
contentOptions={{
|
||||
className: menuContent,
|
||||
className: styles.menuContent,
|
||||
align: 'end',
|
||||
hideWhenDetached: true,
|
||||
onInteractOutside: useCallback(() => {
|
||||
@@ -133,6 +133,7 @@ export const NewPageButton = ({
|
||||
onClick={handleCreateNewDoc}
|
||||
onAuxClick={handleCreateNewPage}
|
||||
onClickDropDown={useCallback(() => setOpen(open => !open), [])}
|
||||
className={styles.button}
|
||||
>
|
||||
{children}
|
||||
</DropdownButton>
|
||||
|
||||
@@ -13,6 +13,19 @@ import type { GroupHeaderProps } from '../explorer/types';
|
||||
import { MemberSelectorInline } from '../member-selector';
|
||||
import * as styles from './created-updated-by.css';
|
||||
|
||||
const CreatedUsernameTip = ({ userName }: { userName: string }) => {
|
||||
const i18n = useI18n();
|
||||
return i18n.t('com.affine.page-properties.property.createdBy.tip', {
|
||||
userName,
|
||||
});
|
||||
};
|
||||
const UpdatedUsernameTip = ({ userName }: { userName: string }) => {
|
||||
const i18n = useI18n();
|
||||
return i18n.t('com.affine.page-properties.property.updatedBy.tip', {
|
||||
userName,
|
||||
});
|
||||
};
|
||||
|
||||
const CreatedByUpdatedByAvatar = (props: {
|
||||
type: 'CreatedBy' | 'UpdatedBy';
|
||||
doc: DocRecord;
|
||||
@@ -33,6 +46,9 @@ const CreatedByUpdatedByAvatar = (props: {
|
||||
id={userId}
|
||||
size={props.size}
|
||||
showName={props.showName}
|
||||
tooltip={
|
||||
props.type === 'CreatedBy' ? CreatedUsernameTip : UpdatedUsernameTip
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export const body = style({
|
||||
export const scrollArea = style({
|
||||
height: 0,
|
||||
flex: 1,
|
||||
paddingTop: '24px',
|
||||
paddingTop: '12px',
|
||||
});
|
||||
|
||||
// group
|
||||
@@ -43,7 +43,7 @@ export const pinnedCollection = style({
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
padding: '0 24px',
|
||||
paddingTop: '24px',
|
||||
paddingTop: '12px',
|
||||
'@container': {
|
||||
'docs-body (width <= 500px)': {
|
||||
padding: '0 20px',
|
||||
|
||||
@@ -68,11 +68,7 @@ const AllDocs = () => {
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<DocsExplorer
|
||||
masonryItemWidthMin={150}
|
||||
heightBase={180}
|
||||
heightScale={12}
|
||||
/>
|
||||
<DocsExplorer masonryItemWidthMin={150} />
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,11 +69,7 @@ const CollectionDocs = ({ collection }: { collection: Collection }) => {
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<DocsExplorer
|
||||
masonryItemWidthMin={150}
|
||||
heightBase={180}
|
||||
heightScale={12}
|
||||
/>
|
||||
<DocsExplorer masonryItemWidthMin={150} />
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,11 +75,7 @@ const TagDocs = ({ tag }: { tag: Tag }) => {
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<DocsExplorer
|
||||
masonryItemWidthMin={150}
|
||||
heightBase={180}
|
||||
heightScale={12}
|
||||
/>
|
||||
<DocsExplorer masonryItemWidthMin={150} />
|
||||
</DocExplorerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Avatar } from '@affine/component';
|
||||
import { Avatar, Tooltip } from '@affine/component';
|
||||
import { useCurrentServerService } from '@affine/core/components/providers/current-server-scope';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useLayoutEffect, useMemo } from 'react';
|
||||
import { type ComponentType, useLayoutEffect, useMemo } from 'react';
|
||||
|
||||
import { PublicUserService } from '../services/public-user';
|
||||
import * as styles from './public-user.css';
|
||||
@@ -11,10 +11,12 @@ export const PublicUserLabel = ({
|
||||
id,
|
||||
size = 20,
|
||||
showName = true,
|
||||
tooltip: NameTip,
|
||||
}: {
|
||||
id: string;
|
||||
size?: number;
|
||||
showName?: boolean;
|
||||
tooltip?: ComponentType<{ userName: string }>;
|
||||
}) => {
|
||||
const serverService = useCurrentServerService();
|
||||
const publicUser = useMemo(() => {
|
||||
@@ -50,15 +52,23 @@ export const PublicUserLabel = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.publicUserLabel}>
|
||||
<Avatar
|
||||
url={user?.avatar}
|
||||
name={user?.name ?? ''}
|
||||
size={size}
|
||||
className={styles.publicUserLabelAvatar}
|
||||
data-show-name={showName}
|
||||
/>
|
||||
{showName && user?.name}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={
|
||||
NameTip ? (
|
||||
<NameTip userName={user?.name || t['Unknown User']()} />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<span className={styles.publicUserLabel}>
|
||||
<Avatar
|
||||
url={user?.avatar}
|
||||
name={user?.name ?? ''}
|
||||
size={size}
|
||||
className={styles.publicUserLabelAvatar}
|
||||
data-show-name={showName}
|
||||
/>
|
||||
{showName && user?.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2189,6 +2189,10 @@ export function useAFFiNEI18N(): {
|
||||
* `Grouping`
|
||||
*/
|
||||
["com.affine.explorer.display-menu.grouping"](): string;
|
||||
/**
|
||||
* `Remove group`
|
||||
*/
|
||||
["com.affine.explorer.display-menu.grouping.remove"](): string;
|
||||
/**
|
||||
* `Ordering`
|
||||
*/
|
||||
@@ -3141,6 +3145,18 @@ export function useAFFiNEI18N(): {
|
||||
* `Mark this doc as a template, which can be used to create new docs.`
|
||||
*/
|
||||
["com.affine.page-properties.property.template.tooltips"](): string;
|
||||
/**
|
||||
* `Created by {{userName}}`
|
||||
*/
|
||||
["com.affine.page-properties.property.createdBy.tip"](options: {
|
||||
readonly userName: string;
|
||||
}): string;
|
||||
/**
|
||||
* `Last edited by {{userName}}`
|
||||
*/
|
||||
["com.affine.page-properties.property.updatedBy.tip"](options: {
|
||||
readonly userName: string;
|
||||
}): string;
|
||||
/**
|
||||
* `Properties`
|
||||
*/
|
||||
|
||||
@@ -546,6 +546,7 @@
|
||||
"com.affine.filterList.button.add": "Add filter",
|
||||
"com.affine.explorer.display-menu.button": "Display",
|
||||
"com.affine.explorer.display-menu.grouping": "Grouping",
|
||||
"com.affine.explorer.display-menu.grouping.remove": "Remove group",
|
||||
"com.affine.explorer.display-menu.ordering": "Ordering",
|
||||
"com.affine.header.mode-switch.page": "View in Page mode",
|
||||
"com.affine.header.mode-switch.edgeless": "View in Edgeless Canvas",
|
||||
@@ -780,6 +781,8 @@
|
||||
"com.affine.page-properties.property.edgelessTheme.tooltips": "Select the doc theme from Light, Dark, or System. Useful for precise control over content viewing style.",
|
||||
"com.affine.page-properties.property.pageWidth.tooltips": "Control the width of this page to fit content display needs.",
|
||||
"com.affine.page-properties.property.template.tooltips": "Mark this doc as a template, which can be used to create new docs.",
|
||||
"com.affine.page-properties.property.createdBy.tip": "Created by {{userName}}",
|
||||
"com.affine.page-properties.property.updatedBy.tip": "Last edited by {{userName}}",
|
||||
"com.affine.propertySidebar.property-list.section": "Properties",
|
||||
"com.affine.propertySidebar.add-more.section": "Add more properties",
|
||||
"com.affine.page-properties.settings.title": "customize properties",
|
||||
|
||||
Reference in New Issue
Block a user