@goplusvn/core 0.1.30 → 0.1.32
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 +31 -0
- package/package.json +1 -2
- package/src/auth/index.ts +1 -1
- package/src/crud/lib/permissions.ts +1 -1
- package/src/rbac/index.ts +1 -2
- package/src/rbac/permissions.ts +1 -2
- package/src/types/index.ts +9 -0
- package/src/ui/auth/auth-bridge.tsx +29 -31
- package/src/ui/forms/multi-select.tsx +47 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.32 — Gỡ NextAuth khỏi core (BREAKING cho app chưa migrate)
|
|
4
|
+
|
|
5
|
+
Core không còn phụ thuộc next-auth (bỏ peer dependency):
|
|
6
|
+
|
|
7
|
+
- **auth-bridge**: gỡ `nextAuthBridgeClient` + import `next-auth/react`.
|
|
8
|
+
Mặc định khi KHÔNG mount `AuthBridgeProvider` giờ là fallback
|
|
9
|
+
unauthenticated (cảnh báo console) thay vì NextAuth. App PHẢI mount
|
|
10
|
+
provider với client auth của mình (vinhhoa: betterAuthBridgeClient).
|
|
11
|
+
- **Type `Session`**: các chữ ký RBAC (`checkPermission`,
|
|
12
|
+
`getUserPermissions`, `hasRole`…) đổi từ `Session` của next-auth sang
|
|
13
|
+
`Session` cấu trúc `{ user?: unknown }` export từ `@goerp/core/types` —
|
|
14
|
+
session của mọi auth gán được, app không phải sửa call-site.
|
|
15
|
+
- `auth/proxy-gate` (gate JWT NextAuth cho app cũ) GIỮ NGUYÊN nhưng app nào
|
|
16
|
+
dùng phải tự cài next-auth (giờ chỉ là devDependency của core).
|
|
17
|
+
|
|
18
|
+
## 0.1.31 — MultiSelect: mặc định 1 DÒNG (hết lệch hàng toolbar)
|
|
19
|
+
|
|
20
|
+
Wrap-mặc-định (0.1.27) làm trigger CAO LÊN khi chọn ≥2 option → lệch hàng
|
|
21
|
+
toolbar (các control h-9 bên cạnh). Sửa:
|
|
22
|
+
|
|
23
|
+
- Đảo mặc định `singleLine = true`; muốn wrap thì truyền `singleLine={false}`
|
|
24
|
+
(chỉ hợp form rộng, không phải toolbar).
|
|
25
|
+
- Chế độ 1 dòng hiện **1 chip đầu + badge "+n"** (thay vì bóp nhiều chip đến
|
|
26
|
+
mất chữ trong trigger hẹp); title tooltip cho label đầy đủ + danh sách mục
|
|
27
|
+
còn lại.
|
|
28
|
+
- Chip ở chế độ 1 dòng KHÔNG có nút × riêng (chật) — bỏ chọn qua dropdown
|
|
29
|
+
hoặc nút × xóa-tất-cả; chế độ wrap giữ × từng chip như cũ.
|
|
30
|
+
|
|
31
|
+
Đã test trên vinhhoa (link file: local): toolbar đơn bán hàng, sheet Bộ lọc,
|
|
32
|
+
báo cáo sổ quỹ — 3 filter cùng h-9, không còn đẩy lệch hàng.
|
|
33
|
+
|
|
3
34
|
## 0.1.30 — Gỡ menu "Thiết bị & phiên đăng nhập" khỏi UserDropdown
|
|
4
35
|
|
|
5
36
|
Trang phiên đăng nhập chuyển thành trang QUẢN TRỊ (gate quyền) — lối vào đúng
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplusvn/core",
|
|
3
3
|
"description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.32",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org",
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"next": ">=14.0.0",
|
|
62
|
-
"next-auth": "4.24.11",
|
|
63
62
|
"react": "^18.0.0 || ^19.0.0",
|
|
64
63
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
65
64
|
},
|
package/src/auth/index.ts
CHANGED
package/src/rbac/index.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Core RBAC (Role-Based Access Control) utilities for GoERP platform
|
|
3
3
|
// Consolidated from packages/shared/rbac
|
|
4
4
|
|
|
5
|
-
import type { Permission } from "../types";
|
|
6
|
-
import type { Session } from "next-auth";
|
|
5
|
+
import type { Permission, Session } from "../types";
|
|
7
6
|
import { logger } from "../utils";
|
|
8
7
|
|
|
9
8
|
// ============================================================================
|
package/src/rbac/permissions.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Server-only permission utilities - no client/UI imports
|
|
3
3
|
// Use this in Server Components and API routes to avoid bundling client code
|
|
4
4
|
|
|
5
|
-
import type { Permission } from "../types";
|
|
6
|
-
import type { Session } from "next-auth";
|
|
5
|
+
import type { Permission, Session } from "../types";
|
|
7
6
|
|
|
8
7
|
// ============================================================================
|
|
9
8
|
// Action Mapping
|
package/src/types/index.ts
CHANGED
|
@@ -13,6 +13,15 @@ import type { z } from "zod";
|
|
|
13
13
|
export const supportedLocales = ["vi", "en"] as const;
|
|
14
14
|
export type LocaleType = (typeof supportedLocales)[number];
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Session cấu trúc — thay `import type { Session } from "next-auth"` (đã gỡ
|
|
18
|
+
* NextAuth khỏi core). Core chỉ cần `session?.user` rồi cast ExtendedUser;
|
|
19
|
+
* session của mọi auth (NextAuth cũ, Better Auth shim) đều gán được.
|
|
20
|
+
*/
|
|
21
|
+
export interface Session {
|
|
22
|
+
user?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
export interface SearchResultItem {
|
|
17
26
|
id: string;
|
|
18
27
|
title: string;
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// AUTH BRIDGE — lớp trừu tượng client-auth
|
|
4
|
-
//
|
|
5
|
-
// hiện tại không đổi hành vi. App migrate sang Better Auth chỉ cần mount:
|
|
3
|
+
// AUTH BRIDGE — lớp trừu tượng client-auth: core KHÔNG phụ thuộc thư viện
|
|
4
|
+
// auth nào. App PHẢI mount provider với client của mình, ví dụ Better Auth:
|
|
6
5
|
//
|
|
7
6
|
// <AuthBridgeProvider client={betterAuthBridgeClient}>
|
|
8
7
|
//
|
|
9
|
-
// với client map từ createAuthClient()
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// với client map từ createAuthClient() (useSession / signIn.email / signOut).
|
|
9
|
+
// LƯU Ý: client phải là hằng số suốt vòng đời app (rules of hooks — useSession
|
|
10
|
+
// của bridge được gọi như một hook). Từ 0.1.32 core đã GỠ next-auth: không
|
|
11
|
+
// còn nextAuthBridgeClient mặc định; app còn dùng NextAuth tự viết client
|
|
12
|
+
// (map useSession/signIn/signOut của next-auth/react vào AuthBridgeClient).
|
|
12
13
|
|
|
13
14
|
import { createContext, useContext } from "react";
|
|
14
15
|
import type { ReactNode } from "react";
|
|
15
|
-
import {
|
|
16
|
-
signIn as nextAuthSignIn,
|
|
17
|
-
signOut as nextAuthSignOut,
|
|
18
|
-
useSession as nextAuthUseSession,
|
|
19
|
-
} from "next-auth/react";
|
|
20
16
|
|
|
21
17
|
export interface AuthSessionState {
|
|
22
18
|
/** Session object (shape do app quyết — core chỉ đọc user.id/permissions...) */
|
|
@@ -37,31 +33,33 @@ export interface AuthBridgeClient {
|
|
|
37
33
|
signOut: (options?: { callbackUrl?: string }) => Promise<void>;
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const MISSING_PROVIDER_MSG =
|
|
37
|
+
"[goerp-core] AuthBridgeProvider chưa được mount — bọc app trong " +
|
|
38
|
+
'<AuthBridgeProvider client={...}> với client auth của app (xem ui/auth).';
|
|
39
|
+
|
|
40
|
+
let warnedMissingProvider = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fallback khi app QUÊN mount AuthBridgeProvider: session luôn
|
|
44
|
+
* unauthenticated (cảnh báo console 1 lần), signIn trả lỗi rõ ràng.
|
|
45
|
+
*/
|
|
46
|
+
const missingProviderClient: AuthBridgeClient = {
|
|
42
47
|
useSession: () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
update: () => s.update(),
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
signInWithCredentials: async ({ email, password }) => {
|
|
52
|
-
const result = await nextAuthSignIn("credentials", {
|
|
53
|
-
redirect: false,
|
|
54
|
-
email,
|
|
55
|
-
password,
|
|
56
|
-
});
|
|
57
|
-
return { error: result?.error ?? null };
|
|
48
|
+
if (!warnedMissingProvider) {
|
|
49
|
+
warnedMissingProvider = true;
|
|
50
|
+
console.warn(MISSING_PROVIDER_MSG);
|
|
51
|
+
}
|
|
52
|
+
return { data: null, status: "unauthenticated", update: async () => null };
|
|
58
53
|
},
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
signInWithCredentials: async () => ({ error: MISSING_PROVIDER_MSG }),
|
|
55
|
+
signOut: async () => {
|
|
56
|
+
console.warn(MISSING_PROVIDER_MSG);
|
|
61
57
|
},
|
|
62
58
|
};
|
|
63
59
|
|
|
64
|
-
const AuthBridgeContext = createContext<AuthBridgeClient>(
|
|
60
|
+
const AuthBridgeContext = createContext<AuthBridgeClient>(
|
|
61
|
+
missingProviderClient,
|
|
62
|
+
);
|
|
65
63
|
|
|
66
64
|
export function AuthBridgeProvider({
|
|
67
65
|
client,
|
|
@@ -47,9 +47,11 @@ interface MultiSelectProps {
|
|
|
47
47
|
*/
|
|
48
48
|
selectedFirst?: boolean;
|
|
49
49
|
/**
|
|
50
|
-
* Mặc định
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Mặc định GIỮ 1 DÒNG cố định: hiện 1 chip đầu (cắt chữ, title=label đầy
|
|
51
|
+
* đủ) + badge "+n" (title=các mục còn lại) — trigger không đổi chiều cao,
|
|
52
|
+
* không lệch hàng toolbar; chip không có nút × riêng (bỏ chọn qua dropdown
|
|
53
|
+
* hoặc × xóa-tất-cả). Đặt false để chip WRAP xuống dòng kèm × từng chip
|
|
54
|
+
* (trigger cao lên — chỉ hợp form rộng rãi, không phải toolbar).
|
|
53
55
|
*/
|
|
54
56
|
singleLine?: boolean;
|
|
55
57
|
/** Đóng dropdown ngay sau mỗi lần chọn (mặc định giữ mở để chọn tiếp). */
|
|
@@ -179,7 +181,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
179
181
|
disabled = false,
|
|
180
182
|
responsive = false,
|
|
181
183
|
selectedFirst = false,
|
|
182
|
-
singleLine =
|
|
184
|
+
singleLine = true,
|
|
183
185
|
closeOnSelect = false,
|
|
184
186
|
deduplicateOptions = false,
|
|
185
187
|
chipColor = "colorful",
|
|
@@ -562,12 +564,16 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
562
564
|
[updateValue, value],
|
|
563
565
|
);
|
|
564
566
|
|
|
567
|
+
// singleLine: chỉ hiện 1 chip + "+n" — nhiều chip trong trigger hẹp sẽ bị
|
|
568
|
+
// bóp đến mất chữ (chỉ còn dấu ×), 1 chip cắt chữ + đếm luôn đọc được
|
|
569
|
+
const chipLimit = singleLine ? 1 : responsiveSettings.maxCount;
|
|
570
|
+
|
|
565
571
|
const clearExtraOptions = useCallback(
|
|
566
572
|
(e: React.MouseEvent) => {
|
|
567
573
|
e.stopPropagation();
|
|
568
|
-
updateValue(value.slice(0,
|
|
574
|
+
updateValue(value.slice(0, chipLimit));
|
|
569
575
|
},
|
|
570
|
-
[value,
|
|
576
|
+
[value, chipLimit, updateValue],
|
|
571
577
|
);
|
|
572
578
|
|
|
573
579
|
// Imperative API via ref
|
|
@@ -604,7 +610,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
604
610
|
|
|
605
611
|
const isAllSelected =
|
|
606
612
|
!hasAsync && options.length > 1 && value.length === options.length;
|
|
607
|
-
const visibleChips = selectedOptions.slice(0,
|
|
613
|
+
const visibleChips = selectedOptions.slice(0, chipLimit);
|
|
608
614
|
const extraCount = value.length - visibleChips.length;
|
|
609
615
|
|
|
610
616
|
const visibleOptions = filteredOptions.slice(
|
|
@@ -667,43 +673,58 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
667
673
|
</Badge>
|
|
668
674
|
) : (
|
|
669
675
|
<>
|
|
676
|
+
{/* singleLine: chip KHÔNG có nút × riêng (trigger hẹp, chữ cần
|
|
677
|
+
chỗ) — bỏ chọn bằng dropdown hoặc × xóa-tất-cả */}
|
|
670
678
|
{visibleChips.map((opt, chipIndex) => (
|
|
671
679
|
<Badge
|
|
672
680
|
key={String(opt.value)}
|
|
673
681
|
variant="secondary"
|
|
682
|
+
title={opt.label}
|
|
674
683
|
className={cn(
|
|
675
|
-
"min-w-0
|
|
684
|
+
"min-w-0 shrink gap-1 font-normal",
|
|
685
|
+
singleLine ? "max-w-full" : "max-w-[8.5rem] pr-1",
|
|
676
686
|
chipColor === "neutral"
|
|
677
687
|
? "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary"
|
|
678
688
|
: CHIP_COLORS[chipIndex % CHIP_COLORS.length],
|
|
679
689
|
)}
|
|
680
690
|
>
|
|
681
691
|
<span className="truncate">{opt.label}</span>
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
692
|
+
{!singleLine && (
|
|
693
|
+
<span
|
|
694
|
+
role="button"
|
|
695
|
+
aria-label={`Bỏ chọn ${opt.label}`}
|
|
696
|
+
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm opacity-60 transition-opacity hover:opacity-100"
|
|
697
|
+
onClick={(e) => handleRemove(opt.value, e)}
|
|
698
|
+
>
|
|
699
|
+
<X className="h-3 w-3" />
|
|
700
|
+
</span>
|
|
701
|
+
)}
|
|
690
702
|
</Badge>
|
|
691
703
|
))}
|
|
692
704
|
{extraCount > 0 && (
|
|
693
705
|
<Badge
|
|
694
706
|
variant="secondary"
|
|
695
|
-
|
|
707
|
+
title={selectedOptions
|
|
708
|
+
.slice(chipLimit)
|
|
709
|
+
.map((o) => o.label)
|
|
710
|
+
.join(", ")}
|
|
711
|
+
className={cn(
|
|
712
|
+
"shrink-0 gap-1 border-transparent bg-secondary font-normal hover:bg-secondary",
|
|
713
|
+
!singleLine && "pr-1",
|
|
714
|
+
)}
|
|
696
715
|
>
|
|
697
716
|
+{extraCount}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
717
|
+
{!singleLine && (
|
|
718
|
+
<span
|
|
719
|
+
role="button"
|
|
720
|
+
aria-label="Bỏ các mục vượt quá"
|
|
721
|
+
title="Bỏ các mục vượt quá"
|
|
722
|
+
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted-foreground/20 hover:text-foreground"
|
|
723
|
+
onClick={clearExtraOptions}
|
|
724
|
+
>
|
|
725
|
+
<X className="h-3 w-3" />
|
|
726
|
+
</span>
|
|
727
|
+
)}
|
|
707
728
|
</Badge>
|
|
708
729
|
)}
|
|
709
730
|
</>
|