Refactor admin components to use atomic component library and add utilities

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 15:20:10 +00:00
parent d7d5bbfb2b
commit ef1a912833
7 changed files with 283 additions and 28 deletions

View File

@@ -1,23 +1,21 @@
'use client';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import { useState } from 'react';
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
IconButton,
Checkbox,
MenuItem,
Select,
TextField,
Typography,
} from '@mui/material';
import { useState } from 'react';
} from '../atoms';
import Button from '../atoms/Button';
import TextField from '../atoms/TextField';
import Typography from '../atoms/Typography';
import IconButton from '../atoms/IconButton';
type Column = {
name: string;
@@ -141,21 +139,15 @@ export default function CreateTableDialog({
sx={{ mr: 1 }}
/>
{columns.length > 1 && (
<IconButton onClick={() => removeColumn(index)} color="error" size="small">
<DeleteIcon />
</IconButton>
<IconButton onClick={() => removeColumn(index)} color="error" size="small" icon="Delete" />
)}
</Box>
))}
<Button startIcon={<AddIcon />} onClick={addColumn} variant="outlined">
Add Column
</Button>
<Button startIcon="Add" onClick={addColumn} variant="outlined" text="Add Column" />
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleCreate} variant="contained" disabled={loading || !tableName.trim()}>
Create Table
</Button>
<Button onClick={handleClose} text="Cancel" />
<Button onClick={handleCreate} variant="contained" disabled={loading || !tableName.trim()} text="Create Table" />
</DialogActions>
</Dialog>
);

View File

