@htlkg/astro 0.0.1 → 0.0.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/README.md +24 -8
- package/dist/chunk-2GML443T.js +273 -0
- package/dist/chunk-2GML443T.js.map +1 -0
- package/dist/{chunk-Z2ZAL7KX.js → chunk-UBF5F2RG.js} +1 -1
- package/dist/{chunk-Z2ZAL7KX.js.map → chunk-UBF5F2RG.js.map} +1 -1
- package/dist/chunk-XOY5BM3N.js +151 -0
- package/dist/chunk-XOY5BM3N.js.map +1 -0
- package/dist/htlkg/config.js +1 -1
- package/dist/htlkg/index.js +1 -1
- package/dist/index.js +126 -14
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.js +27 -28
- package/dist/middleware/index.js.map +1 -1
- package/dist/utils/index.js +31 -12
- package/dist/vue-app-setup.js +47 -0
- package/dist/vue-app-setup.js.map +1 -0
- package/package.json +60 -26
- package/src/auth/auth.md +77 -0
- package/src/components/Island.astro +56 -0
- package/src/components/components.md +79 -0
- package/src/factories/createListPage.ts +290 -0
- package/src/factories/index.ts +16 -0
- package/src/htlkg/config.ts +10 -0
- package/src/htlkg/htlkg.md +63 -0
- package/src/htlkg/index.ts +49 -157
- package/src/index.ts +3 -0
- package/src/layouts/AdminLayout.astro +103 -92
- package/src/layouts/layouts.md +87 -0
- package/src/middleware/auth.ts +42 -0
- package/src/middleware/middleware.md +82 -0
- package/src/middleware/route-guards.ts +4 -28
- package/src/patterns/patterns.md +104 -0
- package/src/utils/filters.ts +320 -0
- package/src/utils/index.ts +8 -2
- package/src/utils/params.ts +260 -0
- package/src/utils/utils.md +86 -0
- package/src/vue-app-setup.ts +21 -28
- package/dist/chunk-WLOFOVCL.js +0 -210
- package/dist/chunk-WLOFOVCL.js.map +0 -1
- package/dist/chunk-ZQ4XMJH7.js +0 -1
- package/dist/chunk-ZQ4XMJH7.js.map +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Middleware Module
|
|
2
|
+
|
|
3
|
+
Authentication middleware and route guards for Astro.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
requireAuth,
|
|
10
|
+
requireAdminAccess,
|
|
11
|
+
requireBrandAccess,
|
|
12
|
+
} from '@htlkg/astro/middleware';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Route Guards
|
|
16
|
+
|
|
17
|
+
### requireAuth
|
|
18
|
+
|
|
19
|
+
Require authenticated user. Redirects to login if not authenticated.
|
|
20
|
+
|
|
21
|
+
```astro
|
|
22
|
+
---
|
|
23
|
+
import { requireAuth } from '@htlkg/astro/middleware';
|
|
24
|
+
|
|
25
|
+
export const prerender = false;
|
|
26
|
+
|
|
27
|
+
const { user } = await requireAuth(Astro);
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<h1>Welcome, {user.username}</h1>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### requireAdminAccess
|
|
34
|
+
|
|
35
|
+
Require admin role.
|
|
36
|
+
|
|
37
|
+
```astro
|
|
38
|
+
---
|
|
39
|
+
import { requireAdminAccess } from '@htlkg/astro/middleware';
|
|
40
|
+
|
|
41
|
+
export const prerender = false;
|
|
42
|
+
|
|
43
|
+
const { user } = await requireAdminAccess(Astro);
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<AdminDashboard user={user} />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### requireBrandAccess
|
|
50
|
+
|
|
51
|
+
Require access to a specific brand.
|
|
52
|
+
|
|
53
|
+
```astro
|
|
54
|
+
---
|
|
55
|
+
import { requireBrandAccess } from '@htlkg/astro/middleware';
|
|
56
|
+
|
|
57
|
+
export const prerender = false;
|
|
58
|
+
|
|
59
|
+
const { brandId } = Astro.params;
|
|
60
|
+
const { user, brand } = await requireBrandAccess(Astro, brandId);
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
<BrandDashboard brand={brand} user={user} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Custom Login URL
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
await requireAuth(Astro, '/custom-login');
|
|
70
|
+
await requireAdminAccess(Astro, '/admin/login');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Route Guard Config
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
interface RouteGuardConfig {
|
|
77
|
+
publicRoutes: RoutePattern[];
|
|
78
|
+
protectedRoutes: RoutePattern[];
|
|
79
|
+
loginPage: string;
|
|
80
|
+
brandRoutePattern?: RegExp;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -36,7 +36,7 @@ function matchesPattern(pathname: string, patterns: RoutePattern[]): boolean {
|
|
|
36
36
|
} catch (error) {
|
|
37
37
|
// Log pattern matching errors but don't block the request
|
|
38
38
|
console.error(
|
|
39
|
-
'[htlkg
|
|
39
|
+
'[htlkg] Error matching route pattern:',
|
|
40
40
|
error instanceof Error ? error.message : 'Unknown error',
|
|
41
41
|
);
|
|
42
42
|
return false;
|
|
@@ -78,7 +78,6 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
|
|
|
78
78
|
try {
|
|
79
79
|
// Public routes - no auth required
|
|
80
80
|
if (matchesPattern(pathname, publicRoutes)) {
|
|
81
|
-
console.log(`[htlkg Route Guard] Public route: ${pathname}`);
|
|
82
81
|
return next();
|
|
83
82
|
}
|
|
84
83
|
|
|
@@ -87,14 +86,8 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
|
|
|
87
86
|
// Admin routes - require admin role
|
|
88
87
|
if (matchesPattern(pathname, adminRoutes)) {
|
|
89
88
|
if (!user || !user.isAdmin) {
|
|
90
|
-
console.log(
|
|
91
|
-
`[htlkg Route Guard] Admin access denied for ${pathname} - User: ${user ? 'authenticated (non-admin)' : 'not authenticated'}`,
|
|
92
|
-
);
|
|
93
89
|
return redirect(`${loginUrl}?error=admin_required`);
|
|
94
90
|
}
|
|
95
|
-
console.log(
|
|
96
|
-
`[htlkg Route Guard] Admin access granted for ${pathname}`,
|
|
97
|
-
);
|
|
98
91
|
return next();
|
|
99
92
|
}
|
|
100
93
|
|
|
@@ -106,26 +99,18 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
|
|
|
106
99
|
const brandId = Number.parseInt(match[brandRoute.brandIdParam], 10);
|
|
107
100
|
|
|
108
101
|
if (Number.isNaN(brandId)) {
|
|
109
|
-
console.warn(
|
|
110
|
-
`[htlkg Route Guard] Invalid brandId extracted from ${pathname}`,
|
|
111
|
-
);
|
|
102
|
+
console.warn(`[htlkg] Invalid brandId extracted from ${pathname}`);
|
|
112
103
|
return redirect(`${loginUrl}?error=invalid_brand`);
|
|
113
104
|
}
|
|
114
105
|
|
|
115
106
|
if (!user || (!user.isAdmin && !user.brandIds.includes(brandId))) {
|
|
116
|
-
console.log(
|
|
117
|
-
`[htlkg Route Guard] Brand access denied for ${pathname} (brandId: ${brandId}) - User: ${user ? `authenticated (brandIds: ${user.brandIds.join(',')})` : 'not authenticated'}`,
|
|
118
|
-
);
|
|
119
107
|
return redirect(`${loginUrl}?error=access_denied`);
|
|
120
108
|
}
|
|
121
|
-
console.log(
|
|
122
|
-
`[htlkg Route Guard] Brand access granted for ${pathname} (brandId: ${brandId})`,
|
|
123
|
-
);
|
|
124
109
|
return next();
|
|
125
110
|
}
|
|
126
111
|
} catch (error) {
|
|
127
112
|
console.error(
|
|
128
|
-
`[htlkg
|
|
113
|
+
`[htlkg] Error processing brand route for ${pathname}:`,
|
|
129
114
|
error instanceof Error ? error.message : 'Unknown error',
|
|
130
115
|
);
|
|
131
116
|
// Fail-safe: deny access on error
|
|
@@ -137,26 +122,17 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
|
|
|
137
122
|
if (matchesPattern(pathname, authenticatedRoutes)) {
|
|
138
123
|
if (!user) {
|
|
139
124
|
const returnUrl = encodeURIComponent(pathname + url.search);
|
|
140
|
-
console.log(
|
|
141
|
-
`[htlkg Route Guard] Authentication required for ${pathname}, redirecting to login`,
|
|
142
|
-
);
|
|
143
125
|
return redirect(`${loginUrl}?redirect=${returnUrl}`);
|
|
144
126
|
}
|
|
145
|
-
console.log(
|
|
146
|
-
`[htlkg Route Guard] Authenticated access granted for ${pathname}`,
|
|
147
|
-
);
|
|
148
127
|
return next();
|
|
149
128
|
}
|
|
150
129
|
|
|
151
130
|
// Default: allow access
|
|
152
|
-
console.log(
|
|
153
|
-
`[htlkg Route Guard] Default access granted for ${pathname}`,
|
|
154
|
-
);
|
|
155
131
|
return next();
|
|
156
132
|
} catch (error) {
|
|
157
133
|
// Catch-all error handler for route guard
|
|
158
134
|
console.error(
|
|
159
|
-
'[htlkg
|
|
135
|
+
'[htlkg] Unexpected error in route guard:',
|
|
160
136
|
error instanceof Error ? error.message : 'Unknown error',
|
|
161
137
|
);
|
|
162
138
|
// Fail-safe: deny access and redirect to login
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Patterns Module
|
|
2
|
+
|
|
3
|
+
Reusable page patterns for common admin and brand pages.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Admin patterns
|
|
9
|
+
import { ListPage } from '@htlkg/astro/patterns/admin';
|
|
10
|
+
import { DetailPage } from '@htlkg/astro/patterns/admin';
|
|
11
|
+
import { FormPage } from '@htlkg/astro/patterns/admin';
|
|
12
|
+
|
|
13
|
+
// Brand patterns
|
|
14
|
+
import { ConfigPage } from '@htlkg/astro/patterns/brand';
|
|
15
|
+
import { PortalPage } from '@htlkg/astro/patterns/brand';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Admin Patterns
|
|
19
|
+
|
|
20
|
+
### ListPage
|
|
21
|
+
|
|
22
|
+
List/table view with filters and pagination.
|
|
23
|
+
|
|
24
|
+
```astro
|
|
25
|
+
---
|
|
26
|
+
import { ListPage } from '@htlkg/astro/patterns/admin';
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<ListPage
|
|
30
|
+
title="Brands"
|
|
31
|
+
items={brands}
|
|
32
|
+
columns={columns}
|
|
33
|
+
filters={filters}
|
|
34
|
+
onRowClick={(brand) => `/admin/brands/${brand.id}`}
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### DetailPage
|
|
39
|
+
|
|
40
|
+
Detail view with tabs and actions.
|
|
41
|
+
|
|
42
|
+
```astro
|
|
43
|
+
---
|
|
44
|
+
import { DetailPage } from '@htlkg/astro/patterns/admin';
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<DetailPage
|
|
48
|
+
title={brand.name}
|
|
49
|
+
tabs={['Overview', 'Settings', 'Users']}
|
|
50
|
+
actions={['Edit', 'Delete']}
|
|
51
|
+
>
|
|
52
|
+
<BrandOverview slot="Overview" brand={brand} />
|
|
53
|
+
<BrandSettings slot="Settings" brand={brand} />
|
|
54
|
+
<BrandUsers slot="Users" brand={brand} />
|
|
55
|
+
</DetailPage>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### FormPage
|
|
59
|
+
|
|
60
|
+
Form view with validation.
|
|
61
|
+
|
|
62
|
+
```astro
|
|
63
|
+
---
|
|
64
|
+
import { FormPage } from '@htlkg/astro/patterns/admin';
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
<FormPage
|
|
68
|
+
title="Create Brand"
|
|
69
|
+
schema={brandSchema}
|
|
70
|
+
onSubmit={handleSubmit}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Brand Patterns
|
|
75
|
+
|
|
76
|
+
### ConfigPage
|
|
77
|
+
|
|
78
|
+
Configuration page for brand settings.
|
|
79
|
+
|
|
80
|
+
```astro
|
|
81
|
+
---
|
|
82
|
+
import { ConfigPage } from '@htlkg/astro/patterns/brand';
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<ConfigPage
|
|
86
|
+
brand={brand}
|
|
87
|
+
product={product}
|
|
88
|
+
config={productConfig}
|
|
89
|
+
/>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### PortalPage
|
|
93
|
+
|
|
94
|
+
Public portal page for brand.
|
|
95
|
+
|
|
96
|
+
```astro
|
|
97
|
+
---
|
|
98
|
+
import { PortalPage } from '@htlkg/astro/patterns/brand';
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
<PortalPage brand={brand}>
|
|
102
|
+
<WifiPortal />
|
|
103
|
+
</PortalPage>
|
|
104
|
+
```
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Building Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for building GraphQL filters from parsed URL parameters,
|
|
5
|
+
* and for applying client-side filters to data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FilterFieldConfig, ListParams } from "./params";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GraphQL filter operators
|
|
12
|
+
*/
|
|
13
|
+
export type GraphQLOperator = "eq" | "ne" | "le" | "lt" | "ge" | "gt" | "contains" | "notContains" | "beginsWith" | "between";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GraphQL filter condition
|
|
17
|
+
*/
|
|
18
|
+
export interface GraphQLFilterCondition {
|
|
19
|
+
[key: string]: {
|
|
20
|
+
[operator in GraphQLOperator]?: any;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GraphQL combined filter
|
|
26
|
+
*/
|
|
27
|
+
export interface GraphQLFilter {
|
|
28
|
+
and?: GraphQLFilterCondition[];
|
|
29
|
+
or?: GraphQLFilterCondition[];
|
|
30
|
+
not?: GraphQLFilter;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build GraphQL filter from parsed URL parameters
|
|
36
|
+
*
|
|
37
|
+
* Only includes fields marked with `graphql: true` in the field config.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const filter = buildGraphQLFilter(
|
|
42
|
+
* { status: 'active', name: 'test' },
|
|
43
|
+
* [
|
|
44
|
+
* { key: 'status', type: 'select', graphql: true },
|
|
45
|
+
* { key: 'name', type: 'text', graphql: true },
|
|
46
|
+
* ]
|
|
47
|
+
* );
|
|
48
|
+
* // Returns: { and: [{ status: { eq: 'active' } }, { name: { contains: 'test' } }] }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function buildGraphQLFilter(
|
|
52
|
+
filters: Record<string, any>,
|
|
53
|
+
filterableFields: FilterFieldConfig[],
|
|
54
|
+
options: { searchFields?: string[]; search?: string } = {}
|
|
55
|
+
): GraphQLFilter | undefined {
|
|
56
|
+
const conditions: GraphQLFilterCondition[] = [];
|
|
57
|
+
|
|
58
|
+
// Build field filters
|
|
59
|
+
for (const field of filterableFields) {
|
|
60
|
+
if (!field.graphql) continue;
|
|
61
|
+
|
|
62
|
+
const value = filters[field.key];
|
|
63
|
+
if (value === undefined || value === null || value === "") continue;
|
|
64
|
+
|
|
65
|
+
const condition = buildFieldCondition(field, value);
|
|
66
|
+
if (condition) {
|
|
67
|
+
conditions.push(condition);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build search filter (applies to multiple fields with OR)
|
|
72
|
+
if (options.search && options.searchFields && options.searchFields.length > 0) {
|
|
73
|
+
const searchConditions = options.searchFields.map((fieldKey) => ({
|
|
74
|
+
[fieldKey]: { contains: options.search },
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
if (searchConditions.length === 1) {
|
|
78
|
+
conditions.push(searchConditions[0]);
|
|
79
|
+
} else if (searchConditions.length > 1) {
|
|
80
|
+
// Multiple search fields use OR
|
|
81
|
+
conditions.push({ or: searchConditions } as unknown as GraphQLFilterCondition);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return combined filter
|
|
86
|
+
if (conditions.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (conditions.length === 1) {
|
|
91
|
+
return conditions[0] as unknown as GraphQLFilter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { and: conditions };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a single field condition
|
|
99
|
+
*/
|
|
100
|
+
function buildFieldCondition(field: FilterFieldConfig, value: any): GraphQLFilterCondition | undefined {
|
|
101
|
+
// Use custom operator if provided
|
|
102
|
+
const operator = field.graphqlOperator;
|
|
103
|
+
|
|
104
|
+
if (operator) {
|
|
105
|
+
// Use the custom operator specified in config
|
|
106
|
+
return { [field.key]: { [operator]: value } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Default behavior based on field type
|
|
110
|
+
switch (field.type) {
|
|
111
|
+
case "text":
|
|
112
|
+
// Text fields use contains by default (case-sensitive in DynamoDB)
|
|
113
|
+
return { [field.key]: { contains: String(value) } };
|
|
114
|
+
|
|
115
|
+
case "number":
|
|
116
|
+
// Number fields use exact match
|
|
117
|
+
return { [field.key]: { eq: Number(value) } };
|
|
118
|
+
|
|
119
|
+
case "boolean":
|
|
120
|
+
// Boolean fields use exact match
|
|
121
|
+
return { [field.key]: { eq: Boolean(value) } };
|
|
122
|
+
|
|
123
|
+
case "date":
|
|
124
|
+
// Date fields - could be exact or range
|
|
125
|
+
if (value instanceof Date) {
|
|
126
|
+
return { [field.key]: { eq: value.toISOString() } };
|
|
127
|
+
}
|
|
128
|
+
return { [field.key]: { eq: value } };
|
|
129
|
+
|
|
130
|
+
case "select":
|
|
131
|
+
// Select fields use exact match
|
|
132
|
+
return { [field.key]: { eq: value } };
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
return { [field.key]: { eq: value } };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply client-side filters to data array
|
|
141
|
+
*
|
|
142
|
+
* Only applies filters for fields NOT marked with `graphql: true`.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const filtered = applyClientFilters(
|
|
147
|
+
* items,
|
|
148
|
+
* { brandCount: 5 },
|
|
149
|
+
* [{ key: 'brandCount', type: 'number', graphql: false }]
|
|
150
|
+
* );
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export function applyClientFilters<T extends Record<string, any>>(
|
|
154
|
+
items: T[],
|
|
155
|
+
filters: Record<string, any>,
|
|
156
|
+
filterableFields: FilterFieldConfig[],
|
|
157
|
+
options: { search?: string; searchFields?: string[] } = {}
|
|
158
|
+
): T[] {
|
|
159
|
+
let result = items;
|
|
160
|
+
|
|
161
|
+
// Apply field filters (only non-GraphQL fields)
|
|
162
|
+
for (const field of filterableFields) {
|
|
163
|
+
if (field.graphql) continue; // Skip GraphQL fields
|
|
164
|
+
|
|
165
|
+
const value = filters[field.key];
|
|
166
|
+
if (value === undefined || value === null || value === "") continue;
|
|
167
|
+
|
|
168
|
+
result = result.filter((item) => matchesFieldFilter(item, field, value));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Apply search filter (only if not applied via GraphQL)
|
|
172
|
+
if (options.search && options.searchFields) {
|
|
173
|
+
const searchLower = options.search.toLowerCase();
|
|
174
|
+
result = result.filter((item) =>
|
|
175
|
+
options.searchFields!.some((fieldKey) => {
|
|
176
|
+
const fieldValue = item[fieldKey];
|
|
177
|
+
if (fieldValue === null || fieldValue === undefined) return false;
|
|
178
|
+
return String(fieldValue).toLowerCase().includes(searchLower);
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if an item matches a field filter
|
|
188
|
+
*/
|
|
189
|
+
function matchesFieldFilter<T extends Record<string, any>>(item: T, field: FilterFieldConfig, filterValue: any): boolean {
|
|
190
|
+
const itemValue = item[field.key];
|
|
191
|
+
|
|
192
|
+
switch (field.type) {
|
|
193
|
+
case "text":
|
|
194
|
+
if (itemValue === null || itemValue === undefined) return false;
|
|
195
|
+
return String(itemValue).toLowerCase().includes(String(filterValue).toLowerCase());
|
|
196
|
+
|
|
197
|
+
case "number":
|
|
198
|
+
return Number(itemValue) === Number(filterValue);
|
|
199
|
+
|
|
200
|
+
case "boolean":
|
|
201
|
+
return Boolean(itemValue) === Boolean(filterValue);
|
|
202
|
+
|
|
203
|
+
case "date":
|
|
204
|
+
if (!itemValue) return false;
|
|
205
|
+
const itemDate = itemValue instanceof Date ? itemValue : new Date(itemValue);
|
|
206
|
+
const filterDate = filterValue instanceof Date ? filterValue : new Date(filterValue);
|
|
207
|
+
// Compare dates (ignoring time)
|
|
208
|
+
return itemDate.toDateString() === filterDate.toDateString();
|
|
209
|
+
|
|
210
|
+
case "select":
|
|
211
|
+
return itemValue === filterValue;
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return itemValue === filterValue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Sort items by a key
|
|
220
|
+
*/
|
|
221
|
+
export function sortItems<T extends Record<string, any>>(items: T[], sortKey: string, sortOrder: "asc" | "desc"): T[] {
|
|
222
|
+
if (!sortKey) return items;
|
|
223
|
+
|
|
224
|
+
const multiplier = sortOrder === "asc" ? 1 : -1;
|
|
225
|
+
|
|
226
|
+
return [...items].sort((a, b) => {
|
|
227
|
+
const aVal = a[sortKey];
|
|
228
|
+
const bVal = b[sortKey];
|
|
229
|
+
|
|
230
|
+
// Handle null/undefined
|
|
231
|
+
if (aVal === null || aVal === undefined) return 1;
|
|
232
|
+
if (bVal === null || bVal === undefined) return -1;
|
|
233
|
+
if (aVal === bVal) return 0;
|
|
234
|
+
|
|
235
|
+
// Handle different types
|
|
236
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
237
|
+
return aVal.localeCompare(bVal) * multiplier;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
241
|
+
return (aVal - bVal) * multiplier;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (aVal instanceof Date && bVal instanceof Date) {
|
|
245
|
+
return (aVal.getTime() - bVal.getTime()) * multiplier;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback to string comparison
|
|
249
|
+
return String(aVal).localeCompare(String(bVal)) * multiplier;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Paginate items
|
|
255
|
+
*/
|
|
256
|
+
export function paginateItems<T>(
|
|
257
|
+
items: T[],
|
|
258
|
+
page: number,
|
|
259
|
+
pageSize: number
|
|
260
|
+
): {
|
|
261
|
+
paginatedItems: T[];
|
|
262
|
+
totalItems: number;
|
|
263
|
+
totalPages: number;
|
|
264
|
+
currentPage: number;
|
|
265
|
+
pageSize: number;
|
|
266
|
+
} {
|
|
267
|
+
const totalItems = items.length;
|
|
268
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
269
|
+
const currentPage = Math.min(Math.max(1, page), totalPages || 1);
|
|
270
|
+
|
|
271
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
272
|
+
const endIndex = startIndex + pageSize;
|
|
273
|
+
const paginatedItems = items.slice(startIndex, endIndex);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
paginatedItems,
|
|
277
|
+
totalItems,
|
|
278
|
+
totalPages,
|
|
279
|
+
currentPage,
|
|
280
|
+
pageSize,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Process list data with filters, sorting, and pagination
|
|
286
|
+
*
|
|
287
|
+
* Convenience function that combines all processing steps.
|
|
288
|
+
*/
|
|
289
|
+
export function processListData<T extends Record<string, any>>(
|
|
290
|
+
items: T[],
|
|
291
|
+
params: ListParams,
|
|
292
|
+
filterableFields: FilterFieldConfig[],
|
|
293
|
+
options: { searchFields?: string[] } = {}
|
|
294
|
+
): {
|
|
295
|
+
items: T[];
|
|
296
|
+
totalItems: number;
|
|
297
|
+
totalPages: number;
|
|
298
|
+
currentPage: number;
|
|
299
|
+
pageSize: number;
|
|
300
|
+
} {
|
|
301
|
+
// Apply client-side filters
|
|
302
|
+
let result = applyClientFilters(items, params.filters, filterableFields, {
|
|
303
|
+
search: params.search,
|
|
304
|
+
searchFields: options.searchFields,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Sort
|
|
308
|
+
result = sortItems(result, params.sortKey, params.sortOrder);
|
|
309
|
+
|
|
310
|
+
// Paginate
|
|
311
|
+
const paginated = paginateItems(result, params.page, params.pageSize);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
items: paginated.paginatedItems,
|
|
315
|
+
totalItems: paginated.totalItems,
|
|
316
|
+
totalPages: paginated.totalPages,
|
|
317
|
+
currentPage: paginated.currentPage,
|
|
318
|
+
pageSize: paginated.pageSize,
|
|
319
|
+
};
|
|
320
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @htlkg/
|
|
3
|
-
*
|
|
2
|
+
* @htlkg/astro - Utilities
|
|
3
|
+
*
|
|
4
4
|
* Helper functions for Astro pages.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from './ssr';
|
|
8
8
|
export * from './static';
|
|
9
9
|
export * from './hydration';
|
|
10
|
+
|
|
11
|
+
// URL parameter parsing and building
|
|
12
|
+
export * from './params';
|
|
13
|
+
|
|
14
|
+
// Filter building and processing
|
|
15
|
+
export * from './filters';
|