mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 17:16:16 +08:00
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:
@@ -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,
|
||||
});
|
||||
+1
-1
@@ -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;
|
||||
+21
-15
@@ -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]);
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import { Pagination } from '@affine/component/member-components';
|
||||
import {
|
||||
Pagination,
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Button, Loading } from '@affine/component';
|
||||
import { Pagination } from '@affine/component/member-components';
|
||||
import {
|
||||
Pagination,
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+79
-28
@@ -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>
|
||||
|
||||
+6
-1
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user