@@ -9,7 +9,7 @@ import {
TableHead,
TableRow,
Tooltip,
} from '@mui/material';
} from '../atoms';
import IconButton from '../atoms/IconButton';
type DataGridProps = {

View File

@@ -1,16 +1,16 @@
'use client';
import { useState } from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
Select,
Typography,
} from '@mui/material';
import { useState } from 'react';
} from '../atoms';
import Button from '../atoms/Button';
import Typography from '../atoms/Typography';
type DropTableDialogProps = {
open: boolean;
@@ -70,15 +70,14 @@ export default function DropTableDialog({
</Select>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose} text="Cancel" />
<Button
onClick={handleDrop}
color="error"
variant="contained"
disabled={loading || !selectedTable}
>
Drop Table
</Button>
text="Drop Table"
/>
</DialogActions>
</Dialog>
);

View File

@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
import { generateMeta, generateStories } from '@/utils/storybook/storyGenerator';
/**
* Example of using story generator with features.json configuration
* This demonstrates how to leverage the storybookStories section from features.json
*/
// Generate meta from features.json
const meta = generateMeta(Button, 'Button', {
title: 'Atoms/Button',
}) satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Generate stories from features.json
const generatedStories = generateStories<typeof Button>('Button');
// Export individual stories
export const Primary: Story = generatedStories.primary || {
args: {
variant: 'contained',
color: 'primary',
text: 'Primary Button',
},
};
export const Secondary: Story = generatedStories.secondary || {
args: {
variant: 'outlined',
color: 'secondary',
text: 'Secondary Button',
},
};
export const WithIcon: Story = generatedStories.withIcon || {
args: {
variant: 'contained',
startIcon: 'Add',
text: 'Add Item',
},
};
export const Loading: Story = generatedStories.loading || {
args: {
variant: 'contained',
disabled: true,
text: 'Loading...',
},
};

View File

@@ -44,4 +44,5 @@ export {
AccordionSummary,
AccordionDetails,
Chip,
Tooltip,
} from '@mui/material';

View File

@@ -0,0 +1,76 @@
import type { Meta, StoryObj } from '@storybook/react';
import { getAllStorybookStories, getStorybookStoriesForComponent, StorybookStory } from '@/utils/featureConfig';
/**
* Generate Storybook meta configuration from features.json
*/
export function generateMeta<T>(
component: T,
componentName: string,
customMeta?: Partial<Meta<T>>
): Meta<T> {
const stories = getStorybookStoriesForComponent(componentName);
const defaultStory = stories.default;
return {
title: `Components/${componentName}`,
component: component as any,
parameters: {
layout: 'centered',
...defaultStory?.parameters,
},
tags: ['autodocs'],
...customMeta,
};
}
/**
* Generate a single story from features.json story definition
*/
export function generateStory<T>(
storyConfig: StorybookStory
): StoryObj<T> {
return {
name: storyConfig.name,
args: storyConfig.args || {},
parameters: storyConfig.parameters,
// Note: play functions would need to be converted from strings to actual functions
// This is a limitation of JSON - we can only store the play steps as strings
};
}
/**
* Generate all stories for a component from features.json
*/
export function generateStories<T>(componentName: string): Record<string, StoryObj<T>> {
const stories = getStorybookStoriesForComponent(componentName);
const result: Record<string, StoryObj<T>> = {};
for (const [key, storyConfig] of Object.entries(stories)) {
result[key] = generateStory<T>(storyConfig);
}
return result;
}
/**
* Get all available story configurations
*/
export function listStorybookComponents(): string[] {
return Object.keys(getAllStorybookStories());
}
/**
* Helper to create mock handlers for stories
*/
export function createMockHandlers(handlerNames: string[]): Record<string, () => void> {
const handlers: Record<string, () => void> = {};
for (const name of handlerNames) {
handlers[name] = () => {
console.log(`Mock handler called: ${name}`);
};
}
return handlers;
}

View File

@@ -0,0 +1,135 @@
import { Page, expect } from '@playwright/test';
import { getAllPlaywrightPlaybooks, PlaywrightPlaybook, PlaywrightStep } from '@/utils/featureConfig';
/**
* Execute a single Playwright step from a playbook
*/
export async function executeStep(page: Page, step: PlaywrightStep, variables: Record<string, string> = {}) {
// Replace variables in step properties
const replaceVars = (str: string | undefined): string => {
if (!str) return '';
return str.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] || '');
};
const selector = replaceVars(step.selector);
const value = replaceVars(step.value);
const url = replaceVars(step.url);
const text = replaceVars(step.text);
switch (step.action) {
case 'goto':
if (url) {
await page.goto(url);
}
break;
case 'click':
if (selector) {
await page.click(selector);
}
break;
case 'fill':
if (selector && value) {
await page.fill(selector, value);
}
break;
case 'select':
if (selector && value) {
await page.selectOption(selector, value);
}
break;
case 'wait':
if (step.timeout) {
await page.waitForTimeout(step.timeout);
}
break;
case 'expect':
if (url === 'redirected') {
await expect(page).toHaveURL(new RegExp(selector || ''));
} else if (text === 'visible' && selector) {
await expect(page.locator(selector)).toBeVisible();
} else if (text && selector) {
await expect(page.locator(selector)).toContainText(text);
} else if (text) {
// Check for status code in text
const statusCode = parseInt(text, 10);
if (!isNaN(statusCode)) {
// This is a status code check - would need API interception
// For now, we skip this as it requires special handling
console.warn(`Status code check (${statusCode}) not yet implemented in playbook runner`);
}
}
break;
case 'screenshot':
if (selector) {
await page.locator(selector).screenshot({ path: `screenshots/${Date.now()}-${selector.replace(/[^a-z0-9]/gi, '_')}.png` });
} else {
await page.screenshot({ path: `screenshots/${Date.now()}-page.png` });
}
break;
default:
console.warn(`Unknown step action: ${step.action}`);
}
}
/**
* Execute a full playbook from features.json
*/
export async function runPlaybook(
page: Page,
playbookName: string,
variables: Record<string, string> = {},
options: { runCleanup?: boolean } = {}
) {
const playbooks = getAllPlaywrightPlaybooks();
const playbook = playbooks[playbookName];
if (!playbook) {
throw new Error(`Playbook not found: ${playbookName}`);
}
console.log(`Running playbook: ${playbook.name}`);
console.log(`Description: ${playbook.description}`);
// Execute main steps
for (const step of playbook.steps) {
await executeStep(page, step, variables);
}
// Execute cleanup steps if requested and they exist
if (options.runCleanup && playbook.cleanup) {
console.log('Running cleanup steps...');
for (const step of playbook.cleanup) {
await executeStep(page, step, variables);
}
}
}
/**
* Get all playbooks by tag
*/
export function getPlaybooksByTag(tag: string): Record<string, PlaywrightPlaybook> {
const allPlaybooks = getAllPlaywrightPlaybooks();
const filtered: Record<string, PlaywrightPlaybook> = {};
for (const [name, playbook] of Object.entries(allPlaybooks)) {
if (playbook.tags?.includes(tag)) {
filtered[name] = playbook;
}
}
return filtered;
}
/**
* List all available playbooks
*/
export function listPlaybooks(): string[] {
return Object.keys(getAllPlaywrightPlaybooks());
}