mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 17:16:07 +00:00
7078abc14a
Storybook existed only as an undocumented local tool: 9 of 24 reusable components had stories, autodocs pages were bare prop tables, nothing built or tested the stories, and no contributor doc mentioned the workbench existed. Every reusable component under src/components/ now has a co-located story with enriched autodocs (component descriptions plus per-prop argTypes, kept as string metadata since the repo bans line comments). Stories double as headless Chromium tests through the Storybook vitest addon, with axe accessibility checks enforced as errors and play-function interaction tests covering the modals, the RHF field bridge, the config block, and the select-all buttons. The preview now mirrors the panel's real theme DOM (body class, shared AntD theme config, seeded theme storage) so what stories render matches production. CI and make verify gain a static Storybook build as a compile gate, and the frontend test job installs Chromium so story tests run on every PR. Contributor docs (frontend README, CONTRIBUTING, agent guides) document the workbench, the story conventions, and the Controls setup. Node engines move to 24 LTS and gen:api drops the type-stripping flags that Node 24 makes default.
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
import { useState } from 'react';
|
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
import { expect } from 'storybook/test';
|
|
import { Select } from 'antd';
|
|
|
|
import SelectAllClearButtons from './SelectAllClearButtons';
|
|
|
|
const inboundOptions: Array<{ value: number; label: string }> = [
|
|
{ value: 1, label: 'VLESS Reality — 443' },
|
|
{ value: 2, label: 'VMess WS — 8443' },
|
|
{ value: 3, label: 'Trojan TCP — 2053' },
|
|
{ value: 4, label: 'Shadowsocks — 8388' },
|
|
];
|
|
|
|
const clientEmailOptions: Array<{ value: string; label: string }> = [
|
|
{ value: 'ava@corp.example', label: 'ava@corp.example' },
|
|
{ value: 'reza.mobile', label: 'reza.mobile' },
|
|
{ value: 'office-tv', label: 'office-tv' },
|
|
{ value: 'guest-42', label: 'guest-42' },
|
|
];
|
|
|
|
const meta = {
|
|
title: 'Form/SelectAllClearButtons',
|
|
component: SelectAllClearButtons,
|
|
tags: ['autodocs'],
|
|
parameters: {
|
|
layout: 'padded',
|
|
docs: {
|
|
description: {
|
|
component:
|
|
'Small "Select all" / "Clear all" button pair rendered above a multi-select. The panel places it over the attached-inbounds picker in the client form and the bulk attach/detach modals.',
|
|
},
|
|
},
|
|
},
|
|
argTypes: {
|
|
options: { description: 'Option list whose values define the "all" set; matches the AntD Select option shape.' },
|
|
value: { description: 'Currently selected values (controlled).' },
|
|
onChange: { description: 'Called with the union of the current selection and every option value, or with an empty array on clear.' },
|
|
selectAllLabel: { description: 'Override for the "Select all" button text; defaults to the translated inbound copy.' },
|
|
clearLabel: { description: 'Override for the "Clear all" button text; defaults to the translated inbound copy.' },
|
|
},
|
|
} satisfies Meta<typeof SelectAllClearButtons>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
function InboundPickerDemo() {
|
|
const [selected, setSelected] = useState<number[]>([1]);
|
|
return (
|
|
<div style={{ maxWidth: 360 }}>
|
|
<SelectAllClearButtons options={inboundOptions} value={selected} onChange={setSelected} />
|
|
<Select
|
|
mode="multiple"
|
|
style={{ width: '100%' }}
|
|
value={selected}
|
|
onChange={setSelected}
|
|
options={inboundOptions}
|
|
placeholder="Select inbounds"
|
|
aria-label="Select inbounds"
|
|
maxTagCount="responsive"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ClientEmailsDemo() {
|
|
const [selected, setSelected] = useState<string[]>(['ava@corp.example']);
|
|
return (
|
|
<div style={{ maxWidth: 360 }}>
|
|
<SelectAllClearButtons
|
|
options={clientEmailOptions}
|
|
value={selected}
|
|
onChange={setSelected}
|
|
selectAllLabel="Select all clients"
|
|
clearLabel="Deselect clients"
|
|
/>
|
|
<Select
|
|
mode="multiple"
|
|
style={{ width: '100%' }}
|
|
value={selected}
|
|
onChange={setSelected}
|
|
options={clientEmailOptions}
|
|
placeholder="Select clients"
|
|
aria-label="Select clients"
|
|
maxTagCount="responsive"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const placeholderArgs = {
|
|
options: [],
|
|
value: [],
|
|
onChange: () => undefined,
|
|
};
|
|
|
|
export const PartiallySelected: Story = {
|
|
args: {
|
|
options: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }],
|
|
value: [1, 3],
|
|
onChange: () => undefined,
|
|
},
|
|
};
|
|
|
|
export const AllSelected: Story = {
|
|
args: {
|
|
options: [{ value: 1 }, { value: 2 }, { value: 3 }],
|
|
value: [1, 2, 3],
|
|
onChange: () => undefined,
|
|
},
|
|
};
|
|
|
|
export const WithInboundSelect: Story = {
|
|
args: placeholderArgs,
|
|
render: () => <InboundPickerDemo />,
|
|
};
|
|
|
|
export const CustomLabels: Story = {
|
|
args: placeholderArgs,
|
|
render: () => <ClientEmailsDemo />,
|
|
play: async ({ canvas, userEvent }) => {
|
|
const selectAll = canvas.getByRole('button', { name: 'Select all clients' });
|
|
const clearAll = canvas.getByRole('button', { name: 'Deselect clients' });
|
|
await expect(selectAll).toBeEnabled();
|
|
await userEvent.click(selectAll);
|
|
await expect(selectAll).toBeDisabled();
|
|
await userEvent.click(clearAll);
|
|
await expect(clearAll).toBeDisabled();
|
|
await expect(selectAll).toBeEnabled();
|
|
},
|
|
};
|