feat: add manager region invite switch

This commit is contained in:
zhx 2026-07-11 18:27:19 +08:00
parent c206c72612
commit 8ef5b0c08d
9 changed files with 173 additions and 37 deletions

View File

@ -8875,8 +8875,11 @@
},
"ApiPageManager": {
"type": "object",
"required": ["items", "page", "pageSize", "total"],
"required": ["items", "page", "pageSize", "total", "bdLeaderInviteScopePolicy"],
"properties": {
"bdLeaderInviteScopePolicy": {
"$ref": "#/components/schemas/ManagerRoleScopePolicy"
},
"items": {
"type": "array",
"items": {
@ -10112,6 +10115,19 @@
}
}
},
"ManagerRoleScopePolicy": {
"type": "object",
"required": ["baseScope", "regionExpansionConfigurable"],
"properties": {
"baseScope": {
"type": "string",
"enum": ["country", "region", "global"]
},
"regionExpansionConfigurable": {
"type": "boolean"
}
}
},
"Manager": {
"type": "object",
"required": ["userId"],
@ -10128,6 +10144,20 @@
"canAddBdLeader": {
"type": "boolean"
},
"canAddBdLeaderInRegion": {
"type": "boolean"
},
"bdLeaderInviteBaseScope": {
"type": "string",
"enum": ["country", "region", "global"]
},
"bdLeaderInviteEffectiveScope": {
"type": "string",
"enum": ["country", "region", "global"]
},
"bdLeaderRegionExpansionConfigurable": {
"type": "boolean"
},
"canAddSuperadmin": {
"type": "boolean"
},
@ -10638,6 +10668,9 @@
"canAddBdLeader": {
"type": "boolean"
},
"canAddBdLeaderInRegion": {
"type": "boolean"
},
"canAddSuperadmin": {
"type": "boolean"
},
@ -10680,6 +10713,9 @@
"canAddBdLeader": {
"type": "boolean"
},
"canAddBdLeaderInRegion": {
"type": "boolean"
},
"canAddSuperadmin": {
"type": "boolean"
},

View File

@ -69,6 +69,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
status: "active",
});
await createManager({
canAddBdLeaderInRegion: false,
canBlockUser: false,
canGrantBadge: false,
canGrantVip: true,
@ -76,6 +77,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
targetUserId: "1003",
});
await updateManager("1003", {
canAddBdLeaderInRegion: true,
canBlockUser: false,
canGrantVehicle: true,
canGrantVip: false,
@ -158,6 +160,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
expect(String(managerCreateUrl)).toContain("/api/v1/admin/managers");
expect(managerCreateInit?.method).toBe("POST");
expect(JSON.parse(String(managerCreateInit?.body))).toEqual({
canAddBdLeaderInRegion: false,
canBlockUser: false,
canGrantBadge: false,
canGrantVip: true,
@ -167,6 +170,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
expect(String(managerUpdateUrl)).toContain("/api/v1/admin/managers/1003");
expect(managerUpdateInit?.method).toBe("PUT");
expect(JSON.parse(String(managerUpdateInit?.body))).toEqual({
canAddBdLeaderInRegion: true,
canBlockUser: false,
canGrantVehicle: true,
canGrantVip: false,

View File

@ -17,29 +17,44 @@ export const managerPermissionFields = [
"canGrantVip",
"canUpdateUserLevel",
"canAddBdLeader",
"canAddBdLeaderInRegion",
"canAddAdmin",
"canAddSuperadmin",
"canBlockUser",
"canTransferUserCountry",
];
const emptyPermissions = () => Object.fromEntries(managerPermissionFields.map((field) => [field, true]));
const managerPermissionDefault = (field) => field !== "canAddBdLeaderInRegion";
const emptyManagerForm = () => ({ contact: "", targetUserId: "", ...emptyPermissions() });
const managerPermissionEnabled = (source, field) => {
const value = source?.[field];
return value === undefined || value === null ? managerPermissionDefault(field) : value !== false;
};
const emptyPermissions = () =>
Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionDefault(field)]));
const emptyManagerForm = (regionExpansionConfigurable = false) => ({
bdLeaderRegionExpansionConfigurable: regionExpansionConfigurable,
contact: "",
targetUserId: "",
...emptyPermissions(),
});
const managerPermissionPayload = (form) =>
Object.fromEntries(managerPermissionFields.map((field) => [field, form[field] !== false]));
Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(form, field)]));
const managerStatusPayload = (item, status) => ({
contact: item?.contact || "",
status,
...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])),
...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])),
});
const managerFormFromItem = (item) => ({
bdLeaderRegionExpansionConfigurable: item?.bdLeaderRegionExpansionConfigurable === true,
contact: item?.contact || "",
targetUserId: String(item?.userId || ""),
...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])),
...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])),
});
export function useHostManagersPage() {
@ -118,7 +133,7 @@ export function useHostManagersPage() {
});
showToast("经理已添加", "success");
}
setManagerForm(emptyManagerForm());
setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true));
setActiveAction("");
setEditingManager(null);
await reload();
@ -132,12 +147,12 @@ export function useHostManagersPage() {
const closeAction = () => {
setActiveAction("");
setEditingManager(null);
setManagerForm(emptyManagerForm());
setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true));
};
const openManagerForm = () => {
setEditingManager(null);
setManagerForm(emptyManagerForm());
setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true));
setActiveAction("manager");
};

