mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
whats changed:
### orm
add a new `select$` method, can subscribe on only one field to improve batch subscribe performance
### yjs-observable
add a new `yjsObservePath` method, which can subscribe to changes from specific path in yjs. Improves batch subscribe performance
```ts
yjsGetPath(
this.workspaceService.workspace.rootYDoc.getMap('meta'),
'pages'
).pipe(
switchMap(pages => yjsObservePath(pages, '*.tags')),
map(pages => {
// only when tags changed
})
)
```
### standard property naming
All `DocProperty` components renamed to `WorkspaceProperty` which is consistent with the product definition.
### `WorkspacePropertyService`
Split the workspace property management logic from the `doc` module and create a new `WorkspacePropertyService`. The new service manages the creation and modification of properties, and the `docService` is only responsible for storing the property value data.
### new `<Filters />` component
in `core/component/filter`
### new `<ExplorerDisplayMenuButton />` component
in `core/component/explorer/display-menu`

### new `/workspace/xxx/all-new` route
New route for test components and functions
### new collection role service
Implemented some filter group order rules
see `collection-rules/index.ts`
### standard property type definition
define type in `modules\workspace-property\types.ts`

define components (name,icon,....) in `components\workspace-property-types\index.ts`

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
- **New Features**
- Introduced comprehensive filtering, grouping, and ordering capabilities for workspace documents with reactive updates.
- Added a new "All Pages" workspace view supporting dynamic filters and display preferences.
- Developed UI components for filter creation, condition editing, and display menu controls.
- Launched enhanced tag management with inline editors, selection, creation, and deletion workflows.
- Added workspace property types with dedicated filter UIs including checkbox, date, tags, and text.
- Introduced workspace property management replacing document property handling.
- Added modular providers for filters, group-by, and order-by operations supporting various property types and system attributes.
- **Improvements**
- Standardized tag and property naming conventions across the application (using `name` instead of `value` or `title`).
- Migrated document property handling to workspace property-centric logic.
- Enhanced internationalization with additional filter and display menu labels.
- Improved styling for filter conditions, display menus, and workspace pages.
- Optimized reactive data subscriptions and state management for performance.
- Refined schema typings and type safety for workspace properties.
- Updated imports and component references to workspace property equivalents throughout frontend.
- **Bug Fixes**
- Resolved tag property inconsistencies affecting display and filtering.
- Fixed filter and tag selection behaviors for accurate and reliable UI interactions.
- **Chores**
- Added and refined test cases for ORM, observables, and filtering logic.
- Cleaned up legacy document property code and improved type safety.
- Modularized and restructured components for better maintainability.
- Introduced new CSS styles for workspace pages and display menus.
- Added framework module configurations for collection rules and workspace property features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
202 lines
5.1 KiB
TypeScript
202 lines
5.1 KiB
TypeScript
import { distinctUntilChanged, Observable, of, switchMap } from 'rxjs';
|
|
import {
|
|
AbstractType as YAbstractType,
|
|
Array as YArray,
|
|
Map as YMap,
|
|
YArrayEvent,
|
|
type YEvent,
|
|
YMapEvent,
|
|
} from 'yjs';
|
|
|
|
/**
|
|
*
|
|
* @param path key.[0].key2.[1]
|
|
*/
|
|
function parsePath(path: string): (string | number)[] {
|
|
const parts = path.split('.');
|
|
return parts.map(part => {
|
|
if (part.startsWith('[') && part.endsWith(']')) {
|
|
const token = part.slice(1, -1);
|
|
if (token === '*') {
|
|
return '*';
|
|
}
|
|
const index = parseInt(token, 10);
|
|
if (isNaN(index)) {
|
|
throw new Error(`index: ${part} is not a number`);
|
|
}
|
|
return index;
|
|
}
|
|
return part;
|
|
});
|
|
}
|
|
|
|
function _yjsDeepWatch(
|
|
target: any,
|
|
path: ReturnType<typeof parsePath>
|
|
): Observable<unknown | undefined> {
|
|
if (path.length === 0) {
|
|
return of(target);
|
|
}
|
|
const current = path[0];
|
|
|
|
if (target instanceof YArray || target instanceof YMap) {
|
|
return new Observable(subscriber => {
|
|
const refresh = () => {
|
|
if (typeof current === 'number' && target instanceof YArray) {
|
|
subscriber.next(target.get(current));
|
|
} else if (typeof current === 'string' && target instanceof YMap) {
|
|
subscriber.next(target.get(current));
|
|
} else {
|
|
subscriber.next(undefined);
|
|
}
|
|
};
|
|
refresh();
|
|
target.observe(refresh);
|
|
return () => {
|
|
target.unobserve(refresh);
|
|
};
|
|
}).pipe(
|
|
distinctUntilChanged(),
|
|
switchMap(arr => _yjsDeepWatch(arr, path.slice(1)))
|
|
);
|
|
} else {
|
|
return of(undefined);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* extract data from yjs type based on path, and return an observable.
|
|
* observable will automatically update when yjs data changed.
|
|
* if data is not exist on path, the observable will emit undefined.
|
|
*
|
|
* this function is optimized for deep watch performance.
|
|
*
|
|
* @example
|
|
* yjsGetPath(yjs, 'pages.[0].id') -> get pages[0].id and emit when changed
|
|
* yjsGetPath(yjs, 'pages.[0]').switchMap(yjsObserve) -> get pages[0] and emit when any of pages[0] or its children changed
|
|
* yjsGetPath(yjs, 'pages.[0]').switchMap(yjsObserveDeep) -> get pages[0] and emit when pages[0] or any of its deep children changed
|
|
*/
|
|
export function yjsGetPath(yjs: YAbstractType<any>, path: string) {
|
|
const parsedPath = parsePath(path);
|
|
return _yjsDeepWatch(yjs, parsedPath);
|
|
}
|
|
|
|
/**
|
|
* convert yjs type to observable.
|
|
* observable will automatically update when yjs data changed.
|
|
*
|
|
* @example
|
|
* yjsObserveDeep(yjs) -> emit when any of its deep children changed
|
|
*/
|
|
export function yjsObserveDeep(yjs?: any) {
|
|
return new Observable(subscriber => {
|
|
const refresh = () => {
|
|
subscriber.next(yjs);
|
|
};
|
|
refresh();
|
|
if (yjs instanceof YAbstractType) {
|
|
yjs.observeDeep(refresh);
|
|
return () => {
|
|
yjs.unobserveDeep(refresh);
|
|
};
|
|
}
|
|
return;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* convert yjs type to observable.
|
|
* observable will automatically update when data changed on the path.
|
|
*
|
|
* @example
|
|
* yjsObservePath(yjs, 'pages.[0].id') -> only emit when pages[0].id changed
|
|
* yjsObservePath(yjs, 'pages.*.tags') -> only emit when tags of any page changed
|
|
*/
|
|
export function yjsObservePath(yjs?: any, path?: string) {
|
|
const parsedPath = path ? parsePath(path) : [];
|
|
|
|
return new Observable(subscriber => {
|
|
const refresh = (event?: YEvent<any>[]) => {
|
|
if (!event) {
|
|
subscriber.next(yjs);
|
|
return;
|
|
}
|
|
|
|
const changedPaths: (string | number)[][] = [];
|
|
event.forEach(e => {
|
|
if (e instanceof YArrayEvent) {
|
|
changedPaths.push(e.path);
|
|
} else if (e instanceof YMapEvent) {
|
|
for (const key of e.keysChanged) {
|
|
changedPaths.push([...e.path, key]);
|
|
}
|
|
}
|
|
});
|
|
|
|
for (const changedPath of changedPaths) {
|
|
let changed = true;
|
|
for (let i = 0; i < parsedPath.length; i++) {
|
|
const changedKey = changedPath[i];
|
|
const parsedKey = parsedPath[i];
|
|
if (changedKey === undefined) {
|
|
changed = true;
|
|
break;
|
|
}
|
|
|
|
if (parsedKey === undefined) {
|
|
changed = true;
|
|
break;
|
|
}
|
|
|
|
if (changedKey === parsedKey) {
|
|
continue;
|
|
}
|
|
|
|
if (parsedKey === '*') {
|
|
continue;
|
|
}
|
|
|
|
changed = false;
|
|
break;
|
|
}
|
|
|
|
if (changed) {
|
|
subscriber.next(yjs);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
refresh();
|
|
if (yjs instanceof YAbstractType) {
|
|
yjs.observeDeep(refresh);
|
|
return () => {
|
|
yjs.unobserveDeep(refresh);
|
|
};
|
|
}
|
|
return;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* convert yjs type to observable.
|
|
* observable will automatically update when yjs data changed.
|
|
*
|
|
* @example
|
|
* yjsObserveDeep(yjs) -> emit when any of children changed
|
|
*/
|
|
export function yjsObserve(yjs?: any) {
|
|
return new Observable(subscriber => {
|
|
const refresh = () => {
|
|
subscriber.next(yjs);
|
|
};
|
|
refresh();
|
|
if (yjs instanceof YAbstractType) {
|
|
yjs.observe(refresh);
|
|
return () => {
|
|
yjs.unobserve(refresh);
|
|
};
|
|
}
|
|
return;
|
|
});
|
|
}
|