Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
840 changes: 840 additions & 0 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: CI - Release Check
on:
pull_request:
branches: [master]
push:
branches: [master]
workflow_dispatch:
inputs:
sonar:
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ Reusable React TypeScript library for dashboard widgets, grid layout, and advanc
- Dashboard widget grid: drag, resize, duplicate, remove, actions
- Layout persistence: localStorage hydration and commit
- Data table: typed columns, selection, sorting, filtering, inline edit, pagination
- Error boundaries for widgets and tables
- Error boundaries for widgets and tables (enhanced with reset and logging capabilities)
- Pluggable chart adapters (default SVG)
- ESM + CJS + Types build (tsup)
- Vitest unit/integration tests
- Vitest unit/integration tests (80%+ coverage target)
- Playwright E2E tests for critical user paths (e.g., drag/resize in DashboardGrid)
- ESLint + Prettier (flat config)
- Changesets (manual release flow)
- Husky (pre-commit + pre-push)
Expand Down Expand Up @@ -40,6 +41,18 @@ npm i react react-dom react-router-dom zod @ciscode/ui-translate-core

Anything not exported from `src/index.ts` is considered private.

## New Features

### Enhanced Error Handling

- Error boundaries now include a reset mechanism and optional logging integration (e.g., Sentry).
- Hooks (`useLogin`, `usePasswordReset`, `useRegister`) support error reporting and user-friendly messages.

### Improved Testing

- **Integration Tests**: Added workflows for `ControlledZodDynamicForm` and `TableDataCustom`.
- **E2E Tests**: Expanded coverage to include `DashboardGrid` drag/resize functionality.

## Scripts

- `npm run dev` – start Vite dev server (for docs/examples)
Expand Down
8 changes: 4 additions & 4 deletions examples/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ export default function App(): JSX.Element {
];

