feat: improve admin panel design (#14464)

This commit is contained in:
DarkSky
2026-02-17 17:40:29 +08:00
committed by GitHub
parent 850e646ab9
commit 8f833388eb
86 changed files with 2633 additions and 1431 deletions
@@ -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' ? (