@goplusvn/core 0.1.21 → 0.1.23
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/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/src/rbac/components/roles/role-card.tsx +25 -24
- package/src/rbac/components/roles/role-toolbar.tsx +70 -84
- package/src/rbac/index.ts +8 -0
- package/src/rbac/lib/fetch-error.ts +26 -0
- package/src/rbac/pages/action-list-page.tsx +581 -19
- package/src/rbac/pages/index.ts +13 -0
- package/src/rbac/pages/resource-list-page.tsx +1415 -19
- package/src/rbac/pages/role-form-page.tsx +704 -0
- package/src/rbac/pages/role-list-page.tsx +14 -12
- package/src/ui/forms/date-picker.tsx +167 -19
- package/src/ui/forms/multi-select.tsx +475 -153
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.23 — Trang RBAC hoàn chỉnh trong core (roles/resources/actions), restyle trắng
|
|
4
|
+
|
|
5
|
+
3 trang quản trị RBAC trước đây mỗi app tự chép (vinhhoa/wu-vpbank…) nay là trang
|
|
6
|
+
core dùng chung, đồng thời restyle bỏ các mảng `bg-muted`/raw-slate xám → bề mặt
|
|
7
|
+
trắng `bg-card`, phân tách bằng border; nút sửa/xóa trên hàng LUÔN hiển thị
|
|
8
|
+
(trước ẩn sau hover → mobile không dùng được).
|
|
9
|
+
|
|
10
|
+
- `ResourceListPage` (viết lại): nhóm tài nguyên theo section, bung hàng sửa
|
|
11
|
+
action inline, Action Manager dialog, sync. API: `/api/resources`, `/api/actions`.
|
|
12
|
+
- `ActionListPage` (viết lại): danh sách hành động + ngữ cảnh sử dụng
|
|
13
|
+
(tài nguyên · vai trò · người dùng, cần `/api/actions/usage` — optional).
|
|
14
|
+
- `RoleFormPage` (MỚI): form tạo/sửa vai trò + ma trận phân quyền theo nhóm;
|
|
15
|
+
prop `actionLabels` cho app bổ sung nhãn action nghiệp vụ riêng.
|
|
16
|
+
- `RoleListPage`: thêm prop `importSlot` (nhét ImportDialog app-specific vào
|
|
17
|
+
toolbar); `RoleToolbar` viết lại theo `ListToolbar` + `CrudExportButton`
|
|
18
|
+
(bỏ raw hex, nút import/export chết); `RoleCard` restyle token semantic.
|
|
19
|
+
- Barrel client-safe mới: `@goerp/core/rbac/pages` (không kéo server code).
|
|
20
|
+
|
|
21
|
+
⚠️ Breaking (không consumer nào dùng bản cũ): `ResourceListPage`/`ActionListPage`
|
|
22
|
+
đổi từ wrapper `EntityCrudPage` (props `{lang, session, dictionary, config}`)
|
|
23
|
+
sang trang đầy đủ (props không bắt buộc / `{permissions}`).
|
|
24
|
+
|
|
3
25
|
## 0.1.19 — applyBrandThemeTokens (gom logic áp theme, subtle dark-aware)
|
|
4
26
|
|
|
5
27
|
Mỗi app copy y hệt logic áp bảng màu thương hiệu vào CSS token trong theme-provider
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
// Thẻ vai trò trong grid danh sách — bề mặt trắng (bg-card), số liệu đóng
|
|
4
|
+
// khung border thay vì tô nền xám; token semantic thay raw slate.
|
|
5
|
+
|
|
3
6
|
import { memo, useCallback } from "react";
|
|
4
|
-
import { format } from "date-fns";
|
|
5
7
|
import { useRouter } from "next/navigation";
|
|
8
|
+
import { Copy, Edit, Trash2 } from "lucide-react";
|
|
9
|
+
|
|
6
10
|
import {
|
|
11
|
+
Badge,
|
|
12
|
+
Button,
|
|
7
13
|
Card,
|
|
8
14
|
CardContent,
|
|
9
15
|
CardDescription,
|
|
10
16
|
CardHeader,
|
|
11
17
|
CardTitle,
|
|
12
|
-
Button,
|
|
13
|
-
Badge,
|
|
14
18
|
} from "../../../ui";
|
|
15
|
-
import { Edit, Copy, Trash2 } from "lucide-react";
|
|
16
19
|
import type { Role } from "../../types";
|
|
17
20
|
|
|
18
21
|
interface RoleCardProps {
|
|
@@ -60,18 +63,16 @@ export const RoleCard = memo(function RoleCard({
|
|
|
60
63
|
}, [onDelete, role]);
|
|
61
64
|
|
|
62
65
|
return (
|
|
63
|
-
<Card className="
|
|
66
|
+
<Card className="bg-card border border-border rounded-[12px] shadow-sm transition-all hover:shadow-md hover:border-primary/40 overflow-hidden">
|
|
64
67
|
<CardHeader className="p-4 pb-2">
|
|
65
68
|
<div className="flex items-start justify-between">
|
|
66
|
-
<div className="flex items-center space-x-
|
|
67
|
-
<div className=
|
|
68
|
-
<div className={`w-3 h-3 rounded-full ${roleColor} ring-4 ring-slate-50 dark:ring-slate-800/50`} />
|
|
69
|
-
</div>
|
|
69
|
+
<div className="flex items-center space-x-2.5">
|
|
70
|
+
<div className={`w-2.5 h-2.5 rounded-full ${roleColor}`} />
|
|
70
71
|
<div>
|
|
71
|
-
<CardTitle className="text-[15px] font-bold text-
|
|
72
|
+
<CardTitle className="text-[15px] font-bold text-foreground leading-tight">
|
|
72
73
|
{role.name}
|
|
73
74
|
</CardTitle>
|
|
74
|
-
<CardDescription className="text-[11px] font-mono text-
|
|
75
|
+
<CardDescription className="text-[11px] font-mono text-muted-foreground mt-0.5">
|
|
75
76
|
{role.code}
|
|
76
77
|
</CardDescription>
|
|
77
78
|
</div>
|
|
@@ -86,7 +87,7 @@ export const RoleCard = memo(function RoleCard({
|
|
|
86
87
|
) : (
|
|
87
88
|
<Badge
|
|
88
89
|
variant="outline"
|
|
89
|
-
className="bg-
|
|
90
|
+
className="bg-muted text-muted-foreground border-border text-[10px] px-2 py-0 h-5 font-semibold"
|
|
90
91
|
>
|
|
91
92
|
Khóa
|
|
92
93
|
</Badge>
|
|
@@ -94,24 +95,24 @@ export const RoleCard = memo(function RoleCard({
|
|
|
94
95
|
</div>
|
|
95
96
|
</CardHeader>
|
|
96
97
|
<CardContent className="p-4 pt-0 space-y-3">
|
|
97
|
-
<p className="text-[13px] text-
|
|
98
|
+
<p className="text-[13px] text-muted-foreground line-clamp-2 min-h-[2.5rem] leading-relaxed">
|
|
98
99
|
{role.description || "Chưa có mô tả chi tiết cho vai trò này."}
|
|
99
100
|
</p>
|
|
100
101
|
|
|
101
|
-
<div className="flex items-center justify-between
|
|
102
|
-
<div className="flex flex-col items-center flex-1 border-r border-
|
|
103
|
-
<span className="text-[16px] font-black text-primary leading-none mb-1">
|
|
102
|
+
<div className="flex items-center justify-between rounded-lg p-2.5 border border-border bg-card">
|
|
103
|
+
<div className="flex flex-col items-center flex-1 border-r border-border">
|
|
104
|
+
<span className="text-[16px] font-black text-primary leading-none mb-1 tabular-nums">
|
|
104
105
|
{permissionsCount}
|
|
105
106
|
</span>
|
|
106
|
-
<span className="text-[9px] text-
|
|
107
|
+
<span className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider">
|
|
107
108
|
Quyền hạn
|
|
108
109
|
</span>
|
|
109
110
|
</div>
|
|
110
111
|
<div className="flex flex-col items-center flex-1">
|
|
111
|
-
<span className="text-[16px] font-black text-emerald-600 dark:text-emerald-500 leading-none mb-1">
|
|
112
|
+
<span className="text-[16px] font-black text-emerald-600 dark:text-emerald-500 leading-none mb-1 tabular-nums">
|
|
112
113
|
{role.usersCount}
|
|
113
114
|
</span>
|
|
114
|
-
<span className="text-[9px] text-
|
|
115
|
+
<span className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider">
|
|
115
116
|
Người dùng
|
|
116
117
|
</span>
|
|
117
118
|
</div>
|
|
@@ -122,10 +123,10 @@ export const RoleCard = memo(function RoleCard({
|
|
|
122
123
|
<Button
|
|
123
124
|
variant="outline"
|
|
124
125
|
size="sm"
|
|
125
|
-
className="flex-1 h-8 text-[12px] font-medium bg-
|
|
126
|
+
className="flex-1 h-8 text-[12px] font-medium bg-card hover:bg-muted/50 transition-colors rounded-[8px] shadow-sm"
|
|
126
127
|
onClick={handleEdit}
|
|
127
128
|
>
|
|
128
|
-
<Edit className="mr-1.5 h-3.5 w-3.5 text-
|
|
129
|
+
<Edit className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
|
|
129
130
|
Sửa
|
|
130
131
|
</Button>
|
|
131
132
|
)}
|
|
@@ -133,10 +134,10 @@ export const RoleCard = memo(function RoleCard({
|
|
|
133
134
|
<Button
|
|
134
135
|
variant="outline"
|
|
135
136
|
size="sm"
|
|
136
|
-
className="flex-1 h-8 text-[12px] font-medium bg-
|
|
137
|
+
className="flex-1 h-8 text-[12px] font-medium bg-card hover:bg-muted/50 transition-colors rounded-[8px] shadow-sm"
|
|
137
138
|
onClick={handleDuplicate}
|
|
138
139
|
>
|
|
139
|
-
<Copy className="mr-1.5 h-3.5 w-3.5 text-
|
|
140
|
+
<Copy className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
|
|
140
141
|
Chép
|
|
141
142
|
</Button>
|
|
142
143
|
)}
|
|
@@ -144,7 +145,7 @@ export const RoleCard = memo(function RoleCard({
|
|
|
144
145
|
<Button
|
|
145
146
|
variant="outline"
|
|
146
147
|
size="sm"
|
|
147
|
-
className="flex-1 h-8 text-[12px] font-medium
|
|
148
|
+
className="flex-1 h-8 text-[12px] font-medium bg-card text-destructive hover:bg-destructive/10 hover:text-destructive transition-colors rounded-[8px] shadow-sm"
|
|
148
149
|
onClick={handleDelete}
|
|
149
150
|
>
|
|
150
151
|
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
|
|
@@ -1,123 +1,109 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Toolbar trang vai trò — dùng ListToolbar chung (trắng, không raw hex).
|
|
4
|
+
// Import là phần app-specific (mỗi app một ImportDialog riêng) nên nhận qua
|
|
5
|
+
// slot `importSlot`; Export dùng CrudExportButton của core.
|
|
6
|
+
|
|
7
|
+
import { memo, useMemo } from "react";
|
|
4
8
|
import { useRouter } from "next/navigation";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
|
|
5
11
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
AdvancedField,
|
|
13
|
+
ListToolbar,
|
|
8
14
|
Select,
|
|
9
15
|
SelectContent,
|
|
10
16
|
SelectItem,
|
|
11
17
|
SelectTrigger,
|
|
12
18
|
SelectValue,
|
|
13
19
|
} from "../../../ui";
|
|
14
|
-
import {
|
|
20
|
+
import { CrudExportButton } from "../../../crud/components/crud-export-button";
|
|
21
|
+
import type { ActiveFilter } from "../../../types";
|
|
15
22
|
|
|
16
23
|
interface RoleToolbarProps {
|
|
17
|
-
dictionary: any;
|
|
18
|
-
totalRows: number;
|
|
19
24
|
searchTerm: string;
|
|
20
25
|
statusFilter: string;
|
|
21
26
|
onSearchChange: (value: string) => void;
|
|
22
27
|
onStatusFilterChange: (value: string) => void;
|
|
23
28
|
lang: string;
|
|
29
|
+
dictionary?: any;
|
|
24
30
|
canCreate?: boolean;
|
|
25
31
|
canImport?: boolean;
|
|
26
32
|
canExport?: boolean;
|
|
33
|
+
/** Nút Import app-specific (vd ImportDialog của vinhhoa). Chỉ render khi canImport. */
|
|
34
|
+
importSlot?: ReactNode;
|
|
35
|
+
/** Giữ tương thích chữ ký cũ — không còn hiển thị. */
|
|
36
|
+
totalRows?: number;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
export const RoleToolbar = memo(function RoleToolbar({
|
|
30
|
-
dictionary,
|
|
31
|
-
totalRows,
|
|
32
40
|
searchTerm,
|
|
33
41
|
statusFilter,
|
|
34
42
|
onSearchChange,
|
|
35
43
|
onStatusFilterChange,
|
|
36
44
|
lang,
|
|
45
|
+
dictionary,
|
|
37
46
|
canCreate,
|
|
38
47
|
canImport,
|
|
39
48
|
canExport,
|
|
49
|
+
importSlot,
|
|
40
50
|
}: RoleToolbarProps) {
|
|
41
51
|
const router = useRouter();
|
|
42
|
-
const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm);
|
|
43
|
-
|
|
44
|
-
// Sync local state when prop changes (e.g. clear filter from parent)
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
setLocalSearchTerm(searchTerm);
|
|
47
|
-
}, [searchTerm]);
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const exportFilters = useMemo(() => {
|
|
54
|
+
const filters: ActiveFilter[] = [];
|
|
55
|
+
if (statusFilter && statusFilter !== "all") {
|
|
56
|
+
filters.push({ name: "status", operator: "eq", value: statusFilter });
|
|
57
|
+
}
|
|
58
|
+
return filters;
|
|
59
|
+
}, [statusFilter]);
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
}, [localSearchTerm, onSearchChange, searchTerm]);
|
|
61
|
+
const advancedCount = statusFilter && statusFilter !== "all" ? 1 : 0;
|
|
62
|
+
const isFiltered = !!searchTerm || (!!statusFilter && statusFilter !== "all");
|
|
61
63
|
|
|
62
64
|
return (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<span className="hidden lg:inline text-[13px]">Xuất</span>
|
|
107
|
-
</Button>
|
|
108
|
-
)}
|
|
109
|
-
|
|
110
|
-
{canCreate && (
|
|
111
|
-
<Button
|
|
112
|
-
onClick={() => router.push(`/${lang}/roles/new`)}
|
|
113
|
-
size="sm"
|
|
114
|
-
className="h-9 px-4 text-[13px] bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm rounded-[6px] font-medium border-0"
|
|
115
|
-
>
|
|
116
|
-
<Plus className="mr-1.5 h-4 w-4" />
|
|
117
|
-
<span>Thêm mới</span>
|
|
118
|
-
</Button>
|
|
119
|
-
)}
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
65
|
+
<ListToolbar
|
|
66
|
+
search={searchTerm}
|
|
67
|
+
onSearchChange={onSearchChange}
|
|
68
|
+
searchPlaceholder={dictionary?.common?.search || "Tìm kiếm vai trò..."}
|
|
69
|
+
isFiltered={isFiltered}
|
|
70
|
+
onReset={() => {
|
|
71
|
+
onSearchChange("");
|
|
72
|
+
onStatusFilterChange("all");
|
|
73
|
+
}}
|
|
74
|
+
onCreate={() => router.push(`/${lang}/roles/new`)}
|
|
75
|
+
canCreate={!!canCreate}
|
|
76
|
+
createLabel="Tạo mới"
|
|
77
|
+
advancedCount={advancedCount}
|
|
78
|
+
advanced={
|
|
79
|
+
<AdvancedField label="Trạng thái">
|
|
80
|
+
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
|
81
|
+
<SelectTrigger className="h-9 w-full shadow-sm">
|
|
82
|
+
<SelectValue placeholder="Tất cả" />
|
|
83
|
+
</SelectTrigger>
|
|
84
|
+
<SelectContent>
|
|
85
|
+
<SelectItem value="all">Tất cả</SelectItem>
|
|
86
|
+
<SelectItem value="active">Hoạt động</SelectItem>
|
|
87
|
+
<SelectItem value="inactive">Không hoạt động</SelectItem>
|
|
88
|
+
</SelectContent>
|
|
89
|
+
</Select>
|
|
90
|
+
</AdvancedField>
|
|
91
|
+
}
|
|
92
|
+
tableControls={
|
|
93
|
+
canImport || canExport ? (
|
|
94
|
+
<>
|
|
95
|
+
{canImport && importSlot}
|
|
96
|
+
{canExport && (
|
|
97
|
+
<CrudExportButton
|
|
98
|
+
endpoint="/api/roles/export"
|
|
99
|
+
filters={exportFilters}
|
|
100
|
+
search={searchTerm}
|
|
101
|
+
canExport={true}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</>
|
|
105
|
+
) : undefined
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
122
108
|
);
|
|
123
109
|
});
|
package/src/rbac/index.ts
CHANGED
|
@@ -136,4 +136,12 @@ export * from "./role-service";
|
|
|
136
136
|
export * from "./resource-service";
|
|
137
137
|
export * from "./resource-validator";
|
|
138
138
|
export { RoleListPage } from "./pages/role-list-page";
|
|
139
|
+
export { RoleFormPage } from "./pages/role-form-page";
|
|
140
|
+
export type {
|
|
141
|
+
RoleFormPageProps,
|
|
142
|
+
ActionLabelConfig,
|
|
143
|
+
} from "./pages/role-form-page";
|
|
144
|
+
export { ResourceListPage } from "./pages/resource-list-page";
|
|
145
|
+
export { ActionListPage } from "./pages/action-list-page";
|
|
146
|
+
export type { ActionListPageProps } from "./pages/action-list-page";
|
|
139
147
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Small fetch-error helpers for the RBAC management pages. Apps with richer
|
|
2
|
+
// error glue (persisted error logs, i18n) can wrap these pages and keep their
|
|
3
|
+
// own handling; the defaults below only surface the server message via toast.
|
|
4
|
+
|
|
5
|
+
export function errorMessage(error: unknown, fallback: string): string {
|
|
6
|
+
if (typeof error === "string" && error) return error;
|
|
7
|
+
if (error && typeof error === "object") {
|
|
8
|
+
const e = error as { error?: unknown; message?: unknown };
|
|
9
|
+
if (typeof e.error === "string" && e.error) return e.error;
|
|
10
|
+
if (typeof e.message === "string" && e.message) return e.message;
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function throwFetchError(
|
|
16
|
+
res: Response,
|
|
17
|
+
fallback: string,
|
|
18
|
+
): Promise<never> {
|
|
19
|
+
let data: unknown = null;
|
|
20
|
+
try {
|
|
21
|
+
data = await res.json();
|
|
22
|
+
} catch {
|
|
23
|
+
// non-JSON body — fall back to the provided message
|
|
24
|
+
}
|
|
25
|
+
throw new Error(errorMessage(data, fallback));
|
|
26
|
+
}
|