mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
Compare commits
1 Commits
v0.26.0-be
...
graphite-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa108d517 |
@@ -114,6 +114,18 @@ export const AllPage = () => {
|
||||
const collectionService = useService(CollectionService);
|
||||
const pinnedCollectionService = useService(PinnedCollectionService);
|
||||
|
||||
const isCollectionDataReady = useLiveData(
|
||||
collectionService.collectionDataReady$
|
||||
);
|
||||
|
||||
const isPinnedCollectionDataReady = useLiveData(
|
||||
pinnedCollectionService.pinnedCollectionDataReady$
|
||||
);
|
||||
|
||||
const pinnedCollections = useLiveData(
|
||||
pinnedCollectionService.pinnedCollections$
|
||||
);
|
||||
|
||||
const [selectedCollectionId, setSelectedCollectionId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
@@ -124,17 +136,28 @@ export const AllPage = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if selected collection is not found, set selected collection id to null
|
||||
if (!selectedCollection && selectedCollectionId) {
|
||||
// if selected collection is not in pinned collections, set selected collection id to null
|
||||
if (
|
||||
isPinnedCollectionDataReady &&
|
||||
selectedCollectionId &&
|
||||
!pinnedCollections.some(c => c.collectionId === selectedCollectionId)
|
||||
) {
|
||||
setSelectedCollectionId(null);
|
||||
}
|
||||
}, [selectedCollection, selectedCollectionId]);
|
||||
}, [isPinnedCollectionDataReady, pinnedCollections, selectedCollectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
// if selected collection is not found, set selected collection id to null
|
||||
if (!selectedCollection && selectedCollectionId && isCollectionDataReady) {
|
||||
setSelectedCollectionId(null);
|
||||
}
|
||||
}, [isCollectionDataReady, selectedCollection, selectedCollectionId]);
|
||||
|
||||
const selectedCollectionInfo = useLiveData(
|
||||
selectedCollection ? selectedCollection.info$ : null
|
||||
);
|
||||
|
||||
const [tempFilters, setTempFilters] = useState<FilterParams[]>([]);
|
||||
const [tempFilters, setTempFilters] = useState<FilterParams[] | null>(null);
|
||||
|
||||
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||
|
||||
@@ -178,10 +201,9 @@ export const AllPage = () => {
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch(
|
||||
// collection filters and temp filters can't exist at the same time
|
||||
selectedCollectionInfo
|
||||
? {
|
||||
filters: selectedCollectionInfo.rules.filters,
|
||||
filters: tempFilters ?? selectedCollectionInfo.rules.filters,
|
||||
groupBy,
|
||||
orderBy,
|
||||
extraAllowList: selectedCollectionInfo.allowList,
|
||||
@@ -303,42 +325,74 @@ export const AllPage = () => {
|
||||
});
|
||||
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
||||
|
||||
const handleSelectCollection = useCallback((collectionId: string) => {
|
||||
setSelectedCollectionId(collectionId);
|
||||
setTempFilters(null);
|
||||
}, []);
|
||||
|
||||
const handleEditCollection = useCallback(
|
||||
(collectionId: string) => {
|
||||
const collection = collectionService.collection$(collectionId).value;
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
setSelectedCollectionId(collectionId);
|
||||
setTempFilters(collection.info$.value.rules.filters);
|
||||
},
|
||||
[collectionService]
|
||||
);
|
||||
|
||||
const handleSaveFilters = useCallback(() => {
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
children: t['com.affine.editCollectionName.createTips'](),
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
const id = collectionService.createCollection({
|
||||
name,
|
||||
rules: {
|
||||
filters: tempFilters,
|
||||
},
|
||||
});
|
||||
pinnedCollectionService.addPinnedCollection({
|
||||
collectionId: id,
|
||||
index: pinnedCollectionService.indexAt('after'),
|
||||
});
|
||||
setTempFilters([]);
|
||||
setSelectedCollectionId(id);
|
||||
},
|
||||
});
|
||||
if (selectedCollectionId) {
|
||||
collectionService.updateCollection(selectedCollectionId, {
|
||||
rules: {
|
||||
filters: tempFilters ?? [],
|
||||
},
|
||||
});
|
||||
setTempFilters(null);
|
||||
} else {
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
children: t['com.affine.editCollectionName.createTips'](),
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
const id = collectionService.createCollection({
|
||||
name,
|
||||
rules: {
|
||||
filters: tempFilters ?? [],
|
||||
},
|
||||
});
|
||||
pinnedCollectionService.addPinnedCollection({
|
||||
collectionId: id,
|
||||
index: pinnedCollectionService.indexAt('after'),
|
||||
});
|
||||
setTempFilters(null);
|
||||
setSelectedCollectionId(id);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
collectionService,
|
||||
openPromptModal,
|
||||
pinnedCollectionService,
|
||||
selectedCollectionId,
|
||||
t,
|
||||
tempFilters,
|
||||
]);
|
||||
|
||||
const handleNewTempFilter = useCallback((params: FilterParams) => {
|
||||
setSelectedCollectionId(null);
|
||||
setTempFilters([params]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewTitle title={t['All pages']()} />
|
||||
@@ -352,29 +406,24 @@ export const AllPage = () => {
|
||||
<div className={styles.pinnedCollection}>
|
||||
<PinnedCollections
|
||||
activeCollectionId={selectedCollectionId}
|
||||
onClickAll={() => setSelectedCollectionId(null)}
|
||||
onClickCollection={collectionId => {
|
||||
setSelectedCollectionId(collectionId);
|
||||
setTempFilters([]);
|
||||
}}
|
||||
onAddFilter={params => {
|
||||
setSelectedCollectionId(null);
|
||||
setTempFilters([...(tempFilters ?? []), params]);
|
||||
}}
|
||||
hiddenAdd={tempFilters.length > 0}
|
||||
onActiveAll={() => setSelectedCollectionId(null)}
|
||||
onActiveCollection={handleSelectCollection}
|
||||
onAddFilter={handleNewTempFilter}
|
||||
onEditCollection={handleEditCollection}
|
||||
hiddenAdd={tempFilters !== null}
|
||||
/>
|
||||
</div>
|
||||
{tempFilters.length > 0 && (
|
||||
{tempFilters !== null && (
|
||||
<div className={styles.filterArea}>
|
||||
<Filters
|
||||
className={styles.filters}
|
||||
filters={tempFilters ?? []}
|
||||
filters={tempFilters}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
setTempFilters([]);
|
||||
setTempFilters(null);
|
||||
}}
|
||||
>
|
||||
{t['Cancel']()}
|
||||
|
||||
@@ -27,6 +27,38 @@ export const item = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const itemContent = style({
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
textAlign: 'center',
|
||||
maxWidth: '128px',
|
||||
minWidth: '32px',
|
||||
|
||||
selectors: {
|
||||
[`${item}:hover > &`]: {
|
||||
mask:
|
||||
'linear-gradient(#fff) left / calc(100% - 32px) no-repeat,' +
|
||||
'linear-gradient(90deg,#fff 0%,transparent 50%,transparent 100%) right / 32px no-repeat',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const editIconButton = style({
|
||||
opacity: 0,
|
||||
marginLeft: -16,
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
|
||||
selectors: {
|
||||
[`${item}:hover > &`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const closeButton = style({});
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
} from '@affine/core/modules/collection';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CollectionsIcon, FilterIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
CloseIcon,
|
||||
CollectionsIcon,
|
||||
EditIcon,
|
||||
FilterIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
@@ -17,10 +23,14 @@ export const PinnedCollectionItem = ({
|
||||
record,
|
||||
isActive,
|
||||
onClick,
|
||||
onClickRemove,
|
||||
onClickEdit,
|
||||
}: {
|
||||
record: PinnedCollectionRecord;
|
||||
onClickRemove: () => void;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
onClickEdit: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const collectionService = useService(CollectionService);
|
||||
@@ -38,22 +48,46 @@ export const PinnedCollectionItem = ({
|
||||
data-active={isActive ? 'true' : undefined}
|
||||
onClick={onClick}
|
||||
>
|
||||
{name ?? t['Untitled']()}
|
||||
<span className={styles.itemContent}>{name ?? t['Untitled']()}</span>
|
||||
<IconButton
|
||||
size="16"
|
||||
className={styles.editIconButton}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClickEdit();
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
{isActive && (
|
||||
<IconButton
|
||||
className={styles.closeButton}
|
||||
size="16"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClickRemove();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PinnedCollections = ({
|
||||
activeCollectionId,
|
||||
onClickAll,
|
||||
onClickCollection,
|
||||
onActiveAll,
|
||||
onActiveCollection,
|
||||
onAddFilter,
|
||||
onEditCollection,
|
||||
hiddenAdd,
|
||||
}: {
|
||||
activeCollectionId: string | null;
|
||||
onClickAll: () => void;
|
||||
onClickCollection: (collectionId: string) => void;
|
||||
onActiveAll: () => void;
|
||||
onActiveCollection: (collectionId: string) => void;
|
||||
onAddFilter: (params: FilterParams) => void;
|
||||
onEditCollection: (collectionId: string) => void;
|
||||
hiddenAdd?: boolean;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
@@ -74,22 +108,32 @@ export const PinnedCollections = ({
|
||||
<div
|
||||
className={styles.item}
|
||||
data-active={activeCollectionId === null ? 'true' : undefined}
|
||||
onClick={onClickAll}
|
||||
onClick={onActiveAll}
|
||||
role="button"
|
||||
>
|
||||
{t['com.affine.all-docs.pinned-collection.all']()}
|
||||
</div>
|
||||
{pinnedCollections.map(record => (
|
||||
{pinnedCollections.map((record, index) => (
|
||||
<PinnedCollectionItem
|
||||
key={record.collectionId}
|
||||
record={record}
|
||||
isActive={activeCollectionId === record.collectionId}
|
||||
onClick={() => onClickCollection(record.collectionId)}
|
||||
onClick={() => onActiveCollection(record.collectionId)}
|
||||
onClickEdit={() => onEditCollection(record.collectionId)}
|
||||
onClickRemove={() => {
|
||||
const nextCollectionId = pinnedCollections[index - 1]?.collectionId;
|
||||
if (nextCollectionId) {
|
||||
onActiveCollection(nextCollectionId);
|
||||
} else {
|
||||
onActiveAll();
|
||||
}
|
||||
pinnedCollectionService.removePinnedCollection(record.collectionId);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!hiddenAdd && (
|
||||
<AddPinnedCollection
|
||||
onAddPinnedCollection={handleAddPinnedCollection}
|
||||
onPinCollection={handleAddPinnedCollection}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
)}
|
||||
@@ -98,17 +142,17 @@ export const PinnedCollections = ({
|
||||
};
|
||||
|
||||
export const AddPinnedCollection = ({
|
||||
onAddPinnedCollection,
|
||||
onPinCollection,
|
||||
onAddFilter,
|
||||
}: {
|
||||
onAddPinnedCollection: (collectionId: string) => void;
|
||||
onPinCollection: (collectionId: string) => void;
|
||||
onAddFilter: (params: FilterParams) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
items={
|
||||
<AddPinnedCollectionMenuContent
|
||||
onAddPinnedCollection={onAddPinnedCollection}
|
||||
onPinCollection={onPinCollection}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
}
|
||||
@@ -121,10 +165,10 @@ export const AddPinnedCollection = ({
|
||||
};
|
||||
|
||||
export const AddPinnedCollectionMenuContent = ({
|
||||
onAddPinnedCollection,
|
||||
onPinCollection,
|
||||
onAddFilter,
|
||||
}: {
|
||||
onAddPinnedCollection: (collectionId: string) => void;
|
||||
onPinCollection: (collectionId: string) => void;
|
||||
onAddFilter: (params: FilterParams) => void;
|
||||
}) => {
|
||||
const [addingFilter, setAddingFilter] = useState<boolean>(false);
|
||||
@@ -167,7 +211,7 @@ export const AddPinnedCollectionMenuContent = ({
|
||||
prefixIcon={<CollectionsIcon />}
|
||||
suffixIcon={<PlusIcon />}
|
||||
onClick={() => {
|
||||
onAddPinnedCollection(meta.id);
|
||||
onPinCollection(meta.id);
|
||||
}}
|
||||
>
|
||||
{meta.name ?? t['Untitled']()}
|
||||
|
||||
@@ -19,6 +19,11 @@ export class CollectionService extends Service {
|
||||
},
|
||||
});
|
||||
|
||||
readonly collectionDataReady$ = LiveData.from(
|
||||
this.store.watchCollectionDataReady(),
|
||||
false
|
||||
);
|
||||
|
||||
// collection metas used in collection list, only include `id` and `name`, without `rules` and `allowList`
|
||||
readonly collectionMetas$ = LiveData.from(
|
||||
this.store.watchCollectionMetas(),
|
||||
|
||||
@@ -14,6 +14,11 @@ export class PinnedCollectionService extends Service {
|
||||
super();
|
||||
}
|
||||
|
||||
pinnedCollectionDataReady$ = LiveData.from(
|
||||
this.pinnedCollectionStore.watchPinnedCollectionDataReady(),
|
||||
false
|
||||
);
|
||||
|
||||
pinnedCollections$ = LiveData.from<PinnedCollectionRecord[]>(
|
||||
this.pinnedCollectionStore.watchPinnedCollections(),
|
||||
[]
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { map, type Observable, switchMap } from 'rxjs';
|
||||
import { distinctUntilChanged, map, type Observable, switchMap } from 'rxjs';
|
||||
import { Array as YArray } from 'yjs';
|
||||
|
||||
import type { FilterParams } from '../../collection-rules';
|
||||
@@ -35,6 +35,17 @@ export class CollectionStore extends Store {
|
||||
return this.rootYDoc.getMap('setting');
|
||||
}
|
||||
|
||||
watchCollectionDataReady() {
|
||||
return this.workspaceService.workspace.engine.doc
|
||||
.docState$(this.workspaceService.workspace.id)
|
||||
.pipe(
|
||||
map(docState => {
|
||||
return docState.ready;
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
watchCollectionMetas() {
|
||||
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
|
||||
switchMap(yjsObserveDeep),
|
||||
|
||||
@@ -13,6 +13,10 @@ export class PinnedCollectionStore extends Store {
|
||||
super();
|
||||
}
|
||||
|
||||
watchPinnedCollectionDataReady() {
|
||||
return this.workspaceDBService.db.pinnedCollections.isReady$;
|
||||
}
|
||||
|
||||
watchPinnedCollections(): Observable<PinnedCollectionRecord[]> {
|
||||
return this.workspaceDBService.db.pinnedCollections.find$();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
TableSchemaBuilder,
|
||||
} from '@toeverything/infra';
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs';
|
||||
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
|
||||
@@ -19,10 +19,23 @@ export class WorkspaceDBTable<
|
||||
super();
|
||||
}
|
||||
|
||||
isReady$ = LiveData.from(
|
||||
this.workspaceService.workspace.engine.doc
|
||||
.docState$(this.props.storageDocId)
|
||||
.pipe(
|
||||
map(docState => docState.ready),
|
||||
distinctUntilChanged()
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
isSyncing$ = LiveData.from(
|
||||
this.workspaceService.workspace.engine.doc
|
||||
.docState$(this.props.storageDocId)
|
||||
.pipe(map(docState => docState.syncing)),
|
||||
.pipe(
|
||||
map(docState => docState.syncing),
|
||||
distinctUntilChanged()
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user