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 && (
+
+ )}