mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +08:00
refactor(core): refactor collection to use new filter system (#12228)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new Collection entity and store with reactive management and real-time updates. - Added reactive favorite and shared filters with expanded filtering options. - **Refactor** - Overhauled collection and filtering logic for better performance and maintainability. - Replaced legacy filtering UI and logic with a streamlined, service-driven rules system. - Updated collection components to use reactive data streams and simplified props. - Simplified collection creation by delegating ID generation and instantiation to the service layer. - Removed deprecated hooks and replaced state-based filtering with observable-driven filtering. - **Bug Fixes** - Improved accuracy and consistency of tag and favorite filtering in collections. - **Chores** - Removed deprecated and unused filter-related files, types, components, and styles to reduce complexity. - Cleaned up imports and removed unused code across multiple components. - **Documentation** - Corrected inline documentation for improved clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
17
packages/common/env/src/filter.ts
vendored
17
packages/common/env/src/filter.ts
vendored
@@ -55,23 +55,6 @@ export const collectionSchema = z.object({
|
|||||||
createDate: z.union([z.date(), z.number()]).optional(),
|
createDate: z.union([z.date(), z.number()]).optional(),
|
||||||
updateDate: z.union([z.date(), z.number()]).optional(),
|
updateDate: z.union([z.date(), z.number()]).optional(),
|
||||||
});
|
});
|
||||||
export const deletedCollectionSchema = z.object({
|
|
||||||
userId: z.string().optional(),
|
|
||||||
userName: z.string(),
|
|
||||||
collection: collectionSchema,
|
|
||||||
});
|
|
||||||
export type DeprecatedCollection = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
workspaceId: string;
|
|
||||||
filterList: z.infer<typeof filterSchema>[];
|
|
||||||
allowList?: string[];
|
|
||||||
};
|
|
||||||
export type Collection = z.input<typeof collectionSchema>;
|
export type Collection = z.input<typeof collectionSchema>;
|
||||||
export type DeleteCollectionInfo = {
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
} | null;
|
|
||||||
export type DeletedCollection = z.input<typeof deletedCollectionSchema>;
|
|
||||||
|
|
||||||
export type PropertiesMeta = DocsPropertiesMeta;
|
export type PropertiesMeta = DocsPropertiesMeta;
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export function yjsObservePath(yjs?: any, path?: string) {
|
|||||||
* observable will automatically update when yjs data changed.
|
* observable will automatically update when yjs data changed.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* yjsObserveDeep(yjs) -> emit when any of children changed
|
* yjsObserve(yjs) -> emit when yjs type changed
|
||||||
*/
|
*/
|
||||||
export function yjsObserve(yjs?: any) {
|
export function yjsObserve(yjs?: any) {
|
||||||
return new Observable(subscriber => {
|
return new Observable(subscriber => {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import type {
|
import type { TagMeta } from '@affine/core/components/page-list';
|
||||||
CollectionMeta,
|
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||||
TagMeta,
|
|
||||||
} from '@affine/core/components/page-list';
|
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { TagMeta } from '@affine/core/components/page-list';
|
import type { TagMeta } from '@affine/core/components/page-list';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { createLitPortal } from '@blocksuite/affine/components/portal';
|
import { createLitPortal } from '@blocksuite/affine/components/portal';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
@@ -128,7 +127,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
|
|
||||||
private _tags: Signal<TagMeta[]> = signal([]);
|
private _tags: Signal<TagMeta[]> = signal([]);
|
||||||
|
|
||||||
private _collections: Signal<Collection[]> = signal([]);
|
private _collections: Signal<{ id: string; name: string }[]> = signal([]);
|
||||||
|
|
||||||
private _cleanup: (() => void) | null = null;
|
private _cleanup: (() => void) | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
||||||
@@ -18,7 +17,7 @@ export class ChatPanelCollectionChip extends SignalWatcher(
|
|||||||
accessor removeChip!: (chip: CollectionChip) => void;
|
accessor removeChip!: (chip: CollectionChip) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor collection!: Collection;
|
accessor collection!: { id: string; name: string };
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const { state } = this.chip;
|
const { state } = this.chip;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type {
|
|||||||
SearchDocMenuAction,
|
SearchDocMenuAction,
|
||||||
SearchTagMenuAction,
|
SearchTagMenuAction,
|
||||||
} from '@affine/core/modules/search-menu/services';
|
} from '@affine/core/modules/search-menu/services';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import type { DocMeta, Store } from '@blocksuite/affine/store';
|
import type { DocMeta, Store } from '@blocksuite/affine/store';
|
||||||
import type { LinkedMenuGroup } from '@blocksuite/affine/widgets/linked-doc';
|
import type { LinkedMenuGroup } from '@blocksuite/affine/widgets/linked-doc';
|
||||||
import type { Signal } from '@preact/signals-core';
|
import type { Signal } from '@preact/signals-core';
|
||||||
@@ -71,7 +70,7 @@ export interface DocDisplayConfig {
|
|||||||
getTagTitle: (tagId: string) => string;
|
getTagTitle: (tagId: string) => string;
|
||||||
getTagPageIds: (tagId: string) => string[];
|
getTagPageIds: (tagId: string) => string[];
|
||||||
getCollections: () => {
|
getCollections: () => {
|
||||||
signal: Signal<Collection[]>;
|
signal: Signal<{ id: string; name: string }[]>;
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
};
|
};
|
||||||
getCollectionPageIds: (collectionId: string) => string[];
|
getCollectionPageIds: (collectionId: string) => string[];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { Collection } from '@affine/core/modules/collection';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { AllDocsIcon, FilterIcon } from '@blocksuite/icons/rc';
|
import { AllDocsIcon, FilterIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import { WorkspaceService } from '@affine/core/modules/workspace';
|
|||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { createEmptyCollection } from '../../page-list';
|
|
||||||
import { ActionButton } from './action-button';
|
import { ActionButton } from './action-button';
|
||||||
import collectionListDark from './assets/collection-list.dark.png';
|
import collectionListDark from './assets/collection-list.dark.png';
|
||||||
import collectionListLight from './assets/collection-list.light.png';
|
import collectionListLight from './assets/collection-list.light.png';
|
||||||
@@ -39,8 +37,7 @@ export const EmptyCollections = (props: UniversalEmptyProps) => {
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
const id = nanoid();
|
const id = collectionService.createCollection({ name });
|
||||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
|
||||||
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { IconButton, Menu, MenuItem, MenuSeparator } from '@affine/component';
|
|||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
import { WorkspacePropertyIcon, WorkspacePropertyName } from '../properties';
|
import { WorkspacePropertyIcon, WorkspacePropertyName } from '../properties';
|
||||||
@@ -24,6 +24,36 @@ export const AddFilterMenu = ({
|
|||||||
{t['com.affine.filter']()}
|
{t['com.affine.filter']()}
|
||||||
</div>
|
</div>
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<FavoriteIcon className={styles.filterTypeItemIcon} />}
|
||||||
|
key={'favorite'}
|
||||||
|
onClick={() => {
|
||||||
|
onAdd({
|
||||||
|
type: 'system',
|
||||||
|
key: 'favorite',
|
||||||
|
method: 'is',
|
||||||
|
value: 'true',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.filterTypeItemName}>{t['Favorited']()}</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<FavoriteIcon className={styles.filterTypeItemIcon} />}
|
||||||
|
key={'shared'}
|
||||||
|
onClick={() => {
|
||||||
|
onAdd({
|
||||||
|
type: 'system',
|
||||||
|
key: 'shared',
|
||||||
|
method: 'is',
|
||||||
|
value: 'true',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.filterTypeItemName}>
|
||||||
|
{t['com.affine.filter.is-public']()}
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
{workspaceProperties.map(property => {
|
{workspaceProperties.map(property => {
|
||||||
const type = WorkspacePropertyTypes[property.type];
|
const type = WorkspacePropertyTypes[property.type];
|
||||||
const defaultFilter = type?.defaultFilter;
|
const defaultFilter = type?.defaultFilter;
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ export function useAIChatConfig() {
|
|||||||
return tag$.value?.pageIds$.value ?? [];
|
return tag$.value?.pageIds$.value ?? [];
|
||||||
},
|
},
|
||||||
getCollections: () => {
|
getCollections: () => {
|
||||||
const collections$ = collectionService.collections$;
|
const collectionMetas$ = collectionService.collectionMetas$;
|
||||||
return createSignalFromObservable(collections$, []);
|
return createSignalFromObservable(collectionMetas$, []);
|
||||||
},
|
},
|
||||||
getCollectionPageIds: (collectionId: string) => {
|
getCollectionPageIds: (collectionId: string) => {
|
||||||
const collection$ = collectionService.collection$(collectionId);
|
const collection$ = collectionService.collection$(collectionId);
|
||||||
// TODO: lack of documents that meet the collection rules
|
// TODO: lack of documents that meet the collection rules
|
||||||
return collection$?.value?.allowList ?? [];
|
return collection$?.value?.info$.value.allowList ?? [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { AuthService } from '@affine/core/modules/cloud';
|
|
||||||
import type { DeleteCollectionInfo } from '@affine/env/filter';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export const useDeleteCollectionInfo = () => {
|
|
||||||
const authService = useService(AuthService);
|
|
||||||
|
|
||||||
const user = useLiveData(authService.session.account$);
|
|
||||||
|
|
||||||
return useMemo<DeleteCollectionInfo | null>(
|
|
||||||
() => (user ? { userName: user.label, userId: user.id } : null),
|
|
||||||
[user]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import 'fake-indexeddb/auto';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Filter,
|
|
||||||
LiteralValue,
|
|
||||||
PropertiesMeta,
|
|
||||||
Ref,
|
|
||||||
VariableMap,
|
|
||||||
} from '@affine/env/filter';
|
|
||||||
import { getOrCreateI18n, I18nextProvider } from '@affine/i18n';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import { Condition } from '../filter/condition';
|
|
||||||
import { tBoolean, tDate } from '../filter/logical/custom-type';
|
|
||||||
import { toLiteral } from '../filter/shared-types';
|
|
||||||
import type { FilterMatcherDataType } from '../filter/vars';
|
|
||||||
import { filterMatcher } from '../filter/vars';
|
|
||||||
import { filterByFilterList } from '../use-collection-manager';
|
|
||||||
const ref = (name: keyof VariableMap): Ref => {
|
|
||||||
return {
|
|
||||||
type: 'ref',
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const mockVariableMap = (vars: Partial<VariableMap>): VariableMap => {
|
|
||||||
return {
|
|
||||||
Created: 0,
|
|
||||||
Updated: 0,
|
|
||||||
'Is Favourited': false,
|
|
||||||
'Is Public': false,
|
|
||||||
Tags: [],
|
|
||||||
...vars,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const mockPropertiesMeta = (meta: Partial<PropertiesMeta>): PropertiesMeta => {
|
|
||||||
return {
|
|
||||||
tags: {
|
|
||||||
options: [],
|
|
||||||
},
|
|
||||||
...meta,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const filter = (
|
|
||||||
matcherData: FilterMatcherDataType,
|
|
||||||
left: Ref,
|
|
||||||
args: LiteralValue[]
|
|
||||||
): Filter => {
|
|
||||||
return {
|
|
||||||
type: 'filter',
|
|
||||||
left,
|
|
||||||
funcName: matcherData.name,
|
|
||||||
args: args.map(toLiteral),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
describe('match filter', () => {
|
|
||||||
test('boolean variable will match `is` filter', () => {
|
|
||||||
const is = filterMatcher
|
|
||||||
.allMatchedData(tBoolean.create())
|
|
||||||
.find(v => v.name === 'is');
|
|
||||||
expect(is?.name).toBe('is');
|
|
||||||
});
|
|
||||||
test('Date variable will match `before` filter', () => {
|
|
||||||
const before = filterMatcher
|
|
||||||
.allMatchedData(tDate.create())
|
|
||||||
.find(v => v.name === 'before');
|
|
||||||
expect(before?.name).toBe('before');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('eval filter', () => {
|
|
||||||
test('before', async () => {
|
|
||||||
const before = filterMatcher.findData(v => v.name === 'before');
|
|
||||||
if (!before) {
|
|
||||||
throw new Error('before is not found');
|
|
||||||
}
|
|
||||||
const filter1 = filter(before, ref('Created'), [
|
|
||||||
new Date(2023, 5, 28).getTime(),
|
|
||||||
]);
|
|
||||||
const filter2 = filter(before, ref('Created'), [
|
|
||||||
new Date(2023, 5, 30).getTime(),
|
|
||||||
]);
|
|
||||||
const filter3 = filter(before, ref('Created'), [
|
|
||||||
new Date(2023, 5, 29).getTime(),
|
|
||||||
]);
|
|
||||||
const varMap = mockVariableMap({
|
|
||||||
Created: new Date(2023, 5, 29).getTime(),
|
|
||||||
});
|
|
||||||
expect(filterByFilterList([filter1], varMap)).toBe(false);
|
|
||||||
expect(filterByFilterList([filter2], varMap)).toBe(true);
|
|
||||||
expect(filterByFilterList([filter3], varMap)).toBe(false);
|
|
||||||
});
|
|
||||||
test('after', async () => {
|
|
||||||
const after = filterMatcher.findData(v => v.name === 'after');
|
|
||||||
if (!after) {
|
|
||||||
throw new Error('after is not found');
|
|
||||||
}
|
|
||||||
const filter1 = filter(after, ref('Created'), [
|
|
||||||
new Date(2023, 5, 28).getTime(),
|
|
||||||
]);
|
|
||||||
const filter2 = filter(after, ref('Created'), [
|
|
||||||
new Date(2023, 5, 30).getTime(),
|
|
||||||
]);
|
|
||||||
const filter3 = filter(after, ref('Created'), [
|
|
||||||
new Date(2023, 5, 29).getTime(),
|
|
||||||
]);
|
|
||||||
const varMap = mockVariableMap({
|
|
||||||
Created: new Date(2023, 5, 29).getTime(),
|
|
||||||
});
|
|
||||||
expect(filterByFilterList([filter1], varMap)).toBe(true);
|
|
||||||
expect(filterByFilterList([filter2], varMap)).toBe(false);
|
|
||||||
expect(filterByFilterList([filter3], varMap)).toBe(false);
|
|
||||||
});
|
|
||||||
test('is', async () => {
|
|
||||||
const is = filterMatcher.findData(v => v.name === 'is');
|
|
||||||
if (!is) {
|
|
||||||
throw new Error('is is not found');
|
|
||||||
}
|
|
||||||
const filter1 = filter(is, ref('Is Favourited'), [false]);
|
|
||||||
const filter2 = filter(is, ref('Is Favourited'), [true]);
|
|
||||||
const varMap = mockVariableMap({
|
|
||||||
'Is Favourited': true,
|
|
||||||
});
|
|
||||||
expect(filterByFilterList([filter1], varMap)).toBe(false);
|
|
||||||
expect(filterByFilterList([filter2], varMap)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('render filter', () => {
|
|
||||||
test('boolean condition value change', async () => {
|
|
||||||
const is = filterMatcher.match(tBoolean.create());
|
|
||||||
const i18n = getOrCreateI18n();
|
|
||||||
if (!is) {
|
|
||||||
throw new Error('is is not found');
|
|
||||||
}
|
|
||||||
const Wrapper = () => {
|
|
||||||
const [value, onChange] = useState(
|
|
||||||
filter(is, ref('Is Favourited'), [true])
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<Condition
|
|
||||||
propertiesMeta={mockPropertiesMeta({})}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</I18nextProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const result = render(<Wrapper />);
|
|
||||||
const dom = await result.findByText('true');
|
|
||||||
dom.click();
|
|
||||||
await result.findByText('false');
|
|
||||||
result.unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
const WrapperCreator = (fn: FilterMatcherDataType) =>
|
|
||||||
function Wrapper(): ReactElement {
|
|
||||||
const [value, onChange] = useState(
|
|
||||||
filter(fn, ref('Created'), [new Date(2023, 5, 29).getTime()])
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Condition
|
|
||||||
propertiesMeta={mockPropertiesMeta({})}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
test('date condition function change', async () => {
|
|
||||||
const dateFunction = filterMatcher.match(tDate.create());
|
|
||||||
if (!dateFunction) {
|
|
||||||
throw new Error('dateFunction is not found');
|
|
||||||
}
|
|
||||||
const Wrapper = WrapperCreator(dateFunction);
|
|
||||||
const result = render(<Wrapper />);
|
|
||||||
const dom = await result.findByTestId('filter-name');
|
|
||||||
dom.click();
|
|
||||||
await result.findByTestId('filter-name');
|
|
||||||
result.unmount();
|
|
||||||
});
|
|
||||||
test('date condition variable change', async () => {
|
|
||||||
const dateFunction = filterMatcher.match(tDate.create());
|
|
||||||
if (!dateFunction) {
|
|
||||||
throw new Error('dateFunction is not found');
|
|
||||||
}
|
|
||||||
const Wrapper = WrapperCreator(dateFunction);
|
|
||||||
const result = render(<Wrapper />);
|
|
||||||
const dom = await result.findByTestId('variable-name');
|
|
||||||
dom.click();
|
|
||||||
await result.findByTestId('variable-name');
|
|
||||||
result.unmount();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +1,25 @@
|
|||||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { CollectionService } from '../../../modules/collection';
|
import {
|
||||||
|
type CollectionMeta,
|
||||||
|
CollectionService,
|
||||||
|
} from '../../../modules/collection';
|
||||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||||
import { collectionHeaderColsDef } from '../header-col-def';
|
import { collectionHeaderColsDef } from '../header-col-def';
|
||||||
import { CollectionOperationCell } from '../operation-cell';
|
import { CollectionOperationCell } from '../operation-cell';
|
||||||
import { CollectionListItemRenderer } from '../page-group';
|
import { CollectionListItemRenderer } from '../page-group';
|
||||||
import { ListTableHeader } from '../page-header';
|
import { ListTableHeader } from '../page-header';
|
||||||
import type { CollectionMeta, ItemListHandle, ListItem } from '../types';
|
import type { ItemListHandle, ListItem } from '../types';
|
||||||
import { VirtualizedList } from '../virtualized-list';
|
import { VirtualizedList } from '../virtualized-list';
|
||||||
import { CollectionListHeader } from './collection-list-header';
|
import { CollectionListHeader } from './collection-list-header';
|
||||||
|
|
||||||
const useCollectionOperationsRenderer = ({
|
|
||||||
info,
|
|
||||||
service,
|
|
||||||
}: {
|
|
||||||
info: DeleteCollectionInfo;
|
|
||||||
service: CollectionService;
|
|
||||||
}) => {
|
|
||||||
const collectionOperationsRenderer = useCallback(
|
|
||||||
(collection: Collection) => {
|
|
||||||
return (
|
|
||||||
<CollectionOperationCell
|
|
||||||
info={info}
|
|
||||||
collection={collection}
|
|
||||||
service={service}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[info, service]
|
|
||||||
);
|
|
||||||
|
|
||||||
return collectionOperationsRenderer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VirtualizedCollectionList = ({
|
export const VirtualizedCollectionList = ({
|
||||||
collections,
|
|
||||||
collectionMetas,
|
|
||||||
setHideHeaderCreateNewCollection,
|
setHideHeaderCreateNewCollection,
|
||||||
handleCreateCollection,
|
handleCreateCollection,
|
||||||
}: {
|
}: {
|
||||||
collections: Collection[];
|
|
||||||
collectionMetas: CollectionMeta[];
|
|
||||||
handleCreateCollection: () => void;
|
handleCreateCollection: () => void;
|
||||||
setHideHeaderCreateNewCollection: (hide: boolean) => void;
|
setHideHeaderCreateNewCollection: (hide: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -55,30 +29,24 @@ export const VirtualizedCollectionList = ({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
|
const collectionMetas = useLiveData(collectionService.collectionMetas$);
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const info = useDeleteCollectionInfo();
|
|
||||||
|
|
||||||
const collectionOperations = useCollectionOperationsRenderer({
|
|
||||||
info,
|
|
||||||
service: collectionService,
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredSelectedCollectionIds = useMemo(() => {
|
const filteredSelectedCollectionIds = useMemo(() => {
|
||||||
const ids = new Set(collections.map(collection => collection.id));
|
const ids = new Set(collectionMetas.map(collection => collection.id));
|
||||||
return selectedCollectionIds.filter(id => ids.has(id));
|
return selectedCollectionIds.filter(id => ids.has(id));
|
||||||
}, [collections, selectedCollectionIds]);
|
}, [collectionMetas, selectedCollectionIds]);
|
||||||
|
|
||||||
const hideFloatingToolbar = useCallback(() => {
|
const hideFloatingToolbar = useCallback(() => {
|
||||||
listRef.current?.toggleSelectable();
|
listRef.current?.toggleSelectable();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const collectionOperationRenderer = useCallback(
|
const collectionOperationRenderer = useCallback((item: ListItem) => {
|
||||||
(item: ListItem) => {
|
const collection = item;
|
||||||
const collection = item as CollectionMeta;
|
return (
|
||||||
return collectionOperations(collection);
|
<CollectionOperationCell collectionMeta={collection as CollectionMeta} />
|
||||||
},
|
);
|
||||||
[collectionOperations]
|
}, []);
|
||||||
);
|
|
||||||
|
|
||||||
const collectionHeaderRenderer = useCallback(() => {
|
const collectionHeaderRenderer = useCallback(() => {
|
||||||
return <ListTableHeader headerCols={collectionHeaderColsDef} />;
|
return <ListTableHeader headerCols={collectionHeaderColsDef} />;
|
||||||
@@ -92,9 +60,11 @@ export const VirtualizedCollectionList = ({
|
|||||||
if (selectedCollectionIds.length === 0) {
|
if (selectedCollectionIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
collectionService.deleteCollection(info, ...selectedCollectionIds);
|
for (const collectionId of selectedCollectionIds) {
|
||||||
|
collectionService.deleteCollection(collectionId);
|
||||||
|
}
|
||||||
hideFloatingToolbar();
|
hideFloatingToolbar();
|
||||||
}, [collectionService, hideFloatingToolbar, info, selectedCollectionIds]);
|
}, [collectionService, hideFloatingToolbar, selectedCollectionIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { TagService } from '@affine/core/modules/tag';
|
|||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { inferOpenMode } from '@affine/core/utils';
|
import { inferOpenMode } from '@affine/core/utils';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { DocMode } from '@blocksuite/affine/model';
|
import type { DocMode } from '@blocksuite/affine/model';
|
||||||
@@ -29,8 +28,10 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||||
import { CollectionService } from '../../../modules/collection';
|
import {
|
||||||
import { createTagFilter } from '../filter/utils';
|
type Collection,
|
||||||
|
CollectionService,
|
||||||
|
} from '../../../modules/collection';
|
||||||
import { SaveAsCollectionButton } from '../view';
|
import { SaveAsCollectionButton } from '../view';
|
||||||
import * as styles from './page-list-header.css';
|
import * as styles from './page-list-header.css';
|
||||||
import { PageListNewPageButton } from './page-list-new-page-button';
|
import { PageListNewPageButton } from './page-list-new-page-button';
|
||||||
@@ -133,11 +134,12 @@ export const CollectionPageListHeader = ({
|
|||||||
const workspace = workspaceService.workspace;
|
const workspace = workspaceService.workspace;
|
||||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
const name = useLiveData(collection.name$);
|
||||||
|
|
||||||
const createAndAddDocument = useCallback(
|
const createAndAddDocument = useCallback(
|
||||||
(createDocumentFn: () => DocRecord) => {
|
(createDocumentFn: () => DocRecord) => {
|
||||||
const newDoc = createDocumentFn();
|
const newDoc = createDocumentFn();
|
||||||
collectionService.addPageToCollection(collection.id, newDoc.id);
|
collectionService.addDocToCollection(collection.id, newDoc.id);
|
||||||
},
|
},
|
||||||
[collection.id, collectionService]
|
[collection.id, collectionService]
|
||||||
);
|
);
|
||||||
@@ -183,7 +185,7 @@ export const CollectionPageListHeader = ({
|
|||||||
<div className={styles.titleIcon}>
|
<div className={styles.titleIcon}>
|
||||||
<ViewLayersIcon />
|
<ViewLayersIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.titleCollectionName}>{collection.name}</div>
|
<div className={styles.titleCollectionName}>{name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightButtonGroup}>
|
<div className={styles.rightButtonGroup}>
|
||||||
<Button onClick={handleEdit}>{t['Edit']()}</Button>
|
<Button onClick={handleEdit}>{t['Edit']()}</Button>
|
||||||
@@ -221,12 +223,21 @@ export const TagPageListHeader = ({
|
|||||||
}, [jumpToTags, workspaceId]);
|
}, [jumpToTags, workspaceId]);
|
||||||
|
|
||||||
const saveToCollection = useCallback(
|
const saveToCollection = useCallback(
|
||||||
(collection: Collection) => {
|
(collectionName: string) => {
|
||||||
collectionService.addCollection({
|
const id = collectionService.createCollection({
|
||||||
...collection,
|
name: collectionName,
|
||||||
filterList: [createTagFilter(tag.id)],
|
rules: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'include-all',
|
||||||
|
value: tag.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
jumpToCollection(workspaceId, collection.id);
|
jumpToCollection(workspaceId, id);
|
||||||
},
|
},
|
||||||
[collectionService, tag.id, jumpToCollection, workspaceId]
|
[collectionService, tag.id, jumpToCollection, workspaceId]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { IconButton, Menu, toast } from '@affine/component';
|
import { IconButton, Menu, toast } from '@affine/component';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
|
import {
|
||||||
|
CollectionRulesService,
|
||||||
|
type FilterParams,
|
||||||
|
} from '@affine/core/modules/collection-rules';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { PublicDocMode } from '@affine/graphql';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { FilterIcon } from '@blocksuite/icons/rc';
|
import { FilterIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { type ReactNode, useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { Filters } from '../../filter';
|
||||||
|
import { AddFilterMenu } from '../../filter/add-filter';
|
||||||
import { AffineShapeIcon, FavoriteTag } from '..';
|
import { AffineShapeIcon, FavoriteTag } from '..';
|
||||||
import { FilterList } from '../filter';
|
|
||||||
import { VariableSelect } from '../filter/vars';
|
|
||||||
import { usePageHeaderColsDef } from '../header-col-def';
|
import { usePageHeaderColsDef } from '../header-col-def';
|
||||||
import { PageListItemRenderer } from '../page-group';
|
import { PageListItemRenderer } from '../page-group';
|
||||||
import { ListTableHeader } from '../page-header';
|
import { ListTableHeader } from '../page-header';
|
||||||
@@ -20,7 +29,6 @@ import { SelectorLayout } from '../selector/selector-layout';
|
|||||||
import type { ListItem } from '../types';
|
import type { ListItem } from '../types';
|
||||||
import { VirtualizedList } from '../virtualized-list';
|
import { VirtualizedList } from '../virtualized-list';
|
||||||
import * as styles from './select-page.css';
|
import * as styles from './select-page.css';
|
||||||
import { useFilter } from './use-filter';
|
|
||||||
import { useSearch } from './use-search';
|
import { useSearch } from './use-search';
|
||||||
|
|
||||||
export const SelectPage = ({
|
export const SelectPage = ({
|
||||||
@@ -58,12 +66,13 @@ export const SelectPage = ({
|
|||||||
workspaceService,
|
workspaceService,
|
||||||
compatibleFavoriteItemsAdapter,
|
compatibleFavoriteItemsAdapter,
|
||||||
shareDocsListService,
|
shareDocsListService,
|
||||||
|
collectionRulesService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
ShareDocsListService,
|
ShareDocsListService,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
|
CollectionRulesService,
|
||||||
});
|
});
|
||||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
|
||||||
const workspace = workspaceService.workspace;
|
const workspace = workspaceService.workspace;
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||||
@@ -73,20 +82,6 @@ export const SelectPage = ({
|
|||||||
shareDocsListService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocsListService.shareDocs]);
|
}, [shareDocsListService.shareDocs]);
|
||||||
|
|
||||||
const getPublicMode = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
|
||||||
if (mode === PublicDocMode.Edgeless) {
|
|
||||||
return 'edgeless';
|
|
||||||
} else if (mode === PublicDocMode.Page) {
|
|
||||||
return 'page';
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[shareDocs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isFavorite = useCallback(
|
const isFavorite = useCallback(
|
||||||
(meta: DocMeta) => favourites.some(fav => fav.id === meta.id),
|
(meta: DocMeta) => favourites.some(fav => fav.id === meta.id),
|
||||||
[favourites]
|
[favourites]
|
||||||
@@ -106,22 +101,41 @@ export const SelectPage = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
const {
|
const [filters, setFilters] = useState<FilterParams[]>([]);
|
||||||
clickFilter,
|
|
||||||
createFilter,
|
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
|
||||||
filters,
|
const filteredPageMetas = useMemo(() => {
|
||||||
showFilter,
|
const idSet = new Set(filteredDocIds);
|
||||||
updateFilters,
|
return pageMetas.filter(page => idSet.has(page.id));
|
||||||
filteredList,
|
}, [pageMetas, filteredDocIds]);
|
||||||
} = useFilter(
|
|
||||||
pageMetas.map(meta => ({
|
|
||||||
meta,
|
|
||||||
publicMode: getPublicMode(meta.id),
|
|
||||||
favorite: isFavorite(meta),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
const { searchText, updateSearchText, searchedList } =
|
const { searchText, updateSearchText, searchedList } =
|
||||||
useSearch(filteredList);
|
useSearch(filteredPageMetas);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = collectionRulesService
|
||||||
|
.watch([
|
||||||
|
...filters,
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'empty-journal',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.subscribe(result => {
|
||||||
|
setFilteredDocIds(result.groups.flatMap(group => group.items));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [collectionRulesService, filters]);
|
||||||
|
|
||||||
const operationsRenderer = useCallback(
|
const operationsRenderer = useCallback(
|
||||||
(item: ListItem) => {
|
(item: ListItem) => {
|
||||||
@@ -162,29 +176,21 @@ export const SelectPage = ({
|
|||||||
{t['com.affine.selectPage.title']()}
|
{t['com.affine.selectPage.title']()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!showFilter && filters.length === 0 ? (
|
{filters.length === 0 ? (
|
||||||
<Menu
|
<Menu
|
||||||
items={
|
items={
|
||||||
<VariableSelect
|
<AddFilterMenu
|
||||||
propertiesMeta={docCollection.meta.properties}
|
onAdd={params => setFilters([...filters, params])}
|
||||||
selected={filters}
|
|
||||||
onSelect={createFilter}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton icon={<FilterIcon />} onClick={clickFilter} />
|
<IconButton icon={<FilterIcon />} />
|
||||||
</Menu>
|
</Menu>
|
||||||
) : (
|
) : null}
|
||||||
<IconButton icon={<FilterIcon />} onClick={clickFilter} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{showFilter ? (
|
{filters.length !== 0 ? (
|
||||||
<div style={{ padding: '12px 16px 16px' }}>
|
<div style={{ padding: '12px 16px 16px' }}>
|
||||||
<FilterList
|
<Filters filters={filters} onChange={setFilters} />
|
||||||
propertiesMeta={docCollection.meta.properties}
|
|
||||||
value={filters}
|
|
||||||
onChange={updateFilters}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{searchedList.length ? (
|
{searchedList.length ? (
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import type { Filter } from '@affine/env/filter';
|
|
||||||
import type { MouseEvent } from 'react';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
filterPageByRules,
|
|
||||||
type PageDataForFilter,
|
|
||||||
} from '../use-collection-manager';
|
|
||||||
|
|
||||||
export const useFilter = (list: PageDataForFilter[]) => {
|
|
||||||
const [filters, changeFilters] = useState<Filter[]>([]);
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
const clickFilter = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (showFilter || filters.length !== 0) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setShowFilter(!showFilter);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[filters.length, showFilter]
|
|
||||||
);
|
|
||||||
const onCreateFilter = useCallback(
|
|
||||||
(filter: Filter) => {
|
|
||||||
changeFilters([...filters, filter]);
|
|
||||||
setShowFilter(true);
|
|
||||||
},
|
|
||||||
[filters]
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
showFilter,
|
|
||||||
filters,
|
|
||||||
updateFilters: changeFilters,
|
|
||||||
clickFilter,
|
|
||||||
createFilter: onCreateFilter,
|
|
||||||
filteredList: list
|
|
||||||
.filter(pageData => {
|
|
||||||
if (pageData.meta.trash) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return filterPageByRules(filters, [], pageData);
|
|
||||||
})
|
|
||||||
.map(pageData => pageData.meta),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { toast, useConfirmModal } from '@affine/component';
|
import { toast, useConfirmModal } from '@affine/component';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { type Collection } from '@affine/core/modules/collection';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||||
import { usePageItemGroupDefinitions } from '../group-definitions';
|
import { usePageItemGroupDefinitions } from '../group-definitions';
|
||||||
@@ -17,7 +16,6 @@ import { PageOperationCell } from '../operation-cell';
|
|||||||
import { PageListItemRenderer } from '../page-group';
|
import { PageListItemRenderer } from '../page-group';
|
||||||
import { ListTableHeader } from '../page-header';
|
import { ListTableHeader } from '../page-header';
|
||||||
import type { ItemListHandle, ListItem } from '../types';
|
import type { ItemListHandle, ListItem } from '../types';
|
||||||
import { useFilteredPageMetas } from '../use-filtered-page-metas';
|
|
||||||
import { VirtualizedList } from '../virtualized-list';
|
import { VirtualizedList } from '../virtualized-list';
|
||||||
import {
|
import {
|
||||||
CollectionPageListHeader,
|
CollectionPageListHeader,
|
||||||
@@ -25,15 +23,14 @@ import {
|
|||||||
TagPageListHeader,
|
TagPageListHeader,
|
||||||
} from './page-list-header';
|
} from './page-list-header';
|
||||||
|
|
||||||
const usePageOperationsRenderer = () => {
|
const usePageOperationsRenderer = (collection?: Collection) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const collectionService = useService(CollectionService);
|
|
||||||
const removeFromAllowList = useCallback(
|
const removeFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
collectionService.deletePagesFromCollections([id]);
|
collection?.removeDoc(id);
|
||||||
toast(t['com.affine.collection.removePage.success']());
|
toast(t['com.affine.collection.removePage.success']());
|
||||||
},
|
},
|
||||||
[collectionService, t]
|
[collection, t]
|
||||||
);
|
);
|
||||||
const pageOperationsRenderer = useCallback(
|
const pageOperationsRenderer = useCallback(
|
||||||
(page: DocMeta, isInAllowList?: boolean) => {
|
(page: DocMeta, isInAllowList?: boolean) => {
|
||||||
@@ -53,14 +50,12 @@ const usePageOperationsRenderer = () => {
|
|||||||
export const VirtualizedPageList = memo(function VirtualizedPageList({
|
export const VirtualizedPageList = memo(function VirtualizedPageList({
|
||||||
tag,
|
tag,
|
||||||
collection,
|
collection,
|
||||||
filters,
|
|
||||||
listItem,
|
listItem,
|
||||||
setHideHeaderCreateNewPage,
|
setHideHeaderCreateNewPage,
|
||||||
disableMultiDelete,
|
disableMultiDelete,
|
||||||
}: {
|
}: {
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
collection?: Collection;
|
collection?: Collection;
|
||||||
filters?: Filter[];
|
|
||||||
listItem?: DocMeta[];
|
listItem?: DocMeta[];
|
||||||
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
||||||
disableMultiDelete?: boolean;
|
disableMultiDelete?: boolean;
|
||||||
@@ -72,19 +67,28 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
|||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||||
const pageOperations = usePageOperationsRenderer();
|
const pageOperations = usePageOperationsRenderer(collection);
|
||||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
|
|
||||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
const [filteredPageIds, setFilteredPageIds] = useState<string[]>([]);
|
||||||
filters,
|
useEffect(() => {
|
||||||
collection,
|
const subscription = collection?.watch().subscribe(docIds => {
|
||||||
});
|
setFilteredPageIds(docIds);
|
||||||
|
});
|
||||||
|
return () => subscription?.unsubscribe();
|
||||||
|
}, [collection]);
|
||||||
|
const allowList = useLiveData(collection?.info$.map(info => info.allowList));
|
||||||
const pageMetasToRender = useMemo(() => {
|
const pageMetasToRender = useMemo(() => {
|
||||||
if (listItem) {
|
if (listItem) {
|
||||||
return listItem;
|
return listItem;
|
||||||
}
|
}
|
||||||
return filteredPageMetas;
|
if (collection) {
|
||||||
}, [filteredPageMetas, listItem]);
|
return pageMetas.filter(
|
||||||
|
page => filteredPageIds.includes(page.id) && !page.trash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return pageMetas.filter(page => !page.trash);
|
||||||
|
}, [collection, filteredPageIds, listItem, pageMetas]);
|
||||||
|
|
||||||
const filteredSelectedPageIds = useMemo(() => {
|
const filteredSelectedPageIds = useMemo(() => {
|
||||||
const ids = new Set(pageMetasToRender.map(page => page.id));
|
const ids = new Set(pageMetasToRender.map(page => page.id));
|
||||||
@@ -98,10 +102,10 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
|||||||
const pageOperationRenderer = useCallback(
|
const pageOperationRenderer = useCallback(
|
||||||
(item: ListItem) => {
|
(item: ListItem) => {
|
||||||
const page = item as DocMeta;
|
const page = item as DocMeta;
|
||||||
const isInAllowList = collection?.allowList?.includes(page.id);
|
const isInAllowList = allowList?.includes(page.id);
|
||||||
return pageOperations(page, isInAllowList);
|
return pageOperations(page, isInAllowList);
|
||||||
},
|
},
|
||||||
[collection, pageOperations]
|
[allowList, pageOperations]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageHeaderRenderer = useCallback(() => {
|
const pageHeaderRenderer = useCallback(() => {
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
import { Menu, MenuItem, Tooltip } from '@affine/component';
|
|
||||||
import type { Filter, Literal, PropertiesMeta } from '@affine/env/filter';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { FilterTag } from './filter-tag-translation';
|
|
||||||
import * as styles from './index.css';
|
|
||||||
import { literalMatcher } from './literal-matcher';
|
|
||||||
import { tBoolean } from './logical/custom-type';
|
|
||||||
import type { TFunction, TType } from './logical/typesystem';
|
|
||||||
import { typesystem } from './logical/typesystem';
|
|
||||||
import { variableDefineMap } from './shared-types';
|
|
||||||
import { filterMatcher, VariableSelect, vars } from './vars';
|
|
||||||
|
|
||||||
export const Condition = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
propertiesMeta,
|
|
||||||
}: {
|
|
||||||
value: Filter;
|
|
||||||
onChange: (filter: Filter) => void;
|
|
||||||
propertiesMeta: PropertiesMeta;
|
|
||||||
}) => {
|
|
||||||
const data = useMemo(() => {
|
|
||||||
const data = filterMatcher.find(v => v.data.name === value.funcName);
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const instance = typesystem.instance(
|
|
||||||
{},
|
|
||||||
[variableDefineMap[value.left.name].type(propertiesMeta)],
|
|
||||||
tBoolean.create(),
|
|
||||||
data.type
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
render: data.data.render,
|
|
||||||
type: instance,
|
|
||||||
};
|
|
||||||
}, [propertiesMeta, value.funcName, value.left.name]);
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const render =
|
|
||||||
data.render ??
|
|
||||||
(({ ast }) => {
|
|
||||||
const args = renderArgs(value, onChange, data.type);
|
|
||||||
return (
|
|
||||||
<div className={styles.filterContainerStyle}>
|
|
||||||
<Menu
|
|
||||||
items={
|
|
||||||
<VariableSelect
|
|
||||||
propertiesMeta={propertiesMeta}
|
|
||||||
selected={[]}
|
|
||||||
onSelect={onChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="variable-name"
|
|
||||||
className={clsx(styles.filterTypeStyle, styles.ellipsisTextStyle)}
|
|
||||||
>
|
|
||||||
<Tooltip content={ast.left.name}>
|
|
||||||
<div className={styles.filterTypeIconStyle}>
|
|
||||||
{variableDefineMap[ast.left.name].icon}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<FilterTag name={ast.left.name} />
|
|
||||||
</div>
|
|
||||||
</Menu>
|
|
||||||
<Menu
|
|
||||||
items={
|
|
||||||
<FunctionSelect
|
|
||||||
propertiesMeta={propertiesMeta}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(styles.switchStyle, styles.ellipsisTextStyle)}
|
|
||||||
data-testid="filter-name"
|
|
||||||
>
|
|
||||||
<FilterTag name={ast.funcName} />
|
|
||||||
</div>
|
|
||||||
</Menu>
|
|
||||||
{args}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return <>{render({ ast: value })}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FunctionSelect = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
propertiesMeta,
|
|
||||||
}: {
|
|
||||||
value: Filter;
|
|
||||||
onChange: (value: Filter) => void;
|
|
||||||
propertiesMeta: PropertiesMeta;
|
|
||||||
}) => {
|
|
||||||
const list = useMemo(() => {
|
|
||||||
const type = vars.find(v => v.name === value.left.name)?.type;
|
|
||||||
if (!type) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return filterMatcher.allMatchedData(type(propertiesMeta));
|
|
||||||
}, [propertiesMeta, value.left.name]);
|
|
||||||
return (
|
|
||||||
<div data-testid="filter-name-select">
|
|
||||||
{list.map(v => (
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
funcName: v.name,
|
|
||||||
args: v.defaultArgs().map(v => ({ type: 'literal', value: v })),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
key={v.name}
|
|
||||||
>
|
|
||||||
<FilterTag name={v.name} />
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Arg = ({
|
|
||||||
type,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
type: TType;
|
|
||||||
value: Literal;
|
|
||||||
onChange: (lit: Literal) => void;
|
|
||||||
}) => {
|
|
||||||
const data = useMemo(() => literalMatcher.match(type), [type]);
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="filter-arg"
|
|
||||||
className={clsx(styles.argStyle, styles.ellipsisTextStyle)}
|
|
||||||
>
|
|
||||||
{data.render({
|
|
||||||
type,
|
|
||||||
value: value?.value,
|
|
||||||
onChange: v => onChange({ type: 'literal', value: v }),
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const renderArgs = (
|
|
||||||
filter: Filter,
|
|
||||||
onChange: (value: Filter) => void,
|
|
||||||
type: TFunction
|
|
||||||
): ReactNode => {
|
|
||||||
const rest = type.args.slice(1);
|
|
||||||
return rest.map((argType, i) => {
|
|
||||||
const value = filter.args[i];
|
|
||||||
return (
|
|
||||||
<Arg
|
|
||||||
key={`${argType.type}-${i}`}
|
|
||||||
type={argType}
|
|
||||||
value={value}
|
|
||||||
onChange={value => {
|
|
||||||
const args = type.args.map((_, index) =>
|
|
||||||
i === index ? value : filter.args[index]
|
|
||||||
);
|
|
||||||
onChange({
|
|
||||||
...filter,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
></Arg>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
export const datePickerTriggerInput = style({
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
width: '50px',
|
|
||||||
fontWeight: '600',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '22px',
|
|
||||||
textAlign: 'center',
|
|
||||||
':hover': {
|
|
||||||
background: cssVar('hoverColor'),
|
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { PopoverProps } from '@affine/component';
|
|
||||||
import { DatePicker, Popover } from '@affine/component';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
import { datePickerTriggerInput } from './date-select.css';
|
|
||||||
|
|
||||||
const datePickerPopperContentOptions: PopoverProps['contentOptions'] = {
|
|
||||||
style: { padding: 20, marginTop: 10 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DateSelect = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: number;
|
|
||||||
onChange: (value: number) => void;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const onDateChange = useCallback(
|
|
||||||
(e: string) => {
|
|
||||||
setOpen(false);
|
|
||||||
onChange(dayjs(e, 'YYYY-MM-DD').valueOf());
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
contentOptions={datePickerPopperContentOptions}
|
|
||||||
content={
|
|
||||||
<DatePicker
|
|
||||||
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
|
||||||
monthNames={t['com.affine.calendar-date-picker.month-names']()}
|
|
||||||
todayLabel={t['com.affine.calendar-date-picker.today']()}
|
|
||||||
value={dayjs(value as number).format('YYYY-MM-DD')}
|
|
||||||
onChange={onDateChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
value={dayjs(value as number).format('MMM DD')}
|
|
||||||
className={datePickerTriggerInput}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { Filter, Literal, Ref, VariableMap } from '@affine/env/filter';
|
|
||||||
|
|
||||||
import { filterMatcher } from './vars';
|
|
||||||
|
|
||||||
const evalRef = (ref: Ref, variableMap: VariableMap) => {
|
|
||||||
return variableMap[ref.name];
|
|
||||||
};
|
|
||||||
const evalLiteral = (lit?: Literal) => {
|
|
||||||
return lit?.value;
|
|
||||||
};
|
|
||||||
const evalFilter = (filter: Filter, variableMap: VariableMap): boolean => {
|
|
||||||
const impl = filterMatcher.findData(v => v.name === filter.funcName)?.impl;
|
|
||||||
if (!impl) {
|
|
||||||
throw new Error('No function implementation found');
|
|
||||||
}
|
|
||||||
const leftValue = evalRef(filter.left, variableMap);
|
|
||||||
const args = filter.args.map(evalLiteral);
|
|
||||||
return impl(leftValue, ...args);
|
|
||||||
};
|
|
||||||
export const evalFilterList = (
|
|
||||||
filterList: Filter[],
|
|
||||||
variableMap: VariableMap
|
|
||||||
) => {
|
|
||||||
return filterList.every(filter => evalFilter(filter, variableMap));
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { Button, IconButton, Menu } from '@affine/component';
|
|
||||||
import type { Filter, PropertiesMeta } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import { CloseIcon, PlusIcon } from '@blocksuite/icons/rc';
|
|
||||||
|
|
||||||
import { Condition } from './condition';
|
|
||||||
import * as styles from './index.css';
|
|
||||||
import { CreateFilterMenu } from './vars';
|
|
||||||
|
|
||||||
export const FilterList = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
propertiesMeta,
|
|
||||||
}: {
|
|
||||||
value: Filter[];
|
|
||||||
onChange: (value: Filter[]) => void;
|
|
||||||
propertiesMeta: PropertiesMeta;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 10,
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value.map((filter, i) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.filterItemStyle} key={i}>
|
|
||||||
<Condition
|
|
||||||
propertiesMeta={propertiesMeta}
|
|
||||||
value={filter}
|
|
||||||
onChange={filter => {
|
|
||||||
onChange(
|
|
||||||
value.map((old, oldIndex) => (oldIndex === i ? filter : old))
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={styles.filterItemCloseStyle}
|
|
||||||
onClick={() => {
|
|
||||||
onChange(value.filter((_, index) => i !== index));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Menu
|
|
||||||
key={value.length} // hack to force menu to rerender (disable unmount animation)
|
|
||||||
items={
|
|
||||||
<CreateFilterMenu
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
propertiesMeta={propertiesMeta}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{value.length === 0 ? (
|
|
||||||
<Button suffix={<PlusIcon />}>
|
|
||||||
{t['com.affine.filterList.button.add']()}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<IconButton size="16">
|
|
||||||
<PlusIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Tooltip } from '@affine/component';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
|
|
||||||
import { ellipsisTextStyle } from './index.css';
|
|
||||||
type FilterTagProps = {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFilterTag = ({ name }: FilterTagProps) => {
|
|
||||||
const t = useI18n();
|
|
||||||
switch (name) {
|
|
||||||
case 'Created':
|
|
||||||
return t['Created']();
|
|
||||||
case 'Updated':
|
|
||||||
return t['Updated']();
|
|
||||||
case 'Tags':
|
|
||||||
return t['Tags']();
|
|
||||||
case 'Is Favourited':
|
|
||||||
return t['com.affine.filter.is-favourited']();
|
|
||||||
case 'Is Public':
|
|
||||||
return t['com.affine.filter.is-public']();
|
|
||||||
case 'after':
|
|
||||||
return t['com.affine.filter.after']();
|
|
||||||
case 'before':
|
|
||||||
return t['com.affine.filter.before']();
|
|
||||||
case 'last':
|
|
||||||
return t['com.affine.filter.last']();
|
|
||||||
case 'is':
|
|
||||||
return t['com.affine.filter.is']();
|
|
||||||
case 'is not empty':
|
|
||||||
return t['com.affine.filter.is not empty']();
|
|
||||||
case 'is empty':
|
|
||||||
return t['com.affine.filter.is empty']();
|
|
||||||
case 'contains all':
|
|
||||||
return t['com.affine.filter.contains all']();
|
|
||||||
case 'contains one of':
|
|
||||||
return t['com.affine.filter.contains one of']();
|
|
||||||
case 'does not contains all':
|
|
||||||
return t['com.affine.filter.does not contains all']();
|
|
||||||
case 'does not contains one of':
|
|
||||||
return t['com.affine.filter.does not contains one of']();
|
|
||||||
case 'true':
|
|
||||||
return t['com.affine.filter.true']();
|
|
||||||
case 'false':
|
|
||||||
return t['com.affine.filter.false']();
|
|
||||||
default:
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FilterTag = ({ name }: FilterTagProps) => {
|
|
||||||
const tag = useFilterTag({ name });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip content={tag}>
|
|
||||||
<span className={ellipsisTextStyle} data-testid={`filler-tag-${tag}`}>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
export const filterContainerStyle = style({
|
|
||||||
display: 'flex',
|
|
||||||
userSelect: 'none',
|
|
||||||
alignItems: 'center',
|
|
||||||
overflow: 'hidden',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const menuItemStyle = style({
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
});
|
|
||||||
export const variableSelectTitleStyle = style({
|
|
||||||
margin: '7px 16px',
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: '20px',
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
});
|
|
||||||
export const variableSelectDividerStyle = style({
|
|
||||||
marginTop: '2px',
|
|
||||||
marginBottom: '2px',
|
|
||||||
marginLeft: '12px',
|
|
||||||
marginRight: '8px',
|
|
||||||
height: '1px',
|
|
||||||
background: cssVar('borderColor'),
|
|
||||||
});
|
|
||||||
export const menuItemTextStyle = style({
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
});
|
|
||||||
export const filterItemStyle = style({
|
|
||||||
display: 'flex',
|
|
||||||
border: `1px solid ${cssVar('borderColor')}`,
|
|
||||||
borderRadius: '8px',
|
|
||||||
background: cssVar('white'),
|
|
||||||
padding: '4px 8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
});
|
|
||||||
export const filterItemCloseStyle = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
marginLeft: '4px',
|
|
||||||
});
|
|
||||||
export const inputStyle = style({
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
padding: '2px 4px',
|
|
||||||
transition: 'all 0.15s ease-in-out',
|
|
||||||
':hover': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: cssVar('hoverColor'),
|
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const switchStyle = style({
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
padding: '2px 4px',
|
|
||||||
transition: 'all 0.15s ease-in-out',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: '3 1 auto',
|
|
||||||
minWidth: '28px',
|
|
||||||
':hover': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: cssVar('hoverColor'),
|
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const filterTypeStyle = style({
|
|
||||||
fontSize: cssVar('fontSm'),
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '2px 4px',
|
|
||||||
transition: 'all 0.15s ease-in-out',
|
|
||||||
marginRight: '6px',
|
|
||||||
flex: '1 0 auto',
|
|
||||||
':hover': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: cssVar('hoverColor'),
|
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const filterTypeIconStyle = style({
|
|
||||||
fontSize: cssVar('fontBase'),
|
|
||||||
marginRight: '6px',
|
|
||||||
padding: '1px 0',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: cssVar('iconColor'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const argStyle = style({
|
|
||||||
marginLeft: 4,
|
|
||||||
fontWeight: 600,
|
|
||||||
flex: '1 0 auto',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ellipsisTextStyle = style({
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './eval';
|
|
||||||
export * from './filter-list';
|
|
||||||
export * from './utils';
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { Input, Menu, MenuItem } from '@affine/component';
|
|
||||||
import type { LiteralValue } from '@affine/env/filter';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import type { TagMeta } from '../types';
|
|
||||||
import { DateSelect } from './date-select';
|
|
||||||
import { FilterTag } from './filter-tag-translation';
|
|
||||||
import { inputStyle } from './index.css';
|
|
||||||
import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type';
|
|
||||||
import { Matcher } from './logical/matcher';
|
|
||||||
import type { TType } from './logical/typesystem';
|
|
||||||
import { tArray, typesystem } from './logical/typesystem';
|
|
||||||
import { MultiSelect } from './multi-select';
|
|
||||||
|
|
||||||
export const literalMatcher = new Matcher<{
|
|
||||||
render: (props: {
|
|
||||||
type: TType;
|
|
||||||
value: LiteralValue;
|
|
||||||
onChange: (lit: LiteralValue) => void;
|
|
||||||
}) => ReactNode;
|
|
||||||
}>((type, target) => {
|
|
||||||
return typesystem.isSubtype(type, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
literalMatcher.register(tDateRange.create(), {
|
|
||||||
render: ({ value, onChange }) => (
|
|
||||||
<Menu
|
|
||||||
items={
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
// Handle the input change and update the value accordingly
|
|
||||||
onChange={i => (i ? onChange(parseInt(i)) : onChange(0))}
|
|
||||||
/>
|
|
||||||
{[1, 2, 3, 7, 14, 30].map(i => (
|
|
||||||
<MenuItem
|
|
||||||
key={i}
|
|
||||||
onClick={() => {
|
|
||||||
// Handle the menu item click and update the value accordingly
|
|
||||||
onChange(i);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i} {i > 1 ? 'days' : 'day'}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span>{value.toString()}</span> {(value as number) > 1 ? 'days' : 'day'}
|
|
||||||
</div>
|
|
||||||
</Menu>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
literalMatcher.register(tBoolean.create(), {
|
|
||||||
render: ({ value, onChange }) => (
|
|
||||||
<div
|
|
||||||
className={inputStyle}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
onChange(!value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FilterTag name={value?.toString()} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
literalMatcher.register(tDate.create(), {
|
|
||||||
render: ({ value, onChange }) => (
|
|
||||||
<DateSelect value={value as number} onChange={onChange} />
|
|
||||||
),
|
|
||||||
});
|
|
||||||
const getTagsOfArrayTag = (type: TType): TagMeta[] => {
|
|
||||||
if (type.type === 'array') {
|
|
||||||
if (tTag.is(type.ele)) {
|
|
||||||
return type.ele.data?.tags ?? [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
literalMatcher.register(tArray(tTag.create()), {
|
|
||||||
render: ({ type, value, onChange }) => {
|
|
||||||
return (
|
|
||||||
<MultiSelect
|
|
||||||
value={(value ?? []) as string[]}
|
|
||||||
onChange={value => onChange(value)}
|
|
||||||
options={getTagsOfArrayTag(type).map((v: any) => ({
|
|
||||||
label: v.name,
|
|
||||||
value: v.id,
|
|
||||||
}))}
|
|
||||||
></MultiSelect>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { TagMeta } from '../../types';
|
|
||||||
import { DataHelper, typesystem } from './typesystem';
|
|
||||||
|
|
||||||
export const tNumber = typesystem.defineData(
|
|
||||||
DataHelper.create<{ value: number }>('Number')
|
|
||||||
);
|
|
||||||
export const tString = typesystem.defineData(
|
|
||||||
DataHelper.create<{ value: string }>('String')
|
|
||||||
);
|
|
||||||
export const tBoolean = typesystem.defineData(
|
|
||||||
DataHelper.create<{ value: boolean }>('Boolean')
|
|
||||||
);
|
|
||||||
export const tDate = typesystem.defineData(
|
|
||||||
DataHelper.create<{ value: number }>('Date')
|
|
||||||
);
|
|
||||||
|
|
||||||
export const tTag = typesystem.defineData<{ tags: TagMeta[] }>({
|
|
||||||
name: 'Tag',
|
|
||||||
supers: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tDateRange = typesystem.defineData(
|
|
||||||
DataHelper.create<{ value: number }>('DateRange')
|
|
||||||
);
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import type { TType } from './typesystem';
|
|
||||||
import { typesystem } from './typesystem';
|
|
||||||
|
|
||||||
type MatcherData<Data, Type extends TType = TType> = { type: Type; data: Data };
|
|
||||||
|
|
||||||
export class Matcher<Data, Type extends TType = TType> {
|
|
||||||
private readonly list: MatcherData<Data, Type>[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _match?: (type: Type, target: TType) => boolean
|
|
||||||
) {}
|
|
||||||
|
|
||||||
register(type: Type, data: Data) {
|
|
||||||
this.list.push({ type, data });
|
|
||||||
}
|
|
||||||
|
|
||||||
match(type: TType) {
|
|
||||||
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
|
||||||
for (const t of this.list) {
|
|
||||||
if (match(t.type, type)) {
|
|
||||||
return t.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
allMatched(type: TType): MatcherData<Data>[] {
|
|
||||||
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
|
||||||
const result: MatcherData<Data>[] = [];
|
|
||||||
for (const t of this.list) {
|
|
||||||
if (match(t.type, type)) {
|
|
||||||
result.push(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
allMatchedData(type: TType): Data[] {
|
|
||||||
return this.allMatched(type).map(v => v.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
findData(f: (data: Data) => boolean): Data | undefined {
|
|
||||||
return this.list.find(data => f(data.data))?.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
find(
|
|
||||||
f: (data: MatcherData<Data, Type>) => boolean
|
|
||||||
): MatcherData<Data, Type> | undefined {
|
|
||||||
return this.list.find(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
all(): MatcherData<Data, Type>[] {
|
|
||||||
return this.list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file will be moved to a separate package soon.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface TUnion {
|
|
||||||
type: 'union';
|
|
||||||
title: 'union';
|
|
||||||
list: TType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tUnion = (list: TType[]): TUnion => ({
|
|
||||||
type: 'union',
|
|
||||||
title: 'union',
|
|
||||||
list,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO treat as data type
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export interface TArray<Ele extends TType = TType> {
|
|
||||||
type: 'array';
|
|
||||||
ele: Ele;
|
|
||||||
title: 'array';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tArray = <const T extends TType>(ele: T): TArray<T> => {
|
|
||||||
return {
|
|
||||||
type: 'array',
|
|
||||||
title: 'array',
|
|
||||||
ele,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type TTypeVar = {
|
|
||||||
type: 'typeVar';
|
|
||||||
title: 'typeVar';
|
|
||||||
name: string;
|
|
||||||
bound: TType;
|
|
||||||
};
|
|
||||||
export const tTypeVar = (name: string, bound: TType): TTypeVar => {
|
|
||||||
return {
|
|
||||||
type: 'typeVar',
|
|
||||||
title: 'typeVar',
|
|
||||||
name,
|
|
||||||
bound,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type TTypeRef = {
|
|
||||||
type: 'typeRef';
|
|
||||||
title: 'typeRef';
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
export const tTypeRef = (name: string): TTypeRef => {
|
|
||||||
return {
|
|
||||||
type: 'typeRef',
|
|
||||||
title: 'typeRef',
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TFunction = {
|
|
||||||
type: 'function';
|
|
||||||
title: 'function';
|
|
||||||
typeVars: TTypeVar[];
|
|
||||||
args: TType[];
|
|
||||||
rt: TType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tFunction = (fn: {
|
|
||||||
typeVars?: TTypeVar[];
|
|
||||||
args: TType[];
|
|
||||||
rt: TType;
|
|
||||||
}): TFunction => {
|
|
||||||
return {
|
|
||||||
type: 'function',
|
|
||||||
title: 'function',
|
|
||||||
typeVars: fn.typeVars ?? [],
|
|
||||||
args: fn.args,
|
|
||||||
rt: fn.rt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TType = TDataType | TArray | TUnion | TTypeRef | TFunction;
|
|
||||||
|
|
||||||
export type DataTypeShape = Record<string, unknown>;
|
|
||||||
export type TDataType<Data extends DataTypeShape = Record<string, unknown>> = {
|
|
||||||
type: 'data';
|
|
||||||
name: string;
|
|
||||||
data?: Data;
|
|
||||||
};
|
|
||||||
export type ValueOfData<T extends DataDefine> =
|
|
||||||
T extends DataDefine<infer R> ? R : never;
|
|
||||||
|
|
||||||
export class DataDefine<Data extends DataTypeShape = Record<string, unknown>> {
|
|
||||||
constructor(
|
|
||||||
private readonly config: DataDefineConfig<Data>,
|
|
||||||
private readonly dataMap: Map<string, DataDefine>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
create(data?: Data): TDataType<Data> {
|
|
||||||
return {
|
|
||||||
type: 'data',
|
|
||||||
name: this.config.name,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
is(data: TType): data is TDataType<Data> {
|
|
||||||
if (data.type !== 'data') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return data.name === this.config.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isByName(name: string): boolean {
|
|
||||||
return name === this.config.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSubOf(superType: TDataType): boolean {
|
|
||||||
if (this.is(superType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.config.supers.some(sup => sup.isSubOf(superType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSubOfByName(superType: string): boolean {
|
|
||||||
if (this.isByName(superType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.config.supers.some(sup => sup.isSubOfByName(superType));
|
|
||||||
}
|
|
||||||
|
|
||||||
isSuperOf(subType: TDataType): boolean {
|
|
||||||
const dataDefine = this.dataMap.get(subType.name);
|
|
||||||
if (!dataDefine) {
|
|
||||||
throw new Error('bug');
|
|
||||||
}
|
|
||||||
return dataDefine.isSubOfByName(this.config.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// type DataTypeVar = {};
|
|
||||||
|
|
||||||
// TODO support generic data type
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
interface DataDefineConfig<T extends DataTypeShape> {
|
|
||||||
name: string;
|
|
||||||
supers: DataDefine[];
|
|
||||||
_phantom?: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataHelper<T extends DataTypeShape> {
|
|
||||||
create<V = Record<string, unknown>>(name: string): DataDefineConfig<T & V>;
|
|
||||||
|
|
||||||
extends<V extends DataTypeShape>(
|
|
||||||
dataDefine: DataDefine<V>
|
|
||||||
): DataHelper<T & V>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createDataHelper = <T extends DataTypeShape = Record<string, unknown>>(
|
|
||||||
...supers: DataDefine[]
|
|
||||||
): DataHelper<T> => {
|
|
||||||
return {
|
|
||||||
create(name: string) {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
supers,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
extends(dataDefine) {
|
|
||||||
return createDataHelper(...supers, dataDefine);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const DataHelper = createDataHelper();
|
|
||||||
|
|
||||||
export class Typesystem {
|
|
||||||
dataMap = new Map<string, DataDefine<any>>();
|
|
||||||
|
|
||||||
defineData<T extends DataTypeShape>(
|
|
||||||
config: DataDefineConfig<T>
|
|
||||||
): DataDefine<T> {
|
|
||||||
const result = new DataDefine(config, this.dataMap);
|
|
||||||
this.dataMap.set(config.name, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDataType(t: TType): t is TDataType {
|
|
||||||
return t.type === 'data';
|
|
||||||
}
|
|
||||||
|
|
||||||
isSubtype(
|
|
||||||
superType: TType,
|
|
||||||
sub: TType,
|
|
||||||
context?: Record<string, TType>
|
|
||||||
): boolean {
|
|
||||||
if (superType.type === 'typeRef') {
|
|
||||||
// TODO both are ref
|
|
||||||
if (context && sub.type !== 'typeRef') {
|
|
||||||
context[superType.name] = sub;
|
|
||||||
}
|
|
||||||
// TODO bound
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (sub.type === 'typeRef') {
|
|
||||||
// TODO both are ref
|
|
||||||
if (context) {
|
|
||||||
context[sub.name] = superType;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (tUnknown.is(superType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (superType.type === 'union') {
|
|
||||||
return superType.list.some(type => this.isSubtype(type, sub, context));
|
|
||||||
}
|
|
||||||
if (sub.type === 'union') {
|
|
||||||
return sub.list.every(type => this.isSubtype(superType, type, context));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isDataType(sub)) {
|
|
||||||
const dataDefine = this.dataMap.get(sub.name);
|
|
||||||
if (!dataDefine) {
|
|
||||||
throw new Error('bug');
|
|
||||||
}
|
|
||||||
if (!this.isDataType(superType)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return dataDefine.isSubOf(superType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (superType.type === 'array' || sub.type === 'array') {
|
|
||||||
if (superType.type !== 'array' || sub.type !== 'array') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.isSubtype(superType.ele, sub.ele, context);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
subst(context: Record<string, TType>, template: TFunction): TFunction {
|
|
||||||
const subst = (type: TType): TType => {
|
|
||||||
if (this.isDataType(type)) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
switch (type.type) {
|
|
||||||
case 'typeRef':
|
|
||||||
return { ...context[type.name] };
|
|
||||||
case 'union':
|
|
||||||
return tUnion(type.list.map(type => subst(type)));
|
|
||||||
case 'array':
|
|
||||||
return tArray(subst(type.ele));
|
|
||||||
case 'function':
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const result = tFunction({
|
|
||||||
args: template.args.map(type => subst(type)),
|
|
||||||
rt: subst(template.rt),
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance(
|
|
||||||
context: Record<string, TType>,
|
|
||||||
realArgs: TType[],
|
|
||||||
realRt: TType,
|
|
||||||
template: TFunction
|
|
||||||
): TFunction {
|
|
||||||
const ctx = { ...context };
|
|
||||||
template.args.forEach((arg, i) => {
|
|
||||||
const realArg = realArgs[i];
|
|
||||||
if (realArg) {
|
|
||||||
this.isSubtype(arg, realArg, ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.isSubtype(realRt, template.rt);
|
|
||||||
return this.subst(ctx, template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const typesystem = new Typesystem();
|
|
||||||
export const tUnknown = typesystem.defineData(DataHelper.create('Unknown'));
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
export const content = style({
|
|
||||||
fontSize: 12,
|
|
||||||
color: cssVar('textPrimaryColor'),
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: '3px 4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
overflow: 'hidden',
|
|
||||||
':hover': {
|
|
||||||
backgroundColor: cssVar('hoverColor'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const text = style({
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
maxWidth: 350,
|
|
||||||
selectors: {
|
|
||||||
'&.empty': {
|
|
||||||
color: 'var(--affine-text-secondary-color)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const optionList = style({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 4,
|
|
||||||
padding: '0 4px',
|
|
||||||
maxHeight: '220px',
|
|
||||||
});
|
|
||||||
export const scrollbar = style({
|
|
||||||
vars: {
|
|
||||||
'--scrollbar-width': '4px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const selectOption = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: 14,
|
|
||||||
height: 26,
|
|
||||||
borderRadius: 5,
|
|
||||||
maxWidth: 240,
|
|
||||||
minWidth: 100,
|
|
||||||
padding: '0 12px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
':hover': {
|
|
||||||
backgroundColor: cssVar('hoverColor'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const optionLabel = style({
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
flex: 1,
|
|
||||||
});
|
|
||||||
export const done = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: cssVar('primaryColor'),
|
|
||||||
marginLeft: 8,
|
|
||||||
});
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { Menu, MenuItem, Scrollable, Tooltip } from '@affine/component';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { MouseEvent } from 'react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import * as styles from './multi-select.css';
|
|
||||||
|
|
||||||
export const MultiSelect = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
value: string[];
|
|
||||||
onChange: (value: string[]) => void;
|
|
||||||
options: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const optionMap = useMemo(
|
|
||||||
() => Object.fromEntries(options.map(v => [v.value, v])),
|
|
||||||
[options]
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = useMemo(
|
|
||||||
() => value.map(id => optionMap[id]?.label).join(', '),
|
|
||||||
[optionMap, value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
|
||||||
return (
|
|
||||||
<Scrollable.Root>
|
|
||||||
<Scrollable.Viewport
|
|
||||||
data-testid="multi-select"
|
|
||||||
className={styles.optionList}
|
|
||||||
>
|
|
||||||
{options.length === 0 ? (
|
|
||||||
<MenuItem checked={true}>
|
|
||||||
{t['com.affine.filter.empty-tag']()}
|
|
||||||
</MenuItem>
|
|
||||||
) : (
|
|
||||||
options.map(option => {
|
|
||||||
const selected = value.includes(option.value);
|
|
||||||
const click = (e: MouseEvent<HTMLDivElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
if (selected) {
|
|
||||||
onChange(value.filter(v => v !== option.value));
|
|
||||||
} else {
|
|
||||||
onChange([...value, option.value]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
data-testid={`multi-select-${option.label}`}
|
|
||||||
checked={selected}
|
|
||||||
onClick={click}
|
|
||||||
key={option.value}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</Scrollable.Viewport>
|
|
||||||
<Scrollable.Scrollbar className={styles.scrollbar} />
|
|
||||||
</Scrollable.Root>
|
|
||||||
);
|
|
||||||
}, [onChange, options, t, value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu items={items}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
content.length ? content : t['com.affine.filter.empty-tag']()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{value.length ? (
|
|
||||||
<div className={styles.text}>{content}</div>
|
|
||||||
) : (
|
|
||||||
<div className={clsx(styles.text, 'empty')}>
|
|
||||||
{t['com.affine.filter.empty-tag']()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import type {
|
|
||||||
Literal,
|
|
||||||
LiteralValue,
|
|
||||||
PropertiesMeta,
|
|
||||||
VariableMap,
|
|
||||||
} from '@affine/env/filter';
|
|
||||||
import {
|
|
||||||
CloudWorkspaceIcon,
|
|
||||||
CreatedIcon,
|
|
||||||
FavoriteIcon,
|
|
||||||
TagsIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
} from '@blocksuite/icons/rc';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
|
|
||||||
import { tBoolean, tDate, tTag } from './logical/custom-type';
|
|
||||||
import type { TType } from './logical/typesystem';
|
|
||||||
import { tArray } from './logical/typesystem';
|
|
||||||
|
|
||||||
export const toLiteral = (value: LiteralValue): Literal => ({
|
|
||||||
type: 'literal',
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FilterVariable = {
|
|
||||||
name: keyof VariableMap;
|
|
||||||
type: (propertiesMeta: PropertiesMeta) => TType;
|
|
||||||
icon: ReactElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const variableDefineMap = {
|
|
||||||
Created: {
|
|
||||||
type: () => tDate.create(),
|
|
||||||
icon: <CreatedIcon />,
|
|
||||||
},
|
|
||||||
Updated: {
|
|
||||||
type: () => tDate.create(),
|
|
||||||
icon: <UpdatedIcon />,
|
|
||||||
},
|
|
||||||
'Is Favourited': {
|
|
||||||
type: () => tBoolean.create(),
|
|
||||||
icon: <FavoriteIcon />,
|
|
||||||
},
|
|
||||||
Tags: {
|
|
||||||
type: meta =>
|
|
||||||
tArray(
|
|
||||||
tTag.create({
|
|
||||||
tags:
|
|
||||||
meta.tags?.options.map(t => ({
|
|
||||||
id: t.id,
|
|
||||||
name: t.value,
|
|
||||||
color: t.color,
|
|
||||||
})) ?? [],
|
|
||||||
})
|
|
||||||
),
|
|
||||||
icon: <TagsIcon />,
|
|
||||||
},
|
|
||||||
'Is Public': {
|
|
||||||
type: () => tBoolean.create(),
|
|
||||||
icon: <CloudWorkspaceIcon />,
|
|
||||||
},
|
|
||||||
// Imported: {
|
|
||||||
// type: tBoolean.create(),
|
|
||||||
// },
|
|
||||||
// 'Daily Note': {
|
|
||||||
// type: tBoolean.create(),
|
|
||||||
// },
|
|
||||||
} satisfies Record<string, Omit<FilterVariable, 'name'>>;
|
|
||||||
|
|
||||||
export type InternalVariableMap = {
|
|
||||||
[K in keyof typeof variableDefineMap]: LiteralValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare module '@affine/env/filter' {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
interface VariableMap extends InternalVariableMap {}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { Filter } from '@affine/env/filter';
|
|
||||||
|
|
||||||
export const createTagFilter = (id: string): Filter => {
|
|
||||||
return {
|
|
||||||
type: 'filter',
|
|
||||||
left: { type: 'ref', name: 'Tags' },
|
|
||||||
funcName: 'contains all',
|
|
||||||
args: [{ type: 'literal', value: [id] }],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
import { MenuItem, MenuSeparator } from '@affine/component';
|
|
||||||
import type {
|
|
||||||
Filter,
|
|
||||||
LiteralValue,
|
|
||||||
PropertiesMeta,
|
|
||||||
VariableMap,
|
|
||||||
} from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { FilterTag } from './filter-tag-translation';
|
|
||||||
import * as styles from './index.css';
|
|
||||||
import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type';
|
|
||||||
import { Matcher } from './logical/matcher';
|
|
||||||
import type { TFunction } from './logical/typesystem';
|
|
||||||
import {
|
|
||||||
tArray,
|
|
||||||
tFunction,
|
|
||||||
tTypeRef,
|
|
||||||
tTypeVar,
|
|
||||||
typesystem,
|
|
||||||
} from './logical/typesystem';
|
|
||||||
import type { FilterVariable } from './shared-types';
|
|
||||||
import { variableDefineMap } from './shared-types';
|
|
||||||
|
|
||||||
export const vars: FilterVariable[] = Object.entries(variableDefineMap).map(
|
|
||||||
([key, value]) => ({
|
|
||||||
name: key as keyof VariableMap,
|
|
||||||
type: value.type,
|
|
||||||
icon: value.icon,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const createDefaultFilter = (
|
|
||||||
variable: FilterVariable,
|
|
||||||
propertiesMeta: PropertiesMeta
|
|
||||||
): Filter => {
|
|
||||||
const data = filterMatcher.match(variable.type(propertiesMeta));
|
|
||||||
if (!data) {
|
|
||||||
throw new Error('No matching function found');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'filter',
|
|
||||||
left: {
|
|
||||||
type: 'ref',
|
|
||||||
name: variable.name,
|
|
||||||
},
|
|
||||||
funcName: data.name,
|
|
||||||
args: data.defaultArgs().map(value => ({
|
|
||||||
type: 'literal',
|
|
||||||
value,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateFilterMenu = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
propertiesMeta,
|
|
||||||
}: {
|
|
||||||
value: Filter[];
|
|
||||||
onChange: (value: Filter[]) => void;
|
|
||||||
propertiesMeta: PropertiesMeta;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<VariableSelect
|
|
||||||
propertiesMeta={propertiesMeta}
|
|
||||||
selected={value}
|
|
||||||
onSelect={filter => {
|
|
||||||
onChange([...value, filter]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const VariableSelect = ({
|
|
||||||
onSelect,
|
|
||||||
propertiesMeta,
|
|
||||||
}: {
|
|
||||||
selected: Filter[];
|
|
||||||
onSelect: (value: Filter) => void;
|
|
||||||
propertiesMeta: PropertiesMeta;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<div data-testid="variable-select">
|
|
||||||
<div className={styles.variableSelectTitleStyle}>
|
|
||||||
{t['com.affine.filter']()}
|
|
||||||
</div>
|
|
||||||
<MenuSeparator />
|
|
||||||
{vars
|
|
||||||
// .filter(v => !selected.find(filter => filter.left.name === v.name))
|
|
||||||
.map(v => (
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={variableDefineMap[v.name].icon}
|
|
||||||
key={v.name}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(createDefaultFilter(v, propertiesMeta));
|
|
||||||
}}
|
|
||||||
className={styles.menuItemStyle}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="variable-select-item"
|
|
||||||
className={styles.menuItemTextStyle}
|
|
||||||
>
|
|
||||||
<FilterTag name={v.name} />
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FilterMatcherDataType = {
|
|
||||||
name: string;
|
|
||||||
defaultArgs: () => LiteralValue[];
|
|
||||||
render?: (props: { ast: Filter }) => ReactNode;
|
|
||||||
impl: (...args: (LiteralValue | undefined)[]) => boolean;
|
|
||||||
};
|
|
||||||
export const filterMatcher = new Matcher<FilterMatcherDataType, TFunction>(
|
|
||||||
(type, target) => {
|
|
||||||
const staticType = typesystem.subst(
|
|
||||||
Object.fromEntries(type.typeVars?.map(v => [v.name, v.bound]) ?? []),
|
|
||||||
type
|
|
||||||
);
|
|
||||||
const firstArg = staticType.args[0];
|
|
||||||
return firstArg && typesystem.isSubtype(firstArg, target);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
args: [tBoolean.create(), tBoolean.create()],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'is',
|
|
||||||
defaultArgs: () => [true],
|
|
||||||
impl: (value, target) => {
|
|
||||||
return value === target;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
args: [tDate.create(), tDate.create()],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'after',
|
|
||||||
defaultArgs: () => {
|
|
||||||
return [dayjs().subtract(1, 'day').endOf('day').valueOf()];
|
|
||||||
},
|
|
||||||
impl: (date, target) => {
|
|
||||||
if (typeof date !== 'number' || typeof target !== 'number') {
|
|
||||||
throw new Error('argument type error');
|
|
||||||
}
|
|
||||||
return dayjs(date).isAfter(dayjs(target).endOf('day'));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
args: [tDate.create(), tDateRange.create()],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'last',
|
|
||||||
defaultArgs: () => [30], // Default to the last 30 days
|
|
||||||
impl: (date, n) => {
|
|
||||||
if (typeof date !== 'number' || typeof n !== 'number') {
|
|
||||||
throw new Error('Argument type error: date and n must be numbers');
|
|
||||||
}
|
|
||||||
const startDate = dayjs().subtract(n, 'day').startOf('day').valueOf();
|
|
||||||
return date > startDate;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
args: [tDate.create(), tDate.create()],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'before',
|
|
||||||
defaultArgs: () => [dayjs().endOf('day').valueOf()],
|
|
||||||
impl: (date, target) => {
|
|
||||||
if (typeof date !== 'number' || typeof target !== 'number') {
|
|
||||||
throw new Error('argument type error');
|
|
||||||
}
|
|
||||||
return dayjs(date).isBefore(dayjs(target).startOf('day'));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const safeArray = (arr: unknown): LiteralValue[] => {
|
|
||||||
return Array.isArray(arr) ? arr : [];
|
|
||||||
};
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
args: [tArray(tTag.create())],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'is not empty',
|
|
||||||
defaultArgs: () => [],
|
|
||||||
impl: tags => {
|
|
||||||
const safeTags = safeArray(tags);
|
|
||||||
return safeTags.length > 0;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
args: [tArray(tTag.create())],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'is empty',
|
|
||||||
defaultArgs: () => [],
|
|
||||||
impl: tags => {
|
|
||||||
const safeTags = safeArray(tags);
|
|
||||||
return safeTags.length === 0;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
typeVars: [tTypeVar('T', tTag.create())],
|
|
||||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'contains all',
|
|
||||||
defaultArgs: () => [],
|
|
||||||
impl: (tags, target) => {
|
|
||||||
if (!Array.isArray(target)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const safeTags = safeArray(tags);
|
|
||||||
return target.every(id => safeTags.includes(id));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
typeVars: [tTypeVar('T', tTag.create())],
|
|
||||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'contains one of',
|
|
||||||
defaultArgs: () => [],
|
|
||||||
impl: (tags, target) => {
|
|
||||||
if (!Array.isArray(target)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const safeTags = safeArray(tags);
|
|
||||||
return target.some(id => safeTags.includes(id));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
typeVars: [tTypeVar('T', tTag.create())],
|
|
||||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'does not contains all',
|
|
||||||
defaultArgs: () => [],
|
|
||||||
impl: (tags, target) => {
|
|
||||||
if (!Array.isArray(target)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const safeTags = safeArray(tags);
|
|
||||||
return !target.every(id => safeTags.includes(id));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
filterMatcher.register(
|
|
||||||
tFunction({
|
|
||||||
typeVars: [tTypeVar('T', tTag.create())],
|
|
||||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
|
||||||
rt: tBoolean.create(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'does not contains one of',
|
|
||||||
defaultArgs: () => [],
|
|
||||||
impl: (tags, target) => {
|
|
||||||
if (!Array.isArray(target)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const safeTags = safeArray(tags);
|
|
||||||
return !target.some(id => safeTags.includes(id));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -6,7 +6,6 @@ export * from './components/page-display-menu';
|
|||||||
export * from './docs';
|
export * from './docs';
|
||||||
export * from './docs/page-list-item';
|
export * from './docs/page-list-item';
|
||||||
export * from './docs/page-tags';
|
export * from './docs/page-tags';
|
||||||
export * from './filter';
|
|
||||||
export * from './group-definitions';
|
export * from './group-definitions';
|
||||||
export * from './header-col-def';
|
export * from './header-col-def';
|
||||||
export * from './list';
|
export * from './list';
|
||||||
@@ -17,8 +16,6 @@ export * from './page-header';
|
|||||||
export * from './tags';
|
export * from './tags';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './use-all-doc-display-properties';
|
export * from './use-all-doc-display-properties';
|
||||||
export * from './use-collection-manager';
|
|
||||||
export * from './use-filtered-page-metas';
|
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './view';
|
export * from './view';
|
||||||
export * from './virtualized-list';
|
export * from './virtualized-list';
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from '@affine/core/modules/favorite';
|
} from '@affine/core/modules/favorite';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
@@ -38,8 +37,10 @@ import { useLiveData, useService, useServices } from '@toeverything/infra';
|
|||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import {
|
||||||
import type { CollectionService } from '../../modules/collection';
|
type CollectionMeta,
|
||||||
|
CollectionService,
|
||||||
|
} from '../../modules/collection';
|
||||||
import { useGuard } from '../guard';
|
import { useGuard } from '../guard';
|
||||||
import { IsFavoriteIcon } from '../pure/icons';
|
import { IsFavoriteIcon } from '../pure/icons';
|
||||||
import { FavoriteTag } from './components/favorite-tag';
|
import { FavoriteTag } from './components/favorite-tag';
|
||||||
@@ -328,31 +329,28 @@ export const TrashOperationCell = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface CollectionOperationCellProps {
|
export interface CollectionOperationCellProps {
|
||||||
collection: Collection;
|
collectionMeta: CollectionMeta;
|
||||||
info: DeleteCollectionInfo;
|
|
||||||
service: CollectionService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollectionOperationCell = ({
|
export const CollectionOperationCell = ({
|
||||||
collection,
|
collectionMeta,
|
||||||
service,
|
|
||||||
info,
|
|
||||||
}: CollectionOperationCellProps) => {
|
}: CollectionOperationCellProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const {
|
const {
|
||||||
compatibleFavoriteItemsAdapter: favAdapter,
|
compatibleFavoriteItemsAdapter: favAdapter,
|
||||||
workspaceService,
|
|
||||||
workspaceDialogService,
|
workspaceDialogService,
|
||||||
|
collectionService,
|
||||||
|
docsService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
WorkspaceService,
|
|
||||||
WorkspaceDialogService,
|
WorkspaceDialogService,
|
||||||
|
CollectionService,
|
||||||
|
DocsService,
|
||||||
});
|
});
|
||||||
const docCollection = workspaceService.workspace.docCollection;
|
const collectionId = collectionMeta.id;
|
||||||
const { createPage } = usePageHelper(docCollection);
|
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const favourite = useLiveData(
|
const favourite = useLiveData(
|
||||||
favAdapter.isFavorite$(collection.id, 'collection')
|
favAdapter.isFavorite$(collectionId, 'collection')
|
||||||
);
|
);
|
||||||
|
|
||||||
const { openPromptModal } = usePromptModal();
|
const { openPromptModal } = usePromptModal();
|
||||||
@@ -377,44 +375,43 @@ export const CollectionOperationCell = ({
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
service.updateCollection(collection.id, () => ({
|
collectionService.updateCollection(collectionId, {
|
||||||
...collection,
|
|
||||||
name,
|
name,
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[collection, handlePropagation, openPromptModal, service, t]
|
[collectionId, collectionService, handlePropagation, openPromptModal, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEdit = useCallback(
|
const handleEdit = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
handlePropagation(event);
|
handlePropagation(event);
|
||||||
workspaceDialogService.open('collection-editor', {
|
workspaceDialogService.open('collection-editor', {
|
||||||
collectionId: collection.id,
|
collectionId: collectionId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[handlePropagation, workspaceDialogService, collection.id]
|
[handlePropagation, workspaceDialogService, collectionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
return service.deleteCollection(info, collection.id);
|
return collectionService.deleteCollection(collectionId);
|
||||||
}, [service, info, collection]);
|
}, [collectionId, collectionService]);
|
||||||
|
|
||||||
const onToggleFavoriteCollection = useCallback(() => {
|
const onToggleFavoriteCollection = useCallback(() => {
|
||||||
const status = favAdapter.isFavorite(collection.id, 'collection');
|
const status = favAdapter.isFavorite(collectionId, 'collection');
|
||||||
favAdapter.toggle(collection.id, 'collection');
|
favAdapter.toggle(collectionId, 'collection');
|
||||||
toast(
|
toast(
|
||||||
status
|
status
|
||||||
? t['com.affine.toastMessage.removedFavorites']()
|
? t['com.affine.toastMessage.removedFavorites']()
|
||||||
: t['com.affine.toastMessage.addedFavorites']()
|
: t['com.affine.toastMessage.addedFavorites']()
|
||||||
);
|
);
|
||||||
}, [favAdapter, collection.id, t]);
|
}, [favAdapter, collectionId, t]);
|
||||||
|
|
||||||
const createAndAddDocument = useCallback(() => {
|
const createAndAddDocument = useCallback(() => {
|
||||||
const newDoc = createPage();
|
const newDoc = docsService.createDoc();
|
||||||
service.addPageToCollection(collection.id, newDoc.id);
|
collectionService.addDocToCollection(collectionId, newDoc.id);
|
||||||
}, [collection.id, createPage, service]);
|
}, [docsService, collectionService, collectionId]);
|
||||||
|
|
||||||
const onConfirmAddDocToCollection = useCallback(() => {
|
const onConfirmAddDocToCollection = useCallback(() => {
|
||||||
openConfirmModal({
|
openConfirmModal({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { shallowEqual } from '@affine/component';
|
import { shallowEqual } from '@affine/component';
|
||||||
|
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
@@ -25,7 +26,6 @@ import {
|
|||||||
import { TagListItem } from './tags/tag-list-item';
|
import { TagListItem } from './tags/tag-list-item';
|
||||||
import type {
|
import type {
|
||||||
CollectionListItemProps,
|
CollectionListItemProps,
|
||||||
CollectionMeta,
|
|
||||||
ItemGroupProps,
|
ItemGroupProps,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListProps,
|
ListProps,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Collection } from '@affine/env/filter';
|
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||||
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
||||||
import type { JSX, PropsWithChildren, ReactNode } from 'react';
|
import type { JSX, PropsWithChildren, ReactNode } from 'react';
|
||||||
import type { To } from 'react-router-dom';
|
import type { To } from 'react-router-dom';
|
||||||
|
|
||||||
export type ListItem = DocMeta | CollectionMeta | TagMeta;
|
export type ListItem =
|
||||||
|
| DocMeta
|
||||||
export interface CollectionMeta extends Collection {
|
| (CollectionMeta & {
|
||||||
title: string;
|
createDate?: Date | number;
|
||||||
createDate?: Date | number;
|
updatedDate?: Date | number;
|
||||||
updatedDate?: Date | number;
|
})
|
||||||
}
|
| TagMeta;
|
||||||
|
|
||||||
export type TagMeta = {
|
export type TagMeta = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
|
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
|
||||||
|
|
||||||
import { evalFilterList } from './filter';
|
|
||||||
|
|
||||||
export const createEmptyCollection = (
|
|
||||||
id: string,
|
|
||||||
data?: Partial<Omit<Collection, 'id'>>
|
|
||||||
): Collection => {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: '',
|
|
||||||
filterList: [],
|
|
||||||
allowList: [],
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
|
||||||
evalFilterList(filterList, varMap);
|
|
||||||
|
|
||||||
export type PageDataForFilter = {
|
|
||||||
meta: DocMeta;
|
|
||||||
favorite: boolean;
|
|
||||||
publicMode: undefined | 'page' | 'edgeless';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterPage = (collection: Collection, page: PageDataForFilter) => {
|
|
||||||
if (collection.filterList.length === 0) {
|
|
||||||
return collection.allowList.includes(page.meta.id);
|
|
||||||
}
|
|
||||||
return filterPageByRules(collection.filterList, collection.allowList, page);
|
|
||||||
};
|
|
||||||
export const filterPageByRules = (
|
|
||||||
rules: Filter[],
|
|
||||||
allowList: string[],
|
|
||||||
{ meta, publicMode, favorite }: PageDataForFilter
|
|
||||||
) => {
|
|
||||||
if (allowList?.includes(meta.id)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return filterByFilterList(rules, {
|
|
||||||
'Is Favourited': !!favorite,
|
|
||||||
'Is Public': !!publicMode,
|
|
||||||
Created: meta.createDate,
|
|
||||||
Updated: meta.updatedDate ?? meta.createDate,
|
|
||||||
Tags: meta.tags,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
|
||||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
|
||||||
import { PublicDocMode } from '@affine/graphql';
|
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { filterPage, filterPageByRules } from './use-collection-manager';
|
|
||||||
|
|
||||||
export const useFilteredPageMetas = (
|
|
||||||
pageMetas: DocMeta[],
|
|
||||||
options: {
|
|
||||||
trash?: boolean;
|
|
||||||
filters?: Filter[];
|
|
||||||
collection?: Collection;
|
|
||||||
} = {}
|
|
||||||
) => {
|
|
||||||
const shareDocsListService = useService(ShareDocsListService);
|
|
||||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
|
||||||
|
|
||||||
const getPublicMode = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
|
||||||
return mode
|
|
||||||
? mode === PublicDocMode.Edgeless
|
|
||||||
? ('edgeless' as const)
|
|
||||||
: ('page' as const)
|
|
||||||
: undefined;
|
|
||||||
},
|
|
||||||
[shareDocs]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// TODO(@eyhn): loading & error UI
|
|
||||||
shareDocsListService.shareDocs?.revalidate();
|
|
||||||
}, [shareDocsListService]);
|
|
||||||
|
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
|
||||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
|
||||||
|
|
||||||
const filteredPageMetas = useMemo(
|
|
||||||
() =>
|
|
||||||
pageMetas.filter(pageMeta => {
|
|
||||||
if (options.trash) {
|
|
||||||
if (!pageMeta.trash) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (pageMeta.trash) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const pageData = {
|
|
||||||
meta: pageMeta,
|
|
||||||
favorite: favoriteItems.some(fav => fav.id === pageMeta.id),
|
|
||||||
publicMode: getPublicMode(pageMeta.id),
|
|
||||||
};
|
|
||||||
if (
|
|
||||||
options.filters &&
|
|
||||||
!filterPageByRules(options.filters, [], pageData)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.collection && !filterPage(options.collection, pageData)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
pageMetas,
|
|
||||||
options.trash,
|
|
||||||
options.filters,
|
|
||||||
options.collection,
|
|
||||||
favoriteItems,
|
|
||||||
getPublicMode,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return filteredPageMetas;
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
export const menuTitleStyle = style({
|
|
||||||
marginLeft: '12px',
|
|
||||||
marginTop: '10px',
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
});
|
|
||||||
export const menuDividerStyle = style({
|
|
||||||
marginTop: '2px',
|
|
||||||
marginBottom: '2px',
|
|
||||||
marginLeft: '12px',
|
|
||||||
marginRight: '8px',
|
|
||||||
height: '1px',
|
|
||||||
background: cssVar('borderColor'),
|
|
||||||
});
|
|
||||||
export const viewMenu = style({});
|
|
||||||
export const viewOption = style({
|
|
||||||
borderRadius: 8,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginLeft: 6,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
opacity: 0,
|
|
||||||
':hover': {
|
|
||||||
backgroundColor: cssVar('hoverColor'),
|
|
||||||
},
|
|
||||||
selectors: {
|
|
||||||
[`${viewMenu}:hover &`]: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const filterMenuTrigger = style({
|
|
||||||
padding: '6px 8px',
|
|
||||||
selectors: {
|
|
||||||
[`&[data-is-hidden="true"]`]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Button, FlexWrapper, Menu } from '@affine/component';
|
|
||||||
import type { Filter, PropertiesMeta } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import { FilterIcon } from '@blocksuite/icons/rc';
|
|
||||||
|
|
||||||
import { CreateFilterMenu } from '../filter/vars';
|
|
||||||
import * as styles from './collection-list.css';
|
|
||||||
|
|
||||||
export const AllPageListOperationsMenu = ({
|
|
||||||
propertiesMeta,
|
|
||||||
filterList,
|
|
||||||
onChangeFilterList,
|
|
||||||
}: {
|
|
||||||
propertiesMeta: PropertiesMeta;
|
|
||||||
filterList: Filter[];
|
|
||||||
onChangeFilterList: (filterList: Filter[]) => void;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FlexWrapper alignItems="center">
|
|
||||||
<Menu
|
|
||||||
items={
|
|
||||||
<CreateFilterMenu
|
|
||||||
propertiesMeta={propertiesMeta}
|
|
||||||
value={filterList}
|
|
||||||
onChange={onChangeFilterList}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={styles.filterMenuTrigger}
|
|
||||||
prefix={<FilterIcon />}
|
|
||||||
data-testid="create-first-filter"
|
|
||||||
>
|
|
||||||
{t['com.affine.filter']()}
|
|
||||||
</Button>
|
|
||||||
</Menu>
|
|
||||||
</FlexWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { MenuItemProps } from '@affine/component';
|
import type { MenuItemProps } from '@affine/component';
|
||||||
import { Menu, MenuItem, usePromptModal } from '@affine/component';
|
import { Menu, MenuItem, usePromptModal } from '@affine/component';
|
||||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
@@ -18,7 +16,10 @@ import { useLiveData, useService, useServices } from '@toeverything/infra';
|
|||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { CollectionService } from '../../../modules/collection';
|
import {
|
||||||
|
type Collection,
|
||||||
|
CollectionService,
|
||||||
|
} from '../../../modules/collection';
|
||||||
import { IsFavoriteIcon } from '../../pure/icons';
|
import { IsFavoriteIcon } from '../../pure/icons';
|
||||||
import * as styles from './collection-operations.css';
|
import * as styles from './collection-operations.css';
|
||||||
|
|
||||||
@@ -41,7 +42,6 @@ export const CollectionOperations = ({
|
|||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
WorkspaceDialogService,
|
WorkspaceDialogService,
|
||||||
});
|
});
|
||||||
const deleteInfo = useDeleteCollectionInfo();
|
|
||||||
const workbench = workbenchService.workbench;
|
const workbench = workbenchService.workbench;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { openPromptModal } = usePromptModal();
|
const { openPromptModal } = usePromptModal();
|
||||||
@@ -63,10 +63,9 @@ export const CollectionOperations = ({
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
service.updateCollection(collection.id, () => ({
|
service.updateCollection(collection.id, {
|
||||||
...collection,
|
|
||||||
name,
|
name,
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [openRenameModal, openPromptModal, t, service, collection]);
|
}, [openRenameModal, openPromptModal, t, service, collection]);
|
||||||
@@ -160,7 +159,7 @@ export const CollectionOperations = ({
|
|||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
name: t['Delete'](),
|
name: t['Delete'](),
|
||||||
click: () => {
|
click: () => {
|
||||||
service.deleteCollection(deleteInfo, collection.id);
|
service.deleteCollection(collection.id);
|
||||||
},
|
},
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
},
|
},
|
||||||
@@ -175,7 +174,6 @@ export const CollectionOperations = ({
|
|||||||
openCollectionNewTab,
|
openCollectionNewTab,
|
||||||
openCollectionSplitView,
|
openCollectionSplitView,
|
||||||
service,
|
service,
|
||||||
deleteInfo,
|
|
||||||
collection.id,
|
collection.id,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * from './affine-shape';
|
export * from './affine-shape';
|
||||||
export * from './collection-list';
|
|
||||||
export * from './collection-operations';
|
export * from './collection-operations';
|
||||||
export * from './create-collection';
|
export * from './create-collection';
|
||||||
export * from './save-as-collection-button';
|
export * from './save-as-collection-button';
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { Button, usePromptModal } from '@affine/component';
|
import { Button, usePromptModal } from '@affine/component';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { SaveIcon } from '@blocksuite/icons/rc';
|
import { SaveIcon } from '@blocksuite/icons/rc';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { createEmptyCollection } from '../use-collection-manager';
|
|
||||||
import * as styles from './save-as-collection-button.css';
|
import * as styles from './save-as-collection-button.css';
|
||||||
|
|
||||||
interface SaveAsCollectionButtonProps {
|
interface SaveAsCollectionButtonProps {
|
||||||
onConfirm: (collection: Collection) => void;
|
onConfirm: (collectionName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SaveAsCollectionButton = ({
|
export const SaveAsCollectionButton = ({
|
||||||
@@ -35,7 +32,7 @@ export const SaveAsCollectionButton = ({
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
onConfirm(createEmptyCollection(nanoid(), { name }));
|
onConfirm(name);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [openPromptModal, t, onConfirm]);
|
}, [openPromptModal, t, onConfirm]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/core/modules/collection';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { DeleteIcon, FilterIcon } from '@blocksuite/icons/rc';
|
import { DeleteIcon, FilterIcon } from '@blocksuite/icons/rc';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { toast, useConfirmModal } from '@affine/component';
|
import { toast, useConfirmModal } from '@affine/component';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { GuardService } from '@affine/core/modules/permissions';
|
import { GuardService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { useService } from '@toeverything/infra';
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { ListFloatingToolbar } from './components/list-floating-toolbar';
|
import { ListFloatingToolbar } from './components/list-floating-toolbar';
|
||||||
@@ -14,7 +15,6 @@ import { TrashOperationCell } from './operation-cell';
|
|||||||
import { PageListItemRenderer } from './page-group';
|
import { PageListItemRenderer } from './page-group';
|
||||||
import { ListTableHeader } from './page-header';
|
import { ListTableHeader } from './page-header';
|
||||||
import type { ItemListHandle, ListItem } from './types';
|
import type { ItemListHandle, ListItem } from './types';
|
||||||
import { useFilteredPageMetas } from './use-filtered-page-metas';
|
|
||||||
import { VirtualizedList } from './virtualized-list';
|
import { VirtualizedList } from './virtualized-list';
|
||||||
|
|
||||||
export const VirtualizedTrashList = ({
|
export const VirtualizedTrashList = ({
|
||||||
@@ -25,13 +25,17 @@ export const VirtualizedTrashList = ({
|
|||||||
disableMultiRestore?: boolean;
|
disableMultiRestore?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
const docsService = useService(DocsService);
|
||||||
const guardService = useService(GuardService);
|
const guardService = useService(GuardService);
|
||||||
const docCollection = currentWorkspace.docCollection;
|
const docCollection = currentWorkspace.docCollection;
|
||||||
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
|
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
|
||||||
|
const allTrashPageIds = useLiveData(
|
||||||
|
LiveData.from(docsService.allTrashDocIds$(), [])
|
||||||
|
);
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
const filteredPageMetas = useMemo(() => {
|
||||||
trash: true,
|
return pageMetas.filter(page => allTrashPageIds.includes(page.id));
|
||||||
});
|
}, [pageMetas, allTrashPageIds]);
|
||||||
|
|
||||||
const listRef = useRef<ItemListHandle>(null);
|
const listRef = useRef<ItemListHandle>(null);
|
||||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Menu, MenuItem } from '@affine/component';
|
||||||
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
|
|
||||||
|
export const FavoriteFilterValue = ({
|
||||||
|
filter,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
filter: FilterParams;
|
||||||
|
onChange: (filter: FilterParams) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onChange({
|
||||||
|
...filter,
|
||||||
|
value: 'true',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
selected={filter.value === 'true'}
|
||||||
|
>
|
||||||
|
{'True'}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onChange({
|
||||||
|
...filter,
|
||||||
|
value: 'false',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
selected={filter.value !== 'true'}
|
||||||
|
>
|
||||||
|
{'False'}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{filter.value === 'true' ? 'True' : 'False'}</span>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import type { I18nString } from '@affine/i18n';
|
import type { I18nString } from '@affine/i18n';
|
||||||
import { TagIcon } from '@blocksuite/icons/rc';
|
import { FavoriteIcon, ShareIcon, TagIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
|
import { FavoriteFilterValue } from './favorite';
|
||||||
|
import { SharedFilterValue } from './shared';
|
||||||
import { TagsFilterValue } from './tags';
|
import { TagsFilterValue } from './tags';
|
||||||
|
|
||||||
export const SystemPropertyTypes = {
|
export const SystemPropertyTypes = {
|
||||||
@@ -15,6 +17,22 @@ export const SystemPropertyTypes = {
|
|||||||
},
|
},
|
||||||
filterValue: TagsFilterValue,
|
filterValue: TagsFilterValue,
|
||||||
},
|
},
|
||||||
|
favorite: {
|
||||||
|
icon: FavoriteIcon,
|
||||||
|
name: 'Favorited',
|
||||||
|
filterMethod: {
|
||||||
|
is: 'com.affine.filter.is',
|
||||||
|
},
|
||||||
|
filterValue: FavoriteFilterValue,
|
||||||
|
},
|
||||||
|
shared: {
|
||||||
|
icon: ShareIcon,
|
||||||
|
name: 'Shared',
|
||||||
|
filterMethod: {
|
||||||
|
is: 'com.affine.filter.is',
|
||||||
|
},
|
||||||
|
filterValue: SharedFilterValue,
|
||||||
|
},
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[type: string]: {
|
[type: string]: {
|
||||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Menu, MenuItem } from '@affine/component';
|
||||||
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
|
|
||||||
|
export const SharedFilterValue = ({
|
||||||
|
filter,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
filter: FilterParams;
|
||||||
|
onChange: (filter: FilterParams) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onChange({
|
||||||
|
...filter,
|
||||||
|
value: 'true',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
selected={filter.value === 'true'}
|
||||||
|
>
|
||||||
|
{'True'}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onChange({
|
||||||
|
...filter,
|
||||||
|
value: 'false',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
selected={filter.value !== 'true'}
|
||||||
|
>
|
||||||
|
{'False'}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{filter.value === 'true' ? 'True' : 'False'}</span>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -102,7 +102,10 @@ export const WorkspacePropertyTypes = {
|
|||||||
renameable: false,
|
renameable: false,
|
||||||
description: 'com.affine.page-properties.property.tags.tooltips',
|
description: 'com.affine.page-properties.property.tags.tooltips',
|
||||||
filterMethod: {
|
filterMethod: {
|
||||||
include: 'com.affine.filter.contains all',
|
'include-all': 'com.affine.filter.contains all',
|
||||||
|
'include-any-of': 'com.affine.filter.contains one of',
|
||||||
|
'not-include-all': 'com.affine.filter.does not contains all',
|
||||||
|
'not-include-any-of': 'com.affine.filter.does not contains one of',
|
||||||
'is-not-empty': 'com.affine.filter.is not empty',
|
'is-not-empty': 'com.affine.filter.is not empty',
|
||||||
'is-empty': 'com.affine.filter.is empty',
|
'is-empty': 'com.affine.filter.is empty',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,26 +5,17 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
toast,
|
toast,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { filterPage } from '@affine/core/components/page-list';
|
import {
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
type Collection,
|
||||||
|
CollectionService,
|
||||||
|
} from '@affine/core/modules/collection';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { PublicDocMode } from '@affine/graphql';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
|
||||||
import { FilterMinusIcon } from '@blocksuite/icons/rc';
|
import { FilterMinusIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||||
LiveData,
|
|
||||||
useLiveData,
|
|
||||||
useService,
|
|
||||||
useServices,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -71,6 +62,7 @@ export const NavigationPanelCollectionNode = ({
|
|||||||
|
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||||
|
const name = useLiveData(collection?.name$);
|
||||||
|
|
||||||
const dndData = useMemo(() => {
|
const dndData = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -89,11 +81,10 @@ export const NavigationPanelCollectionNode = ({
|
|||||||
|
|
||||||
const handleRename = useCallback(
|
const handleRename = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
if (collection && collection.name !== name) {
|
if (collection && collection.name$.value !== name) {
|
||||||
collectionService.updateCollection(collectionId, () => ({
|
collectionService.updateCollection(collectionId, {
|
||||||
...collection,
|
|
||||||
name,
|
name,
|
||||||
}));
|
});
|
||||||
|
|
||||||
track.$.navigationPanel.organize.renameOrganizeItem({
|
track.$.navigationPanel.organize.renameOrganizeItem({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
@@ -109,10 +100,10 @@ export const NavigationPanelCollectionNode = ({
|
|||||||
if (!collection) {
|
if (!collection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (collection.allowList.includes(docId)) {
|
if (collection.allowList$.value.includes(docId)) {
|
||||||
toast(t['com.affine.collection.addPage.alreadyExists']());
|
toast(t['com.affine.collection.addPage.alreadyExists']());
|
||||||
} else {
|
} else {
|
||||||
collectionService.addPageToCollection(collection.id, docId);
|
collectionService.addDocToCollection(collection.id, docId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[collection, collectionService, t]
|
[collection, collectionService, t]
|
||||||
@@ -210,7 +201,7 @@ export const NavigationPanelCollectionNode = ({
|
|||||||
return (
|
return (
|
||||||
<NavigationPanelTreeNode
|
<NavigationPanelTreeNode
|
||||||
icon={CollectionIcon}
|
icon={CollectionIcon}
|
||||||
name={collection.name || t['Untitled']()}
|
name={name || t['Untitled']()}
|
||||||
dndData={dndData}
|
dndData={dndData}
|
||||||
onDrop={handleDropOnCollection}
|
onDrop={handleDropOnCollection}
|
||||||
renameable
|
renameable
|
||||||
@@ -237,84 +228,51 @@ const NavigationPanelCollectionNodeChildren = ({
|
|||||||
collection: Collection;
|
collection: Collection;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const {
|
const { collectionService } = useServices({
|
||||||
docsService,
|
|
||||||
compatibleFavoriteItemsAdapter,
|
|
||||||
shareDocsListService,
|
|
||||||
collectionService,
|
|
||||||
} = useServices({
|
|
||||||
DocsService,
|
|
||||||
CompatibleFavoriteItemsAdapter,
|
|
||||||
ShareDocsListService,
|
|
||||||
CollectionService,
|
CollectionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const allowList = useLiveData(
|
||||||
// TODO(@eyhn): loading & error UI
|
collection.allowList$.map(list => new Set(list))
|
||||||
shareDocsListService.shareDocs?.revalidate();
|
|
||||||
}, [shareDocsListService]);
|
|
||||||
|
|
||||||
const docMetas = useLiveData(
|
|
||||||
useMemo(
|
|
||||||
() =>
|
|
||||||
LiveData.computed(get => {
|
|
||||||
return get(docsService.list.docs$).map(
|
|
||||||
doc => get(doc.meta$) as DocMeta
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[docsService]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
|
||||||
const allowList = useMemo(
|
|
||||||
() => new Set(collection.allowList),
|
|
||||||
[collection.allowList]
|
|
||||||
);
|
|
||||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
|
||||||
|
|
||||||
const handleRemoveFromAllowList = useCallback(
|
const handleRemoveFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
track.$.navigationPanel.collections.removeOrganizeItem({ type: 'doc' });
|
track.$.navigationPanel.collections.removeOrganizeItem({ type: 'doc' });
|
||||||
collectionService.deletePageFromCollection(collection.id, id);
|
collectionService.removeDocFromCollection(collection.id, id);
|
||||||
toast(t['com.affine.collection.removePage.success']());
|
toast(t['com.affine.collection.removePage.success']());
|
||||||
},
|
},
|
||||||
[collection.id, collectionService, t]
|
[collection.id, collectionService, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filtered = docMetas.filter(meta => {
|
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
|
||||||
if (meta.trash) return false;
|
|
||||||
const publicMode = shareDocs?.find(d => d.id === meta.id)?.mode;
|
|
||||||
const pageData = {
|
|
||||||
meta: meta as DocMeta,
|
|
||||||
publicMode:
|
|
||||||
publicMode === PublicDocMode.Edgeless
|
|
||||||
? ('edgeless' as const)
|
|
||||||
: publicMode === PublicDocMode.Page
|
|
||||||
? ('page' as const)
|
|
||||||
: undefined,
|
|
||||||
favorite: favourites.some(fav => fav.id === meta.id),
|
|
||||||
};
|
|
||||||
return filterPage(collection, pageData);
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered.map(doc => (
|
useEffect(() => {
|
||||||
|
const subscription = collection.watch().subscribe(docIds => {
|
||||||
|
setFilteredDocIds(docIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [collection]);
|
||||||
|
|
||||||
|
return filteredDocIds.map(docId => (
|
||||||
<NavigationPanelDocNode
|
<NavigationPanelDocNode
|
||||||
key={doc.id}
|
key={docId}
|
||||||
docId={doc.id}
|
docId={docId}
|
||||||
reorderable={false}
|
reorderable={false}
|
||||||
location={{
|
location={{
|
||||||
at: 'navigation-panel:collection:filtered-docs',
|
at: 'navigation-panel:collection:filtered-docs',
|
||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
}}
|
}}
|
||||||
operations={
|
operations={
|
||||||
allowList
|
allowList.has(docId)
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
index: 99,
|
index: 99,
|
||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<FilterMinusIcon />}
|
prefixIcon={<FilterMinusIcon />}
|
||||||
onClick={() => handleRemoveFromAllowList(doc.id)}
|
onClick={() => handleRemoveFromAllowList(docId)}
|
||||||
>
|
>
|
||||||
{t['Remove special filter']()}
|
{t['Remove special filter']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
@@ -42,7 +41,6 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
CollectionService,
|
CollectionService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
});
|
});
|
||||||
const deleteInfo = useDeleteCollectionInfo();
|
|
||||||
|
|
||||||
const { createPage } = usePageHelper(
|
const { createPage } = usePageHelper(
|
||||||
workspaceService.workspace.docCollection
|
workspaceService.workspace.docCollection
|
||||||
@@ -59,7 +57,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
|
|
||||||
const createAndAddDocument = useCallback(() => {
|
const createAndAddDocument = useCallback(() => {
|
||||||
const newDoc = createPage();
|
const newDoc = createPage();
|
||||||
collectionService.addPageToCollection(collectionId, newDoc.id);
|
collectionService.addDocToCollection(collectionId, newDoc.id);
|
||||||
track.$.navigationPanel.collections.createDoc();
|
track.$.navigationPanel.collections.createDoc();
|
||||||
track.$.navigationPanel.collections.addDocToCollection({
|
track.$.navigationPanel.collections.addDocToCollection({
|
||||||
control: 'button',
|
control: 'button',
|
||||||
@@ -100,11 +98,11 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
}, [collectionId, workbenchService.workbench]);
|
}, [collectionId, workbenchService.workbench]);
|
||||||
|
|
||||||
const handleDeleteCollection = useCallback(() => {
|
const handleDeleteCollection = useCallback(() => {
|
||||||
collectionService.deleteCollection(deleteInfo, collectionId);
|
collectionService.deleteCollection(collectionId);
|
||||||
track.$.navigationPanel.organize.deleteOrganizeItem({
|
track.$.navigationPanel.organize.deleteOrganizeItem({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
});
|
});
|
||||||
}, [collectionId, collectionService, deleteInfo]);
|
}, [collectionId, collectionService]);
|
||||||
|
|
||||||
const handleShowEdit = useCallback(() => {
|
const handleShowEdit = useCallback(() => {
|
||||||
onOpenEdit();
|
onOpenEdit();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { IconButton, usePromptModal } from '@affine/component';
|
import { IconButton, usePromptModal } from '@affine/component';
|
||||||
import { createEmptyCollection } from '@affine/core/components/page-list/use-collection-manager';
|
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
@@ -7,7 +6,6 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||||
@@ -46,8 +44,7 @@ export const NavigationPanelCollections = () => {
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
const id = nanoid();
|
const id = collectionService.createCollection({ name });
|
||||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
|
||||||
track.$.navigationPanel.organize.createOrganizeItem({
|
track.$.navigationPanel.organize.createOrganizeItem({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
});
|
});
|
||||||
@@ -84,7 +81,7 @@ export const NavigationPanelCollections = () => {
|
|||||||
<NavigationPanelTreeRoot
|
<NavigationPanelTreeRoot
|
||||||
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
|
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
|
||||||
>
|
>
|
||||||
{collections.map(collection => (
|
{Array.from(collections.values()).map(collection => (
|
||||||
<NavigationPanelCollectionNode
|
<NavigationPanelCollectionNode
|
||||||
key={collection.id}
|
key={collection.id}
|
||||||
collectionId={collection.id}
|
collectionId={collection.id}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, RadioGroup } from '@affine/component';
|
import { Button, RadioGroup } from '@affine/component';
|
||||||
import { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
import { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||||
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
@@ -12,10 +12,10 @@ export type EditCollectionMode = 'page' | 'rule';
|
|||||||
|
|
||||||
export interface EditCollectionProps {
|
export interface EditCollectionProps {
|
||||||
onConfirmText?: string;
|
onConfirmText?: string;
|
||||||
init: Collection;
|
init: CollectionInfo;
|
||||||
mode?: EditCollectionMode;
|
mode?: EditCollectionMode;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: (collection: Collection) => void;
|
onConfirm: (collection: CollectionInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditCollection = ({
|
export const EditCollection = ({
|
||||||
@@ -27,9 +27,9 @@ export const EditCollection = ({
|
|||||||
}: EditCollectionProps) => {
|
}: EditCollectionProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const config = useAllPageListConfig();
|
const config = useAllPageListConfig();
|
||||||
const [value, onChange] = useState<Collection>(init);
|
const [value, onChange] = useState<CollectionInfo>(init);
|
||||||
const [mode, setMode] = useState<'page' | 'rule'>(
|
const [mode, setMode] = useState<'page' | 'rule'>(
|
||||||
initMode ?? (init.filterList.length === 0 ? 'page' : 'rule')
|
initMode ?? (init.rules.filters.length === 0 ? 'page' : 'rule')
|
||||||
);
|
);
|
||||||
const isNameEmpty = useMemo(() => value.name.trim().length === 0, [value]);
|
const isNameEmpty = useMemo(() => value.name.trim().length === 0, [value]);
|
||||||
const onSaveCollection = useCallback(() => {
|
const onSaveCollection = useCallback(() => {
|
||||||
@@ -40,10 +40,10 @@ export const EditCollection = ({
|
|||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
filterList: init.filterList,
|
rules: init.rules,
|
||||||
allowList: init.allowList,
|
allowList: init.allowList,
|
||||||
});
|
});
|
||||||
}, [init.allowList, init.filterList, value]);
|
}, [init, value]);
|
||||||
const onIdsChange = useCallback(
|
const onIdsChange = useCallback(
|
||||||
(ids: string[]) => {
|
(ids: string[]) => {
|
||||||
onChange({ ...value, allowList: ids });
|
onChange({ ...value, allowList: ids });
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Modal } from '@affine/component';
|
import { Modal } from '@affine/component';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import {
|
||||||
|
type CollectionInfo,
|
||||||
|
CollectionService,
|
||||||
|
} from '@affine/core/modules/collection';
|
||||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -18,17 +20,18 @@ export const CollectionEditorDialog = ({
|
|||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||||
const onConfirmOnCollection = useCallback(
|
const onConfirmOnCollection = useCallback(
|
||||||
(collection: Collection) => {
|
(collection: CollectionInfo) => {
|
||||||
collectionService.updateCollection(collection.id, () => collection);
|
collectionService.updateCollection(collection.id, collection);
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
[close, collectionService]
|
[close, collectionService]
|
||||||
);
|
);
|
||||||
|
const info = useLiveData(collection?.info$);
|
||||||
const onCancel = useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
close();
|
close();
|
||||||
}, [close]);
|
}, [close]);
|
||||||
|
|
||||||
if (!collection) {
|
if (!collection || !info) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +53,7 @@ export const CollectionEditorDialog = ({
|
|||||||
>
|
>
|
||||||
<EditCollection
|
<EditCollection
|
||||||
onConfirmText={t['com.affine.editCollection.save']()}
|
onConfirmText={t['com.affine.editCollection.save']()}
|
||||||
init={collection}
|
init={info}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onConfirm={onConfirmOnCollection}
|
onConfirm={onConfirmOnCollection}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Button, IconButton, Tooltip } from '@affine/component';
|
import { Button, IconButton, Tooltip } from '@affine/component';
|
||||||
|
import { Filters } from '@affine/core/components/filter';
|
||||||
import type { AllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
import type { AllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||||
import {
|
import {
|
||||||
AffineShapeIcon,
|
AffineShapeIcon,
|
||||||
FilterList,
|
|
||||||
filterPageByRules,
|
|
||||||
List,
|
List,
|
||||||
type ListItem,
|
type ListItem,
|
||||||
ListScrollContainer,
|
ListScrollContainer,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
|
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||||
|
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import {
|
import {
|
||||||
@@ -19,11 +18,11 @@ import {
|
|||||||
PageIcon,
|
PageIcon,
|
||||||
ToggleRightIcon,
|
ToggleRightIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './edit-collection.css';
|
import * as styles from './edit-collection.css';
|
||||||
|
|
||||||
@@ -35,8 +34,8 @@ export const RulesMode = ({
|
|||||||
switchMode,
|
switchMode,
|
||||||
allPageListConfig,
|
allPageListConfig,
|
||||||
}: {
|
}: {
|
||||||
collection: Collection;
|
collection: CollectionInfo;
|
||||||
updateCollection: (collection: Collection) => void;
|
updateCollection: (collection: CollectionInfo) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
buttons: ReactNode;
|
buttons: ReactNode;
|
||||||
switchMode: ReactNode;
|
switchMode: ReactNode;
|
||||||
@@ -44,30 +43,50 @@ export const RulesMode = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [showPreview, setShowPreview] = useState(true);
|
const [showPreview, setShowPreview] = useState(true);
|
||||||
const allowListPages: DocMeta[] = [];
|
|
||||||
const rulesPages: DocMeta[] = [];
|
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
const favorites = useLiveData(favAdapter.favorites$);
|
const [rulesPageIds, setRulesPageIds] = useState<string[]>([]);
|
||||||
allPageListConfig.allPages.forEach(meta => {
|
|
||||||
if (meta.trash) {
|
useEffect(() => {
|
||||||
return;
|
const subscription = collectionRulesService
|
||||||
}
|
.watch(
|
||||||
const pageData = {
|
collection.rules.filters.length > 0
|
||||||
meta,
|
? [
|
||||||
publicMode: allPageListConfig.getPublicMode(meta.id),
|
...collection.rules.filters,
|
||||||
favorite: favorites.some(f => f.id === meta.id),
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
.subscribe(rules => {
|
||||||
|
setRulesPageIds(rules.groups.flatMap(group => group.items));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
if (
|
}, [collection, collectionRulesService]);
|
||||||
collection.filterList.length &&
|
|
||||||
filterPageByRules(collection.filterList, [], pageData)
|
const rulesPages = useMemo(() => {
|
||||||
) {
|
return allPageListConfig.allPages.filter(meta => {
|
||||||
rulesPages.push(meta);
|
return rulesPageIds.includes(meta.id);
|
||||||
}
|
});
|
||||||
if (collection.allowList.includes(meta.id)) {
|
}, [allPageListConfig.allPages, rulesPageIds]);
|
||||||
allowListPages.push(meta);
|
|
||||||
}
|
const allowListPages = useMemo(() => {
|
||||||
});
|
return allPageListConfig.allPages.filter(meta => {
|
||||||
|
return (
|
||||||
|
collection.allowList.includes(meta.id) &&
|
||||||
|
!rulesPageIds.includes(meta.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
|
||||||
|
|
||||||
const [expandInclude, setExpandInclude] = useState(
|
const [expandInclude, setExpandInclude] = useState(
|
||||||
collection.allowList.length > 0
|
collection.allowList.length > 0
|
||||||
);
|
);
|
||||||
@@ -113,13 +132,17 @@ export const RulesMode = ({
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FilterList
|
<Filters
|
||||||
propertiesMeta={allPageListConfig.docCollection.meta.properties}
|
filters={collection.rules.filters}
|
||||||
value={collection.filterList}
|
onChange={filters => {
|
||||||
onChange={useCallback(
|
updateCollection({
|
||||||
filterList => updateCollection({ ...collection, filterList }),
|
...collection,
|
||||||
[collection, updateCollection]
|
rules: {
|
||||||
)}
|
...collection.rules,
|
||||||
|
filters,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles.rulesContainerLeftContentInclude}>
|
<div className={styles.rulesContainerLeftContentInclude}>
|
||||||
{collection.allowList.length > 0 ? (
|
{collection.allowList.length > 0 ? (
|
||||||
@@ -215,7 +238,7 @@ export const RulesMode = ({
|
|||||||
></List>
|
></List>
|
||||||
) : (
|
) : (
|
||||||
<RulesEmpty
|
<RulesEmpty
|
||||||
noRules={collection.filterList.length === 0}
|
noRules={collection.rules.filters.length === 0}
|
||||||
fullHeight={allowListPages.length === 0}
|
fullHeight={allowListPages.length === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { Modal, toast } from '@affine/component';
|
|||||||
import {
|
import {
|
||||||
collectionHeaderColsDef,
|
collectionHeaderColsDef,
|
||||||
CollectionListItemRenderer,
|
CollectionListItemRenderer,
|
||||||
type CollectionMeta,
|
|
||||||
FavoriteTag,
|
FavoriteTag,
|
||||||
type ListItem,
|
type ListItem,
|
||||||
ListTableHeader,
|
ListTableHeader,
|
||||||
VirtualizedList,
|
VirtualizedList,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { SelectorLayout } from '@affine/core/components/page-list/selector/selector-layout';
|
import { SelectorLayout } from '@affine/core/components/page-list/selector/selector-layout';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import {
|
||||||
|
type CollectionMeta,
|
||||||
|
CollectionService,
|
||||||
|
} from '@affine/core/modules/collection';
|
||||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
@@ -52,22 +54,15 @@ export const CollectionSelectorDialog = ({
|
|||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collections = useLiveData(collectionService.collectionMetas$);
|
||||||
const [selection, setSelection] = useState(selectedCollectionIds);
|
const [selection, setSelection] = useState(selectedCollectionIds);
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
|
|
||||||
const collectionMetas = useMemo(() => {
|
const collectionMetas = useMemo(() => {
|
||||||
const collectionsList: CollectionMeta[] = collections
|
const collectionsList: CollectionMeta[] = collections.filter(meta => {
|
||||||
.map(collection => {
|
const reg = new RegExp(keyword, 'i');
|
||||||
return {
|
return reg.test(meta.title);
|
||||||
...collection,
|
});
|
||||||
title: collection.name,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(meta => {
|
|
||||||
const reg = new RegExp(keyword, 'i');
|
|
||||||
return reg.test(meta.title);
|
|
||||||
});
|
|
||||||
return collectionsList;
|
return collectionsList;
|
||||||
}, [collections, keyword]);
|
}, [collections, keyword]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { usePromptModal } from '@affine/component';
|
import { usePromptModal } from '@affine/component';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
import type { CollectionMeta } from '@affine/core/components/page-list';
|
|
||||||
import {
|
import {
|
||||||
CollectionListHeader,
|
CollectionListHeader,
|
||||||
createEmptyCollection,
|
|
||||||
VirtualizedCollectionList,
|
VirtualizedCollectionList,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +11,7 @@ import {
|
|||||||
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 { nanoid } from 'nanoid';
|
import { useCallback, useState } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { CollectionService } from '../../../../modules/collection';
|
import { CollectionService } from '../../../../modules/collection';
|
||||||
import { ViewBody, ViewHeader } from '../../../../modules/workbench';
|
import { ViewBody, ViewHeader } from '../../../../modules/workbench';
|
||||||
@@ -31,16 +28,6 @@ export const AllCollection = () => {
|
|||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collections = useLiveData(collectionService.collections$);
|
||||||
|
|
||||||
const collectionMetas = useMemo(() => {
|
|
||||||
const collectionsList: CollectionMeta[] = collections.map(collection => {
|
|
||||||
return {
|
|
||||||
...collection,
|
|
||||||
title: collection.name,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return collectionsList;
|
|
||||||
}, [collections]);
|
|
||||||
|
|
||||||
const navigateHelper = useNavigateHelper();
|
const navigateHelper = useNavigateHelper();
|
||||||
const { openPromptModal } = usePromptModal();
|
const { openPromptModal } = usePromptModal();
|
||||||
|
|
||||||
@@ -62,8 +49,7 @@ export const AllCollection = () => {
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
const id = nanoid();
|
const id = collectionService.createCollection({ name });
|
||||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
|
||||||
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -87,10 +73,8 @@ export const AllCollection = () => {
|
|||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{collectionMetas.length > 0 ? (
|
{collections.size > 0 ? (
|
||||||
<VirtualizedCollectionList
|
<VirtualizedCollectionList
|
||||||
collections={collections}
|
|
||||||
collectionMetas={collectionMetas}
|
|
||||||
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
||||||
handleCreateCollection={handleCreateCollection}
|
handleCreateCollection={handleCreateCollection}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { CollectionService } from '@affine/core/modules/collection';
|
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
|
||||||
import { useService } from '@toeverything/infra';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { filterContainerStyle } from '../../../../components/filter-container.css';
|
|
||||||
import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper';
|
|
||||||
import {
|
|
||||||
FilterList,
|
|
||||||
SaveAsCollectionButton,
|
|
||||||
} from '../../../../components/page-list';
|
|
||||||
|
|
||||||
export const FilterContainer = ({
|
|
||||||
filters,
|
|
||||||
onChangeFilters,
|
|
||||||
}: {
|
|
||||||
filters: Filter[];
|
|
||||||
onChangeFilters: (filters: Filter[]) => void;
|
|
||||||
}) => {
|
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
|
||||||
const navigateHelper = useNavigateHelper();
|
|
||||||
const collectionService = useService(CollectionService);
|
|
||||||
const saveToCollection = useCallback(
|
|
||||||
(collection: Collection) => {
|
|
||||||
collectionService.addCollection({
|
|
||||||
...collection,
|
|
||||||
filterList: filters,
|
|
||||||
});
|
|
||||||
navigateHelper.jumpToCollection(currentWorkspace.id, collection.id);
|
|
||||||
},
|
|
||||||
[collectionService, filters, navigateHelper, currentWorkspace.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!filters.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={filterContainerStyle}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<FilterList
|
|
||||||
propertiesMeta={currentWorkspace.docCollection.meta.properties}
|
|
||||||
value={filters}
|
|
||||||
onChange={onChangeFilters}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{filters.length > 0 ? (
|
|
||||||
<SaveAsCollectionButton onConfirm={saveToCollection} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
import {
|
import {
|
||||||
AllPageListOperationsMenu,
|
|
||||||
PageDisplayMenu,
|
PageDisplayMenu,
|
||||||
PageListNewPageButton,
|
PageListNewPageButton,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
@@ -10,7 +9,6 @@ import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
|||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { inferOpenMode } from '@affine/core/utils';
|
import { inferOpenMode } from '@affine/core/utils';
|
||||||
import type { Filter } from '@affine/env/filter';
|
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { useServices } from '@toeverything/infra';
|
import { useServices } from '@toeverything/infra';
|
||||||
@@ -21,12 +19,8 @@ import * as styles from './all-page.css';
|
|||||||
|
|
||||||
export const AllPageHeader = ({
|
export const AllPageHeader = ({
|
||||||
showCreateNew,
|
showCreateNew,
|
||||||
filters,
|
|
||||||
onChangeFilters,
|
|
||||||
}: {
|
}: {
|
||||||
showCreateNew: boolean;
|
showCreateNew: boolean;
|
||||||
filters: Filter[];
|
|
||||||
onChangeFilters: (filters: Filter[]) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { workspaceService, workspaceDialogService, workbenchService } =
|
const { workspaceService, workspaceDialogService, workbenchService } =
|
||||||
useServices({
|
useServices({
|
||||||
@@ -90,11 +84,6 @@ export const AllPageHeader = ({
|
|||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</PageListNewPageButton>
|
</PageListNewPageButton>
|
||||||
<AllPageListOperationsMenu
|
|
||||||
filterList={filters}
|
|
||||||
onChangeFilterList={onChangeFilters}
|
|
||||||
propertiesMeta={workspace.docCollection.meta.properties}
|
|
||||||
/>
|
|
||||||
<PageDisplayMenu />
|
<PageDisplayMenu />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
import {
|
import {
|
||||||
PageListHeader,
|
PageListHeader,
|
||||||
useFilteredPageMetas,
|
|
||||||
VirtualizedPageList,
|
VirtualizedPageList,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { IntegrationService } from '@affine/core/modules/integration';
|
import { IntegrationService } from '@affine/core/modules/integration';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Filter } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsActiveView,
|
useIsActiveView,
|
||||||
@@ -23,7 +21,6 @@ import {
|
|||||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||||
import { EmptyPageList } from '../page-list-empty';
|
import { EmptyPageList } from '../page-list-empty';
|
||||||
import * as styles from './all-page.css';
|
import * as styles from './all-page.css';
|
||||||
import { FilterContainer } from './all-page-filter';
|
|
||||||
import { AllPageHeader } from './all-page-header';
|
import { AllPageHeader } from './all-page-header';
|
||||||
|
|
||||||
export const AllPage = () => {
|
export const AllPage = () => {
|
||||||
@@ -37,10 +34,10 @@ export const AllPage = () => {
|
|||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
const importing = useLiveData(integrationService.importing$);
|
const importing = useLiveData(integrationService.importing$);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<Filter[]>([]);
|
const filteredPageMetas = useMemo(
|
||||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
() => pageMetas.filter(page => !page.trash),
|
||||||
filters: filters,
|
[pageMetas]
|
||||||
});
|
);
|
||||||
|
|
||||||
const isActiveView = useIsActiveView();
|
const isActiveView = useIsActiveView();
|
||||||
|
|
||||||
@@ -66,20 +63,14 @@ export const AllPage = () => {
|
|||||||
<ViewTitle title={t['All pages']()} />
|
<ViewTitle title={t['All pages']()} />
|
||||||
<ViewIcon icon="allDocs" />
|
<ViewIcon icon="allDocs" />
|
||||||
<ViewHeader>
|
<ViewHeader>
|
||||||
<AllPageHeader
|
<AllPageHeader showCreateNew={!hideHeaderCreateNew} />
|
||||||
showCreateNew={!hideHeaderCreateNew}
|
|
||||||
filters={filters}
|
|
||||||
onChangeFilters={setFilters}
|
|
||||||
/>
|
|
||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
|
||||||
{filteredPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
<VirtualizedPageList
|
<VirtualizedPageList
|
||||||
disableMultiDelete={!isAdmin && !isOwner}
|
disableMultiDelete={!isAdmin && !isOwner}
|
||||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||||
filters={filters}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyPageList type="all" heading={<PageListHeader />} />
|
<EmptyPageList type="all" heading={<PageListHeader />} />
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { notify } from '@affine/component';
|
|
||||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
|
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
|
||||||
import { VirtualizedPageList } from '@affine/core/components/page-list';
|
import { VirtualizedPageList } from '@affine/core/components/page-list';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import {
|
||||||
|
type Collection,
|
||||||
|
CollectionService,
|
||||||
|
} from '@affine/core/modules/collection';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
ViewIcon,
|
ViewIcon,
|
||||||
ViewTitle,
|
ViewTitle,
|
||||||
} from '../../../../modules/workbench';
|
} from '../../../../modules/workbench';
|
||||||
|
import { PageNotFound } from '../../404';
|
||||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||||
import { CollectionDetailHeader } from './header';
|
import { CollectionDetailHeader } from './header';
|
||||||
|
|
||||||
@@ -68,30 +70,16 @@ export const Component = function CollectionPage() {
|
|||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
});
|
});
|
||||||
const globalContext = globalContextService.globalContext;
|
const globalContext = globalContextService.globalContext;
|
||||||
|
const t = useI18n();
|
||||||
const collections = useLiveData(collectionService.collections$);
|
|
||||||
const navigate = useNavigateHelper();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const collection = useLiveData(
|
||||||
const collection = collections.find(v => v.id === params.collectionId);
|
params.collectionId
|
||||||
|
? collectionService.collection$(params.collectionId)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const name = useLiveData(collection?.name$);
|
||||||
const isActiveView = useIsActiveView();
|
const isActiveView = useIsActiveView();
|
||||||
|
|
||||||
const notifyCollectionDeleted = useCallback(() => {
|
|
||||||
navigate.jumpToPage(workspace.id, 'all');
|
|
||||||
const collection = collectionService.collectionsTrash$.value.find(
|
|
||||||
v => v.collection.id === params.collectionId
|
|
||||||
);
|
|
||||||
let text = 'Collection does not exist';
|
|
||||||
if (collection) {
|
|
||||||
if (collection.userId) {
|
|
||||||
text = `${collection.collection.name} has been deleted by ${collection.userName}`;
|
|
||||||
} else {
|
|
||||||
text = `${collection.collection.name} has been deleted`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return notify.error({ title: text });
|
|
||||||
}, [collectionService, navigate, params.collectionId, workspace.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActiveView && collection) {
|
if (isActiveView && collection) {
|
||||||
globalContext.collectionId.set(collection.id);
|
globalContext.collectionId.set(collection.id);
|
||||||
@@ -105,25 +93,22 @@ export const Component = function CollectionPage() {
|
|||||||
return;
|
return;
|
||||||
}, [collection, globalContext, isActiveView]);
|
}, [collection, globalContext, isActiveView]);
|
||||||
|
|
||||||
useEffect(() => {
|
const info = useLiveData(collection?.info$);
|
||||||
if (!collection) {
|
|
||||||
notifyCollectionDeleted();
|
|
||||||
}
|
|
||||||
}, [collection, notifyCollectionDeleted]);
|
|
||||||
|
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return null;
|
return <PageNotFound />;
|
||||||
}
|
}
|
||||||
const inner = isEmptyCollection(collection) ? (
|
const inner =
|
||||||
<Placeholder collection={collection} />
|
info?.allowList.length === 0 && info?.rules.filters.length === 0 ? (
|
||||||
) : (
|
<Placeholder collection={collection} />
|
||||||
<CollectionDetail collection={collection} />
|
) : (
|
||||||
);
|
<CollectionDetail collection={collection} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewIcon icon="collection" />
|
<ViewIcon icon="collection" />
|
||||||
<ViewTitle title={collection.name} />
|
<ViewTitle title={name ?? t['Untitled']()} />
|
||||||
<AllDocSidebarTabs />
|
<AllDocSidebarTabs />
|
||||||
{inner}
|
{inner}
|
||||||
</>
|
</>
|
||||||
@@ -134,6 +119,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const { jumpToCollections } = useNavigateHelper();
|
const { jumpToCollections } = useNavigateHelper();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const name = useLiveData(collection?.name$);
|
||||||
|
|
||||||
const handleJumpToCollections = useCallback(() => {
|
const handleJumpToCollections = useCallback(() => {
|
||||||
jumpToCollections(workspace.id);
|
jumpToCollections(workspace.id);
|
||||||
@@ -176,7 +162,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
['WebkitAppRegion' as string]: 'no-drag',
|
['WebkitAppRegion' as string]: 'no-drag',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collection.name}
|
{name ?? t['Untitled']()}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -190,9 +176,3 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isEmptyCollection = (collection: Collection) => {
|
|
||||||
return (
|
|
||||||
collection.allowList.length === 0 && collection.filterList.length === 0
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||||
import {
|
import { VirtualizedTrashList } from '@affine/core/components/page-list';
|
||||||
useFilteredPageMetas,
|
|
||||||
VirtualizedTrashList,
|
|
||||||
} from '@affine/core/components/page-list';
|
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { Header } from '@affine/core/components/pure/header';
|
||||||
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
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 { DeleteIcon } from '@blocksuite/icons/rc';
|
import { DeleteIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsActiveView,
|
useIsActiveView,
|
||||||
@@ -43,11 +41,15 @@ export const TrashPage = () => {
|
|||||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
const docCollection = currentWorkspace.docCollection;
|
const docCollection = currentWorkspace.docCollection;
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const allTrashPageIds = useLiveData(
|
||||||
|
LiveData.from(docsService.allTrashDocIds$(), [])
|
||||||
|
);
|
||||||
|
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
const filteredPageMetas = useMemo(() => {
|
||||||
trash: true,
|
return pageMetas.filter(page => allTrashPageIds.includes(page.id));
|
||||||
});
|
}, [pageMetas, allTrashPageIds]);
|
||||||
|
|
||||||
const isActiveView = useIsActiveView();
|
const isActiveView = useIsActiveView();
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { MenuItem, notify } from '@affine/component';
|
import { MenuItem, notify } from '@affine/component';
|
||||||
import { filterPage } from '@affine/core/components/page-list';
|
|
||||||
import type { NodeOperation } from '@affine/core/desktop/components/navigation-panel';
|
import type { NodeOperation } from '@affine/core/desktop/components/navigation-panel';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import {
|
||||||
|
type Collection,
|
||||||
|
CollectionService,
|
||||||
|
} from '@affine/core/modules/collection';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import { PublicDocMode } from '@affine/graphql';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
|
||||||
import { FilterMinusIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { FilterMinusIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
import { LiveData, useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||||
@@ -46,6 +43,7 @@ export const NavigationPanelCollectionNode = ({
|
|||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
|
|
||||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||||
|
const name = useLiveData(collection?.name$);
|
||||||
|
|
||||||
const handleOpenCollapsed = useCallback(() => {
|
const handleOpenCollapsed = useCallback(() => {
|
||||||
setCollapsed(false);
|
setCollapsed(false);
|
||||||
@@ -86,7 +84,7 @@ export const NavigationPanelCollectionNode = ({
|
|||||||
return (
|
return (
|
||||||
<NavigationPanelTreeNode
|
<NavigationPanelTreeNode
|
||||||
icon={CollectionIcon}
|
icon={CollectionIcon}
|
||||||
name={collection.name || t['Untitled']()}
|
name={name || t['Untitled']()}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
to={`/collection/${collection.id}`}
|
to={`/collection/${collection.id}`}
|
||||||
@@ -110,14 +108,7 @@ const NavigationPanelCollectionNodeChildren = ({
|
|||||||
onAddDoc?: () => void;
|
onAddDoc?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const {
|
const { shareDocsListService, collectionService } = useServices({
|
||||||
docsService,
|
|
||||||
compatibleFavoriteItemsAdapter,
|
|
||||||
shareDocsListService,
|
|
||||||
collectionService,
|
|
||||||
} = useServices({
|
|
||||||
DocsService,
|
|
||||||
CompatibleFavoriteItemsAdapter,
|
|
||||||
ShareDocsListService,
|
ShareDocsListService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
});
|
});
|
||||||
@@ -127,28 +118,12 @@ const NavigationPanelCollectionNodeChildren = ({
|
|||||||
shareDocsListService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocsListService]);
|
}, [shareDocsListService]);
|
||||||
|
|
||||||
const docMetas = useLiveData(
|
const allowList = useLiveData(collection.allowList$);
|
||||||
useMemo(
|
|
||||||
() =>
|
|
||||||
LiveData.computed(get => {
|
|
||||||
return get(docsService.list.docs$).map(
|
|
||||||
doc => get(doc.meta$) as DocMeta
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[docsService]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
|
||||||
const allowList = useMemo(
|
|
||||||
() => new Set(collection.allowList),
|
|
||||||
[collection.allowList]
|
|
||||||
);
|
|
||||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
|
||||||
|
|
||||||
const handleRemoveFromAllowList = useCallback(
|
const handleRemoveFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
track.$.navigationPanel.collections.removeOrganizeItem({ type: 'doc' });
|
track.$.navigationPanel.collections.removeOrganizeItem({ type: 'doc' });
|
||||||
collectionService.deletePageFromCollection(collection.id, id);
|
collectionService.removeDocFromCollection(collection.id, id);
|
||||||
notify.success({
|
notify.success({
|
||||||
message: t['com.affine.collection.removePage.success'](),
|
message: t['com.affine.collection.removePage.success'](),
|
||||||
});
|
});
|
||||||
@@ -156,28 +131,22 @@ const NavigationPanelCollectionNodeChildren = ({
|
|||||||
[collection.id, collectionService, t]
|
[collection.id, collectionService, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filtered = docMetas.filter(meta => {
|
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
|
||||||
if (meta.trash) return false;
|
|
||||||
const publicMode = shareDocs?.find(d => d.id === meta.id)?.mode;
|
useEffect(() => {
|
||||||
const pageData = {
|
const subscription = collection.watch().subscribe(docIds => {
|
||||||
meta: meta as DocMeta,
|
setFilteredDocIds(docIds);
|
||||||
publicMode:
|
});
|
||||||
publicMode === PublicDocMode.Edgeless
|
|
||||||
? ('edgeless' as const)
|
return () => subscription.unsubscribe();
|
||||||
: publicMode === PublicDocMode.Page
|
}, [collection]);
|
||||||
? ('page' as const)
|
|
||||||
: undefined,
|
|
||||||
favorite: favourites.some(fav => fav.id === meta.id),
|
|
||||||
};
|
|
||||||
return filterPage(collection, pageData);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{filtered.map(doc => (
|
{filteredDocIds.map(docId => (
|
||||||
<NavigationPanelDocNode
|
<NavigationPanelDocNode
|
||||||
key={doc.id}
|
key={docId}
|
||||||
docId={doc.id}
|
docId={docId}
|
||||||
operations={
|
operations={
|
||||||
allowList
|
allowList
|
||||||
? [
|
? [
|
||||||
@@ -186,7 +155,7 @@ const NavigationPanelCollectionNodeChildren = ({
|
|||||||
view: (
|
view: (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<FilterMinusIcon />}
|
prefixIcon={<FilterMinusIcon />}
|
||||||
onClick={() => handleRemoveFromAllowList(doc.id)}
|
onClick={() => handleRemoveFromAllowList(docId)}
|
||||||
>
|
>
|
||||||
{t['Remove special filter']()}
|
{t['Remove special filter']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
import type { NodeOperation } from '@affine/core/desktop/components/navigation-panel';
|
import type { NodeOperation } from '@affine/core/desktop/components/navigation-panel';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
@@ -44,7 +43,6 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
CollectionService,
|
CollectionService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
});
|
});
|
||||||
const deleteInfo = useDeleteCollectionInfo();
|
|
||||||
|
|
||||||
const { createPage } = usePageHelper(
|
const { createPage } = usePageHelper(
|
||||||
workspaceService.workspace.docCollection
|
workspaceService.workspace.docCollection
|
||||||
@@ -61,7 +59,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
|
|
||||||
const createAndAddDocument = useCallback(() => {
|
const createAndAddDocument = useCallback(() => {
|
||||||
const newDoc = createPage();
|
const newDoc = createPage();
|
||||||
collectionService.addPageToCollection(collectionId, newDoc.id);
|
collectionService.addDocToCollection(collectionId, newDoc.id);
|
||||||
track.$.navigationPanel.collections.createDoc();
|
track.$.navigationPanel.collections.createDoc();
|
||||||
track.$.navigationPanel.collections.addDocToCollection({
|
track.$.navigationPanel.collections.addDocToCollection({
|
||||||
control: 'button',
|
control: 'button',
|
||||||
@@ -102,11 +100,11 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
}, [collectionId, workbenchService.workbench]);
|
}, [collectionId, workbenchService.workbench]);
|
||||||
|
|
||||||
const handleDeleteCollection = useCallback(() => {
|
const handleDeleteCollection = useCallback(() => {
|
||||||
collectionService.deleteCollection(deleteInfo, collectionId);
|
collectionService.deleteCollection(collectionId);
|
||||||
track.$.navigationPanel.organize.deleteOrganizeItem({
|
track.$.navigationPanel.organize.deleteOrganizeItem({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
});
|
});
|
||||||
}, [collectionId, collectionService, deleteInfo]);
|
}, [collectionId, collectionService]);
|
||||||
|
|
||||||
const handleShowEdit = useCallback(() => {
|
const handleShowEdit = useCallback(() => {
|
||||||
onOpenEdit();
|
onOpenEdit();
|
||||||
@@ -115,11 +113,10 @@ export const useNavigationPanelCollectionNodeOperations = (
|
|||||||
const handleRename = useCallback(
|
const handleRename = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
const collection = collectionService.collection$(collectionId).value;
|
const collection = collectionService.collection$(collectionId).value;
|
||||||
if (collection && collection.name !== name) {
|
if (collection && collection.name$.value !== name) {
|
||||||
collectionService.updateCollection(collectionId, () => ({
|
collectionService.updateCollection(collectionId, {
|
||||||
...collection,
|
|
||||||
name,
|
name,
|
||||||
}));
|
});
|
||||||
|
|
||||||
track.$.navigationPanel.organize.renameOrganizeItem({
|
track.$.navigationPanel.organize.renameOrganizeItem({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { usePromptModal } from '@affine/component';
|
import { usePromptModal } from '@affine/component';
|
||||||
import { createEmptyCollection } from '@affine/core/components/page-list/use-collection-manager';
|
|
||||||
import { NavigationPanelTreeRoot } from '@affine/core/desktop/components/navigation-panel';
|
import { NavigationPanelTreeRoot } from '@affine/core/desktop/components/navigation-panel';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||||
@@ -8,7 +7,6 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||||
@@ -25,7 +23,7 @@ export const NavigationPanelCollections = () => {
|
|||||||
NavigationPanelService,
|
NavigationPanelService,
|
||||||
});
|
});
|
||||||
const navigationPanelSection = navigationPanelService.sections.collections;
|
const navigationPanelSection = navigationPanelService.sections.collections;
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collectionMetas = useLiveData(collectionService.collectionMetas$);
|
||||||
const { openPromptModal } = usePromptModal();
|
const { openPromptModal } = usePromptModal();
|
||||||
|
|
||||||
const handleCreateCollection = useCallback(() => {
|
const handleCreateCollection = useCallback(() => {
|
||||||
@@ -46,8 +44,7 @@ export const NavigationPanelCollections = () => {
|
|||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
},
|
},
|
||||||
onConfirm(name) {
|
onConfirm(name) {
|
||||||
const id = nanoid();
|
const id = collectionService.createCollection({ name });
|
||||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
|
||||||
track.$.navigationPanel.organize.createOrganizeItem({
|
track.$.navigationPanel.organize.createOrganizeItem({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
});
|
});
|
||||||
@@ -70,7 +67,7 @@ export const NavigationPanelCollections = () => {
|
|||||||
title={t['com.affine.rootAppSidebar.collections']()}
|
title={t['com.affine.rootAppSidebar.collections']()}
|
||||||
>
|
>
|
||||||
<NavigationPanelTreeRoot>
|
<NavigationPanelTreeRoot>
|
||||||
{collections.map(collection => (
|
{collectionMetas.map(collection => (
|
||||||
<NavigationPanelCollectionNode
|
<NavigationPanelCollectionNode
|
||||||
key={collection.id}
|
key={collection.id}
|
||||||
collectionId={collection.id}
|
collectionId={collection.id}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const CollectionSelectorDialog = ({
|
|||||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['collection-selector']>) => {
|
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['collection-selector']>) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collections = useLiveData(collectionService.collectionMetas$);
|
||||||
|
|
||||||
const list = useMemo(() => {
|
const list = useMemo(() => {
|
||||||
return collections.map(collection => ({
|
return collections.map(collection => ({
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
import { notify, useThemeColorV2 } from '@affine/component';
|
import { useThemeColorV2 } from '@affine/component';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { CollectionDetail } from '../../../views';
|
import { CollectionDetail } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
useThemeColorV2('layer/background/mobile/primary');
|
useThemeColorV2('layer/background/mobile/primary');
|
||||||
const { collectionService, globalContextService, workspaceService } =
|
const { collectionService, globalContextService } = useServices({
|
||||||
useServices({
|
CollectionService,
|
||||||
WorkspaceService,
|
GlobalContextService,
|
||||||
CollectionService,
|
});
|
||||||
GlobalContextService,
|
|
||||||
});
|
|
||||||
|
|
||||||
const globalContext = globalContextService.globalContext;
|
const globalContext = globalContextService.globalContext;
|
||||||
const collections = useLiveData(collectionService.collections$);
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigateHelper();
|
const collection = useLiveData(
|
||||||
const workspace = workspaceService.workspace;
|
params.collectionId
|
||||||
const collection = collections.find(v => v.id === params.collectionId);
|
? collectionService.collection$(params.collectionId)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (collection) {
|
if (collection) {
|
||||||
@@ -38,30 +35,9 @@ export const Component = () => {
|
|||||||
return;
|
return;
|
||||||
}, [collection, globalContext]);
|
}, [collection, globalContext]);
|
||||||
|
|
||||||
const notifyCollectionDeleted = useCallback(() => {
|
|
||||||
navigate.jumpToPage(workspace.id, 'home');
|
|
||||||
const collection = collectionService.collectionsTrash$.value.find(
|
|
||||||
v => v.collection.id === params.collectionId
|
|
||||||
);
|
|
||||||
let text = 'Collection does not exist';
|
|
||||||
if (collection) {
|
|
||||||
if (collection.userId) {
|
|
||||||
text = `${collection.collection.name} has been deleted by ${collection.userName}`;
|
|
||||||
} else {
|
|
||||||
text = `${collection.collection.name} has been deleted`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return notify.error({ title: text });
|
|
||||||
}, [collectionService, navigate, params.collectionId, workspace.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!collection) {
|
|
||||||
notifyCollectionDeleted();
|
|
||||||
}
|
|
||||||
}, [collection, notifyCollectionDeleted]);
|
|
||||||
|
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return null;
|
// TODO: implement 404 page
|
||||||
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CollectionDetail collection={collection} />;
|
return <CollectionDetail collection={collection} />;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const RecentList = () => {
|
|||||||
TagService,
|
TagService,
|
||||||
});
|
});
|
||||||
const recentDocsList = useLiveData(mobileSearchService.recentDocs.items$);
|
const recentDocsList = useLiveData(mobileSearchService.recentDocs.items$);
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collectionMetas = useLiveData(collectionService.collectionMetas$);
|
||||||
const tags = useLiveData(
|
const tags = useLiveData(
|
||||||
LiveData.computed(get =>
|
LiveData.computed(get =>
|
||||||
get(tagService.tagList.tags$).map(tag => ({
|
get(tagService.tagList.tags$).map(tag => ({
|
||||||
@@ -63,7 +63,7 @@ const RecentList = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const collectionList = useMemo(() => {
|
const collectionList = useMemo(() => {
|
||||||
return collections.slice(0, 3).map(item => {
|
return collectionMetas.slice(0, 3).map(item => {
|
||||||
return {
|
return {
|
||||||
id: 'collection:' + item.id,
|
id: 'collection:' + item.id,
|
||||||
source: 'collection',
|
source: 'collection',
|
||||||
@@ -72,7 +72,7 @@ const RecentList = () => {
|
|||||||
payload: { collectionId: item.id },
|
payload: { collectionId: item.id },
|
||||||
} satisfies QuickSearchItem<'collection', { collectionId: string }>;
|
} satisfies QuickSearchItem<'collection', { collectionId: string }>;
|
||||||
});
|
});
|
||||||
}, [collections]);
|
}, [collectionMetas]);
|
||||||
|
|
||||||
const tagList = useMemo(() => {
|
const tagList = useMemo(() => {
|
||||||
return tags
|
return tags
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
||||||
import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection';
|
import { PageHeader } from '@affine/core/mobile/components';
|
||||||
import { AppTabs, PageHeader } from '@affine/core/mobile/components';
|
|
||||||
import { Page } from '@affine/core/mobile/components/page';
|
import { Page } from '@affine/core/mobile/components/page';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/core/modules/collection';
|
||||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
import { AllDocList } from '../doc/list';
|
import { AllDocList } from '../doc/list';
|
||||||
import * as styles from './detail.css';
|
import * as styles from './detail.css';
|
||||||
|
|
||||||
export const DetailHeader = ({ collection }: { collection: Collection }) => {
|
export const DetailHeader = ({ collection }: { collection: Collection }) => {
|
||||||
|
const name = useLiveData(collection.name$);
|
||||||
return (
|
return (
|
||||||
<PageHeader className={styles.header} back>
|
<PageHeader className={styles.header} back>
|
||||||
<div className={styles.headerContent}>
|
<div className={styles.headerContent}>
|
||||||
<ViewLayersIcon className={styles.headerIcon} />
|
<ViewLayersIcon className={styles.headerIcon} />
|
||||||
{collection.name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
@@ -24,13 +25,14 @@ export const CollectionDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
}) => {
|
}) => {
|
||||||
if (isEmptyCollection(collection)) {
|
const info = useLiveData(collection.info$);
|
||||||
|
if (info.allowList.length === 0 && info.rules.filters.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Page header={<DetailHeader collection={collection} />}>
|
||||||
<DetailHeader collection={collection} />
|
<div style={{ flexGrow: 1 }}>
|
||||||
<EmptyCollectionDetail collection={collection} absoluteCenter />
|
<EmptyCollectionDetail collection={collection} absoluteCenter />
|
||||||
<AppTabs />
|
</div>
|
||||||
</>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/core/modules/collection';
|
||||||
|
|
||||||
import { DetailHeader } from './detail';
|
import { DetailHeader } from './detail';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
import type { CollectionMeta } from '@affine/core/components/page-list';
|
|
||||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||||
|
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { EmptyCollections } from '@affine/core/components/affine/empty';
|
import { EmptyCollections } from '@affine/core/components/affine/empty';
|
||||||
import type { CollectionMeta } from '@affine/core/components/page-list';
|
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { CollectionListItem } from './item';
|
import { CollectionListItem } from './item';
|
||||||
import { list } from './styles.css';
|
import { list } from './styles.css';
|
||||||
|
|
||||||
export const CollectionList = () => {
|
export const CollectionList = () => {
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collectionMetas = useLiveData(collectionService.collectionMetas$);
|
||||||
|
|
||||||
const collectionMetas = useMemo(
|
|
||||||
() =>
|
|
||||||
collections.map(
|
|
||||||
collection =>
|
|
||||||
({ ...collection, title: collection.name }) satisfies CollectionMeta
|
|
||||||
),
|
|
||||||
[collections]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!collectionMetas.length) {
|
if (!collectionMetas.length) {
|
||||||
return <EmptyCollections absoluteCenter />;
|
return <EmptyCollections absoluteCenter />;
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-su
|
|||||||
import {
|
import {
|
||||||
type ItemGroupProps,
|
type ItemGroupProps,
|
||||||
useAllDocDisplayProperties,
|
useAllDocDisplayProperties,
|
||||||
useFilteredPageMetas,
|
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
|
import type { Collection } from '@affine/core/modules/collection';
|
||||||
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { Collection, Filter } from '@affine/env/filter';
|
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { ToggleDownIcon } from '@blocksuite/icons/rc';
|
import { ToggleDownIcon } from '@blocksuite/icons/rc';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './list.css';
|
import * as styles from './list.css';
|
||||||
import { MasonryDocs } from './masonry';
|
import { MasonryDocs } from './masonry';
|
||||||
@@ -41,42 +41,53 @@ export const DocGroup = ({ group }: { group: ItemGroupProps<DocMeta> }) => {
|
|||||||
export interface AllDocListProps {
|
export interface AllDocListProps {
|
||||||
collection?: Collection;
|
collection?: Collection;
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
filters?: Filter[];
|
|
||||||
trash?: boolean;
|
trash?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AllDocList = ({
|
export const AllDocList = ({ trash, collection, tag }: AllDocListProps) => {
|
||||||
trash,
|
|
||||||
collection,
|
|
||||||
tag,
|
|
||||||
filters = [],
|
|
||||||
}: AllDocListProps) => {
|
|
||||||
const [properties] = useAllDocDisplayProperties();
|
const [properties] = useAllDocDisplayProperties();
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection);
|
const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection);
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
|
||||||
|
const allTrashPageIds = useLiveData(
|
||||||
|
LiveData.from(docsService.allTrashDocIds$(), [])
|
||||||
|
);
|
||||||
|
|
||||||
const tagPageIds = useLiveData(tag?.pageIds$);
|
const tagPageIds = useLiveData(tag?.pageIds$);
|
||||||
|
|
||||||
const filteredPageMetas = useFilteredPageMetas(allPageMetas, {
|
const [filteredPageIds, setFilteredPageIds] = useState<string[]>([]);
|
||||||
trash,
|
|
||||||
filters,
|
useEffect(() => {
|
||||||
collection,
|
const subscription = collection?.watch().subscribe(docIds => {
|
||||||
});
|
setFilteredPageIds(docIds);
|
||||||
|
});
|
||||||
|
return () => subscription?.unsubscribe();
|
||||||
|
}, [collection]);
|
||||||
|
|
||||||
const finalPageMetas = useMemo(() => {
|
const finalPageMetas = useMemo(() => {
|
||||||
|
const collectionFilteredPageMetas = collection
|
||||||
|
? allPageMetas.filter(page => filteredPageIds.includes(page.id))
|
||||||
|
: allPageMetas;
|
||||||
|
|
||||||
|
const filteredPageMetas = collectionFilteredPageMetas.filter(
|
||||||
|
page => allTrashPageIds.includes(page.id) === !!trash
|
||||||
|
);
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const pageIdsSet = new Set(tagPageIds);
|
const pageIdsSet = new Set(tagPageIds);
|
||||||
return filteredPageMetas.filter(page => pageIdsSet.has(page.id));
|
return filteredPageMetas.filter(page => pageIdsSet.has(page.id));
|
||||||
}
|
}
|
||||||
return filteredPageMetas;
|
return filteredPageMetas;
|
||||||
}, [filteredPageMetas, tag, tagPageIds]);
|
}, [
|
||||||
|
allPageMetas,
|
||||||
// const groupDefs =
|
allTrashPageIds,
|
||||||
// usePageItemGroupDefinitions() as ItemGroupDefinition<DocMeta>[];
|
collection,
|
||||||
|
filteredPageIds,
|
||||||
// const groups = useMemo(() => {
|
tag,
|
||||||
// return itemsToItemGroups(finalPageMetas ?? [], groupDefs);
|
tagPageIds,
|
||||||
// }, [finalPageMetas, groupDefs]);
|
trash,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!finalPageMetas.length) {
|
if (!finalPageMetas.length) {
|
||||||
return (
|
return (
|
||||||
@@ -87,14 +98,6 @@ export const AllDocList = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className={styles.groups}>
|
|
||||||
// {groups.map(group => (
|
|
||||||
// <DocGroup key={group.id} group={group} />
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MasonryDocs
|
<MasonryDocs
|
||||||
items={finalPageMetas}
|
items={finalPageMetas}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class CreatedByFilterProvider extends Service implements FilterProvider {
|
|||||||
filter$(params: FilterParams): Observable<Set<string>> {
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
const method = params.method as WorkspacePropertyFilter<'createdBy'>;
|
const method = params.method as WorkspacePropertyFilter<'createdBy'>;
|
||||||
if (method === 'include') {
|
if (method === 'include') {
|
||||||
const userIds = params.value?.split(',') ?? [];
|
const userIds = params.value?.split(',').filter(Boolean) ?? [];
|
||||||
|
|
||||||
return this.docsService.propertyValues$('createdBy').pipe(
|
return this.docsService.propertyValues$('createdBy').pipe(
|
||||||
map(o => {
|
map(o => {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import type { FavoriteService } from '@affine/core/modules/favorite';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
import { combineLatest, map, type Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import type { FilterProvider } from '../../provider';
|
||||||
|
import type { FilterParams } from '../../types';
|
||||||
|
|
||||||
|
export class FavoriteFilterProvider extends Service implements FilterProvider {
|
||||||
|
constructor(
|
||||||
|
private readonly favoriteService: FavoriteService,
|
||||||
|
private readonly docsService: DocsService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
|
const method = params.method;
|
||||||
|
if (method === 'is') {
|
||||||
|
return combineLatest([
|
||||||
|
this.favoriteService.favoriteList.list$,
|
||||||
|
this.docsService.allDocIds$(),
|
||||||
|
]).pipe(
|
||||||
|
map(([favoriteList, allDocIds]) => {
|
||||||
|
const favoriteDocIds = new Set<string>();
|
||||||
|
for (const { id, type } of favoriteList) {
|
||||||
|
if (type === 'doc') {
|
||||||
|
favoriteDocIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.value === 'true') {
|
||||||
|
return favoriteDocIds;
|
||||||
|
} else if (params.value === 'false') {
|
||||||
|
const notFavoriteDocIds = new Set<string>();
|
||||||
|
for (const id of allDocIds) {
|
||||||
|
if (!favoriteDocIds.has(id)) {
|
||||||
|
notFavoriteDocIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notFavoriteDocIds;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported value: ${params.value}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported method: ${params.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import type { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||||
|
import { onStart, Service } from '@toeverything/infra';
|
||||||
|
import { combineLatest, map, type Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import type { FilterProvider } from '../../provider';
|
||||||
|
import type { FilterParams } from '../../types';
|
||||||
|
|
||||||
|
export class SharedFilterProvider extends Service implements FilterProvider {
|
||||||
|
constructor(
|
||||||
|
private readonly shareDocsListService: ShareDocsListService,
|
||||||
|
private readonly docsService: DocsService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
|
const method = params.method;
|
||||||
|
if (method === 'is') {
|
||||||
|
return combineLatest([
|
||||||
|
this.shareDocsListService.shareDocs?.list$ ??
|
||||||
|
(of([]) as Observable<{ id: string }[]>),
|
||||||
|
this.docsService.allDocIds$(),
|
||||||
|
]).pipe(
|
||||||
|
onStart(() => {
|
||||||
|
this.shareDocsListService.shareDocs?.revalidate();
|
||||||
|
}),
|
||||||
|
map(([shareDocsList, allDocIds]) => {
|
||||||
|
const shareDocIds = new Set(shareDocsList.map(item => item.id));
|
||||||
|
if (params.value === 'true') {
|
||||||
|
return shareDocIds;
|
||||||
|
} else if (params.value === 'false') {
|
||||||
|
const notShareDocIds = new Set<string>();
|
||||||
|
for (const id of allDocIds) {
|
||||||
|
if (!shareDocIds.has(id)) {
|
||||||
|
notShareDocIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notShareDocIds;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported value: ${params.value}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported method: ${params.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { DocsService } from '@affine/core/modules/doc';
|
import type { DocsService } from '@affine/core/modules/doc';
|
||||||
import type { TagService } from '@affine/core/modules/tag';
|
import type { TagService } from '@affine/core/modules/tag';
|
||||||
|
import type { WorkspacePropertyFilter } from '@affine/core/modules/workspace-property';
|
||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
import { combineLatest, map, type Observable, of, switchMap } from 'rxjs';
|
import { combineLatest, map, type Observable, of, switchMap } from 'rxjs';
|
||||||
|
|
||||||
@@ -15,23 +16,66 @@ export class TagsFilterProvider extends Service implements FilterProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filter$(params: FilterParams): Observable<Set<string>> {
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
if (params.method === 'include') {
|
const method = params.method as WorkspacePropertyFilter<'tags'>;
|
||||||
const tagIds = params.value?.split(',') ?? [];
|
const tagIds = params.value?.split(',').filter(Boolean) ?? [];
|
||||||
|
const tags = tagIds.map(id => this.tagService.tagList.tagByTagId$(id));
|
||||||
const tags = tagIds.map(id => this.tagService.tagList.tagByTagId$(id));
|
if (method === 'include-all' || method === 'not-include-all') {
|
||||||
|
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
return of(new Set<string>());
|
return of(new Set<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineLatest(tags).pipe(
|
const includeDocIds$ = combineLatest(tags).pipe(
|
||||||
|
switchMap(tags =>
|
||||||
|
combineLatest(
|
||||||
|
tags
|
||||||
|
.filter(tag => tag !== undefined)
|
||||||
|
.map(tag => tag.pageIds$.map(ids => new Set(ids)))
|
||||||
|
).pipe(
|
||||||
|
map(pageIds =>
|
||||||
|
pageIds.reduce((acc, curr) => acc.intersection(curr))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (method === 'include-all') {
|
||||||
|
return includeDocIds$;
|
||||||
|
} else {
|
||||||
|
return combineLatest([
|
||||||
|
this.docsService.allDocIds$(),
|
||||||
|
includeDocIds$,
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([docIds, includeDocIds]) =>
|
||||||
|
new Set(docIds.filter(id => !includeDocIds.has(id)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (method === 'include-any-of' || method === 'not-include-any-of') {
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return of(new Set<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
const includeAnyOfDocIds$ = combineLatest(tags).pipe(
|
||||||
switchMap(tags =>
|
switchMap(tags =>
|
||||||
combineLatest(
|
combineLatest(
|
||||||
tags.filter(tag => tag !== undefined).map(tag => tag.pageIds$)
|
tags.filter(tag => tag !== undefined).map(tag => tag.pageIds$)
|
||||||
).pipe(map(pageIds => new Set(pageIds.flat())))
|
).pipe(map(pageIds => new Set(pageIds.flat())))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if (params.method === 'is-not-empty') {
|
if (method === 'include-any-of') {
|
||||||
|
return includeAnyOfDocIds$;
|
||||||
|
} else {
|
||||||
|
return combineLatest([
|
||||||
|
this.docsService.allDocIds$(),
|
||||||
|
includeAnyOfDocIds$,
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([docIds, includeAnyOfDocIds]) =>
|
||||||
|
new Set(docIds.filter(id => !includeAnyOfDocIds.has(id)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (method === 'is-not-empty') {
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
this.tagService.tagList.tags$.map(tags => new Set(tags.map(t => t.id))),
|
this.tagService.tagList.tags$.map(tags => new Set(tags.map(t => t.id))),
|
||||||
this.docsService.allDocsTagIds$(),
|
this.docsService.allDocsTagIds$(),
|
||||||
@@ -49,7 +93,7 @@ export class TagsFilterProvider extends Service implements FilterProvider {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if (params.method === 'is-empty') {
|
} else if (method === 'is-empty') {
|
||||||
return this.tagService.tagList.tags$
|
return this.tagService.tagList.tags$
|
||||||
.map(tags => new Set(tags.map(t => t.id)))
|
.map(tags => new Set(tags.map(t => t.id)))
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -70,6 +114,6 @@ export class TagsFilterProvider extends Service implements FilterProvider {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error(`Unsupported method: ${params.method}`);
|
throw new Error(`Unsupported method: ${method}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class UpdatedByFilterProvider extends Service implements FilterProvider {
|
|||||||
filter$(params: FilterParams): Observable<Set<string>> {
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
const method = params.method as WorkspacePropertyFilter<'updatedBy'>;
|
const method = params.method as WorkspacePropertyFilter<'updatedBy'>;
|
||||||
if (method === 'include') {
|
if (method === 'include') {
|
||||||
const userIds = params.value?.split(',') ?? [];
|
const userIds = params.value?.split(',').filter(Boolean) ?? [];
|
||||||
|
|
||||||
return this.docsService.propertyValues$('updatedBy').pipe(
|
return this.docsService.propertyValues$('updatedBy').pipe(
|
||||||
map(o => {
|
map(o => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Framework } from '@toeverything/infra';
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { DocsService } from '../doc';
|
import { DocsService } from '../doc';
|
||||||
|
import { FavoriteService } from '../favorite';
|
||||||
|
import { ShareDocsListService } from '../share-doc';
|
||||||
import { TagService } from '../tag';
|
import { TagService } from '../tag';
|
||||||
import { WorkspaceScope } from '../workspace';
|
import { WorkspaceScope } from '../workspace';
|
||||||
import { WorkspacePropertyService } from '../workspace-property';
|
import { WorkspacePropertyService } from '../workspace-property';
|
||||||
@@ -10,8 +12,10 @@ import { CreatedByFilterProvider } from './impls/filters/created-by';
|
|||||||
import { DatePropertyFilterProvider } from './impls/filters/date';
|
import { DatePropertyFilterProvider } from './impls/filters/date';
|
||||||
import { DocPrimaryModeFilterProvider } from './impls/filters/doc-primary-mode';
|
import { DocPrimaryModeFilterProvider } from './impls/filters/doc-primary-mode';
|
||||||
import { EmptyJournalFilterProvider } from './impls/filters/empty-journal';
|
import { EmptyJournalFilterProvider } from './impls/filters/empty-journal';
|
||||||
|
import { FavoriteFilterProvider } from './impls/filters/favorite';
|
||||||
import { JournalFilterProvider } from './impls/filters/journal';
|
import { JournalFilterProvider } from './impls/filters/journal';
|
||||||
import { PropertyFilterProvider } from './impls/filters/property';
|
import { PropertyFilterProvider } from './impls/filters/property';
|
||||||
|
import { SharedFilterProvider } from './impls/filters/shared';
|
||||||
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';
|
||||||
@@ -118,6 +122,14 @@ export function configureCollectionRulesModule(framework: Framework) {
|
|||||||
.impl(FilterProvider('system:empty-journal'), EmptyJournalFilterProvider, [
|
.impl(FilterProvider('system:empty-journal'), EmptyJournalFilterProvider, [
|
||||||
DocsService,
|
DocsService,
|
||||||
])
|
])
|
||||||
|
.impl(FilterProvider('system:favorite'), FavoriteFilterProvider, [
|
||||||
|
FavoriteService,
|
||||||
|
DocsService,
|
||||||
|
])
|
||||||
|
.impl(FilterProvider('system:shared'), SharedFilterProvider, [
|
||||||
|
ShareDocsListService,
|
||||||
|
DocsService,
|
||||||
|
])
|
||||||
// --------------- Group By ---------------
|
// --------------- Group By ---------------
|
||||||
.impl(GroupByProvider('system'), SystemGroupByProvider)
|
.impl(GroupByProvider('system'), SystemGroupByProvider)
|
||||||
.impl(GroupByProvider('property'), PropertyGroupByProvider, [
|
.impl(GroupByProvider('property'), PropertyGroupByProvider, [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
type Observable,
|
type Observable,
|
||||||
of,
|
of,
|
||||||
@@ -21,7 +22,8 @@ export class CollectionRulesService extends Service {
|
|||||||
watch(
|
watch(
|
||||||
filters: FilterParams[],
|
filters: FilterParams[],
|
||||||
groupBy?: GroupByParams,
|
groupBy?: GroupByParams,
|
||||||
orderBy?: OrderByParams
|
orderBy?: OrderByParams,
|
||||||
|
extraAllowList?: string[]
|
||||||
): Observable<{
|
): Observable<{
|
||||||
groups: {
|
groups: {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -36,7 +38,10 @@ export class CollectionRulesService extends Service {
|
|||||||
filterErrors: any[]; // errors from the filter providers
|
filterErrors: any[]; // errors from the filter providers
|
||||||
}> =
|
}> =
|
||||||
filters.length === 0
|
filters.length === 0
|
||||||
? of({ filtered: new Set<string>(), filterErrors: [] })
|
? of({
|
||||||
|
filtered: new Set<string>(extraAllowList ?? []),
|
||||||
|
filterErrors: [],
|
||||||
|
})
|
||||||
: combineLatest(
|
: combineLatest(
|
||||||
filters.map(filter => {
|
filters.map(filter => {
|
||||||
const provider = filterProviders.get(filter.type);
|
const provider = filterProviders.get(filter.type);
|
||||||
@@ -57,7 +62,7 @@ export class CollectionRulesService extends Service {
|
|||||||
})
|
})
|
||||||
).pipe(
|
).pipe(
|
||||||
map(results => {
|
map(results => {
|
||||||
const finalSet = results.reduce((acc, result) => {
|
const aggregated = results.reduce((acc, result) => {
|
||||||
if ('error' in acc) {
|
if ('error' in acc) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
@@ -67,8 +72,15 @@ export class CollectionRulesService extends Service {
|
|||||||
return acc.intersection(result);
|
return acc.intersection(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
'error' in aggregated ? new Set<string>() : aggregated;
|
||||||
|
|
||||||
|
const finalSet = filtered.union(
|
||||||
|
new Set<string>(extraAllowList ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filtered: 'error' in finalSet ? new Set<string>() : finalSet,
|
filtered: finalSet,
|
||||||
filterErrors: results.map(i => ('error' in i ? i.error : null)),
|
filterErrors: results.map(i => ('error' in i ? i.error : null)),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -204,4 +216,15 @@ export class CollectionRulesService extends Service {
|
|||||||
|
|
||||||
return final$;
|
return final$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compute(
|
||||||
|
filters: FilterParams[],
|
||||||
|
groupBy?: GroupByParams,
|
||||||
|
orderBy?: OrderByParams,
|
||||||
|
extraAllowList?: string[]
|
||||||
|
) {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.watch(filters, groupBy, orderBy, extraAllowList)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
import { uniq } from 'lodash-es';
|
||||||
|
import { map, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
import type { CollectionRulesService } from '../../collection-rules';
|
||||||
|
import type { CollectionInfo, CollectionStore } from '../stores/collection';
|
||||||
|
|
||||||
|
export class Collection extends Entity<{ id: string }> {
|
||||||
|
constructor(
|
||||||
|
private readonly store: CollectionStore,
|
||||||
|
private readonly rulesService: CollectionRulesService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
id = this.props.id;
|
||||||
|
|
||||||
|
info$ = LiveData.from<CollectionInfo>(
|
||||||
|
this.store.watchCollectionInfo(this.id).pipe(
|
||||||
|
map(
|
||||||
|
info =>
|
||||||
|
({
|
||||||
|
// default fields in case collection info is not found
|
||||||
|
name: '',
|
||||||
|
id: this.id,
|
||||||
|
rules: {
|
||||||
|
filters: [],
|
||||||
|
},
|
||||||
|
allowList: [],
|
||||||
|
...info,
|
||||||
|
}) as CollectionInfo
|
||||||
|
)
|
||||||
|
),
|
||||||
|
{} as CollectionInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
name$ = this.info$.map(info => info.name);
|
||||||
|
allowList$ = this.info$.map(info => info.allowList);
|
||||||
|
rules$ = this.info$.map(info => info.rules);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of document IDs that match the collection rules and allow list.
|
||||||
|
*
|
||||||
|
* For performance optimization,
|
||||||
|
* Developers must explicitly call `watch()` to retrieve the result and properly manage the subscription lifecycle.
|
||||||
|
*/
|
||||||
|
watch() {
|
||||||
|
return this.info$.pipe(
|
||||||
|
switchMap(info => {
|
||||||
|
return this.rulesService
|
||||||
|
.watch(
|
||||||
|
info.rules.filters.length > 0
|
||||||
|
? [
|
||||||
|
...info.rules.filters,
|
||||||
|
// if we have more than one filter, we need to add a system filter to exclude trash
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [], // If no filters are provided, an empty filter list will match no documents
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
info.allowList
|
||||||
|
)
|
||||||
|
.pipe(map(result => result.groups.map(group => group.items).flat()));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInfo(info: Partial<CollectionInfo>) {
|
||||||
|
this.store.updateCollectionInfo(this.id, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDoc(...docIds: string[]) {
|
||||||
|
this.store.updateCollectionInfo(this.id, {
|
||||||
|
allowList: uniq([...this.info$.value.allowList, ...docIds]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDoc(...docIds: string[]) {
|
||||||
|
this.store.updateCollectionInfo(this.id, {
|
||||||
|
allowList: this.info$.value.allowList.filter(id => !docIds.includes(id)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
|
export { Collection } from './entities/collection';
|
||||||
|
export type { CollectionMeta } from './services/collection';
|
||||||
export { CollectionService } from './services/collection';
|
export { CollectionService } from './services/collection';
|
||||||
|
export type { CollectionInfo } from './stores/collection';
|
||||||
|
|
||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { CollectionRulesService } from '../collection-rules';
|
||||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
|
import { Collection } from './entities/collection';
|
||||||
import { CollectionService } from './services/collection';
|
import { CollectionService } from './services/collection';
|
||||||
|
import { CollectionStore } from './stores/collection';
|
||||||
|
|
||||||
export function configureCollectionModule(framework: Framework) {
|
export function configureCollectionModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(CollectionService, [WorkspaceService]);
|
.service(CollectionService, [CollectionStore])
|
||||||
|
.store(CollectionStore, [WorkspaceService])
|
||||||
|
.entity(Collection, [CollectionStore, CollectionRulesService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,193 +1,78 @@
|
|||||||
import type {
|
import { LiveData, ObjectPool, Service } from '@toeverything/infra';
|
||||||
Collection,
|
import { map } from 'rxjs';
|
||||||
DeleteCollectionInfo,
|
|
||||||
DeletedCollection,
|
|
||||||
} from '@affine/env/filter';
|
|
||||||
import { LiveData, Service } from '@toeverything/infra';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { Array as YArray } from 'yjs';
|
|
||||||
|
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import { Collection } from '../entities/collection';
|
||||||
|
import type { CollectionInfo, CollectionStore } from '../stores/collection';
|
||||||
|
|
||||||
const SETTING_KEY = 'setting';
|
export interface CollectionMeta extends Pick<CollectionInfo, 'id' | 'name'> {
|
||||||
|
title: string;
|
||||||
const COLLECTIONS_KEY = 'collections';
|
}
|
||||||
const COLLECTIONS_TRASH_KEY = 'collections_trash';
|
|
||||||
|
|
||||||
export class CollectionService extends Service {
|
export class CollectionService extends Service {
|
||||||
constructor(private readonly workspaceService: WorkspaceService) {
|
constructor(private readonly store: CollectionStore) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get doc() {
|
pool = new ObjectPool<string, Collection>({
|
||||||
return this.workspaceService.workspace.docCollection.doc;
|
onDelete(obj) {
|
||||||
}
|
obj.dispose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
private get setting() {
|
// collection metas used in collection list, only include `id` and `name`, without `rules` and `allowList`
|
||||||
return this.workspaceService.workspace.docCollection.doc.getMap(
|
readonly collectionMetas$ = LiveData.from(
|
||||||
SETTING_KEY
|
this.store.watchCollectionMetas(),
|
||||||
);
|
[]
|
||||||
}
|
);
|
||||||
|
|
||||||
private get collectionsYArray(): YArray<Collection> | undefined {
|
|
||||||
return this.setting.get(COLLECTIONS_KEY) as YArray<Collection>;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get collectionsTrashYArray(): YArray<DeletedCollection> | undefined {
|
|
||||||
return this.setting.get(COLLECTIONS_TRASH_KEY) as YArray<DeletedCollection>;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly collections$ = LiveData.from(
|
readonly collections$ = LiveData.from(
|
||||||
new Observable<Collection[]>(subscriber => {
|
this.store.watchCollectionIds().pipe(
|
||||||
subscriber.next(this.collectionsYArray?.toArray() ?? []);
|
map(
|
||||||
const fn = () => {
|
ids =>
|
||||||
subscriber.next(this.collectionsYArray?.toArray() ?? []);
|
new Map<string, Collection>(
|
||||||
};
|
ids.map(id => {
|
||||||
this.setting.observeDeep(fn);
|
const exists = this.pool.get(id);
|
||||||
return () => {
|
if (exists) {
|
||||||
this.setting.unobserveDeep(fn);
|
return [id, exists.obj];
|
||||||
};
|
}
|
||||||
}),
|
const record = this.framework.createEntity(Collection, { id });
|
||||||
[]
|
this.pool.put(id, record);
|
||||||
|
return [id, record] as const;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new Map<string, Collection>()
|
||||||
);
|
);
|
||||||
|
|
||||||
collection$(id: string) {
|
collection$(id: string) {
|
||||||
return this.collections$.map(collections => {
|
return this.collections$.selector(collections => {
|
||||||
return collections.find(v => v.id === id);
|
return collections.get(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly collectionsTrash$ = LiveData.from(
|
createCollection(collectionInfo: Partial<Omit<CollectionInfo, 'id'>>) {
|
||||||
new Observable<DeletedCollection[]>(subscriber => {
|
return this.store.createCollection(collectionInfo);
|
||||||
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
|
|
||||||
const fn = () => {
|
|
||||||
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
|
|
||||||
};
|
|
||||||
this.setting.observeDeep(fn);
|
|
||||||
return () => {
|
|
||||||
this.setting.unobserveDeep(fn);
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
addCollection(...collections: Collection[]) {
|
|
||||||
if (!this.setting.has(COLLECTIONS_KEY)) {
|
|
||||||
this.setting.set(COLLECTIONS_KEY, new YArray());
|
|
||||||
}
|
|
||||||
this.doc.transact(() => {
|
|
||||||
this.collectionsYArray?.insert(0, collections);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCollection(id: string, updater: (value: Collection) => Collection) {
|
updateCollection(
|
||||||
if (this.collectionsYArray) {
|
id: string,
|
||||||
updateFirstOfYArray(
|
collectionInfo: Partial<Omit<CollectionInfo, 'id'>>
|
||||||
this.collectionsYArray,
|
|
||||||
v => v.id === id,
|
|
||||||
v => {
|
|
||||||
return updater(v);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addPageToCollection(collectionId: string, pageId: string) {
|
|
||||||
this.updateCollection(collectionId, old => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
allowList: [pageId, ...(old.allowList ?? [])],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePageFromCollection(collectionId: string, pageId: string) {
|
|
||||||
this.updateCollection(collectionId, old => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
allowList: old.allowList?.filter(id => id !== pageId),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
|
|
||||||
const collectionsYArray = this.collectionsYArray;
|
|
||||||
if (!collectionsYArray) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const set = new Set(ids);
|
|
||||||
this.workspaceService.workspace.docCollection.doc.transact(() => {
|
|
||||||
const indexList: number[] = [];
|
|
||||||
const list: Collection[] = [];
|
|
||||||
collectionsYArray.forEach((collection, i) => {
|
|
||||||
if (set.has(collection.id)) {
|
|
||||||
set.delete(collection.id);
|
|
||||||
indexList.unshift(i);
|
|
||||||
list.push(JSON.parse(JSON.stringify(collection)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
indexList.forEach(i => {
|
|
||||||
collectionsYArray.delete(i);
|
|
||||||
});
|
|
||||||
if (!this.collectionsTrashYArray) {
|
|
||||||
this.setting.set(COLLECTIONS_TRASH_KEY, new YArray());
|
|
||||||
}
|
|
||||||
const collectionsTrashYArray = this.collectionsTrashYArray;
|
|
||||||
if (!collectionsTrashYArray) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
collectionsTrashYArray.insert(
|
|
||||||
0,
|
|
||||||
list.map(collection => ({
|
|
||||||
userId: info?.userId,
|
|
||||||
userName: info ? info.userName : 'Local User',
|
|
||||||
collection,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
if (collectionsTrashYArray.length > 10) {
|
|
||||||
collectionsTrashYArray.delete(10, collectionsTrashYArray.length - 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private deletePagesFromCollection(
|
|
||||||
collection: Collection,
|
|
||||||
idSet: Set<string>
|
|
||||||
) {
|
) {
|
||||||
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
|
return this.store.updateCollectionInfo(id, collectionInfo);
|
||||||
if (newAllowList.length !== collection.allowList.length) {
|
|
||||||
this.updateCollection(collection.id, old => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
allowList: newAllowList,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePagesFromCollections(ids: string[]) {
|
addDocToCollection(collectionId: string, docId: string) {
|
||||||
const idSet = new Set(ids);
|
const collection = this.collection$(collectionId).value;
|
||||||
this.doc.transact(() => {
|
collection?.addDoc(docId);
|
||||||
this.collections$.value.forEach(collection => {
|
}
|
||||||
this.deletePagesFromCollection(collection, idSet);
|
|
||||||
});
|
removeDocFromCollection(collectionId: string, docId: string) {
|
||||||
});
|
const collection = this.collection$(collectionId).value;
|
||||||
|
collection?.removeDoc(docId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCollection(id: string) {
|
||||||
|
this.store.deleteCollection(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFirstOfYArray = <T>(
|
|
||||||
array: YArray<T>,
|
|
||||||
p: (value: T) => boolean,
|
|
||||||
update: (value: T) => T
|
|
||||||
) => {
|
|
||||||
array.doc?.transact(() => {
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
const ele = array.get(i);
|
|
||||||
if (p(ele)) {
|
|
||||||
array.delete(i);
|
|
||||||
array.insert(i, [update(ele)]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import type { Collection as LegacyCollectionInfo } from '@affine/env/filter';
|
||||||
|
import {
|
||||||
|
Store,
|
||||||
|
yjsGetPath,
|
||||||
|
yjsObserve,
|
||||||
|
yjsObserveDeep,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { map, type Observable, switchMap } from 'rxjs';
|
||||||
|
import { Array as YArray } from 'yjs';
|
||||||
|
|
||||||
|
import type { FilterParams } from '../../collection-rules';
|
||||||
|
import type { WorkspaceService } from '../../workspace';
|
||||||
|
|
||||||
|
export interface CollectionInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
rules: {
|
||||||
|
filters: FilterParams[];
|
||||||
|
};
|
||||||
|
allowList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CollectionStore extends Store {
|
||||||
|
constructor(private readonly workspaceService: WorkspaceService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private get rootYDoc() {
|
||||||
|
return this.workspaceService.workspace.rootYDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get workspaceSettingYMap() {
|
||||||
|
return this.rootYDoc.getMap('setting');
|
||||||
|
}
|
||||||
|
|
||||||
|
watchCollectionMetas() {
|
||||||
|
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
|
||||||
|
switchMap(yjsObserveDeep),
|
||||||
|
map(yjs => {
|
||||||
|
if (yjs instanceof YArray) {
|
||||||
|
return yjs.map(v => {
|
||||||
|
return {
|
||||||
|
id: v.id as string,
|
||||||
|
name: v.name as string,
|
||||||
|
// for old code compatibility
|
||||||
|
title: v.name as string,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchCollectionIds() {
|
||||||
|
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
|
||||||
|
switchMap(yjsObserve),
|
||||||
|
map(yjs => {
|
||||||
|
if (yjs instanceof YArray) {
|
||||||
|
return yjs.map(v => {
|
||||||
|
return v.id as string;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchCollectionInfo(id: string): Observable<CollectionInfo | null> {
|
||||||
|
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
|
||||||
|
switchMap(yjsObserve),
|
||||||
|
map(meta => {
|
||||||
|
if (meta instanceof YArray) {
|
||||||
|
// meta is YArray, `for-of` is faster then `for`
|
||||||
|
for (const doc of meta) {
|
||||||
|
if (doc && doc.id === id) {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap(yjsObserveDeep),
|
||||||
|
map(yjs => {
|
||||||
|
if (yjs) {
|
||||||
|
return this.migrateCollectionInfo(yjs as LegacyCollectionInfo);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCollection(info: Partial<Omit<CollectionInfo, 'id'>>) {
|
||||||
|
const id = nanoid();
|
||||||
|
let yArray = this.rootYDoc.getMap('setting').get('collections') as
|
||||||
|
| YArray<any>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!(yArray instanceof YArray)) {
|
||||||
|
// if collections list is not a YArray, create a new one
|
||||||
|
yArray = new YArray<any>();
|
||||||
|
this.rootYDoc.getMap('setting').set('collections', yArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
yArray.push([
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
name: info.name ?? '',
|
||||||
|
rules: info.rules ?? { filters: [] },
|
||||||
|
allowList: info.allowList ?? [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCollection(id: string) {
|
||||||
|
const yArray = this.rootYDoc.getMap('setting').get('collections') as
|
||||||
|
| YArray<any>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!(yArray instanceof YArray)) {
|
||||||
|
throw new Error('Collections is not a YArray');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < yArray.length; i++) {
|
||||||
|
const collection = yArray.get(i);
|
||||||
|
if (collection.id === id) {
|
||||||
|
yArray.delete(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCollectionInfo(id: string, info: Partial<Omit<CollectionInfo, 'id'>>) {
|
||||||
|
const yArray = this.rootYDoc.getMap('setting').get('collections') as
|
||||||
|
| YArray<any>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!(yArray instanceof YArray)) {
|
||||||
|
throw new Error('Collections is not a YArray');
|
||||||
|
}
|
||||||
|
|
||||||
|
let collectionIndex = 0;
|
||||||
|
for (const collection of yArray) {
|
||||||
|
if (collection.id === id) {
|
||||||
|
this.rootYDoc.transact(() => {
|
||||||
|
yArray.delete(collectionIndex, 1);
|
||||||
|
yArray.insert(collectionIndex, [
|
||||||
|
{
|
||||||
|
id: collection.id,
|
||||||
|
name: info.name ?? collection.name,
|
||||||
|
rules: info.rules ?? collection.rules,
|
||||||
|
allowList: info.allowList ?? collection.allowList,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
collectionIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateCollectionInfo(
|
||||||
|
legacyCollectionInfo: LegacyCollectionInfo
|
||||||
|
): CollectionInfo {
|
||||||
|
if ('rules' in legacyCollectionInfo) {
|
||||||
|
return legacyCollectionInfo as CollectionInfo;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: legacyCollectionInfo.id,
|
||||||
|
name: legacyCollectionInfo.name,
|
||||||
|
rules: {
|
||||||
|
filters: this.migrateFilterList(legacyCollectionInfo.filterList),
|
||||||
|
},
|
||||||
|
allowList: legacyCollectionInfo.allowList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateFilterList(
|
||||||
|
filterList: LegacyCollectionInfo['filterList']
|
||||||
|
): FilterParams[] {
|
||||||
|
return filterList.map(filter => {
|
||||||
|
const leftValue = filter.left.name;
|
||||||
|
const method = filter.funcName;
|
||||||
|
const args = filter.args.map(arg => arg.value);
|
||||||
|
const arg0 = args[0];
|
||||||
|
if (leftValue === 'Created' || leftValue === 'Updated') {
|
||||||
|
const key = leftValue === 'Created' ? 'createdAt' : 'updatedAt';
|
||||||
|
if (method === 'after' && typeof arg0 === 'number') {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key,
|
||||||
|
method: 'after',
|
||||||
|
value: dayjs(arg0).format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
} else if (method === 'before' && typeof arg0 === 'number') {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key,
|
||||||
|
method: 'before',
|
||||||
|
value: dayjs(arg0).format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
} else if (method === 'last' && typeof arg0 === 'number') {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key,
|
||||||
|
method: 'last',
|
||||||
|
value: dayjs().subtract(arg0, 'day').format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (leftValue === 'Is Favourited') {
|
||||||
|
if (method === 'is') {
|
||||||
|
const value = arg0 ? 'true' : 'false';
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'favorite',
|
||||||
|
method: 'is',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (leftValue === 'Tags') {
|
||||||
|
if (method === 'is not empty') {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'is-not-empty',
|
||||||
|
};
|
||||||
|
} else if (method === 'is empty') {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'is-empty',
|
||||||
|
};
|
||||||
|
} else if (method === 'contains all' && Array.isArray(arg0)) {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'include-all',
|
||||||
|
value: arg0.join(','),
|
||||||
|
};
|
||||||
|
} else if (method === 'contains one of' && Array.isArray(arg0)) {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'include-any-of',
|
||||||
|
value: arg0.join(','),
|
||||||
|
};
|
||||||
|
} else if (method === 'does not contains all' && Array.isArray(arg0)) {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'not-include-all',
|
||||||
|
value: arg0.join(','),
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
method === 'does not contains one of' &&
|
||||||
|
Array.isArray(arg0)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'tags',
|
||||||
|
method: 'not-include-any-of',
|
||||||
|
value: arg0.join(','),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (leftValue === 'Is Public' && method === 'is') {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
key: 'shared',
|
||||||
|
method: 'is',
|
||||||
|
value: arg0 ? 'true' : 'false',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'unknown',
|
||||||
|
key: 'unknown',
|
||||||
|
method: 'unknown',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,10 @@ export class DocsService extends Service {
|
|||||||
return this.store.watchAllDocTagIds();
|
return this.store.watchAllDocTagIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allDocIds$() {
|
||||||
|
return this.store.watchDocIds();
|
||||||
|
}
|
||||||
|
|
||||||
allNonTrashDocIds$() {
|
allNonTrashDocIds$() {
|
||||||
return this.store.watchNonTrashDocIds();
|
return this.store.watchNonTrashDocIds();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class CollectionsQuickSearchSession
|
|||||||
LiveData.computed(get => {
|
LiveData.computed(get => {
|
||||||
const query = get(this.query$);
|
const query = get(this.query$);
|
||||||
|
|
||||||
const collections = get(this.collectionService.collections$);
|
const collections = get(this.collectionService.collectionMetas$);
|
||||||
|
|
||||||
const fuse = new Fuse(collections, {
|
const fuse = new Fuse(collections, {
|
||||||
keys: ['name'],
|
keys: ['name'],
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import type {
|
import type { TagMeta } from '@affine/core/components/page-list';
|
||||||
CollectionMeta,
|
|
||||||
TagMeta,
|
|
||||||
} from '@affine/core/components/page-list';
|
|
||||||
import { I18n } from '@affine/i18n';
|
import { I18n } from '@affine/i18n';
|
||||||
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
@@ -18,7 +15,7 @@ import { html } from 'lit';
|
|||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||||
import { map } from 'rxjs';
|
import { map } from 'rxjs';
|
||||||
|
|
||||||
import type { CollectionService } from '../../collection';
|
import type { CollectionMeta, CollectionService } from '../../collection';
|
||||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||||
import type { DocsSearchService } from '../../docs-search';
|
import type { DocsSearchService } from '../../docs-search';
|
||||||
import { type RecentDocsService } from '../../quicksearch';
|
import { type RecentDocsService } from '../../quicksearch';
|
||||||
@@ -298,7 +295,7 @@ export class SearchMenuService extends Service {
|
|||||||
action: SearchCollectionMenuAction,
|
action: SearchCollectionMenuAction,
|
||||||
_abortSignal: AbortSignal
|
_abortSignal: AbortSignal
|
||||||
): LinkedMenuGroup {
|
): LinkedMenuGroup {
|
||||||
const collections = this.collectionService.collections$.value;
|
const collections = this.collectionService.collectionMetas$.value;
|
||||||
if (query.trim().length === 0) {
|
if (query.trim().length === 0) {
|
||||||
return {
|
return {
|
||||||
name: I18n.t('com.affine.editor.at-menu.collections', {
|
name: I18n.t('com.affine.editor.at-menu.collections', {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
onStart,
|
onStart,
|
||||||
smartRetry,
|
smartRetry,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { tap } from 'rxjs';
|
import { map, tap } from 'rxjs';
|
||||||
|
|
||||||
import type { GlobalCache } from '../../storage';
|
import type { GlobalCache } from '../../storage';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
@@ -22,7 +22,12 @@ type ShareDocListType = GetWorkspacePublicPagesQuery['workspace']['publicDocs'];
|
|||||||
export const logger = new DebugLogger('affine:share-doc-list');
|
export const logger = new DebugLogger('affine:share-doc-list');
|
||||||
|
|
||||||
export class ShareDocsList extends Entity {
|
export class ShareDocsList extends Entity {
|
||||||
list$ = LiveData.from(this.cache.watch<ShareDocListType>('share-docs'), []);
|
list$ = LiveData.from(
|
||||||
|
this.cache
|
||||||
|
.watch<ShareDocListType>('share-docs')
|
||||||
|
.pipe(map(list => list ?? [])),
|
||||||
|
[]
|
||||||
|
);
|
||||||
isLoading$ = new LiveData<boolean>(false);
|
isLoading$ = new LiveData<boolean>(false);
|
||||||
error$ = new LiveData<any>(null);
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ type DateFilters =
|
|||||||
|
|
||||||
export type WorkspacePropertyTypes = {
|
export type WorkspacePropertyTypes = {
|
||||||
tags: {
|
tags: {
|
||||||
filter: 'include' | 'is-not-empty' | 'is-empty';
|
filter:
|
||||||
|
| 'include-all'
|
||||||
|
| 'include-any-of'
|
||||||
|
| 'not-include-all'
|
||||||
|
| 'not-include-any-of'
|
||||||
|
| 'is-not-empty'
|
||||||
|
| 'is-empty';
|
||||||
};
|
};
|
||||||
text: {
|
text: {
|
||||||
filter: 'is' | 'is-not' | 'is-not-empty' | 'is-empty';
|
filter: 'is' | 'is-not' | 'is-not-empty' | 'is-empty';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Collection } from '@affine/env/filter';
|
|
||||||
import type { Workspace } from '@blocksuite/affine/store';
|
import type { Workspace } from '@blocksuite/affine/store';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { Map as YMap } from 'yjs';
|
import type { Map as YMap } from 'yjs';
|
||||||
@@ -29,13 +28,6 @@ export class UserSetting {
|
|||||||
}
|
}
|
||||||
return this.setting.whenLoaded;
|
return this.setting.whenLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
get view() {
|
|
||||||
return this.setting.getMap('view') as YMap<Collection>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserSetting = (docCollection: Workspace, userId: string) => {
|
export const getUserSetting = (docCollection: Workspace, userId: string) => {
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
/* oxlint-disable unicorn/prefer-dom-node-dataset */
|
/* oxlint-disable unicorn/prefer-dom-node-dataset */
|
||||||
import { test } from '@affine-test/kit/playwright';
|
import { test } from '@affine-test/kit/playwright';
|
||||||
import {
|
import { getPagesCount } from '@affine-test/kit/utils/filter';
|
||||||
changeFilter,
|
|
||||||
checkDatePicker,
|
|
||||||
checkDatePickerMonth,
|
|
||||||
checkFilterName,
|
|
||||||
clickDatePicker,
|
|
||||||
createFirstFilter,
|
|
||||||
createPageWithTag,
|
|
||||||
getPagesCount,
|
|
||||||
selectMonthFromMonthPicker,
|
|
||||||
selectTag,
|
|
||||||
} from '@affine-test/kit/utils/filter';
|
|
||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
@@ -52,75 +41,6 @@ test('all page can create new edgeless page', async ({ page }) => {
|
|||||||
await expect(page.locator('affine-edgeless-root')).toBeVisible();
|
await expect(page.locator('affine-edgeless-root')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow creation of filters by favorite', async ({ page }) => {
|
|
||||||
await openHomePage(page);
|
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickSideBarAllPageButton(page);
|
|
||||||
await createFirstFilter(page, 'Favourited');
|
|
||||||
await page
|
|
||||||
.locator('[data-testid="filter-arg"]', { hasText: 'true' })
|
|
||||||
.locator('div')
|
|
||||||
.click();
|
|
||||||
expect(await page.locator('[data-testid="filter-arg"]').textContent()).toBe(
|
|
||||||
'false'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('use monthpicker to modify the month of datepicker', async ({ page }) => {
|
|
||||||
await openHomePage(page);
|
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickSideBarAllPageButton(page);
|
|
||||||
await createFirstFilter(page, 'Created');
|
|
||||||
await checkFilterName(page, 'after');
|
|
||||||
// init date
|
|
||||||
const yesterday = new Date();
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
await checkDatePicker(page, yesterday);
|
|
||||||
// change month
|
|
||||||
await clickDatePicker(page);
|
|
||||||
const lastMonth = new Date();
|
|
||||||
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
||||||
const datePicker = page.locator(
|
|
||||||
'[role="dialog"] [data-testid="date-picker-calendar"]'
|
|
||||||
);
|
|
||||||
await selectMonthFromMonthPicker(datePicker, lastMonth);
|
|
||||||
await checkDatePickerMonth(datePicker, lastMonth);
|
|
||||||
// change month
|
|
||||||
const nextMonth = new Date();
|
|
||||||
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
|
||||||
await selectMonthFromMonthPicker(datePicker, nextMonth);
|
|
||||||
await checkDatePickerMonth(datePicker, nextMonth);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('allow creation of filters by tags', async ({ page }) => {
|
|
||||||
await openHomePage(page);
|
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickSideBarAllPageButton(page);
|
|
||||||
await waitForAllPagesLoad(page);
|
|
||||||
const pageCount = await getPagesCount(page);
|
|
||||||
expect(pageCount).not.toBe(0);
|
|
||||||
await createFirstFilter(page, 'Tags');
|
|
||||||
await checkFilterName(page, 'is not empty');
|
|
||||||
const pagesWithTags = await page
|
|
||||||
.locator('[data-testid="page-list-item"]')
|
|
||||||
.all();
|
|
||||||
const pagesWithTagsCount = pagesWithTags.length;
|
|
||||||
expect(pagesWithTagsCount).toBe(0);
|
|
||||||
await createPageWithTag(page, { title: 'Page A', tags: ['Page A'] });
|
|
||||||
await createPageWithTag(page, { title: 'Page B', tags: ['Page B'] });
|
|
||||||
await clickSideBarAllPageButton(page);
|
|
||||||
await createFirstFilter(page, 'Tags');
|
|
||||||
await checkFilterName(page, 'is not empty');
|
|
||||||
expect(await getPagesCount(page)).toBe(pagesWithTagsCount + 2);
|
|
||||||
await changeFilter(page, 'contains all');
|
|
||||||
expect(await getPagesCount(page)).toBe(pageCount + 2);
|
|
||||||
await selectTag(page, 'Page A');
|
|
||||||
expect(await getPagesCount(page)).toBe(1);
|
|
||||||
await changeFilter(page, 'does not contains all');
|
|
||||||
await selectTag(page, 'Page B');
|
|
||||||
expect(await getPagesCount(page)).toBe(pageCount + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('enable selection and use ESC to disable selection', async ({ page }) => {
|
test('enable selection and use ESC to disable selection', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
@@ -155,8 +75,8 @@ test('enable selection and use ESC to disable selection', async ({ page }) => {
|
|||||||
.count()
|
.count()
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
// wait for 300ms
|
// wait for 500ms
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
// esc again, checkboxes should disappear
|
// esc again, checkboxes should disappear
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|||||||
@@ -34,48 +34,43 @@ const createAndPinCollection = async (
|
|||||||
collectionName?: string;
|
collectionName?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).click();
|
|
||||||
await getBlockSuiteEditorTitle(page).fill('test page');
|
|
||||||
|
|
||||||
// fixme: remove this timeout. looks like an issue with useBindWorkbenchToBrowserRouter?
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await page.getByTestId('all-pages').click();
|
await page.getByTestId('all-pages').click();
|
||||||
|
|
||||||
const cell = page.getByTestId('page-list-item-title').getByText('test page');
|
await page.getByTestId('navigation-panel-bar-add-collection-button').click();
|
||||||
await expect(cell).toBeVisible();
|
|
||||||
await page.getByTestId('create-first-filter').click({
|
|
||||||
delay: 200,
|
|
||||||
});
|
|
||||||
await page
|
|
||||||
.getByTestId('variable-select')
|
|
||||||
.getByTestId(`filler-tag-Created`)
|
|
||||||
.click({
|
|
||||||
delay: 200,
|
|
||||||
});
|
|
||||||
await page.getByTestId('save-as-collection').click({
|
|
||||||
delay: 200,
|
|
||||||
});
|
|
||||||
const title = page.getByTestId('prompt-modal-input');
|
const title = page.getByTestId('prompt-modal-input');
|
||||||
await expect(title).toBeVisible();
|
await expect(title).toBeVisible();
|
||||||
await title.fill(options?.collectionName ?? 'test collection');
|
await title.fill(options?.collectionName ?? 'test collection');
|
||||||
await page.getByTestId('prompt-modal-confirm').click();
|
await page.getByTestId('prompt-modal-confirm').click();
|
||||||
await page.waitForTimeout(100);
|
await page.waitForTimeout(100);
|
||||||
|
await page
|
||||||
|
.locator('[data-testid^="navigation-panel-collection-"]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await page.getByTestId('collection-add-doc-button').click();
|
||||||
|
await page.getByTestId('confirm-modal-confirm').click();
|
||||||
|
|
||||||
|
// fixme: remove this timeout. looks like an issue with useBindWorkbenchToBrowserRouter?
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await getBlockSuiteEditorTitle(page).click();
|
||||||
|
await getBlockSuiteEditorTitle(page).fill('test page');
|
||||||
|
|
||||||
|
await page.getByTestId('all-pages').click();
|
||||||
|
|
||||||
|
const cell = page.getByTestId('page-list-item-title').getByText('test page');
|
||||||
|
await expect(cell).toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Show collections items in sidebar', async ({ page }) => {
|
test('Show collections items in sidebar', async ({ page }) => {
|
||||||
await removeOnboardingPages(page);
|
await removeOnboardingPages(page);
|
||||||
await createAndPinCollection(page);
|
await createAndPinCollection(page);
|
||||||
const collections = page.getByTestId('navigation-panel-collections');
|
const collections = page.getByTestId('navigation-panel-collections');
|
||||||
await collections.getByTestId('category-divider-collapse-button').click();
|
|
||||||
const items = collections.locator(
|
const items = collections.locator(
|
||||||
'[data-testid^="navigation-panel-collection-"]'
|
'[data-testid^="navigation-panel-collection-"]'
|
||||||
);
|
);
|
||||||
await expect(items).toHaveCount(1);
|
await expect(items).toHaveCount(1);
|
||||||
const first = items.first();
|
const first = items.first();
|
||||||
expect(await first.textContent()).toBe('test collection');
|
expect((await first.textContent())!.startsWith('test collection')).toBe(true);
|
||||||
await first.getByTestId('navigation-panel-collapsed-button').click();
|
|
||||||
const collectionPage = first
|
const collectionPage = first
|
||||||
.locator('[data-testid^="navigation-panel-doc-"]')
|
.locator('[data-testid^="navigation-panel-doc-"]')
|
||||||
.nth(0);
|
.nth(0);
|
||||||
@@ -118,34 +113,12 @@ test('edit collection', async ({ page }) => {
|
|||||||
await removeOnboardingPages(page);
|
await removeOnboardingPages(page);
|
||||||
await createAndPinCollection(page);
|
await createAndPinCollection(page);
|
||||||
const collections = page.getByTestId('navigation-panel-collections');
|
const collections = page.getByTestId('navigation-panel-collections');
|
||||||
await collections.getByTestId('category-divider-collapse-button').click();
|
|
||||||
const items = collections.locator(
|
|
||||||
'[data-testid^="navigation-panel-collection-"]'
|
|
||||||
);
|
|
||||||
await expect(items).toHaveCount(1);
|
|
||||||
const first = items.first();
|
|
||||||
await first.hover();
|
|
||||||
await first
|
|
||||||
.getByTestId('navigation-panel-tree-node-operation-button')
|
|
||||||
.click();
|
|
||||||
const editCollection = page.getByText('Rename');
|
|
||||||
await editCollection.click();
|
|
||||||
await page.getByTestId('rename-modal-input').fill('123');
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
expect(await first.textContent()).toBe('123');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('edit collection and change filter date', async ({ page }) => {
|
|
||||||
await removeOnboardingPages(page);
|
|
||||||
await createAndPinCollection(page);
|
|
||||||
const collections = page.getByTestId('navigation-panel-collections');
|
|
||||||
await collections.getByTestId('category-divider-collapse-button').click();
|
|
||||||
const items = collections.locator(
|
const items = collections.locator(
|
||||||
'[data-testid^="navigation-panel-collection-"]'
|
'[data-testid^="navigation-panel-collection-"]'
|
||||||
);
|
);
|
||||||
await expect(items).toHaveCount(1);
|
await expect(items).toHaveCount(1);
|
||||||
const first = items.first();
|
const first = items.first();
|
||||||
|
await first.getByTestId('navigation-panel-collapsed-button').first().click();
|
||||||
await first.hover();
|
await first.hover();
|
||||||
await first
|
await first
|
||||||
.getByTestId('navigation-panel-tree-node-operation-button')
|
.getByTestId('navigation-panel-tree-node-operation-button')
|
||||||
|
|||||||
@@ -1,43 +1,4 @@
|
|||||||
import type { Locator, Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
|
|
||||||
import { clickNewPageButton, getBlockSuiteEditorTitle } from './page-logic';
|
|
||||||
|
|
||||||
const monthNames = [
|
|
||||||
'Jan',
|
|
||||||
'Feb',
|
|
||||||
'Mar',
|
|
||||||
'Apr',
|
|
||||||
'May',
|
|
||||||
'Jun',
|
|
||||||
'Jul',
|
|
||||||
'Aug',
|
|
||||||
'Sep',
|
|
||||||
'Oct',
|
|
||||||
'Nov',
|
|
||||||
'Dec',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const createFirstFilter = async (page: Page, name: string) => {
|
|
||||||
await page.locator('[data-testid="create-first-filter"]').click();
|
|
||||||
await page
|
|
||||||
.locator('[data-testid="variable-select-item"]', { hasText: name })
|
|
||||||
.click();
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkFilterName = async (page: Page, name: string) => {
|
|
||||||
const filterName = await page
|
|
||||||
.locator('[data-testid="filter-name"]')
|
|
||||||
.textContent();
|
|
||||||
expect(filterName).toBe(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateFormat = (date: Date) => {
|
|
||||||
const month = monthNames[date.getMonth()];
|
|
||||||
const day = date.getDate().toString().padStart(2, '0');
|
|
||||||
return `${month} ${day}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// fixme: there could be multiple page lists in the Page
|
// fixme: there could be multiple page lists in the Page
|
||||||
export const getPagesCount = async (page: Page) => {
|
export const getPagesCount = async (page: Page) => {
|
||||||
@@ -54,97 +15,6 @@ export const getPagesCount = async (page: Page) => {
|
|||||||
return count ? parseInt(count) : 0;
|
return count ? parseInt(count) : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkDatePicker = async (page: Page, date: Date) => {
|
|
||||||
expect(
|
|
||||||
await page
|
|
||||||
.locator('[data-testid="filter-arg"]')
|
|
||||||
.locator('input')
|
|
||||||
.inputValue()
|
|
||||||
).toBe(dateFormat(date));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clickDatePicker = async (page: Page) => {
|
|
||||||
await page.locator('[data-testid="filter-arg"]').locator('input').click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickMonthPicker = async (page: Page | Locator) => {
|
|
||||||
await page.locator('[data-testid="month-picker-button"]').click();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fillDatePicker = async (page: Page, date: Date) => {
|
|
||||||
await page
|
|
||||||
.locator('[data-testid="filter-arg"]')
|
|
||||||
.locator('input')
|
|
||||||
.fill(dateFormat(date));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectMonthFromMonthPicker = async (
|
|
||||||
page: Page | Locator,
|
|
||||||
date: Date
|
|
||||||
) => {
|
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
||||||
const year = date.getFullYear();
|
|
||||||
// Open the month picker popup
|
|
||||||
await clickMonthPicker(page);
|
|
||||||
const selectMonth = async (): Promise<void> => {
|
|
||||||
const selectedYear = +(await page
|
|
||||||
.getByTestId('month-picker-current-year')
|
|
||||||
.innerText());
|
|
||||||
if (selectedYear > year) {
|
|
||||||
await page.locator('[data-testid="date-picker-nav-prev"]').click();
|
|
||||||
return await selectMonth();
|
|
||||||
} else if (selectedYear < year) {
|
|
||||||
await page.locator('[data-testid="date-picker-nav-next"]').click();
|
|
||||||
return await selectMonth();
|
|
||||||
}
|
|
||||||
// Click on the day cell
|
|
||||||
const monthCell = page.locator(
|
|
||||||
`[data-is-month-cell][aria-label="${year}-${month}"]`
|
|
||||||
);
|
|
||||||
await monthCell.click();
|
|
||||||
};
|
|
||||||
await selectMonth();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkDatePickerMonth = async (
|
|
||||||
page: Page | Locator,
|
|
||||||
date: Date
|
|
||||||
) => {
|
|
||||||
expect(
|
|
||||||
await page.getByTestId('month-picker-button').evaluate(e => e.dataset.month)
|
|
||||||
).toBe(date.getMonth().toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTag = async (page: Page, name: string) => {
|
|
||||||
await page.keyboard.type(name);
|
|
||||||
await page.keyboard.press('ArrowUp');
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createPageWithTag = async (
|
|
||||||
page: Page,
|
|
||||||
options: {
|
|
||||||
title: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
await page.getByTestId('all-pages').click();
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).click();
|
|
||||||
await getBlockSuiteEditorTitle(page).fill('test page');
|
|
||||||
await page.getByTestId('page-info-collapse').click();
|
|
||||||
await page.locator('[data-testid="property-tags-value"]').click();
|
|
||||||
for (const name of options.tags) {
|
|
||||||
await createTag(page, name);
|
|
||||||
}
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changeFilter = async (page: Page, to: string) => {
|
|
||||||
await page.getByTestId('filter-name').click();
|
|
||||||
await page.getByTestId(`filler-tag-${to}`).click();
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function selectTag(page: Page, name: string | RegExp) {
|
export async function selectTag(page: Page, name: string | RegExp) {
|
||||||
await page.getByTestId('filter-arg').click();
|
await page.getByTestId('filter-arg').click();
|
||||||
await page.getByTestId(`multi-select-${name}`).click();
|
await page.getByTestId(`multi-select-${name}`).click();
|
||||||
|
|||||||
@@ -30,10 +30,13 @@ export async function waitForEditorLoad(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForAllPagesLoad(page: Page) {
|
export async function waitForAllPagesLoad(page: Page) {
|
||||||
// if filters tag is rendered, we believe all_pages is ready
|
// if page-list-header-selection-checkbox is rendered, we believe all_pages is ready
|
||||||
await page.waitForSelector('[data-testid="create-first-filter"]', {
|
await page.waitForSelector(
|
||||||
timeout: 20000,
|
'[data-testid="page-list-header-selection-checkbox"]',
|
||||||
});
|
{
|
||||||
|
timeout: 20000,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickNewPageButton(page: Page, title?: string) {
|
export async function clickNewPageButton(page: Page, title?: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user