@ram_28/kf-ai-sdk 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/QUICK_REFERENCE.md +528 -0
- package/docs/useAuth.md +402 -0
- package/docs/useFilter.md +273 -0
- package/docs/useForm.md +629 -0
- package/docs/useKanban.md +421 -0
- package/docs/useTable.md +372 -0
- package/package.json +4 -2
package/docs/useAuth.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# useAuth
|
|
2
|
+
|
|
3
|
+
## Brief Description
|
|
4
|
+
|
|
5
|
+
- Provides authentication state management with support for multiple OAuth providers (Google, Microsoft, GitHub, custom)
|
|
6
|
+
- Handles session checking, login flow initiation, and logout operations with automatic session refresh
|
|
7
|
+
- Includes role-based access control helpers (`hasRole`, `hasAnyRole`) for permission checking
|
|
8
|
+
- Must be used within an `AuthProvider` component that manages the authentication context
|
|
9
|
+
|
|
10
|
+
## Type Reference
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { useAuth, AuthProvider } from "@ram_28/kf-ai-sdk/auth";
|
|
14
|
+
import type {
|
|
15
|
+
UseAuthReturnType,
|
|
16
|
+
UserDetailsType,
|
|
17
|
+
AuthStatusType,
|
|
18
|
+
AuthProviderPropsType,
|
|
19
|
+
AuthProviderNameType,
|
|
20
|
+
AuthConfigType,
|
|
21
|
+
AuthEndpointConfigType,
|
|
22
|
+
LoginOptionsType,
|
|
23
|
+
LogoutOptionsType,
|
|
24
|
+
SessionResponseType,
|
|
25
|
+
} from "@ram_28/kf-ai-sdk/auth/types";
|
|
26
|
+
|
|
27
|
+
// User details from session
|
|
28
|
+
interface UserDetailsType {
|
|
29
|
+
_id: string;
|
|
30
|
+
_name: string;
|
|
31
|
+
Role: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Session response from API
|
|
36
|
+
interface SessionResponseType {
|
|
37
|
+
userDetails: UserDetailsType;
|
|
38
|
+
staticBaseUrl: string;
|
|
39
|
+
buildId: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Authentication status
|
|
43
|
+
type AuthStatusType = "loading" | "authenticated" | "unauthenticated";
|
|
44
|
+
|
|
45
|
+
// Supported auth providers
|
|
46
|
+
type AuthProviderNameType = "google" | "microsoft" | "github" | "custom";
|
|
47
|
+
|
|
48
|
+
// Auth endpoint configuration for a provider
|
|
49
|
+
interface AuthEndpointConfigType {
|
|
50
|
+
loginPath: string;
|
|
51
|
+
logoutPath?: string;
|
|
52
|
+
callbackPath?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Global auth configuration
|
|
56
|
+
interface AuthConfigType {
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
sessionEndpoint: string;
|
|
59
|
+
providers: Partial<Record<AuthProviderNameType, AuthEndpointConfigType>>;
|
|
60
|
+
defaultProvider: AuthProviderNameType;
|
|
61
|
+
autoRedirect: boolean;
|
|
62
|
+
loginRedirectUrl?: string;
|
|
63
|
+
callbackUrl?: string;
|
|
64
|
+
sessionCheckInterval: number;
|
|
65
|
+
retry: { count: number; delay: number };
|
|
66
|
+
staleTime: number;
|
|
67
|
+
refetchOnWindowFocus?: boolean;
|
|
68
|
+
refetchOnReconnect?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// AuthProvider component props
|
|
72
|
+
interface AuthProviderPropsType {
|
|
73
|
+
children: React.ReactNode;
|
|
74
|
+
config?: Partial<AuthConfigType>;
|
|
75
|
+
onAuthChange?: (status: AuthStatusType, user: UserDetailsType | null) => void;
|
|
76
|
+
onError?: (error: Error) => void;
|
|
77
|
+
loadingComponent?: React.ReactNode;
|
|
78
|
+
unauthenticatedComponent?: React.ReactNode;
|
|
79
|
+
skipInitialCheck?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Login options
|
|
83
|
+
interface LoginOptionsType {
|
|
84
|
+
callbackUrl?: string;
|
|
85
|
+
params?: Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Logout options
|
|
89
|
+
interface LogoutOptionsType {
|
|
90
|
+
redirectUrl?: string;
|
|
91
|
+
callLogoutEndpoint?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Hook return type
|
|
95
|
+
interface UseAuthReturnType {
|
|
96
|
+
// User state
|
|
97
|
+
user: UserDetailsType | null;
|
|
98
|
+
staticBaseUrl: string | null;
|
|
99
|
+
buildId: string | null;
|
|
100
|
+
status: AuthStatusType;
|
|
101
|
+
isAuthenticated: boolean;
|
|
102
|
+
isLoading: boolean;
|
|
103
|
+
|
|
104
|
+
// Auth operations
|
|
105
|
+
login: (provider?: AuthProviderNameType, options?: LoginOptionsType) => void;
|
|
106
|
+
logout: (options?: LogoutOptionsType) => Promise<void>;
|
|
107
|
+
refreshSession: () => Promise<SessionResponseType | null>;
|
|
108
|
+
hasRole: (role: string) => boolean;
|
|
109
|
+
hasAnyRole: (roles: string[]) => boolean;
|
|
110
|
+
|
|
111
|
+
// Error state
|
|
112
|
+
error: Error | null;
|
|
113
|
+
clearError: () => void;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Usage Example
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { useAuth, AuthProvider } from "@ram_28/kf-ai-sdk/auth";
|
|
121
|
+
import type {
|
|
122
|
+
UseAuthReturnType,
|
|
123
|
+
UserDetailsType,
|
|
124
|
+
AuthStatusType,
|
|
125
|
+
AuthProviderPropsType,
|
|
126
|
+
AuthProviderNameType,
|
|
127
|
+
AuthConfigType,
|
|
128
|
+
AuthEndpointConfigType,
|
|
129
|
+
LoginOptionsType,
|
|
130
|
+
LogoutOptionsType,
|
|
131
|
+
SessionResponseType,
|
|
132
|
+
} from "@ram_28/kf-ai-sdk/auth/types";
|
|
133
|
+
|
|
134
|
+
// Define available roles
|
|
135
|
+
type Role = "Admin" | "Buyer" | "Seller" | "InventoryManager";
|
|
136
|
+
|
|
137
|
+
// Auth configuration
|
|
138
|
+
const authConfig: Partial<AuthConfigType> = {
|
|
139
|
+
sessionEndpoint: "/api/id",
|
|
140
|
+
defaultProvider: "google",
|
|
141
|
+
autoRedirect: false,
|
|
142
|
+
sessionCheckInterval: 5 * 60 * 1000, // 5 minutes
|
|
143
|
+
providers: {
|
|
144
|
+
google: {
|
|
145
|
+
loginPath: "/api/auth/google/login",
|
|
146
|
+
logoutPath: "/api/auth/logout",
|
|
147
|
+
},
|
|
148
|
+
microsoft: {
|
|
149
|
+
loginPath: "/api/auth/microsoft/login",
|
|
150
|
+
logoutPath: "/api/auth/logout",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
refetchOnWindowFocus: true,
|
|
154
|
+
refetchOnReconnect: true,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// App wrapper with AuthProvider
|
|
158
|
+
function App() {
|
|
159
|
+
// Auth status change handler
|
|
160
|
+
const handleAuthChange = (status: AuthStatusType, user: UserDetailsType | null) => {
|
|
161
|
+
console.log("Auth status:", status, "User:", user?._name);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Auth error handler
|
|
165
|
+
const handleAuthError = (error: Error) => {
|
|
166
|
+
console.error("Auth error:", error.message);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// AuthProviderPropsType configuration
|
|
170
|
+
const providerProps: AuthProviderPropsType = {
|
|
171
|
+
children: <AppRoutes />,
|
|
172
|
+
config: authConfig,
|
|
173
|
+
onAuthChange: handleAuthChange,
|
|
174
|
+
onError: handleAuthError,
|
|
175
|
+
loadingComponent: <div>Checking authentication...</div>,
|
|
176
|
+
unauthenticatedComponent: <LoginPage />,
|
|
177
|
+
skipInitialCheck: false,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return <AuthProvider {...providerProps} />;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Login page component
|
|
184
|
+
function LoginPage() {
|
|
185
|
+
const auth: UseAuthReturnType = useAuth();
|
|
186
|
+
|
|
187
|
+
// Login with Google
|
|
188
|
+
const handleGoogleLogin = () => {
|
|
189
|
+
const options: LoginOptionsType = {
|
|
190
|
+
callbackUrl: "/dashboard",
|
|
191
|
+
};
|
|
192
|
+
auth.login("google", options);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Login with Microsoft
|
|
196
|
+
const handleMicrosoftLogin = () => {
|
|
197
|
+
const options: LoginOptionsType = {
|
|
198
|
+
callbackUrl: "/dashboard",
|
|
199
|
+
params: { prompt: "select_account" },
|
|
200
|
+
};
|
|
201
|
+
auth.login("microsoft", options);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Login with default provider
|
|
205
|
+
const handleDefaultLogin = () => {
|
|
206
|
+
auth.login(); // Uses defaultProvider from config
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Access auth status
|
|
210
|
+
const status: AuthStatusType = auth.status;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className="login-page">
|
|
214
|
+
<h1>Welcome</h1>
|
|
215
|
+
<p>Please sign in to continue</p>
|
|
216
|
+
|
|
217
|
+
{/* Error display */}
|
|
218
|
+
{auth.error && (
|
|
219
|
+
<div className="error">
|
|
220
|
+
{auth.error.message}
|
|
221
|
+
<button onClick={auth.clearError}>Dismiss</button>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* Login buttons */}
|
|
226
|
+
<div className="login-buttons">
|
|
227
|
+
<button onClick={handleGoogleLogin} disabled={auth.isLoading}>
|
|
228
|
+
Sign in with Google
|
|
229
|
+
</button>
|
|
230
|
+
<button onClick={handleMicrosoftLogin} disabled={auth.isLoading}>
|
|
231
|
+
Sign in with Microsoft
|
|
232
|
+
</button>
|
|
233
|
+
<button onClick={handleDefaultLogin} disabled={auth.isLoading}>
|
|
234
|
+
Sign in (Default)
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* Loading state */}
|
|
239
|
+
{auth.isLoading && <span>Loading...</span>}
|
|
240
|
+
|
|
241
|
+
{/* Status display */}
|
|
242
|
+
<p>Current status: {status}</p>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Dashboard component (authenticated users)
|
|
248
|
+
function Dashboard() {
|
|
249
|
+
const auth: UseAuthReturnType = useAuth();
|
|
250
|
+
|
|
251
|
+
// Logout handler
|
|
252
|
+
const handleLogout = async () => {
|
|
253
|
+
const options: LogoutOptionsType = {
|
|
254
|
+
redirectUrl: "/login",
|
|
255
|
+
callLogoutEndpoint: true,
|
|
256
|
+
};
|
|
257
|
+
await auth.logout(options);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Refresh session handler
|
|
261
|
+
const handleRefreshSession = async () => {
|
|
262
|
+
const session: SessionResponseType | null = await auth.refreshSession();
|
|
263
|
+
if (session) {
|
|
264
|
+
console.log("Session refreshed:", session.userDetails._name);
|
|
265
|
+
console.log("Static URL:", session.staticBaseUrl);
|
|
266
|
+
console.log("Build ID:", session.buildId);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Access user details
|
|
271
|
+
const user: UserDetailsType | null = auth.user;
|
|
272
|
+
|
|
273
|
+
// Role-based access control
|
|
274
|
+
const isAdmin: boolean = auth.hasRole("Admin");
|
|
275
|
+
const canManageProducts: boolean = auth.hasAnyRole(["Admin", "Seller"]);
|
|
276
|
+
const canViewReports: boolean = auth.hasAnyRole(["Admin", "InventoryManager"]);
|
|
277
|
+
|
|
278
|
+
// Guard against unauthenticated access
|
|
279
|
+
if (!auth.isAuthenticated) {
|
|
280
|
+
return <div>Please log in to access the dashboard</div>;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className="dashboard">
|
|
285
|
+
{/* Header */}
|
|
286
|
+
<header>
|
|
287
|
+
<h1>Dashboard</h1>
|
|
288
|
+
<div className="user-info">
|
|
289
|
+
<span>Welcome, {user?._name}</span>
|
|
290
|
+
<span>Role: {user?.Role}</span>
|
|
291
|
+
<button onClick={handleRefreshSession}>Refresh Session</button>
|
|
292
|
+
<button onClick={handleLogout}>Logout</button>
|
|
293
|
+
</div>
|
|
294
|
+
</header>
|
|
295
|
+
|
|
296
|
+
{/* Main content */}
|
|
297
|
+
<main>
|
|
298
|
+
{/* User info section */}
|
|
299
|
+
<section className="user-section">
|
|
300
|
+
<h2>User Information</h2>
|
|
301
|
+
<p>User ID: {user?._id}</p>
|
|
302
|
+
<p>Name: {user?._name}</p>
|
|
303
|
+
<p>Role: {user?.Role}</p>
|
|
304
|
+
<p>Static Base URL: {auth.staticBaseUrl}</p>
|
|
305
|
+
<p>Build ID: {auth.buildId}</p>
|
|
306
|
+
<p>Status: {auth.status}</p>
|
|
307
|
+
<p>Authenticated: {auth.isAuthenticated ? "Yes" : "No"}</p>
|
|
308
|
+
</section>
|
|
309
|
+
|
|
310
|
+
{/* Role-based sections */}
|
|
311
|
+
{canManageProducts && (
|
|
312
|
+
<section className="products-section">
|
|
313
|
+
<h2>Product Management</h2>
|
|
314
|
+
<p>You have access to manage products.</p>
|
|
315
|
+
</section>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{canViewReports && (
|
|
319
|
+
<section className="reports-section">
|
|
320
|
+
<h2>Reports</h2>
|
|
321
|
+
<p>You have access to view reports.</p>
|
|
322
|
+
</section>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{isAdmin && (
|
|
326
|
+
<section className="admin-section">
|
|
327
|
+
<h2>Admin Panel</h2>
|
|
328
|
+
<p>Full administrative access.</p>
|
|
329
|
+
</section>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{!isAdmin && (
|
|
333
|
+
<section className="restricted-section">
|
|
334
|
+
<p>Admin panel is restricted to administrators.</p>
|
|
335
|
+
</section>
|
|
336
|
+
)}
|
|
337
|
+
</main>
|
|
338
|
+
|
|
339
|
+
{/* Error banner */}
|
|
340
|
+
{auth.error && (
|
|
341
|
+
<div className="error-banner">
|
|
342
|
+
Error: {auth.error.message}
|
|
343
|
+
<button onClick={auth.clearError}>Dismiss</button>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Protected route component
|
|
351
|
+
function ProtectedRoute({
|
|
352
|
+
children,
|
|
353
|
+
requiredRoles,
|
|
354
|
+
}: {
|
|
355
|
+
children: React.ReactNode;
|
|
356
|
+
requiredRoles?: string[];
|
|
357
|
+
}) {
|
|
358
|
+
const auth: UseAuthReturnType = useAuth();
|
|
359
|
+
|
|
360
|
+
// Show loading while checking auth
|
|
361
|
+
if (auth.isLoading) {
|
|
362
|
+
return <div>Loading...</div>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Redirect if not authenticated
|
|
366
|
+
if (!auth.isAuthenticated) {
|
|
367
|
+
return <div>Access denied. Please log in.</div>;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check role requirements
|
|
371
|
+
if (requiredRoles && !auth.hasAnyRole(requiredRoles)) {
|
|
372
|
+
return <div>Access denied. Insufficient permissions.</div>;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return <>{children}</>;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// App routes with protected routes
|
|
379
|
+
function AppRoutes() {
|
|
380
|
+
return (
|
|
381
|
+
<div>
|
|
382
|
+
{/* Public route */}
|
|
383
|
+
<LoginPage />
|
|
384
|
+
|
|
385
|
+
{/* Protected route - any authenticated user */}
|
|
386
|
+
<ProtectedRoute>
|
|
387
|
+
<Dashboard />
|
|
388
|
+
</ProtectedRoute>
|
|
389
|
+
|
|
390
|
+
{/* Protected route - admin only */}
|
|
391
|
+
<ProtectedRoute requiredRoles={["Admin"]}>
|
|
392
|
+
<div>Admin Only Content</div>
|
|
393
|
+
</ProtectedRoute>
|
|
394
|
+
|
|
395
|
+
{/* Protected route - multiple roles */}
|
|
396
|
+
<ProtectedRoute requiredRoles={["Admin", "Seller", "InventoryManager"]}>
|
|
397
|
+
<div>Staff Only Content</div>
|
|
398
|
+
</ProtectedRoute>
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
```
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# useFilter
|
|
2
|
+
|
|
3
|
+
## Brief Description
|
|
4
|
+
|
|
5
|
+
- Manages filter conditions with support for nested filter groups (AND/OR/NOT operators)
|
|
6
|
+
- Provides a clean API for building complex filter payloads that match the backend API format
|
|
7
|
+
- Supports both flat conditions and nested condition groups for advanced filtering scenarios
|
|
8
|
+
- Includes type guards (`isCondition`, `isConditionGroup`) for safely working with the filter tree structure
|
|
9
|
+
|
|
10
|
+
## Type Reference
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { useFilter, isCondition, isConditionGroup } from "@ram_28/kf-ai-sdk/filter";
|
|
14
|
+
import type {
|
|
15
|
+
UseFilterOptionsType,
|
|
16
|
+
UseFilterReturnType,
|
|
17
|
+
ConditionType,
|
|
18
|
+
ConditionGroupType,
|
|
19
|
+
ConditionOperatorType,
|
|
20
|
+
ConditionGroupOperatorType,
|
|
21
|
+
FilterType,
|
|
22
|
+
FilterRHSTypeType,
|
|
23
|
+
} from "@ram_28/kf-ai-sdk/filter/types";
|
|
24
|
+
|
|
25
|
+
// Condition operators for comparing field values
|
|
26
|
+
type ConditionOperatorType =
|
|
27
|
+
| "EQ" | "NE" | "GT" | "GTE" | "LT" | "LTE"
|
|
28
|
+
| "Between" | "NotBetween"
|
|
29
|
+
| "IN" | "NIN"
|
|
30
|
+
| "Empty" | "NotEmpty"
|
|
31
|
+
| "Contains" | "NotContains"
|
|
32
|
+
| "MinLength" | "MaxLength";
|
|
33
|
+
|
|
34
|
+
// Group operators for combining conditions
|
|
35
|
+
type ConditionGroupOperatorType = "And" | "Or" | "Not";
|
|
36
|
+
|
|
37
|
+
// RHS type for condition values
|
|
38
|
+
type FilterRHSTypeType = "Constant" | "BOField" | "AppVariable";
|
|
39
|
+
|
|
40
|
+
// Leaf condition (matches API format)
|
|
41
|
+
interface ConditionType {
|
|
42
|
+
id?: string;
|
|
43
|
+
Operator: ConditionOperatorType;
|
|
44
|
+
LHSField: string;
|
|
45
|
+
RHSValue: any;
|
|
46
|
+
RHSType?: FilterRHSTypeType;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Condition group (recursive structure)
|
|
50
|
+
interface ConditionGroupType {
|
|
51
|
+
id?: string;
|
|
52
|
+
Operator: ConditionGroupOperatorType;
|
|
53
|
+
Condition: Array<ConditionType | ConditionGroupType>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Filter payload (alias for ConditionGroupType)
|
|
57
|
+
type FilterType = ConditionGroupType;
|
|
58
|
+
|
|
59
|
+
// Hook options
|
|
60
|
+
interface UseFilterOptionsType {
|
|
61
|
+
initialConditions?: Array<ConditionType | ConditionGroupType>;
|
|
62
|
+
initialOperator?: ConditionGroupOperatorType;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Hook return type
|
|
66
|
+
interface UseFilterReturnType {
|
|
67
|
+
// State (read-only)
|
|
68
|
+
operator: ConditionGroupOperatorType;
|
|
69
|
+
items: Array<ConditionType | ConditionGroupType>;
|
|
70
|
+
payload: FilterType | undefined;
|
|
71
|
+
hasConditions: boolean;
|
|
72
|
+
|
|
73
|
+
// Add operations (return id of created item)
|
|
74
|
+
addCondition: (condition: Omit<ConditionType, "id">, parentId?: string) => string;
|
|
75
|
+
addConditionGroup: (operator: ConditionGroupOperatorType, parentId?: string) => string;
|
|
76
|
+
|
|
77
|
+
// Update operations
|
|
78
|
+
updateCondition: (id: string, updates: Partial<Omit<ConditionType, "id">>) => void;
|
|
79
|
+
updateGroupOperator: (id: string, operator: ConditionGroupOperatorType) => void;
|
|
80
|
+
|
|
81
|
+
// Remove & access
|
|
82
|
+
removeCondition: (id: string) => void;
|
|
83
|
+
getCondition: (id: string) => ConditionType | ConditionGroupType | undefined;
|
|
84
|
+
|
|
85
|
+
// Utility
|
|
86
|
+
clearAllConditions: () => void;
|
|
87
|
+
setRootOperator: (op: ConditionGroupOperatorType) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Type guards
|
|
91
|
+
const isCondition: (item: ConditionType | ConditionGroupType) => item is ConditionType;
|
|
92
|
+
const isConditionGroup: (item: ConditionType | ConditionGroupType) => item is ConditionGroupType;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage Example
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useFilter, isCondition, isConditionGroup } from "@ram_28/kf-ai-sdk/filter";
|
|
99
|
+
import type {
|
|
100
|
+
ConditionType,
|
|
101
|
+
ConditionGroupType,
|
|
102
|
+
ConditionGroupOperatorType,
|
|
103
|
+
FilterType,
|
|
104
|
+
UseFilterOptionsType,
|
|
105
|
+
UseFilterReturnType,
|
|
106
|
+
} from "@ram_28/kf-ai-sdk/filter/types";
|
|
107
|
+
|
|
108
|
+
function ProductFilterBuilder() {
|
|
109
|
+
// Initialize hook with options
|
|
110
|
+
const options: UseFilterOptionsType = {
|
|
111
|
+
initialOperator: "And",
|
|
112
|
+
};
|
|
113
|
+
const filter: UseFilterReturnType = useFilter(options);
|
|
114
|
+
|
|
115
|
+
// Add a simple condition at root level
|
|
116
|
+
const handleAddCondition = () => {
|
|
117
|
+
const id = filter.addCondition({
|
|
118
|
+
Operator: "EQ",
|
|
119
|
+
LHSField: "Category",
|
|
120
|
+
RHSValue: "Electronics",
|
|
121
|
+
RHSType: "Constant",
|
|
122
|
+
});
|
|
123
|
+
console.log("Created condition with id:", id);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Build nested filter: (Category = "Electronics") AND (Price > 100 OR OnSale = true)
|
|
127
|
+
const handleBuildComplexFilter = () => {
|
|
128
|
+
filter.clearAllConditions();
|
|
129
|
+
|
|
130
|
+
// Add root condition
|
|
131
|
+
filter.addCondition({
|
|
132
|
+
Operator: "EQ",
|
|
133
|
+
LHSField: "Category",
|
|
134
|
+
RHSValue: "Electronics",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Create nested OR group
|
|
138
|
+
const groupId = filter.addConditionGroup("Or");
|
|
139
|
+
|
|
140
|
+
// Add conditions to the group
|
|
141
|
+
filter.addCondition({
|
|
142
|
+
Operator: "GT",
|
|
143
|
+
LHSField: "Price",
|
|
144
|
+
RHSValue: 100,
|
|
145
|
+
}, groupId);
|
|
146
|
+
|
|
147
|
+
const saleConditionId = filter.addCondition({
|
|
148
|
+
Operator: "EQ",
|
|
149
|
+
LHSField: "OnSale",
|
|
150
|
+
RHSValue: true,
|
|
151
|
+
}, groupId);
|
|
152
|
+
|
|
153
|
+
// Update a condition
|
|
154
|
+
filter.updateCondition(saleConditionId, { RHSValue: false });
|
|
155
|
+
|
|
156
|
+
// Toggle group operator
|
|
157
|
+
filter.updateGroupOperator(groupId, "And");
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Render filter tree recursively
|
|
161
|
+
const renderFilterItem = (item: ConditionType | ConditionGroupType, depth = 0): JSX.Element => {
|
|
162
|
+
const indent = { marginLeft: depth * 20 };
|
|
163
|
+
|
|
164
|
+
if (isCondition(item)) {
|
|
165
|
+
return (
|
|
166
|
+
<div key={item.id} style={indent} className="filter-condition">
|
|
167
|
+
<span>
|
|
168
|
+
{item.LHSField} {item.Operator} {String(item.RHSValue)}
|
|
169
|
+
</span>
|
|
170
|
+
<button onClick={() => filter.removeCondition(item.id!)}>Remove</button>
|
|
171
|
+
<button onClick={() => filter.updateCondition(item.id!, { RHSValue: "Updated" })}>
|
|
172
|
+
Update
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isConditionGroup(item)) {
|
|
179
|
+
return (
|
|
180
|
+
<div key={item.id} style={indent} className="filter-group">
|
|
181
|
+
<div className="group-header">
|
|
182
|
+
<select
|
|
183
|
+
value={item.Operator}
|
|
184
|
+
onChange={(e) =>
|
|
185
|
+
filter.updateGroupOperator(item.id!, e.target.value as ConditionGroupOperatorType)
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
<option value="And">AND</option>
|
|
189
|
+
<option value="Or">OR</option>
|
|
190
|
+
<option value="Not">NOT</option>
|
|
191
|
+
</select>
|
|
192
|
+
<button onClick={() => filter.addCondition({ Operator: "EQ", LHSField: "Field", RHSValue: "" }, item.id!)}>
|
|
193
|
+
+ Condition
|
|
194
|
+
</button>
|
|
195
|
+
<button onClick={() => filter.addConditionGroup("And", item.id!)}>+ Group</button>
|
|
196
|
+
<button onClick={() => filter.removeCondition(item.id!)}>Remove Group</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="group-conditions">
|
|
199
|
+
{item.Condition.map((child) => renderFilterItem(child, depth + 1))}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return <></>;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Use filter payload in API call
|
|
209
|
+
const handleApplyFilter = async () => {
|
|
210
|
+
const payload: FilterType | undefined = filter.payload;
|
|
211
|
+
if (payload) {
|
|
212
|
+
console.log("API Payload (no ids):", JSON.stringify(payload, null, 2));
|
|
213
|
+
const response = await fetch("/api/products", {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify({ Filter: payload }),
|
|
217
|
+
});
|
|
218
|
+
return response.json();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Access individual item by id
|
|
223
|
+
const handleGetItem = (id: string) => {
|
|
224
|
+
const item = filter.getCondition(id);
|
|
225
|
+
if (item) {
|
|
226
|
+
console.log("Found item:", item);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="filter-builder">
|
|
232
|
+
{/* Root operator control */}
|
|
233
|
+
<div className="root-controls">
|
|
234
|
+
<label>
|
|
235
|
+
Root Operator:
|
|
236
|
+
<select
|
|
237
|
+
value={filter.operator}
|
|
238
|
+
onChange={(e) => filter.setRootOperator(e.target.value as ConditionGroupOperator)}
|
|
239
|
+
>
|
|
240
|
+
<option value="And">AND</option>
|
|
241
|
+
<option value="Or">OR</option>
|
|
242
|
+
</select>
|
|
243
|
+
</label>
|
|
244
|
+
<button onClick={handleAddCondition}>Add Condition</button>
|
|
245
|
+
<button onClick={() => filter.addConditionGroup("Or")}>Add Group</button>
|
|
246
|
+
<button onClick={handleBuildComplexFilter}>Build Complex Filter</button>
|
|
247
|
+
<button onClick={filter.clearAllConditions} disabled={!filter.hasConditions}>
|
|
248
|
+
Clear All
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Filter tree display */}
|
|
253
|
+
<div className="filter-tree">
|
|
254
|
+
<h3>Filter Tree ({filter.items.length} root items)</h3>
|
|
255
|
+
{filter.items.map((item) => renderFilterItem(item))}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Apply filter */}
|
|
259
|
+
<div className="filter-actions">
|
|
260
|
+
<button onClick={handleApplyFilter} disabled={!filter.hasConditions}>
|
|
261
|
+
Apply Filter
|
|
262
|
+
</button>
|
|
263
|
+
<span>Has conditions: {filter.hasConditions ? "Yes" : "No"}</span>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Debug payload (id fields are stripped) */}
|
|
267
|
+
<pre className="filter-payload">
|
|
268
|
+
{JSON.stringify(filter.payload, null, 2)}
|
|
269
|
+
</pre>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
```
|