fix(core): some storage setting enhancements (#9877)

fix AF-2157, AF-2155, AF-2156
1. add shift selection for grid blob card
2. various style issues
3. unused blobs list will also wait for workspace syncing
This commit is contained in:
pengx17
2025-01-24 04:35:54 +00:00
parent c0eb735890
commit 6a74107010
13 changed files with 189 additions and 101 deletions
@@ -2,4 +2,3 @@ export * from './accept-invite-page';
export * from './invite-modal';
export * from './invite-team-modal';
export * from './member-limit-modal';
export * from './pagination';
@@ -1,6 +1,6 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
export const inviteModalTitle = style({
fontWeight: '600',
@@ -35,52 +35,6 @@ export const userWrapper = style({
gap: '4px',
});
export const pagination = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '6px',
marginTop: 5,
});
export const pageItem = style({
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
width: '20px',
height: '20px',
fontSize: cssVar('fontXs'),
color: cssVarV2('text/primary'),
borderRadius: '4px',
selectors: {
'&:hover': {
background: cssVarV2('layer/background/hoverOverlay'),
},
'&.active': {
color: cssVarV2('text/emphasis'),
cursor: 'default',
pointerEvents: 'none',
},
'&.label': {
color: cssVarV2('icon/primary'),
fontSize: '16px',
},
'&.disabled': {
opacity: '.4',
cursor: 'default',
color: cssVarV2('text/disable'),
pointerEvents: 'none',
},
},
});
globalStyle(`${pageItem} a`, {
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const modalContent = style({
display: 'flex',
flexDirection: 'column',
@@ -1,3 +1,4 @@
export * from './pagination';
export { SettingHeader } from './setting-header';
export { SettingRow } from './setting-row';
export * from './workspace-detail-skeleton';
@@ -0,0 +1,50 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
export const pageItem = style({
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
width: '20px',
height: '20px',
fontSize: cssVar('fontXs'),
color: cssVarV2('text/primary'),
borderRadius: '4px',
selectors: {
'&:hover': {
background: cssVarV2('layer/background/hoverOverlay'),
},
'&.active': {
color: cssVarV2('text/emphasis'),
cursor: 'default',
pointerEvents: 'none',
},
'&.label': {
color: cssVarV2('icon/primary'),
fontSize: '16px',
},
'&.disabled': {
opacity: '.4',
cursor: 'default',
color: cssVarV2('text/disable'),
pointerEvents: 'none',
},
},
});
globalStyle(`${pageItem} a`, {
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const pagination = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '6px',
marginTop: 5,
});
@@ -3,7 +3,7 @@ import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import ReactPaginate from 'react-paginate';
import * as styles from './styles.css';
import * as styles from './pagination.css';
export interface PaginationProps {
totalCount: number;
pageNum?: number;
@@ -7,8 +7,10 @@ import {
Skeleton,
useConfirmModal,
} from '@affine/component';
import { Pagination } from '@affine/component/member-components';
import { SettingHeader } from '@affine/component/setting-components';
import {
Pagination,
SettingHeader,
} from '@affine/component/setting-components';
import { Avatar } from '@affine/component/ui/avatar';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
@@ -209,26 +211,30 @@ export const BackupSettingPanel = () => {
/>
);
}
if (backupWorkspaces?.items.length === 0) {
if (backupWorkspaces?.items.length === 0 || !backupWorkspaces) {
return <Empty />;
}
return (
<>
<div className={styles.list}>
{backupWorkspaces?.items
{backupWorkspaces.items
.slice(pageNum * PAGE_SIZE, (pageNum + 1) * PAGE_SIZE)
.map(item => <BackupWorkspaceItem key={item.id} item={item} />)}
</div>
<div className={styles.pagination}>
<Pagination
totalCount={backupWorkspaces?.items.length ?? 0}
countPerPage={PAGE_SIZE}
pageNum={pageNum}
onPageChange={(_, pageNum) => {
setPageNum(pageNum);
}}
/>
.map(item => (
<BackupWorkspaceItem key={item.id} item={item} />
))}
</div>
{backupWorkspaces.items.length > PAGE_SIZE && (
<div className={styles.pagination}>
<Pagination
totalCount={backupWorkspaces?.items.length ?? 0}
countPerPage={PAGE_SIZE}
pageNum={pageNum}
onPageChange={(_, pageNum) => {
setPageNum(pageNum);
}}
/>
</div>
)}
</>
);
}, [isLoading, backupWorkspaces, pageNum]);
@@ -15,7 +15,7 @@ export const listContainer = style({
export const list = style({
display: 'flex',
flexDirection: 'column',
gap: '8px',
gap: '4px',
});
export const listItem = style({
@@ -1,6 +1,6 @@
import { Skeleton } from '@affine/component';
import { Pagination } from '@affine/component/member-components';
import {
Pagination,
SettingHeader,
SettingRow,
SettingWrapper,
@@ -1,6 +1,6 @@
import { Button, Loading } from '@affine/component';
import { Pagination } from '@affine/component/member-components';
import {
Pagination,
SettingHeader,
SettingRow,
SettingWrapper,
@@ -1,5 +1,5 @@
import { Avatar, IconButton, Loading, Menu, notify } from '@affine/component';
import { Pagination } from '@affine/component/member-components';
import { Pagination } from '@affine/component/setting-components';
import { type AuthAccountInfo, AuthService } from '@affine/core/modules/cloud';
import {
type Member,
@@ -6,7 +6,7 @@ import {
useConfirmModal,
useDisposable,
} from '@affine/component';
import { Pagination } from '@affine/component/member-components';
import { Pagination } from '@affine/component/setting-components';
import { BlobManagementService } from '@affine/core/modules/blob-management/services';
import { useI18n } from '@affine/i18n';
import type { ListedBlobRecord } from '@affine/nbstore';
@@ -80,7 +80,8 @@ const BlobPreview = ({ blobRecord }: { blobRecord: ListedBlobRecord }) => {
<div className={styles.blobPreviewFooter}>
<div className={styles.blobPreviewName}>{blobRecord.key}</div>
<div className={styles.blobPreviewInfo}>
{data?.type} · {bytes(blobRecord.size)}
{data?.type ? `${data.type} · ` : ''}
{bytes(blobRecord.size)}
</div>
</div>
</div>
@@ -93,7 +94,7 @@ const BlobCard = ({
selected,
}: {
blobRecord: ListedBlobRecord;
onClick: () => void;
onClick: (e: React.MouseEvent) => void;
selected: boolean;
}) => {
return (
@@ -119,6 +120,8 @@ export const BlobManagementPanel = () => {
const isLoading = useLiveData(unusedBlobsEntity.isLoading$);
const [pageNum, setPageNum] = useState(0);
const [skip, setSkip] = useState(0);
const [selectionAnchor, setSelectionAnchor] =
useState<ListedBlobRecord | null>(null);
const [unusedBlobs, setUnusedBlobs] = useState<ListedBlobRecord[]>([]);
const unusedBlobsPage = useMemo(() => {
@@ -149,9 +152,60 @@ export const BlobManagementPanel = () => {
setSelectedBlobs(prev => prev.filter(b => b.key !== blob.key));
}, []);
const handleSelectAll = useCallback(() => {
unusedBlobsPage.forEach(blob => handleSelectBlob(blob));
}, [unusedBlobsPage, handleSelectBlob]);
const handleBlobClick = useCallback(
(blob: ListedBlobRecord, event: React.MouseEvent) => {
const isMetaKey = event.metaKey || event.ctrlKey;
if (event.shiftKey && selectionAnchor) {
// Shift+click: Select range from anchor to current
const anchorIndex = unusedBlobsPage.findIndex(
b => b.key === selectionAnchor.key
);
const currentIndex = unusedBlobsPage.findIndex(b => b.key === blob.key);
if (anchorIndex !== -1 && currentIndex !== -1) {
const start = Math.min(anchorIndex, currentIndex);
const end = Math.max(anchorIndex, currentIndex);
const blobsToSelect = unusedBlobsPage.slice(start, end + 1);
setSelectedBlobs(prev => {
// If meta/ctrl is also pressed, add to existing selection
const baseSelection = isMetaKey ? prev : [];
const newSelection = new Set([...baseSelection, ...blobsToSelect]);
return Array.from(newSelection);
});
}
} else {
if (selectedBlobs.includes(blob)) {
handleUnselectBlob(blob);
} else {
handleSelectBlob(blob);
}
if (selectedBlobs.length === 0) {
setSelectionAnchor(selectedBlobs.includes(blob) ? null : blob);
}
}
},
[
selectionAnchor,
unusedBlobsPage,
selectedBlobs,
handleSelectBlob,
handleUnselectBlob,
]
);
const handleSelectAll = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
unusedBlobsPage.forEach(blob => handleSelectBlob(blob));
},
[unusedBlobsPage, handleSelectBlob]
);
const showSelectAll = !unusedBlobsPage.every(blob =>
selectedBlobs.includes(blob)
);
const { openConfirmModal } = useConfirmModal();
@@ -193,10 +247,7 @@ export const BlobManagementPanel = () => {
if (blobPreviewGridRef.current) {
const unselectBlobs = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
!blobPreviewGridRef.current?.contains(target) &&
!target.closest('modal-transition-container')
) {
if (!blobPreviewGridRef.current?.contains(target)) {
setSelectedBlobs([]);
}
};
@@ -216,9 +267,11 @@ export const BlobManagementPanel = () => {
{`${selectedBlobs.length} ${t['com.affine.settings.workspace.storage.unused-blobs.selected']()}`}
</div>
<div className={styles.spacer} />
<Button onClick={handleSelectAll} variant="primary">
{t['com.affine.keyboardShortcuts.selectAll']()}
</Button>
{showSelectAll && (
<Button onClick={handleSelectAll} variant="primary">
{t['com.affine.keyboardShortcuts.selectAll']()}
</Button>
)}
<Button
loading={deleting}
onClick={handleDeleteSelectedBlobs}
@@ -243,31 +296,29 @@ export const BlobManagementPanel = () => {
) : (
<>
<div className={styles.blobPreviewGrid} ref={blobPreviewGridRef}>
{unusedBlobs.slice(skip, skip + PAGE_SIZE).map(blob => {
{unusedBlobsPage.map(blob => {
const selected = selectedBlobs.includes(blob);
return (
<BlobCard
key={blob.key}
blobRecord={blob}
onClick={() =>
selected
? handleUnselectBlob(blob)
: handleSelectBlob(blob)
}
onClick={e => handleBlobClick(blob, e)}
selected={selected}
/>
);
})}
</div>
<Pagination
pageNum={pageNum}
totalCount={unusedBlobs.length}
countPerPage={PAGE_SIZE}
onPageChange={(_, pageNum) => {
setPageNum(pageNum);
setSkip(pageNum * PAGE_SIZE);
}}
/>
{unusedBlobs.length > PAGE_SIZE && (
<Pagination
pageNum={pageNum}
totalCount={unusedBlobs.length}
countPerPage={PAGE_SIZE}
onPageChange={(_, pageNum) => {
setPageNum(pageNum);
setSkip(pageNum * PAGE_SIZE);
}}
/>
)}
</>
)}
</div>
@@ -77,6 +77,7 @@ export const blobCard = style({
borderRadius: '4px',
overflow: 'hidden',
position: 'relative',
userSelect: 'none',
});
export const loadingContainer = style({
@@ -123,7 +124,7 @@ export const blobGridItemCheckbox = style({
position: 'absolute',
top: 8,
right: 8,
fontSize: 16,
fontSize: 24,
opacity: 0,
selectors: {
[`${blobCard}:hover &`]: {
@@ -135,6 +136,10 @@ export const blobGridItemCheckbox = style({
},
});
globalStyle(`${blobGridItemCheckbox} path`, {
backgroundColor: cssVarV2('layer/background/primary'),
});
export const blobImagePreview = style({
width: '100%',
height: '100%',
@@ -8,7 +8,17 @@ import {
onStart,
} from '@toeverything/infra';
import { fileTypeFromBuffer } from 'file-type';
import { EMPTY, mergeMap, switchMap } from 'rxjs';
import {
combineLatest,
EMPTY,
filter,
firstValueFrom,
fromEvent,
map,
mergeMap,
switchMap,
takeUntil,
} from 'rxjs';
import type { DocsSearchService } from '../../docs-search';
import type { WorkspaceService } from '../../workspace';
@@ -91,10 +101,22 @@ export class UnusedBlobs extends Entity {
}
async getUnusedBlobs(abortSignal?: AbortSignal) {
// wait for the indexer to finish
await this.docsSearchService.indexer.status$.waitFor(
status => status.remaining === undefined || status.remaining === 0,
// Wait for both sync and indexing to complete
const ready$ = combineLatest([
this.workspaceService.workspace.engine.doc.state$.pipe(
filter(state => state.syncing === 0 && !state.syncRetrying)
),
this.docsSearchService.indexer.status$.pipe(
filter(
status => status.remaining === undefined || status.remaining === 0
)
),
]).pipe(map(() => true));
await firstValueFrom(
abortSignal
? ready$.pipe(takeUntil(fromEvent(abortSignal, 'abort')))
: ready$
);
const [blobs, usedBlobs] = await Promise.all([