@pattern-stack/frontend-patterns 0.2.0-alpha.0 → 0.2.0-alpha.3
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/cli/commands/generate-hooks.ts +13 -4
- package/cli/src/codegen/openapi/__tests__/naming-utils.test.js +367 -0
- package/cli/src/codegen/openapi/client-generator.js +87 -19
- package/cli/src/codegen/openapi/confidence-scorer.js +93 -0
- package/cli/src/codegen/openapi/hook-config.js +48 -0
- package/cli/src/codegen/openapi/hook-generator.js +100 -62
- package/cli/src/codegen/openapi/naming-constants.js +98 -0
- package/cli/src/codegen/openapi/naming-utils.js +149 -0
- package/dist/atoms/components/core/Badge/Badge.d.ts +1 -1
- package/dist/atoms/components/data/DataTable/DataTable.d.ts.map +1 -1
- package/dist/atoms/components/data/DataTable/DataTable.types.d.ts +15 -3
- package/dist/atoms/components/data/DataTable/DataTable.types.d.ts.map +1 -1
- package/dist/atoms/hooks/index.d.ts +2 -0
- package/dist/atoms/hooks/index.d.ts.map +1 -1
- package/dist/atoms/hooks/useFieldMetadata.d.ts +18 -0
- package/dist/atoms/hooks/useFieldMetadata.d.ts.map +1 -0
- package/dist/atoms/hooks/useResponsiveTable.d.ts +103 -0
- package/dist/atoms/hooks/useResponsiveTable.d.ts.map +1 -0
- package/dist/atoms/primitives/sheet.d.ts +23 -0
- package/dist/atoms/primitives/sheet.d.ts.map +1 -0
- package/dist/atoms/services/auth-service.d.ts.map +1 -1
- package/dist/atoms/types/auth.d.ts +51 -0
- package/dist/atoms/types/auth.d.ts.map +1 -1
- package/dist/atoms/types/index.d.ts +1 -0
- package/dist/atoms/types/index.d.ts.map +1 -1
- package/dist/atoms/types/ui-config.d.ts +25 -8
- package/dist/atoms/types/ui-config.d.ts.map +1 -1
- package/dist/atoms/types/ui-metadata.d.ts +112 -0
- package/dist/atoms/types/ui-metadata.d.ts.map +1 -0
- package/dist/atoms/utils/ui-mapping.d.ts +9 -3
- package/dist/atoms/utils/ui-mapping.d.ts.map +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts.map +1 -1
- package/dist/frontend-patterns.css +82 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +1030 -248
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1031 -248
- package/dist/index.js.map +1 -1
- package/dist/molecules/layout/ListToolbar/ListToolbar.d.ts +37 -0
- package/dist/molecules/layout/ListToolbar/ListToolbar.d.ts.map +1 -0
- package/dist/molecules/layout/ListToolbar/index.d.ts +2 -0
- package/dist/molecules/layout/ListToolbar/index.d.ts.map +1 -0
- package/dist/molecules/layout/PageTitle/PageTitle.d.ts +17 -0
- package/dist/molecules/layout/PageTitle/PageTitle.d.ts.map +1 -0
- package/dist/molecules/layout/PageTitle/index.d.ts +2 -0
- package/dist/molecules/layout/PageTitle/index.d.ts.map +1 -0
- package/dist/molecules/layout/index.d.ts +2 -0
- package/dist/molecules/layout/index.d.ts.map +1 -1
- package/dist/templates/ListPageTemplate.d.ts +21 -0
- package/dist/templates/ListPageTemplate.d.ts.map +1 -0
- package/dist/templates/admin/AdminCRUDTemplate.d.ts.map +1 -1
- package/dist/templates/factory.d.ts.map +1 -1
- package/dist/templates/index.d.ts +1 -0
- package/dist/templates/index.d.ts.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Configuration Manager (Stub)
|
|
3
|
+
*
|
|
4
|
+
* Provides name overrides and pattern configuration for hook generation.
|
|
5
|
+
* This is a minimal stub to unblock the hook generator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class HookConfigManager {
|
|
9
|
+
constructor(configPath) {
|
|
10
|
+
this.configPath = configPath;
|
|
11
|
+
this.overrides = {};
|
|
12
|
+
this.patterns = {
|
|
13
|
+
deduplicateSegments: true
|
|
14
|
+
};
|
|
15
|
+
this.reviewedNames = [];
|
|
16
|
+
this.statistics = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async load() {
|
|
20
|
+
// No-op for stub - would load from config file
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async save() {
|
|
25
|
+
// No-op for stub - would save to config file
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getOverride(operationId) {
|
|
30
|
+
return this.overrides[operationId] || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getPatterns() {
|
|
34
|
+
return this.patterns;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getReviewedNames() {
|
|
38
|
+
return this.reviewedNames;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
updateStatistics(stats) {
|
|
42
|
+
this.statistics = { ...this.statistics, ...stats };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setOverride(operationId, hookName) {
|
|
46
|
+
this.overrides[operationId] = hookName;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Part of FRO-3: React Hook Generator
|
|
8
8
|
*/
|
|
9
|
-
import { HookConfigManager } from './hook-config';
|
|
10
|
-
import { ConfidenceScorer } from './confidence-scorer';
|
|
9
|
+
import { HookConfigManager } from './hook-config.js';
|
|
10
|
+
import { ConfidenceScorer } from './confidence-scorer.js';
|
|
11
|
+
import { singularize, pluralize, isPlural } from './naming-utils.js';
|
|
12
|
+
import { AUTH_ACTIONS, HEALTH_ENDPOINTS, USER_PROFILE_ENDPOINTS, SINGLETON_RESOURCES, COLLECTION_ACTIONS, SPECIAL_ACTIONS } from './naming-constants.js';
|
|
11
13
|
export class ReactHookGenerator {
|
|
12
14
|
options;
|
|
13
15
|
configManager;
|
|
@@ -203,13 +205,13 @@ export class ReactHookGenerator {
|
|
|
203
205
|
return ` onMutate: async (newData) => {
|
|
204
206
|
// Cancel outgoing refetches
|
|
205
207
|
await queryClient.cancelQueries({ queryKey: queryKeys.all })
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
// Snapshot previous value
|
|
208
210
|
const previousData = queryClient.getQueryData(queryKeys.all)
|
|
209
|
-
|
|
211
|
+
|
|
210
212
|
// Optimistically update cache
|
|
211
213
|
queryClient.setQueryData(queryKeys.all, (old: any) => [...(old || []), newData])
|
|
212
|
-
|
|
214
|
+
|
|
213
215
|
return { previousData }
|
|
214
216
|
},
|
|
215
217
|
onError: (err, newData, context) => {
|
|
@@ -220,14 +222,14 @@ export class ReactHookGenerator {
|
|
|
220
222
|
case 'patch':
|
|
221
223
|
return ` onMutate: async (updatedData) => {
|
|
222
224
|
await queryClient.cancelQueries({ queryKey: queryKeys.all })
|
|
223
|
-
|
|
225
|
+
|
|
224
226
|
const previousData = queryClient.getQueryData(queryKeys.all)
|
|
225
|
-
|
|
227
|
+
|
|
226
228
|
// Update specific item in cache
|
|
227
|
-
queryClient.setQueryData(queryKeys.all, (old: any) =>
|
|
229
|
+
queryClient.setQueryData(queryKeys.all, (old: any) =>
|
|
228
230
|
old?.map((item: any) => item.id === updatedData.id ? { ...item, ...updatedData } : item)
|
|
229
231
|
)
|
|
230
|
-
|
|
232
|
+
|
|
231
233
|
return { previousData }
|
|
232
234
|
},
|
|
233
235
|
onError: (err, updatedData, context) => {
|
|
@@ -236,14 +238,14 @@ export class ReactHookGenerator {
|
|
|
236
238
|
case 'delete':
|
|
237
239
|
return ` onMutate: async (id) => {
|
|
238
240
|
await queryClient.cancelQueries({ queryKey: queryKeys.all })
|
|
239
|
-
|
|
241
|
+
|
|
240
242
|
const previousData = queryClient.getQueryData(queryKeys.all)
|
|
241
|
-
|
|
243
|
+
|
|
242
244
|
// Remove item from cache
|
|
243
|
-
queryClient.setQueryData(queryKeys.all, (old: any) =>
|
|
245
|
+
queryClient.setQueryData(queryKeys.all, (old: any) =>
|
|
244
246
|
old?.filter((item: any) => item.id !== id)
|
|
245
247
|
)
|
|
246
|
-
|
|
248
|
+
|
|
247
249
|
return { previousData }
|
|
248
250
|
},
|
|
249
251
|
onError: (err, id, context) => {
|
|
@@ -275,18 +277,36 @@ export class ReactHookGenerator {
|
|
|
275
277
|
// Group endpoints by resource/tag
|
|
276
278
|
const groupedEndpoints = this.groupEndpointsByResource(endpoints);
|
|
277
279
|
for (const [resource, resourceEndpoints] of Object.entries(groupedEndpoints)) {
|
|
280
|
+
// Sanitize resource name to be a valid JS identifier
|
|
281
|
+
const sanitizedResource = this.sanitizeIdentifier(resource);
|
|
282
|
+
console.log(`DEBUG: resource="${resource}" -> sanitizedResource="${sanitizedResource}"`);
|
|
278
283
|
keys.push(` // ${resource} keys`);
|
|
279
|
-
keys.push(` ${
|
|
284
|
+
keys.push(` ${sanitizedResource}: () => [...queryKeys.all, '${sanitizedResource}'] as const,`);
|
|
285
|
+
const usedKeys = new Set([sanitizedResource]); // Track used keys to avoid duplicates
|
|
280
286
|
for (const endpoint of resourceEndpoints) {
|
|
281
287
|
if (endpoint.method !== 'get')
|
|
282
288
|
continue;
|
|
283
289
|
const operationName = this.getOperationName(endpoint);
|
|
284
|
-
|
|
290
|
+
let keyName = this.camelCase(operationName.replace(/^get/, ''));
|
|
291
|
+
// Skip if keyName matches sanitizedResource (base key already handles this)
|
|
292
|
+
if (keyName === sanitizedResource) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
// If keyName already used, add suffix to disambiguate
|
|
296
|
+
if (usedKeys.has(keyName)) {
|
|
297
|
+
const isList = !endpoint.path.includes('{');
|
|
298
|
+
keyName = isList ? `${keyName}List` : `${keyName}Detail`;
|
|
299
|
+
}
|
|
300
|
+
// Still duplicate after suffix? Skip to avoid compilation error
|
|
301
|
+
if (usedKeys.has(keyName)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
usedKeys.add(keyName);
|
|
285
305
|
if (this.hasRequiredParams(endpoint)) {
|
|
286
|
-
keys.push(` ${keyName}: (params:
|
|
306
|
+
keys.push(` ${keyName}: (params: Record<string, unknown>) => [...queryKeys.${sanitizedResource}(), '${keyName}', params] as const,`);
|
|
287
307
|
}
|
|
288
308
|
else {
|
|
289
|
-
keys.push(` ${keyName}: () => [...queryKeys.${
|
|
309
|
+
keys.push(` ${keyName}: () => [...queryKeys.${sanitizedResource}(), '${keyName}'] as const,`);
|
|
290
310
|
}
|
|
291
311
|
}
|
|
292
312
|
keys.push('');
|
|
@@ -336,7 +356,7 @@ export class ReactHookGenerator {
|
|
|
336
356
|
generateFileHeader(title) {
|
|
337
357
|
return `/**
|
|
338
358
|
* ${title}
|
|
339
|
-
*
|
|
359
|
+
*
|
|
340
360
|
* Auto-generated React hooks from OpenAPI specification
|
|
341
361
|
* Do not edit manually - regenerate using the hook generator
|
|
342
362
|
*/`;
|
|
@@ -422,6 +442,8 @@ export class ReactHookGenerator {
|
|
|
422
442
|
/^(?<action>\w+?)_api_v\d+_(?<path>.+?)_(?<method>get|post|put|patch|delete)$/i,
|
|
423
443
|
// Pattern: action_resource_path_method
|
|
424
444
|
/^(?<action>\w+?)_(?<resource>\w+?)_(?<path>.+?)_(?<method>get|post|put|patch|delete)$/i,
|
|
445
|
+
// Pattern: simple action_action_method (e.g., ready_ready_get, health_health_get)
|
|
446
|
+
/^(?<action>\w+?)_\1_(?<method>get|post|put|patch|delete)$/i,
|
|
425
447
|
// Pattern: simple action_resource
|
|
426
448
|
/^(?<action>\w+?)_(?<resource>\w+?)$/i,
|
|
427
449
|
];
|
|
@@ -439,7 +461,12 @@ export class ReactHookGenerator {
|
|
|
439
461
|
// Check if it's a custom action
|
|
440
462
|
const urlSegments = endpoint.path.split('/');
|
|
441
463
|
const lastSegment = urlSegments[urlSegments.length - 1];
|
|
442
|
-
const
|
|
464
|
+
const prevSegment = urlSegments[urlSegments.length - 2];
|
|
465
|
+
// Custom action: either after a path param (e.g., /accounts/{id}/archive)
|
|
466
|
+
// or a known action verb on a collection (e.g., /activities/search)
|
|
467
|
+
const isResourceAction = !lastSegment.startsWith('{') && prevSegment?.startsWith('{');
|
|
468
|
+
const isCollectionAction = !lastSegment.startsWith('{') && !prevSegment?.startsWith('{') && COLLECTION_ACTIONS.includes(lastSegment);
|
|
469
|
+
const isCustomAction = isResourceAction || isCollectionAction;
|
|
443
470
|
// Extract or infer the action verb
|
|
444
471
|
const actionVerb = this.extractActionVerb(components.action || operationId, endpoint.method);
|
|
445
472
|
// Extract or infer the resource
|
|
@@ -449,7 +476,7 @@ export class ReactHookGenerator {
|
|
|
449
476
|
'resource';
|
|
450
477
|
return {
|
|
451
478
|
action: actionVerb,
|
|
452
|
-
resource: resource,
|
|
479
|
+
resource: isCollectionAction ? prevSegment : resource,
|
|
453
480
|
path: components.path || '',
|
|
454
481
|
method: endpoint.method,
|
|
455
482
|
isCustomAction,
|
|
@@ -463,10 +490,10 @@ export class ReactHookGenerator {
|
|
|
463
490
|
extractActionVerb(actionPart, method) {
|
|
464
491
|
// Common action verbs to recognize
|
|
465
492
|
const actionVerbs = [
|
|
466
|
-
|
|
467
|
-
|
|
493
|
+
...AUTH_ACTIONS,
|
|
494
|
+
...HEALTH_ENDPOINTS,
|
|
468
495
|
'list', 'get', 'create', 'update', 'delete',
|
|
469
|
-
|
|
496
|
+
...COLLECTION_ACTIONS,
|
|
470
497
|
'approve', 'reject', 'archive', 'restore',
|
|
471
498
|
'sync', 'refresh', 'reset', 'verify', 'validate'
|
|
472
499
|
];
|
|
@@ -501,11 +528,7 @@ export class ReactHookGenerator {
|
|
|
501
528
|
* Check if this is a special endpoint that should keep its action verb
|
|
502
529
|
*/
|
|
503
530
|
isSpecialEndpoint(parsed) {
|
|
504
|
-
|
|
505
|
-
'login', 'logout', 'register', 'signup', 'signin',
|
|
506
|
-
'health', 'status', 'check', 'verify', 'validate'
|
|
507
|
-
];
|
|
508
|
-
return specialActions.includes(parsed.action);
|
|
531
|
+
return SPECIAL_ACTIONS.includes(parsed.action);
|
|
509
532
|
}
|
|
510
533
|
/**
|
|
511
534
|
* Format name for custom action endpoints
|
|
@@ -519,17 +542,18 @@ export class ReactHookGenerator {
|
|
|
519
542
|
return customActionName;
|
|
520
543
|
}
|
|
521
544
|
// Otherwise combine resource and action
|
|
522
|
-
return `${
|
|
545
|
+
return `${singularize(resource)}_${customActionName}`;
|
|
523
546
|
}
|
|
524
|
-
return `${action}_${
|
|
547
|
+
return `${action}_${singularize(resource)}`;
|
|
525
548
|
}
|
|
526
549
|
/**
|
|
527
550
|
* Format name for special endpoints (login, health, etc.)
|
|
528
551
|
*/
|
|
529
552
|
formatSpecialEndpointName(parsed) {
|
|
530
553
|
const { action, resource } = parsed;
|
|
531
|
-
// For
|
|
532
|
-
|
|
554
|
+
// For simple singleton endpoints, just return the action
|
|
555
|
+
const simpleActions = [...AUTH_ACTIONS.filter(a => ['login', 'logout'].includes(a)), ...HEALTH_ENDPOINTS.filter(h => ['health', 'ready', 'status'].includes(h)), ...USER_PROFILE_ENDPOINTS];
|
|
556
|
+
if (simpleActions.includes(action)) {
|
|
533
557
|
return action;
|
|
534
558
|
}
|
|
535
559
|
// For other special endpoints, combine with resource if meaningful
|
|
@@ -544,19 +568,19 @@ export class ReactHookGenerator {
|
|
|
544
568
|
formatStandardOperationName(parsed, endpoint) {
|
|
545
569
|
const { resource } = parsed;
|
|
546
570
|
// Determine if it's a list or single resource operation
|
|
547
|
-
const isList = endpoint.method === 'get' && !endpoint.path.includes('{');
|
|
571
|
+
const isList = endpoint.method === 'get' && !endpoint.path.includes('{') && !this.isSingletonEndpoint(resource, endpoint.path);
|
|
548
572
|
switch (endpoint.method) {
|
|
549
573
|
case 'get':
|
|
550
|
-
return isList ?
|
|
574
|
+
return isList ? pluralize(resource) : singularize(resource);
|
|
551
575
|
case 'post':
|
|
552
|
-
return `create_${
|
|
576
|
+
return `create_${singularize(resource)}`;
|
|
553
577
|
case 'put':
|
|
554
578
|
case 'patch':
|
|
555
|
-
return `update_${
|
|
579
|
+
return `update_${singularize(resource)}`;
|
|
556
580
|
case 'delete':
|
|
557
|
-
return `delete_${
|
|
581
|
+
return `delete_${singularize(resource)}`;
|
|
558
582
|
default:
|
|
559
|
-
return
|
|
583
|
+
return singularize(resource);
|
|
560
584
|
}
|
|
561
585
|
}
|
|
562
586
|
generateCleanName(endpoint) {
|
|
@@ -578,7 +602,7 @@ export class ReactHookGenerator {
|
|
|
578
602
|
const action = lastSegment.replace(/-/g, '_');
|
|
579
603
|
return `${mainResource}_${action}`;
|
|
580
604
|
}
|
|
581
|
-
// For nested resources (e.g., /categories/{id}/subcategories),
|
|
605
|
+
// For nested resources (e.g., /categories/{id}/subcategories),
|
|
582
606
|
// just use the last resource name
|
|
583
607
|
if (pathParts.length > 2 && segments.some(s => s.startsWith('{'))) {
|
|
584
608
|
// This is a nested resource, just use the last part
|
|
@@ -591,7 +615,7 @@ export class ReactHookGenerator {
|
|
|
591
615
|
const isList = !endpoint.path.includes('{');
|
|
592
616
|
// Only pluralize if it's a list and not already plural
|
|
593
617
|
if (isList && !cleanResource.endsWith('s') && !cleanResource.endsWith('ies')) {
|
|
594
|
-
return
|
|
618
|
+
return pluralize(cleanResource);
|
|
595
619
|
}
|
|
596
620
|
return cleanResource;
|
|
597
621
|
case 'post':
|
|
@@ -605,28 +629,22 @@ export class ReactHookGenerator {
|
|
|
605
629
|
return `${endpoint.method}_${cleanResource}`;
|
|
606
630
|
}
|
|
607
631
|
}
|
|
608
|
-
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
632
|
+
isSingletonEndpoint(resource, path) {
|
|
633
|
+
// Check if resource matches singleton pattern
|
|
634
|
+
const lowerResource = resource.toLowerCase().replace(/_/g, '');
|
|
635
|
+
if (SINGLETON_RESOURCES.some(s => lowerResource === s || lowerResource.endsWith(s))) {
|
|
636
|
+
return true;
|
|
612
637
|
}
|
|
613
|
-
|
|
614
|
-
|
|
638
|
+
|
|
639
|
+
// Check path patterns for common singletons
|
|
640
|
+
const pathSegments = path.split('/').filter(s => s && !s.startsWith('{'));
|
|
641
|
+
const lastSegment = pathSegments[pathSegments.length - 1]?.toLowerCase();
|
|
642
|
+
|
|
643
|
+
if (SINGLETON_RESOURCES.includes(lastSegment)) {
|
|
644
|
+
return true;
|
|
615
645
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
singularize(word) {
|
|
619
|
-
// Simple singularization rules
|
|
620
|
-
if (word.endsWith('ies')) {
|
|
621
|
-
return word.slice(0, -3) + 'y';
|
|
622
|
-
}
|
|
623
|
-
if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('ches') || word.endsWith('shes')) {
|
|
624
|
-
return word.slice(0, -2);
|
|
625
|
-
}
|
|
626
|
-
if (word.endsWith('s') && !word.endsWith('ss')) {
|
|
627
|
-
return word.slice(0, -1);
|
|
628
|
-
}
|
|
629
|
-
return word;
|
|
646
|
+
|
|
647
|
+
return false;
|
|
630
648
|
}
|
|
631
649
|
hasRequiredParams(endpoint) {
|
|
632
650
|
return endpoint.parameters.some(p => p.required) ||
|
|
@@ -663,6 +681,15 @@ export class ReactHookGenerator {
|
|
|
663
681
|
}
|
|
664
682
|
}
|
|
665
683
|
isListEndpoint(endpoint) {
|
|
684
|
+
// Extract resource for singleton check
|
|
685
|
+
const pathSegments = endpoint.path.split('/').filter(s => s && !s.startsWith('{'));
|
|
686
|
+
const resource = pathSegments[pathSegments.length - 1]?.replace(/-/g, '_') || '';
|
|
687
|
+
|
|
688
|
+
// Singleton endpoints are never list endpoints
|
|
689
|
+
if (this.isSingletonEndpoint(resource, endpoint.path)) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
666
693
|
return endpoint.path.includes('list') ||
|
|
667
694
|
!endpoint.path.includes('{') ||
|
|
668
695
|
(endpoint.summary?.toLowerCase().includes('list') ?? false) ||
|
|
@@ -671,7 +698,9 @@ export class ReactHookGenerator {
|
|
|
671
698
|
groupEndpointsByResource(endpoints) {
|
|
672
699
|
const groups = {};
|
|
673
700
|
for (const endpoint of endpoints) {
|
|
674
|
-
const
|
|
701
|
+
const tag = endpoint.tags?.[0] || 'default';
|
|
702
|
+
// Sanitize tag to be a valid JS identifier (camelCase, no spaces)
|
|
703
|
+
const resource = this.sanitizeIdentifier(tag);
|
|
675
704
|
if (!groups[resource]) {
|
|
676
705
|
groups[resource] = [];
|
|
677
706
|
}
|
|
@@ -679,8 +708,17 @@ export class ReactHookGenerator {
|
|
|
679
708
|
}
|
|
680
709
|
return groups;
|
|
681
710
|
}
|
|
711
|
+
sanitizeIdentifier(str) {
|
|
712
|
+
// Convert to camelCase and remove invalid characters
|
|
713
|
+
return str
|
|
714
|
+
.replace(/[^a-zA-Z0-9\s]/g, '') // Remove special chars except spaces
|
|
715
|
+
.split(/\s+/) // Split on whitespace
|
|
716
|
+
.map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
717
|
+
.join('');
|
|
718
|
+
}
|
|
682
719
|
getRelatedQueryTags(endpoint) {
|
|
683
|
-
const
|
|
720
|
+
const tag = endpoint.tags?.[0] || 'default';
|
|
721
|
+
const resource = this.sanitizeIdentifier(tag);
|
|
684
722
|
return [resource, 'all'];
|
|
685
723
|
}
|
|
686
724
|
camelCase(str) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming Constants
|
|
3
|
+
*
|
|
4
|
+
* Shared constants for magic strings used across hook and client generators.
|
|
5
|
+
* These constants help identify special endpoints that get unique naming treatment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Authentication action endpoints
|
|
10
|
+
* These endpoints are named directly without CRUD prefixes (e.g., 'login' not 'createLogin')
|
|
11
|
+
*/
|
|
12
|
+
export const AUTH_ACTIONS = [
|
|
13
|
+
'login',
|
|
14
|
+
'logout',
|
|
15
|
+
'register',
|
|
16
|
+
'signup',
|
|
17
|
+
'signin',
|
|
18
|
+
'refresh'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Health/status check endpoints
|
|
23
|
+
* These are singleton endpoints that return system status
|
|
24
|
+
*/
|
|
25
|
+
export const HEALTH_ENDPOINTS = [
|
|
26
|
+
'health',
|
|
27
|
+
'ready',
|
|
28
|
+
'status',
|
|
29
|
+
'info',
|
|
30
|
+
'version'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* User profile endpoints
|
|
35
|
+
* These endpoints reference the current/authenticated user
|
|
36
|
+
*/
|
|
37
|
+
export const USER_PROFILE_ENDPOINTS = [
|
|
38
|
+
'me',
|
|
39
|
+
'self',
|
|
40
|
+
'profile',
|
|
41
|
+
'current'
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Singleton resources
|
|
46
|
+
* Resources that return a single item, not a collection
|
|
47
|
+
* Includes all auth, health, and user profile endpoints plus common singleton patterns
|
|
48
|
+
*/
|
|
49
|
+
export const SINGLETON_RESOURCES = [
|
|
50
|
+
'me',
|
|
51
|
+
'self',
|
|
52
|
+
'current',
|
|
53
|
+
'profile',
|
|
54
|
+
'health',
|
|
55
|
+
'ready',
|
|
56
|
+
'status',
|
|
57
|
+
'info',
|
|
58
|
+
'version',
|
|
59
|
+
'metadata',
|
|
60
|
+
'config',
|
|
61
|
+
'settings',
|
|
62
|
+
'preferences',
|
|
63
|
+
'summary',
|
|
64
|
+
'stats',
|
|
65
|
+
'statistics',
|
|
66
|
+
'dashboard',
|
|
67
|
+
'schema',
|
|
68
|
+
'spec',
|
|
69
|
+
'openapi'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Collection action verbs
|
|
74
|
+
* Actions that operate on collections (e.g., /activities/search)
|
|
75
|
+
*/
|
|
76
|
+
export const COLLECTION_ACTIONS = [
|
|
77
|
+
'search',
|
|
78
|
+
'export',
|
|
79
|
+
'import',
|
|
80
|
+
'bulk',
|
|
81
|
+
'batch',
|
|
82
|
+
'count',
|
|
83
|
+
'stats',
|
|
84
|
+
'validate'
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Special actions - combination of auth, health, and user profile endpoints
|
|
89
|
+
* These endpoints get special naming treatment and keep their action verb
|
|
90
|
+
*/
|
|
91
|
+
export const SPECIAL_ACTIONS = [
|
|
92
|
+
...AUTH_ACTIONS,
|
|
93
|
+
...HEALTH_ENDPOINTS,
|
|
94
|
+
...USER_PROFILE_ENDPOINTS,
|
|
95
|
+
'check',
|
|
96
|
+
'verify',
|
|
97
|
+
'validate'
|
|
98
|
+
];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared naming utilities for OpenAPI code generation
|
|
3
|
+
* Handles singular/plural conversions with proper edge case handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map of irregular plural forms
|
|
8
|
+
*/
|
|
9
|
+
export const IRREGULAR_PLURALS = {
|
|
10
|
+
person: 'people',
|
|
11
|
+
child: 'children',
|
|
12
|
+
data: 'data', // already plural
|
|
13
|
+
media: 'media', // already plural
|
|
14
|
+
metadata: 'metadata', // already plural
|
|
15
|
+
people: 'people', // already plural
|
|
16
|
+
children: 'children', // already plural
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Map of irregular singular forms (reverse lookup)
|
|
21
|
+
*/
|
|
22
|
+
const IRREGULAR_SINGULARS = {
|
|
23
|
+
people: 'person',
|
|
24
|
+
children: 'child',
|
|
25
|
+
data: 'data', // already singular
|
|
26
|
+
media: 'media', // already singular
|
|
27
|
+
metadata: 'metadata', // already singular
|
|
28
|
+
person: 'person', // already singular
|
|
29
|
+
child: 'child', // already singular
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Words that naturally end in 's' but are singular
|
|
34
|
+
*/
|
|
35
|
+
const SINGULAR_EXCEPTIONS = [
|
|
36
|
+
'status',
|
|
37
|
+
'class',
|
|
38
|
+
'address',
|
|
39
|
+
'access',
|
|
40
|
+
'success',
|
|
41
|
+
'process',
|
|
42
|
+
'bus',
|
|
43
|
+
'focus',
|
|
44
|
+
'virus',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a word is already plural
|
|
49
|
+
* @param {string} word - The word to check
|
|
50
|
+
* @returns {boolean} - True if the word is plural
|
|
51
|
+
*/
|
|
52
|
+
export function isPlural(word) {
|
|
53
|
+
// Check irregular plurals first
|
|
54
|
+
if (IRREGULAR_PLURALS[word.toLowerCase()]) {
|
|
55
|
+
return IRREGULAR_PLURALS[word.toLowerCase()] === word.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check singular exceptions - these end in 's' but are not plural
|
|
59
|
+
if (SINGULAR_EXCEPTIONS.includes(word.toLowerCase())) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check common plural endings
|
|
64
|
+
if (word.endsWith('ies') || word.endsWith('es') || word.endsWith('s')) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert plural word to singular
|
|
73
|
+
* @param {string} word - The word to singularize
|
|
74
|
+
* @returns {string} - The singular form of the word
|
|
75
|
+
*/
|
|
76
|
+
export function singularize(word) {
|
|
77
|
+
// Handle empty or short words
|
|
78
|
+
if (!word || word.length <= 1) {
|
|
79
|
+
return word;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check irregular singulars first
|
|
83
|
+
const lowerWord = word.toLowerCase();
|
|
84
|
+
if (IRREGULAR_SINGULARS[lowerWord]) {
|
|
85
|
+
return IRREGULAR_SINGULARS[lowerWord];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Already singular (check exceptions)
|
|
89
|
+
if (SINGULAR_EXCEPTIONS.includes(lowerWord)) {
|
|
90
|
+
return word;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// -ies → -y (activities → activity)
|
|
94
|
+
if (word.endsWith('ies')) {
|
|
95
|
+
return word.slice(0, -3) + 'y';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -ses → -s (processes → process, but 'process' is already singular)
|
|
99
|
+
// -xes → -x (boxes → box)
|
|
100
|
+
// -ches → -ch (watches → watch)
|
|
101
|
+
// -shes → -sh (dishes → dish)
|
|
102
|
+
if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('ches') || word.endsWith('shes')) {
|
|
103
|
+
return word.slice(0, -2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Regular -s ending (cats → cat)
|
|
107
|
+
// But avoid words ending in -ss (class, success, etc.)
|
|
108
|
+
if (word.endsWith('s') && !word.endsWith('ss') && word.length > 1) {
|
|
109
|
+
return word.slice(0, -1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return word;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Convert singular word to plural
|
|
117
|
+
* @param {string} word - The word to pluralize
|
|
118
|
+
* @returns {string} - The plural form of the word
|
|
119
|
+
*/
|
|
120
|
+
export function pluralize(word) {
|
|
121
|
+
// Handle empty or short words
|
|
122
|
+
if (!word || word.length <= 1) {
|
|
123
|
+
return word;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if already plural
|
|
127
|
+
if (isPlural(word)) {
|
|
128
|
+
return word;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check irregular plurals first
|
|
132
|
+
const lowerWord = word.toLowerCase();
|
|
133
|
+
if (IRREGULAR_PLURALS[lowerWord]) {
|
|
134
|
+
return IRREGULAR_PLURALS[lowerWord];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -y ending (not vowel+y) → -ies (activity → activities)
|
|
138
|
+
if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].includes(word.slice(-2))) {
|
|
139
|
+
return word.slice(0, -1) + 'ies';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// -x, -ch, -sh endings → add 'es' (box → boxes, watch → watches, dish → dishes)
|
|
143
|
+
if (word.endsWith('x') || word.endsWith('ch') || word.endsWith('sh')) {
|
|
144
|
+
return word + 'es';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Regular: add 's' (cat → cats, dog → dogs)
|
|
148
|
+
return word + 's';
|
|
149
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const badgeVariants: (props?: {
|
|
4
|
-
variant?: "success" | "secondary" | "
|
|
4
|
+
variant?: "success" | "secondary" | "warning" | "info" | "default" | "destructive" | "outline";
|
|
5
5
|
size?: "sm" | "lg" | "default";
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) => string;
|
|
7
7
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../../../src/atoms/components/data/DataTable/DataTable.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AASpD,OAAO,KAAK,EAGV,cAAc,EACf,MAAM,mBAAmB,CAAC;AAK3B,YAAY,EACV,MAAM,EACN,gBAAgB,EAChB,cAAc,GACf,MAAM,mBAAmB,CAAC;AAI3B,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3D,IAAI,EACJ,OAAO,EACP,iBAA+B,EAC/B,QAAa,EACb,cAAqB,EACrB,UAAiB,EACjB,UAAU,EACV,YAAkC,EAClC,SAAc,EACd,KAAa,EACb,SAAiB,EACjB,gBAAoB,EACpB,UAAiB,EACjB,gBAAgB,EAChB,EAAE,GACH,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../../../src/atoms/components/data/DataTable/DataTable.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AASpD,OAAO,KAAK,EAGV,cAAc,EACf,MAAM,mBAAmB,CAAC;AAK3B,YAAY,EACV,MAAM,EACN,gBAAgB,EAChB,cAAc,GACf,MAAM,mBAAmB,CAAC;AAI3B,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3D,IAAI,EACJ,OAAO,EACP,iBAA+B,EAC/B,QAAa,EACb,cAAqB,EACrB,UAAiB,EACjB,UAAU,EACV,YAAkC,EAClC,SAAc,EACd,KAAa,EACb,SAAiB,EACjB,gBAAoB,EACpB,UAAiB,EACjB,gBAAgB,EAChB,EAAE,GACH,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,2CA4YnC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResponsiveColumnConfig, DisplayMode, ResponsiveValue, Breakpoint } from "../../../config/responsive";
|
|
2
|
-
import type { UIConfig } from "../../../types/ui-config";
|
|
2
|
+
import type { UIConfig, UIFieldType, FieldFormat } from "../../../types/ui-config";
|
|
3
3
|
export interface Column<T> {
|
|
4
4
|
key: string;
|
|
5
5
|
header: string | React.ReactNode;
|
|
@@ -7,8 +7,20 @@ export interface Column<T> {
|
|
|
7
7
|
sortable?: boolean;
|
|
8
8
|
filterable?: boolean;
|
|
9
9
|
width?: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Field type for automatic rendering.
|
|
12
|
+
* When set, DataTable uses defaultFieldRenderers to format the cell.
|
|
13
|
+
* Overridden by custom `cell` renderer if provided.
|
|
14
|
+
*
|
|
15
|
+
* Supports all UIFieldType values: status, category, money, percent,
|
|
16
|
+
* date, datetime, user, entity, boolean, text, number, email, url, phone.
|
|
17
|
+
*/
|
|
18
|
+
type?: UIFieldType;
|
|
19
|
+
/**
|
|
20
|
+
* Format configuration for typed fields.
|
|
21
|
+
* Used by money (currency, decimals, locale), date (locale), status (statusColors), etc.
|
|
22
|
+
*/
|
|
23
|
+
format?: FieldFormat;
|
|
12
24
|
}
|
|
13
25
|
export interface ResponsiveColumn<T> extends Column<T>, ResponsiveColumnConfig {
|
|
14
26
|
/** Responsive width adjustments */
|