mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
refactor(core): use fractional indexing for sorting (#5809)
use https://github.com/rocicorp/fractional-indexing to enable better sorting logic for crdt app
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
} from '@affine/core/modules/workspace/properties/schema';
|
||||
import { PagePropertyType } from '@affine/core/modules/workspace/properties/schema';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { generateKeyBetween } from 'fractional-indexing';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { getDefaultIconName } from './icons-mapping';
|
||||
@@ -216,16 +217,13 @@ export class PagePropertiesManager {
|
||||
}
|
||||
|
||||
getOrderedCustomProperties() {
|
||||
return Object.values(this.getCustomProperties()).sort(
|
||||
(a, b) => a.order - b.order
|
||||
return Object.values(this.getCustomProperties()).sort((a, b) =>
|
||||
a.order > b.order ? 1 : a.order < b.order ? -1 : 0
|
||||
);
|
||||
}
|
||||
|
||||
largestOrder() {
|
||||
return Math.max(
|
||||
...Object.values(this.properties.custom).map(p => p.order),
|
||||
0
|
||||
);
|
||||
return this.getOrderedCustomProperties().at(-1)?.order ?? null;
|
||||
}
|
||||
|
||||
getCustomPropertyMeta(id: string): PageInfoCustomPropertyMeta | undefined {
|
||||
@@ -247,7 +245,7 @@ export class PagePropertiesManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOrder = this.largestOrder() + 1;
|
||||
const newOrder = generateKeyBetween(this.largestOrder(), null);
|
||||
if (this.properties.custom[id]) {
|
||||
logger.warn(`custom property ${id} already exists`);
|
||||
}
|
||||
@@ -260,6 +258,20 @@ export class PagePropertiesManager {
|
||||
};
|
||||
}
|
||||
|
||||
moveCustomProperty(from: number, to: number) {
|
||||
// move from -> to means change from's order to a new order between to and to -1/+1
|
||||
const properties = this.getOrderedCustomProperties();
|
||||
const fromProperty = properties[from];
|
||||
const toProperty = properties[to];
|
||||
const toNextProperty = properties[from < to ? to + 1 : to - 1];
|
||||
const args: [string?, string?] =
|
||||
from < to
|
||||
? [toProperty.order, toNextProperty?.order ?? null]
|
||||
: [toNextProperty?.order ?? null, toProperty.order];
|
||||
const newOrder = generateKeyBetween(...args);
|
||||
this.properties.custom[fromProperty.id].order = newOrder;
|
||||
}
|
||||
|
||||
hasCustomProperty(id: string) {
|
||||
return !!this.properties.custom[id];
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
restrictToParentElement,
|
||||
restrictToVerticalAxis,
|
||||
} from '@dnd-kit/modifiers';
|
||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import clsx from 'clsx';
|
||||
import { use } from 'foxact/use';
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -95,7 +96,12 @@ type PropertyVisibility = PageInfoCustomProperty['visibility'];
|
||||
const editingPropertyAtom = atom<string | null>(null);
|
||||
|
||||
const modifiers = [restrictToParentElement, restrictToVerticalAxis];
|
||||
const SortableProperties = ({ children }: PropsWithChildren) => {
|
||||
|
||||
interface SortablePropertiesProps {
|
||||
children: (properties: PageInfoCustomProperty[]) => React.ReactNode;
|
||||
}
|
||||
|
||||
const SortableProperties = ({ children }: SortablePropertiesProps) => {
|
||||
const manager = useContext(managerContext);
|
||||
const properties = useMemo(
|
||||
() => manager.getOrderedCustomProperties(),
|
||||
@@ -110,6 +116,14 @@ const SortableProperties = ({ children }: PropsWithChildren) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
// use localProperties since changes applied to upstream may be delayed
|
||||
// if we use that one, there will be weird behavior after reordering
|
||||
const [localProperties, setLocalProperties] = useState(properties);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalProperties(properties);
|
||||
}, [properties]);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
if (!draggable) {
|
||||
@@ -120,22 +134,22 @@ const SortableProperties = ({ children }: PropsWithChildren) => {
|
||||
const toIndex = properties.findIndex(p => p.id === over?.id);
|
||||
|
||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||
const newOrdered = arrayMove(properties, fromIndex, toIndex);
|
||||
manager.transact(() => {
|
||||
newOrdered.forEach((p, i) => {
|
||||
manager.updateCustomProperty(p.id, {
|
||||
order: i,
|
||||
});
|
||||
});
|
||||
});
|
||||
manager.moveCustomProperty(fromIndex, toIndex);
|
||||
setLocalProperties(manager.getOrderedCustomProperties());
|
||||
}
|
||||
},
|
||||
[manager, properties, draggable]
|
||||
);
|
||||
|
||||
const filteredProperties = useMemo(
|
||||
() => localProperties.filter(p => manager.getCustomPropertyMeta(p.id)),
|
||||
[localProperties, manager]
|
||||
);
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} onDragEnd={onDragEnd} modifiers={modifiers}>
|
||||
<SortableContext disabled={!draggable} items={properties}>
|
||||
{children}
|
||||
{children(filteredProperties)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
@@ -167,6 +181,7 @@ const SortablePropertyRow = ({
|
||||
transition,
|
||||
active,
|
||||
isDragging,
|
||||
isSorting,
|
||||
} = useSortable({
|
||||
id: property.id,
|
||||
});
|
||||
@@ -175,10 +190,10 @@ const SortablePropertyRow = ({
|
||||
transform: transform
|
||||
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
|
||||
: undefined,
|
||||
transition,
|
||||
transition: isSorting ? transition : undefined,
|
||||
pointerEvents: manager.readonly ? 'none' : undefined,
|
||||
}),
|
||||
[manager.readonly, transform, transition]
|
||||
[isSorting, manager.readonly, transform, transition]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -305,7 +320,6 @@ export const PagePropertiesSettingsPopup = ({
|
||||
}: PagePropertiesSettingsPopupProps) => {
|
||||
const manager = useContext(managerContext);
|
||||
const t = useAFFiNEI18N();
|
||||
const properties = manager.getOrderedCustomProperties();
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const options: MenuItemOption[] = [];
|
||||
@@ -321,35 +335,37 @@ export const PagePropertiesSettingsPopup = ({
|
||||
options.push('-');
|
||||
options.push([
|
||||
<SortableProperties key="sortable-settings">
|
||||
{properties.map(property => {
|
||||
const meta = manager.getCustomPropertyMeta(property.id);
|
||||
assertExists(meta, 'meta should exist for property');
|
||||
const Icon = nameToIcon(meta.icon, meta.type);
|
||||
const name = meta.name;
|
||||
return (
|
||||
<SortablePropertyRow
|
||||
key={meta.id}
|
||||
property={property}
|
||||
className={styles.propertySettingRow}
|
||||
data-testid="page-properties-settings-menu-item"
|
||||
>
|
||||
<MenuIcon>
|
||||
<Icon />
|
||||
</MenuIcon>
|
||||
<div
|
||||
data-testid="page-property-setting-row-name"
|
||||
className={styles.propertyRowName}
|
||||
{properties =>
|
||||
properties.map(property => {
|
||||
const meta = manager.getCustomPropertyMeta(property.id);
|
||||
assertExists(meta, 'meta should exist for property');
|
||||
const Icon = nameToIcon(meta.icon, meta.type);
|
||||
const name = meta.name;
|
||||
return (
|
||||
<SortablePropertyRow
|
||||
key={meta.id}
|
||||
property={property}
|
||||
className={styles.propertySettingRow}
|
||||
data-testid="page-properties-settings-menu-item"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<VisibilityModeSelector property={property} />
|
||||
</SortablePropertyRow>
|
||||
);
|
||||
})}
|
||||
<MenuIcon>
|
||||
<Icon />
|
||||
</MenuIcon>
|
||||
<div
|
||||
data-testid="page-property-setting-row-name"
|
||||
className={styles.propertyRowName}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<VisibilityModeSelector property={property} />
|
||||
</SortablePropertyRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</SortableProperties>,
|
||||
]);
|
||||
return renderMenuItemOptions(options);
|
||||
}, [manager, properties, t]);
|
||||
}, [manager, t]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -423,6 +439,14 @@ export const PagePropertyRowNameMenu = ({
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalPropertyMeta(meta);
|
||||
}, [meta]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalProperty(property);
|
||||
}, [property]);
|
||||
|
||||
const handleFinishEditing = useCallback(() => {
|
||||
onFinishEditing();
|
||||
manager.updateCustomPropertyMeta(meta.id, localPropertyMeta);
|
||||
@@ -752,10 +776,6 @@ export const PagePropertiesTableBody = ({
|
||||
style,
|
||||
}: PagePropertiesTableBodyProps) => {
|
||||
const manager = useContext(managerContext);
|
||||
|
||||
const properties = useMemo(() => {
|
||||
return manager.getOrderedCustomProperties();
|
||||
}, [manager]);
|
||||
return (
|
||||
<Collapsible.Content
|
||||
className={clsx(styles.tableBodyRoot, className)}
|
||||
@@ -764,16 +784,20 @@ export const PagePropertiesTableBody = ({
|
||||
<PageTagsRow />
|
||||
<div className={styles.tableBodySortable}>
|
||||
<SortableProperties>
|
||||
{properties
|
||||
.filter(
|
||||
property =>
|
||||
manager.isPropertyRequired(property.id) ||
|
||||
(property.visibility !== 'hide' &&
|
||||
!(property.visibility === 'hide-if-empty' && !property.value))
|
||||
)
|
||||
.map(property => (
|
||||
<PagePropertyRow key={property.id} property={property} />
|
||||
))}
|
||||
{properties =>
|
||||
properties
|
||||
.filter(
|
||||
property =>
|
||||
manager.isPropertyRequired(property.id) ||
|
||||
(property.visibility !== 'hide' &&
|
||||
!(
|
||||
property.visibility === 'hide-if-empty' && !property.value
|
||||
))
|
||||
)
|
||||
.map(property => (
|
||||
<PagePropertyRow key={property.id} property={property} />
|
||||
))
|
||||
}
|
||||
</SortableProperties>
|
||||
</div>
|
||||
{manager.readonly ? null : <PagePropertiesAddProperty />}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const inlineTagsContainer = style({
|
||||
export const tagsMenu = style({
|
||||
padding: 0,
|
||||
transform:
|
||||
'translate(-3px, calc(-3px + var(--radix-popper-anchor-height) * -1))',
|
||||
'translate(-3.5px, calc(-3.5px + var(--radix-popper-anchor-height) * -1))',
|
||||
width: 'calc(var(--radix-popper-anchor-width) + 16px)',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { use } from 'foxact/use';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useDebouncedState } from 'foxact/use-debounced-state';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
|
||||
import { useAllBlockSuitePageMeta } from './use-all-block-suite-page-meta';
|
||||
|
||||
function getProxy<T extends object>(obj: T) {
|
||||
return new Proxy(obj, {});
|
||||
}
|
||||
|
||||
const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
|
||||
use(adapter.workspace.blockSuiteWorkspace.doc.whenSynced);
|
||||
const [proxy, setProxy] = useState(adapter);
|
||||
// fixme: this is a hack to force re-render when default meta changed
|
||||
useAllBlockSuitePageMeta(adapter.workspace.blockSuiteWorkspace);
|
||||
// hack: delay proxy creation to avoid unnecessary re-render + render in another component issue
|
||||
const [proxy, setProxy] = useDebouncedState(adapter, 0);
|
||||
useEffect(() => {
|
||||
// todo: track which properties are used and then filter by property path change
|
||||
// using Y.YEvent.path
|
||||
function observe() {
|
||||
requestAnimationFrame(() => {
|
||||
setProxy(getProxy(adapter));
|
||||
});
|
||||
setProxy(getProxy(adapter));
|
||||
}
|
||||
const disposables: (() => void)[] = [];
|
||||
disposables.push(
|
||||
adapter.workspace.blockSuiteWorkspace.meta.pageMetasUpdated.on(observe)
|
||||
.dispose
|
||||
);
|
||||
adapter.properties.observeDeep(observe);
|
||||
disposables.push(() => adapter.properties.unobserveDeep(observe));
|
||||
return () => {
|
||||
adapter.properties.unobserveDeep(observe);
|
||||
for (const dispose of disposables) {
|
||||
dispose();
|
||||
}
|
||||
};
|
||||
}, [adapter]);
|
||||
}, [adapter, setProxy]);
|
||||
|
||||
return proxy;
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const WorkspaceAffinePropertiesSchemaSchema = z.object({
|
||||
});
|
||||
|
||||
const PageInfoCustomPropertyItemSchema = PageInfoItemSchema.extend({
|
||||
order: z.number(),
|
||||
order: z.string(),
|
||||
});
|
||||
|
||||
const WorkspacePagePropertiesSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user