身份自动赠送
This commit is contained in:
parent
4166fe0786
commit
dae91e892e
@ -2959,6 +2959,28 @@
|
|||||||
"x-permissions": ["resource-shop:update"]
|
"x-permissions": ["resource-shop:update"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/resource-groups/identity-auto-grant-config": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getResourceIdentityAutoGrantConfig",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "resource-group:view",
|
||||||
|
"x-permissions": ["resource-group:view"]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateResourceIdentityAutoGrantConfig",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "resource-group:update",
|
||||||
|
"x-permissions": ["resource-group:update"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/resource-groups": {
|
"/admin/resource-groups": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listResourceGroups",
|
"operationId": "listResourceGroups",
|
||||||
|
|||||||
@ -61,8 +61,13 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
region_id: 7,
|
region_id: 7,
|
||||||
status: "active",
|
status: "active",
|
||||||
});
|
});
|
||||||
await createManager({ canBlockUser: false, contact: "+63", targetUserId: "1003" });
|
await createManager({ canBlockUser: false, canGrantBadge: false, contact: "+63", targetUserId: "1003" });
|
||||||
await updateManager("1003", { canBlockUser: false, canGrantVehicle: true, contact: "+63" });
|
await updateManager("1003", {
|
||||||
|
canBlockUser: false,
|
||||||
|
canGrantVehicle: true,
|
||||||
|
canTransferUserCountry: false,
|
||||||
|
contact: "+63",
|
||||||
|
});
|
||||||
await listHosts({ agency_id: 41, page: 1, page_size: 10, sort_by: "diamond", sort_direction: "desc" });
|
await listHosts({ agency_id: 41, page: 1, page_size: 10, sort_by: "diamond", sort_direction: "desc" });
|
||||||
await listCoinSellers({
|
await listCoinSellers({
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -133,6 +138,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
expect(managerCreateInit?.method).toBe("POST");
|
expect(managerCreateInit?.method).toBe("POST");
|
||||||
expect(JSON.parse(String(managerCreateInit?.body))).toEqual({
|
expect(JSON.parse(String(managerCreateInit?.body))).toEqual({
|
||||||
canBlockUser: false,
|
canBlockUser: false,
|
||||||
|
canGrantBadge: false,
|
||||||
contact: "+63",
|
contact: "+63",
|
||||||
targetUserId: "1003",
|
targetUserId: "1003",
|
||||||
});
|
});
|
||||||
@ -141,6 +147,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
expect(JSON.parse(String(managerUpdateInit?.body))).toEqual({
|
expect(JSON.parse(String(managerUpdateInit?.body))).toEqual({
|
||||||
canBlockUser: false,
|
canBlockUser: false,
|
||||||
canGrantVehicle: true,
|
canGrantVehicle: true,
|
||||||
|
canTransferUserCountry: false,
|
||||||
contact: "+63",
|
contact: "+63",
|
||||||
});
|
});
|
||||||
expect(String(hostUrl)).toContain("/api/v1/admin/hosts?");
|
expect(String(hostUrl)).toContain("/api/v1/admin/hosts?");
|
||||||
|
|||||||
@ -12,11 +12,13 @@ const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
|||||||
export const managerPermissionFields = [
|
export const managerPermissionFields = [
|
||||||
"canGrantAvatarFrame",
|
"canGrantAvatarFrame",
|
||||||
"canGrantVehicle",
|
"canGrantVehicle",
|
||||||
|
"canGrantBadge",
|
||||||
"canUpdateUserLevel",
|
"canUpdateUserLevel",
|
||||||
"canAddBdLeader",
|
"canAddBdLeader",
|
||||||
"canAddAdmin",
|
"canAddAdmin",
|
||||||
"canAddSuperadmin",
|
"canAddSuperadmin",
|
||||||
"canBlockUser",
|
"canBlockUser",
|
||||||
|
"canTransferUserCountry",
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptyPermissions = () => Object.fromEntries(managerPermissionFields.map((field) => [field, true]));
|
const emptyPermissions = () => Object.fromEntries(managerPermissionFields.map((field) => [field, true]));
|
||||||
|
|||||||
@ -18,11 +18,13 @@ import styles from "@/features/host-org/host-org.module.css";
|
|||||||
const managerPermissionOptions = [
|
const managerPermissionOptions = [
|
||||||
{ key: "canGrantAvatarFrame", label: "头像框" },
|
{ key: "canGrantAvatarFrame", label: "头像框" },
|
||||||
{ key: "canGrantVehicle", label: "Car" },
|
{ key: "canGrantVehicle", label: "Car" },
|
||||||
|
{ key: "canGrantBadge", label: "徽章" },
|
||||||
{ key: "canUpdateUserLevel", label: "升级等级" },
|
{ key: "canUpdateUserLevel", label: "升级等级" },
|
||||||
{ key: "canAddBdLeader", label: "BD Leader" },
|
{ key: "canAddBdLeader", label: "BD Leader" },
|
||||||
{ 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" },
|
||||||
|
{ key: "canTransferUserCountry", label: "转移国家" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const managerColumns = [
|
const managerColumns = [
|
||||||
|
|||||||
@ -114,7 +114,9 @@ export const createManagerSchema = z.object({
|
|||||||
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),
|
||||||
|
canGrantBadge: z.boolean().optional().default(true),
|
||||||
canGrantVehicle: z.boolean().optional().default(true),
|
canGrantVehicle: z.boolean().optional().default(true),
|
||||||
|
canTransferUserCountry: z.boolean().optional().default(true),
|
||||||
canUpdateUserLevel: z.boolean().optional().default(true),
|
canUpdateUserLevel: z.boolean().optional().default(true),
|
||||||
contact: z.string().trim().max(128, "联系方式不能超过 128 个字符").optional().default(""),
|
contact: z.string().trim().max(128, "联系方式不能超过 128 个字符").optional().default(""),
|
||||||
targetUserId: userIdSchema,
|
targetUserId: userIdSchema,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
enableGift,
|
enableGift,
|
||||||
enableResource,
|
enableResource,
|
||||||
enableResourceGroup,
|
enableResourceGroup,
|
||||||
|
getResourceIdentityAutoGrantConfig,
|
||||||
grantResource,
|
grantResource,
|
||||||
grantResourceGroup,
|
grantResourceGroup,
|
||||||
listEmojiPackCategories,
|
listEmojiPackCategories,
|
||||||
@ -28,6 +29,7 @@ import {
|
|||||||
updateGift,
|
updateGift,
|
||||||
updateGiftTypes,
|
updateGiftTypes,
|
||||||
updateResource,
|
updateResource,
|
||||||
|
updateResourceIdentityAutoGrantConfig,
|
||||||
updateResourceMp4Layouts,
|
updateResourceMp4Layouts,
|
||||||
updateResourceGroup,
|
updateResourceGroup,
|
||||||
upsertResourceShopItems,
|
upsertResourceShopItems,
|
||||||
@ -262,6 +264,58 @@ test("resource APIs use generated admin paths", async () => {
|
|||||||
expect(JSON.parse(String(grantGroupInit?.body))).toMatchObject({ groupId: 22, targetUserId: "1001" });
|
expect(JSON.parse(String(grantGroupInit?.body))).toMatchObject({ groupId: 22, targetUserId: "1001" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resource identity auto grant config APIs use generated admin paths", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{ identity_type: "host", resource_group_id: 11 },
|
||||||
|
{ identityType: "bd", resourceGroupId: 12 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = await getResourceIdentityAutoGrantConfig();
|
||||||
|
await updateResourceIdentityAutoGrantConfig({
|
||||||
|
items: [
|
||||||
|
{ identity_type: "host", resource_group_id: 11 },
|
||||||
|
{ identity_type: "bd", resource_group_id: 12 },
|
||||||
|
{ identity_type: "bd_leader", resource_group_id: 13 },
|
||||||
|
{ identity_type: "agency", resource_group_id: 14 },
|
||||||
|
{ identity_type: "manager", resource_group_id: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [getUrl, getInit] = vi.mocked(fetch).mock.calls[0];
|
||||||
|
const [putUrl, putInit] = vi.mocked(fetch).mock.calls[1];
|
||||||
|
|
||||||
|
expect(config.items).toMatchObject([
|
||||||
|
{ identityType: "host", resourceGroupId: 11 },
|
||||||
|
{ identityType: "bd", resourceGroupId: 12 },
|
||||||
|
]);
|
||||||
|
expect(String(getUrl)).toContain("/api/v1/admin/resource-groups/identity-auto-grant-config");
|
||||||
|
expect(getInit?.method).toBe("GET");
|
||||||
|
expect(String(putUrl)).toContain("/api/v1/admin/resource-groups/identity-auto-grant-config");
|
||||||
|
expect(putInit?.method).toBe("PUT");
|
||||||
|
expect(JSON.parse(String(putInit?.body))).toEqual({
|
||||||
|
items: [
|
||||||
|
{ identity_type: "host", resource_group_id: 11 },
|
||||||
|
{ identity_type: "bd", resource_group_id: 12 },
|
||||||
|
{ identity_type: "bd_leader", resource_group_id: 13 },
|
||||||
|
{ identity_type: "agency", resource_group_id: 14 },
|
||||||
|
{ identity_type: "manager", resource_group_id: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("resource mp4 layout batch API uses narrow update endpoint", async () => {
|
test("resource mp4 layout batch API uses narrow update endpoint", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { apiRequest } from "@/shared/api/request";
|
import { apiRequest } from "@/shared/api/request";
|
||||||
import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
|
import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
|
||||||
import type { ApiPage, CoinLedgerUserDto, EntityId, PageQuery } from "@/shared/api/types";
|
import type { ApiPage, CoinLedgerUserDto, EntityId, PageQuery } from "@/shared/api/types";
|
||||||
|
import { resourceIdentityAutoGrantTypes } from "@/features/resources/constants.js";
|
||||||
|
|
||||||
export interface ResourceDto {
|
export interface ResourceDto {
|
||||||
appCode?: string;
|
appCode?: string;
|
||||||
@ -108,6 +109,30 @@ export interface ResourceGroupDto {
|
|||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResourceIdentityAutoGrantType = "host" | "bd" | "bd_leader" | "agency" | "manager";
|
||||||
|
|
||||||
|
export interface ResourceIdentityAutoGrantItemDto {
|
||||||
|
identityType: ResourceIdentityAutoGrantType;
|
||||||
|
resourceGroupId: number;
|
||||||
|
resourceGroup?: ResourceGroupDto;
|
||||||
|
createdAtMs?: number;
|
||||||
|
updatedAtMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceIdentityAutoGrantConfigDto {
|
||||||
|
items: ResourceIdentityAutoGrantItemDto[];
|
||||||
|
updatedByAdminId?: number;
|
||||||
|
createdAtMs?: number;
|
||||||
|
updatedAtMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceIdentityAutoGrantConfigPayload {
|
||||||
|
items: Array<{
|
||||||
|
identity_type: ResourceIdentityAutoGrantType;
|
||||||
|
resource_group_id: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResourceShopItemDto {
|
export interface ResourceShopItemDto {
|
||||||
appCode?: string;
|
appCode?: string;
|
||||||
shopItemId: number;
|
shopItemId: number;
|
||||||
@ -446,6 +471,29 @@ export function disableResourceGroup(groupId: EntityId): Promise<ResourceGroupDt
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getResourceIdentityAutoGrantConfig(): Promise<ResourceIdentityAutoGrantConfigDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.getResourceIdentityAutoGrantConfig;
|
||||||
|
return apiRequest<RawResourceIdentityAutoGrantConfig | RawResourceIdentityAutoGrantItem[]>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.getResourceIdentityAutoGrantConfig),
|
||||||
|
{
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
).then(normalizeResourceIdentityAutoGrantConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateResourceIdentityAutoGrantConfig(
|
||||||
|
payload: ResourceIdentityAutoGrantConfigPayload,
|
||||||
|
): Promise<ResourceIdentityAutoGrantConfigDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.updateResourceIdentityAutoGrantConfig;
|
||||||
|
return apiRequest<
|
||||||
|
RawResourceIdentityAutoGrantConfig | RawResourceIdentityAutoGrantItem[],
|
||||||
|
ResourceIdentityAutoGrantConfigPayload
|
||||||
|
>(apiEndpointPath(API_OPERATIONS.updateResourceIdentityAutoGrantConfig), {
|
||||||
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
}).then(normalizeResourceIdentityAutoGrantConfig);
|
||||||
|
}
|
||||||
|
|
||||||
export function listGifts(query: PageQuery = {}): Promise<ApiPage<GiftDto>> {
|
export function listGifts(query: PageQuery = {}): Promise<ApiPage<GiftDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listGifts;
|
const endpoint = API_ENDPOINTS.listGifts;
|
||||||
return apiRequest<ApiPage<GiftDto>>(apiEndpointPath(API_OPERATIONS.listGifts), {
|
return apiRequest<ApiPage<GiftDto>>(apiEndpointPath(API_OPERATIONS.listGifts), {
|
||||||
@ -571,3 +619,58 @@ export function grantResourceGroup(payload: GrantResourceGroupPayload): Promise<
|
|||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawResourceIdentityAutoGrantItem = ResourceIdentityAutoGrantItemDto & {
|
||||||
|
created_at_ms?: unknown;
|
||||||
|
identity_type?: unknown;
|
||||||
|
resource_group?: unknown;
|
||||||
|
resource_group_id?: unknown;
|
||||||
|
updated_at_ms?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RawResourceIdentityAutoGrantConfig = ResourceIdentityAutoGrantConfigDto & {
|
||||||
|
configs?: RawResourceIdentityAutoGrantItem[];
|
||||||
|
created_at_ms?: unknown;
|
||||||
|
items?: RawResourceIdentityAutoGrantItem[];
|
||||||
|
updated_at_ms?: unknown;
|
||||||
|
updated_by_admin_id?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeResourceIdentityAutoGrantConfig(
|
||||||
|
source: RawResourceIdentityAutoGrantConfig | RawResourceIdentityAutoGrantItem[],
|
||||||
|
): ResourceIdentityAutoGrantConfigDto {
|
||||||
|
const rawItems = Array.isArray(source) ? source : source.items || source.configs || [];
|
||||||
|
return {
|
||||||
|
items: rawItems.map(normalizeResourceIdentityAutoGrantItem),
|
||||||
|
updatedByAdminId: Array.isArray(source) ? 0 : numberValue(source.updatedByAdminId ?? source.updated_by_admin_id),
|
||||||
|
createdAtMs: Array.isArray(source) ? 0 : numberValue(source.createdAtMs ?? source.created_at_ms),
|
||||||
|
updatedAtMs: Array.isArray(source) ? 0 : numberValue(source.updatedAtMs ?? source.updated_at_ms),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResourceIdentityAutoGrantItem(
|
||||||
|
item: RawResourceIdentityAutoGrantItem,
|
||||||
|
): ResourceIdentityAutoGrantItemDto {
|
||||||
|
const resourceGroup = item.resourceGroup ?? item.resource_group;
|
||||||
|
return {
|
||||||
|
identityType: normalizeIdentityType(item.identityType ?? item.identity_type),
|
||||||
|
resourceGroupId: numberValue(item.resourceGroupId ?? item.resource_group_id),
|
||||||
|
resourceGroup: resourceGroup && typeof resourceGroup === "object" ? (resourceGroup as ResourceGroupDto) : undefined,
|
||||||
|
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
||||||
|
updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeIdentityType(value: unknown): ResourceIdentityAutoGrantType {
|
||||||
|
const identityType = stringValue(value) as ResourceIdentityAutoGrantType;
|
||||||
|
return resourceIdentityAutoGrantTypes.includes(identityType) ? identityType : "host";
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValue(value: unknown): number {
|
||||||
|
const parsed = Number(value || 0);
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringValue(value: unknown): string {
|
||||||
|
return value === undefined || value === null ? "" : String(value);
|
||||||
|
}
|
||||||
|
|||||||
@ -127,6 +127,18 @@ export const resourceGroupAssetOptions = [
|
|||||||
|
|
||||||
export const resourceGroupAssetLabels = Object.fromEntries(resourceGroupAssetOptions);
|
export const resourceGroupAssetLabels = Object.fromEntries(resourceGroupAssetOptions);
|
||||||
|
|
||||||
|
export const resourceIdentityAutoGrantOptions = [
|
||||||
|
["host", "主播"],
|
||||||
|
["bd", "BD"],
|
||||||
|
["bd_leader", "BD Leader"],
|
||||||
|
["agency", "Agency"],
|
||||||
|
["manager", "Manager"],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const resourceIdentityAutoGrantTypes = resourceIdentityAutoGrantOptions.map(([value]) => value);
|
||||||
|
|
||||||
|
export const resourceIdentityAutoGrantLabels = Object.fromEntries(resourceIdentityAutoGrantOptions);
|
||||||
|
|
||||||
export const grantStrategyLabels = {
|
export const grantStrategyLabels = {
|
||||||
extend_expiry: "延长有效期",
|
extend_expiry: "延长有效期",
|
||||||
increase_quantity: "增加数量",
|
increase_quantity: "增加数量",
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
enableGift,
|
enableGift,
|
||||||
enableResource,
|
enableResource,
|
||||||
enableResourceGroup,
|
enableResourceGroup,
|
||||||
|
getResourceIdentityAutoGrantConfig,
|
||||||
grantResource,
|
grantResource,
|
||||||
grantResourceGroup,
|
grantResourceGroup,
|
||||||
listEmojiPackCategories,
|
listEmojiPackCategories,
|
||||||
@ -33,6 +34,7 @@ import {
|
|||||||
updateGift,
|
updateGift,
|
||||||
updateGiftTypes,
|
updateGiftTypes,
|
||||||
updateResource,
|
updateResource,
|
||||||
|
updateResourceIdentityAutoGrantConfig,
|
||||||
updateResourceGroup,
|
updateResourceGroup,
|
||||||
upsertResourceShopItems,
|
upsertResourceShopItems,
|
||||||
} from "@/features/resources/api";
|
} from "@/features/resources/api";
|
||||||
@ -40,6 +42,7 @@ import {
|
|||||||
cpRelationTypeLabels,
|
cpRelationTypeLabels,
|
||||||
cpRelationTypeOptions,
|
cpRelationTypeOptions,
|
||||||
defaultGiftTypeOptions,
|
defaultGiftTypeOptions,
|
||||||
|
resourceIdentityAutoGrantOptions,
|
||||||
resourceShopSellableTypes,
|
resourceShopSellableTypes,
|
||||||
} from "@/features/resources/constants.js";
|
} from "@/features/resources/constants.js";
|
||||||
import { useResourceAbilities } from "@/features/resources/permissions.js";
|
import { useResourceAbilities } from "@/features/resources/permissions.js";
|
||||||
@ -51,6 +54,7 @@ import {
|
|||||||
resourceFormSchema,
|
resourceFormSchema,
|
||||||
resourceGrantFormSchema,
|
resourceGrantFormSchema,
|
||||||
resourceGroupCreateFormSchema,
|
resourceGroupCreateFormSchema,
|
||||||
|
resourceIdentityAutoGrantFormSchema,
|
||||||
resourceShopItemsFormSchema,
|
resourceShopItemsFormSchema,
|
||||||
} from "@/features/resources/schema.js";
|
} from "@/features/resources/schema.js";
|
||||||
|
|
||||||
@ -109,6 +113,15 @@ const emptyResourceGroupForm = (group = {}) => ({
|
|||||||
name: group.name || "",
|
name: group.name || "",
|
||||||
sortOrder: group.sortOrder === 0 || group.sortOrder ? String(group.sortOrder) : "0",
|
sortOrder: group.sortOrder === 0 || group.sortOrder ? String(group.sortOrder) : "0",
|
||||||
});
|
});
|
||||||
|
const emptyResourceIdentityAutoGrantForm = (items = []) => {
|
||||||
|
const itemByIdentityType = new Map(items.map((item) => [item.identityType, item]));
|
||||||
|
return resourceIdentityAutoGrantOptions.map(([identityType]) => ({
|
||||||
|
identityType,
|
||||||
|
resourceGroupId: itemByIdentityType.get(identityType)?.resourceGroupId
|
||||||
|
? String(itemByIdentityType.get(identityType).resourceGroupId)
|
||||||
|
: "",
|
||||||
|
}));
|
||||||
|
};
|
||||||
const emptyGroupResourceItem = () => ({
|
const emptyGroupResourceItem = () => ({
|
||||||
durationDays: "1",
|
durationDays: "1",
|
||||||
itemType: "resource",
|
itemType: "resource",
|
||||||
@ -778,6 +791,9 @@ export function useResourceGroupListPage() {
|
|||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
const [resourceOptions, setResourceOptions] = useState([]);
|
const [resourceOptions, setResourceOptions] = useState([]);
|
||||||
const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false);
|
const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false);
|
||||||
|
const [identityGrantForm, setIdentityGrantForm] = useState(() => emptyResourceIdentityAutoGrantForm());
|
||||||
|
const [identityGrantGroupOptions, setIdentityGrantGroupOptions] = useState([]);
|
||||||
|
const [identityGrantLoading, setIdentityGrantLoading] = useState(false);
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
keyword: query,
|
keyword: query,
|
||||||
@ -821,6 +837,40 @@ export function useResourceGroupListPage() {
|
|||||||
};
|
};
|
||||||
}, [activeAction, selectedGroup, showToast]);
|
}, [activeAction, selectedGroup, showToast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeAction !== "identity-config") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let ignore = false;
|
||||||
|
setIdentityGrantLoading(true);
|
||||||
|
// 这里并行拉取配置和可发放资源组,避免弹窗先显示旧配置再异步换值造成误保存。
|
||||||
|
Promise.all([
|
||||||
|
getResourceIdentityAutoGrantConfig(),
|
||||||
|
fetchAllOptionPages(listResourceGroups, { status: "active" }),
|
||||||
|
])
|
||||||
|
.then(([config, groups]) => {
|
||||||
|
if (ignore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIdentityGrantForm(emptyResourceIdentityAutoGrantForm(config.items || []));
|
||||||
|
setIdentityGrantGroupOptions(mergeResourceGroupOptions(groups.items || [], config.items || []));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!ignore) {
|
||||||
|
showToast(err.message || "加载身份资源组配置失败", "error");
|
||||||
|
setActiveAction("");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!ignore) {
|
||||||
|
setIdentityGrantLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
ignore = true;
|
||||||
|
};
|
||||||
|
}, [activeAction, showToast]);
|
||||||
|
|
||||||
const openCreateGroup = () => {
|
const openCreateGroup = () => {
|
||||||
setForm(emptyResourceGroupForm());
|
setForm(emptyResourceGroupForm());
|
||||||
setSelectedGroup(null);
|
setSelectedGroup(null);
|
||||||
@ -836,6 +886,11 @@ export function useResourceGroupListPage() {
|
|||||||
setActiveAction("edit");
|
setActiveAction("edit");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openIdentityGrantConfig = () => {
|
||||||
|
setIdentityGrantLoading(true);
|
||||||
|
setActiveAction("identity-config");
|
||||||
|
};
|
||||||
|
|
||||||
const closeAction = () => {
|
const closeAction = () => {
|
||||||
setActiveAction("");
|
setActiveAction("");
|
||||||
setSelectedGroup(null);
|
setSelectedGroup(null);
|
||||||
@ -887,6 +942,27 @@ export function useResourceGroupListPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitIdentityGrantConfig = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!abilities.canUpdateGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingAction("identity-grant-config");
|
||||||
|
try {
|
||||||
|
const payload = buildResourceIdentityAutoGrantPayload(
|
||||||
|
parseForm(resourceIdentityAutoGrantFormSchema, { items: identityGrantForm }),
|
||||||
|
);
|
||||||
|
const nextConfig = await updateResourceIdentityAutoGrantConfig(payload);
|
||||||
|
setIdentityGrantForm(emptyResourceIdentityAutoGrantForm(nextConfig.items || []));
|
||||||
|
showToast("身份资源组配置已保存", "success");
|
||||||
|
closeAction();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "保存身份资源组配置失败", "error");
|
||||||
|
} finally {
|
||||||
|
setLoadingAction("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleGroup = async (group, nextEnabled = group.status !== "active") => {
|
const toggleGroup = async (group, nextEnabled = group.status !== "active") => {
|
||||||
if (!abilities.canUpdateGroup || !group?.groupId) {
|
if (!abilities.canUpdateGroup || !group?.groupId) {
|
||||||
return;
|
return;
|
||||||
@ -925,16 +1001,22 @@ export function useResourceGroupListPage() {
|
|||||||
changeStatus: resetSetter(setStatus, setPage),
|
changeStatus: resetSetter(setStatus, setPage),
|
||||||
closeAction,
|
closeAction,
|
||||||
form,
|
form,
|
||||||
|
identityGrantForm,
|
||||||
|
identityGrantGroupOptions,
|
||||||
|
identityGrantLoading,
|
||||||
loadingAction,
|
loadingAction,
|
||||||
openCreateGroup,
|
openCreateGroup,
|
||||||
openEditGroup,
|
openEditGroup,
|
||||||
|
openIdentityGrantConfig,
|
||||||
removeGroupItem,
|
removeGroupItem,
|
||||||
resourceOptions,
|
resourceOptions,
|
||||||
resourceOptionsLoading,
|
resourceOptionsLoading,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
setForm,
|
setForm,
|
||||||
|
setIdentityGrantForm,
|
||||||
status,
|
status,
|
||||||
|
submitIdentityGrantConfig,
|
||||||
submitGroup,
|
submitGroup,
|
||||||
toggleGroup,
|
toggleGroup,
|
||||||
updateGroupItem,
|
updateGroupItem,
|
||||||
@ -1744,6 +1826,35 @@ function buildResourceGroupPayload(form) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildResourceIdentityAutoGrantPayload(form) {
|
||||||
|
const itemByIdentityType = new Map(form.items.map((item) => [item.identityType, item]));
|
||||||
|
return {
|
||||||
|
items: resourceIdentityAutoGrantOptions.map(([identityType]) => {
|
||||||
|
const resourceGroupId = Number(itemByIdentityType.get(identityType)?.resourceGroupId || 0);
|
||||||
|
return {
|
||||||
|
identity_type: identityType,
|
||||||
|
resource_group_id: Number.isInteger(resourceGroupId) && resourceGroupId >= 0 ? resourceGroupId : 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeResourceGroupOptions(activeGroups = [], configItems = []) {
|
||||||
|
const groupById = new Map();
|
||||||
|
activeGroups.forEach((group) => {
|
||||||
|
if (group?.groupId) {
|
||||||
|
groupById.set(String(group.groupId), group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
configItems.forEach((item) => {
|
||||||
|
const group = item?.resourceGroup;
|
||||||
|
if (group?.groupId && !groupById.has(String(group.groupId))) {
|
||||||
|
groupById.set(String(group.groupId), group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(groupById.values());
|
||||||
|
}
|
||||||
|
|
||||||
function buildGiftPayload(form) {
|
function buildGiftPayload(form) {
|
||||||
const cpRelationType = form.giftTypeCode === cpGiftTypeCode ? normalizedCPRelationType(form.cpRelationType) : "";
|
const cpRelationType = form.giftTypeCode === cpGiftTypeCode ? normalizedCPRelationType(form.cpRelationType) : "";
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
|||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
||||||
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
||||||
|
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
||||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {
|
import {
|
||||||
@ -28,8 +29,14 @@ import {
|
|||||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx";
|
import { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx";
|
||||||
import { resourceGroupAssetLabels, resourceStatusFilters, resourceTypeLabels } from "@/features/resources/constants.js";
|
import {
|
||||||
|
resourceGroupAssetLabels,
|
||||||
|
resourceIdentityAutoGrantLabels,
|
||||||
|
resourceStatusFilters,
|
||||||
|
resourceTypeLabels,
|
||||||
|
} from "@/features/resources/constants.js";
|
||||||
import { useResourceGroupListPage } from "@/features/resources/hooks/useResourcePages.js";
|
import { useResourceGroupListPage } from "@/features/resources/hooks/useResourcePages.js";
|
||||||
|
import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.jsx";
|
||||||
import styles from "@/features/resources/resources.module.css";
|
import styles from "@/features/resources/resources.module.css";
|
||||||
|
|
||||||
const dayMillis = 24 * 60 * 60 * 1000;
|
const dayMillis = 24 * 60 * 60 * 1000;
|
||||||
@ -114,6 +121,13 @@ export function ResourceGroupListPage() {
|
|||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
filters={
|
||||||
|
page.abilities.canViewGroup ? (
|
||||||
|
<AdminActionIconButton label="配置身份资源组" onClick={page.openIdentityGrantConfig}>
|
||||||
|
<SettingsOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
@ -151,6 +165,16 @@ export function ResourceGroupListPage() {
|
|||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitGroup}
|
onSubmit={page.submitGroup}
|
||||||
/>
|
/>
|
||||||
|
<ResourceIdentityAutoGrantDialog
|
||||||
|
disabled={!page.abilities.canUpdateGroup}
|
||||||
|
form={page.identityGrantForm}
|
||||||
|
groupOptions={page.identityGrantGroupOptions}
|
||||||
|
loading={page.identityGrantLoading || page.loadingAction === "identity-grant-config"}
|
||||||
|
open={page.activeAction === "identity-config"}
|
||||||
|
page={page}
|
||||||
|
onClose={page.closeAction}
|
||||||
|
onSubmit={page.submitIdentityGrantConfig}
|
||||||
|
/>
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -271,6 +295,53 @@ function ResourceGroupFormDialog({ disabled, form, loading, mode, onClose, onSub
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResourceIdentityAutoGrantDialog({ disabled, form, groupOptions, loading, onClose, onSubmit, open, page }) {
|
||||||
|
const submitDisabled = disabled || loading;
|
||||||
|
return (
|
||||||
|
<AdminFormDialog
|
||||||
|
loading={loading}
|
||||||
|
open={open}
|
||||||
|
size="wide"
|
||||||
|
submitDisabled={submitDisabled}
|
||||||
|
submitLabel="保存配置"
|
||||||
|
title="身份资源组配置"
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<AdminFormSection title="自动发放资源组">
|
||||||
|
<AdminFormList>
|
||||||
|
{form.map((item, index) => (
|
||||||
|
<AdminFormListRow columns="140px minmax(260px, 1fr)" key={item.identityType}>
|
||||||
|
<span className={styles.identityGrantLabel}>
|
||||||
|
{resourceIdentityAutoGrantLabels[item.identityType] || item.identityType}
|
||||||
|
</span>
|
||||||
|
<ResourceGroupSelectField
|
||||||
|
allowEmpty
|
||||||
|
disabled={disabled || loading}
|
||||||
|
drawerTitle={`${resourceIdentityAutoGrantLabels[item.identityType] || item.identityType}资源组`}
|
||||||
|
emptyLabel="不自动发放"
|
||||||
|
groups={groupOptions}
|
||||||
|
label="资源组"
|
||||||
|
placeholder="点击选择资源组"
|
||||||
|
value={item.resourceGroupId}
|
||||||
|
onChange={(value) =>
|
||||||
|
page.setIdentityGrantForm((current) =>
|
||||||
|
current.map((currentItem, itemIndex) =>
|
||||||
|
itemIndex === index
|
||||||
|
? { ...currentItem, resourceGroupId: value }
|
||||||
|
: currentItem,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AdminFormListRow>
|
||||||
|
))}
|
||||||
|
</AdminFormList>
|
||||||
|
</AdminFormSection>
|
||||||
|
</AdminFormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ResourceGroupItemEditor({ disabled, index, item, page }) {
|
function ResourceGroupItemEditor({ disabled, index, item, page }) {
|
||||||
if (item.itemType === "wallet_asset") {
|
if (item.itemType === "wallet_asset") {
|
||||||
const label = resourceGroupAssetLabels[item.walletAssetType] || "钱包资产";
|
const label = resourceGroupAssetLabels[item.walletAssetType] || "钱包资产";
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export function useResourceAbilities() {
|
|||||||
canView: can(PERMISSIONS.resourceView),
|
canView: can(PERMISSIONS.resourceView),
|
||||||
canViewEmojiPack: can(PERMISSIONS.emojiPackView),
|
canViewEmojiPack: can(PERMISSIONS.emojiPackView),
|
||||||
canViewGrant: can(PERMISSIONS.resourceGrantView),
|
canViewGrant: can(PERMISSIONS.resourceGrantView),
|
||||||
|
canViewGroup: can(PERMISSIONS.resourceGroupView),
|
||||||
canViewShop: can(PERMISSIONS.resourceShopView),
|
canViewShop: can(PERMISSIONS.resourceShopView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,14 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.identityGrantLabel {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: var(--control-height);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.selectColumn {
|
.selectColumn {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { cpRelationTypeOptions } from "@/features/resources/constants.js";
|
import { cpRelationTypeOptions, resourceIdentityAutoGrantTypes } from "@/features/resources/constants.js";
|
||||||
import { hasUnconfirmedMp4Layout } from "@/features/resources/mp4AlphaLayout.js";
|
import { hasUnconfirmedMp4Layout } from "@/features/resources/mp4AlphaLayout.js";
|
||||||
|
|
||||||
const resourceTypes = [
|
const resourceTypes = [
|
||||||
@ -177,6 +177,58 @@ export const resourceGroupCreateFormSchema = z
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const identityAutoGrantItemSchema = z.object({
|
||||||
|
identityType: z.enum(resourceIdentityAutoGrantTypes, "请选择身份"),
|
||||||
|
resourceGroupId: z.union([z.string(), z.number()]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourceIdentityAutoGrantFormSchema = z
|
||||||
|
.object({
|
||||||
|
items: z.array(identityAutoGrantItemSchema),
|
||||||
|
})
|
||||||
|
.superRefine((value, context) => {
|
||||||
|
const requiredIdentityTypes = new Set(resourceIdentityAutoGrantTypes);
|
||||||
|
const seenIdentityTypes = new Set();
|
||||||
|
|
||||||
|
if (value.items.length !== resourceIdentityAutoGrantTypes.length) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "必须配置所有身份",
|
||||||
|
path: ["items"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
value.items.forEach((item, index) => {
|
||||||
|
if (seenIdentityTypes.has(item.identityType)) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "身份不能重复配置",
|
||||||
|
path: ["items", index, "identityType"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
seenIdentityTypes.add(item.identityType);
|
||||||
|
requiredIdentityTypes.delete(item.identityType);
|
||||||
|
|
||||||
|
const resourceGroupId =
|
||||||
|
item.resourceGroupId === undefined || item.resourceGroupId === "" ? 0 : Number(item.resourceGroupId);
|
||||||
|
if (!Number.isInteger(resourceGroupId) || resourceGroupId < 0) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "资源组必须是有效资源组",
|
||||||
|
path: ["items", index, "resourceGroupId"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
requiredIdentityTypes.forEach((identityType) => {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "必须配置所有身份",
|
||||||
|
path: ["items", identityType],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export const giftFormSchema = z
|
export const giftFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
chargeAssetType: z.enum(["COIN"]),
|
chargeAssetType: z.enum(["COIN"]),
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
resourceFormSchema,
|
resourceFormSchema,
|
||||||
resourceGrantFormSchema,
|
resourceGrantFormSchema,
|
||||||
resourceGroupCreateFormSchema,
|
resourceGroupCreateFormSchema,
|
||||||
|
resourceIdentityAutoGrantFormSchema,
|
||||||
resourceShopItemsFormSchema,
|
resourceShopItemsFormSchema,
|
||||||
} from "./schema.js";
|
} from "./schema.js";
|
||||||
|
|
||||||
@ -162,6 +163,62 @@ describe("resource form schema", () => {
|
|||||||
).toThrow(FormValidationError);
|
).toThrow(FormValidationError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validates identity auto grant resource group config", () => {
|
||||||
|
const payload = parseForm(resourceIdentityAutoGrantFormSchema, {
|
||||||
|
items: [
|
||||||
|
{ identityType: "host", resourceGroupId: "11" },
|
||||||
|
{ identityType: "bd", resourceGroupId: "12" },
|
||||||
|
{ identityType: "bd_leader", resourceGroupId: "13" },
|
||||||
|
{ identityType: "agency", resourceGroupId: "14" },
|
||||||
|
{ identityType: "manager", resourceGroupId: "" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload.items).toHaveLength(5);
|
||||||
|
expect(payload.items[4].resourceGroupId).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects incomplete identity auto grant config", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseForm(resourceIdentityAutoGrantFormSchema, {
|
||||||
|
items: [
|
||||||
|
{ identityType: "host", resourceGroupId: "11" },
|
||||||
|
{ identityType: "bd", resourceGroupId: "12" },
|
||||||
|
{ identityType: "bd_leader", resourceGroupId: "13" },
|
||||||
|
{ identityType: "agency", resourceGroupId: "14" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrow(FormValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects duplicate identity auto grant config", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseForm(resourceIdentityAutoGrantFormSchema, {
|
||||||
|
items: [
|
||||||
|
{ identityType: "host", resourceGroupId: "11" },
|
||||||
|
{ identityType: "bd", resourceGroupId: "12" },
|
||||||
|
{ identityType: "bd_leader", resourceGroupId: "13" },
|
||||||
|
{ identityType: "agency", resourceGroupId: "14" },
|
||||||
|
{ identityType: "agency", resourceGroupId: "15" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrow(FormValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid identity auto grant resource group id", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseForm(resourceIdentityAutoGrantFormSchema, {
|
||||||
|
items: [
|
||||||
|
{ identityType: "host", resourceGroupId: "-1" },
|
||||||
|
{ identityType: "bd", resourceGroupId: "12" },
|
||||||
|
{ identityType: "bd_leader", resourceGroupId: "13" },
|
||||||
|
{ identityType: "agency", resourceGroupId: "14" },
|
||||||
|
{ identityType: "manager", resourceGroupId: "0" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrow(FormValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
test("validates editable gift form fields", () => {
|
test("validates editable gift form fields", () => {
|
||||||
const payload = parseForm(giftFormSchema, {
|
const payload = parseForm(giftFormSchema, {
|
||||||
chargeAssetType: "COIN",
|
chargeAssetType: "COIN",
|
||||||
|
|||||||
@ -117,6 +117,7 @@ export const API_OPERATIONS = {
|
|||||||
getRegistrationRewardConfig: "getRegistrationRewardConfig",
|
getRegistrationRewardConfig: "getRegistrationRewardConfig",
|
||||||
getResource: "getResource",
|
getResource: "getResource",
|
||||||
getResourceGroup: "getResourceGroup",
|
getResourceGroup: "getResourceGroup",
|
||||||
|
getResourceIdentityAutoGrantConfig: "getResourceIdentityAutoGrantConfig",
|
||||||
getRoleDataScopes: "getRoleDataScopes",
|
getRoleDataScopes: "getRoleDataScopes",
|
||||||
getRoomConfig: "getRoomConfig",
|
getRoomConfig: "getRoomConfig",
|
||||||
getRoomRocketConfig: "getRoomRocketConfig",
|
getRoomRocketConfig: "getRoomRocketConfig",
|
||||||
@ -267,6 +268,7 @@ export const API_OPERATIONS = {
|
|||||||
updateResource: "updateResource",
|
updateResource: "updateResource",
|
||||||
updateResourceGroup: "updateResourceGroup",
|
updateResourceGroup: "updateResourceGroup",
|
||||||
updateResourceGroupItems: "updateResourceGroupItems",
|
updateResourceGroupItems: "updateResourceGroupItems",
|
||||||
|
updateResourceIdentityAutoGrantConfig: "updateResourceIdentityAutoGrantConfig",
|
||||||
updateRole: "updateRole",
|
updateRole: "updateRole",
|
||||||
updateRoom: "updateRoom",
|
updateRoom: "updateRoom",
|
||||||
updateRoomConfig: "updateRoomConfig",
|
updateRoomConfig: "updateRoomConfig",
|
||||||
@ -1021,6 +1023,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "resource-group:view",
|
permission: "resource-group:view",
|
||||||
permissions: ["resource-group:view"]
|
permissions: ["resource-group:view"]
|
||||||
},
|
},
|
||||||
|
getResourceIdentityAutoGrantConfig: {
|
||||||
|
method: "GET",
|
||||||
|
operationId: API_OPERATIONS.getResourceIdentityAutoGrantConfig,
|
||||||
|
path: "/v1/admin/resource-groups/identity-auto-grant-config",
|
||||||
|
permission: "resource-group:view",
|
||||||
|
permissions: ["resource-group:view"]
|
||||||
|
},
|
||||||
getRoleDataScopes: {
|
getRoleDataScopes: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getRoleDataScopes,
|
operationId: API_OPERATIONS.getRoleDataScopes,
|
||||||
@ -2057,6 +2066,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "resource-group:update",
|
permission: "resource-group:update",
|
||||||
permissions: ["resource-group:update"]
|
permissions: ["resource-group:update"]
|
||||||
},
|
},
|
||||||
|
updateResourceIdentityAutoGrantConfig: {
|
||||||
|
method: "PUT",
|
||||||
|
operationId: API_OPERATIONS.updateResourceIdentityAutoGrantConfig,
|
||||||
|
path: "/v1/admin/resource-groups/identity-auto-grant-config",
|
||||||
|
permission: "resource-group:update",
|
||||||
|
permissions: ["resource-group:update"]
|
||||||
|
},
|
||||||
updateRole: {
|
updateRole: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateRole,
|
operationId: API_OPERATIONS.updateRole,
|
||||||
|
|||||||
40
src/shared/api/generated/schema.d.ts
vendored
40
src/shared/api/generated/schema.d.ts
vendored
@ -2084,6 +2084,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/resource-groups/identity-auto-grant-config": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["getResourceIdentityAutoGrantConfig"];
|
||||||
|
put: operations["updateResourceIdentityAutoGrantConfig"];
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/resource-groups": {
|
"/admin/resource-groups": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -6151,6 +6167,30 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
getResourceIdentityAutoGrantConfig: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updateResourceIdentityAutoGrantConfig: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listResourceGroups: {
|
listResourceGroups: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@ -566,7 +566,9 @@ export interface ManagerDto {
|
|||||||
canAddSuperadmin?: boolean;
|
canAddSuperadmin?: boolean;
|
||||||
canBlockUser?: boolean;
|
canBlockUser?: boolean;
|
||||||
canGrantAvatarFrame?: boolean;
|
canGrantAvatarFrame?: boolean;
|
||||||
|
canGrantBadge?: boolean;
|
||||||
canGrantVehicle?: boolean;
|
canGrantVehicle?: boolean;
|
||||||
|
canTransferUserCountry?: boolean;
|
||||||
canUpdateUserLevel?: boolean;
|
canUpdateUserLevel?: boolean;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
@ -586,7 +588,9 @@ export interface CreateManagerPayload {
|
|||||||
canAddSuperadmin?: boolean;
|
canAddSuperadmin?: boolean;
|
||||||
canBlockUser?: boolean;
|
canBlockUser?: boolean;
|
||||||
canGrantAvatarFrame?: boolean;
|
canGrantAvatarFrame?: boolean;
|
||||||
|
canGrantBadge?: boolean;
|
||||||
canGrantVehicle?: boolean;
|
canGrantVehicle?: boolean;
|
||||||
|
canTransferUserCountry?: boolean;
|
||||||
canUpdateUserLevel?: boolean;
|
canUpdateUserLevel?: boolean;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
targetUserId: string;
|
targetUserId: string;
|
||||||
@ -598,7 +602,9 @@ export interface UpdateManagerPayload {
|
|||||||
canAddSuperadmin?: boolean;
|
canAddSuperadmin?: boolean;
|
||||||
canBlockUser?: boolean;
|
canBlockUser?: boolean;
|
||||||
canGrantAvatarFrame?: boolean;
|
canGrantAvatarFrame?: boolean;
|
||||||
|
canGrantBadge?: boolean;
|
||||||
canGrantVehicle?: boolean;
|
canGrantVehicle?: boolean;
|
||||||
|
canTransferUserCountry?: boolean;
|
||||||
canUpdateUserLevel?: boolean;
|
canUpdateUserLevel?: boolean;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,10 @@ export function TimeRangeFilter({ className = "", disabled = false, label = "时
|
|||||||
const clearRange = () => applyRange(emptyRange);
|
const clearRange = () => applyRange(emptyRange);
|
||||||
const submitDraft = (event) => {
|
const submitDraft = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
// Popover content is rendered through a portal, but React still bubbles
|
||||||
|
// submit events through the component tree. Stop here so a picker confirm
|
||||||
|
// never submits an outer edit drawer before the new range reaches state.
|
||||||
|
event.stopPropagation();
|
||||||
const nextRange = normalizeRange(draft);
|
const nextRange = normalizeRange(draft);
|
||||||
if (nextRange.startMs && nextRange.endMs && nextRange.endMs <= nextRange.startMs) {
|
if (nextRange.startMs && nextRange.endMs && nextRange.endMs <= nextRange.startMs) {
|
||||||
setError("结束时间必须晚于开始时间");
|
setError("结束时间必须晚于开始时间");
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { datetimeTextToMs, normalizeRange, timeRangeLabel } from "@/shared/ui/TimeRangeFilter.jsx";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import { TimeRangeFilter, datetimeTextToMs, normalizeRange, timeRangeLabel } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||||
|
|
||||||
describe("TimeRangeFilter helpers", () => {
|
describe("TimeRangeFilter helpers", () => {
|
||||||
test("parses strict local date time text", () => {
|
test("parses strict local date time text", () => {
|
||||||
@ -24,4 +26,23 @@ describe("TimeRangeFilter helpers", () => {
|
|||||||
expect(timeRangeLabel("时间", {})).toBe("时间");
|
expect(timeRangeLabel("时间", {})).toBe("时间");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("confirms picker changes without submitting an outer form", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const onSubmit = vi.fn((event) => event.preventDefault());
|
||||||
|
const startMs = datetimeTextToMs("2026-05-13 10:00");
|
||||||
|
const endMs = datetimeTextToMs("2026-05-13 12:00");
|
||||||
|
|
||||||
|
render(
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<TimeRangeFilter label="活动时间" value={{ endMs, startMs }} onChange={onChange} />
|
||||||
|
</form>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /活动时间/ }));
|
||||||
|
await user.click(screen.getByRole("button", { name: "确定" }));
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ endMs, startMs });
|
||||||
|
expect(onSubmit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user