View File

@ -22,6 +22,7 @@ const managerPermissionOptions = [
{ key: "canGrantVip", label: "VIP" },
{ key: "canUpdateUserLevel", label: "升级等级" },
{ key: "canAddBdLeader", label: "BD Leader" },
{ key: "canAddBdLeaderInRegion", label: "区域邀请 BD Leader", regionExpansionOnly: true },
{ key: "canAddAdmin", label: "Admin" },
{ key: "canAddSuperadmin", label: "Superadmin" },
{ key: "canBlockUser", label: "Block User" },
@ -201,20 +202,28 @@ function ManagerStatusSwitch({ item, page }) {
function ManagerPermissions({ item }) {
return (
<div className={styles.permissionList}>
{managerPermissionOptions.map((permission) => {
const enabled = item?.[permission.key] !== false;
return (
<span
className={
enabled ? styles.permissionPill : `${styles.permissionPill} ${styles.permissionPillOff}`
}
key={permission.key}
>
{permission.label}
<span>{enabled ? "开" : "关"}</span>
</span>
);
})}
{managerPermissionOptions
.filter(
(permission) =>
!permission.regionExpansionOnly || item?.bdLeaderRegionExpansionConfigurable === true,
)
.map((permission) => {
const enabled =
permission.key === "canAddBdLeaderInRegion"
? item?.[permission.key] === true
: item?.[permission.key] !== false;
return (
<span
className={
enabled ? styles.permissionPill : `${styles.permissionPill} ${styles.permissionPillOff}`
}
key={permission.key}
>
{permission.label}
<span>{enabled ? "开" : "关"}</span>
</span>
);
})}
</div>
);
}
@ -237,19 +246,28 @@ function ManagerActions({ item, page }) {
function PermissionSwitchGrid({ disabled, form, onChange }) {
return (
<div className={styles.permissionGrid}>
{managerPermissionOptions.map((permission) => (
<label className={styles.permissionSwitch} key={permission.key}>
<span>{permission.label}</span>
<AdminSwitch
checked={form?.[permission.key] !== false}
checkedLabel="开"
disabled={disabled}
label={permission.label}
uncheckedLabel="关"
onChange={(event) => onChange({ ...form, [permission.key]: event.target.checked })}
/>
</label>
))}
{managerPermissionOptions
.filter(
(permission) =>
!permission.regionExpansionOnly || form?.bdLeaderRegionExpansionConfigurable === true,
)
.map((permission) => (
<label className={styles.permissionSwitch} key={permission.key}>
<span>{permission.label}</span>
<AdminSwitch
checked={
permission.key === "canAddBdLeaderInRegion"
? form?.[permission.key] === true
: form?.[permission.key] !== false
}
checkedLabel="开"
disabled={disabled}
label={permission.label}
uncheckedLabel="关"
onChange={(event) => onChange({ ...form, [permission.key]: event.target.checked })}
/>
</label>
))}
</div>
);
}

View File

@ -17,7 +17,9 @@ afterEach(() => {
test("manager status column uses a switch and toggles through the page hook", () => {
const manager = managerFixture();
vi.mocked(useHostManagersPage).mockReturnValue(pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } }));
vi.mocked(useHostManagersPage).mockReturnValue(
pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } }),
);
render(<HostManagersPage />);
@ -31,6 +33,35 @@ test("manager status column uses a switch and toggles through the page hook", ()
expect(mocks.toggleManager).toHaveBeenCalledWith(manager, false);
});
test("manager form only shows the region invitation switch when backend policy allows configuration", () => {
vi.mocked(useHostManagersPage).mockReturnValue(
pageFixture({
activeAction: "manager",
managerForm: {
...pageFixture().managerForm,
bdLeaderRegionExpansionConfigurable: true,
canAddBdLeaderInRegion: false,
},
}),
);
const { rerender } = render(<HostManagersPage />);
expect(screen.getByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeChecked();
vi.mocked(useHostManagersPage).mockReturnValue(
pageFixture({
activeAction: "manager",
managerForm: {
...pageFixture().managerForm,
bdLeaderRegionExpansionConfigurable: false,
canAddBdLeaderInRegion: false,
},
}),
);
rerender(<HostManagersPage />);
expect(screen.queryByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeInTheDocument();
});
function pageFixture(patch = {}) {
return {
abilities: {
@ -50,8 +81,10 @@ function pageFixture(patch = {}) {
loadingAction: "",
loadingRegions: false,
managerForm: {
bdLeaderRegionExpansionConfigurable: true,
canAddAdmin: true,
canAddBdLeader: true,
canAddBdLeaderInRegion: false,
canAddSuperadmin: true,
canBlockUser: true,
canGrantAvatarFrame: true,
@ -86,6 +119,8 @@ function managerFixture() {
bdLeaderCount: 3,
canAddAdmin: true,
canAddBdLeader: false,
canAddBdLeaderInRegion: false,
bdLeaderRegionExpansionConfigurable: true,
canAddSuperadmin: true,
canBlockUser: true,
canGrantAvatarFrame: true,

View File

@ -132,6 +132,7 @@ test("manager schema defaults vip grant permission on and preserves explicit fal
expect(disabledResult.success).toBe(true);
if (defaultResult.success) {
expect(defaultResult.data.canGrantVip).toBe(true);
expect(defaultResult.data.canAddBdLeaderInRegion).toBe(false);
}
if (disabledResult.success) {
expect(disabledResult.data.canGrantVip).toBe(false);

View File

@ -112,6 +112,7 @@ export const createCoinSellerSchema = commandContactSchema.extend({
export const createManagerSchema = z.object({
canAddAdmin: z.boolean().optional().default(true),
canAddBdLeader: z.boolean().optional().default(true),
canAddBdLeaderInRegion: z.boolean().optional().default(false),
canAddSuperadmin: z.boolean().optional().default(true),
canBlockUser: z.boolean().optional().default(true),
canGrantAvatarFrame: z.boolean().optional().default(true),

View File

@ -4677,6 +4677,7 @@ export interface components {
total: number;
};
ApiPageManager: {
bdLeaderInviteScopePolicy: components["schemas"]["ManagerRoleScopePolicy"];
items: components["schemas"]["Manager"][];
page: number;
pageSize: number;
@ -5056,11 +5057,22 @@ export interface components {
status?: string;
updatedAtMs?: number;
};
ManagerRoleScopePolicy: {
/** @enum {string} */
baseScope: "country" | "region" | "global";
regionExpansionConfigurable: boolean;
};
Manager: {
avatar?: string;
bdLeaderCount?: number;
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
canAddBdLeaderInRegion?: boolean;
/** @enum {string} */
bdLeaderInviteBaseScope?: "country" | "region" | "global";
/** @enum {string} */
bdLeaderInviteEffectiveScope?: "country" | "region" | "global";
bdLeaderRegionExpansionConfigurable?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@ -5232,6 +5244,7 @@ export interface components {
CreateManagerPayload: {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@ -5246,6 +5259,7 @@ export interface components {
UpdateManagerPayload: {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;

View File

@ -1,10 +1,16 @@
export interface ApiPage<TItem> {
bdLeaderInviteScopePolicy?: ManagerRoleScopePolicy;
items: TItem[];
page: number;
pageSize: number;
total: number;
}
export interface ManagerRoleScopePolicy {
baseScope: "country" | "region" | "global";
regionExpansionConfigurable: boolean;
}
export interface ApiList<TItem> {
items: TItem[];
total: number;
@ -756,6 +762,10 @@ export interface ManagerDto {
bdLeaderCount?: number;
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
canAddBdLeaderInRegion?: boolean;
bdLeaderInviteBaseScope?: "country" | "region" | "global";
bdLeaderInviteEffectiveScope?: "country" | "region" | "global";
bdLeaderRegionExpansionConfigurable?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@ -779,6 +789,7 @@ export interface ManagerDto {
export interface CreateManagerPayload {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@ -794,6 +805,7 @@ export interface CreateManagerPayload {
export interface UpdateManagerPayload {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;