feat(core): new all docs list ui (#12102)
### New all docs list ui close AF-2531, AF-2585, AF-2586, AF-2580 ### What changed - a new `display-menu` component - properties visibility - quick actions visibility - extend DocPropertyType definition - `showInDocList`: configure whether to show in doc and how to show (stack | inline) - `docListProperty`: define how to render property in doc - `groupHeader`: define how to render group header when grouped - implement all properties's `docListProperty` renderer and `groupHeader` renderer - new `docs-view` component - render doc in `card` | `list` view - split doc card into minimal components for reuse in list and card views, as well as visibility control - implement docs-list with `<Masonry />` and multi-view support - for list view, make masonry column count always `1` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Redesigned document explorer with multiple view modes (list, grid, masonry), grouping, selection, and multi-delete. - Added customizable display properties and quick actions (favorite, trash, split view, new tab, select) for documents. - Introduced new group header and document item components with improved styling and interaction. - Enabled dynamic rendering of document properties including tags, dates, users, templates, and themes. - Added filtering support for trash status and enhanced localization for UI elements. - Introduced drag handle size customization and expanded masonry layout configurability. - Added contextual "More" menu with document operations like favorite, info, duplicate, and trash. - Implemented shared context for explorer state management and multi-selection logic. - **Enhancements** - Improved virtual scrolling with active item tracking and configurable preload and debounce settings. - Responsive, theme-aware styling applied across explorer and property components. - Configurable UI elements and quick actions via user preferences. - Enhanced error messages for unsupported property types in filters. - Refined padding and layout calculations in masonry component for better visual consistency. - Avatar and date components refactored for explicit prop-driven rendering and customization. - **Bug Fixes** - Improved error messages for unsupported property types in filters. - **Documentation** - Added new localization keys and updated language completeness for new features. - **Chores** - Modularized workspace property types with list and group header display components. - Consolidated imports and enhanced code maintainability. - Added new CSS styling modules for explorer components and workspace property types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -8,8 +8,10 @@ export const DragHandle = forwardRef<
|
|||||||
{
|
{
|
||||||
className?: string;
|
className?: string;
|
||||||
dragging?: boolean;
|
dragging?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
}
|
}
|
||||||
>(({ className, dragging, ...props }, ref) => {
|
>(({ className, dragging, width = 10, height = 22, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
@@ -18,8 +20,8 @@ export const DragHandle = forwardRef<
|
|||||||
className={clsx(styles.root, className)}
|
className={clsx(styles.root, className)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="10"
|
width={width}
|
||||||
height="22"
|
height={height}
|
||||||
viewBox="0 0 10 22"
|
viewBox="0 0 10 22"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './masonry';
|
export * from './masonry';
|
||||||
|
export type { MasonryGroup, MasonryItem } from './type';
|
||||||
|
|||||||
@@ -14,15 +14,26 @@ import {
|
|||||||
import { observeResize } from '../../utils';
|
import { observeResize } from '../../utils';
|
||||||
import { Scrollable } from '../scrollbar';
|
import { Scrollable } from '../scrollbar';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
import type { MasonryGroup, MasonryItem, MasonryItemXYWH } from './type';
|
import type {
|
||||||
import { calcColumns, calcLayout, calcSleep, calcSticky } from './utils';
|
MasonryGroup,
|
||||||
|
MasonryItem,
|
||||||
|
MasonryItemXYWH,
|
||||||
|
MasonryPX,
|
||||||
|
} from './type';
|
||||||
|
import {
|
||||||
|
calcActive,
|
||||||
|
calcColumns,
|
||||||
|
calcLayout,
|
||||||
|
calcPX,
|
||||||
|
calcSticky,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
export interface MasonryProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface MasonryProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
items: MasonryItem[] | MasonryGroup[];
|
items: MasonryItem[] | MasonryGroup[];
|
||||||
|
|
||||||
gapX?: number;
|
gapX?: number;
|
||||||
gapY?: number;
|
gapY?: number;
|
||||||
paddingX?: number;
|
paddingX?: MasonryPX;
|
||||||
paddingY?: number;
|
paddingY?: number;
|
||||||
|
|
||||||
groupsGap?: number;
|
groupsGap?: number;
|
||||||
@@ -48,6 +59,8 @@ export interface MasonryProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
* Specify the number of columns, will override the calculated
|
* Specify the number of columns, will override the calculated
|
||||||
*/
|
*/
|
||||||
columns?: number;
|
columns?: number;
|
||||||
|
resizeDebounce?: number;
|
||||||
|
preloadHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Masonry = ({
|
export const Masonry = ({
|
||||||
@@ -66,6 +79,8 @@ export const Masonry = ({
|
|||||||
stickyGroupHeader = true,
|
stickyGroupHeader = true,
|
||||||
collapsedGroups,
|
collapsedGroups,
|
||||||
columns,
|
columns,
|
||||||
|
preloadHeight = 50,
|
||||||
|
resizeDebounce = 20,
|
||||||
onGroupCollapse,
|
onGroupCollapse,
|
||||||
...props
|
...props
|
||||||
}: MasonryProps) => {
|
}: MasonryProps) => {
|
||||||
@@ -74,13 +89,16 @@ export const Masonry = ({
|
|||||||
const [layoutMap, setLayoutMap] = useState<
|
const [layoutMap, setLayoutMap] = useState<
|
||||||
Map<MasonryItem['id'], MasonryItemXYWH>
|
Map<MasonryItem['id'], MasonryItemXYWH>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
const [sleepMap, setSleepMap] = useState<Map<
|
/**
|
||||||
MasonryItem['id'],
|
* Record active items, to ensure all items won't be rendered when initialized.
|
||||||
boolean
|
*/
|
||||||
> | null>(null);
|
const [activeMap, setActiveMap] = useState<Map<MasonryItem['id'], boolean>>(
|
||||||
|
new Map()
|
||||||
|
);
|
||||||
const [stickyGroupId, setStickyGroupId] = useState<string | undefined>(
|
const [stickyGroupId, setStickyGroupId] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [totalWidth, setTotalWidth] = useState(0);
|
||||||
const stickyGroupCollapsed = !!(
|
const stickyGroupCollapsed = !!(
|
||||||
collapsedGroups &&
|
collapsedGroups &&
|
||||||
stickyGroupId &&
|
stickyGroupId &&
|
||||||
@@ -91,7 +109,7 @@ export const Masonry = ({
|
|||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if ('items' in items[0]) return items as MasonryGroup[];
|
if (items[0] && 'items' in items[0]) return items as MasonryGroup[];
|
||||||
return [{ id: '', height: 0, items: items as MasonryItem[] }];
|
return [{ id: '', height: 0, items: items as MasonryItem[] }];
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
@@ -100,7 +118,7 @@ export const Masonry = ({
|
|||||||
return groups.find(group => group.id === stickyGroupId);
|
return groups.find(group => group.id === stickyGroupId);
|
||||||
}, [groups, stickyGroupId]);
|
}, [groups, stickyGroupId]);
|
||||||
|
|
||||||
const updateSleepMap = useCallback(
|
const updateActiveMap = useCallback(
|
||||||
(layoutMap: Map<MasonryItem['id'], MasonryItemXYWH>, _scrollY?: number) => {
|
(layoutMap: Map<MasonryItem['id'], MasonryItemXYWH>, _scrollY?: number) => {
|
||||||
if (!virtualScroll) return;
|
if (!virtualScroll) return;
|
||||||
|
|
||||||
@@ -109,16 +127,16 @@ export const Masonry = ({
|
|||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const scrollY = _scrollY ?? rootEl.scrollTop;
|
const scrollY = _scrollY ?? rootEl.scrollTop;
|
||||||
const sleepMap = calcSleep({
|
const activeMap = calcActive({
|
||||||
viewportHeight: rootEl.clientHeight,
|
viewportHeight: rootEl.clientHeight,
|
||||||
scrollY,
|
scrollY,
|
||||||
layoutMap,
|
layoutMap,
|
||||||
preloadHeight: 50,
|
preloadHeight,
|
||||||
});
|
});
|
||||||
setSleepMap(sleepMap);
|
setActiveMap(activeMap);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[virtualScroll]
|
[preloadHeight, virtualScroll]
|
||||||
);
|
);
|
||||||
|
|
||||||
const calculateLayout = useCallback(() => {
|
const calculateLayout = useCallback(() => {
|
||||||
@@ -149,7 +167,8 @@ export const Masonry = ({
|
|||||||
});
|
});
|
||||||
setLayoutMap(layout);
|
setLayoutMap(layout);
|
||||||
setHeight(height);
|
setHeight(height);
|
||||||
updateSleepMap(layout);
|
setTotalWidth(totalWidth);
|
||||||
|
updateActiveMap(layout);
|
||||||
if (stickyGroupHeader && rootRef.current) {
|
if (stickyGroupHeader && rootRef.current) {
|
||||||
setStickyGroupId(
|
setStickyGroupId(
|
||||||
calcSticky({ scrollY: rootRef.current.scrollTop, layoutMap: layout })
|
calcSticky({ scrollY: rootRef.current.scrollTop, layoutMap: layout })
|
||||||
@@ -168,17 +187,20 @@ export const Masonry = ({
|
|||||||
paddingX,
|
paddingX,
|
||||||
paddingY,
|
paddingY,
|
||||||
stickyGroupHeader,
|
stickyGroupHeader,
|
||||||
updateSleepMap,
|
updateActiveMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// handle resize
|
// handle resize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateLayout();
|
calculateLayout();
|
||||||
if (rootRef.current) {
|
if (rootRef.current) {
|
||||||
return observeResize(rootRef.current, debounce(calculateLayout, 50));
|
return observeResize(
|
||||||
|
rootRef.current,
|
||||||
|
debounce(calculateLayout, resizeDebounce)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [calculateLayout]);
|
}, [calculateLayout, resizeDebounce]);
|
||||||
|
|
||||||
// handle scroll
|
// handle scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -188,7 +210,7 @@ export const Masonry = ({
|
|||||||
if (virtualScroll) {
|
if (virtualScroll) {
|
||||||
const handler = throttle((e: Event) => {
|
const handler = throttle((e: Event) => {
|
||||||
const scrollY = (e.target as HTMLElement).scrollTop;
|
const scrollY = (e.target as HTMLElement).scrollTop;
|
||||||
updateSleepMap(layoutMap, scrollY);
|
updateActiveMap(layoutMap, scrollY);
|
||||||
if (stickyGroupHeader) {
|
if (stickyGroupHeader) {
|
||||||
setStickyGroupId(calcSticky({ scrollY, layoutMap }));
|
setStickyGroupId(calcSticky({ scrollY, layoutMap }));
|
||||||
}
|
}
|
||||||
@@ -199,7 +221,7 @@ export const Masonry = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [layoutMap, stickyGroupHeader, updateSleepMap, virtualScroll]);
|
}, [layoutMap, stickyGroupHeader, updateActiveMap, virtualScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scrollable.Root>
|
<Scrollable.Root>
|
||||||
@@ -211,11 +233,9 @@ export const Masonry = ({
|
|||||||
>
|
>
|
||||||
{groups.map(group => {
|
{groups.map(group => {
|
||||||
// sleep is not calculated, do not render
|
// sleep is not calculated, do not render
|
||||||
if (virtualScroll && !sleepMap) return null;
|
|
||||||
const {
|
const {
|
||||||
id: groupId,
|
id: groupId,
|
||||||
items,
|
items,
|
||||||
children,
|
|
||||||
className,
|
className,
|
||||||
Component,
|
Component,
|
||||||
...groupProps
|
...groupProps
|
||||||
@@ -223,14 +243,11 @@ export const Masonry = ({
|
|||||||
const collapsed =
|
const collapsed =
|
||||||
collapsedGroups && collapsedGroups.includes(groupId);
|
collapsedGroups && collapsedGroups.includes(groupId);
|
||||||
|
|
||||||
const sleep =
|
|
||||||
(virtualScroll && sleepMap && sleepMap.get(groupId)) ?? false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={groupId}>
|
<Fragment key={groupId}>
|
||||||
{/* group header */}
|
{/* group header */}
|
||||||
{sleep ? null : (
|
{virtualScroll && !activeMap.get(group.id) ? null : (
|
||||||
<MasonryItem
|
<MasonryGroupHeader
|
||||||
className={clsx(styles.groupHeader, className)}
|
className={clsx(styles.groupHeader, className)}
|
||||||
key={`header-${groupId}`}
|
key={`header-${groupId}`}
|
||||||
id={groupId}
|
id={groupId}
|
||||||
@@ -238,41 +255,30 @@ export const Masonry = ({
|
|||||||
xywh={layoutMap.get(groupId)}
|
xywh={layoutMap.get(groupId)}
|
||||||
{...groupProps}
|
{...groupProps}
|
||||||
onClick={() => onGroupCollapse?.(groupId, !collapsed)}
|
onClick={() => onGroupCollapse?.(groupId, !collapsed)}
|
||||||
>
|
Component={Component}
|
||||||
{Component ? (
|
itemCount={items.length}
|
||||||
<Component
|
collapsed={!!collapsed}
|
||||||
itemCount={items.length}
|
groupId={groupId}
|
||||||
groupId={groupId}
|
paddingX={calcPX(paddingX, totalWidth)}
|
||||||
collapsed={collapsed}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</MasonryItem>
|
|
||||||
)}
|
)}
|
||||||
{/* group items */}
|
{/* group items */}
|
||||||
{collapsed
|
{collapsed
|
||||||
? null
|
? null
|
||||||
: items.map(({ id: itemId, Component, ...item }) => {
|
: items.map(({ id: itemId, Component, ...item }) => {
|
||||||
const mixId = groupId ? `${groupId}:${itemId}` : itemId;
|
const mixId = groupId ? `${groupId}:${itemId}` : itemId;
|
||||||
const sleep =
|
if (virtualScroll && !activeMap.get(mixId)) return null;
|
||||||
(virtualScroll && sleepMap && sleepMap.get(mixId)) ??
|
|
||||||
false;
|
|
||||||
if (sleep) return null;
|
|
||||||
return (
|
return (
|
||||||
<MasonryItem
|
<MasonryGroupItem
|
||||||
key={mixId}
|
key={mixId}
|
||||||
id={mixId}
|
id={mixId}
|
||||||
{...item}
|
{...item}
|
||||||
locateMode={locateMode}
|
locateMode={locateMode}
|
||||||
xywh={layoutMap.get(mixId)}
|
xywh={layoutMap.get(mixId)}
|
||||||
>
|
groupId={groupId}
|
||||||
{Component ? (
|
itemId={itemId}
|
||||||
<Component groupId={groupId} itemId={itemId} />
|
Component={Component}
|
||||||
) : (
|
/>
|
||||||
item.children
|
|
||||||
)}
|
|
||||||
</MasonryItem>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -283,10 +289,11 @@ export const Masonry = ({
|
|||||||
<Scrollable.Scrollbar />
|
<Scrollable.Scrollbar />
|
||||||
{stickyGroup ? (
|
{stickyGroup ? (
|
||||||
<div
|
<div
|
||||||
className={styles.stickyGroupHeader}
|
className={clsx(styles.stickyGroupHeader, stickyGroup.className)}
|
||||||
style={{
|
style={{
|
||||||
padding: `0 ${paddingX}px`,
|
padding: `0 ${calcPX(paddingX, totalWidth)}px`,
|
||||||
height: stickyGroup.height,
|
height: stickyGroup.height,
|
||||||
|
...stickyGroup.style,
|
||||||
}}
|
}}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onGroupCollapse?.(stickyGroup.id, !stickyGroupCollapsed)
|
onGroupCollapse?.(stickyGroup.id, !stickyGroupCollapsed)
|
||||||
@@ -314,11 +321,87 @@ interface MasonryItemProps
|
|||||||
xywh?: MasonryItemXYWH;
|
xywh?: MasonryItemXYWH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MasonryGroupHeader = memo(function MasonryGroupHeader({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
Component,
|
||||||
|
groupId,
|
||||||
|
itemCount,
|
||||||
|
collapsed,
|
||||||
|
height,
|
||||||
|
paddingX,
|
||||||
|
...props
|
||||||
|
}: Omit<MasonryItemProps, 'Component'> & {
|
||||||
|
Component?: MasonryGroup['Component'];
|
||||||
|
groupId: string;
|
||||||
|
itemCount: number;
|
||||||
|
collapsed: boolean;
|
||||||
|
paddingX?: number;
|
||||||
|
}) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (Component) {
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
groupId={groupId}
|
||||||
|
itemCount={itemCount}
|
||||||
|
collapsed={collapsed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}, [Component, children, collapsed, groupId, itemCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MasonryItem
|
||||||
|
id={id}
|
||||||
|
height={height}
|
||||||
|
style={{
|
||||||
|
padding: `0 ${paddingX}px`,
|
||||||
|
height: '100%',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</MasonryItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const MasonryGroupItem = memo(function MasonryGroupItem({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
Component,
|
||||||
|
groupId,
|
||||||
|
itemId,
|
||||||
|
...props
|
||||||
|
}: MasonryItemProps & {
|
||||||
|
groupId: string;
|
||||||
|
itemId: string;
|
||||||
|
}) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (Component) {
|
||||||
|
return <Component groupId={groupId} itemId={itemId} />;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}, [Component, children, groupId, itemId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MasonryItem id={id} className={className} {...props}>
|
||||||
|
{content}
|
||||||
|
</MasonryItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const MasonryItem = memo(function MasonryItem({
|
const MasonryItem = memo(function MasonryItem({
|
||||||
id,
|
id,
|
||||||
xywh,
|
xywh,
|
||||||
locateMode = 'leftTop',
|
locateMode = 'leftTop',
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
style: styleProp,
|
style: styleProp,
|
||||||
...props
|
...props
|
||||||
}: MasonryItemProps) {
|
}: MasonryItemProps) {
|
||||||
@@ -341,14 +424,19 @@ const MasonryItem = memo(function MasonryItem({
|
|||||||
...posStyle,
|
...posStyle,
|
||||||
width: `${w}px`,
|
width: `${w}px`,
|
||||||
height: `${h}px`,
|
height: `${h}px`,
|
||||||
position: 'absolute' as const,
|
|
||||||
};
|
};
|
||||||
}, [locateMode, styleProp, xywh]);
|
}, [locateMode, styleProp, xywh]);
|
||||||
|
|
||||||
if (!xywh) return null;
|
if (!xywh) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-masonry-item data-masonry-item-id={id} style={style} {...props}>
|
<div
|
||||||
|
data-masonry-item
|
||||||
|
data-masonry-item-id={id}
|
||||||
|
style={style}
|
||||||
|
className={clsx(styles.item, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ export const stickyGroupHeader = style({
|
|||||||
top: 0,
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const item = style({
|
||||||
|
position: 'absolute',
|
||||||
|
});
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ export interface MasonryItemXYWH {
|
|||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MasonryPX = number | ((totalWidth: number) => number);
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import type { MasonryGroup, MasonryItem, MasonryItemXYWH } from './type';
|
import type {
|
||||||
|
MasonryGroup,
|
||||||
|
MasonryItem,
|
||||||
|
MasonryItemXYWH,
|
||||||
|
MasonryPX,
|
||||||
|
} from './type';
|
||||||
|
|
||||||
|
export const calcPX = (px: MasonryPX, totalWidth: number) =>
|
||||||
|
typeof px === 'number' ? px : px(totalWidth);
|
||||||
|
|
||||||
export const calcColumns = (
|
export const calcColumns = (
|
||||||
totalWidth: number,
|
totalWidth: number,
|
||||||
itemWidth: number | 'stretch',
|
itemWidth: number | 'stretch',
|
||||||
itemWidthMin: number,
|
itemWidthMin: number,
|
||||||
gapX: number,
|
gapX: number,
|
||||||
paddingX: number,
|
_paddingX: MasonryPX,
|
||||||
columns?: number
|
columns?: number
|
||||||
) => {
|
) => {
|
||||||
|
const paddingX = calcPX(_paddingX, totalWidth);
|
||||||
const availableWidth = totalWidth - paddingX * 2;
|
const availableWidth = totalWidth - paddingX * 2;
|
||||||
|
|
||||||
if (columns) {
|
if (columns) {
|
||||||
@@ -47,7 +56,7 @@ export const calcLayout = (
|
|||||||
width: number;
|
width: number;
|
||||||
gapX: number;
|
gapX: number;
|
||||||
gapY: number;
|
gapY: number;
|
||||||
paddingX: number;
|
paddingX: MasonryPX;
|
||||||
paddingY: number;
|
paddingY: number;
|
||||||
groupsGap: number;
|
groupsGap: number;
|
||||||
groupHeaderGapWithItems: number;
|
groupHeaderGapWithItems: number;
|
||||||
@@ -60,12 +69,13 @@ export const calcLayout = (
|
|||||||
width,
|
width,
|
||||||
gapX,
|
gapX,
|
||||||
gapY,
|
gapY,
|
||||||
paddingX,
|
paddingX: _paddingX,
|
||||||
paddingY,
|
paddingY,
|
||||||
groupsGap,
|
groupsGap,
|
||||||
groupHeaderGapWithItems,
|
groupHeaderGapWithItems,
|
||||||
collapsedGroups,
|
collapsedGroups,
|
||||||
} = options;
|
} = options;
|
||||||
|
const paddingX = calcPX(_paddingX, totalWidth);
|
||||||
|
|
||||||
const layout = new Map<string, MasonryItemXYWH>();
|
const layout = new Map<string, MasonryItemXYWH>();
|
||||||
let finalHeight = paddingY;
|
let finalHeight = paddingY;
|
||||||
@@ -79,9 +89,9 @@ export const calcLayout = (
|
|||||||
// calculate group header
|
// calculate group header
|
||||||
const groupHeaderLayout: MasonryItemXYWH = {
|
const groupHeaderLayout: MasonryItemXYWH = {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
x: paddingX,
|
x: 0,
|
||||||
y: finalHeight,
|
y: finalHeight,
|
||||||
w: totalWidth - paddingX * 2,
|
w: totalWidth,
|
||||||
h: group.height,
|
h: group.height,
|
||||||
};
|
};
|
||||||
layout.set(group.id, groupHeaderLayout);
|
layout.set(group.id, groupHeaderLayout);
|
||||||
@@ -97,10 +107,11 @@ export const calcLayout = (
|
|||||||
const itemId = group.id ? `${group.id}:${item.id}` : item.id;
|
const itemId = group.id ? `${group.id}:${item.id}` : item.id;
|
||||||
const minHeight = Math.min(...heightStack);
|
const minHeight = Math.min(...heightStack);
|
||||||
const minHeightIndex = heightStack.indexOf(minHeight);
|
const minHeightIndex = heightStack.indexOf(minHeight);
|
||||||
|
const hasGap = heightStack[minHeightIndex] ? gapY : 0;
|
||||||
const x = minHeightIndex * (width + gapX) + paddingX;
|
const x = minHeightIndex * (width + gapX) + paddingX;
|
||||||
const y = minHeight + finalHeight;
|
const y = finalHeight + minHeight + hasGap;
|
||||||
|
|
||||||
heightStack[minHeightIndex] += item.height + gapY;
|
heightStack[minHeightIndex] += item.height + hasGap;
|
||||||
layout.set(itemId, {
|
layout.set(itemId, {
|
||||||
type: 'item',
|
type: 'item',
|
||||||
x,
|
x,
|
||||||
@@ -117,7 +128,7 @@ export const calcLayout = (
|
|||||||
return { layout, height: finalHeight };
|
return { layout, height: finalHeight };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calcSleep = (options: {
|
export const calcActive = (options: {
|
||||||
viewportHeight: number;
|
viewportHeight: number;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
layoutMap: Map<MasonryItem['id'], MasonryItemXYWH>;
|
layoutMap: Map<MasonryItem['id'], MasonryItemXYWH>;
|
||||||
@@ -125,7 +136,7 @@ export const calcSleep = (options: {
|
|||||||
}) => {
|
}) => {
|
||||||
const { viewportHeight, scrollY, layoutMap, preloadHeight } = options;
|
const { viewportHeight, scrollY, layoutMap, preloadHeight } = options;
|
||||||
|
|
||||||
const sleepMap = new Map<MasonryItem['id'], boolean>();
|
const activeMap = new Map<MasonryItem['id'], boolean>();
|
||||||
|
|
||||||
layoutMap.forEach((layout, id) => {
|
layoutMap.forEach((layout, id) => {
|
||||||
const { y, h } = layout;
|
const { y, h } = layout;
|
||||||
@@ -134,10 +145,12 @@ export const calcSleep = (options: {
|
|||||||
y + h + preloadHeight > scrollY &&
|
y + h + preloadHeight > scrollY &&
|
||||||
y - preloadHeight < scrollY + viewportHeight;
|
y - preloadHeight < scrollY + viewportHeight;
|
||||||
|
|
||||||
sleepMap.set(id, !isInView);
|
if (isInView) {
|
||||||
|
activeMap.set(id, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return sleepMap;
|
return activeMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calcSticky = (options: {
|
export const calcSticky = (options: {
|
||||||
|
|||||||
28
packages/frontend/core/src/components/explorer/context.ts
Normal file
28
packages/frontend/core/src/components/explorer/context.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createContext, type Dispatch, type SetStateAction } from 'react';
|
||||||
|
|
||||||
|
import type { ExplorerPreference } from './types';
|
||||||
|
|
||||||
|
export type DocExplorerContextType = ExplorerPreference & {
|
||||||
|
view: 'list' | 'grid' | 'masonry';
|
||||||
|
groups: Array<{ key: string; items: string[] }>;
|
||||||
|
collapsed: string[];
|
||||||
|
selectMode?: boolean;
|
||||||
|
selectedDocIds: string[];
|
||||||
|
prevCheckAnchorId?: string | null;
|
||||||
|
onToggleCollapse: (groupId: string) => void;
|
||||||
|
onToggleSelect: (docId: string) => void;
|
||||||
|
onSelect: Dispatch<SetStateAction<string[]>>;
|
||||||
|
setPrevCheckAnchorId: Dispatch<SetStateAction<string | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocExplorerContext = createContext<DocExplorerContextType>({
|
||||||
|
view: 'list',
|
||||||
|
groups: [],
|
||||||
|
collapsed: [],
|
||||||
|
selectedDocIds: [],
|
||||||
|
prevCheckAnchorId: null,
|
||||||
|
onToggleSelect: () => {},
|
||||||
|
onToggleCollapse: () => {},
|
||||||
|
onSelect: () => {},
|
||||||
|
setPrevCheckAnchorId: () => {},
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Menu, MenuSub } from '@affine/component';
|
import { Button, Divider, Menu, MenuSub } from '@affine/component';
|
||||||
import type {
|
import type {
|
||||||
GroupByParams,
|
GroupByParams,
|
||||||
OrderByParams,
|
OrderByParams,
|
||||||
@@ -11,6 +11,8 @@ import { useCallback } from 'react';
|
|||||||
import type { ExplorerPreference } from '../types';
|
import type { ExplorerPreference } from '../types';
|
||||||
import { GroupByList, GroupByName } from './group';
|
import { GroupByList, GroupByName } from './group';
|
||||||
import { OrderByList, OrderByName } from './order';
|
import { OrderByList, OrderByName } from './order';
|
||||||
|
import { DisplayProperties } from './properties';
|
||||||
|
import { QuickActionsConfig } from './quick-actions';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const ExplorerDisplayMenu = ({
|
const ExplorerDisplayMenu = ({
|
||||||
@@ -78,6 +80,10 @@ const ExplorerDisplayMenu = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</MenuSub>
|
</MenuSub>
|
||||||
|
<Divider size="thinner" />
|
||||||
|
<DisplayProperties preference={preference} onChange={onChange} />
|
||||||
|
<Divider size="thinner" />
|
||||||
|
<QuickActionsConfig preference={preference} onChange={onChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const properties = style({
|
||||||
|
padding: '4px 0px 8px 0px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sectionLabel = style({
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '22px',
|
||||||
|
color: cssVarV2.text.primary,
|
||||||
|
padding: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const property = style({
|
||||||
|
selectors: {
|
||||||
|
'&[data-show="false"]': {
|
||||||
|
backgroundColor: cssVarV2.button.emptyIconBackground,
|
||||||
|
color: cssVarV2.icon.disable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Button, Divider } from '@affine/component';
|
||||||
|
import {
|
||||||
|
WorkspacePropertyService,
|
||||||
|
type WorkspacePropertyType,
|
||||||
|
} from '@affine/core/modules/workspace-property';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { WorkspacePropertyName } from '../../properties';
|
||||||
|
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||||
|
import type { ExplorerPreference } from '../types';
|
||||||
|
import * as styles from './properties.css';
|
||||||
|
|
||||||
|
export const filterDisplayProperties = <
|
||||||
|
T extends { type: WorkspacePropertyType },
|
||||||
|
>(
|
||||||
|
propertyList: T[],
|
||||||
|
showInDocList: 'inline' | 'stack'
|
||||||
|
) => {
|
||||||
|
return propertyList
|
||||||
|
.filter(
|
||||||
|
property =>
|
||||||
|
WorkspacePropertyTypes[property.type].showInDocList === showInDocList
|
||||||
|
)
|
||||||
|
.map(property => ({
|
||||||
|
property,
|
||||||
|
config: WorkspacePropertyTypes[property.type],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisplayProperties = ({
|
||||||
|
preference,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
preference: ExplorerPreference;
|
||||||
|
onChange?: (preference: ExplorerPreference) => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
|
const propertyList = useLiveData(workspacePropertyService.properties$);
|
||||||
|
|
||||||
|
const displayProperties = preference.displayProperties;
|
||||||
|
const showIcon = preference.showDocIcon;
|
||||||
|
const showBody = preference.showDocPreview;
|
||||||
|
|
||||||
|
const propertiesGroups = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
type: 'inline',
|
||||||
|
properties: filterDisplayProperties(propertyList, 'inline'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'stack',
|
||||||
|
properties: filterDisplayProperties(propertyList, 'stack'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[propertyList]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDisplayPropertiesChange = useCallback(
|
||||||
|
(displayProperties: string[]) => {
|
||||||
|
onChange?.({
|
||||||
|
...preference,
|
||||||
|
displayProperties,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, preference]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePropertyClick = useCallback(
|
||||||
|
(propertyId: string) => {
|
||||||
|
handleDisplayPropertiesChange(
|
||||||
|
displayProperties && displayProperties.includes(propertyId)
|
||||||
|
? displayProperties.filter(id => id !== propertyId)
|
||||||
|
: [...(displayProperties || []), propertyId]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[displayProperties, handleDisplayPropertiesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleIcon = useCallback(() => {
|
||||||
|
onChange?.({
|
||||||
|
...preference,
|
||||||
|
showDocIcon: !showIcon,
|
||||||
|
});
|
||||||
|
}, [onChange, preference, showIcon]);
|
||||||
|
|
||||||
|
const toggleBody = useCallback(() => {
|
||||||
|
onChange?.({
|
||||||
|
...preference,
|
||||||
|
showDocPreview: !showBody,
|
||||||
|
});
|
||||||
|
}, [onChange, preference, showBody]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<section className={styles.sectionLabel}>
|
||||||
|
{t['com.affine.all-docs.display.properties']()}
|
||||||
|
</section>
|
||||||
|
{propertiesGroups.map(list => {
|
||||||
|
return (
|
||||||
|
<div className={styles.properties} key={list.type}>
|
||||||
|
{list.properties.map(({ property }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={property.id}
|
||||||
|
data-show={
|
||||||
|
displayProperties && displayProperties.includes(property.id)
|
||||||
|
}
|
||||||
|
onClick={() => handlePropertyClick(property.id)}
|
||||||
|
className={styles.property}
|
||||||
|
data-property-id={property.id}
|
||||||
|
>
|
||||||
|
<WorkspacePropertyName propertyInfo={property} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Divider size="thinner" />
|
||||||
|
<section className={styles.sectionLabel}>
|
||||||
|
{t['com.affine.all-docs.display.list-view']()}
|
||||||
|
</section>
|
||||||
|
<div className={styles.properties}>
|
||||||
|
<Button
|
||||||
|
className={styles.property}
|
||||||
|
data-show={showIcon}
|
||||||
|
onClick={toggleIcon}
|
||||||
|
>
|
||||||
|
{t['com.affine.all-docs.display.list-view.icon']()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={styles.property}
|
||||||
|
data-show={showBody}
|
||||||
|
onClick={toggleBody}
|
||||||
|
>
|
||||||
|
{t['com.affine.all-docs.display.list-view.body']()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Checkbox, MenuItem, MenuSub } from '@affine/component';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { type QuickActionKey, quickActions } from '../quick-actions.constants';
|
||||||
|
import type { ExplorerPreference } from '../types';
|
||||||
|
|
||||||
|
export const QuickActionsConfig = ({
|
||||||
|
preference,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
preference: ExplorerPreference;
|
||||||
|
onChange?: (preference: ExplorerPreference) => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const toggleAction = useCallback(
|
||||||
|
(key: QuickActionKey) => {
|
||||||
|
onChange?.({
|
||||||
|
...preference,
|
||||||
|
[key]: !preference[key],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[preference, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuSub
|
||||||
|
items={quickActions.map(action => {
|
||||||
|
if (action.disabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={action.key}
|
||||||
|
onClick={e => {
|
||||||
|
// do not close sub menu
|
||||||
|
e.preventDefault();
|
||||||
|
toggleAction(action.key);
|
||||||
|
}}
|
||||||
|
prefixIcon={<Checkbox checked={!!preference[action.key]} />}
|
||||||
|
>
|
||||||
|
{t.t(action.name)}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t['com.affine.all-docs.quick-actions']()}
|
||||||
|
</MenuSub>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listViewRoot = style({
|
||||||
|
padding: '0px 4px',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
containerName: 'list-view-root',
|
||||||
|
containerType: 'size',
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dragHandle = style({
|
||||||
|
position: 'absolute',
|
||||||
|
padding: '5px 2px',
|
||||||
|
color: cssVarV2.icon.secondary,
|
||||||
|
});
|
||||||
|
export const listDragHandle = style([
|
||||||
|
dragHandle,
|
||||||
|
{
|
||||||
|
left: -4,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%) translateX(-100%)',
|
||||||
|
opacity: 0,
|
||||||
|
selectors: {
|
||||||
|
[`${listViewRoot}:hover &`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const listSelect = style({
|
||||||
|
width: 0,
|
||||||
|
height: 24,
|
||||||
|
fontSize: 20,
|
||||||
|
padding: 2,
|
||||||
|
// to make sure won't take place when hidden
|
||||||
|
// 12 = gap + padding * 2
|
||||||
|
marginLeft: -12,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'flex',
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'end',
|
||||||
|
transition: 'width 0.25s ease, margin-left 0.25s ease',
|
||||||
|
// when select mode is on, the whole item can be clicked,
|
||||||
|
// the selection will be handled by the parent, the checkbox here just for the visual effect
|
||||||
|
pointerEvents: 'none',
|
||||||
|
selectors: {
|
||||||
|
'&[data-select-mode="true"]': {
|
||||||
|
width: 24,
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listIcon = style({
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
fontSize: 24,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
});
|
||||||
|
export const listContent = style({
|
||||||
|
width: 0,
|
||||||
|
height: '100%',
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
export const listBrief = style({
|
||||||
|
height: '100%',
|
||||||
|
flexShrink: 10,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginLeft: 4,
|
||||||
|
minWidth: 200,
|
||||||
|
});
|
||||||
|
export const listSpace = style({
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
// export const listDetails = style({
|
||||||
|
// display: 'flex',
|
||||||
|
// gap: 8,
|
||||||
|
// alignItems: 'center',
|
||||||
|
// flexShrink: 1,
|
||||||
|
// minWidth: 0,
|
||||||
|
// justifyContent: 'flex-end',
|
||||||
|
// });
|
||||||
|
const ellipsis = style({
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
export const listTitle = style([
|
||||||
|
ellipsis,
|
||||||
|
{
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '22px',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: cssVarV2.text.primary,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const listPreview = style([
|
||||||
|
ellipsis,
|
||||||
|
{
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontWeight: 400,
|
||||||
|
color: cssVarV2.text.secondary,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const listQuickActions = style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listHide750 = style({
|
||||||
|
'@container': {
|
||||||
|
'list-view-root (width <= 750px)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listHide560 = style({
|
||||||
|
'@container': {
|
||||||
|
'list-view-root (width <= 560px)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- card view ---
|
||||||
|
export const cardViewRoot = style({
|
||||||
|
vars: {
|
||||||
|
'--ring-color': 'transparent',
|
||||||
|
},
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
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)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
selectors: {
|
||||||
|
[`${root}[data-selected="true"] &`]: {
|
||||||
|
vars: {
|
||||||
|
'--ring-color': cssVarV2.layer.insideBorder.primaryBorder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const cardViewHeader = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
export const cardViewIcon = style({
|
||||||
|
fontSize: 24,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
lineHeight: 0,
|
||||||
|
});
|
||||||
|
export const cardViewTitle = style({
|
||||||
|
fontSize: 18,
|
||||||
|
lineHeight: '26px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: cssVarV2.text.primary,
|
||||||
|
letterSpacing: '-0.24px',
|
||||||
|
width: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
export const cardPreviewContainer = style({
|
||||||
|
width: '100%',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontWeight: 400,
|
||||||
|
color: cssVarV2.text.primary,
|
||||||
|
minHeight: 20,
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
export const cardViewCheckbox = style({
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
fontSize: 16,
|
||||||
|
padding: 2,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
});
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
DragHandle as DragHandleIcon,
|
||||||
|
Skeleton,
|
||||||
|
useDraggable,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import {
|
||||||
|
AutoTidyUpIcon,
|
||||||
|
PropertyIcon,
|
||||||
|
ResizeTidyUpIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
type HTMLProps,
|
||||||
|
memo,
|
||||||
|
type ReactNode,
|
||||||
|
type SVGProps,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { PagePreview } from '../../page-list/page-content-preview';
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
import { quickActions } from '../quick-actions.constants';
|
||||||
|
import * as styles from './doc-list-item.css';
|
||||||
|
import { MoreMenuButton } from './more-menu';
|
||||||
|
import { CardViewProperties, ListViewProperties } from './properties';
|
||||||
|
|
||||||
|
export type DocListItemView = 'list' | 'grid' | 'masonry';
|
||||||
|
|
||||||
|
export const DocListViewIcon = ({
|
||||||
|
view,
|
||||||
|
...props
|
||||||
|
}: { view: DocListItemView } & SVGProps<SVGSVGElement>) => {
|
||||||
|
const Component = {
|
||||||
|
list: PropertyIcon,
|
||||||
|
grid: ResizeTidyUpIcon,
|
||||||
|
masonry: AutoTidyUpIcon,
|
||||||
|
}[view];
|
||||||
|
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DocListItemProps {
|
||||||
|
docId: string;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MixId {
|
||||||
|
static connector = '||';
|
||||||
|
static create(groupId: string, docId: string) {
|
||||||
|
return `${groupId}${this.connector}${docId}`;
|
||||||
|
}
|
||||||
|
static parse(mixId: string) {
|
||||||
|
if (!mixId) {
|
||||||
|
return { groupId: null, docId: null };
|
||||||
|
}
|
||||||
|
const [groupId, docId] = mixId.split(this.connector);
|
||||||
|
return { groupId, docId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||||
|
const {
|
||||||
|
view,
|
||||||
|
groups,
|
||||||
|
selectMode,
|
||||||
|
selectedDocIds,
|
||||||
|
prevCheckAnchorId,
|
||||||
|
onSelect,
|
||||||
|
onToggleSelect,
|
||||||
|
setPrevCheckAnchorId,
|
||||||
|
} = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const handleMultiSelect = useCallback(
|
||||||
|
(prevCursor: string, currCursor: string) => {
|
||||||
|
const flattenList = groups.flatMap(group =>
|
||||||
|
group.items.map(docId => MixId.create(group.key, docId))
|
||||||
|
);
|
||||||
|
const prevIndex = flattenList.indexOf(prevCursor);
|
||||||
|
const currIndex = flattenList.indexOf(currCursor);
|
||||||
|
|
||||||
|
onSelect(prev => {
|
||||||
|
const lowerIndex = Math.min(prevIndex, currIndex);
|
||||||
|
const upperIndex = Math.max(prevIndex, currIndex);
|
||||||
|
|
||||||
|
const resSet = new Set(prev);
|
||||||
|
const handledSet = new Set<string>();
|
||||||
|
for (let i = lowerIndex; i <= upperIndex; i++) {
|
||||||
|
const mixId = flattenList[i];
|
||||||
|
const { groupId, docId } = MixId.parse(mixId);
|
||||||
|
if (groupId === null || docId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (handledSet.has(docId) || mixId === prevCursor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resSet.has(docId)) {
|
||||||
|
resSet.delete(docId);
|
||||||
|
} else {
|
||||||
|
resSet.add(docId);
|
||||||
|
}
|
||||||
|
handledSet.add(docId);
|
||||||
|
}
|
||||||
|
return [...resSet];
|
||||||
|
});
|
||||||
|
setPrevCheckAnchorId(currCursor);
|
||||||
|
},
|
||||||
|
[groups, onSelect, setPrevCheckAnchorId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(e: React.MouseEvent<Element>) => {
|
||||||
|
const { docId, groupId } = props;
|
||||||
|
const currCursor = MixId.create(groupId, docId);
|
||||||
|
if (selectMode || e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectMode) {
|
||||||
|
if (e.shiftKey && prevCheckAnchorId) {
|
||||||
|
// do multi select
|
||||||
|
handleMultiSelect(prevCheckAnchorId, currCursor);
|
||||||
|
} else {
|
||||||
|
onToggleSelect(docId);
|
||||||
|
setPrevCheckAnchorId(currCursor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
onToggleSelect(docId);
|
||||||
|
setPrevCheckAnchorId(currCursor);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// as link
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
handleMultiSelect,
|
||||||
|
onToggleSelect,
|
||||||
|
prevCheckAnchorId,
|
||||||
|
props,
|
||||||
|
selectMode,
|
||||||
|
setPrevCheckAnchorId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkbenchLink
|
||||||
|
draggable={false}
|
||||||
|
to={`/${props.docId}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
data-selected={selectedDocIds.includes(props.docId)}
|
||||||
|
className={styles.root}
|
||||||
|
>
|
||||||
|
{view === 'list' ? (
|
||||||
|
<ListViewDoc {...props} />
|
||||||
|
) : (
|
||||||
|
<CardViewDoc {...props} />
|
||||||
|
)}
|
||||||
|
</WorkbenchLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RawDocIcon = memo(function RawDocIcon({
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}: HTMLProps<SVGSVGElement>) {
|
||||||
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
|
const Icon = useLiveData(id ? docDisplayMetaService.icon$(id) : null);
|
||||||
|
return <Icon {...props} />;
|
||||||
|
});
|
||||||
|
const RawDocTitle = memo(function RawDocTitle({ id }: { id: string }) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
|
const title = useLiveData(docDisplayMetaService.title$(id));
|
||||||
|
return i18n.t(title);
|
||||||
|
});
|
||||||
|
const RawDocPreview = memo(function RawDocPreview({
|
||||||
|
id,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
loading?: ReactNode;
|
||||||
|
}) {
|
||||||
|
return <PagePreview pageId={id} fallback={loading} />;
|
||||||
|
});
|
||||||
|
const DragHandle = memo(function DragHandle({
|
||||||
|
id,
|
||||||
|
preview,
|
||||||
|
...props
|
||||||
|
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
||||||
|
const { selectMode } = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
||||||
|
() => ({
|
||||||
|
canDrag: true,
|
||||||
|
data: {
|
||||||
|
entity: {
|
||||||
|
type: 'doc',
|
||||||
|
id: id as string,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
at: 'all-docs:list',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectMode || !id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={dragRef} {...props}>
|
||||||
|
<DragHandleIcon />
|
||||||
|
</div>
|
||||||
|
<CustomDragPreview>
|
||||||
|
{preview ?? (
|
||||||
|
<>
|
||||||
|
<RawDocIcon id={id} />
|
||||||
|
<RawDocTitle id={id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CustomDragPreview>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const Select = memo(function Select({
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}: HTMLProps<HTMLDivElement>) {
|
||||||
|
const { selectMode, selectedDocIds, onToggleSelect } =
|
||||||
|
useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const handleSelectChange = useCallback(() => {
|
||||||
|
id && onToggleSelect(id);
|
||||||
|
}, [id, onToggleSelect]);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-select-mode={selectMode} {...props}>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedDocIds.includes(id)}
|
||||||
|
onChange={handleSelectChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Different with RawDocIcon, refer to `ExplorerPreference.showDocIcon`
|
||||||
|
const DocIcon = memo(function DocIcon({
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}: HTMLProps<HTMLDivElement>) {
|
||||||
|
const { showDocIcon } = useContext(DocExplorerContext);
|
||||||
|
if (!showDocIcon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<RawDocIcon id={id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const DocTitle = memo(function DocTitle({
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}: HTMLProps<HTMLDivElement>) {
|
||||||
|
if (!id) return null;
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<RawDocTitle id={id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const DocPreview = memo(function DocPreview({
|
||||||
|
id,
|
||||||
|
loading,
|
||||||
|
...props
|
||||||
|
}: HTMLProps<HTMLDivElement> & { loading?: ReactNode }) {
|
||||||
|
const { showDocPreview } = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
if (!id || !showDocPreview) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<RawDocPreview id={id} loading={loading} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const listMoreMenuContentOptions = {
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
sideOffset: 12,
|
||||||
|
alignOffset: -4,
|
||||||
|
} as const;
|
||||||
|
export const ListViewDoc = ({ docId }: DocListItemProps) => {
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
|
const [previewSkeletonWidth] = useState(
|
||||||
|
Math.round(100 + Math.random() * 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={styles.listViewRoot}>
|
||||||
|
<DragHandle id={docId} className={styles.listDragHandle} />
|
||||||
|
<Select id={docId} className={styles.listSelect} />
|
||||||
|
<DocIcon id={docId} className={styles.listIcon} />
|
||||||
|
<div className={styles.listBrief}>
|
||||||
|
<DocTitle id={docId} className={styles.listTitle} />
|
||||||
|
<DocPreview
|
||||||
|
id={docId}
|
||||||
|
className={styles.listPreview}
|
||||||
|
loading={<Skeleton height={20} width={previewSkeletonWidth} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.listSpace} />
|
||||||
|
<ListViewProperties docId={docId} />
|
||||||
|
{quickActions.map(action => {
|
||||||
|
return <action.Component key={action.key} doc={doc} />;
|
||||||
|
})}
|
||||||
|
<MoreMenuButton
|
||||||
|
docId={docId}
|
||||||
|
contentOptions={listMoreMenuContentOptions}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardMoreMenuContentOptions = {
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
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 { selectMode } = useContext(DocExplorerContext);
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
|
const [previewSkeleton] = useState(randomPreviewSkeleton);
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={styles.cardViewRoot}>
|
||||||
|
<header className={styles.cardViewHeader}>
|
||||||
|
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||||
|
<DocTitle id={docId} className={styles.cardViewTitle} />
|
||||||
|
{quickActions.map(action => {
|
||||||
|
return <action.Component size="16" key={action.key} doc={doc} />;
|
||||||
|
})}
|
||||||
|
{selectMode ? (
|
||||||
|
<Select id={docId} className={styles.cardViewCheckbox} />
|
||||||
|
) : (
|
||||||
|
<MoreMenuButton
|
||||||
|
docId={docId}
|
||||||
|
contentOptions={cardMoreMenuContentOptions}
|
||||||
|
iconProps={{ size: '16' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
<DocPreview
|
||||||
|
id={docId}
|
||||||
|
className={styles.cardPreviewContainer}
|
||||||
|
loading={
|
||||||
|
<div>
|
||||||
|
{previewSkeleton.map(({ id, width }) => (
|
||||||
|
<Skeleton key={id} height={20} width={`${width}%`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<CardViewProperties docId={docId} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const groupHeader = style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0px 4px',
|
||||||
|
borderRadius: 4,
|
||||||
|
':hover': {
|
||||||
|
background: cssVarV2.layer.background.hoverOverlay,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const space = style({
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
export const plainTextGroupHeaderIcon = style({
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
fontSize: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
export const plainTextGroupHeader = style({
|
||||||
|
gap: 4,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 4,
|
||||||
|
selectors: {
|
||||||
|
[`&:has(${plainTextGroupHeaderIcon})`]: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const showOnHover = style({
|
||||||
|
opacity: 0,
|
||||||
|
selectors: {
|
||||||
|
[`${groupHeader}:hover &`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collapseButton = style([
|
||||||
|
showOnHover,
|
||||||
|
{
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
marginLeft: 2,
|
||||||
|
fontSize: 16,
|
||||||
|
flexShrink: 0,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const collapseButtonIcon = style({
|
||||||
|
vars: {
|
||||||
|
'--rotate': '90deg',
|
||||||
|
},
|
||||||
|
transition: 'transform 0.23s cubic-bezier(.56,.15,.37,.97)',
|
||||||
|
transform: 'rotate(var(--rotate))',
|
||||||
|
selectors: {
|
||||||
|
[`${groupHeader}[data-collapsed="true"] &`]: {
|
||||||
|
vars: {
|
||||||
|
'--rotate': '0deg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectInfo = style({
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '22px',
|
||||||
|
color: cssVarV2.text.tertiary,
|
||||||
|
marginLeft: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: '24px',
|
||||||
|
color: cssVarV2.text.secondary,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectAllButton = style([
|
||||||
|
showOnHover,
|
||||||
|
{
|
||||||
|
padding: '0px 4px',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
color: cssVarV2.text.secondary,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
]);
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Button, IconButton } from '@affine/component';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { ToggleRightIcon } from '@blocksuite/icons/rc';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
import * as styles from './group-header.css';
|
||||||
|
|
||||||
|
export const DocGroupHeader = ({
|
||||||
|
className,
|
||||||
|
groupId,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement> & {
|
||||||
|
groupId: string;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const {
|
||||||
|
selectMode,
|
||||||
|
collapsed,
|
||||||
|
groups,
|
||||||
|
selectedDocIds,
|
||||||
|
onToggleCollapse,
|
||||||
|
onSelect,
|
||||||
|
} = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const group = groups.find(g => g.key === groupId);
|
||||||
|
const isGroupAllSelected = group?.items.every(id =>
|
||||||
|
selectedDocIds.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleCollapse = useCallback(() => {
|
||||||
|
onToggleCollapse(groupId);
|
||||||
|
}, [groupId, onToggleCollapse]);
|
||||||
|
|
||||||
|
const handleSelectAll = useCallback(() => {
|
||||||
|
if (isGroupAllSelected) {
|
||||||
|
onSelect(prev => prev.filter(id => !group?.items.includes(id)));
|
||||||
|
} else {
|
||||||
|
onSelect(prev => {
|
||||||
|
const newSelected = [...prev];
|
||||||
|
group?.items.forEach(id => {
|
||||||
|
if (!newSelected.includes(id)) {
|
||||||
|
newSelected.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newSelected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [group?.items, isGroupAllSelected, onSelect]);
|
||||||
|
|
||||||
|
const selectedCount = group?.items.filter(id =>
|
||||||
|
selectedDocIds.includes(id)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.groupHeader}
|
||||||
|
data-collapsed={collapsed.includes(groupId)}
|
||||||
|
>
|
||||||
|
<div className={clsx(styles.content, className)} {...props} />
|
||||||
|
{selectMode ? (
|
||||||
|
<div className={styles.selectInfo}>
|
||||||
|
{selectedCount}/{group?.items.length}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<IconButton
|
||||||
|
className={styles.collapseButton}
|
||||||
|
icon={<ToggleRightIcon className={styles.collapseButtonIcon} />}
|
||||||
|
onClick={handleToggleCollapse}
|
||||||
|
/>
|
||||||
|
<div className={styles.space} />
|
||||||
|
<Button
|
||||||
|
size="custom"
|
||||||
|
className={styles.selectAllButton}
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
>
|
||||||
|
{t[
|
||||||
|
isGroupAllSelected
|
||||||
|
? 'com.affine.page.group-header.clear'
|
||||||
|
: 'com.affine.page.group-header.select-all'
|
||||||
|
]()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlainTextDocGroupHeader = ({
|
||||||
|
groupId,
|
||||||
|
docCount,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement> & {
|
||||||
|
groupId: string;
|
||||||
|
docCount: number;
|
||||||
|
icon?: ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<DocGroupHeader
|
||||||
|
className={clsx(styles.plainTextGroupHeader, className)}
|
||||||
|
groupId={groupId}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<div className={styles.plainTextGroupHeaderIcon}>{icon}</div>
|
||||||
|
) : null}
|
||||||
|
<div>{children ?? groupId}</div>
|
||||||
|
<div>·</div>
|
||||||
|
<div>{docCount}</div>
|
||||||
|
</DocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
type IconButtonProps,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
type MenuProps,
|
||||||
|
useConfirmModal,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import track from '@affine/track';
|
||||||
|
import {
|
||||||
|
DeleteIcon,
|
||||||
|
DuplicateIcon,
|
||||||
|
InformationIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
OpenInNewIcon,
|
||||||
|
SplitViewIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper';
|
||||||
|
import { IsFavoriteIcon } from '../../pure/icons';
|
||||||
|
|
||||||
|
interface DocOperationProps {
|
||||||
|
docId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Favorite Operation
|
||||||
|
*/
|
||||||
|
const ToggleFavorite = ({ docId }: DocOperationProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
|
const favourite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
|
||||||
|
|
||||||
|
const toggleFavorite = useCallback(() => {
|
||||||
|
favAdapter.toggle(docId, 'doc');
|
||||||
|
}, [docId, favAdapter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<IsFavoriteIcon favorite={favourite} />}
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
>
|
||||||
|
{favourite
|
||||||
|
? t['com.affine.favoritePageOperation.remove']()
|
||||||
|
: t['com.affine.favoritePageOperation.add']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doc Info Operation
|
||||||
|
*/
|
||||||
|
const DocInfo = ({ docId }: DocOperationProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||||
|
|
||||||
|
const onOpenInfoModal = useCallback(() => {
|
||||||
|
if (docId) {
|
||||||
|
track.$.docInfoPanel.$.open();
|
||||||
|
workspaceDialogService.open('doc-info', { docId });
|
||||||
|
}
|
||||||
|
}, [docId, workspaceDialogService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={onOpenInfoModal} prefixIcon={<InformationIcon />}>
|
||||||
|
{t['com.affine.page-properties.page-info.view']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open in New Tab Operation
|
||||||
|
*/
|
||||||
|
const NewTab = ({ docId }: DocOperationProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const onOpenInNewTab = useCallback(() => {
|
||||||
|
workbench.openDoc(docId, { at: 'new-tab' });
|
||||||
|
}, [docId, workbench]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={onOpenInNewTab} prefixIcon={<OpenInNewIcon />}>
|
||||||
|
{t['com.affine.workbench.tab.page-menu-open']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open in Split View Operation
|
||||||
|
*/
|
||||||
|
const SplitView = ({ docId }: DocOperationProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
|
const onOpenInSplitView = useCallback(() => {
|
||||||
|
track.allDocs.list.docMenu.openInSplitView();
|
||||||
|
workbench.openDoc(docId, { at: 'tail' });
|
||||||
|
}, [docId, workbench]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={onOpenInSplitView} prefixIcon={<SplitViewIcon />}>
|
||||||
|
{t['com.affine.workbench.tab.page-menu-open']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate Operation
|
||||||
|
*/
|
||||||
|
const Duplicate = ({ docId }: DocOperationProps) => {
|
||||||
|
const { duplicate } = useBlockSuiteMetaHelper();
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const onDuplicate = useCallback(() => {
|
||||||
|
duplicate(docId, false);
|
||||||
|
track.allDocs.list.docMenu.createDoc({
|
||||||
|
control: 'duplicate',
|
||||||
|
});
|
||||||
|
}, [docId, duplicate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem prefixIcon={<DuplicateIcon />} onSelect={onDuplicate}>
|
||||||
|
{t['com.affine.header.option.duplicate']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to Trash Operation
|
||||||
|
*/
|
||||||
|
const MoveToTrash = ({ docId }: DocOperationProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
|
|
||||||
|
const onMoveToTrash = useCallback(() => {
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
track.allDocs.list.docMenu.deleteDoc();
|
||||||
|
openConfirmModal({
|
||||||
|
title: t['com.affine.moveToTrash.confirmModal.title'](),
|
||||||
|
description: t['com.affine.moveToTrash.confirmModal.description']({
|
||||||
|
title: doc.title$.value || t['Untitled'](),
|
||||||
|
}),
|
||||||
|
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||||
|
confirmText: t.Delete(),
|
||||||
|
confirmButtonOptions: {
|
||||||
|
variant: 'error',
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
doc.moveToTrash();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [doc, openConfirmModal, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem prefixIcon={<DeleteIcon />} onClick={onMoveToTrash}>
|
||||||
|
{t['com.affine.moveToTrash.title']()}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MoreMenuContent = (props: DocOperationProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToggleFavorite {...props} />
|
||||||
|
<DocInfo {...props} />
|
||||||
|
<NewTab {...props} />
|
||||||
|
{BUILD_CONFIG.isElectron ? <SplitView {...props} /> : null}
|
||||||
|
<Duplicate {...props} />
|
||||||
|
<MoveToTrash {...props} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MoreMenu = ({
|
||||||
|
docId,
|
||||||
|
children,
|
||||||
|
...menuProps
|
||||||
|
}: Omit<MenuProps, 'items'> & { docId: string }) => {
|
||||||
|
return (
|
||||||
|
<Menu items={<MoreMenuContent docId={docId} />} {...menuProps}>
|
||||||
|
{children}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MoreMenuButton = ({
|
||||||
|
docId,
|
||||||
|
iconProps,
|
||||||
|
...menuProps
|
||||||
|
}: Omit<MenuProps, 'items' | 'children'> & {
|
||||||
|
docId: string;
|
||||||
|
iconProps?: IconButtonProps;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<MoreMenu docId={docId} {...menuProps}>
|
||||||
|
<IconButton icon={<MoreVerticalIcon />} {...iconProps} />
|
||||||
|
</MoreMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
// export const root = style({
|
||||||
|
// display: 'flex',
|
||||||
|
// gap: 8,
|
||||||
|
// justifyContent: 'flex-end',
|
||||||
|
// minWidth: 0,
|
||||||
|
// flexGrow: 1,
|
||||||
|
// flexShrink: 0,
|
||||||
|
// });
|
||||||
|
export const stackContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
flexShrink: 1.5,
|
||||||
|
minWidth: 0,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
});
|
||||||
|
export const stackProperties = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 10,
|
||||||
|
minWidth: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 5,
|
||||||
|
transition: 'flex-shrink 0.23s cubic-bezier(.56,.15,.37,.97)',
|
||||||
|
selectors: {
|
||||||
|
'&:empty': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'&:not(:empty)': {
|
||||||
|
minWidth: 40,
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
flexShrink: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const stackItem = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: 128,
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&:last-child': {
|
||||||
|
minWidth: 'fit-content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const stackItemContent = style({
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: cssVarV2.layer.insideBorder.blackBorder,
|
||||||
|
padding: '0px 8px 0px 6px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
maxWidth: 'min(128px, 300%)',
|
||||||
|
minWidth: 48,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
backgroundColor: cssVarV2.layer.background.primary,
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const stackItemIcon = style({
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
fontSize: 16,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const stackItemLabel = style({
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
color: cssVarV2.text.primary,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const inlineProperty = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
selectors: {
|
||||||
|
'&:empty': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cardProperties = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||||
|
import { type DocRecord, DocsService } from '@affine/core/modules/doc';
|
||||||
|
import {
|
||||||
|
WorkspacePropertyService,
|
||||||
|
type WorkspacePropertyType,
|
||||||
|
} from '@affine/core/modules/workspace-property';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
import type { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
import { filterDisplayProperties } from '../display-menu/properties';
|
||||||
|
import { listHide560, listHide750 } from './doc-list-item.css';
|
||||||
|
import * as styles from './properties.css';
|
||||||
|
|
||||||
|
const listInlinePropertyOrder: WorkspacePropertyType[] = [
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy',
|
||||||
|
];
|
||||||
|
const cardInlinePropertyOrder: WorkspacePropertyType[] = [
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
];
|
||||||
|
|
||||||
|
const useProperties = (docId: string, view: 'list' | 'card') => {
|
||||||
|
const { displayProperties } = useContext(DocExplorerContext);
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
|
|
||||||
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
|
const properties = useLiveData(doc?.properties$);
|
||||||
|
const propertyList = useLiveData(workspacePropertyService.properties$);
|
||||||
|
|
||||||
|
const stackProperties = useMemo(
|
||||||
|
() => (properties ? filterDisplayProperties(propertyList, 'stack') : []),
|
||||||
|
[properties, propertyList]
|
||||||
|
);
|
||||||
|
const inlineProperties = useMemo(
|
||||||
|
() =>
|
||||||
|
properties
|
||||||
|
? filterDisplayProperties(propertyList, 'inline')
|
||||||
|
.filter(p => p.property.type !== 'tags')
|
||||||
|
.sort((a, b) => {
|
||||||
|
const orderList =
|
||||||
|
view === 'list'
|
||||||
|
? listInlinePropertyOrder
|
||||||
|
: cardInlinePropertyOrder;
|
||||||
|
const aIndex = orderList.indexOf(a.property.type);
|
||||||
|
const bIndex = orderList.indexOf(b.property.type);
|
||||||
|
// Push un-recognised types to the tail instead of the head
|
||||||
|
return (
|
||||||
|
(aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
|
||||||
|
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
[properties, propertyList, view]
|
||||||
|
);
|
||||||
|
const tagsProperty = useMemo(() => {
|
||||||
|
return propertyList
|
||||||
|
? filterDisplayProperties(propertyList, 'inline').find(
|
||||||
|
prop => prop.property.type === 'tags'
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
}, [propertyList]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
doc,
|
||||||
|
displayProperties,
|
||||||
|
stackProperties,
|
||||||
|
inlineProperties,
|
||||||
|
tagsProperty,
|
||||||
|
}),
|
||||||
|
[doc, displayProperties, stackProperties, inlineProperties, tagsProperty]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const ListViewProperties = ({ docId }: { docId: string }) => {
|
||||||
|
const {
|
||||||
|
doc,
|
||||||
|
displayProperties,
|
||||||
|
stackProperties,
|
||||||
|
inlineProperties,
|
||||||
|
tagsProperty,
|
||||||
|
} = useProperties(docId, 'list');
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* stack properties */}
|
||||||
|
<div className={clsx(styles.stackContainer, listHide750)}>
|
||||||
|
<div className={styles.stackProperties}>
|
||||||
|
{stackProperties.map(({ property, config }) => {
|
||||||
|
if (!displayProperties?.includes(property.id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PropertyRenderer
|
||||||
|
key={property.id}
|
||||||
|
doc={doc}
|
||||||
|
property={property}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{tagsProperty &&
|
||||||
|
displayProperties?.includes(tagsProperty.property.id) ? (
|
||||||
|
<div className={styles.stackProperties}>
|
||||||
|
<PropertyRenderer
|
||||||
|
doc={doc}
|
||||||
|
property={tagsProperty.property}
|
||||||
|
config={tagsProperty.config}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{/* inline properties */}
|
||||||
|
{inlineProperties.map(({ property, config }) => {
|
||||||
|
if (!displayProperties?.includes(property.id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={property.id}
|
||||||
|
className={clsx(styles.inlineProperty, listHide560)}
|
||||||
|
>
|
||||||
|
<PropertyRenderer doc={doc} property={property} config={config} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardViewProperties = ({ docId }: { docId: string }) => {
|
||||||
|
const {
|
||||||
|
doc,
|
||||||
|
displayProperties,
|
||||||
|
stackProperties,
|
||||||
|
inlineProperties,
|
||||||
|
tagsProperty,
|
||||||
|
} = useProperties(docId, 'card');
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.cardProperties}>
|
||||||
|
{inlineProperties.map(({ property, config }) => {
|
||||||
|
if (!displayProperties?.includes(property.id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={property.id} className={styles.inlineProperty}>
|
||||||
|
<PropertyRenderer doc={doc} property={property} config={config} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{tagsProperty && displayProperties?.includes(tagsProperty.property.id) ? (
|
||||||
|
<PropertyRenderer
|
||||||
|
doc={doc}
|
||||||
|
property={tagsProperty.property}
|
||||||
|
config={tagsProperty.config}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{stackProperties.map(({ property, config }) => {
|
||||||
|
if (!displayProperties?.includes(property.id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PropertyRenderer
|
||||||
|
key={property.id}
|
||||||
|
doc={doc}
|
||||||
|
property={property}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PropertyRenderer = ({
|
||||||
|
property,
|
||||||
|
doc,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
property: DocCustomPropertyInfo;
|
||||||
|
doc: DocRecord;
|
||||||
|
config: (typeof WorkspacePropertyTypes)[keyof typeof WorkspacePropertyTypes];
|
||||||
|
}) => {
|
||||||
|
const customPropertyValue = useLiveData(doc.customProperty$(property.id));
|
||||||
|
if (!config.docListProperty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<config.docListProperty
|
||||||
|
value={customPropertyValue}
|
||||||
|
doc={doc}
|
||||||
|
propertyInfo={property}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
IconButton,
|
||||||
|
type IconButtonProps,
|
||||||
|
useConfirmModal,
|
||||||
|
} from '@affine/component';
|
||||||
|
import type { DocRecord } from '@affine/core/modules/doc';
|
||||||
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import track from '@affine/track';
|
||||||
|
import { DeleteIcon, OpenInNewIcon, SplitViewIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { memo, useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
import { IsFavoriteIcon } from '../../pure/icons';
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
|
||||||
|
export interface QuickActionProps extends IconButtonProps {
|
||||||
|
doc: DocRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickFavorite = memo(function QuickFavorite({
|
||||||
|
doc,
|
||||||
|
...iconButtonProps
|
||||||
|
}: QuickActionProps) {
|
||||||
|
const { quickFavorite } = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
|
const favourite = useLiveData(favAdapter.isFavorite$(doc.id, 'doc'));
|
||||||
|
|
||||||
|
const toggleFavorite = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
favAdapter.toggle(doc.id, 'doc');
|
||||||
|
},
|
||||||
|
[doc.id, favAdapter]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!quickFavorite) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={<IsFavoriteIcon favorite={favourite} />}
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
{...iconButtonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuickTab = memo(function QuickTab({
|
||||||
|
doc,
|
||||||
|
...iconButtonProps
|
||||||
|
}: QuickActionProps) {
|
||||||
|
const { quickTab } = useContext(DocExplorerContext);
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const onOpenInNewTab = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
workbench.openDoc(doc.id, { at: 'new-tab' });
|
||||||
|
},
|
||||||
|
[doc.id, workbench]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!quickTab) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={onOpenInNewTab}
|
||||||
|
icon={<OpenInNewIcon />}
|
||||||
|
{...iconButtonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuickSplit = memo(function QuickSplit({
|
||||||
|
doc,
|
||||||
|
...iconButtonProps
|
||||||
|
}: QuickActionProps) {
|
||||||
|
const { quickSplit } = useContext(DocExplorerContext);
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
|
const onOpenInSplitView = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
track.allDocs.list.docMenu.openInSplitView();
|
||||||
|
workbench.openDoc(doc.id, { at: 'tail' });
|
||||||
|
},
|
||||||
|
[doc.id, workbench]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!quickSplit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={onOpenInSplitView}
|
||||||
|
icon={<SplitViewIcon />}
|
||||||
|
{...iconButtonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuickDelete = memo(function QuickDelete({
|
||||||
|
doc,
|
||||||
|
...iconButtonProps
|
||||||
|
}: QuickActionProps) {
|
||||||
|
const t = useI18n();
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
const { quickTrash } = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const onMoveToTrash = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
track.allDocs.list.docMenu.deleteDoc();
|
||||||
|
openConfirmModal({
|
||||||
|
title: t['com.affine.moveToTrash.confirmModal.title'](),
|
||||||
|
description: t['com.affine.moveToTrash.confirmModal.description']({
|
||||||
|
title: doc.title$.value || t['Untitled'](),
|
||||||
|
}),
|
||||||
|
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||||
|
confirmText: t.Delete(),
|
||||||
|
confirmButtonOptions: {
|
||||||
|
variant: 'error',
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
doc.moveToTrash();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[doc, openConfirmModal, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!quickTrash) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={onMoveToTrash}
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
variant="danger"
|
||||||
|
{...iconButtonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuickSelect = memo(function QuickSelect({
|
||||||
|
doc,
|
||||||
|
...iconButtonProps
|
||||||
|
}: QuickActionProps) {
|
||||||
|
const { quickSelect, selectedDocIds, onToggleSelect } =
|
||||||
|
useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const selected = selectedDocIds.includes(doc.id);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onToggleSelect(doc.id);
|
||||||
|
},
|
||||||
|
[doc.id, onToggleSelect]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!quickSelect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={onChange}
|
||||||
|
icon={<Checkbox checked={selected} style={{ pointerEvents: 'none' }} />}
|
||||||
|
{...iconButtonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as styles from './properties.css';
|
||||||
|
|
||||||
|
export const StackProperty = ({
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.stackItem}>
|
||||||
|
<div className={styles.stackItemContent}>
|
||||||
|
<div className={styles.stackItemIcon}>{icon}</div>
|
||||||
|
<div className={styles.stackItemLabel}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { I18nString } from '@affine/i18n';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type QuickActionProps,
|
||||||
|
QuickDelete,
|
||||||
|
QuickFavorite,
|
||||||
|
QuickSelect,
|
||||||
|
QuickSplit,
|
||||||
|
QuickTab,
|
||||||
|
} from './docs-view/quick-actions';
|
||||||
|
import type { ExplorerPreference } from './types';
|
||||||
|
|
||||||
|
export interface QuickAction {
|
||||||
|
name: I18nString;
|
||||||
|
Component: React.FC<QuickActionProps>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtractPrefixKeys<Obj extends object, Prefix extends string> = {
|
||||||
|
[Key in keyof Obj]-?: Key extends `${Prefix}${string}` ? Key : never;
|
||||||
|
}[keyof Obj];
|
||||||
|
|
||||||
|
export type QuickActionKey = ExtractPrefixKeys<ExplorerPreference, 'quick'>;
|
||||||
|
|
||||||
|
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickAction> = {
|
||||||
|
quickFavorite: {
|
||||||
|
name: 'com.affine.all-docs.quick-action.favorite',
|
||||||
|
Component: QuickFavorite,
|
||||||
|
},
|
||||||
|
quickTrash: {
|
||||||
|
name: 'com.affine.all-docs.quick-action.trash',
|
||||||
|
Component: QuickDelete,
|
||||||
|
},
|
||||||
|
quickSplit: {
|
||||||
|
name: 'com.affine.all-docs.quick-action.split',
|
||||||
|
Component: QuickSplit,
|
||||||
|
disabled: !BUILD_CONFIG.isElectron,
|
||||||
|
},
|
||||||
|
quickTab: {
|
||||||
|
name: 'com.affine.all-docs.quick-action.tab',
|
||||||
|
Component: QuickTab,
|
||||||
|
},
|
||||||
|
quickSelect: {
|
||||||
|
name: 'com.affine.all-docs.quick-action.select',
|
||||||
|
Component: QuickSelect,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const quickActions = Object.entries(QUICK_ACTION_MAP).map(
|
||||||
|
([key, config]) => {
|
||||||
|
return { key: key as QuickActionKey, ...config };
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -3,9 +3,31 @@ import type {
|
|||||||
GroupByParams,
|
GroupByParams,
|
||||||
OrderByParams,
|
OrderByParams,
|
||||||
} from '@affine/core/modules/collection-rules/types';
|
} from '@affine/core/modules/collection-rules/types';
|
||||||
|
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||||
|
import type { DocRecord } from '@affine/core/modules/doc';
|
||||||
|
|
||||||
export interface ExplorerPreference {
|
export interface ExplorerPreference {
|
||||||
filters?: FilterParams[];
|
filters?: FilterParams[];
|
||||||
groupBy?: GroupByParams;
|
groupBy?: GroupByParams;
|
||||||
orderBy?: OrderByParams;
|
orderBy?: OrderByParams;
|
||||||
|
displayProperties?: string[];
|
||||||
|
showDocIcon?: boolean;
|
||||||
|
showDocPreview?: boolean;
|
||||||
|
quickFavorite?: boolean;
|
||||||
|
quickTrash?: boolean;
|
||||||
|
quickSplit?: boolean;
|
||||||
|
quickTab?: boolean;
|
||||||
|
quickSelect?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocListPropertyProps {
|
||||||
|
value: any;
|
||||||
|
doc: DocRecord;
|
||||||
|
propertyInfo: DocCustomPropertyInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupHeaderProps {
|
||||||
|
groupId: string;
|
||||||
|
docCount: number;
|
||||||
|
collapsed: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Checkbox, Menu, MenuItem, PropertyValue } from '@affine/component';
|
import { Checkbox, Menu, MenuItem, PropertyValue } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { CheckBoxCheckLinearIcon } from '@blocksuite/icons/rc';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './checkbox.css';
|
import * as styles from './checkbox.css';
|
||||||
|
|
||||||
@@ -73,3 +78,33 @@ export const CheckboxFilterValue = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CheckboxDocListProperty = ({
|
||||||
|
value,
|
||||||
|
propertyInfo,
|
||||||
|
}: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<CheckBoxCheckLinearIcon />}>
|
||||||
|
{/*
|
||||||
|
Has circular dependency issue (WorkspacePropertyName -> WorkspacePropertyTypes -> Checkbox)
|
||||||
|
<WorkspacePropertyName propertyInfo={propertyInfo} />
|
||||||
|
*/}
|
||||||
|
{propertyInfo.name || t['unnamed']()}
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckboxGroupHeader = ({
|
||||||
|
groupId,
|
||||||
|
docCount,
|
||||||
|
}: GroupHeaderProps) => {
|
||||||
|
const text = groupId === 'true' ? 'Checked' : 'Unchecked';
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader docCount={docCount} groupId={groupId}>
|
||||||
|
{text}
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,3 +3,9 @@ export const userWrapper = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const userLabelContainer = style({
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,34 +1,47 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import { PublicUserLabel } from '@affine/core/modules/cloud/views/public-user';
|
import { PublicUserLabel } from '@affine/core/modules/cloud/views/public-user';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { type DocRecord, DocService } from '@affine/core/modules/doc';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { type ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import { MemberSelectorInline } from '../member-selector';
|
import { MemberSelectorInline } from '../member-selector';
|
||||||
import { userWrapper } from './created-updated-by.css';
|
import * as styles from './created-updated-by.css';
|
||||||
|
|
||||||
const CreatedByUpdatedByAvatar = (props: {
|
const CreatedByUpdatedByAvatar = (props: {
|
||||||
type: 'CreatedBy' | 'UpdatedBy';
|
type: 'CreatedBy' | 'UpdatedBy';
|
||||||
|
doc: DocRecord;
|
||||||
|
size?: number;
|
||||||
|
showName?: boolean;
|
||||||
|
emptyFallback?: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const docService = useService(DocService);
|
const doc = props.doc;
|
||||||
|
|
||||||
const userId = useLiveData(
|
const userId = useLiveData(
|
||||||
props.type === 'CreatedBy'
|
props.type === 'CreatedBy' ? doc?.createdBy$ : doc?.updatedBy$
|
||||||
? docService.doc.createdBy$
|
|
||||||
: docService.doc.updatedBy$
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return (
|
return (
|
||||||
<div className={userWrapper}>
|
<div className={styles.userWrapper}>
|
||||||
<PublicUserLabel id={userId} />
|
<PublicUserLabel
|
||||||
|
id={userId}
|
||||||
|
size={props.size}
|
||||||
|
showName={props.showName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <NoRecordValue />;
|
return props.emptyFallback === undefined ? (
|
||||||
|
<NoRecordValue />
|
||||||
|
) : (
|
||||||
|
props.emptyFallback
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NoRecordValue = () => {
|
const NoRecordValue = () => {
|
||||||
@@ -46,6 +59,7 @@ const LocalUserValue = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CreatedByValue = () => {
|
export const CreatedByValue = () => {
|
||||||
|
const doc = useService(DocService).doc.record;
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const isCloud = workspaceService.workspace.flavour !== 'local';
|
const isCloud = workspaceService.workspace.flavour !== 'local';
|
||||||
|
|
||||||
@@ -59,12 +73,13 @@ export const CreatedByValue = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue readonly>
|
<PropertyValue readonly>
|
||||||
<CreatedByUpdatedByAvatar type="CreatedBy" />
|
<CreatedByUpdatedByAvatar type="CreatedBy" doc={doc} />
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UpdatedByValue = () => {
|
export const UpdatedByValue = () => {
|
||||||
|
const doc = useService(DocService).doc.record;
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const isCloud = workspaceService.workspace.flavour !== 'local';
|
const isCloud = workspaceService.workspace.flavour !== 'local';
|
||||||
|
|
||||||
@@ -78,7 +93,7 @@ export const UpdatedByValue = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue readonly>
|
<PropertyValue readonly>
|
||||||
<CreatedByUpdatedByAvatar type="UpdatedBy" />
|
<CreatedByUpdatedByAvatar type="UpdatedBy" doc={doc} />
|
||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -119,3 +134,45 @@ export const CreatedByUpdatedByFilterValue = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CreatedByDocListInlineProperty = ({
|
||||||
|
doc,
|
||||||
|
}: DocListPropertyProps) => {
|
||||||
|
return (
|
||||||
|
<CreatedByUpdatedByAvatar
|
||||||
|
doc={doc}
|
||||||
|
type="CreatedBy"
|
||||||
|
size={20}
|
||||||
|
emptyFallback={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatedByDocListInlineProperty = ({
|
||||||
|
doc,
|
||||||
|
}: DocListPropertyProps) => {
|
||||||
|
return (
|
||||||
|
<CreatedByUpdatedByAvatar
|
||||||
|
type="UpdatedBy"
|
||||||
|
doc={doc}
|
||||||
|
showName={false}
|
||||||
|
size={20}
|
||||||
|
emptyFallback={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModifiedByGroupHeader = ({
|
||||||
|
groupId,
|
||||||
|
docCount,
|
||||||
|
}: GroupHeaderProps) => {
|
||||||
|
const userId = groupId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||||
|
<div className={styles.userLabelContainer}>
|
||||||
|
<PublicUserLabel id={userId} size={20} />
|
||||||
|
</div>
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const empty = style({
|
export const empty = style({
|
||||||
color: cssVar('placeholderColor'),
|
color: cssVar('placeholderColor'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const dateDocListInlineProperty = style({
|
||||||
|
width: 60,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
color: cssVarV2.text.secondary,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tooltip = style({
|
||||||
|
display: 'inline-block',
|
||||||
|
selectors: {
|
||||||
|
'&::first-letter': {
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import { DatePicker, Menu, PropertyValue, Tooltip } from '@affine/component';
|
|||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { i18nTime, useI18n } from '@affine/i18n';
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
|
import { DateTimeIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './date.css';
|
import * as styles from './date.css';
|
||||||
|
|
||||||
@@ -184,3 +188,71 @@ export const DateFilterValue = ({
|
|||||||
</>
|
</>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DateDocListProperty = ({ value }: DocListPropertyProps) => {
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<DateTimeIcon />}>
|
||||||
|
{i18nTime(value, { absolute: { accuracy: 'day' } })}
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateDateDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const docMeta = useLiveData(doc.meta$);
|
||||||
|
const createDate = docMeta?.createDate;
|
||||||
|
|
||||||
|
if (!createDate) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<span className={styles.tooltip}>
|
||||||
|
{t.t('created at', { time: i18nTime(createDate) })}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.dateDocListInlineProperty}>
|
||||||
|
{i18nTime(createDate, { relative: true })}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatedDateDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const docMeta = useLiveData(doc.meta$);
|
||||||
|
const updatedDate = docMeta?.updatedDate;
|
||||||
|
|
||||||
|
if (!updatedDate) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<span className={styles.tooltip}>
|
||||||
|
{t.t('updated at', { time: i18nTime(updatedDate) })}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.dateDocListInlineProperty}>
|
||||||
|
{i18nTime(updatedDate, { relative: true })}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DateGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||||
|
const date = groupId || 'No Date';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||||
|
{date}
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatedGroupHeader = (props: GroupHeaderProps) => {
|
||||||
|
return <PlainTextDocGroupHeader {...props} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ import type { FilterParams } from '@affine/core/modules/collection-rules';
|
|||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { DocMode } from '@blocksuite/affine/model';
|
import type { DocMode } from '@blocksuite/affine/model';
|
||||||
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
|
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
|
||||||
import * as styles from './doc-primary-mode.css';
|
import * as styles from './doc-primary-mode.css';
|
||||||
@@ -114,3 +118,37 @@ export const DocPrimaryModeFilterValue = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DocPrimaryModeDocListProperty = ({
|
||||||
|
doc,
|
||||||
|
}: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const primaryMode = useLiveData(doc.primaryMode$);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty
|
||||||
|
icon={primaryMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
||||||
|
>
|
||||||
|
{primaryMode === 'edgeless' ? t['Edgeless']() : t['Page']()}
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocPrimaryModeGroupHeader = ({
|
||||||
|
groupId,
|
||||||
|
docCount,
|
||||||
|
}: GroupHeaderProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const text =
|
||||||
|
groupId === 'edgeless'
|
||||||
|
? t['com.affine.edgelessMode']()
|
||||||
|
: groupId === 'page'
|
||||||
|
? t['com.affine.pageMode']()
|
||||||
|
: 'Default';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||||
|
{text}
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { PropertyValue, type RadioItem } from '@affine/component';
|
import { PropertyValue, type RadioItem } from '@affine/component';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { EdgelessIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
|
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
|
||||||
import * as styles from './edgeless-theme.css';
|
import * as styles from './edgeless-theme.css';
|
||||||
@@ -56,3 +59,20 @@ export const EdgelessThemeValue = ({
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EdgelessThemeDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const edgelessTheme = useLiveData(
|
||||||
|
doc.properties$.selector(p => p.edgelessColorTheme)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<EdgelessIcon />}>
|
||||||
|
{edgelessTheme === 'system' || !edgelessTheme
|
||||||
|
? t['com.affine.themeSettings.auto']()
|
||||||
|
: edgelessTheme === 'light'
|
||||||
|
? t['com.affine.themeSettings.light']()
|
||||||
|
: t['com.affine.themeSettings.dark']()}
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -20,30 +20,64 @@ import {
|
|||||||
TodayIcon,
|
TodayIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import { CheckboxFilterValue, CheckboxValue } from './checkbox';
|
|
||||||
import {
|
import {
|
||||||
|
CheckboxDocListProperty,
|
||||||
|
CheckboxFilterValue,
|
||||||
|
CheckboxGroupHeader,
|
||||||
|
CheckboxValue,
|
||||||
|
} from './checkbox';
|
||||||
|
import {
|
||||||
|
CreatedByDocListInlineProperty,
|
||||||
CreatedByUpdatedByFilterValue,
|
CreatedByUpdatedByFilterValue,
|
||||||
CreatedByValue,
|
CreatedByValue,
|
||||||
|
ModifiedByGroupHeader,
|
||||||
|
UpdatedByDocListInlineProperty,
|
||||||
UpdatedByValue,
|
UpdatedByValue,
|
||||||
} from './created-updated-by';
|
} from './created-updated-by';
|
||||||
import {
|
import {
|
||||||
|
CreateDateDocListProperty,
|
||||||
CreateDateValue,
|
CreateDateValue,
|
||||||
|
CreatedGroupHeader,
|
||||||
|
DateDocListProperty,
|
||||||
DateFilterValue,
|
DateFilterValue,
|
||||||
|
DateGroupHeader,
|
||||||
DateValue,
|
DateValue,
|
||||||
|
UpdatedDateDocListProperty,
|
||||||
UpdatedDateValue,
|
UpdatedDateValue,
|
||||||
} from './date';
|
} from './date';
|
||||||
import {
|
import {
|
||||||
|
DocPrimaryModeDocListProperty,
|
||||||
DocPrimaryModeFilterValue,
|
DocPrimaryModeFilterValue,
|
||||||
|
DocPrimaryModeGroupHeader,
|
||||||
DocPrimaryModeValue,
|
DocPrimaryModeValue,
|
||||||
} from './doc-primary-mode';
|
} from './doc-primary-mode';
|
||||||
import { EdgelessThemeValue } from './edgeless-theme';
|
import {
|
||||||
import { JournalFilterValue, JournalValue } from './journal';
|
EdgelessThemeDocListProperty,
|
||||||
import { NumberValue } from './number';
|
EdgelessThemeValue,
|
||||||
import { PageWidthValue } from './page-width';
|
} from './edgeless-theme';
|
||||||
import { TagsFilterValue, TagsValue } from './tags';
|
import {
|
||||||
import { TemplateValue } from './template';
|
JournalDocListProperty,
|
||||||
import { TextFilterValue, TextValue } from './text';
|
JournalFilterValue,
|
||||||
|
JournalGroupHeader,
|
||||||
|
JournalValue,
|
||||||
|
} from './journal';
|
||||||
|
import { NumberDocListProperty, NumberValue } from './number';
|
||||||
|
import { PageWidthDocListProperty, PageWidthValue } from './page-width';
|
||||||
|
import {
|
||||||
|
TagsDocListProperty,
|
||||||
|
TagsFilterValue,
|
||||||
|
TagsGroupHeader,
|
||||||
|
TagsValue,
|
||||||
|
} from './tags';
|
||||||
|
import { TemplateDocListProperty, TemplateValue } from './template';
|
||||||
|
import {
|
||||||
|
TextDocListProperty,
|
||||||
|
TextFilterValue,
|
||||||
|
TextGroupHeader,
|
||||||
|
TextValue,
|
||||||
|
} from './text';
|
||||||
|
|
||||||
const DateFilterMethod = {
|
const DateFilterMethod = {
|
||||||
after: 'com.affine.filter.after',
|
after: 'com.affine.filter.after',
|
||||||
@@ -76,6 +110,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
allowInOrderBy: true,
|
allowInOrderBy: true,
|
||||||
defaultFilter: { method: 'is-not-empty' },
|
defaultFilter: { method: 'is-not-empty' },
|
||||||
filterValue: TagsFilterValue,
|
filterValue: TagsFilterValue,
|
||||||
|
showInDocList: 'inline',
|
||||||
|
docListProperty: TagsDocListProperty,
|
||||||
|
groupHeader: TagsGroupHeader,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
icon: TextIcon,
|
icon: TextIcon,
|
||||||
@@ -92,12 +129,17 @@ export const WorkspacePropertyTypes = {
|
|||||||
allowInOrderBy: true,
|
allowInOrderBy: true,
|
||||||
filterValue: TextFilterValue,
|
filterValue: TextFilterValue,
|
||||||
defaultFilter: { method: 'is-not-empty' },
|
defaultFilter: { method: 'is-not-empty' },
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: TextDocListProperty,
|
||||||
|
groupHeader: TextGroupHeader,
|
||||||
},
|
},
|
||||||
number: {
|
number: {
|
||||||
icon: NumberIcon,
|
icon: NumberIcon,
|
||||||
value: NumberValue,
|
value: NumberValue,
|
||||||
name: 'com.affine.page-properties.property.number',
|
name: 'com.affine.page-properties.property.number',
|
||||||
description: 'com.affine.page-properties.property.number.tooltips',
|
description: 'com.affine.page-properties.property.number.tooltips',
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: NumberDocListProperty,
|
||||||
},
|
},
|
||||||
checkbox: {
|
checkbox: {
|
||||||
icon: CheckBoxCheckLinearIcon,
|
icon: CheckBoxCheckLinearIcon,
|
||||||
@@ -112,6 +154,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
allowInOrderBy: true,
|
allowInOrderBy: true,
|
||||||
filterValue: CheckboxFilterValue,
|
filterValue: CheckboxFilterValue,
|
||||||
defaultFilter: { method: 'is', value: 'true' },
|
defaultFilter: { method: 'is', value: 'true' },
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: CheckboxDocListProperty,
|
||||||
|
groupHeader: CheckboxGroupHeader,
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
icon: DateTimeIcon,
|
icon: DateTimeIcon,
|
||||||
@@ -127,6 +172,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
allowInOrderBy: true,
|
allowInOrderBy: true,
|
||||||
filterValue: DateFilterValue,
|
filterValue: DateFilterValue,
|
||||||
defaultFilter: { method: 'is-not-empty' },
|
defaultFilter: { method: 'is-not-empty' },
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: DateDocListProperty,
|
||||||
|
groupHeader: DateGroupHeader,
|
||||||
},
|
},
|
||||||
createdBy: {
|
createdBy: {
|
||||||
icon: MemberIcon,
|
icon: MemberIcon,
|
||||||
@@ -140,6 +188,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: CreatedByUpdatedByFilterValue,
|
filterValue: CreatedByUpdatedByFilterValue,
|
||||||
defaultFilter: { method: 'include', value: '' },
|
defaultFilter: { method: 'include', value: '' },
|
||||||
|
showInDocList: 'inline',
|
||||||
|
docListProperty: CreatedByDocListInlineProperty,
|
||||||
|
groupHeader: ModifiedByGroupHeader,
|
||||||
},
|
},
|
||||||
updatedBy: {
|
updatedBy: {
|
||||||
icon: MemberIcon,
|
icon: MemberIcon,
|
||||||
@@ -153,6 +204,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: CreatedByUpdatedByFilterValue,
|
filterValue: CreatedByUpdatedByFilterValue,
|
||||||
defaultFilter: { method: 'include', value: '' },
|
defaultFilter: { method: 'include', value: '' },
|
||||||
|
showInDocList: 'inline',
|
||||||
|
docListProperty: UpdatedByDocListInlineProperty,
|
||||||
|
groupHeader: ModifiedByGroupHeader,
|
||||||
},
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
icon: DateTimeIcon,
|
icon: DateTimeIcon,
|
||||||
@@ -167,6 +221,8 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: DateFilterValue,
|
filterValue: DateFilterValue,
|
||||||
defaultFilter: { method: 'this-week' },
|
defaultFilter: { method: 'this-week' },
|
||||||
|
showInDocList: 'inline',
|
||||||
|
docListProperty: UpdatedDateDocListProperty,
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
icon: HistoryIcon,
|
icon: HistoryIcon,
|
||||||
@@ -181,6 +237,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: DateFilterValue,
|
filterValue: DateFilterValue,
|
||||||
defaultFilter: { method: 'this-week' },
|
defaultFilter: { method: 'this-week' },
|
||||||
|
showInDocList: 'inline',
|
||||||
|
docListProperty: CreateDateDocListProperty,
|
||||||
|
groupHeader: CreatedGroupHeader,
|
||||||
},
|
},
|
||||||
docPrimaryMode: {
|
docPrimaryMode: {
|
||||||
icon: FileIcon,
|
icon: FileIcon,
|
||||||
@@ -195,6 +254,9 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: DocPrimaryModeFilterValue,
|
filterValue: DocPrimaryModeFilterValue,
|
||||||
defaultFilter: { method: 'is', value: 'page' },
|
defaultFilter: { method: 'is', value: 'page' },
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: DocPrimaryModeDocListProperty,
|
||||||
|
groupHeader: DocPrimaryModeGroupHeader,
|
||||||
},
|
},
|
||||||
journal: {
|
journal: {
|
||||||
icon: TodayIcon,
|
icon: TodayIcon,
|
||||||
@@ -209,18 +271,25 @@ export const WorkspacePropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: JournalFilterValue,
|
filterValue: JournalFilterValue,
|
||||||
defaultFilter: { method: 'is', value: 'true' },
|
defaultFilter: { method: 'is', value: 'true' },
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: JournalDocListProperty,
|
||||||
|
groupHeader: JournalGroupHeader,
|
||||||
},
|
},
|
||||||
edgelessTheme: {
|
edgelessTheme: {
|
||||||
icon: EdgelessIcon,
|
icon: EdgelessIcon,
|
||||||
value: EdgelessThemeValue,
|
value: EdgelessThemeValue,
|
||||||
name: 'com.affine.page-properties.property.edgelessTheme',
|
name: 'com.affine.page-properties.property.edgelessTheme',
|
||||||
description: 'com.affine.page-properties.property.edgelessTheme.tooltips',
|
description: 'com.affine.page-properties.property.edgelessTheme.tooltips',
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: EdgelessThemeDocListProperty,
|
||||||
},
|
},
|
||||||
pageWidth: {
|
pageWidth: {
|
||||||
icon: LongerIcon,
|
icon: LongerIcon,
|
||||||
value: PageWidthValue,
|
value: PageWidthValue,
|
||||||
name: 'com.affine.page-properties.property.pageWidth',
|
name: 'com.affine.page-properties.property.pageWidth',
|
||||||
description: 'com.affine.page-properties.property.pageWidth.tooltips',
|
description: 'com.affine.page-properties.property.pageWidth.tooltips',
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: PageWidthDocListProperty,
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
icon: TemplateIcon,
|
icon: TemplateIcon,
|
||||||
@@ -228,6 +297,8 @@ export const WorkspacePropertyTypes = {
|
|||||||
name: 'com.affine.page-properties.property.template',
|
name: 'com.affine.page-properties.property.template',
|
||||||
renameable: true,
|
renameable: true,
|
||||||
description: 'com.affine.page-properties.property.template.tooltips',
|
description: 'com.affine.page-properties.property.template.tooltips',
|
||||||
|
showInDocList: 'stack',
|
||||||
|
docListProperty: TemplateDocListProperty,
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
icon: PropertyIcon,
|
icon: PropertyIcon,
|
||||||
@@ -254,6 +325,14 @@ export const WorkspacePropertyTypes = {
|
|||||||
name: I18nString;
|
name: I18nString;
|
||||||
renameable?: boolean;
|
renameable?: boolean;
|
||||||
description?: I18nString;
|
description?: I18nString;
|
||||||
|
/**
|
||||||
|
* Whether to show the property in the doc list,
|
||||||
|
* - `inline`: show the property in the doc list inline
|
||||||
|
* - `stack`: show as tags
|
||||||
|
*/
|
||||||
|
showInDocList?: 'inline' | 'stack';
|
||||||
|
docListProperty?: React.FC<DocListPropertyProps>;
|
||||||
|
groupHeader?: React.FC<GroupHeaderProps>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { JournalService } from '@affine/core/modules/journal';
|
|||||||
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 { i18nTime, useI18n } from '@affine/i18n';
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
|
import { TodayIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
@@ -20,6 +21,9 @@ import {
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './journal.css';
|
import * as styles from './journal.css';
|
||||||
|
|
||||||
@@ -216,3 +220,27 @@ export const JournalFilterValue = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const JournalDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const journalService = useService(JournalService);
|
||||||
|
const journalDate = useLiveData(journalService.journalDate$(doc.id));
|
||||||
|
|
||||||
|
if (!journalDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<TodayIcon />}>
|
||||||
|
{i18nTime(journalDate, { absolute: { accuracy: 'day' } })}
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JournalGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||||
|
const text = groupId === 'true' ? 'Journal' : 'Not Journal';
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||||
|
{text}
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { NumberIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
type ChangeEventHandler,
|
type ChangeEventHandler,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './number.css';
|
import * as styles from './number.css';
|
||||||
|
|
||||||
@@ -55,3 +57,11 @@ export const NumberValue = ({
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NumberDocListProperty = ({ value }: { value: number }) => {
|
||||||
|
if (value !== 0 && !value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StackProperty icon={<NumberIcon />}>{value}</StackProperty>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { PropertyValue, type RadioItem } from '@affine/component';
|
|||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { LongerIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
|
import { PropertyRadioGroup } from '../properties/widgets/radio-group';
|
||||||
import { container } from './page-width.css';
|
import { container } from './page-width.css';
|
||||||
@@ -58,3 +61,20 @@ export const PageWidthValue = ({ readonly }: PropertyValueProps) => {
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PageWidthDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const pageWidth = useLiveData(doc.properties$.selector(p => p.pageWidth));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<LongerIcon />}>
|
||||||
|
{pageWidth === 'standard' || !pageWidth
|
||||||
|
? t[
|
||||||
|
'com.affine.settings.editorSettings.page.default-page-width.standard'
|
||||||
|
]()
|
||||||
|
: t[
|
||||||
|
'com.affine.settings.editorSettings.page.default-page-width.full-width'
|
||||||
|
]()}
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,3 +5,22 @@ export const tagInlineEditor = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const container = style({});
|
export const container = style({});
|
||||||
|
|
||||||
|
export const groupHeader = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
export const groupHeaderIcon = style({
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
fontSize: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
export const groupHeaderLabel = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { type Tag, TagService } from '@affine/core/modules/tag';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { TagsIcon } from '@blocksuite/icons/rc';
|
import { TagsIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -9,6 +9,9 @@ import { useLiveData, useService } from '@toeverything/infra';
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import {
|
import {
|
||||||
@@ -164,3 +167,73 @@ const TagsInlineEditor = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TagName = ({ tag }: { tag: Tag }) => {
|
||||||
|
const name = useLiveData(tag.value$);
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
const TagIcon = ({ tag, size = 8 }: { tag: Tag; size?: number }) => {
|
||||||
|
const color = useLiveData(tag.color$);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: '50%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const TagsDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const tagList = useService(TagService).tagList;
|
||||||
|
const tags = useLiveData(tagList.tagsByPageId$(doc.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tags.map(tag => {
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<TagIcon tag={tag} />} key={tag.id}>
|
||||||
|
<TagName tag={tag} />
|
||||||
|
</StackProperty>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TagsGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const tagService = useService(TagService);
|
||||||
|
const tag = useLiveData(tagService.tagList.tagByTagId$(groupId));
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader
|
||||||
|
groupId={groupId}
|
||||||
|
docCount={docCount}
|
||||||
|
icon={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: cssVarV2.icon.secondary,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t['com.affine.page.display.grouping.group-by-tag.untagged']()}
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader
|
||||||
|
groupId={groupId}
|
||||||
|
docCount={docCount}
|
||||||
|
icon={<TagIcon tag={tag} />}
|
||||||
|
>
|
||||||
|
<TagName tag={tag} />
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Checkbox, PropertyValue } from '@affine/component';
|
import { Checkbox, PropertyValue } from '@affine/component';
|
||||||
import { DocService } from '@affine/core/modules/doc';
|
import { DocService } from '@affine/core/modules/doc';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { TemplateIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { type ChangeEvent, useCallback } from 'react';
|
import { type ChangeEvent, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { DocListPropertyProps } from '../explorer/types';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './template.css';
|
import * as styles from './template.css';
|
||||||
|
|
||||||
@@ -39,3 +43,16 @@ export const TemplateValue = ({ readonly }: PropertyValueProps) => {
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TemplateDocListProperty = ({ doc }: DocListPropertyProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const isTemplate = useLiveData(doc.properties$.selector(p => p.isTemplate));
|
||||||
|
|
||||||
|
if (!isTemplate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackProperty icon={<TemplateIcon />}>{t['Template']()}</StackProperty>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Input, Menu, PropertyValue } from '@affine/component';
|
import { Input, Menu, PropertyValue } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { TextIcon } from '@blocksuite/icons/rc';
|
import { TextIcon, TextTypeIcon } from '@blocksuite/icons/rc';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
|
import type { GroupHeaderProps } from '../explorer/types';
|
||||||
import { ConfigModal } from '../mobile';
|
import { ConfigModal } from '../mobile';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './text.css';
|
import * as styles from './text.css';
|
||||||
@@ -248,3 +251,20 @@ export const TextFilterValue = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TextDocListProperty = ({ value }: { value: string }) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StackProperty icon={<TextTypeIcon />}>{value}</StackProperty>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||||
|
const text = groupId || 'No Text';
|
||||||
|
return (
|
||||||
|
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||||
|
{text}
|
||||||
|
</PlainTextDocGroupHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const scrollContainer = style({
|
export const scrollContainer = style({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -27,3 +28,17 @@ export const body = style({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const scrollArea = style({
|
||||||
|
height: 0,
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// group
|
||||||
|
export const groupHeader = style({
|
||||||
|
background: cssVarV2.layer.background.primary,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const docItem = style({
|
||||||
|
transition: 'width 0.2s ease-in-out',
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Masonry,
|
||||||
|
type MasonryGroup,
|
||||||
|
RadioGroup,
|
||||||
|
useConfirmModal,
|
||||||
|
} from '@affine/component';
|
||||||
|
import {
|
||||||
|
DocExplorerContext,
|
||||||
|
type DocExplorerContextType,
|
||||||
|
} from '@affine/core/components/explorer/context';
|
||||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||||
|
import {
|
||||||
|
DocListItem,
|
||||||
|
type DocListItemView,
|
||||||
|
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||||
import { Filters } from '@affine/core/components/filter';
|
import { Filters } from '@affine/core/components/filter';
|
||||||
|
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
||||||
|
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
||||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ViewBody,
|
ViewBody,
|
||||||
@@ -16,14 +42,123 @@ import {
|
|||||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||||
import * as styles from './all-page.css';
|
import * as styles from './all-page.css';
|
||||||
import { MigrationAllDocsDataNotification } from './migration-data';
|
import { MigrationAllDocsDataNotification } from './migration-data';
|
||||||
|
|
||||||
|
const GroupHeader = memo(function GroupHeader({
|
||||||
|
groupId,
|
||||||
|
collapsed,
|
||||||
|
itemCount,
|
||||||
|
}: {
|
||||||
|
groupId: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
itemCount: number;
|
||||||
|
}) {
|
||||||
|
const { groupBy } = useContext(DocExplorerContext);
|
||||||
|
const propertyService = useService(WorkspacePropertyService);
|
||||||
|
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||||
|
|
||||||
|
const groupType = groupBy?.type;
|
||||||
|
const groupKey = groupBy?.key;
|
||||||
|
|
||||||
|
const header = useMemo(() => {
|
||||||
|
if (groupType === 'property') {
|
||||||
|
const property = allProperties.find(p => p.id === groupKey);
|
||||||
|
if (!property) return null;
|
||||||
|
|
||||||
|
const config = WorkspacePropertyTypes[property.type];
|
||||||
|
if (!config?.groupHeader) return null;
|
||||||
|
return (
|
||||||
|
<config.groupHeader
|
||||||
|
groupId={groupId}
|
||||||
|
docCount={itemCount}
|
||||||
|
collapsed={!!collapsed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return '// TODO: ' + groupType;
|
||||||
|
}
|
||||||
|
}, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
|
||||||
|
|
||||||
|
if (!groupType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calcCardHeightById = (id: string) => {
|
||||||
|
const max = 5;
|
||||||
|
const min = 1;
|
||||||
|
const code = id.charCodeAt(0);
|
||||||
|
const value = Math.floor((code % (max - min)) + min);
|
||||||
|
return 250 + value * 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DocListItemComponent = memo(function DocListItemComponent({
|
||||||
|
itemId,
|
||||||
|
groupId,
|
||||||
|
}: {
|
||||||
|
groupId: string;
|
||||||
|
itemId: string;
|
||||||
|
}) {
|
||||||
|
return <DocListItem docId={itemId} groupId={groupId} />;
|
||||||
|
});
|
||||||
|
|
||||||
export const AllPage = () => {
|
export const AllPage = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const [view, setView] = useState<DocListItemView>('masonry');
|
||||||
|
const [collapsedGroups, setCollapsedGroups] = useState<string[]>([]);
|
||||||
|
const [selectMode, setSelectMode] = useState(false);
|
||||||
|
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([]);
|
||||||
|
const [prevCheckAnchorId, setPrevCheckAnchorId] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const [explorerPreference, setExplorerPreference] =
|
const [explorerPreference, setExplorerPreference] =
|
||||||
useState<ExplorerPreference>({});
|
useState<ExplorerPreference>({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
value: 'false',
|
||||||
|
method: 'is',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayProperties: [],
|
||||||
|
showDocIcon: true,
|
||||||
|
showDocPreview: true,
|
||||||
|
});
|
||||||
|
|
||||||
const [groups, setGroups] = useState<any>([]);
|
const [groups, setGroups] = useState<any>([]);
|
||||||
|
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
|
const masonryItems = useMemo(() => {
|
||||||
|
const items = groups.map((group: any) => {
|
||||||
|
return {
|
||||||
|
id: group.key,
|
||||||
|
Component: groups.length > 1 ? GroupHeader : undefined,
|
||||||
|
height: groups.length > 1 ? 24 : 0,
|
||||||
|
className: styles.groupHeader,
|
||||||
|
items: group.items.map((docId: string) => {
|
||||||
|
return {
|
||||||
|
id: docId,
|
||||||
|
Component: DocListItemComponent,
|
||||||
|
height:
|
||||||
|
view === 'list'
|
||||||
|
? 42
|
||||||
|
: view === 'grid'
|
||||||
|
? 280
|
||||||
|
: calcCardHeightById(docId),
|
||||||
|
'data-view': view,
|
||||||
|
className: styles.docItem,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
} satisfies MasonryGroup;
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, [groups, view]);
|
||||||
|
|
||||||
const collectionRulesService = useService(CollectionRulesService);
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
@@ -43,7 +178,26 @@ export const AllPage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [collectionRulesService, explorerPreference]);
|
}, [
|
||||||
|
collectionRulesService,
|
||||||
|
explorerPreference.filters,
|
||||||
|
explorerPreference.groupBy,
|
||||||
|
explorerPreference.orderBy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setSelectMode(false);
|
||||||
|
setSelectedDocIds([]);
|
||||||
|
setPrevCheckAnchorId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFilterChange = useCallback((filters: FilterParams[]) => {
|
const handleFilterChange = useCallback((filters: FilterParams[]) => {
|
||||||
setExplorerPreference(prev => ({
|
setExplorerPreference(prev => ({
|
||||||
@@ -51,8 +205,92 @@ export const AllPage = () => {
|
|||||||
filters,
|
filters,
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleGroupCollapse = useCallback((groupId: string) => {
|
||||||
|
setCollapsedGroups(prev => {
|
||||||
|
return prev.includes(groupId)
|
||||||
|
? prev.filter(id => id !== groupId)
|
||||||
|
: [...prev, groupId];
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const toggleDocSelect = useCallback((docId: string) => {
|
||||||
|
setSelectMode(true);
|
||||||
|
setSelectedDocIds(prev => {
|
||||||
|
return prev.includes(docId)
|
||||||
|
? prev.filter(id => id !== docId)
|
||||||
|
: [...prev, docId];
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const onSelect = useCallback(
|
||||||
|
(...args: Parameters<typeof setSelectedDocIds>) => {
|
||||||
|
setSelectMode(true);
|
||||||
|
setSelectedDocIds(...args);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleCloseFloatingToolbar = useCallback(() => {
|
||||||
|
setSelectMode(false);
|
||||||
|
setSelectedDocIds([]);
|
||||||
|
}, []);
|
||||||
|
const handleMultiDelete = useCallback(() => {
|
||||||
|
if (selectedDocIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openConfirmModal({
|
||||||
|
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
|
||||||
|
number: selectedDocIds.length.toString(),
|
||||||
|
}),
|
||||||
|
description: t[
|
||||||
|
'com.affine.moveToTrash.confirmModal.description.multiple'
|
||||||
|
]({
|
||||||
|
number: selectedDocIds.length.toString(),
|
||||||
|
}),
|
||||||
|
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||||
|
confirmText: t.Delete(),
|
||||||
|
confirmButtonOptions: {
|
||||||
|
variant: 'error',
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
for (const docId of selectedDocIds) {
|
||||||
|
const doc = docsService.list.doc$(docId).value;
|
||||||
|
doc?.moveToTrash();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
||||||
|
|
||||||
|
const explorerContextValue = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
...explorerPreference,
|
||||||
|
view,
|
||||||
|
groups,
|
||||||
|
collapsed: collapsedGroups,
|
||||||
|
selectedDocIds,
|
||||||
|
selectMode,
|
||||||
|
prevCheckAnchorId,
|
||||||
|
setPrevCheckAnchorId,
|
||||||
|
onToggleCollapse: toggleGroupCollapse,
|
||||||
|
onToggleSelect: toggleDocSelect,
|
||||||
|
onSelect,
|
||||||
|
}) satisfies DocExplorerContextType,
|
||||||
|
[
|
||||||
|
collapsedGroups,
|
||||||
|
explorerPreference,
|
||||||
|
groups,
|
||||||
|
onSelect,
|
||||||
|
prevCheckAnchorId,
|
||||||
|
selectMode,
|
||||||
|
selectedDocIds,
|
||||||
|
toggleDocSelect,
|
||||||
|
toggleGroupCollapse,
|
||||||
|
view,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||||
<ViewTitle title={t['All pages']()} />
|
<ViewTitle title={t['All pages']()} />
|
||||||
<ViewIcon icon="allDocs" />
|
<ViewIcon icon="allDocs" />
|
||||||
<ViewHeader></ViewHeader>
|
<ViewHeader></ViewHeader>
|
||||||
@@ -60,6 +298,12 @@ export const AllPage = () => {
|
|||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<MigrationAllDocsDataNotification />
|
<MigrationAllDocsDataNotification />
|
||||||
<div>
|
<div>
|
||||||
|
<RadioGroup
|
||||||
|
items={['masonry', 'grid', 'list']}
|
||||||
|
value={view}
|
||||||
|
onChange={setView}
|
||||||
|
width={240}
|
||||||
|
/>
|
||||||
<Filters
|
<Filters
|
||||||
filters={explorerPreference.filters ?? []}
|
filters={explorerPreference.filters ?? []}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
@@ -69,12 +313,45 @@ export const AllPage = () => {
|
|||||||
onChange={setExplorerPreference}
|
onChange={setExplorerPreference}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<pre>{JSON.stringify(explorerPreference, null, 2)}</pre>
|
<div className={styles.scrollArea}>
|
||||||
<pre>{JSON.stringify(groups, null, 2)}</pre>
|
<Masonry
|
||||||
|
items={masonryItems}
|
||||||
|
gapY={12}
|
||||||
|
gapX={12}
|
||||||
|
groupsGap={12}
|
||||||
|
groupHeaderGapWithItems={12}
|
||||||
|
columns={view === 'list' ? 1 : undefined}
|
||||||
|
itemWidthMin={220}
|
||||||
|
preloadHeight={100}
|
||||||
|
itemWidth={'stretch'}
|
||||||
|
virtualScroll
|
||||||
|
collapsedGroups={collapsedGroups}
|
||||||
|
paddingX={useCallback(
|
||||||
|
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
||||||
|
[]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ListFloatingToolbar
|
||||||
|
open={selectMode}
|
||||||
|
onDelete={handleMultiDelete}
|
||||||
|
onClose={handleCloseFloatingToolbar}
|
||||||
|
content={
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.page.toolbar.selected"
|
||||||
|
count={selectedDocIds.length}
|
||||||
|
>
|
||||||
|
<div style={{ color: cssVarV2.text.secondary }}>
|
||||||
|
{{ count: selectedDocIds.length } as any}
|
||||||
|
</div>
|
||||||
|
selected
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
<AllDocSidebarTabs />
|
<AllDocSidebarTabs />
|
||||||
</>
|
</DocExplorerContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ export type { ServerConfig } from './types';
|
|||||||
// eslint-disable-next-line simple-import-sort/imports
|
// eslint-disable-next-line simple-import-sort/imports
|
||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { DocScope } from '../doc/scopes/doc';
|
|
||||||
import { DocService } from '../doc/services/doc';
|
|
||||||
import { GlobalCache, GlobalState } from '../storage/providers/global';
|
import { GlobalCache, GlobalState } from '../storage/providers/global';
|
||||||
import { GlobalStateService } from '../storage/services/global';
|
import { GlobalStateService } from '../storage/services/global';
|
||||||
import { UrlService } from '../url';
|
import { UrlService } from '../url';
|
||||||
@@ -101,7 +99,7 @@ import { DocCreatedByService } from './services/doc-created-by';
|
|||||||
import { DocUpdatedByService } from './services/doc-updated-by';
|
import { DocUpdatedByService } from './services/doc-updated-by';
|
||||||
import { DocCreatedByUpdatedBySyncService } from './services/doc-created-by-updated-by-sync';
|
import { DocCreatedByUpdatedBySyncService } from './services/doc-created-by-updated-by-sync';
|
||||||
import { WorkspacePermissionService } from '../permissions';
|
import { WorkspacePermissionService } from '../permissions';
|
||||||
import { DocsService } from '../doc';
|
import { DocScope, DocService, DocsService } from '../doc';
|
||||||
import { DocCreatedByUpdatedBySyncStore } from './stores/doc-created-by-updated-by-sync';
|
import { DocCreatedByUpdatedBySyncStore } from './stores/doc-created-by-updated-by-sync';
|
||||||
|
|
||||||
export function configureCloudModule(framework: Framework) {
|
export function configureCloudModule(framework: Framework) {
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import { useLayoutEffect, useMemo } from 'react';
|
|||||||
import { PublicUserService } from '../services/public-user';
|
import { PublicUserService } from '../services/public-user';
|
||||||
import * as styles from './public-user.css';
|
import * as styles from './public-user.css';
|
||||||
|
|
||||||
export const PublicUserLabel = ({ id }: { id: string }) => {
|
export const PublicUserLabel = ({
|
||||||
|
id,
|
||||||
|
size = 20,
|
||||||
|
showName = true,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
size?: number;
|
||||||
|
showName?: boolean;
|
||||||
|
}) => {
|
||||||
const serverService = useCurrentServerService();
|
const serverService = useCurrentServerService();
|
||||||
const publicUser = useMemo(() => {
|
const publicUser = useMemo(() => {
|
||||||
return serverService?.scope.get(PublicUserService);
|
return serverService?.scope.get(PublicUserService);
|
||||||
@@ -28,10 +36,16 @@ export const PublicUserLabel = ({ id }: { id: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user?.removed) {
|
if (user?.removed) {
|
||||||
return (
|
return showName ? (
|
||||||
<span className={styles.publicUserLabelRemoved}>
|
<span className={styles.publicUserLabelRemoved}>
|
||||||
{t['Unknown User']()}
|
{t['Unknown User']()}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
size={size}
|
||||||
|
name={t['Unknown User']()}
|
||||||
|
className={styles.publicUserLabelAvatar}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +54,10 @@ export const PublicUserLabel = ({ id }: { id: string }) => {
|
|||||||
<Avatar
|
<Avatar
|
||||||
url={user?.avatar}
|
url={user?.avatar}
|
||||||
name={user?.name ?? ''}
|
name={user?.name ?? ''}
|
||||||
size={20}
|
size={size}
|
||||||
className={styles.publicUserLabelAvatar}
|
className={styles.publicUserLabelAvatar}
|
||||||
/>
|
/>
|
||||||
{user?.name}
|
{showName && user?.name}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class PropertyFilterProvider extends Service implements FilterProvider {
|
|||||||
FilterProvider('property:' + type)
|
FilterProvider('property:' + type)
|
||||||
);
|
);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error('Unsupported property type');
|
throw new Error(`Unsupported property type: ${type}`);
|
||||||
}
|
}
|
||||||
return provider.filter$(params);
|
return provider.filter$(params);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
import { map, type Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import type { FilterProvider } from '../../provider';
|
||||||
|
import type { FilterParams } from '../../types';
|
||||||
|
|
||||||
|
export class TrashFilterProvider extends Service implements FilterProvider {
|
||||||
|
constructor(private readonly docsService: DocsService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
|
if (params.value === 'true') {
|
||||||
|
return this.docsService.allTrashDocIds$().pipe(map(ids => new Set(ids)));
|
||||||
|
} else {
|
||||||
|
return this.docsService
|
||||||
|
.allNonTrashDocIds$()
|
||||||
|
.pipe(map(ids => new Set(ids)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { PropertyFilterProvider } from './impls/filters/property';
|
|||||||
import { SystemFilterProvider } from './impls/filters/system';
|
import { SystemFilterProvider } from './impls/filters/system';
|
||||||
import { TagsFilterProvider } from './impls/filters/tags';
|
import { TagsFilterProvider } from './impls/filters/tags';
|
||||||
import { TextPropertyFilterProvider } from './impls/filters/text';
|
import { TextPropertyFilterProvider } from './impls/filters/text';
|
||||||
|
import { TrashFilterProvider } from './impls/filters/trash';
|
||||||
import { UpdatedAtFilterProvider } from './impls/filters/updated-at';
|
import { UpdatedAtFilterProvider } from './impls/filters/updated-at';
|
||||||
import { UpdatedByFilterProvider } from './impls/filters/updated-by';
|
import { UpdatedByFilterProvider } from './impls/filters/updated-by';
|
||||||
import { CheckboxPropertyGroupByProvider } from './impls/group-by/checkbox';
|
import { CheckboxPropertyGroupByProvider } from './impls/group-by/checkbox';
|
||||||
@@ -79,6 +80,7 @@ export function configureCollectionRulesModule(framework: Framework) {
|
|||||||
DocPrimaryModeFilterProvider,
|
DocPrimaryModeFilterProvider,
|
||||||
[DocsService]
|
[DocsService]
|
||||||
)
|
)
|
||||||
|
.impl(FilterProvider('system:trash'), TrashFilterProvider, [DocsService])
|
||||||
.impl(FilterProvider('property:date'), DatePropertyFilterProvider, [
|
.impl(FilterProvider('property:date'), DatePropertyFilterProvider, [
|
||||||
DocsService,
|
DocsService,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ export class DocsService extends Service {
|
|||||||
return this.store.watchAllDocTagIds();
|
return this.store.watchAllDocTagIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allNonTrashDocIds$() {
|
||||||
|
return this.store.watchNonTrashDocIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
allTrashDocIds$() {
|
||||||
|
return this.store.watchTrashDocIds();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly store: DocsStore,
|
private readonly store: DocsStore,
|
||||||
private readonly docPropertiesStore: DocPropertiesStore,
|
private readonly docPropertiesStore: DocPropertiesStore,
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"ar": 97,
|
"ar": 96,
|
||||||
"ca": 4,
|
"ca": 4,
|
||||||
"da": 4,
|
"da": 4,
|
||||||
"de": 97,
|
"de": 96,
|
||||||
"el-GR": 97,
|
"el-GR": 96,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-AR": 97,
|
"es-AR": 96,
|
||||||
"es-CL": 98,
|
"es-CL": 98,
|
||||||
"es": 97,
|
"es": 96,
|
||||||
"fa": 97,
|
"fa": 96,
|
||||||
"fr": 97,
|
"fr": 96,
|
||||||
"hi": 2,
|
"hi": 2,
|
||||||
"it-IT": 97,
|
"it-IT": 96,
|
||||||
"it": 1,
|
"it": 1,
|
||||||
"ja": 97,
|
"ja": 96,
|
||||||
"ko": 56,
|
"ko": 56,
|
||||||
"pl": 97,
|
"pl": 96,
|
||||||
"pt-BR": 97,
|
"pt-BR": 96,
|
||||||
"ru": 97,
|
"ru": 96,
|
||||||
"sv-SE": 97,
|
"sv-SE": 96,
|
||||||
"uk": 97,
|
"uk": 96,
|
||||||
"ur": 2,
|
"ur": 2,
|
||||||
"zh-Hans": 97,
|
"zh-Hans": 96,
|
||||||
"zh-Hant": 97
|
"zh-Hant": 96
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -593,6 +593,18 @@ export function useAFFiNEI18N(): {
|
|||||||
* `current`
|
* `current`
|
||||||
*/
|
*/
|
||||||
current(): string;
|
current(): string;
|
||||||
|
/**
|
||||||
|
* `created at {{time}}`
|
||||||
|
*/
|
||||||
|
["created at"](options: {
|
||||||
|
readonly time: string;
|
||||||
|
}): string;
|
||||||
|
/**
|
||||||
|
* `last updated at {{time}}`
|
||||||
|
*/
|
||||||
|
["updated at"](options: {
|
||||||
|
readonly time: string;
|
||||||
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `Automatically check for new updates periodically.`
|
* `Automatically check for new updates periodically.`
|
||||||
*/
|
*/
|
||||||
@@ -6908,6 +6920,46 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Inactive workspace`
|
* `Inactive workspace`
|
||||||
*/
|
*/
|
||||||
["com.affine.inactive-workspace"](): string;
|
["com.affine.inactive-workspace"](): string;
|
||||||
|
/**
|
||||||
|
* `Display Properties`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.display.properties"](): string;
|
||||||
|
/**
|
||||||
|
* `List view options`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.display.list-view"](): string;
|
||||||
|
/**
|
||||||
|
* `Icon`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.display.list-view.icon"](): string;
|
||||||
|
/**
|
||||||
|
* `Body`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.display.list-view.body"](): string;
|
||||||
|
/**
|
||||||
|
* `Quick actions`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.quick-actions"](): string;
|
||||||
|
/**
|
||||||
|
* `Favorite`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.quick-action.favorite"](): string;
|
||||||
|
/**
|
||||||
|
* `Move to trash`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.quick-action.trash"](): string;
|
||||||
|
/**
|
||||||
|
* `Open in split view`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.quick-action.split"](): string;
|
||||||
|
/**
|
||||||
|
* `Open in new tab`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.quick-action.tab"](): string;
|
||||||
|
/**
|
||||||
|
* `Select checkbox`
|
||||||
|
*/
|
||||||
|
["com.affine.all-docs.quick-action.select"](): string;
|
||||||
/**
|
/**
|
||||||
* `core`
|
* `core`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -137,6 +137,8 @@
|
|||||||
"Deleted User": "Deleted User",
|
"Deleted User": "Deleted User",
|
||||||
"all": "all",
|
"all": "all",
|
||||||
"current": "current",
|
"current": "current",
|
||||||
|
"created at": "created at {{time}}",
|
||||||
|
"updated at": "last updated at {{time}}",
|
||||||
"com.affine.aboutAFFiNE.autoCheckUpdate.description": "Automatically check for new updates periodically.",
|
"com.affine.aboutAFFiNE.autoCheckUpdate.description": "Automatically check for new updates periodically.",
|
||||||
"com.affine.aboutAFFiNE.autoCheckUpdate.title": "Check for updates automatically",
|
"com.affine.aboutAFFiNE.autoCheckUpdate.title": "Check for updates automatically",
|
||||||
"com.affine.aboutAFFiNE.autoDownloadUpdate.description": "Automatically download updates (to this device).",
|
"com.affine.aboutAFFiNE.autoDownloadUpdate.description": "Automatically download updates (to this device).",
|
||||||
@@ -1725,6 +1727,16 @@
|
|||||||
"com.affine.inactive": "Inactive",
|
"com.affine.inactive": "Inactive",
|
||||||
"com.affine.inactive-member": "Inactive member",
|
"com.affine.inactive-member": "Inactive member",
|
||||||
"com.affine.inactive-workspace": "Inactive workspace",
|
"com.affine.inactive-workspace": "Inactive workspace",
|
||||||
|
"com.affine.all-docs.display.properties": "Display Properties",
|
||||||
|
"com.affine.all-docs.display.list-view": "List view options",
|
||||||
|
"com.affine.all-docs.display.list-view.icon": "Icon",
|
||||||
|
"com.affine.all-docs.display.list-view.body": "Body",
|
||||||
|
"com.affine.all-docs.quick-actions": "Quick actions",
|
||||||
|
"com.affine.all-docs.quick-action.favorite": "Favorite",
|
||||||
|
"com.affine.all-docs.quick-action.trash": "Move to trash",
|
||||||
|
"com.affine.all-docs.quick-action.split": "Open in split view",
|
||||||
|
"com.affine.all-docs.quick-action.tab": "Open in new tab",
|
||||||
|
"com.affine.all-docs.quick-action.select": "Select checkbox",
|
||||||
"core": "core",
|
"core": "core",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"invited you to join": "invited you to join",
|
"invited you to join": "invited you to join",
|
||||||
|
|||||||
Reference in New Issue
Block a user