@goplusvn/core 0.1.0 → 0.1.1
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/package.json +2 -1
- package/src/assets/erp_wallpaper.png +0 -0
- package/src/assets/goeat_logo.png +0 -0
- package/src/audit/audit-manager.ts +139 -0
- package/src/audit/index.ts +11 -0
- package/src/audit/memory-audit-logger.ts +86 -0
- package/src/audit/types.ts +50 -0
- package/src/auth/auth-service.ts +97 -0
- package/src/auth/index.ts +266 -0
- package/src/code-generation/index.ts +69 -0
- package/src/configs/auth-routes.ts +17 -0
- package/src/configs/crud.ts +136 -0
- package/src/configs/data/navigations.ts +781 -0
- package/src/configs/data/oauth-links.ts +10 -0
- package/src/configs/entities/material-categories.config.ts +125 -0
- package/src/configs/i18n.ts +12 -0
- package/src/configs/index.ts +26 -0
- package/src/configs/status.ts +25 -0
- package/src/configs/themes.ts +100 -0
- package/src/crud/components/crud-bulk-actions.tsx +91 -0
- package/src/crud/components/crud-card-view.tsx +241 -0
- package/src/crud/components/crud-context.tsx +122 -0
- package/src/crud/components/crud-delete-dialog.tsx +145 -0
- package/src/crud/components/crud-dialog.tsx +406 -0
- package/src/crud/components/crud-empty-state.tsx +104 -0
- package/src/crud/components/crud-export-button.tsx +170 -0
- package/src/crud/components/crud-field-renderer.tsx +653 -0
- package/src/crud/components/crud-filter-chips.tsx +102 -0
- package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
- package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
- package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
- package/src/crud/components/crud-filters/index.tsx +76 -0
- package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
- package/src/crud/components/crud-filters/select-filter.tsx +141 -0
- package/src/crud/components/crud-filters/text-filter.tsx +86 -0
- package/src/crud/components/crud-form.tsx +642 -0
- package/src/crud/components/crud-import-dialog.tsx +440 -0
- package/src/crud/components/crud-infinite-scroll.tsx +116 -0
- package/src/crud/components/crud-page.tsx +1017 -0
- package/src/crud/components/crud-provider.tsx +277 -0
- package/src/crud/components/crud-row-actions.tsx +189 -0
- package/src/crud/components/crud-search.tsx +82 -0
- package/src/crud/components/crud-sheet.tsx +336 -0
- package/src/crud/components/crud-table-skeleton.tsx +26 -0
- package/src/crud/components/crud-table-toolbar.tsx +91 -0
- package/src/crud/components/crud-table.tsx +352 -0
- package/src/crud/components/crud-virtual-table.tsx +55 -0
- package/src/crud/components/index.tsx +20 -0
- package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
- package/src/crud/crud-filters/datetime-filter.tsx +82 -0
- package/src/crud/crud-filters/filter-builder.tsx +64 -0
- package/src/crud/crud-filters/index.tsx +78 -0
- package/src/crud/crud-filters/radio-filter.tsx +79 -0
- package/src/crud/crud-filters/select-filter.tsx +148 -0
- package/src/crud/crud-filters/text-filter.tsx +81 -0
- package/src/crud/index.ts +43 -0
- package/src/crud/lib/crud-service.test.ts +334 -0
- package/src/crud/lib/crud-service.ts +358 -0
- package/src/crud/lib/crud-utils.test.ts +354 -0
- package/src/crud/lib/crud-utils.ts +299 -0
- package/src/crud/lib/crud-validator.ts +247 -0
- package/src/crud/lib/data-loader.ts +234 -0
- package/src/crud/lib/field-calculator.ts +241 -0
- package/src/crud/lib/field-formatter.ts +240 -0
- package/src/crud/lib/import-export-service.test.ts +290 -0
- package/src/crud/lib/import-export-service.ts +352 -0
- package/src/crud/lib/import-server-utils.ts +109 -0
- package/src/crud/lib/lazy-loader.ts +241 -0
- package/src/crud/lib/parse-filters.ts +85 -0
- package/src/crud/lib/permissions.ts +52 -0
- package/src/crud/lib/serialize-config.ts +60 -0
- package/src/crud/lib/stream-loader.ts +145 -0
- package/src/crud/lib/translate-config.ts +335 -0
- package/src/crud/lib/types.ts +11 -0
- package/src/crud/pages/entity-crud-page.tsx +144 -0
- package/src/crud/server.ts +8 -0
- package/src/home/constants.tsx +142 -0
- package/src/home/feature-showcase.tsx +171 -0
- package/src/home/home-page.tsx +191 -0
- package/src/home/hooks/index.ts +1 -0
- package/src/home/hooks/useWidgetPreferences.ts +167 -0
- package/src/home/index.ts +33 -0
- package/src/home/quick-access-dialog.tsx +271 -0
- package/src/home/quick-access-menu.tsx +267 -0
- package/src/home/types.ts +140 -0
- package/src/home/welcome-card.tsx +92 -0
- package/src/home/widget-container.tsx +258 -0
- package/src/home/widgets/base-widget.tsx +200 -0
- package/src/home/widgets/customers-widget.tsx +74 -0
- package/src/home/widgets/index.ts +6 -0
- package/src/home/widgets/orders-widget.tsx +87 -0
- package/src/home/widgets/revenue-widget.tsx +71 -0
- package/src/home/widgets/stock-widget.tsx +109 -0
- package/src/hooks/index.tsx +598 -0
- package/src/hooks/use-tenant.test.tsx +30 -0
- package/src/hooks/use-tenant.ts +5 -0
- package/src/index.ts +17 -0
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
- package/src/infrastructure/api-service.ts +317 -0
- package/src/infrastructure/cache/cache-manager.ts +107 -0
- package/src/infrastructure/cache/cache.ts +120 -0
- package/src/infrastructure/cache/index.ts +8 -0
- package/src/infrastructure/cache/types.ts +48 -0
- package/src/infrastructure/cron/cron-manager.ts +239 -0
- package/src/infrastructure/cron/index.ts +6 -0
- package/src/infrastructure/cron/types.ts +41 -0
- package/src/infrastructure/event-bus/event-bus.ts +145 -0
- package/src/infrastructure/event-bus/index.ts +2 -0
- package/src/infrastructure/event-bus/types.ts +22 -0
- package/src/infrastructure/index.ts +32 -0
- package/src/infrastructure/lock/decorators.ts +67 -0
- package/src/infrastructure/lock/index.ts +2 -0
- package/src/infrastructure/lock/lock-manager.ts +33 -0
- package/src/infrastructure/logger/index.ts +2 -0
- package/src/infrastructure/logger/logger.ts +96 -0
- package/src/infrastructure/logger/types.ts +25 -0
- package/src/layout/index.tsx +185 -0
- package/src/navigation/index.ts +91 -0
- package/src/notification/index.ts +14 -0
- package/src/notification/notification-service.ts +120 -0
- package/src/notification/storage/in-memory.ts +56 -0
- package/src/notification/storage/index.ts +1 -0
- package/src/notification/types.ts +51 -0
- package/src/organization/branch-service.ts +299 -0
- package/src/organization/branches.config.ts +154 -0
- package/src/organization/index.ts +5 -0
- package/src/plugin/apps-registry.ts +97 -0
- package/src/plugin/index.ts +5 -0
- package/src/plugin/types.ts +41 -0
- package/src/providers/index.tsx +109 -0
- package/src/providers/tenant-provider.tsx +45 -0
- package/src/rbac/components/roles/role-card.tsx +158 -0
- package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
- package/src/rbac/components/roles/role-toolbar.tsx +123 -0
- package/src/rbac/hooks/use-role-operations.ts +159 -0
- package/src/rbac/hooks/use-roles-data.ts +59 -0
- package/src/rbac/index.ts +297 -0
- package/src/rbac/lib/permission-helpers.ts +63 -0
- package/src/rbac/pages/action-list-page.tsx +25 -0
- package/src/rbac/pages/resource-list-page.tsx +25 -0
- package/src/rbac/pages/role-list-page.tsx +378 -0
- package/src/rbac/permission-service.ts +140 -0
- package/src/rbac/permissions.ts +135 -0
- package/src/rbac/resource-service.ts +115 -0
- package/src/rbac/resource-validator.ts +119 -0
- package/src/rbac/role-service.ts +165 -0
- package/src/rbac/server.ts +16 -0
- package/src/rbac/types.ts +38 -0
- package/src/schemas/action.schema.ts +66 -0
- package/src/schemas/branch.schema.ts +52 -0
- package/src/schemas/coming-soon-schema.ts +9 -0
- package/src/schemas/company.schema.ts +44 -0
- package/src/schemas/forgot-passward-schema.ts +9 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/material-category.schema.ts +43 -0
- package/src/schemas/material-pricing.schema.ts +74 -0
- package/src/schemas/material.schema.ts +76 -0
- package/src/schemas/materials.ts +52 -0
- package/src/schemas/new-passward-schema.ts +15 -0
- package/src/schemas/partner-company.schema.ts +149 -0
- package/src/schemas/register-schema.ts +36 -0
- package/src/schemas/resource.schema.ts +133 -0
- package/src/schemas/role.schema.ts +11 -0
- package/src/schemas/sign-in-schema.ts +24 -0
- package/src/schemas/supplier-pricing.schema.ts +15 -0
- package/src/schemas/supplier.schema.ts +120 -0
- package/src/schemas/system-category-group.schema.ts +67 -0
- package/src/schemas/system-category.schema.ts +77 -0
- package/src/schemas/system-config.schema.ts +118 -0
- package/src/schemas/uom.schema.ts +75 -0
- package/src/schemas/user-supplier.schema.ts +179 -0
- package/src/schemas/user.schema.ts +18 -0
- package/src/schemas/verify-email-schema.ts +9 -0
- package/src/schemas/warehouse.schema.ts +49 -0
- package/src/system/components/categories/category-list.tsx +529 -0
- package/src/system/components/categories/category-manager.tsx +89 -0
- package/src/system/components/categories/group-sidebar.tsx +308 -0
- package/src/system/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/components/settings/setting-field.tsx +291 -0
- package/src/system/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/components/settings/settings-groups.ts +80 -0
- package/src/system/components/settings/settings-search.tsx +71 -0
- package/src/system/components/settings/settings-section.tsx +74 -0
- package/src/system/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/constants.ts +3 -0
- package/src/system/index.ts +150 -0
- package/src/system/job-manager.ts +176 -0
- package/src/system/pages/components/categories/category-list.tsx +537 -0
- package/src/system/pages/components/categories/category-manager.tsx +90 -0
- package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
- package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
- package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/pages/components/settings/setting-field.tsx +292 -0
- package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/pages/components/settings/settings-groups.ts +87 -0
- package/src/system/pages/components/settings/settings-page.tsx +372 -0
- package/src/system/pages/components/settings/settings-search.tsx +71 -0
- package/src/system/pages/components/settings/settings-section.tsx +74 -0
- package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/pages/components/settings/system-settings.tsx +244 -0
- package/src/system/pages/system-category-page.tsx +15 -0
- package/src/system/pages/system-settings-page.tsx +380 -0
- package/src/system/schemas/system-category-group.schema.ts +46 -0
- package/src/system/schemas/system-category.schema.ts +56 -0
- package/src/system/services/settings-service.ts +127 -0
- package/src/system/services/system-category-service.ts +63 -0
- package/src/system/types.ts +45 -0
- package/src/types/index.ts +703 -0
- package/src/ui/auth/auth-layout.tsx +135 -0
- package/src/ui/auth/forgot-password-form.tsx +98 -0
- package/src/ui/auth/index.tsx +7 -0
- package/src/ui/auth/new-password-form.tsx +107 -0
- package/src/ui/auth/oauth-links.tsx +30 -0
- package/src/ui/auth/register-form.tsx +202 -0
- package/src/ui/auth/sign-in-form.tsx +238 -0
- package/src/ui/auth/verify-email-form.tsx +104 -0
- package/src/ui/crud/index.tsx +10 -0
- package/src/ui/data-display/accordion.tsx +65 -0
- package/src/ui/data-display/aspect-ratio.tsx +11 -0
- package/src/ui/data-display/avatar.tsx +163 -0
- package/src/ui/data-display/bento-grid.tsx +77 -0
- package/src/ui/data-display/carousel.tsx +249 -0
- package/src/ui/data-display/chart.tsx +363 -0
- package/src/ui/data-display/code-block-highlight.tsx +54 -0
- package/src/ui/data-display/collapsible.tsx +42 -0
- package/src/ui/data-display/compact-stat-bar.tsx +149 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
- package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
- package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
- package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
- package/src/ui/data-display/data-table/data-table.tsx +597 -0
- package/src/ui/data-display/data-table/index.ts +44 -0
- package/src/ui/data-display/data-table-column-header.tsx +75 -0
- package/src/ui/data-display/data-table-pagination.tsx +130 -0
- package/src/ui/data-display/data-table-view-options.tsx +59 -0
- package/src/ui/data-display/formatted-number-input.tsx +210 -0
- package/src/ui/data-display/highlight.tsx +20 -0
- package/src/ui/data-display/hover-card.tsx +48 -0
- package/src/ui/data-display/index.tsx +50 -0
- package/src/ui/data-display/iphone-15-pro.tsx +114 -0
- package/src/ui/data-display/kanban/index.ts +4 -0
- package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
- package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
- package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
- package/src/ui/data-display/kanban/kanban-types.ts +21 -0
- package/src/ui/data-display/kpi-card.tsx +68 -0
- package/src/ui/data-display/media-grid.tsx +110 -0
- package/src/ui/data-display/safari.tsx +175 -0
- package/src/ui/data-display/show-more-text.tsx +55 -0
- package/src/ui/data-display/tabs.tsx +68 -0
- package/src/ui/data-display/timeline.tsx +256 -0
- package/src/ui/feedback/alert.tsx +60 -0
- package/src/ui/feedback/context-menu.tsx +245 -0
- package/src/ui/feedback/drawer.tsx +132 -0
- package/src/ui/feedback/error-dialog.tsx +273 -0
- package/src/ui/feedback/index.tsx +183 -0
- package/src/ui/feedback/progress.tsx +32 -0
- package/src/ui/feedback/sheet.tsx +148 -0
- package/src/ui/feedback/sonner.tsx +36 -0
- package/src/ui/forms/command.tsx +157 -0
- package/src/ui/forms/date-picker.tsx +73 -0
- package/src/ui/forms/date-range-picker.tsx +76 -0
- package/src/ui/forms/date-time-picker.tsx +109 -0
- package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
- package/src/ui/forms/editor/index.tsx +130 -0
- package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
- package/src/ui/forms/emoji-picker.tsx +109 -0
- package/src/ui/forms/file-dropzone.tsx +169 -0
- package/src/ui/forms/file-thumbnail.tsx +29 -0
- package/src/ui/forms/index.tsx +201 -0
- package/src/ui/forms/input-file.tsx +99 -0
- package/src/ui/forms/input-group.tsx +46 -0
- package/src/ui/forms/input-otp.tsx +81 -0
- package/src/ui/forms/input-phone.tsx +172 -0
- package/src/ui/forms/input-spin.tsx +116 -0
- package/src/ui/forms/input-tags.tsx +219 -0
- package/src/ui/forms/input-time.tsx +42 -0
- package/src/ui/forms/multi-select.tsx +629 -0
- package/src/ui/forms/multiple-date-picker.tsx +74 -0
- package/src/ui/forms/radio-group.tsx +42 -0
- package/src/ui/forms/rating.tsx +158 -0
- package/src/ui/forms/time-picker.tsx +57 -0
- package/src/ui/index.tsx +17 -0
- package/src/ui/layout/animated-list.tsx +77 -0
- package/src/ui/layout/animated-sidebar.tsx +294 -0
- package/src/ui/layout/command-menu.tsx +355 -0
- package/src/ui/layout/customizer.tsx +324 -0
- package/src/ui/layout/footer.tsx +43 -0
- package/src/ui/layout/full-screen-toggle.tsx +52 -0
- package/src/ui/layout/header-breadcrumb.tsx +77 -0
- package/src/ui/layout/horizontal-layout-header.tsx +83 -0
- package/src/ui/layout/horizontal-layout.tsx +50 -0
- package/src/ui/layout/index.tsx +25 -0
- package/src/ui/layout/language-dropdown.tsx +103 -0
- package/src/ui/layout/logo.tsx +63 -0
- package/src/ui/layout/main-layout.tsx +57 -0
- package/src/ui/layout/mode-dropdown.tsx +58 -0
- package/src/ui/layout/notification-dropdown.tsx +127 -0
- package/src/ui/layout/page-tabs.tsx +306 -0
- package/src/ui/layout/route-cache.tsx +214 -0
- package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
- package/src/ui/layout/sidebar.tsx +279 -0
- package/src/ui/layout/tab-content-cache.tsx +201 -0
- package/src/ui/layout/tab-navigation-provider.tsx +536 -0
- package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
- package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
- package/src/ui/layout/user-dropdown.tsx +188 -0
- package/src/ui/layout/vertical-layout-header.tsx +65 -0
- package/src/ui/layout/vertical-layout.tsx +47 -0
- package/src/ui/management/audit-log-page.tsx +209 -0
- package/src/ui/management/cache-management.tsx +349 -0
- package/src/ui/management/index.ts +3 -0
- package/src/ui/management/job-management.tsx +308 -0
- package/src/ui/pages/not-found.tsx +30 -0
- package/src/ui/primitives/badge.tsx +66 -0
- package/src/ui/primitives/breadcrumb.tsx +103 -0
- package/src/ui/primitives/button.tsx +129 -0
- package/src/ui/primitives/calendar.tsx +74 -0
- package/src/ui/primitives/card.tsx +86 -0
- package/src/ui/primitives/checkbox.tsx +31 -0
- package/src/ui/primitives/client.ts +30 -0
- package/src/ui/primitives/combobox.tsx +290 -0
- package/src/ui/primitives/dialog.tsx +121 -0
- package/src/ui/primitives/dropdown-menu.tsx +239 -0
- package/src/ui/primitives/dynamic-icon.tsx +24 -0
- package/src/ui/primitives/index.tsx +134 -0
- package/src/ui/primitives/input-number.tsx +131 -0
- package/src/ui/primitives/input.tsx +22 -0
- package/src/ui/primitives/keyboard.tsx +23 -0
- package/src/ui/primitives/label.tsx +24 -0
- package/src/ui/primitives/menubar.tsx +262 -0
- package/src/ui/primitives/navigation-menu.tsx +157 -0
- package/src/ui/primitives/pagination.tsx +118 -0
- package/src/ui/primitives/popover.tsx +56 -0
- package/src/ui/primitives/prefetch-link.tsx +60 -0
- package/src/ui/primitives/resizable.tsx +59 -0
- package/src/ui/primitives/scroll-area.tsx +63 -0
- package/src/ui/primitives/select.tsx +172 -0
- package/src/ui/primitives/separator.tsx +51 -0
- package/src/ui/primitives/sidebar.tsx +844 -0
- package/src/ui/primitives/slider.tsx +27 -0
- package/src/ui/primitives/status-badge.tsx +47 -0
- package/src/ui/primitives/sticky-layout.tsx +50 -0
- package/src/ui/primitives/switch.tsx +29 -0
- package/src/ui/primitives/table.tsx +116 -0
- package/src/ui/primitives/tabs.tsx +55 -0
- package/src/ui/primitives/toggle-group.tsx +70 -0
- package/src/ui/primitives/toggle.tsx +47 -0
- package/src/ui/primitives/tooltip.tsx +59 -0
- package/src/user/components/dangerous-zone.tsx +34 -0
- package/src/user/components/delete-account-form.tsx +40 -0
- package/src/user/components/index.ts +4 -0
- package/src/user/components/profile-info-form.tsx +390 -0
- package/src/user/components/profile-info.tsx +32 -0
- package/src/user/components/unified-profile-dialog.tsx +1019 -0
- package/src/user/components/user-stats.tsx +27 -0
- package/src/user/components/user-toolbar.tsx +137 -0
- package/src/user/components/users-card-view.tsx +253 -0
- package/src/user/index.ts +11 -0
- package/src/user/pages/user-list-page.tsx +234 -0
- package/src/user/pages/users-client-page.tsx +385 -0
- package/src/user/profile-page.tsx +19 -0
- package/src/user/schemas.ts +68 -0
- package/src/user/types.ts +34 -0
- package/src/user/user-service.ts +538 -0
- package/src/utils/index.ts +906 -0
- package/src/workflow/activity-timeline.tsx +412 -0
- package/src/workflow/approval-workflow.tsx +31 -0
- package/src/workflow/index.ts +2 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { EntityConfig, FieldConfig } from "../../types";
|
|
4
|
+
|
|
5
|
+
import { getFieldDefaultValue } from "./crud-utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build Zod schema from FieldConfig
|
|
9
|
+
*/
|
|
10
|
+
export function buildSchemaFromConfig(
|
|
11
|
+
fields: FieldConfig[],
|
|
12
|
+
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
13
|
+
const schemaObject: Record<string, z.ZodTypeAny> = {};
|
|
14
|
+
|
|
15
|
+
fields.forEach((field) => {
|
|
16
|
+
if (field.hideInForm) {
|
|
17
|
+
return; // Skip hidden fields
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let fieldSchema: z.ZodTypeAny;
|
|
21
|
+
|
|
22
|
+
// Base type schema
|
|
23
|
+
switch (field.type) {
|
|
24
|
+
case "text":
|
|
25
|
+
case "email":
|
|
26
|
+
case "url":
|
|
27
|
+
fieldSchema = z.string();
|
|
28
|
+
break;
|
|
29
|
+
case "number":
|
|
30
|
+
fieldSchema = z.preprocess(
|
|
31
|
+
(val) =>
|
|
32
|
+
val === "" || val === null || val === undefined
|
|
33
|
+
? undefined
|
|
34
|
+
: Number(val),
|
|
35
|
+
z.number(),
|
|
36
|
+
);
|
|
37
|
+
break;
|
|
38
|
+
case "integer":
|
|
39
|
+
fieldSchema = z.preprocess(
|
|
40
|
+
(val) =>
|
|
41
|
+
val === "" || val === null || val === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: Number(val),
|
|
44
|
+
z.number().int(),
|
|
45
|
+
);
|
|
46
|
+
break;
|
|
47
|
+
case "textarea":
|
|
48
|
+
fieldSchema = z.string();
|
|
49
|
+
break;
|
|
50
|
+
case "boolean":
|
|
51
|
+
fieldSchema = z.boolean();
|
|
52
|
+
break;
|
|
53
|
+
case "switch":
|
|
54
|
+
// Switch can be boolean or string (active/inactive)
|
|
55
|
+
// We default to string validation here as it's primarily used for status
|
|
56
|
+
fieldSchema = z.string().or(z.boolean());
|
|
57
|
+
break;
|
|
58
|
+
case "date":
|
|
59
|
+
case "datetime":
|
|
60
|
+
case "time":
|
|
61
|
+
fieldSchema = z.string().datetime().or(z.date()).or(z.null());
|
|
62
|
+
break;
|
|
63
|
+
case "select":
|
|
64
|
+
fieldSchema = z.string();
|
|
65
|
+
break;
|
|
66
|
+
case "multiselect":
|
|
67
|
+
fieldSchema = z.array(z.string());
|
|
68
|
+
break;
|
|
69
|
+
case "file":
|
|
70
|
+
case "image":
|
|
71
|
+
fieldSchema = z.instanceof(File).or(z.string()).or(z.null());
|
|
72
|
+
break;
|
|
73
|
+
case "json":
|
|
74
|
+
fieldSchema = z.unknown();
|
|
75
|
+
break;
|
|
76
|
+
case "custom":
|
|
77
|
+
// For custom fields, use unknown or provided validation
|
|
78
|
+
fieldSchema = field.validation || z.unknown();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
fieldSchema = z.string();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply required validation
|
|
85
|
+
if (field.required) {
|
|
86
|
+
if (field.type === "multiselect") {
|
|
87
|
+
// Array: use min length
|
|
88
|
+
fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(
|
|
89
|
+
1,
|
|
90
|
+
`${field.label} is required`,
|
|
91
|
+
);
|
|
92
|
+
} else if (field.type === "boolean") {
|
|
93
|
+
// Boolean fields are always required (true/false)
|
|
94
|
+
// No need to add required message
|
|
95
|
+
} else if (
|
|
96
|
+
field.type === "text" ||
|
|
97
|
+
field.type === "email" ||
|
|
98
|
+
field.type === "url" ||
|
|
99
|
+
field.type === "textarea" ||
|
|
100
|
+
field.type === "select"
|
|
101
|
+
) {
|
|
102
|
+
// String: use min length
|
|
103
|
+
fieldSchema = (fieldSchema as z.ZodString).min(
|
|
104
|
+
1,
|
|
105
|
+
`${field.label} is required`,
|
|
106
|
+
);
|
|
107
|
+
} else if (field.type === "number" || field.type === "integer") {
|
|
108
|
+
// Number: Zod numbers are required by default if not optional
|
|
109
|
+
// No additional validation needed
|
|
110
|
+
} else if (
|
|
111
|
+
field.type === "date" ||
|
|
112
|
+
field.type === "datetime" ||
|
|
113
|
+
field.type === "time"
|
|
114
|
+
) {
|
|
115
|
+
// Date/datetime: use refine to ensure it's not null/undefined
|
|
116
|
+
fieldSchema = (fieldSchema as z.ZodTypeAny).refine(
|
|
117
|
+
(val) => val !== null && val !== undefined,
|
|
118
|
+
{
|
|
119
|
+
message: `${field.label} is required`,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
// Other types (file, image, json, custom): use refine
|
|
124
|
+
fieldSchema = (fieldSchema as z.ZodTypeAny).refine(
|
|
125
|
+
(val) => val !== null && val !== undefined,
|
|
126
|
+
{
|
|
127
|
+
message: `${field.label} is required`,
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Make optional - allow null and undefined
|
|
133
|
+
if (field.type === "multiselect") {
|
|
134
|
+
fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>)
|
|
135
|
+
.nullable()
|
|
136
|
+
.optional();
|
|
137
|
+
} else {
|
|
138
|
+
fieldSchema = (fieldSchema as z.ZodTypeAny).nullable().optional();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Apply custom validation if provided
|
|
143
|
+
if (field.validation) {
|
|
144
|
+
fieldSchema = field.validation as z.ZodTypeAny;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply type-specific validations
|
|
148
|
+
if (field.type === "email") {
|
|
149
|
+
fieldSchema = z.string().email(`${field.label} must be a valid email`);
|
|
150
|
+
} else if (field.type === "url") {
|
|
151
|
+
fieldSchema = z.string().url(`${field.label} must be a valid URL`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
schemaObject[field.name] = fieldSchema;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return z.object(schemaObject);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate form data against EntityConfig
|
|
162
|
+
*/
|
|
163
|
+
export function validateFormData(
|
|
164
|
+
data: Record<string, unknown>,
|
|
165
|
+
config: EntityConfig,
|
|
166
|
+
): { valid: boolean; errors: Record<string, string> } {
|
|
167
|
+
const schema = buildSchemaFromConfig(config.fields);
|
|
168
|
+
const result = schema.safeParse(data);
|
|
169
|
+
|
|
170
|
+
if (result.success) {
|
|
171
|
+
return { valid: true, errors: {} };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const errors: Record<string, string> = {};
|
|
175
|
+
result.error.errors.forEach((error) => {
|
|
176
|
+
const fieldName = error.path[0] as string;
|
|
177
|
+
errors[fieldName] = error.message;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return { valid: false, errors };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get default values object from config
|
|
185
|
+
*/
|
|
186
|
+
export function getDefaultValuesFromConfig(
|
|
187
|
+
config: EntityConfig,
|
|
188
|
+
): Record<string, unknown> {
|
|
189
|
+
const defaults: Record<string, unknown> = {};
|
|
190
|
+
|
|
191
|
+
config.fields.forEach((field) => {
|
|
192
|
+
if (!field.hideInForm) {
|
|
193
|
+
defaults[field.name] = getFieldDefaultValue(field);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return defaults;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Transform form data to match API format
|
|
202
|
+
*/
|
|
203
|
+
export function transformFormData(
|
|
204
|
+
data: Record<string, unknown>,
|
|
205
|
+
config: EntityConfig,
|
|
206
|
+
): Record<string, unknown> {
|
|
207
|
+
const transformed: Record<string, unknown> = {};
|
|
208
|
+
|
|
209
|
+
config.fields.forEach((field) => {
|
|
210
|
+
const value = data[field.name];
|
|
211
|
+
|
|
212
|
+
// Nếu có transform.output thì ưu tiên dùng (ví dụ: mảng -> CSV string)
|
|
213
|
+
if (field.transform?.output) {
|
|
214
|
+
transformed[field.name] = field.transform.output(value);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Handle different field types
|
|
219
|
+
switch (field.type) {
|
|
220
|
+
case "date":
|
|
221
|
+
case "datetime":
|
|
222
|
+
case "time":
|
|
223
|
+
if (value instanceof Date) {
|
|
224
|
+
transformed[field.name] = value.toISOString();
|
|
225
|
+
} else if (typeof value === "string") {
|
|
226
|
+
transformed[field.name] = value;
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
case "number":
|
|
230
|
+
case "integer":
|
|
231
|
+
if (value !== null && value !== undefined && value !== "") {
|
|
232
|
+
transformed[field.name] = Number(value);
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
case "boolean":
|
|
236
|
+
transformed[field.name] = Boolean(value);
|
|
237
|
+
break;
|
|
238
|
+
case "multiselect":
|
|
239
|
+
transformed[field.name] = Array.isArray(value) ? value : [];
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
transformed[field.name] = value;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return transformed;
|
|
247
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { DataSource } from "../../types";
|
|
2
|
+
|
|
3
|
+
import { crudConfig } from "../../configs";
|
|
4
|
+
|
|
5
|
+
interface CachedData {
|
|
6
|
+
data: Array<{ label: string; value: string | number | boolean }>;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class DataLoader {
|
|
11
|
+
private cache = new Map<string, CachedData>();
|
|
12
|
+
// Cache enabled để tránh duplicate API calls
|
|
13
|
+
// Configure via CRUD_CACHE_ENABLED env variable (default: true)
|
|
14
|
+
private cacheEnabled = crudConfig.dataLoader.cacheEnabled;
|
|
15
|
+
// Default cache time in milliseconds
|
|
16
|
+
// Configure via CRUD_CACHE_TIME_MS env variable (default: 300000 = 5 minutes)
|
|
17
|
+
private defaultCacheTime = crudConfig.dataLoader.defaultCacheTime;
|
|
18
|
+
// Track pending requests to prevent duplicate calls
|
|
19
|
+
private pendingRequests = new Map<
|
|
20
|
+
string,
|
|
21
|
+
Promise<Array<{ label: string; value: string | number | boolean }>>
|
|
22
|
+
>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load options from dataSource (static or API)
|
|
26
|
+
*/
|
|
27
|
+
async loadOptions(
|
|
28
|
+
dataSource: DataSource,
|
|
29
|
+
): Promise<Array<{ label: string; value: string | number | boolean }>> {
|
|
30
|
+
// Static data source
|
|
31
|
+
if (dataSource.type === "static" && dataSource.options) {
|
|
32
|
+
return dataSource.options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// API data source
|
|
36
|
+
if (dataSource.type === "api" && dataSource.endpoint) {
|
|
37
|
+
return this.loadFromAPI(dataSource);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error("Invalid dataSource configuration");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load options from API with caching
|
|
45
|
+
*/
|
|
46
|
+
private async loadFromAPI(
|
|
47
|
+
dataSource: DataSource,
|
|
48
|
+
): Promise<Array<{ label: string; value: string | number | boolean }>> {
|
|
49
|
+
const cacheKey = this.getCacheKey(dataSource);
|
|
50
|
+
|
|
51
|
+
// Check cache
|
|
52
|
+
if (this.cacheEnabled) {
|
|
53
|
+
const cached = this.cache.get(cacheKey);
|
|
54
|
+
if (cached) {
|
|
55
|
+
const cacheTime = dataSource.cacheTime || this.defaultCacheTime;
|
|
56
|
+
if (Date.now() - cached.timestamp < cacheTime) {
|
|
57
|
+
console.log("DataLoader: Using cached data for", cacheKey);
|
|
58
|
+
return cached.data;
|
|
59
|
+
}
|
|
60
|
+
// Cache expired, remove it
|
|
61
|
+
this.cache.delete(cacheKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if there's already a pending request for this cacheKey
|
|
66
|
+
const pendingRequest = this.pendingRequests.get(cacheKey);
|
|
67
|
+
if (pendingRequest) {
|
|
68
|
+
console.log("DataLoader: Reusing pending request for", cacheKey);
|
|
69
|
+
return pendingRequest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create new request promise
|
|
73
|
+
const requestPromise = this.fetchFromAPI(dataSource, cacheKey);
|
|
74
|
+
|
|
75
|
+
// Store pending request
|
|
76
|
+
this.pendingRequests.set(cacheKey, requestPromise);
|
|
77
|
+
|
|
78
|
+
// Clean up after request completes (success or error)
|
|
79
|
+
requestPromise
|
|
80
|
+
.finally(() => {
|
|
81
|
+
this.pendingRequests.delete(cacheKey);
|
|
82
|
+
})
|
|
83
|
+
.catch(() => {
|
|
84
|
+
// Error already logged in fetchFromAPI
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return requestPromise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Actually fetch data from API
|
|
92
|
+
*/
|
|
93
|
+
private async fetchFromAPI(
|
|
94
|
+
dataSource: DataSource,
|
|
95
|
+
cacheKey: string,
|
|
96
|
+
): Promise<Array<{ label: string; value: string | number | boolean }>> {
|
|
97
|
+
try {
|
|
98
|
+
// Build URL with params
|
|
99
|
+
const url = new URL(dataSource.endpoint!, window.location.origin);
|
|
100
|
+
if (dataSource.params) {
|
|
101
|
+
Object.entries(dataSource.params).forEach(([key, value]) => {
|
|
102
|
+
url.searchParams.append(key, String(value));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("DataLoader: Fetching from API", url.toString());
|
|
107
|
+
// Fetch from API
|
|
108
|
+
const response = await fetch(url.toString());
|
|
109
|
+
|
|
110
|
+
// Check Content-Type first
|
|
111
|
+
const contentType = response.headers.get("content-type");
|
|
112
|
+
const isJSON = contentType && contentType.includes("application/json");
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const errorText = await response.text();
|
|
116
|
+
console.error(
|
|
117
|
+
`API fetch failed (${response.status}):`,
|
|
118
|
+
errorText.substring(0, 200),
|
|
119
|
+
);
|
|
120
|
+
throw new Error(`Failed to fetch data: ${response.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check Content-Type to ensure it's JSON
|
|
124
|
+
if (!isJSON) {
|
|
125
|
+
const text = await response.text();
|
|
126
|
+
console.error("Invalid Content-Type:", contentType);
|
|
127
|
+
console.error("Response text:", text.substring(0, 200));
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid response format. Expected JSON but got ${contentType || "unknown"}. The endpoint might not exist or returned an error page.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
console.log("DataLoader received data:", {
|
|
135
|
+
endpoint: dataSource.endpoint,
|
|
136
|
+
data,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Transform data
|
|
140
|
+
let options: Array<{ label: string; value: string | number | boolean }>;
|
|
141
|
+
|
|
142
|
+
if (dataSource.transform) {
|
|
143
|
+
// Use custom transform function
|
|
144
|
+
options = dataSource.transform(data);
|
|
145
|
+
} else if (Array.isArray(data)) {
|
|
146
|
+
// Default transformation - direct array
|
|
147
|
+
const labelField = dataSource.labelField || "label";
|
|
148
|
+
const valueField = dataSource.valueField || "value";
|
|
149
|
+
|
|
150
|
+
options = data.map((item) => ({
|
|
151
|
+
label: String(item[labelField]),
|
|
152
|
+
value: item[valueField],
|
|
153
|
+
}));
|
|
154
|
+
} else if (data.data && Array.isArray(data.data)) {
|
|
155
|
+
// Handle paginated response with 'data' property
|
|
156
|
+
const labelField = dataSource.labelField || "label";
|
|
157
|
+
const valueField = dataSource.valueField || "value";
|
|
158
|
+
|
|
159
|
+
options = data.data.map((item: Record<string, unknown>) => ({
|
|
160
|
+
label: String(item[labelField]),
|
|
161
|
+
value: item[valueField],
|
|
162
|
+
}));
|
|
163
|
+
} else if (data.items && Array.isArray(data.items)) {
|
|
164
|
+
// Handle paginated response with 'items' property (common in CRUD APIs)
|
|
165
|
+
const labelField = dataSource.labelField || "label";
|
|
166
|
+
const valueField = dataSource.valueField || "value";
|
|
167
|
+
|
|
168
|
+
options = data.items.map((item: Record<string, unknown>) => ({
|
|
169
|
+
label: String(item[labelField]),
|
|
170
|
+
value: item[valueField],
|
|
171
|
+
}));
|
|
172
|
+
} else {
|
|
173
|
+
console.error("Invalid API response format:", data);
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Invalid API response format. Expected array, {data: []}, or {items: []}, but got: ${JSON.stringify(data).substring(0, 200)}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Cache the result
|
|
180
|
+
if (this.cacheEnabled) {
|
|
181
|
+
this.cache.set(cacheKey, {
|
|
182
|
+
data: options,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return options;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("Error loading data from API:", error);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate cache key from dataSource
|
|
196
|
+
*/
|
|
197
|
+
private getCacheKey(dataSource: DataSource): string {
|
|
198
|
+
if (dataSource.type === "static") {
|
|
199
|
+
return `static-${JSON.stringify(dataSource.options)}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const params = dataSource.params ? JSON.stringify(dataSource.params) : "";
|
|
203
|
+
return `${dataSource.endpoint}-${dataSource.labelField}-${dataSource.valueField}-${params}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear cache for a specific endpoint or all cache
|
|
208
|
+
*/
|
|
209
|
+
clearCache(endpoint?: string): void {
|
|
210
|
+
if (endpoint) {
|
|
211
|
+
// Clear specific endpoint cache
|
|
212
|
+
const keysToDelete: string[] = [];
|
|
213
|
+
this.cache.forEach((_, key) => {
|
|
214
|
+
if (key.startsWith(endpoint)) {
|
|
215
|
+
keysToDelete.push(key);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
keysToDelete.forEach((key) => this.cache.delete(key));
|
|
219
|
+
} else {
|
|
220
|
+
// Clear all cache
|
|
221
|
+
this.cache.clear();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get cache size
|
|
227
|
+
*/
|
|
228
|
+
getCacheSize(): number {
|
|
229
|
+
return this.cache.size;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Singleton instance
|
|
234
|
+
export const dataLoader = new DataLoader();
|