feat(core): edit and delete pinned collections in all docs

This commit is contained in:
EYHN
2025-05-15 13:01:26 +09:00
parent dd113f8605
commit afa108d517
8 changed files with 228 additions and 65 deletions

View File

@@ -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']()}

View File

@@ -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',

View File

@@ -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']()}

View File

@@ -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(),

View File

@@ -14,6 +14,11 @@ export class PinnedCollectionService extends Service {
super();
}
pinnedCollectionDataReady$ = LiveData.from(
this.pinnedCollectionStore.watchPinnedCollectionDataReady(),
false
);
pinnedCollections$ = LiveData.from<PinnedCollectionRecord[]>(
this.pinnedCollectionStore.watchPinnedCollections(),
[]

View File

@@ -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),

View File

@@ -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$();
}

View File

@@ -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
);