feat: rename manager cross-region permission

This commit is contained in:
zhx 2026-07-11 20:36:42 +08:00
parent 8ef5b0c08d
commit f597cb6110
9 changed files with 116 additions and 41 deletions

View File

@ -8875,9 +8875,13 @@
}, },
"ApiPageManager": { "ApiPageManager": {
"type": "object", "type": "object",
"required": ["items", "page", "pageSize", "total", "bdLeaderInviteScopePolicy"], "required": ["items", "page", "pageSize", "total", "managerOperationScopePolicy"],
"properties": { "properties": {
"bdLeaderInviteScopePolicy": { "bdLeaderInviteScopePolicy": {
"$ref": "#/components/schemas/ManagerRoleScopePolicy",
"deprecated": true
},
"managerOperationScopePolicy": {
"$ref": "#/components/schemas/ManagerRoleScopePolicy" "$ref": "#/components/schemas/ManagerRoleScopePolicy"
}, },
"items": { "items": {
@ -10117,12 +10121,16 @@
}, },
"ManagerRoleScopePolicy": { "ManagerRoleScopePolicy": {
"type": "object", "type": "object",
"required": ["baseScope", "regionExpansionConfigurable"], "required": ["baseScope", "expandedScope", "regionExpansionConfigurable"],
"properties": { "properties": {
"baseScope": { "baseScope": {
"type": "string", "type": "string",
"enum": ["country", "region", "global"] "enum": ["country", "region", "global"]
}, },
"expandedScope": {
"type": "string",
"enum": ["country", "region", "global"]
},
"regionExpansionConfigurable": { "regionExpansionConfigurable": {
"type": "boolean" "type": "boolean"
} }
@ -10145,17 +10153,35 @@
"type": "boolean" "type": "boolean"
}, },
"canAddBdLeaderInRegion": { "canAddBdLeaderInRegion": {
"type": "boolean",
"deprecated": true
},
"canManageCrossRegion": {
"type": "boolean" "type": "boolean"
}, },
"bdLeaderInviteBaseScope": { "bdLeaderInviteBaseScope": {
"type": "string", "type": "string",
"enum": ["country", "region", "global"] "enum": ["country", "region", "global"],
"deprecated": true
}, },
"bdLeaderInviteEffectiveScope": { "bdLeaderInviteEffectiveScope": {
"type": "string", "type": "string",
"enum": ["country", "region", "global"] "enum": ["country", "region", "global"],
"deprecated": true
}, },
"bdLeaderRegionExpansionConfigurable": { "bdLeaderRegionExpansionConfigurable": {
"type": "boolean",
"deprecated": true
},
"managerOperationBaseScope": {
"type": "string",
"enum": ["country", "region", "global"]
},
"managerOperationEffectiveScope": {
"type": "string",
"enum": ["country", "region", "global"]
},
"managerCrossRegionConfigurable": {
"type": "boolean" "type": "boolean"
}, },
"canAddSuperadmin": { "canAddSuperadmin": {
@ -10669,6 +10695,10 @@
"type": "boolean" "type": "boolean"
}, },
"canAddBdLeaderInRegion": { "canAddBdLeaderInRegion": {
"type": "boolean",
"deprecated": true
},
"canManageCrossRegion": {
"type": "boolean" "type": "boolean"
}, },
"canAddSuperadmin": { "canAddSuperadmin": {
@ -10714,6 +10744,10 @@
"type": "boolean" "type": "boolean"
}, },
"canAddBdLeaderInRegion": { "canAddBdLeaderInRegion": {
"type": "boolean",
"deprecated": true
},
"canManageCrossRegion": {
"type": "boolean" "type": "boolean"
}, },
"canAddSuperadmin": { "canAddSuperadmin": {

View File

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

View File

@ -17,25 +17,28 @@ export const managerPermissionFields = [
"canGrantVip", "canGrantVip",
"canUpdateUserLevel", "canUpdateUserLevel",
"canAddBdLeader", "canAddBdLeader",
"canAddBdLeaderInRegion", "canManageCrossRegion",
"canAddAdmin", "canAddAdmin",
"canAddSuperadmin", "canAddSuperadmin",
"canBlockUser", "canBlockUser",
"canTransferUserCountry", "canTransferUserCountry",
]; ];
const managerPermissionDefault = (field) => field !== "canAddBdLeaderInRegion"; const managerPermissionDefault = (field) => field !== "canManageCrossRegion";
const managerPermissionEnabled = (source, field) => { const managerPermissionEnabled = (source, field) => {
const value = source?.[field]; const value =
field === "canManageCrossRegion"
? (source?.canManageCrossRegion ?? source?.canAddBdLeaderInRegion)
: source?.[field];
return value === undefined || value === null ? managerPermissionDefault(field) : value !== false; return value === undefined || value === null ? managerPermissionDefault(field) : value !== false;
}; };
const emptyPermissions = () => const emptyPermissions = () =>
Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionDefault(field)])); Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionDefault(field)]));
const emptyManagerForm = (regionExpansionConfigurable = false) => ({ const emptyManagerForm = (crossRegionConfigurable = false) => ({
bdLeaderRegionExpansionConfigurable: regionExpansionConfigurable, managerCrossRegionConfigurable: crossRegionConfigurable,
contact: "", contact: "",
targetUserId: "", targetUserId: "",
...emptyPermissions(), ...emptyPermissions(),
@ -51,7 +54,8 @@ const managerStatusPayload = (item, status) => ({
}); });
const managerFormFromItem = (item) => ({ const managerFormFromItem = (item) => ({
bdLeaderRegionExpansionConfigurable: item?.bdLeaderRegionExpansionConfigurable === true, managerCrossRegionConfigurable:
item?.managerCrossRegionConfigurable === true || item?.bdLeaderRegionExpansionConfigurable === true,
contact: item?.contact || "", contact: item?.contact || "",
targetUserId: String(item?.userId || ""), targetUserId: String(item?.userId || ""),
...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])), ...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])),
@ -133,7 +137,7 @@ export function useHostManagersPage() {
}); });
showToast("经理已添加", "success"); showToast("经理已添加", "success");
} }
setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true)); setManagerForm(emptyManagerForm(managerCrossRegionConfigurable(data)));
setActiveAction(""); setActiveAction("");
setEditingManager(null); setEditingManager(null);
await reload(); await reload();
@ -147,12 +151,12 @@ export function useHostManagersPage() {
const closeAction = () => { const closeAction = () => {
setActiveAction(""); setActiveAction("");
setEditingManager(null); setEditingManager(null);
setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true)); setManagerForm(emptyManagerForm(managerCrossRegionConfigurable(data)));
}; };
const openManagerForm = () => { const openManagerForm = () => {
setEditingManager(null); setEditingManager(null);
setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true)); setManagerForm(emptyManagerForm(managerCrossRegionConfigurable(data)));
setActiveAction("manager"); setActiveAction("manager");
}; };
@ -206,3 +210,10 @@ export function useHostManagersPage() {
userFilter, userFilter,
}; };
} }
function managerCrossRegionConfigurable(data) {
return (
data?.managerOperationScopePolicy?.regionExpansionConfigurable === true ||
data?.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true
);
}