return (
<div style={{ padding: 24 }}>
<div className="p-6">
<Breadcrumb pageName="Examples" />
<div style={{ marginTop: 16 }}>
<div className="mt-4">
<TableDataCustom<Row>
columns={columns}
data={data}
Expand All @@ -85,10 +85,10 @@ export default function App(): JSX.Element {
/>
</div>

<hr style={{ margin: '24px 0' }} />
<hr className="my-6" />
<h2>ControlledZodDynamicForm</h2>
<p>Simple required-field form using Zod.</p>
<div style={{ marginTop: 12 }}>
<div className="mt-3">
<ControlledZodDynamicForm
fields={fields}
schema={schema}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ciscode/ui-widget-kit",
"version": "1.0.3",
"version": "1.0.4",
"description": "",
"main": "dist/index.cjs",
"module": "dist/index.js",
Expand Down
9 changes: 2 additions & 7 deletions src/components/Dashboard/Widgets/DashboardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,24 @@
export default function DashboardGrid({
grid,
widgets,
onLayoutChange,

Check warning on line 18 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

'onLayoutChange' is defined but never used

Check warning on line 18 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / release checks

'onLayoutChange' is defined but never used

Check warning on line 18 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

'onLayoutChange' is defined but never used
enableDrag = true,
enableResize = true,
showActions = true,

Check warning on line 21 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

'showActions' is assigned a value but never used

Check warning on line 21 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / release checks

'showActions' is assigned a value but never used

Check warning on line 21 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

'showActions' is assigned a value but never used
persistKey,

Check warning on line 22 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

'persistKey' is defined but never used

Check warning on line 22 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / release checks

'persistKey' is defined but never used

Check warning on line 22 in src/components/Dashboard/Widgets/DashboardGrid.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

'persistKey' is defined but never used
}: Props): JSX.Element {
// Minimal grid rendering: each widget in a grid cell
return (
<div
className="grid"
className={`grid gap-${grid.gap}`}
style={{
gridTemplateColumns: `repeat(${grid.cols}, minmax(0, 1fr))`,
gap: grid.gap,
}}
>
{widgets.map((widget) => (
<div
key={widget.id}
style={{
gridColumn: `${widget.position.x + 1} / span ${widget.position.w}`,
gridRow: `${widget.position.y + 1} / span ${widget.position.h}`,
}}
className={`col-span-${widget.position.w} row-span-${widget.position.h}`}
>
<WidgetContainer title={widget.title} draggable={enableDrag} resizable={enableResize}>
{/* Render widget content based on type */}
Expand All @@ -56,7 +52,6 @@
style={{ width: `${Number(widget.props?.value ?? 0)}%` }}
/>
</div>
<div className="text-xs mt-1">{Number(widget.props?.value ?? 0)}%</div>
</div>
)}
{widget.type === 'activity' && (
Expand Down
9 changes: 8 additions & 1 deletion src/components/Dashboard/Widgets/WidgetContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ export default function WidgetContainer({
const headerRef = useRef<HTMLDivElement | null>(null);

return (
<div className="relative rounded-lg border border-gray-200 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
<div
className="relative rounded-lg border border-gray-200 bg-white dark:bg-gray-800 shadow-sm overflow-hidden"
role="region"
aria-labelledby={`widget-title-${title}`}
>
<h2 id={`widget-title-${title}`} className="widget-title">
{title}
</h2>
<div
ref={headerRef}
className={
Expand Down
10 changes: 8 additions & 2 deletions src/components/Form/ZodDynamicForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@
* On form submit => parse the entire `values` with Zod.
* If it fails, store the error messages in local state to display.
*/
function handleSubmit(e: FormEvent): void {
function handleSubmit(e: FormEvent<HTMLFormElement>): void {
e.preventDefault();
setErrors({});
try {
// Attempt to parse the entire form data with Zod
const parsed = schema.parse(values);
Expand All @@ -66,7 +67,7 @@
// Check if any error is specifically for "details" path
let detailsErrorFound = false;

err.issues.forEach((issue: { path: any[]; message: string }) => {

Check warning on line 70 in src/components/Form/ZodDynamicForm.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

Unexpected any. Specify a different type

Check warning on line 70 in src/components/Form/ZodDynamicForm.tsx

View workflow job for this annotation

GitHub Actions / release checks

Unexpected any. Specify a different type

Check warning on line 70 in src/components/Form/ZodDynamicForm.tsx

View workflow job for this annotation

GitHub Actions / CI - PR Validation

Unexpected any. Specify a different type
const pathKey = issue.path.join('.');
newErrors[pathKey] = issue.message;

Expand All @@ -87,7 +88,7 @@
}

return (
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} aria-describedby="form-errors">
{/* header */}
{header && <div className="mb-4">{header}</div>}

Expand Down Expand Up @@ -245,6 +246,11 @@
{submitLabel}
</button>
</div>
{Object.keys(errors).length > 0 && (
<div id="form-errors" className="text-red-600 mt-4">
Please fix the errors above.
</div>
)}
</form>
);
}
Expand Down
8 changes: 5 additions & 3 deletions src/components/Table/TableDataCustomBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,9 @@ function TableDataCustomBase<T>({
</tr>
) : (
visibleData.map((row, r) => (
<tr key={r} className="border-b">
<tr key={r} className="border-b" role="row" aria-rowindex={r + 1}>
{enableSelection && (
<td className="px-4 py-3">
<td className="px-4 py-3" role="gridcell">
<input
type="checkbox"
aria-label={`Select row ${r + 1}`}
Expand Down Expand Up @@ -398,7 +398,9 @@ function TableDataCustomBase<T>({
return (
<td
key={c}
className={`px-4 py-3 ltr:text-left rtl:text-right ${col.cellClassName ?? ''}`}
role="gridcell"
className="px-4 py-3"
tabIndex={0} // Ensure focusable cells
>
{content}
</td>
Expand Down
29 changes: 21 additions & 8 deletions src/exceptions/TableErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,30 @@ class TableErrorBoundary extends React.Component<

componentDidCatch(error: unknown, errorInfo: unknown): void {
console.error('TableErrorBoundary caught an error:', error, errorInfo);
// Integrate a logging service for production environments
if (process.env.NODE_ENV === 'production') {
// Replace with your logging service, e.g., Sentry
// Sentry.captureException(error);
}
}

handleReset = (): void => {
this.setState({ hasError: false });
};

render(): React.ReactNode {
if (this.state.hasError) {
// You can't use hooks like useT inside a class component,
// so we wrap it in a functional HOC instead.
return <TranslatedErrorMessage />;
return (
<div role="alert" className="p-4 text-red-600 bg-red-50 border border-red-300 rounded">
<TranslatedErrorMessage />
<button
onClick={this.handleReset}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Retry
</button>
</div>
);
}

return this.props.children;
Expand All @@ -36,11 +53,7 @@ class TableErrorBoundary extends React.Component<

const TranslatedErrorMessage = (): JSX.Element => {
const t = useT('template-fe');
return (
<div className="p-4 text-red-600 bg-red-50 border border-red-300 rounded">
{t('table.errorBoundary.fallbackMessage')}
</div>
);
return <div>{t('table.errorBoundary.fallbackMessage')}</div>;
};

export default TableErrorBoundary;
12 changes: 8 additions & 4 deletions src/hooks/useLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,17 @@ export function useLogin<TUser = unknown>({
setError(null);
setLoading(true);
try {
const input = schema ? schema.parse(values) : values;
const res = await login(input);
const credentials = schema ? schema.parse(values) : values;
const res = await login(credentials);
setResult(res);
return res;
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Login failed';
setError(message);
setError('Login failed. Please try again.');
// Optional: Report error to an external service
if (process.env.NODE_ENV === 'production') {
// Replace with your logging service, e.g., Sentry
// Sentry.captureException(e);
}
throw e;
} finally {
setLoading(false);
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/usePasswordReset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ export function usePasswordReset({
await reset(input);
setSuccess(true);
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Password reset failed';
setError(message);
setError('Password reset failed. Please try again.');
// Optional: Report error to an external service
if (process.env.NODE_ENV === 'production') {
// Replace with your logging service, e.g., Sentry
// Sentry.captureException(e);
}
throw e;
} finally {
setLoading(false);
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/useRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ export function useRegister<TUser = unknown>({
setUser(res);
return res;
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Registration failed';
setError(message);
setError('Registration failed. Please try again.');
// Optional: Report error to an external service
if (process.env.NODE_ENV === 'production') {
// Replace with your logging service, e.g., Sentry
// Sentry.captureException(e);
}
throw e;
} finally {
setLoading(false);
Expand Down
5 changes: 0 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export { default as Breadcrumb } from './components/Breadcrumbs/Breadcrumb';
export { default as ControlledZodDynamicForm } from './components/Form/ZodDynamicForm';
export { default as TableDataCustom } from './components/Table/TableDataCustom';
export { default as DashboardGrid } from './components/Dashboard/Widgets/DashboardGrid';
export { DefaultChartAdapter } from './components/Dashboard/Widgets/ChartAdapters';

// Hooks (public)
export { default as useLocalStorage } from './hooks/useLocalStorage';
Expand Down Expand Up @@ -52,7 +51,3 @@ export type { BreadcrumbProps } from './components/Breadcrumbs/Breadcrumb';
export type { ControlledZodDynamicFormProps } from './components/Form/ZodDynamicForm';
export type { PaginationProps, TableDataCustomProps } from './components/Table/TableDataCustomBase';
export type { DashboardProps } from './main/dashboard';
// Types (hooks)
export type { LoginCredentials, LoginResult, UseLoginOptions } from './hooks/useLogin';
export type { RegisterPayload, UseRegisterOptions } from './hooks/useRegister';
export type { PasswordResetInput, UsePasswordResetOptions } from './hooks/usePasswordReset';
32 changes: 32 additions & 0 deletions tests/e2e/dashboardGrid.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';

test('DashboardGrid drag and resize', async ({ page }) => {
await page.goto('/dashboard');

// Verify initial widget positions
const widget = await page.locator('[data-testid="widget-1"]');
const initialBox = await widget.boundingBox();
expect(initialBox).not.toBeNull();

// Drag the widget
await widget.hover();
await page.mouse.down();
await page.mouse.move(initialBox!.x + 100, initialBox!.y + 100);
await page.mouse.up();

// Verify new widget position
const newBox = await widget.boundingBox();
expect(newBox).not.toEqual(initialBox);

// Resize the widget
const resizeHandle = await widget.locator('[data-testid="resize-handle"]');
await resizeHandle.hover();
await page.mouse.down();
await page.mouse.move(newBox!.x + 50, newBox!.y + 50);
await page.mouse.up();

// Verify widget size changed
const resizedBox = await widget.boundingBox();
expect(resizedBox!.width).toBeGreaterThan(newBox!.width);
expect(resizedBox!.height).toBeGreaterThan(newBox!.height);
});
Loading