mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 13:15:51 +08:00
feat: improve admin panel design (#14464)
This commit is contained in:
@@ -39,7 +39,7 @@ export const ConfirmDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:w-[460px]">
|
||||
<DialogContent className="sm:max-w-[460px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="leading-7">{title}</DialogTitle>
|
||||
<DialogDescription className="leading-6">
|
||||
@@ -48,13 +48,19 @@ export const ConfirmDialog = ({
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-6">
|
||||
<div className="flex justify-end gap-2 items-center w-full">
|
||||
<Button type="button" onClick={handleClose} variant="outline">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<span>{cancelText}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
variant={confirmButtonVariant}
|
||||
size="sm"
|
||||
>
|
||||
<span>{confirmText}</span>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { SharedDataTable } from './data-table';
|
||||
|
||||
const { DataTablePaginationMock } = vi.hoisted(() => ({
|
||||
DataTablePaginationMock: vi.fn(({ disabled }: { disabled?: boolean }) => (
|
||||
<div data-disabled={disabled ? 'true' : 'false'} data-testid="pagination" />
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock('./data-table-pagination', () => ({
|
||||
DataTablePagination: DataTablePaginationMock,
|
||||
}));
|
||||
|
||||
type Row = { id: string; name: string };
|
||||
|
||||
const columns: ColumnDef<Row>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
cell: ({ row }) => row.original.name,
|
||||
},
|
||||
];
|
||||
|
||||
describe('SharedDataTable', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
DataTablePaginationMock.mockClear();
|
||||
});
|
||||
|
||||
test('renders token-aligned table shell and row data', () => {
|
||||
const { container } = render(
|
||||
<SharedDataTable
|
||||
columns={columns}
|
||||
data={[{ id: '1', name: 'Alice' }]}
|
||||
totalCount={1}
|
||||
pagination={{ pageIndex: 0, pageSize: 10 }}
|
||||
onPaginationChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Alice')).not.toBeNull();
|
||||
|
||||
const shell = container.querySelector('.rounded-xl');
|
||||
expect(shell).not.toBeNull();
|
||||
expect(shell?.className).toContain('border-border');
|
||||
expect(shell?.className).toContain('bg-card');
|
||||
expect(shell?.className).toContain('shadow-1');
|
||||
});
|
||||
|
||||
test('shows loading overlay and disables pagination while loading', () => {
|
||||
render(
|
||||
<SharedDataTable
|
||||
columns={columns}
|
||||
data={[{ id: '1', name: 'Alice' }]}
|
||||
totalCount={1}
|
||||
pagination={{ pageIndex: 0, pageSize: 10 }}
|
||||
onPaginationChange={vi.fn()}
|
||||
loading={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Loading...')).not.toBeNull();
|
||||
expect(screen.getByTestId('pagination').dataset.disabled).toBe('true');
|
||||
});
|
||||
|
||||
test('renders empty state when there is no data', () => {
|
||||
render(
|
||||
<SharedDataTable
|
||||
columns={columns}
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
pagination={{ pageIndex: 0, pageSize: 10 }}
|
||||
onPaginationChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('No results.')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,8 @@ import { type ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import { DataTablePagination } from './data-table-pagination';
|
||||
|
||||
const DEFAULT_RESET_FILTERS_DEPS: unknown[] = [];
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
@@ -58,7 +60,7 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
|
||||
rowSelection,
|
||||
onRowSelectionChange,
|
||||
renderToolbar,
|
||||
resetFiltersDeps = [],
|
||||
resetFiltersDeps = DEFAULT_RESET_FILTERS_DEPS,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
||||
@@ -88,13 +90,13 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto relative">
|
||||
<div className="relative flex h-full flex-col gap-4 overflow-auto px-6 py-5">
|
||||
{renderToolbar?.(table)}
|
||||
<div className="rounded-md border h-full flex flex-col overflow-auto relative">
|
||||
<div className="relative flex h-full flex-col overflow-auto rounded-xl border border-border/60 bg-card shadow-1">
|
||||
{loading ? (
|
||||
<div className="absolute inset-0 z-10 bg-gray-50/70 backdrop-blur-[1px] flex flex-col items-center justify-center gap-2 text-sm text-gray-600">
|
||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-background/75 text-sm text-muted-foreground backdrop-blur-[1px]">
|
||||
<svg
|
||||
className="h-5 w-5 animate-spin text-gray-500"
|
||||
className="h-5 w-5 animate-spin text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -119,7 +121,10 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<TableRow key={headerGroup.id} className="flex items-center">
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="flex items-center bg-muted/40"
|
||||
>
|
||||
{headerGroup.headers.map(header => {
|
||||
// Use meta.className if available, otherwise default to flex-1
|
||||
const meta = header.column.columnDef.meta as
|
||||
@@ -154,7 +159,7 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="flex items-center"
|
||||
className="flex items-center bg-card"
|
||||
>
|
||||
{row.getVisibleCells().map(cell => {
|
||||
const meta = cell.column.columnDef.meta as
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Label } from '@affine/admin/components/ui/label';
|
||||
import { Separator } from '@affine/admin/components/ui/separator';
|
||||
import { Switch } from '@affine/admin/components/ui/switch';
|
||||
import type { FeatureType } from '@affine/graphql';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
@@ -42,10 +41,7 @@ export const FeatureToggleList = ({
|
||||
|
||||
if (!features.length) {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'px-3 py-2 text-xs')}
|
||||
style={{ color: cssVarV2('text/secondary') }}
|
||||
>
|
||||
<div className={cn(className, 'px-3 py-2 text-xs text-muted-foreground')}>
|
||||
No configurable features.
|
||||
</div>
|
||||
);
|
||||
@@ -57,10 +53,10 @@ export const FeatureToggleList = ({
|
||||
<div key={feature}>
|
||||
<Label
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
'cursor-pointer transition-colors duration-100',
|
||||
controlPosition === 'right'
|
||||
? 'flex items-center justify-between p-3 text-[15px] gap-2 font-medium leading-6 overflow-hidden'
|
||||
: 'flex items-center gap-2 px-3 py-2 text-sm'
|
||||
? 'flex items-center justify-between p-3 text-sm gap-2 font-medium leading-6 overflow-hidden hover:bg-muted/40'
|
||||
: 'flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
{controlPosition === 'left' ? (
|
||||
|
||||
Reference in New Issue
Block a user