View File

@ -22,7 +22,7 @@ const managerPermissionOptions = [
{ key: "canGrantVip", label: "VIP" }, { key: "canGrantVip", label: "VIP" },
{ key: "canUpdateUserLevel", label: "升级等级" }, { key: "canUpdateUserLevel", label: "升级等级" },
{ key: "canAddBdLeader", label: "BD Leader" }, { key: "canAddBdLeader", label: "BD Leader" },
{ key: "canAddBdLeaderInRegion", label: "区域邀请 BD Leader", regionExpansionOnly: true }, { key: "canManageCrossRegion", label: "跨区经理", crossRegionOnly: true },
{ key: "canAddAdmin", label: "Admin" }, { key: "canAddAdmin", label: "Admin" },
{ key: "canAddSuperadmin", label: "Superadmin" }, { key: "canAddSuperadmin", label: "Superadmin" },
{ key: "canBlockUser", label: "Block User" }, { key: "canBlockUser", label: "Block User" },
@ -205,12 +205,14 @@ function ManagerPermissions({ item }) {
{managerPermissionOptions {managerPermissionOptions
.filter( .filter(
(permission) => (permission) =>
!permission.regionExpansionOnly || item?.bdLeaderRegionExpansionConfigurable === true, !permission.crossRegionOnly ||
item?.managerCrossRegionConfigurable === true ||
item?.bdLeaderRegionExpansionConfigurable === true,
) )
.map((permission) => { .map((permission) => {
const enabled = const enabled =
permission.key === "canAddBdLeaderInRegion" permission.key === "canManageCrossRegion"
? item?.[permission.key] === true ? (item?.canManageCrossRegion ?? item?.canAddBdLeaderInRegion) === true
: item?.[permission.key] !== false; : item?.[permission.key] !== false;
return ( return (
<span <span
@ -249,14 +251,14 @@ function PermissionSwitchGrid({ disabled, form, onChange }) {
{managerPermissionOptions {managerPermissionOptions
.filter( .filter(
(permission) => (permission) =>
!permission.regionExpansionOnly || form?.bdLeaderRegionExpansionConfigurable === true, !permission.crossRegionOnly || form?.managerCrossRegionConfigurable === true,
) )
.map((permission) => ( .map((permission) => (
<label className={styles.permissionSwitch} key={permission.key}> <label className={styles.permissionSwitch} key={permission.key}>
<span>{permission.label}</span> <span>{permission.label}</span>
<AdminSwitch <AdminSwitch
checked={ checked={
permission.key === "canAddBdLeaderInRegion" permission.key === "canManageCrossRegion"
? form?.[permission.key] === true ? form?.[permission.key] === true
: form?.[permission.key] !== false : form?.[permission.key] !== false
} }

View File

@ -33,33 +33,33 @@ test("manager status column uses a switch and toggles through the page hook", ()
expect(mocks.toggleManager).toHaveBeenCalledWith(manager, false); expect(mocks.toggleManager).toHaveBeenCalledWith(manager, false);
}); });
test("manager form only shows the region invitation switch when backend policy allows configuration", () => { test("manager form only shows the cross-region manager switch when backend policy allows configuration", () => {
vi.mocked(useHostManagersPage).mockReturnValue( vi.mocked(useHostManagersPage).mockReturnValue(
pageFixture({ pageFixture({
activeAction: "manager", activeAction: "manager",
managerForm: { managerForm: {
...pageFixture().managerForm, ...pageFixture().managerForm,
bdLeaderRegionExpansionConfigurable: true, managerCrossRegionConfigurable: true,
canAddBdLeaderInRegion: false, canManageCrossRegion: false,
}, },
}), }),
); );
const { rerender } = render(<HostManagersPage />); const { rerender } = render(<HostManagersPage />);
expect(screen.getByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeChecked(); expect(screen.getByRole("switch", { name: "跨区经理" })).not.toBeChecked();
vi.mocked(useHostManagersPage).mockReturnValue( vi.mocked(useHostManagersPage).mockReturnValue(
pageFixture({ pageFixture({
activeAction: "manager", activeAction: "manager",
managerForm: { managerForm: {
...pageFixture().managerForm, ...pageFixture().managerForm,
bdLeaderRegionExpansionConfigurable: false, managerCrossRegionConfigurable: false,
canAddBdLeaderInRegion: false, canManageCrossRegion: false,
}, },
}), }),
); );
rerender(<HostManagersPage />); rerender(<HostManagersPage />);
expect(screen.queryByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeInTheDocument(); expect(screen.queryByRole("switch", { name: "跨区经理" })).not.toBeInTheDocument();
}); });
function pageFixture(patch = {}) { function pageFixture(patch = {}) {
@ -81,10 +81,10 @@ function pageFixture(patch = {}) {
loadingAction: "", loadingAction: "",
loadingRegions: false, loadingRegions: false,
managerForm: { managerForm: {
bdLeaderRegionExpansionConfigurable: true, managerCrossRegionConfigurable: true,
canAddAdmin: true, canAddAdmin: true,
canAddBdLeader: true, canAddBdLeader: true,
canAddBdLeaderInRegion: false, canManageCrossRegion: false,
canAddSuperadmin: true, canAddSuperadmin: true,
canBlockUser: true, canBlockUser: true,
canGrantAvatarFrame: true, canGrantAvatarFrame: true,
@ -119,8 +119,8 @@ function managerFixture() {
bdLeaderCount: 3, bdLeaderCount: 3,
canAddAdmin: true, canAddAdmin: true,
canAddBdLeader: false, canAddBdLeader: false,
canAddBdLeaderInRegion: false, canManageCrossRegion: false,
bdLeaderRegionExpansionConfigurable: true, managerCrossRegionConfigurable: true,
canAddSuperadmin: true, canAddSuperadmin: true,
canBlockUser: true, canBlockUser: true,
canGrantAvatarFrame: true, canGrantAvatarFrame: true,

View File

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

View File

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

View File

@ -4677,7 +4677,9 @@ export interface components {
total: number; total: number;
}; };
ApiPageManager: { ApiPageManager: {
bdLeaderInviteScopePolicy: components["schemas"]["ManagerRoleScopePolicy"]; /** @deprecated */
bdLeaderInviteScopePolicy?: components["schemas"]["ManagerRoleScopePolicy"];
managerOperationScopePolicy: components["schemas"]["ManagerRoleScopePolicy"];
items: components["schemas"]["Manager"][]; items: components["schemas"]["Manager"][];
page: number; page: number;
pageSize: number; pageSize: number;
@ -5060,6 +5062,8 @@ export interface components {
ManagerRoleScopePolicy: { ManagerRoleScopePolicy: {
/** @enum {string} */ /** @enum {string} */
baseScope: "country" | "region" | "global"; baseScope: "country" | "region" | "global";
/** @enum {string} */
expandedScope: "country" | "region" | "global";
regionExpansionConfigurable: boolean; regionExpansionConfigurable: boolean;
}; };
Manager: { Manager: {
@ -5067,12 +5071,26 @@ export interface components {
bdLeaderCount?: number; bdLeaderCount?: number;
canAddAdmin?: boolean; canAddAdmin?: boolean;
canAddBdLeader?: boolean; canAddBdLeader?: boolean;
/** @deprecated */
canAddBdLeaderInRegion?: boolean; canAddBdLeaderInRegion?: boolean;
/** @enum {string} */ canManageCrossRegion?: boolean;
/**
* @deprecated
* @enum {string}
*/
bdLeaderInviteBaseScope?: "country" | "region" | "global"; bdLeaderInviteBaseScope?: "country" | "region" | "global";
/** @enum {string} */ /**
* @deprecated
* @enum {string}
*/
bdLeaderInviteEffectiveScope?: "country" | "region" | "global"; bdLeaderInviteEffectiveScope?: "country" | "region" | "global";
/** @deprecated */
bdLeaderRegionExpansionConfigurable?: boolean; bdLeaderRegionExpansionConfigurable?: boolean;
/** @enum {string} */
managerOperationBaseScope?: "country" | "region" | "global";
/** @enum {string} */
managerOperationEffectiveScope?: "country" | "region" | "global";
managerCrossRegionConfigurable?: boolean;
canAddSuperadmin?: boolean; canAddSuperadmin?: boolean;
canBlockUser?: boolean; canBlockUser?: boolean;
canGrantAvatarFrame?: boolean; canGrantAvatarFrame?: boolean;
@ -5244,7 +5262,9 @@ export interface components {
CreateManagerPayload: { CreateManagerPayload: {
canAddAdmin?: boolean; canAddAdmin?: boolean;
canAddBdLeader?: boolean; canAddBdLeader?: boolean;
/** @deprecated */
canAddBdLeaderInRegion?: boolean; canAddBdLeaderInRegion?: boolean;
canManageCrossRegion?: boolean;
canAddSuperadmin?: boolean; canAddSuperadmin?: boolean;
canBlockUser?: boolean; canBlockUser?: boolean;
canGrantAvatarFrame?: boolean; canGrantAvatarFrame?: boolean;
@ -5259,7 +5279,9 @@ export interface components {
UpdateManagerPayload: { UpdateManagerPayload: {
canAddAdmin?: boolean; canAddAdmin?: boolean;
canAddBdLeader?: boolean; canAddBdLeader?: boolean;
/** @deprecated */
canAddBdLeaderInRegion?: boolean; canAddBdLeaderInRegion?: boolean;
canManageCrossRegion?: boolean;
canAddSuperadmin?: boolean; canAddSuperadmin?: boolean;
canBlockUser?: boolean; canBlockUser?: boolean;
canGrantAvatarFrame?: boolean; canGrantAvatarFrame?: boolean;

View File

@ -1,4 +1,5 @@
export interface ApiPage<TItem> { export interface ApiPage<TItem> {
managerOperationScopePolicy?: ManagerRoleScopePolicy;
bdLeaderInviteScopePolicy?: ManagerRoleScopePolicy; bdLeaderInviteScopePolicy?: ManagerRoleScopePolicy;
items: TItem[]; items: TItem[];
page: number; page: number;
@ -8,6 +9,7 @@ export interface ApiPage<TItem> {
export interface ManagerRoleScopePolicy { export interface ManagerRoleScopePolicy {
baseScope: "country" | "region" | "global"; baseScope: "country" | "region" | "global";
expandedScope: "country" | "region" | "global";
regionExpansionConfigurable: boolean; regionExpansionConfigurable: boolean;
} }
@ -762,9 +764,11 @@ export interface ManagerDto {
bdLeaderCount?: number; bdLeaderCount?: number;
canAddAdmin?: boolean; canAddAdmin?: boolean;
canAddBdLeader?: boolean; canAddBdLeader?: boolean;
canManageCrossRegion?: boolean;
managerOperationBaseScope?: "country" | "region" | "global";
managerOperationEffectiveScope?: "country" | "region" | "global";
managerCrossRegionConfigurable?: boolean;
canAddBdLeaderInRegion?: boolean; canAddBdLeaderInRegion?: boolean;
bdLeaderInviteBaseScope?: "country" | "region" | "global";
bdLeaderInviteEffectiveScope?: "country" | "region" | "global";
bdLeaderRegionExpansionConfigurable?: boolean; bdLeaderRegionExpansionConfigurable?: boolean;
canAddSuperadmin?: boolean; canAddSuperadmin?: boolean;
canBlockUser?: boolean; canBlockUser?: boolean;
@ -789,6 +793,7 @@ export interface ManagerDto {
export interface CreateManagerPayload { export interface CreateManagerPayload {
canAddAdmin?: boolean; canAddAdmin?: boolean;
canAddBdLeader?: boolean; canAddBdLeader?: boolean;
canManageCrossRegion?: boolean;
canAddBdLeaderInRegion?: boolean; canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean; canAddSuperadmin?: boolean;
canBlockUser?: boolean; canBlockUser?: boolean;
@ -805,6 +810,7 @@ export interface CreateManagerPayload {
export interface UpdateManagerPayload { export interface UpdateManagerPayload {
canAddAdmin?: boolean; canAddAdmin?: boolean;
canAddBdLeader?: boolean; canAddBdLeader?: boolean;
canManageCrossRegion?: boolean;
canAddBdLeaderInRegion?: boolean; canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean; canAddSuperadmin?: boolean;
canBlockUser?: boolean; canBlockUser?: boolean;