From 6a74107010905f4d034a9eae091933f3606824b6 Mon Sep 17 00:00:00 2001 From: pengx17 Date: Fri, 24 Jan 2025 04:35:54 +0000 Subject: [PATCH] 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 --- .../components/member-components/index.tsx | 1 - .../member-components/styles.css.ts | 48 +------- .../components/setting-components/index.tsx | 1 + .../setting-components/pagination.css.ts | 50 ++++++++ .../pagination.tsx | 2 +- .../setting/general-setting/backup/index.tsx | 36 +++--- .../general-setting/backup/styles.css.ts | 2 +- .../setting/general-setting/billing/index.tsx | 2 +- .../workspace-setting/billing/index.tsx | 2 +- .../workspace-setting/members/member-list.tsx | 2 +- .../storage/blob-management.tsx | 107 +++++++++++++----- .../workspace-setting/storage/style.css.ts | 7 +- .../blob-management/entity/unused-blobs.ts | 30 ++++- 13 files changed, 189 insertions(+), 101 deletions(-) create mode 100644 packages/frontend/component/src/components/setting-components/pagination.css.ts rename packages/frontend/component/src/components/{member-components => setting-components}/pagination.tsx (97%) diff --git a/packages/frontend/component/src/components/member-components/index.tsx b/packages/frontend/component/src/components/member-components/index.tsx index 1a37fddbbe..f85993ade1 100644 --- a/packages/frontend/component/src/components/member-components/index.tsx +++ b/packages/frontend/component/src/components/member-components/index.tsx @@ -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'; diff --git a/packages/frontend/component/src/components/member-components/styles.css.ts b/packages/frontend/component/src/components/member-components/styles.css.ts index b52bf41c6e..6b377e6e99 100644 --- a/packages/frontend/component/src/components/member-components/styles.css.ts +++ b/packages/frontend/component/src/components/member-components/styles.css.ts @@ -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', diff --git a/packages/frontend/component/src/components/setting-components/index.tsx b/packages/frontend/component/src/components/setting-components/index.tsx index d26bc5d939..9743663d3d 100644 --- a/packages/frontend/component/src/components/setting-components/index.tsx +++ b/packages/frontend/component/src/components/setting-components/index.tsx @@ -1,3 +1,4 @@ +export * from './pagination'; export { SettingHeader } from './setting-header'; export { SettingRow } from './setting-row'; export * from './workspace-detail-skeleton'; diff --git a/packages/frontend/component/src/components/setting-components/pagination.css.ts b/packages/frontend/component/src/components/setting-components/pagination.css.ts new file mode 100644 index 0000000000..abeed9f15d --- /dev/null +++ b/packages/frontend/component/src/components/setting-components/pagination.css.ts @@ -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, +}); diff --git a/packages/frontend/component/src/components/member-components/pagination.tsx b/packages/frontend/component/src/components/setting-components/pagination.tsx similarity index 97% rename from packages/frontend/component/src/components/member-components/pagination.tsx rename to packages/frontend/component/src/components/setting-components/pagination.tsx index 39f79269f1..6db69754d5 100644 --- a/packages/frontend/component/src/components/member-components/pagination.tsx +++ b/packages/frontend/component/src/components/setting-components/pagination.tsx @@ -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; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx index 439951dd9c..2bc71d5330 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx @@ -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 ; } return ( <>
- {backupWorkspaces?.items + {backupWorkspaces.items .slice(pageNum * PAGE_SIZE, (pageNum + 1) * PAGE_SIZE) - .map(item => )} -
-
- { - setPageNum(pageNum); - }} - /> + .map(item => ( + + ))}
+ {backupWorkspaces.items.length > PAGE_SIZE && ( +
+ { + setPageNum(pageNum); + }} + /> +
+ )} ); }, [isLoading, backupWorkspaces, pageNum]); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/styles.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/styles.css.ts index b03c1f005c..37035afe39 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/styles.css.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/styles.css.ts @@ -15,7 +15,7 @@ export const listContainer = style({ export const list = style({ display: 'flex', flexDirection: 'column', - gap: '8px', + gap: '4px', }); export const listItem = style({ diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx index 0938579fe9..ca759ebf29 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx @@ -1,6 +1,6 @@ import { Skeleton } from '@affine/component'; -import { Pagination } from '@affine/component/member-components'; import { + Pagination, SettingHeader, SettingRow, SettingWrapper, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx index e9cce2bfbf..c1fdf3e868 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx @@ -1,6 +1,6 @@ import { Button, Loading } from '@affine/component'; -import { Pagination } from '@affine/component/member-components'; import { + Pagination, SettingHeader, SettingRow, SettingWrapper, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx index 91be4aeaae..7d037b40ba 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx @@ -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, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/blob-management.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/blob-management.tsx index 2f4fdf5a53..d851d0d3e2 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/blob-management.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/blob-management.tsx @@ -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 }) => {
{blobRecord.key}
- {data?.type} · {bytes(blobRecord.size)} + {data?.type ? `${data.type} · ` : ''} + {bytes(blobRecord.size)}
@@ -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(null); const [unusedBlobs, setUnusedBlobs] = useState([]); 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']()}`}
- + {showSelectAll && ( + + )}
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/style.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/style.css.ts index e667e4e3ed..aff8fef9ca 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/style.css.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/storage/style.css.ts @@ -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%', diff --git a/packages/frontend/core/src/modules/blob-management/entity/unused-blobs.ts b/packages/frontend/core/src/modules/blob-management/entity/unused-blobs.ts index 7143d0452d..12f335b1f4 100644 --- a/packages/frontend/core/src/modules/blob-management/entity/unused-blobs.ts +++ b/packages/frontend/core/src/modules/blob-management/entity/unused-blobs.ts @@ -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([