mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
feat(core): add collection rules module (#11683)
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 -->
This commit is contained in:
@@ -102,6 +102,102 @@ describe('ORM entity CRUD', () => {
|
||||
expect(user2).toEqual(user);
|
||||
});
|
||||
|
||||
test('should be able to select', t => {
|
||||
const { client } = t;
|
||||
|
||||
client.users.create({
|
||||
name: 'u1',
|
||||
email: 'e1@example.com',
|
||||
});
|
||||
|
||||
client.users.create({
|
||||
name: 'u2',
|
||||
});
|
||||
|
||||
const users = client.users.select('name');
|
||||
|
||||
expect(users).toStrictEqual([
|
||||
{ id: expect.any(Number), name: 'u1' },
|
||||
{ id: expect.any(Number), name: 'u2' },
|
||||
]);
|
||||
|
||||
const user2 = client.users.select('email');
|
||||
|
||||
expect(user2).toStrictEqual([
|
||||
{ id: expect.any(Number), email: 'e1@example.com' },
|
||||
{ id: expect.any(Number), email: undefined },
|
||||
]);
|
||||
|
||||
const user3 = client.users.select('name', {
|
||||
email: null,
|
||||
});
|
||||
|
||||
expect(user3).toStrictEqual([{ id: expect.any(Number), name: 'u2' }]);
|
||||
});
|
||||
|
||||
test('should be able to observe select', t => {
|
||||
const { client } = t;
|
||||
|
||||
const t1 = client.tags.create({
|
||||
name: 't1',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
const t2 = client.tags.create({
|
||||
name: 't2',
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
let currentValue: any;
|
||||
let callbackCount = 0;
|
||||
|
||||
client.tags.select$('name', { color: 'red' }).subscribe(data => {
|
||||
currentValue = data;
|
||||
callbackCount++;
|
||||
});
|
||||
|
||||
expect(currentValue).toStrictEqual([
|
||||
{ id: expect.any(String), name: 't1' },
|
||||
]);
|
||||
expect(callbackCount).toBe(1);
|
||||
|
||||
const t3 = client.tags.create({
|
||||
name: 't3',
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
expect(currentValue).toStrictEqual([
|
||||
{ id: expect.any(String), name: 't1' },
|
||||
]);
|
||||
expect(callbackCount).toBe(1);
|
||||
|
||||
client.tags.update(t1.id, {
|
||||
name: 't1-updated',
|
||||
});
|
||||
expect(currentValue).toStrictEqual([
|
||||
{ id: expect.any(String), name: 't1-updated' },
|
||||
]);
|
||||
expect(callbackCount).toBe(2);
|
||||
|
||||
client.tags.update(t2.id, {
|
||||
color: 'red',
|
||||
});
|
||||
expect(currentValue).toStrictEqual([
|
||||
{ id: expect.any(String), name: 't1-updated' },
|
||||
{ id: expect.any(String), name: 't2' },
|
||||
]);
|
||||
expect(callbackCount).toBe(3);
|
||||
|
||||
client.tags.delete(t1.id);
|
||||
expect(currentValue).toStrictEqual([
|
||||
{ id: expect.any(String), name: 't2' },
|
||||
]);
|
||||
expect(callbackCount).toBe(4);
|
||||
|
||||
client.tags.delete(t3.id);
|
||||
expect(callbackCount).toBe(4);
|
||||
});
|
||||
|
||||
test('should be able to filter with nullable condition', t => {
|
||||
const { client } = t;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type Transaction,
|
||||
} from 'yjs';
|
||||
|
||||
import { shallowEqual } from '../../../../utils/shallow-equal';
|
||||
import { validators } from '../../validators';
|
||||
import { HookAdapter } from '../mixins';
|
||||
import type {
|
||||
@@ -133,7 +134,16 @@ export class YjsTableAdapter implements TableAdapter {
|
||||
|
||||
if (isMatch && isPrevMatched) {
|
||||
const newValue = this.value(record, select);
|
||||
if (prevMatch !== newValue) {
|
||||
if (
|
||||
!(
|
||||
prevMatch === newValue ||
|
||||
(!select && // if select is set, we will check the value
|
||||
select !== '*' &&
|
||||
select !== 'key' &&
|
||||
// skip if the value is the same
|
||||
shallowEqual(prevMatch, newValue))
|
||||
)
|
||||
) {
|
||||
results.set(key, newValue);
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
@@ -273,6 +273,62 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
});
|
||||
}
|
||||
|
||||
select<Key extends keyof Entity<T>>(
|
||||
selectKey: Key,
|
||||
where?: FindEntityInput<T>
|
||||
): Pick<Entity<T>, Key | PrimaryKeyField<T>>[] {
|
||||
const items = this.adapter.find({
|
||||
where: !where
|
||||
? undefined
|
||||
: Object.entries(where)
|
||||
.map(([field, value]) => ({
|
||||
field,
|
||||
value,
|
||||
}))
|
||||
.filter(({ value }) => value !== undefined),
|
||||
});
|
||||
|
||||
return items.map(item => {
|
||||
const { [this.keyField]: key, [selectKey]: selected } = item;
|
||||
return {
|
||||
[this.keyField]: key,
|
||||
[selectKey]: selected,
|
||||
} as Pick<Entity<T>, Key | PrimaryKeyField<T>>;
|
||||
});
|
||||
}
|
||||
|
||||
select$<Key extends keyof Entity<T>>(
|
||||
selectKey: Key,
|
||||
where?: FindEntityInput<T>
|
||||
): Observable<Pick<Entity<T>, Key | PrimaryKeyField<T>>[]> {
|
||||
return new Observable(subscriber => {
|
||||
const unsubscribe = this.adapter.observe({
|
||||
where: !where
|
||||
? undefined
|
||||
: Object.entries(where)
|
||||
.map(([field, value]) => ({
|
||||
field,
|
||||
value,
|
||||
}))
|
||||
.filter(({ value }) => value !== undefined),
|
||||
select: [this.keyField, selectKey as string],
|
||||
callback: data => {
|
||||
subscriber.next(
|
||||
data.map(item => {
|
||||
const { [this.keyField]: key, [selectKey]: selected } = item;
|
||||
return {
|
||||
[this.keyField]: key,
|
||||
[selectKey]: selected,
|
||||
} as Pick<Entity<T>, Key | PrimaryKeyField<T>>;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
}
|
||||
|
||||
keys(): PrimaryKeyFieldType<T>[] {
|
||||
return this.adapter.find({
|
||||
select: 'key',
|
||||
|
||||
Reference in New Issue
Block a user