@soulbatical/tetra-ui 0.1.0
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/dist/components/AutoCard.d.ts +41 -0
- package/dist/components/AutoCard.d.ts.map +1 -0
- package/dist/components/AutoCard.js +73 -0
- package/dist/components/AutoCard.js.map +1 -0
- package/dist/components/CookieConsent.d.ts +15 -0
- package/dist/components/CookieConsent.d.ts.map +1 -0
- package/dist/components/CookieConsent.js +27 -0
- package/dist/components/CookieConsent.js.map +1 -0
- package/dist/components/GenericListLayout.d.ts +48 -0
- package/dist/components/GenericListLayout.d.ts.map +1 -0
- package/dist/components/GenericListLayout.js +18 -0
- package/dist/components/GenericListLayout.js.map +1 -0
- package/dist/components/ListGenerator.d.ts +41 -0
- package/dist/components/ListGenerator.d.ts.map +1 -0
- package/dist/components/ListGenerator.js +40 -0
- package/dist/components/ListGenerator.js.map +1 -0
- package/dist/components/LoadingStates.d.ts +38 -0
- package/dist/components/LoadingStates.d.ts.map +1 -0
- package/dist/components/LoadingStates.js +51 -0
- package/dist/components/LoadingStates.js.map +1 -0
- package/dist/components/OrgSwitcher.d.ts +15 -0
- package/dist/components/OrgSwitcher.d.ts.map +1 -0
- package/dist/components/OrgSwitcher.js +36 -0
- package/dist/components/OrgSwitcher.js.map +1 -0
- package/dist/components/PageHeader.d.ts +16 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PageHeader.js +10 -0
- package/dist/components/PageHeader.js.map +1 -0
- package/dist/components/ProtectedRoute.d.ts +9 -0
- package/dist/components/ProtectedRoute.d.ts.map +1 -0
- package/dist/components/ProtectedRoute.js +30 -0
- package/dist/components/ProtectedRoute.js.map +1 -0
- package/dist/components/StatusBadge.d.ts +15 -0
- package/dist/components/StatusBadge.d.ts.map +1 -0
- package/dist/components/StatusBadge.js +79 -0
- package/dist/components/StatusBadge.js.map +1 -0
- package/dist/components/ThemeToggle.d.ts +14 -0
- package/dist/components/ThemeToggle.d.ts.map +1 -0
- package/dist/components/ThemeToggle.js +20 -0
- package/dist/components/ThemeToggle.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +17 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/useAuth.d.ts +34 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +156 -0
- package/dist/hooks/useAuth.js.map +1 -0
- package/dist/hooks/useCacheInvalidation.d.ts +25 -0
- package/dist/hooks/useCacheInvalidation.d.ts.map +1 -0
- package/dist/hooks/useCacheInvalidation.js +57 -0
- package/dist/hooks/useCacheInvalidation.js.map +1 -0
- package/dist/hooks/useDebounce.d.ts +2 -0
- package/dist/hooks/useDebounce.d.ts.map +1 -0
- package/dist/hooks/useDebounce.js +15 -0
- package/dist/hooks/useDebounce.js.map +1 -0
- package/dist/hooks/useDialog.d.ts +26 -0
- package/dist/hooks/useDialog.d.ts.map +1 -0
- package/dist/hooks/useDialog.js +37 -0
- package/dist/hooks/useDialog.js.map +1 -0
- package/dist/hooks/useFilterConfigs.d.ts +81 -0
- package/dist/hooks/useFilterConfigs.d.ts.map +1 -0
- package/dist/hooks/useFilterConfigs.js +68 -0
- package/dist/hooks/useFilterConfigs.js.map +1 -0
- package/dist/hooks/useFormSubmit.d.ts +79 -0
- package/dist/hooks/useFormSubmit.d.ts.map +1 -0
- package/dist/hooks/useFormSubmit.js +57 -0
- package/dist/hooks/useFormSubmit.js.map +1 -0
- package/dist/hooks/useGenericList.d.ts +62 -0
- package/dist/hooks/useGenericList.d.ts.map +1 -0
- package/dist/hooks/useGenericList.js +75 -0
- package/dist/hooks/useGenericList.js.map +1 -0
- package/dist/hooks/useGenericListWithConfig.d.ts +65 -0
- package/dist/hooks/useGenericListWithConfig.d.ts.map +1 -0
- package/dist/hooks/useGenericListWithConfig.js +98 -0
- package/dist/hooks/useGenericListWithConfig.js.map +1 -0
- package/dist/hooks/useInfiniteScroll.d.ts +11 -0
- package/dist/hooks/useInfiniteScroll.d.ts.map +1 -0
- package/dist/hooks/useInfiniteScroll.js +63 -0
- package/dist/hooks/useInfiniteScroll.js.map +1 -0
- package/dist/hooks/useOrganizations.d.ts +27 -0
- package/dist/hooks/useOrganizations.d.ts.map +1 -0
- package/dist/hooks/useOrganizations.js +56 -0
- package/dist/hooks/useOrganizations.js.map +1 -0
- package/dist/hooks/usePageContext.d.ts +29 -0
- package/dist/hooks/usePageContext.d.ts.map +1 -0
- package/dist/hooks/usePageContext.js +32 -0
- package/dist/hooks/usePageContext.js.map +1 -0
- package/dist/hooks/usePersistedQueryState.d.ts +35 -0
- package/dist/hooks/usePersistedQueryState.d.ts.map +1 -0
- package/dist/hooks/usePersistedQueryState.js +187 -0
- package/dist/hooks/usePersistedQueryState.js.map +1 -0
- package/dist/hooks/useQueryState.d.ts +40 -0
- package/dist/hooks/useQueryState.d.ts.map +1 -0
- package/dist/hooks/useQueryState.js +246 -0
- package/dist/hooks/useQueryState.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/providers/QueryProvider.d.ts +9 -0
- package/dist/providers/QueryProvider.d.ts.map +1 -0
- package/dist/providers/QueryProvider.js +20 -0
- package/dist/providers/QueryProvider.js.map +1 -0
- package/dist/providers/ThemeProvider.d.ts +15 -0
- package/dist/providers/ThemeProvider.d.ts.map +1 -0
- package/dist/providers/ThemeProvider.js +13 -0
- package/dist/providers/ThemeProvider.js.map +1 -0
- package/dist/services/apiClient.d.ts +20 -0
- package/dist/services/apiClient.d.ts.map +1 -0
- package/dist/services/apiClient.js +85 -0
- package/dist/services/apiClient.js.map +1 -0
- package/dist/types/index.d.ts +119 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +68 -0
- package/src/styles/dark-mode.css +135 -0
- package/src/styles/theme.css +105 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter configuration from backend
|
|
3
|
+
*/
|
|
4
|
+
export interface FilterConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
type: 'column' | 'enum' | 'multiselect' | 'daterange' | 'search' | 'time' | 'related' | 'nullable' | 'boolean';
|
|
7
|
+
column?: string;
|
|
8
|
+
ui: {
|
|
9
|
+
label: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
options?: Array<{
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
}>;
|
|
18
|
+
order?: number;
|
|
19
|
+
defaultVisible?: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sort field configuration from backend (Config-Driven v2.0)
|
|
24
|
+
*/
|
|
25
|
+
export interface SortFieldConfig {
|
|
26
|
+
field: string;
|
|
27
|
+
alias?: string;
|
|
28
|
+
label: string;
|
|
29
|
+
defaultDirection?: 'asc' | 'desc';
|
|
30
|
+
ui?: {
|
|
31
|
+
ascLabel?: string;
|
|
32
|
+
descLabel?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pagination configuration from backend (Config-Driven v2.0)
|
|
37
|
+
*/
|
|
38
|
+
export interface PaginationConfig {
|
|
39
|
+
defaultLimit?: number;
|
|
40
|
+
maxLimit?: number;
|
|
41
|
+
minLimit?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface FilterConfigsResponse {
|
|
44
|
+
filters: FilterConfig[];
|
|
45
|
+
sortOptions?: SortFieldConfig[];
|
|
46
|
+
pagination?: PaginationConfig;
|
|
47
|
+
display?: any;
|
|
48
|
+
meta: {
|
|
49
|
+
totalFilters: number;
|
|
50
|
+
feature: string;
|
|
51
|
+
autoRendered: boolean;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Helper: Generate sort options for GenericListLayout from backend config
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const { data } = useFilterConfigs('orders');
|
|
60
|
+
* const sortOptions = generateSortOptions(data?.sortOptions);
|
|
61
|
+
* // Returns: [
|
|
62
|
+
* // { value: 'order_date-desc', label: 'Date (Newest first)' },
|
|
63
|
+
* // { value: 'order_date-asc', label: 'Date (Oldest first)' },
|
|
64
|
+
* // ...
|
|
65
|
+
* // ]
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function generateSortOptions(sortFields?: SortFieldConfig[]): Array<{
|
|
69
|
+
value: string;
|
|
70
|
+
label: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Hook to fetch filter configurations from backend
|
|
74
|
+
*
|
|
75
|
+
* @param feature - Feature name (e.g., 'orders', 'products')
|
|
76
|
+
* @returns Query result with filter configs
|
|
77
|
+
*/
|
|
78
|
+
export declare const useFilterConfigs: (feature: string, options?: {
|
|
79
|
+
apiPrefix?: string;
|
|
80
|
+
}) => import("@tanstack/react-query").UseQueryResult<FilterConfigsResponse, Error>;
|
|
81
|
+
//# sourceMappingURL=useFilterConfigs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFilterConfigs.d.ts","sourceRoot":"","sources":["../../src/hooks/useFilterConfigs.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/G,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE;QACF,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAClC,EAAE,CAAC,EAAE;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,IAAI,EAAE;QACJ,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CA6B3G;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,EAAE,UAAU;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,iFA4BjF,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { apiClient } from '../services/apiClient.js';
|
|
4
|
+
/**
|
|
5
|
+
* Helper: Generate sort options for GenericListLayout from backend config
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const { data } = useFilterConfigs('orders');
|
|
10
|
+
* const sortOptions = generateSortOptions(data?.sortOptions);
|
|
11
|
+
* // Returns: [
|
|
12
|
+
* // { value: 'order_date-desc', label: 'Date (Newest first)' },
|
|
13
|
+
* // { value: 'order_date-asc', label: 'Date (Oldest first)' },
|
|
14
|
+
* // ...
|
|
15
|
+
* // ]
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function generateSortOptions(sortFields) {
|
|
19
|
+
if (!sortFields || sortFields.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
const options = [];
|
|
22
|
+
for (const field of sortFields) {
|
|
23
|
+
// Use alias if available (for SQL RPC), otherwise use field
|
|
24
|
+
const sortKey = field.alias || field.field;
|
|
25
|
+
// Determine default direction
|
|
26
|
+
const defaultDir = field.defaultDirection || 'desc';
|
|
27
|
+
const otherDir = defaultDir === 'desc' ? 'asc' : 'desc';
|
|
28
|
+
// Add default direction first
|
|
29
|
+
const defaultLabel = field.ui?.[`${defaultDir}Label`] || `${field.label} (${defaultDir})`;
|
|
30
|
+
options.push({
|
|
31
|
+
value: `${sortKey}-${defaultDir}`,
|
|
32
|
+
label: defaultLabel
|
|
33
|
+
});
|
|
34
|
+
// Add other direction
|
|
35
|
+
const otherLabel = field.ui?.[`${otherDir}Label`] || `${field.label} (${otherDir})`;
|
|
36
|
+
options.push({
|
|
37
|
+
value: `${sortKey}-${otherDir}`,
|
|
38
|
+
label: otherLabel
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Hook to fetch filter configurations from backend
|
|
45
|
+
*
|
|
46
|
+
* @param feature - Feature name (e.g., 'orders', 'products')
|
|
47
|
+
* @returns Query result with filter configs
|
|
48
|
+
*/
|
|
49
|
+
export const useFilterConfigs = (feature, options) => {
|
|
50
|
+
const apiPrefix = options?.apiPrefix || 'admin';
|
|
51
|
+
return useQuery({
|
|
52
|
+
queryKey: ['filter-configs', feature, apiPrefix],
|
|
53
|
+
queryFn: async () => {
|
|
54
|
+
// apiClient.get returns parsed JSON directly and throws on error
|
|
55
|
+
const result = await apiClient.get(`/api/${apiPrefix}/${feature}/filters`);
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
throw new Error(`Invalid response from /api/${apiPrefix}/${feature}/filters: missing 'success' property.`);
|
|
58
|
+
}
|
|
59
|
+
if (!result.data) {
|
|
60
|
+
throw new Error(`Invalid response from /api/${apiPrefix}/${feature}/filters: missing 'data' property.`);
|
|
61
|
+
}
|
|
62
|
+
return result.data;
|
|
63
|
+
},
|
|
64
|
+
staleTime: 5 * 60 * 1000, // Cache for 5 minutes (configs rarely change)
|
|
65
|
+
retry: 1, // Retry once on failure
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=useFilterConfigs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFilterConfigs.js","sourceRoot":"","sources":["../../src/hooks/useFilterConfigs.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAuDrD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAA8B;IAChE,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtD,MAAM,OAAO,GAA4C,EAAE,CAAC;IAE5D,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,4DAA4D;QAC5D,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;QAE3C,8BAA8B;QAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,gBAAgB,IAAI,MAAM,CAAC;QACpD,MAAM,QAAQ,GAAG,UAAU,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QAExD,8BAA8B;QAC9B,MAAM,YAAY,GAAG,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,UAAU,OAAgC,CAAW,IAAI,GAAG,KAAK,CAAC,KAAK,KAAK,UAAU,GAAG,CAAC;QAC7H,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,GAAG,OAAO,IAAI,UAAU,EAAE;YACjC,KAAK,EAAE,YAAY;SACpB,CAAC,CAAC;QAEH,sBAAsB;QACtB,MAAM,UAAU,GAAG,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,QAAQ,OAAgC,CAAW,IAAI,GAAG,KAAK,CAAC,KAAK,KAAK,QAAQ,GAAG,CAAC;QACvH,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,GAAG,OAAO,IAAI,QAAQ,EAAE;YAC/B,KAAK,EAAE,UAAU;SAClB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAAE,OAAgC,EAAE,EAAE;IACpF,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,OAAO,CAAC;IAEhD,OAAO,QAAQ,CAAwB;QACrC,QAAQ,EAAE,CAAC,gBAAgB,EAAE,OAAO,EAAE,SAAS,CAAC;QAChD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,iEAAiE;YACjE,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAChC,QAAQ,SAAS,IAAI,OAAO,UAAU,CACvC,CAAC;YAEF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CACb,8BAA8B,SAAS,IAAI,OAAO,uCAAuC,CAC1F,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CACb,8BAA8B,SAAS,IAAI,OAAO,oCAAoC,CACvF,CAAC;YACJ,CAAC;YAED,OAAO,MAAM,CAAC,IAAI,CAAC;QACrB,CAAC;QACD,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,8CAA8C;QACxE,KAAK,EAAE,CAAC,EAAE,wBAAwB;KACnC,CAAC,CAAC;AACL,CAAC,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for handling form submissions with standard patterns
|
|
3
|
+
*
|
|
4
|
+
* Handles loading state, error handling, success messages, cache invalidation, and navigation.
|
|
5
|
+
* Toast and navigation are provided via callbacks -- the consumer project supplies these.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const { handleSubmit, isSubmitting } = useFormSubmit({
|
|
10
|
+
* onSubmit: async (data) => createItem(data),
|
|
11
|
+
* successMessage: 'Item created successfully!',
|
|
12
|
+
* invalidateQueries: ['items', 'item-counts'],
|
|
13
|
+
* onSuccess: () => router.push('/items'),
|
|
14
|
+
* toast: { success: toast.success, error: toast.error },
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* return (
|
|
18
|
+
* <form onSubmit={(e) => handleSubmit(e, formData)}>
|
|
19
|
+
* <Button type="submit" disabled={isSubmitting}>
|
|
20
|
+
* {isSubmitting ? 'Creating...' : 'Create'}
|
|
21
|
+
* </Button>
|
|
22
|
+
* </form>
|
|
23
|
+
* );
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
interface UseFormSubmitOptions<TData = any, TResult = any> {
|
|
27
|
+
/**
|
|
28
|
+
* The async function to execute on form submit.
|
|
29
|
+
* Receives the form data and should return the result.
|
|
30
|
+
*/
|
|
31
|
+
onSubmit: (data: TData) => Promise<TResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Success message to show via toast (optional).
|
|
34
|
+
* If not provided, no success toast is shown.
|
|
35
|
+
*/
|
|
36
|
+
successMessage?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Error message to show via toast (optional).
|
|
39
|
+
* If not provided, uses error.message from caught error.
|
|
40
|
+
*/
|
|
41
|
+
errorMessage?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Array of query keys to invalidate after successful submission.
|
|
44
|
+
*/
|
|
45
|
+
invalidateQueries?: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Optional callback after successful submission.
|
|
48
|
+
* Receives the result from onSubmit. Use for navigation, closing dialogs, etc.
|
|
49
|
+
*/
|
|
50
|
+
onSuccess?: (result: TResult) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Optional callback after failed submission.
|
|
53
|
+
* Receives the error.
|
|
54
|
+
*/
|
|
55
|
+
onError?: (error: Error) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Optional callback that always runs after submission (success or error).
|
|
58
|
+
*/
|
|
59
|
+
onSettled?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Whether to prevent default form submission behavior.
|
|
62
|
+
* Default: true
|
|
63
|
+
*/
|
|
64
|
+
preventDefault?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Toast functions for displaying messages.
|
|
67
|
+
* Consumer project provides these (e.g., from sonner, react-hot-toast, etc.)
|
|
68
|
+
*/
|
|
69
|
+
toast?: {
|
|
70
|
+
success?: (message: string) => void;
|
|
71
|
+
error?: (message: string) => void;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export declare const useFormSubmit: <TData = any, TResult = any>(options: UseFormSubmitOptions<TData, TResult>) => {
|
|
75
|
+
handleSubmit: (e: React.FormEvent, data?: TData) => Promise<TResult | undefined>;
|
|
76
|
+
isSubmitting: boolean;
|
|
77
|
+
};
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=useFormSubmit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFormSubmit.d.ts","sourceRoot":"","sources":["../../src/hooks/useFormSubmit.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,UAAU,oBAAoB,CAAC,KAAK,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG;IACvD;;;OAGG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE5C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACpC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KACnC,CAAC;CACH;AAED,eAAO,MAAM,aAAa,GAAI,KAAK,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,EACtD,SAAS,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC;sBAMxC,KAAK,CAAC,SAAS,SACX,KAAK,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;;CA4DhC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { useCacheInvalidation } from './useCacheInvalidation.js';
|
|
4
|
+
export const useFormSubmit = (options) => {
|
|
5
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
6
|
+
const { invalidate } = useCacheInvalidation();
|
|
7
|
+
const handleSubmit = useCallback(async (e, data) => {
|
|
8
|
+
// Prevent default form submission if enabled
|
|
9
|
+
if (options.preventDefault !== false) {
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
}
|
|
12
|
+
setIsSubmitting(true);
|
|
13
|
+
try {
|
|
14
|
+
// Execute the submit function
|
|
15
|
+
const result = await options.onSubmit(data);
|
|
16
|
+
// Show success message if provided
|
|
17
|
+
if (options.successMessage && options.toast?.success) {
|
|
18
|
+
options.toast.success(options.successMessage);
|
|
19
|
+
}
|
|
20
|
+
// Invalidate queries if provided
|
|
21
|
+
if (options.invalidateQueries && options.invalidateQueries.length > 0) {
|
|
22
|
+
invalidate(...options.invalidateQueries);
|
|
23
|
+
}
|
|
24
|
+
// Call onSuccess callback if provided
|
|
25
|
+
if (options.onSuccess) {
|
|
26
|
+
options.onSuccess(result);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('Form submission error:', error);
|
|
32
|
+
// Show error message
|
|
33
|
+
const errorMsg = options.errorMessage ||
|
|
34
|
+
(error instanceof Error ? error.message : 'An error occurred during submission');
|
|
35
|
+
if (options.toast?.error) {
|
|
36
|
+
options.toast.error(errorMsg);
|
|
37
|
+
}
|
|
38
|
+
// Call onError callback if provided
|
|
39
|
+
if (options.onError && error instanceof Error) {
|
|
40
|
+
options.onError(error);
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
setIsSubmitting(false);
|
|
46
|
+
// Call onSettled callback if provided
|
|
47
|
+
if (options.onSettled) {
|
|
48
|
+
options.onSettled();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, [options, invalidate]);
|
|
52
|
+
return {
|
|
53
|
+
handleSubmit,
|
|
54
|
+
isSubmitting,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=useFormSubmit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFormSubmit.js","sourceRoot":"","sources":["../../src/hooks/useFormSubmit.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAqFjE,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAA6C,EAC7C,EAAE;IACF,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,EAAE,UAAU,EAAE,GAAG,oBAAoB,EAAE,CAAC;IAE9C,MAAM,YAAY,GAAG,WAAW,CAAC,KAAK,EACpC,CAAkB,EAClB,IAAY,EACkB,EAAE;QAChC,6CAA6C;QAC7C,IAAI,OAAO,CAAC,cAAc,KAAK,KAAK,EAAE,CAAC;YACrC,CAAC,CAAC,cAAc,EAAE,CAAC;QACrB,CAAC;QAED,eAAe,CAAC,IAAI,CAAC,CAAC;QAEtB,IAAI,CAAC;YACH,8BAA8B;YAC9B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAa,CAAC,CAAC;YAErD,mCAAmC;YACnC,IAAI,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;gBACrD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YAChD,CAAC;YAED,iCAAiC;YACjC,IAAI,OAAO,CAAC,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtE,UAAU,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;YAC3C,CAAC;YAED,sCAAsC;YACtC,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;gBACtB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC5B,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;YAE/C,qBAAqB;YACrB,MAAM,QAAQ,GACZ,OAAO,CAAC,YAAY;gBACpB,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC;YAEnF,IAAI,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC;gBACzB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAChC,CAAC;YAED,oCAAoC;YACpC,IAAI,OAAO,CAAC,OAAO,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;gBAC9C,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;YAED,OAAO,SAAS,CAAC;QACnB,CAAC;gBAAS,CAAC;YACT,eAAe,CAAC,KAAK,CAAC,CAAC;YAEvB,sCAAsC;YACtC,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;gBACtB,OAAO,CAAC,SAAS,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IAE1B,OAAO;QACL,YAAY;QACZ,YAAY;KACb,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGenericList - Universal hook for list pages with filtering, infinite scroll, and counts
|
|
3
|
+
*
|
|
4
|
+
* Combines:
|
|
5
|
+
* - Filter state management with persistence (usePersistedQueryState)
|
|
6
|
+
* - Data fetching (custom list hook)
|
|
7
|
+
* - Counts fetching (custom counts hook)
|
|
8
|
+
* - Infinite scroll setup (useInfiniteScroll)
|
|
9
|
+
* - Data flattening
|
|
10
|
+
*
|
|
11
|
+
* Filter persistence per feature:
|
|
12
|
+
* - Filters are saved to sessionStorage per entity
|
|
13
|
+
* - Restored when navigating back (breadcrumbs, back button, sidebar)
|
|
14
|
+
* - Each feature remembers its own filters independently
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { items, counts, filters, setFilter, isLoading, sentinelRef } = useGenericList({
|
|
18
|
+
* entity: 'orders',
|
|
19
|
+
* dataKey: 'orders',
|
|
20
|
+
* defaultFilters: { status: 'all', search: '', sortBy: 'date-desc' },
|
|
21
|
+
* useListHook: useOrdersList,
|
|
22
|
+
* useCountsHook: useOrdersCounts
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
export declare function useGenericList<TItem, TFilters extends Record<string, any>, TCounts>({ entity, dataKey, defaultFilters, useListHook, useCountsHook, enabled, }: {
|
|
26
|
+
entity: string;
|
|
27
|
+
dataKey: string;
|
|
28
|
+
defaultFilters: TFilters;
|
|
29
|
+
useListHook: (filters: TFilters, options?: {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}) => {
|
|
32
|
+
data: any;
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
fetchNextPage: () => void;
|
|
35
|
+
hasNextPage?: boolean;
|
|
36
|
+
hasMoreItems?: boolean;
|
|
37
|
+
loadMore?: () => void;
|
|
38
|
+
isFetchingNextPage: boolean;
|
|
39
|
+
refetch?: () => void;
|
|
40
|
+
};
|
|
41
|
+
useCountsHook: (filters: Partial<TFilters>, options?: {
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
}) => {
|
|
44
|
+
data: TCounts | undefined;
|
|
45
|
+
isLoading: boolean;
|
|
46
|
+
};
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
}): {
|
|
49
|
+
items: TItem[];
|
|
50
|
+
counts: TCounts | undefined;
|
|
51
|
+
isLoading: boolean;
|
|
52
|
+
isLoadingCounts: boolean;
|
|
53
|
+
isFetchingNextPage: boolean;
|
|
54
|
+
filters: TFilters;
|
|
55
|
+
setFilter: <K extends keyof TFilters>(key: K, value: TFilters[K]) => void;
|
|
56
|
+
resetFilters: () => void;
|
|
57
|
+
sentinelRef: (node: HTMLDivElement | null) => void;
|
|
58
|
+
hasMore: boolean;
|
|
59
|
+
fetchNextPage: () => void;
|
|
60
|
+
refetch: (() => void) | undefined;
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=useGenericList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGenericList.d.ts","sourceRoot":"","sources":["../../src/hooks/useGenericList.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,EACnF,MAAM,EACN,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,OAAc,GACf,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,QAAQ,CAAC;IACzB,WAAW,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK;QACnE,IAAI,EAAE,GAAG,CAAC;QACV,SAAS,EAAE,OAAO,CAAC;QACnB,aAAa,EAAE,MAAM,IAAI,CAAC;QAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;QACtB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;KACtB,CAAC;IACF,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK;QAC9E,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;QAC1B,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;;;;;;;;;;;yBAZwB,IAAI;oBAKT,IAAI;EA6EvB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { usePersistedQueryState } from './usePersistedQueryState.js';
|
|
3
|
+
import { useInfiniteScroll } from './useInfiniteScroll.js';
|
|
4
|
+
/**
|
|
5
|
+
* useGenericList - Universal hook for list pages with filtering, infinite scroll, and counts
|
|
6
|
+
*
|
|
7
|
+
* Combines:
|
|
8
|
+
* - Filter state management with persistence (usePersistedQueryState)
|
|
9
|
+
* - Data fetching (custom list hook)
|
|
10
|
+
* - Counts fetching (custom counts hook)
|
|
11
|
+
* - Infinite scroll setup (useInfiniteScroll)
|
|
12
|
+
* - Data flattening
|
|
13
|
+
*
|
|
14
|
+
* Filter persistence per feature:
|
|
15
|
+
* - Filters are saved to sessionStorage per entity
|
|
16
|
+
* - Restored when navigating back (breadcrumbs, back button, sidebar)
|
|
17
|
+
* - Each feature remembers its own filters independently
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { items, counts, filters, setFilter, isLoading, sentinelRef } = useGenericList({
|
|
21
|
+
* entity: 'orders',
|
|
22
|
+
* dataKey: 'orders',
|
|
23
|
+
* defaultFilters: { status: 'all', search: '', sortBy: 'date-desc' },
|
|
24
|
+
* useListHook: useOrdersList,
|
|
25
|
+
* useCountsHook: useOrdersCounts
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
export function useGenericList({ entity, dataKey, defaultFilters, useListHook, useCountsHook, enabled = true, }) {
|
|
29
|
+
// 1. Filter state management with persistence
|
|
30
|
+
const { filters, setFilter, resetFilters } = usePersistedQueryState(entity, defaultFilters);
|
|
31
|
+
// 2. Data fetching
|
|
32
|
+
const { data, isLoading, fetchNextPage, hasNextPage, hasMoreItems, loadMore, isFetchingNextPage, refetch } = useListHook(filters, { enabled });
|
|
33
|
+
// 3. Counts fetching (parallel request)
|
|
34
|
+
const { data: counts, isLoading: isLoadingCounts } = useCountsHook(filters, { enabled });
|
|
35
|
+
// 4. Data flattening with deduplication
|
|
36
|
+
// Defensive: filter out pages with missing/invalid data
|
|
37
|
+
const flatItems = (data?.pages?.flatMap((page) => page[dataKey] || []) || []);
|
|
38
|
+
// Deduplicate by ID (prevents React duplicate key warnings)
|
|
39
|
+
// Filter out undefined/null items before accessing properties
|
|
40
|
+
const items = Array.from(new Map(flatItems
|
|
41
|
+
.filter((item) => item != null) // Remove undefined/null items
|
|
42
|
+
.map((item) => {
|
|
43
|
+
// Support both new (id) and legacy (entityid) formats
|
|
44
|
+
const itemId = item.id || item[`${entity.replace(/s$/, '')}id`] || item[`${entity}id`];
|
|
45
|
+
return [itemId, item];
|
|
46
|
+
})).values());
|
|
47
|
+
// 5. Infinite scroll setup
|
|
48
|
+
const { sentinelRef } = useInfiniteScroll({
|
|
49
|
+
onLoadMore: loadMore || fetchNextPage,
|
|
50
|
+
hasMore: hasMoreItems ?? hasNextPage ?? false,
|
|
51
|
+
threshold: 300,
|
|
52
|
+
enabled: true
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
// Data
|
|
56
|
+
items,
|
|
57
|
+
counts,
|
|
58
|
+
// Loading states
|
|
59
|
+
isLoading,
|
|
60
|
+
isLoadingCounts,
|
|
61
|
+
isFetchingNextPage,
|
|
62
|
+
// Filters
|
|
63
|
+
filters,
|
|
64
|
+
setFilter,
|
|
65
|
+
resetFilters,
|
|
66
|
+
// Infinite scroll
|
|
67
|
+
sentinelRef,
|
|
68
|
+
hasMore: hasMoreItems ?? hasNextPage ?? false,
|
|
69
|
+
// Pagination
|
|
70
|
+
fetchNextPage: loadMore || fetchNextPage,
|
|
71
|
+
// Refetch
|
|
72
|
+
refetch,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=useGenericList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGenericList.js","sourceRoot":"","sources":["../../src/hooks/useGenericList.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,cAAc,CAAuD,EACnF,MAAM,EACN,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,OAAO,GAAG,IAAI,GAoBf;IACC,8CAA8C;IAC9C,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,sBAAsB,CAAW,MAAM,EAAE,cAAc,CAAC,CAAC;IAEtG,mBAAmB;IACnB,MAAM,EACJ,IAAI,EACJ,SAAS,EACT,aAAa,EACb,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,kBAAkB,EAClB,OAAO,EACR,GAAG,WAAW,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAEtC,wCAAwC;IACxC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAEzF,wCAAwC;IACxC,wDAAwD;IACxD,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAY,CAAC;IAE9F,4DAA4D;IAC5D,8DAA8D;IAC9D,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CACtB,IAAI,GAAG,CACL,SAAS;SACN,MAAM,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,8BAA8B;SAClE,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE;QACjB,sDAAsD;QACtD,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC;QACvF,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACxB,CAAC,CAAC,CACL,CAAC,MAAM,EAAE,CACA,CAAC;IAEb,2BAA2B;IAC3B,MAAM,EAAE,WAAW,EAAE,GAAG,iBAAiB,CAAC;QACxC,UAAU,EAAE,QAAQ,IAAI,aAAa;QACrC,OAAO,EAAE,YAAY,IAAI,WAAW,IAAI,KAAK;QAC7C,SAAS,EAAE,GAAG;QACd,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,KAAK;QACL,MAAM;QAEN,iBAAiB;QACjB,SAAS;QACT,eAAe;QACf,kBAAkB;QAElB,UAAU;QACV,OAAO;QACP,SAAS;QACT,YAAY;QAEZ,kBAAkB;QAClB,WAAW;QACX,OAAO,EAAE,YAAY,IAAI,WAAW,IAAI,KAAK;QAE7C,aAAa;QACb,aAAa,EAAE,QAAQ,IAAI,aAAa;QAExC,UAAU;QACV,OAAO;KACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGenericListWithConfig - Config-Driven List Hook
|
|
3
|
+
*
|
|
4
|
+
* Zero double configuration - defaults auto-generated from backend config!
|
|
5
|
+
*
|
|
6
|
+
* BEFORE (useGenericList):
|
|
7
|
+
* - Manual defaultFilters in every page
|
|
8
|
+
* - Double configuration (backend config + frontend defaults)
|
|
9
|
+
* - Easy to forget filters
|
|
10
|
+
*
|
|
11
|
+
* AFTER (useGenericListWithConfig):
|
|
12
|
+
* - Defaults auto-generated from backend
|
|
13
|
+
* - Single source of truth
|
|
14
|
+
* - All filters automatically included
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { items, counts, filters, setFilter } = useGenericListWithConfig({
|
|
18
|
+
* featureName: 'orders',
|
|
19
|
+
* entity: 'orders',
|
|
20
|
+
* dataKey: 'orders',
|
|
21
|
+
* useListHook: useOrdersList,
|
|
22
|
+
* useCountsHook: useOrdersCounts,
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
export declare function useGenericListWithConfig<TItem, TFilters extends Record<string, any>, TCounts>({ featureName, entity, dataKey, useListHook, useCountsHook, additionalDefaults, apiPrefix, }: {
|
|
26
|
+
featureName: string;
|
|
27
|
+
entity: string;
|
|
28
|
+
dataKey: string;
|
|
29
|
+
useListHook: (filters: TFilters, options?: {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}) => {
|
|
32
|
+
data: any;
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
fetchNextPage: () => void;
|
|
35
|
+
hasNextPage?: boolean;
|
|
36
|
+
hasMoreItems?: boolean;
|
|
37
|
+
loadMore?: () => void;
|
|
38
|
+
isFetchingNextPage: boolean;
|
|
39
|
+
refetch?: () => void;
|
|
40
|
+
};
|
|
41
|
+
useCountsHook: (filters: Partial<TFilters>, options?: {
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
}) => {
|
|
44
|
+
data: TCounts | undefined;
|
|
45
|
+
isLoading: boolean;
|
|
46
|
+
};
|
|
47
|
+
additionalDefaults?: Partial<TFilters>;
|
|
48
|
+
apiPrefix?: 'admin' | 'superadmin';
|
|
49
|
+
}): {
|
|
50
|
+
filterConfigs: import("./useFilterConfigs.js").FilterConfigsResponse | undefined;
|
|
51
|
+
isLoadingConfigs: boolean;
|
|
52
|
+
items: TItem[];
|
|
53
|
+
counts: TCounts | undefined;
|
|
54
|
+
isLoading: boolean;
|
|
55
|
+
isLoadingCounts: boolean;
|
|
56
|
+
isFetchingNextPage: boolean;
|
|
57
|
+
filters: TFilters;
|
|
58
|
+
setFilter: <K extends keyof TFilters>(key: K, value: TFilters[K]) => void;
|
|
59
|
+
resetFilters: () => void;
|
|
60
|
+
sentinelRef: (node: HTMLDivElement | null) => void;
|
|
61
|
+
hasMore: boolean;
|
|
62
|
+
fetchNextPage: () => void;
|
|
63
|
+
refetch: (() => void) | undefined;
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=useGenericListWithConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGenericListWithConfig.d.ts","sourceRoot":"","sources":["../../src/hooks/useGenericListWithConfig.ts"],"names":[],"mappings":"AAiDA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,EAC7F,WAAW,EACX,MAAM,EACN,OAAO,EACP,WAAW,EACX,aAAa,EACb,kBAAuB,EACvB,SAAmB,GACpB,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK;QACnE,IAAI,EAAE,GAAG,CAAC;QACV,SAAS,EAAE,OAAO,CAAC;QACnB,aAAa,EAAE,MAAM,IAAI,CAAC;QAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;QACtB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;KACtB,CAAC;IACF,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK;QAC9E,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;QAC1B,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,kBAAkB,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,SAAS,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;CACpC;;;;;;;;;;;;;;;EAiCA"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useGenericList } from './useGenericList.js';
|
|
4
|
+
import { useFilterConfigs } from './useFilterConfigs.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate default filter values from backend filter configs.
|
|
7
|
+
* Inlined utility (no external dependency needed).
|
|
8
|
+
*/
|
|
9
|
+
function generateDefaultFilters(filters) {
|
|
10
|
+
if (!filters || filters.length === 0)
|
|
11
|
+
return {};
|
|
12
|
+
const defaults = {};
|
|
13
|
+
for (const filter of filters) {
|
|
14
|
+
switch (filter.type) {
|
|
15
|
+
case 'search':
|
|
16
|
+
defaults[filter.name] = '';
|
|
17
|
+
break;
|
|
18
|
+
case 'enum':
|
|
19
|
+
case 'column':
|
|
20
|
+
case 'nullable':
|
|
21
|
+
defaults[filter.name] = 'all';
|
|
22
|
+
break;
|
|
23
|
+
case 'multiselect':
|
|
24
|
+
defaults[filter.name] = [];
|
|
25
|
+
break;
|
|
26
|
+
case 'boolean':
|
|
27
|
+
defaults[filter.name] = '';
|
|
28
|
+
break;
|
|
29
|
+
case 'daterange':
|
|
30
|
+
defaults[filter.name] = '';
|
|
31
|
+
break;
|
|
32
|
+
case 'time':
|
|
33
|
+
defaults[filter.name] = '';
|
|
34
|
+
break;
|
|
35
|
+
case 'related':
|
|
36
|
+
defaults[filter.name] = '';
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
defaults[filter.name] = '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return defaults;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* useGenericListWithConfig - Config-Driven List Hook
|
|
46
|
+
*
|
|
47
|
+
* Zero double configuration - defaults auto-generated from backend config!
|
|
48
|
+
*
|
|
49
|
+
* BEFORE (useGenericList):
|
|
50
|
+
* - Manual defaultFilters in every page
|
|
51
|
+
* - Double configuration (backend config + frontend defaults)
|
|
52
|
+
* - Easy to forget filters
|
|
53
|
+
*
|
|
54
|
+
* AFTER (useGenericListWithConfig):
|
|
55
|
+
* - Defaults auto-generated from backend
|
|
56
|
+
* - Single source of truth
|
|
57
|
+
* - All filters automatically included
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const { items, counts, filters, setFilter } = useGenericListWithConfig({
|
|
61
|
+
* featureName: 'orders',
|
|
62
|
+
* entity: 'orders',
|
|
63
|
+
* dataKey: 'orders',
|
|
64
|
+
* useListHook: useOrdersList,
|
|
65
|
+
* useCountsHook: useOrdersCounts,
|
|
66
|
+
* });
|
|
67
|
+
*/
|
|
68
|
+
export function useGenericListWithConfig({ featureName, entity, dataKey, useListHook, useCountsHook, additionalDefaults = {}, apiPrefix = 'admin', }) {
|
|
69
|
+
// Fetch filter configs from backend
|
|
70
|
+
const { data: filterConfigsData, isLoading: isLoadingConfigs } = useFilterConfigs(featureName, { apiPrefix });
|
|
71
|
+
// Auto-generate defaultFilters from config
|
|
72
|
+
const defaultFilters = useMemo(() => {
|
|
73
|
+
const configDefaults = generateDefaultFilters(filterConfigsData?.filters);
|
|
74
|
+
return {
|
|
75
|
+
...configDefaults,
|
|
76
|
+
...additionalDefaults,
|
|
77
|
+
};
|
|
78
|
+
}, [filterConfigsData, additionalDefaults]);
|
|
79
|
+
// Wait for configs to load before fetching data
|
|
80
|
+
// This prevents race condition where first fetch uses empty filters
|
|
81
|
+
const shouldEnableQuery = !isLoadingConfigs && filterConfigsData !== undefined;
|
|
82
|
+
// Use standard useGenericList with auto-generated defaults
|
|
83
|
+
const listResult = useGenericList({
|
|
84
|
+
entity,
|
|
85
|
+
dataKey,
|
|
86
|
+
defaultFilters,
|
|
87
|
+
useListHook,
|
|
88
|
+
useCountsHook,
|
|
89
|
+
enabled: shouldEnableQuery,
|
|
90
|
+
});
|
|
91
|
+
// Return combined result
|
|
92
|
+
return {
|
|
93
|
+
...listResult,
|
|
94
|
+
filterConfigs: filterConfigsData,
|
|
95
|
+
isLoadingConfigs,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=useGenericListWithConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGenericListWithConfig.js","sourceRoot":"","sources":["../../src/hooks/useGenericListWithConfig.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGzD;;;GAGG;AACH,SAAS,sBAAsB,CAAC,OAAwB;IACtD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEhD,MAAM,QAAQ,GAAwB,EAAE,CAAC;IAEzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,QAAQ;gBACX,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,MAAM;YACR,KAAK,MAAM,CAAC;YACZ,KAAK,QAAQ,CAAC;YACd,KAAK,UAAU;gBACb,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;gBAC9B,MAAM;YACR,KAAK,aAAa;gBAChB,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,MAAM;YACR,KAAK,SAAS;gBACZ,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,MAAM;YACR,KAAK,WAAW;gBACd,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,MAAM;YACR,KAAK,MAAM;gBACT,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,MAAM;YACR,KAAK,SAAS;gBACZ,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,MAAM;YACR;gBACE,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,wBAAwB,CAAuD,EAC7F,WAAW,EACX,MAAM,EACN,OAAO,EACP,WAAW,EACX,aAAa,EACb,kBAAkB,GAAG,EAAE,EACvB,SAAS,GAAG,OAAO,GAqBpB;IACC,oCAAoC;IACpC,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IAE9G,2CAA2C;IAC3C,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE;QAClC,MAAM,cAAc,GAAG,sBAAsB,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;QAC1E,OAAO;YACL,GAAG,cAAc;YACjB,GAAG,kBAAkB;SACV,CAAC;IAChB,CAAC,EAAE,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAE5C,gDAAgD;IAChD,oEAAoE;IACpE,MAAM,iBAAiB,GAAG,CAAC,gBAAgB,IAAI,iBAAiB,KAAK,SAAS,CAAC;IAE/E,2DAA2D;IAC3D,MAAM,UAAU,GAAG,cAAc,CAA2B;QAC1D,MAAM;QACN,OAAO;QACP,cAAc;QACd,WAAW;QACX,aAAa;QACb,OAAO,EAAE,iBAAiB;KAC3B,CAAC,CAAC;IAEH,yBAAyB;IACzB,OAAO;QACL,GAAG,UAAU;QACb,aAAa,EAAE,iBAAiB;QAChC,gBAAgB;KACjB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface UseInfiniteScrollOptions {
|
|
2
|
+
onLoadMore: () => void;
|
|
3
|
+
hasMore: boolean;
|
|
4
|
+
threshold?: number;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const useInfiniteScroll: ({ onLoadMore, hasMore, threshold, enabled }: UseInfiniteScrollOptions) => {
|
|
8
|
+
sentinelRef: (node: HTMLDivElement | null) => void;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=useInfiniteScroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInfiniteScroll.d.ts","sourceRoot":"","sources":["../../src/hooks/useInfiniteScroll.ts"],"names":[],"mappings":"AAIA,UAAU,wBAAwB;IAChC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,iBAAiB,GAAI,6CAK/B,wBAAwB;wBAsCc,cAAc,GAAG,IAAI;CAoC7D,CAAC"}
|