Skip to content

Commit 2c55eb8

Browse files
Copilothuangyiirene
andcommitted
refactor: improve type safety and reduce duplication in types package
- Use generics for DataSource.bulk() return type - Improve validation function type safety - Extract OverlayPosition and OverlayAlignment shared types - Add comprehensive usage examples Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent c592efe commit 2c55eb8

9 files changed

Lines changed: 347 additions & 13 deletions

File tree

packages/types/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
node_modules/
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Example: Dashboard Layout
3+
*
4+
* This example demonstrates a complete dashboard layout with
5+
* sidebar, header, and data table.
6+
*/
7+
8+
import type { FlexSchema, SidebarSchema, HeaderBarSchema, CardSchema, DataTableSchema } from '../src/index';
9+
10+
export const dashboardSchema: FlexSchema = {
11+
type: 'flex',
12+
direction: 'col',
13+
className: 'h-screen',
14+
15+
children: [
16+
// Header
17+
{
18+
type: 'header-bar',
19+
title: 'Object UI Dashboard',
20+
logo: '/logo.svg',
21+
sticky: true,
22+
right: [
23+
{
24+
type: 'button',
25+
label: 'Profile',
26+
variant: 'ghost',
27+
icon: 'User'
28+
}
29+
]
30+
} as HeaderBarSchema,
31+
32+
// Main content with sidebar
33+
{
34+
type: 'flex',
35+
direction: 'row',
36+
className: 'flex-1',
37+
children: [
38+
// Sidebar
39+
{
40+
type: 'sidebar',
41+
collapsible: true,
42+
nav: [
43+
{ label: 'Dashboard', href: '/', icon: 'Home', active: true },
44+
{ label: 'Users', href: '/users', icon: 'Users' },
45+
{ label: 'Settings', href: '/settings', icon: 'Settings' }
46+
]
47+
} as SidebarSchema,
48+
49+
// Main content area
50+
{
51+
type: 'container',
52+
className: 'flex-1 p-6',
53+
children: [
54+
{
55+
type: 'card',
56+
title: 'User Management',
57+
description: 'Manage your users and permissions',
58+
content: {
59+
type: 'data-table',
60+
columns: [
61+
{ header: 'ID', accessorKey: 'id', width: '80px' },
62+
{ header: 'Name', accessorKey: 'name' },
63+
{ header: 'Email', accessorKey: 'email' },
64+
{ header: 'Role', accessorKey: 'role' },
65+
{ header: 'Status', accessorKey: 'status' }
66+
],
67+
data: [
68+
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'Active' },
69+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User', status: 'Active' },
70+
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User', status: 'Inactive' }
71+
],
72+
pagination: true,
73+
pageSize: 10,
74+
searchable: true,
75+
selectable: true,
76+
sortable: true,
77+
exportable: true,
78+
rowActions: true,
79+
onRowEdit: (row) => console.log('Edit:', row),
80+
onRowDelete: (row) => console.log('Delete:', row)
81+
} as DataTableSchema
82+
} as CardSchema
83+
]
84+
}
85+
]
86+
}
87+
]
88+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Example: Simple Login Form
3+
*
4+
* This example demonstrates how to use @object-ui/types to define
5+
* a complete login form with validation.
6+
*/
7+
8+
import type { FormSchema } from '../src/index';
9+
10+
export const loginFormSchema: FormSchema = {
11+
type: 'form',
12+
className: 'max-w-md mx-auto p-6',
13+
14+
fields: [
15+
{
16+
name: 'email',
17+
type: 'input',
18+
inputType: 'email',
19+
label: 'Email Address',
20+
placeholder: 'you@example.com',
21+
required: true,
22+
validation: {
23+
required: 'Email is required',
24+
pattern: {
25+
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
26+
message: 'Invalid email address'
27+
}
28+
}
29+
},
30+
{
31+
name: 'password',
32+
type: 'input',
33+
inputType: 'password',
34+
label: 'Password',
35+
placeholder: '••••••••',
36+
required: true,
37+
validation: {
38+
required: 'Password is required',
39+
minLength: {
40+
value: 8,
41+
message: 'Password must be at least 8 characters'
42+
}
43+
}
44+
},
45+
{
46+
name: 'remember',
47+
type: 'checkbox',
48+
label: 'Remember me',
49+
defaultChecked: false
50+
}
51+
],
52+
53+
submitLabel: 'Sign In',
54+
showCancel: false,
55+
56+
onSubmit: async (data) => {
57+
console.log('Login attempt:', data);
58+
// Handle authentication
59+
}
60+
};
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Example: Data Source Adapter
3+
*
4+
* This example demonstrates implementing a REST API data source adapter
5+
* using the DataSource interface from @object-ui/types.
6+
*/
7+
8+
import type { DataSource, QueryParams, QueryResult } from '../src/index';
9+
10+
/**
11+
* REST API Data Source Implementation
12+
*
13+
* A generic REST API adapter that works with any REST backend.
14+
*/
15+
export class RestDataSource<T = any> implements DataSource<T> {
16+
constructor(private baseUrl: string) {}
17+
18+
/**
19+
* Build query string from QueryParams
20+
*/
21+
private buildQueryString(params?: QueryParams): string {
22+
if (!params) return '';
23+
24+
const searchParams = new URLSearchParams();
25+
26+
if (params.$select) {
27+
searchParams.append('select', params.$select.join(','));
28+
}
29+
30+
if (params.$filter) {
31+
searchParams.append('filter', JSON.stringify(params.$filter));
32+
}
33+
34+
if (params.$orderby) {
35+
const sort = Object.entries(params.$orderby)
36+
.map(([key, dir]) => `${key}:${dir}`)
37+
.join(',');
38+
searchParams.append('sort', sort);
39+
}
40+
41+
if (params.$skip !== undefined) {
42+
searchParams.append('skip', params.$skip.toString());
43+
}
44+
45+
if (params.$top !== undefined) {
46+
searchParams.append('limit', params.$top.toString());
47+
}
48+
49+
if (params.$search) {
50+
searchParams.append('search', params.$search);
51+
}
52+
53+
return searchParams.toString();
54+
}
55+
56+
/**
57+
* Fetch multiple records
58+
*/
59+
async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
60+
const queryString = this.buildQueryString(params);
61+
const url = `${this.baseUrl}/${resource}${queryString ? '?' + queryString : ''}`;
62+
63+
const response = await fetch(url);
64+
65+
if (!response.ok) {
66+
throw new Error(`HTTP error! status: ${response.status}`);
67+
}
68+
69+
const data = await response.json();
70+
71+
// Assume API returns { data: [], total: number }
72+
return {
73+
data: data.data || data,
74+
total: data.total,
75+
page: params?.$skip && params?.$top
76+
? Math.floor(params.$skip / params.$top) + 1
77+
: 1,
78+
pageSize: params?.$top,
79+
hasMore: data.hasMore
80+
};
81+
}
82+
83+
/**
84+
* Fetch a single record by ID
85+
*/
86+
async findOne(resource: string, id: string | number, params?: QueryParams): Promise<T | null> {
87+
const queryString = this.buildQueryString(params);
88+
const url = `${this.baseUrl}/${resource}/${id}${queryString ? '?' + queryString : ''}`;
89+
90+
const response = await fetch(url);
91+
92+
if (response.status === 404) {
93+
return null;
94+
}
95+
96+
if (!response.ok) {
97+
throw new Error(`HTTP error! status: ${response.status}`);
98+
}
99+
100+
return response.json();
101+
}
102+
103+
/**
104+
* Create a new record
105+
*/
106+
async create(resource: string, data: Partial<T>): Promise<T> {
107+
const url = `${this.baseUrl}/${resource}`;
108+
109+
const response = await fetch(url, {
110+
method: 'POST',
111+
headers: {
112+
'Content-Type': 'application/json',
113+
},
114+
body: JSON.stringify(data),
115+
});
116+
117+
if (!response.ok) {
118+
throw new Error(`HTTP error! status: ${response.status}`);
119+
}
120+
121+
return response.json();
122+
}
123+
124+
/**
125+
* Update an existing record
126+
*/
127+
async update(resource: string, id: string | number, data: Partial<T>): Promise<T> {
128+
const url = `${this.baseUrl}/${resource}/${id}`;
129+
130+
const response = await fetch(url, {
131+
method: 'PATCH',
132+
headers: {
133+
'Content-Type': 'application/json',
134+
},
135+
body: JSON.stringify(data),
136+
});
137+
138+
if (!response.ok) {
139+
throw new Error(`HTTP error! status: ${response.status}`);
140+
}
141+
142+
return response.json();
143+
}
144+
145+
/**
146+
* Delete a record
147+
*/
148+
async delete(resource: string, id: string | number): Promise<boolean> {
149+
const url = `${this.baseUrl}/${resource}/${id}`;
150+
151+
const response = await fetch(url, {
152+
method: 'DELETE',
153+
});
154+
155+
if (!response.ok) {
156+
throw new Error(`HTTP error! status: ${response.status}`);
157+
}
158+
159+
return true;
160+
}
161+
}
162+
163+
// Usage example:
164+
// const dataSource = new RestDataSource('https://api.example.com');
165+
// const users = await dataSource.find('users', {
166+
// $filter: { status: 'active' },
167+
// $orderby: { createdAt: 'desc' },
168+
// $top: 10
169+
// });

packages/types/src/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export interface DataSource<T = any> {
184184
* @param data - Bulk data
185185
* @returns Promise resolving to operation result
186186
*/
187-
bulk?(resource: string, operation: 'create' | 'update' | 'delete', data: any[]): Promise<any>;
187+
bulk?(resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]>;
188188
}
189189

190190
/**

packages/types/src/form.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,10 @@ export interface ValidationRule {
726726
pattern?: { value: string | RegExp; message: string };
727727
/**
728728
* Custom validation function
729+
* @param value - The field value to validate
730+
* @returns true if valid, false or error message if invalid
729731
*/
730-
validate?: (value: any) => boolean | string | Promise<boolean | string>;
732+
validate?: (value: string | number | boolean | null | undefined) => boolean | string | Promise<boolean | string>;
731733
}
732734

733735
/**

packages/types/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ export type {
151151
// Overlay Components - Modals & Popovers
152152
// ============================================================================
153153
export type {
154+
OverlayPosition,
155+
OverlayAlignment,
154156
DialogSchema,
155157
AlertDialogSchema,
156158
SheetSchema,

0 commit comments

Comments
 (0)