@goplusvn/core 0.1.71 → 0.1.73
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/auth/index.ts +5 -1
- package/src/branding/__tests__/identity.test.ts +101 -0
- package/src/branding/__tests__/logo-route.test.ts +143 -0
- package/src/branding/identity.ts +74 -0
- package/src/branding/index.ts +36 -0
- package/src/branding/logo-route.ts +249 -0
- package/src/branding/pwa.tsx +146 -0
- package/src/branding/types.ts +45 -0
- package/src/configs/entities/departments.config.ts +1 -0
- package/src/configs/entities/material-categories.config.ts +1 -0
- package/src/crud/components/crud-export-button.tsx +3 -2
- package/src/crud/components/crud-page.tsx +56 -25
- package/src/crud/components/crud-row-actions.tsx +16 -4
- package/src/crud/crud-route-handlers.test.ts +352 -0
- package/src/crud/crud-route-handlers.ts +260 -13
- package/src/crud/index.ts +3 -0
- package/src/crud/lib/coerce.test.ts +118 -0
- package/src/crud/lib/coerce.ts +95 -0
- package/src/crud/lib/crud-utils.ts +4 -0
- package/src/crud/lib/entity-endpoints.test.ts +41 -0
- package/src/crud/lib/entity-endpoints.ts +23 -0
- package/src/crud/lib/errors.ts +14 -0
- package/src/crud/lib/import-request.ts +99 -0
- package/src/crud/lib/mutation-builder.test.ts +114 -0
- package/src/crud/lib/mutation-builder.ts +27 -34
- package/src/crud/lib/permissions.test.ts +57 -0
- package/src/crud/lib/permissions.ts +25 -13
- package/src/crud/lib/query-builder.ts +13 -6
- package/src/crud/pages/entity-crud-page.tsx +11 -8
- package/src/crud/server-service.test.ts +112 -0
- package/src/crud/server-service.ts +52 -27
- package/src/crud/server.ts +11 -1
- package/src/guardrails/rules/design.ts +3 -2
- package/src/styles/base.css +16 -0
- package/src/types/index.ts +75 -10
- package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
- package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
- package/src/ui/data-display/data-table/data-table.tsx +68 -23
- package/src/ui/data-display/data-table-pagination.tsx +25 -8
- package/src/ui/data-display/index.tsx +1 -0
- package/src/ui/data-display/use-client-pagination.ts +48 -0
- package/src/ui/errors/error-view.tsx +80 -87
- package/src/ui/errors/index.ts +6 -1
- package/src/ui/errors/not-found-view.tsx +89 -16
- package/src/ui/errors/status-page.tsx +132 -0
- package/src/ui/layout/logo.tsx +5 -0
- package/src/ui/pages/not-found.tsx +11 -27
- package/templates/starter-app/AGENTS.md +1 -1
- package/templates/starter-app/next.config.mjs +8 -0
- package/templates/starter-app/package.json +1 -0
- package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
- package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
- package/templates/starter-app/public/images/illustrations/characters/character-01.svg +33 -0
- package/templates/starter-app/public/images/illustrations/characters/character-02.svg +30 -0
- package/templates/starter-app/public/images/illustrations/characters/character-03.svg +30 -0
- package/templates/starter-app/public/images/illustrations/characters/character-04.svg +27 -0
- package/templates/starter-app/public/images/logos/goeat_logo.png +0 -0
- package/templates/starter-app/scripts/rbac-sync.ts +49 -21
- package/templates/starter-app/src/__tests__/architecture.test.ts +3 -2
- package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +4 -4
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +1 -2
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +7 -5
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +38 -11
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +4 -4
- package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +5 -5
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +3 -1
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +13 -5
- package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +3 -2
- package/templates/starter-app/src/app/[lang]/layout.tsx +27 -2
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
- package/templates/starter-app/src/app/api/branding/logo/route.ts +25 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
- package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
- package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
- package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
- package/templates/starter-app/src/app/api/job-titles/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
- package/templates/starter-app/src/app/api/roles/route.ts +2 -1
- package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +5 -5
- package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +1 -1
- package/templates/starter-app/src/app/api/security/sessions/route.ts +2 -2
- package/templates/starter-app/src/app/api/system-categories/route.ts +39 -14
- package/templates/starter-app/src/app/api/system-category-groups/route.ts +32 -14
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
- package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
- package/templates/starter-app/src/app/api/upload/route.ts +1 -2
- package/templates/starter-app/src/app/api/users/[id]/route.ts +20 -12
- package/templates/starter-app/src/app/api/users/route.ts +11 -9
- package/templates/starter-app/src/app/globals.css +0 -1
- package/templates/starter-app/src/app/layout.tsx +42 -1
- package/templates/starter-app/src/app/manifest.ts +23 -19
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
- package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
- package/templates/starter-app/src/configs/entities/index.ts +1 -1
- package/templates/starter-app/src/configs/entities/job-titles.config.ts +2 -0
- package/templates/starter-app/src/configs/entities/system-alerts.config.ts +2 -0
- package/templates/starter-app/src/configs/entities/users.config.ts +15 -2
- package/templates/starter-app/src/configs/i18n.ts +6 -6
- package/templates/starter-app/src/configs/permissions/menu-tree.ts +1 -1
- package/templates/starter-app/src/configs/tenant.ts +6 -1
- package/templates/starter-app/src/data/dictionary.ts +2 -1
- package/templates/starter-app/src/data/navigations.ts +12 -2
- package/templates/starter-app/src/lib/action-guard.ts +3 -4
- package/templates/starter-app/src/lib/api-handler.ts +27 -19
- package/templates/starter-app/src/lib/auth.ts +3 -1
- package/templates/starter-app/src/lib/branch-scope.ts +1 -1
- package/templates/starter-app/src/lib/crud/index.ts +5 -12
- package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
- package/templates/starter-app/src/lib/logger.ts +11 -4
- package/templates/starter-app/src/lib/page-guard.ts +2 -3
- package/templates/starter-app/src/lib/prisma.ts +13 -5
- package/templates/starter-app/src/lib/rbac/access.ts +2 -2
- package/templates/starter-app/src/lib/storage.ts +3 -1
- package/templates/starter-app/src/providers/index.tsx +13 -2
- package/templates/starter-app/src/providers/theme-provider.tsx +4 -3
- package/templates/starter-app/src/proxy.ts +3 -1
- package/templates/starter-app/src/server/services/company-service.ts +4 -2
- package/templates/starter-app/src/server/services/notification-service.ts +6 -6
- package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
- package/templates/starter-app/src/server/services/user-service.ts +2 -2
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
- package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
- package/templates/starter-app/tsconfig.json +8 -0
- package/templates/starter-app/vitest.config.ts +17 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type { Metadata, MetadataRoute } from "next";
|
|
4
|
+
|
|
5
|
+
import type { BrandingIdentity } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PWA "cài được lên màn hình chính" — mức mặc định cho mọi app dựng từ core.
|
|
9
|
+
*
|
|
10
|
+
* Ba mảnh phải khớp nhau, thiếu một là iOS lặng lẽ tạo web clip THƯỜNG (bấm ra
|
|
11
|
+
* mở Safari, mất thanh trạng thái riêng, mất standalone):
|
|
12
|
+
* 1. `buildAppManifest` — `/manifest.webmanifest`, icon lấy từ logo công ty.
|
|
13
|
+
* 2. `buildBrandingMetadata` — `icons` + `appleWebApp` cho Metadata API.
|
|
14
|
+
* 3. `<PwaMetaTags>` — render CỨNG trong shell, xem ghi chú bên dưới.
|
|
15
|
+
*
|
|
16
|
+
* Service worker/offline KHÔNG nằm ở đây: app chạy trong nhà máy sóng yếu thì
|
|
17
|
+
* tự thêm serwist ở tầng app, đó là quyết định riêng từng dự án.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface AppManifestOptions {
|
|
21
|
+
/** Mô tả ngắn. Bỏ trống ⇒ dùng tagline, rồi tới tên app. */
|
|
22
|
+
description?: string;
|
|
23
|
+
display?: MetadataRoute.Manifest["display"];
|
|
24
|
+
orientation?: MetadataRoute.Manifest["orientation"];
|
|
25
|
+
/** `id` PHẢI ổn định — đổi nó là Android coi như một app KHÁC. */
|
|
26
|
+
id?: string;
|
|
27
|
+
scope?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildAppManifest(
|
|
31
|
+
identity: BrandingIdentity,
|
|
32
|
+
options: AppManifestOptions = {},
|
|
33
|
+
): MetadataRoute.Manifest {
|
|
34
|
+
const { logoPath } = identity;
|
|
35
|
+
return {
|
|
36
|
+
id: options.id ?? "/",
|
|
37
|
+
name: identity.appName,
|
|
38
|
+
short_name: identity.appName,
|
|
39
|
+
description: options.description || identity.tagline || identity.appName,
|
|
40
|
+
// start_url có locale sẵn: "/" bắt mỗi lần mở app đi 2–3 chặng redirect
|
|
41
|
+
// server, mấy chặng đó không vẽ được gì ngoài màn trắng.
|
|
42
|
+
start_url: identity.startUrl,
|
|
43
|
+
// Mặc định scope là THƯ MỤC của start_url — khai tường minh, nếu không
|
|
44
|
+
// start_url "/vi/home" sẽ khoá app trong "/vi/".
|
|
45
|
+
scope: options.scope ?? "/",
|
|
46
|
+
display: options.display ?? "standalone",
|
|
47
|
+
orientation: options.orientation,
|
|
48
|
+
background_color: identity.backgroundColor,
|
|
49
|
+
theme_color: identity.themeColor,
|
|
50
|
+
icons: [
|
|
51
|
+
{
|
|
52
|
+
src: `${logoPath}?variant=icon`,
|
|
53
|
+
sizes: "512x512",
|
|
54
|
+
type: "image/png",
|
|
55
|
+
purpose: "any",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
src: `${logoPath}?variant=maskable`,
|
|
59
|
+
sizes: "512x512",
|
|
60
|
+
type: "image/png",
|
|
61
|
+
purpose: "maskable",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Phần `Metadata` phụ thuộc thương hiệu — trải vào `generateMetadata` của root
|
|
69
|
+
* layout. Khai `icons` ở đây khiến Next BỎ QUA file `app/icon.png` /
|
|
70
|
+
* `app/apple-icon.png`; ngược lại, còn hai file đó thì convention file-based
|
|
71
|
+
* THẮNG và favicon sẽ mãi là logo cũ.
|
|
72
|
+
*/
|
|
73
|
+
export function buildBrandingMetadata(identity: BrandingIdentity): Metadata {
|
|
74
|
+
const { logoPath, logoUrl } = identity;
|
|
75
|
+
const version = logoUrl.includes("?v=") ? `&${logoUrl.split("?")[1]}` : "";
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
applicationName: identity.appName,
|
|
79
|
+
manifest: "/manifest.webmanifest",
|
|
80
|
+
icons: {
|
|
81
|
+
icon: [
|
|
82
|
+
{ url: `${logoPath}?variant=favicon${version}`, type: "image/png" },
|
|
83
|
+
],
|
|
84
|
+
shortcut: [{ url: `${logoPath}?variant=favicon${version}` }],
|
|
85
|
+
apple: [
|
|
86
|
+
{
|
|
87
|
+
url: `${logoPath}?variant=apple${version}`,
|
|
88
|
+
sizes: "180x180",
|
|
89
|
+
type: "image/png",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
appleWebApp: {
|
|
94
|
+
capable: true,
|
|
95
|
+
title: identity.appName,
|
|
96
|
+
statusBarStyle: "default",
|
|
97
|
+
},
|
|
98
|
+
openGraph: {
|
|
99
|
+
siteName: identity.appName,
|
|
100
|
+
images: [{ url: `${logoPath}?variant=og${version}`, width: 1200, height: 630 }],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Thẻ PWA render CỨNG trong shell (đặt ngay đầu `<body>` của root layout).
|
|
107
|
+
*
|
|
108
|
+
* Vì sao không trông vào Metadata API: từ khi layout stream qua `<Suspense>`,
|
|
109
|
+
* Next đẩy output của `generateMetadata` (phải chờ DB) xuống SAU vài chục KB
|
|
110
|
+
* đầu tiên của body. Safari lúc "Thêm vào MH chính" đọc `<head>` ở khoảnh khắc
|
|
111
|
+
* đó chưa thấy `<link rel="manifest">` nên tạo web clip thường. React hoist các
|
|
112
|
+
* thẻ này lên `<head>` ngay từ flush đầu.
|
|
113
|
+
*
|
|
114
|
+
* Next 16 cũng KHÔNG còn phát `apple-mobile-web-app-capable` từ
|
|
115
|
+
* `appleWebApp.capable` (chỉ phát bản `mobile-web-app-capable`) — thẻ apple-*
|
|
116
|
+
* chỉ tồn tại nhờ component này. Trùng thẻ với Metadata API là vô hại: thẻ đầu
|
|
117
|
+
* theo tree order thắng.
|
|
118
|
+
*/
|
|
119
|
+
export function PwaMetaTags({
|
|
120
|
+
manifestHref = "/manifest.webmanifest",
|
|
121
|
+
logoPath = "/api/branding/logo",
|
|
122
|
+
appName,
|
|
123
|
+
themeColor,
|
|
124
|
+
}: {
|
|
125
|
+
manifestHref?: string;
|
|
126
|
+
logoPath?: string;
|
|
127
|
+
/** Tên trên biểu tượng iOS. Bỏ trống ⇒ để Metadata API lo. */
|
|
128
|
+
appName?: string;
|
|
129
|
+
themeColor?: string;
|
|
130
|
+
}) {
|
|
131
|
+
// Không nhận `BrandingIdentity`: dựng identity phải chờ DB, mà root layout
|
|
132
|
+
// `await` là màn hình TRẮNG cho tới khi DB trả lời. Các thẻ ở đây cố ý là
|
|
133
|
+
// hằng số thuần.
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
<link rel="manifest" href={manifestHref} />
|
|
137
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
138
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
139
|
+
{appName ? (
|
|
140
|
+
<meta name="apple-mobile-web-app-title" content={appName} />
|
|
141
|
+
) : null}
|
|
142
|
+
{themeColor ? <meta name="theme-color" content={themeColor} /> : null}
|
|
143
|
+
<link rel="apple-touch-icon" href={`${logoPath}?variant=apple`} />
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hồ sơ công ty ở mức core CẦN BIẾT — cố ý hẹp.
|
|
3
|
+
*
|
|
4
|
+
* Mỗi app có schema `companies` hơi khác nhau (vinhhoa/spartronics dùng cột
|
|
5
|
+
* `logo_url`, thingtodo dùng `logo`), nên core nhận cả hai tên và không đụng
|
|
6
|
+
* tới phần còn lại của bản ghi.
|
|
7
|
+
*/
|
|
8
|
+
export interface BrandingCompany {
|
|
9
|
+
/** Tên pháp lý — in trên hợp đồng/hoá đơn. */
|
|
10
|
+
name?: string | null;
|
|
11
|
+
/** Tên hiển thị trên app/PWA. Rỗng ⇒ rơi về `name`, rồi tới tenant. */
|
|
12
|
+
systemName?: string | null;
|
|
13
|
+
/** URL logo đã lưu: `/api/files/<key>`, `/uploads/...` hoặc http(s). */
|
|
14
|
+
logoUrl?: string | null;
|
|
15
|
+
/** Bí danh của `logoUrl` cho app đặt tên cột là `logo`. */
|
|
16
|
+
logo?: string | null;
|
|
17
|
+
/** Dùng làm `?v=` phá cache của URL logo. */
|
|
18
|
+
updatedAt?: Date | string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Bản sắc thương hiệu đã hoà giải (DB đè lên tenant config tĩnh). */
|
|
22
|
+
export interface BrandingIdentity {
|
|
23
|
+
/** Tên ngắn hiển thị sidebar/PWA. */
|
|
24
|
+
appName: string;
|
|
25
|
+
/** Tên pháp lý đầy đủ. */
|
|
26
|
+
companyName: string;
|
|
27
|
+
/** Dòng phụ dưới tên ở sidebar/đăng nhập. `""` = ẩn. */
|
|
28
|
+
tagline: string;
|
|
29
|
+
/** URL logo dùng chung cho MỌI bề mặt, đã kèm `?v=`. */
|
|
30
|
+
logoUrl: string;
|
|
31
|
+
/** Đường dẫn gốc của route logo (mặc định `/api/branding/logo`). */
|
|
32
|
+
logoPath: string;
|
|
33
|
+
themeColor: string;
|
|
34
|
+
backgroundColor: string;
|
|
35
|
+
/** `start_url` của manifest — PHẢI là đường đã có locale. */
|
|
36
|
+
startUrl: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Các biến thể kích thước mà route logo tự render. */
|
|
40
|
+
export type BrandingLogoVariant =
|
|
41
|
+
| "favicon"
|
|
42
|
+
| "apple"
|
|
43
|
+
| "icon"
|
|
44
|
+
| "maskable"
|
|
45
|
+
| "og";
|
|
@@ -53,8 +53,9 @@ export function CrudExportButton({
|
|
|
53
53
|
search: activeSearch,
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
// Always force format to xlsx
|
|
57
|
-
|
|
56
|
+
// Always force format to xlsx. Endpoint từ getEntityEndpoints có thể đã
|
|
57
|
+
// mang sẵn query params — nối bằng "&" thay vì "?" thứ hai.
|
|
58
|
+
const url = `${endpoint}${endpoint.includes("?") ? "&" : "?"}${queryParams}&format=xlsx`;
|
|
58
59
|
|
|
59
60
|
const response = await fetch(url);
|
|
60
61
|
if (!response.ok) {
|
|
@@ -32,6 +32,7 @@ import { useRouter, useParams } from "next/navigation";
|
|
|
32
32
|
import { CrudBulkActions } from "./crud-bulk-actions";
|
|
33
33
|
import { CrudDeleteDialog } from "./crud-delete-dialog";
|
|
34
34
|
import { crudService } from "../lib/crud-service";
|
|
35
|
+
import { getEntityEndpoints } from "../lib/entity-endpoints";
|
|
35
36
|
import { usePrefetch } from "../../hooks";
|
|
36
37
|
import type { DictionaryType } from "../../hooks";
|
|
37
38
|
|
|
@@ -618,31 +619,65 @@ function CrudPageContent({
|
|
|
618
619
|
}
|
|
619
620
|
|
|
620
621
|
default: {
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
622
|
+
// Action HTTP thuần data (urlTemplate + method) — sống sót qua
|
|
623
|
+
// serializeConfig nên khai được ngay trong entity config phía server.
|
|
624
|
+
if (rowAction.urlTemplate) {
|
|
625
|
+
// Fail-closed: khai permissionAction mà user không có quyền thì thôi
|
|
626
|
+
// (menu đã ẩn, đây là chốt chặn thứ hai).
|
|
627
|
+
if (
|
|
628
|
+
rowAction.permissionAction &&
|
|
629
|
+
!permissions[rowAction.permissionAction]
|
|
630
|
+
) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (
|
|
634
|
+
rowAction.confirmMessage &&
|
|
635
|
+
!window.confirm(rowAction.confirmMessage)
|
|
636
|
+
) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const url = rowAction.urlTemplate.replace(
|
|
640
|
+
/\{id\}/g,
|
|
641
|
+
encodeURIComponent(rowId),
|
|
642
|
+
);
|
|
643
|
+
const response = await fetch(url, {
|
|
644
|
+
method: rowAction.method ?? "POST",
|
|
645
|
+
});
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
let message = `${response.status} ${response.statusText}`;
|
|
648
|
+
try {
|
|
649
|
+
const body = await response.json();
|
|
650
|
+
message = body.error || body.message || message;
|
|
651
|
+
} catch {
|
|
652
|
+
// body không phải JSON — giữ status text
|
|
653
|
+
}
|
|
654
|
+
throw new Error(message);
|
|
655
|
+
}
|
|
656
|
+
await fetchData(false);
|
|
657
|
+
toast.success(t("success", "Success"), {
|
|
658
|
+
description:
|
|
659
|
+
rowAction.successMessage ??
|
|
660
|
+
tMessage("updated", { entity: config.label }),
|
|
661
|
+
});
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
626
664
|
|
|
665
|
+
// Custom handler qua config function bị serializeConfig strip —
|
|
666
|
+
// action không có urlTemplate thì phải đi qua customActions prop.
|
|
627
667
|
console.warn(
|
|
628
|
-
`Action "${action}" has no built-in handler and custom handlers via config are not supported in this architecture. Please use customActions prop.`,
|
|
668
|
+
`Action "${action}" has no built-in handler and custom handlers via config are not supported in this architecture. Please use customActions prop or declare urlTemplate.`,
|
|
629
669
|
);
|
|
630
|
-
|
|
631
|
-
/* Deprecated logic removed to fix build:
|
|
632
|
-
if (entityName) {
|
|
633
|
-
try {
|
|
634
|
-
const originalConfig = getEntityConfig(entityName)
|
|
635
|
-
...
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
*/
|
|
639
670
|
}
|
|
640
671
|
}
|
|
641
672
|
} catch (error) {
|
|
642
673
|
console.error(`Error executing ${action} action:`, error);
|
|
643
674
|
globalError.emit({
|
|
644
675
|
title: t("error", "Error"),
|
|
645
|
-
|
|
676
|
+
// Ưu tiên message từ API (đọc từ body JSON) — fallback câu chung.
|
|
677
|
+
description:
|
|
678
|
+
error instanceof Error && error.message
|
|
679
|
+
? error.message
|
|
680
|
+
: tMessage("saveFailed", { entity: rowAction.label }),
|
|
646
681
|
trace: error instanceof Error ? error.stack : undefined,
|
|
647
682
|
});
|
|
648
683
|
}
|
|
@@ -858,18 +893,14 @@ function CrudPageContent({
|
|
|
858
893
|
className="h-6 hidden sm:block"
|
|
859
894
|
/>
|
|
860
895
|
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
|
896
|
+
{/* Import/Export dùng CÙNG quy tắc endpoint từ config
|
|
897
|
+
(getEntityEndpoints) — trước đây export hardcode
|
|
898
|
+
/api/crud/... lệch với import → 404. */}
|
|
861
899
|
{permissions.import && config.features?.import && (
|
|
862
900
|
<Suspense fallback={null}>
|
|
863
901
|
<ImportDialog
|
|
864
902
|
config={config}
|
|
865
|
-
apiUrl={(
|
|
866
|
-
// Handle endpoint with query params correctly
|
|
867
|
-
const baseUrl = config.apiEndpoint.split("?")[0];
|
|
868
|
-
const queryParams = config.apiEndpoint.includes("?")
|
|
869
|
-
? config.apiEndpoint.split("?")[1]
|
|
870
|
-
: "";
|
|
871
|
-
return `${baseUrl}/import${queryParams ? `?${queryParams}` : ""}`;
|
|
872
|
-
})()}
|
|
903
|
+
apiUrl={getEntityEndpoints(config).import}
|
|
873
904
|
canImport={permissions.import}
|
|
874
905
|
/>
|
|
875
906
|
</Suspense>
|
|
@@ -877,7 +908,7 @@ function CrudPageContent({
|
|
|
877
908
|
{permissions.export && config.features?.export && (
|
|
878
909
|
<Suspense fallback={null}>
|
|
879
910
|
<CrudExportButton
|
|
880
|
-
endpoint={
|
|
911
|
+
endpoint={getEntityEndpoints(config).export}
|
|
881
912
|
filters={filters}
|
|
882
913
|
search={search}
|
|
883
914
|
canExport={permissions.export}
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
Archive,
|
|
5
5
|
Copy,
|
|
6
6
|
Eye,
|
|
7
|
+
History,
|
|
7
8
|
MoreHorizontal,
|
|
8
9
|
Pencil,
|
|
10
|
+
Pin,
|
|
11
|
+
SquareArrowOutUpRight,
|
|
9
12
|
Trash2,
|
|
10
13
|
CheckCircle2,
|
|
11
14
|
XCircle,
|
|
@@ -81,8 +84,15 @@ export function CrudRowActions({
|
|
|
81
84
|
return <CheckCircle2 className="mr-2 h-4 w-4" aria-hidden="true" />;
|
|
82
85
|
case "reject":
|
|
83
86
|
return <XCircle className="mr-2 h-4 w-4" aria-hidden="true" />;
|
|
87
|
+
case "set-default":
|
|
88
|
+
return <Pin className="mr-2 h-4 w-4" aria-hidden="true" />;
|
|
89
|
+
case "view-history":
|
|
90
|
+
return <History className="mr-2 h-4 w-4" aria-hidden="true" />;
|
|
84
91
|
default:
|
|
85
|
-
|
|
92
|
+
// Action tự khai không có icon riêng — icon fallback trung tính.
|
|
93
|
+
return (
|
|
94
|
+
<SquareArrowOutUpRight className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
95
|
+
);
|
|
86
96
|
}
|
|
87
97
|
};
|
|
88
98
|
|
|
@@ -139,9 +149,11 @@ export function CrudRowActions({
|
|
|
139
149
|
}
|
|
140
150
|
};
|
|
141
151
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
// Gate theo permissionAction khai trong config; không khai thì
|
|
153
|
+
// giữ special-case approve/reject cũ, còn lại luôn hiện.
|
|
154
|
+
const hasActionPermission = action.permissionAction
|
|
155
|
+
? Boolean(permissions[action.permissionAction])
|
|
156
|
+
: action.action === "approve"
|
|
145
157
|
? permissions.approve
|
|
146
158
|
: action.action === "reject"
|
|
147
159
|
? permissions.reject
|