@htlkg/data 0.0.21 → 0.0.22
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/hooks/index.d.ts +601 -94
- package/dist/hooks/index.js +682 -73
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +691 -82
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.js +4 -4
- package/dist/mutations/index.js.map +1 -1
- package/dist/queries/index.js +5 -5
- package/dist/queries/index.js.map +1 -1
- package/package.json +11 -12
- package/src/hooks/accounts/index.ts +2 -0
- package/src/hooks/{useAccounts.ts → accounts/useAccounts.ts} +48 -5
- package/src/hooks/accounts/usePaginatedAccounts.ts +166 -0
- package/src/hooks/brands/index.ts +2 -0
- package/src/hooks/{useBrands.ts → brands/useBrands.ts} +1 -1
- package/src/hooks/brands/usePaginatedBrands.ts +206 -0
- package/src/hooks/createPaginatedDataHook.ts +359 -0
- package/src/hooks/data-hook-errors.property.test.ts +4 -4
- package/src/hooks/data-hook-filters.property.test.ts +4 -4
- package/src/hooks/data-hooks.property.test.ts +4 -4
- package/src/hooks/index.ts +96 -8
- package/src/hooks/productInstances/index.ts +1 -0
- package/src/hooks/{useProductInstances.ts → productInstances/useProductInstances.ts} +9 -6
- package/src/hooks/products/index.ts +1 -0
- package/src/hooks/{useProducts.ts → products/useProducts.ts} +4 -5
- package/src/hooks/reservations/index.ts +2 -0
- package/src/hooks/reservations/usePaginatedReservations.ts +258 -0
- package/src/hooks/{useReservations.ts → reservations/useReservations.ts} +65 -10
- package/src/hooks/users/index.ts +2 -0
- package/src/hooks/users/usePaginatedUsers.ts +213 -0
- package/src/hooks/{useUsers.ts → users/useUsers.ts} +1 -1
- package/src/mutations/accounts/accounts.test.ts +287 -0
- package/src/mutations/{accounts.ts → accounts/accounts.ts} +2 -2
- package/src/mutations/accounts/index.ts +1 -0
- package/src/mutations/brands/brands.test.ts +292 -0
- package/src/mutations/{brands.ts → brands/brands.ts} +2 -2
- package/src/mutations/brands/index.ts +1 -0
- package/src/mutations/reservations/index.ts +1 -0
- package/src/mutations/{reservations.test.ts → reservations/reservations.test.ts} +1 -1
- package/src/mutations/{reservations.ts → reservations/reservations.ts} +2 -2
- package/src/mutations/users/index.ts +1 -0
- package/src/mutations/users/users.test.ts +289 -0
- package/src/mutations/{users.ts → users/users.ts} +2 -2
- package/src/queries/accounts/accounts.test.ts +228 -0
- package/src/queries/accounts/index.ts +1 -0
- package/src/queries/brands/brands.test.ts +288 -0
- package/src/queries/brands/index.ts +1 -0
- package/src/queries/products/index.ts +1 -0
- package/src/queries/products/products.test.ts +347 -0
- package/src/queries/reservations/index.ts +1 -0
- package/src/queries/users/index.ts +1 -0
- package/src/queries/users/users.test.ts +301 -0
- /package/src/queries/{accounts.ts → accounts/accounts.ts} +0 -0
- /package/src/queries/{brands.ts → brands/brands.ts} +0 -0
- /package/src/queries/{products.ts → products/products.ts} +0 -0
- /package/src/queries/{reservations.test.ts → reservations/reservations.test.ts} +0 -0
- /package/src/queries/{reservations.ts → reservations/reservations.ts} +0 -0
- /package/src/queries/{users.ts → users/users.ts} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated Brands Hooks
|
|
3
|
+
*
|
|
4
|
+
* Vue composables for fetching brands with server-side pagination.
|
|
5
|
+
* Provides separate hooks for active and deleted brands to enable
|
|
6
|
+
* efficient tab-based filtering in large datasets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Ref, ComputedRef } from "vue";
|
|
10
|
+
import type { Brand } from "@htlkg/core/types";
|
|
11
|
+
import {
|
|
12
|
+
createPaginatedDataHook,
|
|
13
|
+
ACTIVE_FILTER,
|
|
14
|
+
DELETED_FILTER,
|
|
15
|
+
type PaginatedHookOptions,
|
|
16
|
+
type PaginationState,
|
|
17
|
+
} from "../createPaginatedDataHook";
|
|
18
|
+
|
|
19
|
+
/** Extended Brand type with computed fields */
|
|
20
|
+
export type BrandWithCounts = Brand & {
|
|
21
|
+
accountName?: string;
|
|
22
|
+
enabledProductCount?: number;
|
|
23
|
+
totalProductCount?: number;
|
|
24
|
+
deletedAt?: string;
|
|
25
|
+
deletedBy?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface UsePaginatedBrandsOptions extends PaginatedHookOptions {
|
|
29
|
+
/** Filter by account ID */
|
|
30
|
+
accountId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UsePaginatedBrandsReturn {
|
|
34
|
+
/** Reactive array of brands */
|
|
35
|
+
brands: Ref<BrandWithCounts[]>;
|
|
36
|
+
/** Loading state (true during any fetch) */
|
|
37
|
+
loading: Ref<boolean>;
|
|
38
|
+
/** Loading state for initial fetch */
|
|
39
|
+
initialLoading: Ref<boolean>;
|
|
40
|
+
/** Loading state for loadMore */
|
|
41
|
+
loadingMore: Ref<boolean>;
|
|
42
|
+
/** Error state */
|
|
43
|
+
error: Ref<Error | null>;
|
|
44
|
+
/** Pagination state */
|
|
45
|
+
pagination: Ref<PaginationState>;
|
|
46
|
+
/** Whether there are more items to load */
|
|
47
|
+
hasMore: ComputedRef<boolean>;
|
|
48
|
+
/** Load next page of data */
|
|
49
|
+
loadMore: () => Promise<void>;
|
|
50
|
+
/** Refetch data (resets to first page) */
|
|
51
|
+
refetch: () => Promise<void>;
|
|
52
|
+
/** Reset data and pagination state */
|
|
53
|
+
reset: () => void;
|
|
54
|
+
/** Update search filter and refetch from server (searches ALL data) */
|
|
55
|
+
setSearchFilter: (filter: any) => Promise<void>;
|
|
56
|
+
/** Current search filter */
|
|
57
|
+
searchFilter: Ref<any>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build additional filter from hook options
|
|
62
|
+
*/
|
|
63
|
+
function buildFilter(options: UsePaginatedBrandsOptions): any {
|
|
64
|
+
const conditions: any[] = [];
|
|
65
|
+
|
|
66
|
+
if (options.accountId) {
|
|
67
|
+
conditions.push({ accountId: { eq: options.accountId } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
71
|
+
conditions.push(options.filter);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (conditions.length === 0) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
if (conditions.length === 1) {
|
|
78
|
+
return conditions[0];
|
|
79
|
+
}
|
|
80
|
+
return { and: conditions };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Transform brand data
|
|
85
|
+
*/
|
|
86
|
+
function transformBrand(brand: any): BrandWithCounts {
|
|
87
|
+
return {
|
|
88
|
+
...brand,
|
|
89
|
+
accountName: brand.account?.name || "",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Selection set for brand queries
|
|
95
|
+
*/
|
|
96
|
+
const BRAND_SELECTION_SET = [
|
|
97
|
+
"id",
|
|
98
|
+
"name",
|
|
99
|
+
"accountId",
|
|
100
|
+
"logo",
|
|
101
|
+
"timezone",
|
|
102
|
+
"status",
|
|
103
|
+
"settings",
|
|
104
|
+
"deletedAt",
|
|
105
|
+
"deletedBy",
|
|
106
|
+
"account.name",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Internal hook for active brands
|
|
111
|
+
*/
|
|
112
|
+
const useActiveBrandsInternal = createPaginatedDataHook<BrandWithCounts, UsePaginatedBrandsOptions>({
|
|
113
|
+
model: "Brand",
|
|
114
|
+
dataPropertyName: "brands",
|
|
115
|
+
defaultPageSize: 25,
|
|
116
|
+
selectionSet: BRAND_SELECTION_SET,
|
|
117
|
+
transform: transformBrand,
|
|
118
|
+
buildFilter,
|
|
119
|
+
baseFilter: ACTIVE_FILTER,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Internal hook for deleted brands
|
|
124
|
+
*/
|
|
125
|
+
const useDeletedBrandsInternal = createPaginatedDataHook<BrandWithCounts, UsePaginatedBrandsOptions>({
|
|
126
|
+
model: "Brand",
|
|
127
|
+
dataPropertyName: "brands",
|
|
128
|
+
defaultPageSize: 25,
|
|
129
|
+
selectionSet: BRAND_SELECTION_SET,
|
|
130
|
+
transform: transformBrand,
|
|
131
|
+
buildFilter,
|
|
132
|
+
baseFilter: DELETED_FILTER,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Composable for fetching active (non-deleted) brands with pagination
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* import { useActiveBrands } from '@htlkg/data/hooks';
|
|
141
|
+
*
|
|
142
|
+
* const { brands, loading, hasMore, loadMore, refetch } = useActiveBrands({
|
|
143
|
+
* pageSize: 25,
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* // Load more when user scrolls or clicks "Load More"
|
|
147
|
+
* async function onLoadMore() {
|
|
148
|
+
* await loadMore();
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example With account filter
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const { brands, loading } = useActiveBrands({
|
|
155
|
+
* accountId: 'account-123',
|
|
156
|
+
* pageSize: 50
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function useActiveBrands(options: UsePaginatedBrandsOptions = {}): UsePaginatedBrandsReturn {
|
|
161
|
+
const result = useActiveBrandsInternal(options);
|
|
162
|
+
return {
|
|
163
|
+
brands: result.brands as Ref<BrandWithCounts[]>,
|
|
164
|
+
loading: result.loading,
|
|
165
|
+
initialLoading: result.initialLoading,
|
|
166
|
+
loadingMore: result.loadingMore,
|
|
167
|
+
error: result.error,
|
|
168
|
+
pagination: result.pagination,
|
|
169
|
+
hasMore: result.hasMore,
|
|
170
|
+
loadMore: result.loadMore,
|
|
171
|
+
refetch: result.refetch,
|
|
172
|
+
reset: result.reset,
|
|
173
|
+
setSearchFilter: result.setSearchFilter,
|
|
174
|
+
searchFilter: result.searchFilter,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Composable for fetching deleted brands with pagination
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* import { useDeletedBrands } from '@htlkg/data/hooks';
|
|
184
|
+
*
|
|
185
|
+
* const { brands, loading, hasMore, loadMore, refetch } = useDeletedBrands({
|
|
186
|
+
* pageSize: 25,
|
|
187
|
+
* });
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function useDeletedBrands(options: UsePaginatedBrandsOptions = {}): UsePaginatedBrandsReturn {
|
|
191
|
+
const result = useDeletedBrandsInternal(options);
|
|
192
|
+
return {
|
|
193
|
+
brands: result.brands as Ref<BrandWithCounts[]>,
|
|
194
|
+
loading: result.loading,
|
|
195
|
+
initialLoading: result.initialLoading,
|
|
196
|
+
loadingMore: result.loadingMore,
|
|
197
|
+
error: result.error,
|
|
198
|
+
pagination: result.pagination,
|
|
199
|
+
hasMore: result.hasMore,
|
|
200
|
+
loadMore: result.loadMore,
|
|
201
|
+
refetch: result.refetch,
|
|
202
|
+
reset: result.reset,
|
|
203
|
+
setSearchFilter: result.setSearchFilter,
|
|
204
|
+
searchFilter: result.searchFilter,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated Data Hook Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates reusable Vue composables for fetching paginated data from GraphQL models.
|
|
5
|
+
* Supports cursor-based pagination (nextToken) for efficient handling of large datasets.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Server-side pagination with nextToken
|
|
9
|
+
* - loadMore() for infinite scroll/pagination
|
|
10
|
+
* - hasMore indicator
|
|
11
|
+
* - Independent pagination state per hook instance
|
|
12
|
+
* - Built-in support for active/deleted filtering
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ref, computed, onMounted, type Ref, type ComputedRef } from "vue";
|
|
16
|
+
import { query, hasErrors, getErrorMessage } from "../client/proxy";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Filter for active items (not deleted)
|
|
20
|
+
* Uses OR condition because deletedAt can be:
|
|
21
|
+
* - Missing (attributeExists: false)
|
|
22
|
+
* - Explicitly null (eq: null)
|
|
23
|
+
*/
|
|
24
|
+
export const ACTIVE_FILTER = {
|
|
25
|
+
or: [{ deletedAt: { attributeExists: false } }, { deletedAt: { eq: null } }],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Filter for deleted items
|
|
30
|
+
* Uses gt: "" to match only actual timestamp strings (not null or missing)
|
|
31
|
+
*/
|
|
32
|
+
export const DELETED_FILTER = {
|
|
33
|
+
deletedAt: { gt: "" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration options for creating a paginated data hook
|
|
38
|
+
*/
|
|
39
|
+
export interface CreatePaginatedDataHookOptions<T, TOptions extends PaginatedHookOptions = PaginatedHookOptions> {
|
|
40
|
+
/** The GraphQL model name (e.g., 'Account', 'User', 'Brand') */
|
|
41
|
+
model: string;
|
|
42
|
+
/** Default page size */
|
|
43
|
+
defaultPageSize?: number;
|
|
44
|
+
/** Selection set for the query (fields to fetch) */
|
|
45
|
+
selectionSet?: string[];
|
|
46
|
+
/** Transform function to apply to fetched data */
|
|
47
|
+
transform?: (item: any) => T;
|
|
48
|
+
/** Build filter from hook options */
|
|
49
|
+
buildFilter?: (options: TOptions) => any;
|
|
50
|
+
/** Plural name for the data property (e.g., 'accounts', 'users') */
|
|
51
|
+
dataPropertyName?: string;
|
|
52
|
+
/** Base filter to always apply (e.g., for active/deleted variants) */
|
|
53
|
+
baseFilter?: any;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Base options available to paginated hooks
|
|
58
|
+
*/
|
|
59
|
+
export interface PaginatedHookOptions {
|
|
60
|
+
/** Additional filter criteria (merged with baseFilter) */
|
|
61
|
+
filter?: any;
|
|
62
|
+
/** Page size (default: 25) */
|
|
63
|
+
pageSize?: number;
|
|
64
|
+
/** Auto-fetch on mount (default: true) */
|
|
65
|
+
autoFetch?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pagination state
|
|
70
|
+
*/
|
|
71
|
+
export interface PaginationState {
|
|
72
|
+
/** Current nextToken for fetching next page */
|
|
73
|
+
nextToken: string | null;
|
|
74
|
+
/** Whether there are more items to load */
|
|
75
|
+
hasMore: boolean;
|
|
76
|
+
/** Total items loaded so far */
|
|
77
|
+
loadedCount: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return type for paginated data hooks
|
|
82
|
+
*/
|
|
83
|
+
export interface PaginatedDataHookReturn<T> {
|
|
84
|
+
/** Reactive array of data (accumulated across pages) */
|
|
85
|
+
data: Ref<T[]>;
|
|
86
|
+
/** Loading state (true during any fetch) */
|
|
87
|
+
loading: Ref<boolean>;
|
|
88
|
+
/** Loading state for initial fetch */
|
|
89
|
+
initialLoading: Ref<boolean>;
|
|
90
|
+
/** Loading state for loadMore */
|
|
91
|
+
loadingMore: Ref<boolean>;
|
|
92
|
+
/** Error state */
|
|
93
|
+
error: Ref<Error | null>;
|
|
94
|
+
/** Pagination state */
|
|
95
|
+
pagination: Ref<PaginationState>;
|
|
96
|
+
/** Whether there are more items to load */
|
|
97
|
+
hasMore: ComputedRef<boolean>;
|
|
98
|
+
/** Load next page of data */
|
|
99
|
+
loadMore: () => Promise<void>;
|
|
100
|
+
/** Refetch data (resets to first page) */
|
|
101
|
+
refetch: () => Promise<void>;
|
|
102
|
+
/** Reset data and pagination state */
|
|
103
|
+
reset: () => void;
|
|
104
|
+
/** Update search filter and refetch from server (searches ALL data) */
|
|
105
|
+
setSearchFilter: (filter: any) => Promise<void>;
|
|
106
|
+
/** Current search filter */
|
|
107
|
+
searchFilter: Ref<any>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates a reusable paginated data hook for a specific model
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // Create a hook for active brands
|
|
116
|
+
* export const useActiveBrands = createPaginatedDataHook<Brand>({
|
|
117
|
+
* model: 'Brand',
|
|
118
|
+
* dataPropertyName: 'brands',
|
|
119
|
+
* baseFilter: ACTIVE_FILTER,
|
|
120
|
+
* selectionSet: ['id', 'name', 'status', 'accountId', 'account.name'],
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* // Usage in component
|
|
124
|
+
* const { brands, loading, hasMore, loadMore, refetch } = useActiveBrands({
|
|
125
|
+
* pageSize: 25,
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example With custom filter builder
|
|
130
|
+
* ```typescript
|
|
131
|
+
* interface UseBrandsOptions extends PaginatedHookOptions {
|
|
132
|
+
* accountId?: string;
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* export const useActiveBrands = createPaginatedDataHook<Brand, UseBrandsOptions>({
|
|
136
|
+
* model: 'Brand',
|
|
137
|
+
* dataPropertyName: 'brands',
|
|
138
|
+
* baseFilter: ACTIVE_FILTER,
|
|
139
|
+
* buildFilter: (options) => {
|
|
140
|
+
* if (options.accountId) {
|
|
141
|
+
* return { accountId: { eq: options.accountId } };
|
|
142
|
+
* }
|
|
143
|
+
* return undefined;
|
|
144
|
+
* },
|
|
145
|
+
* });
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function createPaginatedDataHook<T, TOptions extends PaginatedHookOptions = PaginatedHookOptions>(
|
|
149
|
+
config: CreatePaginatedDataHookOptions<T, TOptions>,
|
|
150
|
+
) {
|
|
151
|
+
const {
|
|
152
|
+
model,
|
|
153
|
+
defaultPageSize = 25,
|
|
154
|
+
selectionSet,
|
|
155
|
+
transform,
|
|
156
|
+
buildFilter,
|
|
157
|
+
dataPropertyName = "data",
|
|
158
|
+
baseFilter,
|
|
159
|
+
} = config;
|
|
160
|
+
|
|
161
|
+
return function usePaginatedData(
|
|
162
|
+
options: TOptions = {} as TOptions,
|
|
163
|
+
): PaginatedDataHookReturn<T> & Record<string, any> {
|
|
164
|
+
const { filter: additionalFilter, pageSize = defaultPageSize, autoFetch = true } = options;
|
|
165
|
+
|
|
166
|
+
// State
|
|
167
|
+
const data = ref<T[]>([]) as Ref<T[]>;
|
|
168
|
+
const loading = ref(false);
|
|
169
|
+
const initialLoading = ref(false);
|
|
170
|
+
const loadingMore = ref(false);
|
|
171
|
+
const error = ref<Error | null>(null);
|
|
172
|
+
const pagination = ref<PaginationState>({
|
|
173
|
+
nextToken: null,
|
|
174
|
+
hasMore: true,
|
|
175
|
+
loadedCount: 0,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Dynamic search filter (for server-side filtering)
|
|
179
|
+
const searchFilter = ref<any>(null);
|
|
180
|
+
|
|
181
|
+
// Computed
|
|
182
|
+
const hasMore = computed(() => pagination.value.hasMore);
|
|
183
|
+
|
|
184
|
+
// Build combined filter
|
|
185
|
+
const getFilter = () => {
|
|
186
|
+
const filters: any[] = [];
|
|
187
|
+
|
|
188
|
+
// Add base filter (e.g., active/deleted)
|
|
189
|
+
if (baseFilter) {
|
|
190
|
+
filters.push(baseFilter);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add custom filter from buildFilter
|
|
194
|
+
if (buildFilter) {
|
|
195
|
+
const customFilter = buildFilter(options);
|
|
196
|
+
if (customFilter) {
|
|
197
|
+
filters.push(customFilter);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add additional filter from options
|
|
202
|
+
if (additionalFilter && Object.keys(additionalFilter).length > 0) {
|
|
203
|
+
filters.push(additionalFilter);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add dynamic search filter (for server-side search)
|
|
207
|
+
if (searchFilter.value && Object.keys(searchFilter.value).length > 0) {
|
|
208
|
+
filters.push(searchFilter.value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Combine filters with AND
|
|
212
|
+
if (filters.length === 0) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if (filters.length === 1) {
|
|
216
|
+
return filters[0];
|
|
217
|
+
}
|
|
218
|
+
return { and: filters };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Fetch function
|
|
222
|
+
async function fetchPage(nextToken?: string | null, append: boolean = false) {
|
|
223
|
+
if (!append) {
|
|
224
|
+
initialLoading.value = true;
|
|
225
|
+
} else {
|
|
226
|
+
loadingMore.value = true;
|
|
227
|
+
}
|
|
228
|
+
loading.value = true;
|
|
229
|
+
error.value = null;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const queryOptions: Record<string, any> = {
|
|
233
|
+
limit: pageSize,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const filter = getFilter();
|
|
237
|
+
if (filter) {
|
|
238
|
+
queryOptions.filter = filter;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (nextToken) {
|
|
242
|
+
queryOptions.nextToken = nextToken;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (selectionSet) {
|
|
246
|
+
queryOptions.selectionSet = selectionSet;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const response = await query(model, "list", queryOptions);
|
|
250
|
+
|
|
251
|
+
if (hasErrors(response)) {
|
|
252
|
+
throw new Error(getErrorMessage(response) || `Failed to fetch ${model}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const items = response.data || [];
|
|
256
|
+
const transformedItems = transform ? items.map(transform) : items;
|
|
257
|
+
|
|
258
|
+
if (append) {
|
|
259
|
+
data.value = [...data.value, ...transformedItems];
|
|
260
|
+
} else {
|
|
261
|
+
data.value = transformedItems;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Update pagination state
|
|
265
|
+
// Note: Amplify returns nextToken in the response
|
|
266
|
+
const responseNextToken = (response as any).nextToken;
|
|
267
|
+
pagination.value = {
|
|
268
|
+
nextToken: responseNextToken || null,
|
|
269
|
+
hasMore: !!responseNextToken,
|
|
270
|
+
loadedCount: data.value.length,
|
|
271
|
+
};
|
|
272
|
+
} catch (e) {
|
|
273
|
+
error.value = e as Error;
|
|
274
|
+
console.error(`[use${model}] Error fetching ${model}:`, e);
|
|
275
|
+
} finally {
|
|
276
|
+
loading.value = false;
|
|
277
|
+
initialLoading.value = false;
|
|
278
|
+
loadingMore.value = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Load more function
|
|
283
|
+
async function loadMore() {
|
|
284
|
+
if (loadingMore.value || !pagination.value.hasMore) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
await fetchPage(pagination.value.nextToken, true);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Refetch (reset to first page)
|
|
291
|
+
async function refetch() {
|
|
292
|
+
pagination.value = {
|
|
293
|
+
nextToken: null,
|
|
294
|
+
hasMore: true,
|
|
295
|
+
loadedCount: 0,
|
|
296
|
+
};
|
|
297
|
+
await fetchPage(null, false);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Reset state
|
|
301
|
+
function reset() {
|
|
302
|
+
data.value = [];
|
|
303
|
+
error.value = null;
|
|
304
|
+
searchFilter.value = null;
|
|
305
|
+
pagination.value = {
|
|
306
|
+
nextToken: null,
|
|
307
|
+
hasMore: true,
|
|
308
|
+
loadedCount: 0,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Set search filter and refetch from server (searches ALL data)
|
|
313
|
+
async function setSearchFilter(filter: any) {
|
|
314
|
+
searchFilter.value = filter;
|
|
315
|
+
// Reset pagination and fetch with new filter
|
|
316
|
+
pagination.value = {
|
|
317
|
+
nextToken: null,
|
|
318
|
+
hasMore: true,
|
|
319
|
+
loadedCount: 0,
|
|
320
|
+
};
|
|
321
|
+
await fetchPage(null, false);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Auto-fetch on mount if enabled
|
|
325
|
+
if (autoFetch) {
|
|
326
|
+
onMounted(() => {
|
|
327
|
+
fetchPage(null, false);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Build return object
|
|
332
|
+
const result: PaginatedDataHookReturn<T> & Record<string, any> = {
|
|
333
|
+
data,
|
|
334
|
+
loading,
|
|
335
|
+
initialLoading,
|
|
336
|
+
loadingMore,
|
|
337
|
+
error,
|
|
338
|
+
pagination,
|
|
339
|
+
hasMore,
|
|
340
|
+
loadMore,
|
|
341
|
+
refetch,
|
|
342
|
+
reset,
|
|
343
|
+
setSearchFilter,
|
|
344
|
+
searchFilter,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Add data property with custom name for backwards compatibility
|
|
348
|
+
if (dataPropertyName !== "data") {
|
|
349
|
+
result[dataPropertyName] = data;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return result;
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Type helper to extract the return type of a created paginated hook
|
|
358
|
+
*/
|
|
359
|
+
export type InferPaginatedHookReturn<THook extends (...args: any[]) => any> = ReturnType<THook>;
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
11
11
|
import * as fc from "fast-check";
|
|
12
|
-
import { useBrands } from "./useBrands";
|
|
13
|
-
import { useAccounts } from "./useAccounts";
|
|
14
|
-
import { useUsers } from "./useUsers";
|
|
15
|
-
import { useProducts } from "./useProducts";
|
|
12
|
+
import { useBrands } from "./brands/useBrands";
|
|
13
|
+
import { useAccounts } from "./accounts/useAccounts";
|
|
14
|
+
import { useUsers } from "./users/useUsers";
|
|
15
|
+
import { useProducts } from "./products/useProducts";
|
|
16
16
|
|
|
17
17
|
// Create separate mock functions for each model
|
|
18
18
|
const mockBrandList = vi.fn();
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
11
11
|
import * as fc from "fast-check";
|
|
12
|
-
import { useBrands } from "./useBrands";
|
|
13
|
-
import { useAccounts } from "./useAccounts";
|
|
14
|
-
import { useUsers } from "./useUsers";
|
|
15
|
-
import { useProducts } from "./useProducts";
|
|
12
|
+
import { useBrands } from "./brands/useBrands";
|
|
13
|
+
import { useAccounts } from "./accounts/useAccounts";
|
|
14
|
+
import { useUsers } from "./users/useUsers";
|
|
15
|
+
import { useProducts } from "./products/useProducts";
|
|
16
16
|
import type { Brand, Account, Product } from "@htlkg/core/types";
|
|
17
17
|
|
|
18
18
|
// Create separate mock functions for each model
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
11
11
|
import * as fc from "fast-check";
|
|
12
12
|
import { ref, isRef, isReactive, computed } from "vue";
|
|
13
|
-
import { useBrands } from "./useBrands";
|
|
14
|
-
import { useAccounts } from "./useAccounts";
|
|
15
|
-
import { useUsers } from "./useUsers";
|
|
16
|
-
import { useProducts } from "./useProducts";
|
|
13
|
+
import { useBrands } from "./brands/useBrands";
|
|
14
|
+
import { useAccounts } from "./accounts/useAccounts";
|
|
15
|
+
import { useUsers } from "./users/useUsers";
|
|
16
|
+
import { useProducts } from "./products/useProducts";
|
|
17
17
|
|
|
18
18
|
// Mock the client module
|
|
19
19
|
vi.mock("../client", () => ({
|