Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857171ce9d | ||
|
|
41962e7a6e | ||
|
|
0ec386e674 |
@@ -13,6 +13,7 @@ import {
|
||||
mappedCoreValue,
|
||||
mappedCorePatchValue,
|
||||
dynamicValues,
|
||||
signatureIdentityReportValues,
|
||||
jsonRecord,
|
||||
hasValue,
|
||||
normalizeImageExtension,
|
||||
@@ -390,8 +391,24 @@ export class ReportImportReviewService {
|
||||
const applicationId = typeof payload.applicationId === 'string' ? payload.applicationId : undefined;
|
||||
const importedDrainage = jsonRecord(payload.drainageInfo);
|
||||
const importedReportValues = jsonRecord(importedDrainage.signatureReportValues);
|
||||
const buildBody = (current?: { drainageInfo: Prisma.JsonValue | null }) => {
|
||||
const [reportFields, tenant, application] = await Promise.all([
|
||||
this.smsConfig.getApplicationReportFields(applicationId, 'signature'),
|
||||
this.prisma.tenant.findUnique({ where: { id: batch.tenantId }, select: { name: true } }),
|
||||
applicationId
|
||||
? this.prisma.smsApplication.findUnique({ where: { id: applicationId }, select: { name: true } })
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
const buildBody = (current?: { drainageInfo: Prisma.JsonValue | null; purpose?: string | null }) => {
|
||||
const currentDrainage = jsonRecord(current?.drainageInfo);
|
||||
const purpose = Object.prototype.hasOwnProperty.call(payload, 'purpose')
|
||||
? String(payload.purpose ?? '')
|
||||
: current?.purpose;
|
||||
const identityValues = signatureIdentityReportValues(reportFields, {
|
||||
signatureName: name,
|
||||
purpose,
|
||||
enterpriseName: tenant?.name,
|
||||
applicationName: application?.name,
|
||||
});
|
||||
return {
|
||||
applicationId,
|
||||
name,
|
||||
@@ -402,6 +419,7 @@ export class ReportImportReviewService {
|
||||
...currentDrainage,
|
||||
signatureReportValues: {
|
||||
...jsonRecord(currentDrainage.signatureReportValues),
|
||||
...identityValues,
|
||||
...importedReportValues,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -237,6 +237,33 @@ export function dynamicValues(mappings: ImportMapping[], values: Record<string,
|
||||
);
|
||||
}
|
||||
|
||||
export function signatureIdentityReportValues(
|
||||
fields: Array<{ code: string; name: string; fieldType?: string }>,
|
||||
identity: {
|
||||
signatureName?: string;
|
||||
purpose?: string | null;
|
||||
enterpriseName?: string;
|
||||
applicationName?: string;
|
||||
},
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
fields.flatMap((field) => {
|
||||
if (field.fieldType && field.fieldType !== 'string') return [];
|
||||
const semantic = normalizeHeader(`${field.code}/${field.name}`);
|
||||
const value = /短信签名|签名名称|signaturename|sms(?:signature|sign)|^sign$/.test(semantic)
|
||||
? identity.signatureName
|
||||
: /签名用途|签名依据|purpose/.test(semantic)
|
||||
? identity.purpose
|
||||
: /企业名称|公司名称|enterprisename|companyname/.test(semantic)
|
||||
? identity.enterpriseName
|
||||
: /应用名称|applicationname|appname/.test(semantic)
|
||||
? identity.applicationName
|
||||
: undefined;
|
||||
return hasValue(value) ? [[field.code, value]] : [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function jsonRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ExcelJS from 'exceljs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { ReportMaterialsService } from './report-materials.service';
|
||||
import { mappedCorePatchValue } from './report-materials.helpers';
|
||||
import { mappedCorePatchValue, signatureIdentityReportValues } from './report-materials.helpers';
|
||||
|
||||
describe('ReportMaterialsService', () => {
|
||||
it('requires an enterprise application before parsing an import workbook', async () => {
|
||||
@@ -43,6 +43,27 @@ describe('ReportMaterialsService', () => {
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('materializes system identity fields without inventing a missing signature purpose', () => {
|
||||
const fields = [
|
||||
{ code: 'qaSignatureName', name: '短信签名', fieldType: 'string' },
|
||||
{ code: 'qaSignaturePurpose', name: '签名用途', fieldType: 'string' },
|
||||
{ code: 'qaEnterpriseName', name: '企业名称', fieldType: 'string' },
|
||||
{ code: 'qaApplicationName', name: '应用名称', fieldType: 'string' },
|
||||
{ code: 'qaLicenseImage', name: '营业执照图片', fieldType: 'image' },
|
||||
];
|
||||
expect(
|
||||
signatureIdentityReportValues(fields, {
|
||||
signatureName: '【导入测试】',
|
||||
enterpriseName: '测试企业',
|
||||
applicationName: '测试应用',
|
||||
}),
|
||||
).toEqual({
|
||||
qaSignatureName: '【导入测试】',
|
||||
qaEnterpriseName: '测试企业',
|
||||
qaApplicationName: '测试应用',
|
||||
});
|
||||
});
|
||||
it('builds an official XLSX import template with documented signature columns', async () => {
|
||||
const operationLog = { create: jest.fn().mockResolvedValue({ id: 'log-template' }) };
|
||||
const service = new ReportMaterialsService({ operationLog } as never, {} as never, {} as never);
|
||||
@@ -602,6 +623,79 @@ describe('ReportMaterialsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('approves an imported signature by supplying configured identity fields from real entities', async () => {
|
||||
const item = {
|
||||
id: 'item-identity-1',
|
||||
rowNumber: 2,
|
||||
reportType: 'signature',
|
||||
targetId: null,
|
||||
status: 'pending_review',
|
||||
payload: {
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
name: '【导入测试】',
|
||||
purpose: '验证码通知',
|
||||
drainageInfo: {
|
||||
signatureReportValues: {
|
||||
qaLicenseImage: { fileObjectId: 'image-1', fileName: 'license.png', contentType: 'image/png' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
reportMaterialImportBatch: {
|
||||
findUnique: jest.fn().mockResolvedValue({
|
||||
id: 'batch-identity-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
reportType: 'signature',
|
||||
items: [item],
|
||||
}),
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
reportMaterialImportItem: {
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
groupBy: jest.fn().mockResolvedValue([{ status: 'approved', _count: { _all: 1 } }]),
|
||||
},
|
||||
smsSignature: {
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
tenant: { findUnique: jest.fn().mockResolvedValue({ name: '测试企业' }) },
|
||||
smsApplication: { findUnique: jest.fn().mockResolvedValue({ name: '测试应用' }) },
|
||||
};
|
||||
const smsConfig = {
|
||||
getApplicationReportFields: jest.fn().mockResolvedValue([
|
||||
{ code: 'qaSignatureName', name: '短信签名', fieldType: 'string' },
|
||||
{ code: 'qaSignaturePurpose', name: '签名用途', fieldType: 'string' },
|
||||
{ code: 'qaEnterpriseName', name: '企业名称', fieldType: 'string' },
|
||||
{ code: 'qaApplicationName', name: '应用名称', fieldType: 'string' },
|
||||
{ code: 'qaLicenseImage', name: '营业执照图片', fieldType: 'image' },
|
||||
]),
|
||||
createSignature: jest.fn().mockImplementation(({ drainageInfo }: { drainageInfo: { signatureReportValues: Record<string, unknown> } }) => {
|
||||
expect(drainageInfo.signatureReportValues).toEqual(expect.objectContaining({
|
||||
qaSignatureName: '【导入测试】',
|
||||
qaSignaturePurpose: '验证码通知',
|
||||
qaEnterpriseName: '测试企业',
|
||||
qaApplicationName: '测试应用',
|
||||
qaLicenseImage: expect.objectContaining({ fileObjectId: 'image-1' }),
|
||||
}));
|
||||
return Promise.resolve({ id: 'signature-created-1' });
|
||||
}),
|
||||
approveSignature: jest.fn().mockResolvedValue({ id: 'signature-created-1', auditStatus: 'approved' }),
|
||||
};
|
||||
const service = new ReportMaterialsService(prisma as never, {} as never, smsConfig as never);
|
||||
|
||||
await expect(service.reviewImportItems('batch-identity-1', {
|
||||
decision: 'approve',
|
||||
itemIds: [item.id],
|
||||
reviewerId: 'reviewer-1',
|
||||
})).resolves.toMatchObject({ status: 'approved', approvedCount: 1, failedCount: 0 });
|
||||
expect(smsConfig.approveSignature).toHaveBeenCalledWith('signature-created-1', expect.objectContaining({
|
||||
reviewerId: 'reviewer-1',
|
||||
}));
|
||||
});
|
||||
|
||||
it('calculates generated batch totals and success rate from per-channel report tasks', async () => {
|
||||
const prisma = {
|
||||
reportMaterialBatch: {
|
||||
|
||||
@@ -3631,8 +3631,9 @@ npm run verify:phase8
|
||||
| TC-REPORT-MATERIAL-IMPORT-001 | 将含两行表头、文本列和营业执照/身份证等内嵌图片的 WPS 在线表格另存为 `.xlsx`,选择企业、应用和签名资料后解析。 | NestJS 读取真实工作表及图片锚点,返回列、组合表头、前十行和图片数预览;原文件写 MinIO,导入批次写 PostgreSQL;解析和提交审核均不直接修改签名、不建通道任务。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-002 | 将源列映射到签名及动态报备字段后提交导入,再进入短信签名审核的“导入批次审核”页签查看100行数据并一次通过其中勾选的多行。 | 每行先以新增/修改/无效状态落待审核明细;只有通过行才创建或修改真实签名并写审核人、审核时间,随后进入待生成资料池;未选行保持待审核,页面不要求逐行打开确认。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-003 | 导入引流资料,其中一行引用不存在或未审核签名;在引流审核页批量通过合法行并驳回部分行,不填写驳回原因。 | 合法行审核通过后创建/更新真实 `SmsDrainageInfo` 并进入待生成池;非法行保留行号和原因;空驳回原因可正常提交,同批其他行不受影响,也不自动创建通道报备任务。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-004 | 导入文件中同时包含已存在对象的修改和不存在对象的新增,提交审核前后分别读取业务表。 | 提交审核前业务表完全不变;审核页展示新增/修改及原数据快照;通过后才应用变更,重复点击已处理行不会再次递增材料版本或重复创建对象。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-005 | 分别在签名和引流审核页面按文件名、状态、时间筛选导入批次,翻页后选择整批或部分明细审核。 | 查询、总数和分页来自真实后端;批次汇总待审、通过、驳回、无效数量,刷新后保持一致。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-004 | 应用将短信签名、签名用途、企业名称和应用名称配置为必填报备字段;导入一份包含签名、用途和标准 Drawing 图片的 XLSX,审核通过后生成并导出报备资料。 | 导入审核从签名、企业、应用实体物化系统身份字段,显式映射值优先;真实缺失值仍被拦截。签名审核通过并进入待生成池,报备批次及导出文件使用真实 PostgreSQL/对象存储数据且图片可回读。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-005 | 导入文件中同时包含已存在对象的修改和不存在对象的新增,提交审核前后分别读取业务表。 | 提交审核前业务表完全不变;审核页展示新增/修改及原数据快照;通过后才应用变更,重复点击已处理行不会再次递增材料版本或重复创建对象。 |
|
||||
| TC-REPORT-MATERIAL-IMPORT-006 | 分别在签名和引流审核页面按文件名、状态、时间筛选导入批次,翻页后选择整批或部分明细审核。 | 查询、总数和分页来自真实后端;批次汇总待审、通过、驳回、无效数量,刷新后保持一致。 |
|
||||
| TC-REPORT-CHANNEL-FIELD-001 | 在同一通道分别打开签名和引流字段配置,添加字段、修改通道表头、上下排序、设置必填/列宽/图片宽高后保存并刷新。 | 两类配置相互独立且完整持久化;刷新后字段池、映射表头和顺序一致;重复字段、停用字段和非法尺寸由 API 拒绝或归一化。 |
|
||||
| TC-REPORT-BATCH-001 | 一个应用配置两个生效通道,选择一个待报备签名创建统一批次。 | 系统从真实应用路由展开两个通道,生成两个独立通道任务和两个 `.xlsx`;每个文件表头名称、列顺序和列宽均来自对应通道配置,批次可下载两份文件。 |
|
||||
| TC-REPORT-BATCH-002 | 两个通道对同一标准字段配置不同表头和顺序,并包含图片列,生成批次后分别用 WPS 打开。 | 两份工作簿各自使用对应通道映射,图片直接显示在数据行内且尺寸按通道配置;文件不是 URL 清单,文本与图片属于同一材料快照。 |
|
||||
@@ -5066,7 +5067,7 @@ npm run verify:phase8
|
||||
| TC-REPORT-WPS-002 | 分别上传10MB以上且不超过100MB、超过100MB、解压后超过500MB的报备XLSX | 第一类可进入解析;后两类分别在上传层或解包层明确拒绝;Nginx私有站点允许100MB业务文件及multipart开销,不扩大公网单条API边界 |
|
||||
| TC-REPORT-WPS-003 | 在批次和单条报备导出选择“系统 Excel 文件”或“WPS 单元格图片文件” | 默认保持ExcelJS Drawing格式;WPS选项生成DISPIMG与cellimages关系且图片按对应单元格可回读;两种格式均来自同一真实资料快照,不改变报备状态或短信链路 |
|
||||
| TC-REPORT-IMPORT-APPLICATION-001 | 未选择企业应用、选择其他企业的应用、选择当前企业应用后解析导入 | 前两种前后端均阻止导入;合法应用可解析并把applicationId写入导入批次,后续补资料只作用于该企业应用范围 |
|
||||
| TC-REPORT-FIELD-MODAL-001 | 打开签名或引流字段配置弹窗,添加字段并检查通用字段与移除按钮 | 左侧字段卡片高度不因添加改变;右侧默认包含同资料类型的通用字段;移除按钮为通用宽度、无红色填充,保存仍调用真实通道字段接口 |
|
||||
| TC-REPORT-FIELD-MODAL-001 | 打开签名或引流字段配置弹窗,在左侧字段池数量不同的情况下连续添加字段,并检查通用字段与移除按钮 | 左侧每个字段卡片始终固定为68px,不因可选字段数量或添加操作被Grid拉伸;右侧默认包含同资料类型的通用字段;移除按钮为通用宽度、无红色填充,保存仍调用真实通道字段接口 |
|
||||
| TC-REPORT-MATERIAL-DETAIL-001 | 查看包含当前图片字段、未删除历史字段及旧图片引用的报备资料 | 当前字段展示名称、代码、导出名及图片预览;未删除历史字段继续展示且同时显示字段名称和代码;图片可内联查看并保留下载入口,缺失内容显示明确占位 |
|
||||
| TC-REPORT-CHANNEL-IDENTITY-001 | 打开短信通道管理的报备详情 | 页面同时明确展示“通道名称”和“通道编号”,列表、筛选、状态修改和窄屏布局不受影响 |
|
||||
| TC-DASHBOARD-METRIC-ORDER-001 | 打开运营看板并按从左到右、从上到下读取指标 | 顺序为发送总量、消息分片数、总体成功率、到达率、活跃签名、消费金额、返还金额、计收金额、利润、利润率;所有数值继续来自真实API口径 |
|
||||
|
||||
@@ -4462,3 +4462,19 @@ git diff --check
|
||||
- 发布后三条Stream的`last-delivered-id`和`entries-read`与发布前逐项相同:commands为`1787806802603-0 / 130918`,results为`1787806917609-0 / 191696`,protocol logs为`1787806802967-0 / 40472`;全部保持`pending=0 / lag=0`,没有发送、补发、重投、重新入队或手工ACK短信,也没有修改余额、通道、客户或签名业务资料。
|
||||
- 工作站从测试环境实际下载主资源`index-DnhPgg2m.js`和`index-D9xR3Zhi.css`,SHA-256分别为`baa23f0aa62b3a8d1051718cca4a927316b4e2724244e3f39147888e3ea31a36`、`b193f9521c0104d35c645492cdf8c6e71e764349ff6bef59c8da77366e327cb4`,与服务器产物一致。真实浏览器读取到“聆界短信管理平台”运营登录页及用户名、密码、图形验证码和登录按钮;后续截图/控制台采集超时,且未使用平台管理员登录态,因此登录后功能与控制台仍不冒充已完成真实浏览器验收。
|
||||
- 服务器本轮临时上传包和`/tmp`脚本已在日志归档后清理,完整恢复点及上一运行目录保留。预生产`8.160.169.106`未访问、未部署、未覆盖、未回退或修改。
|
||||
|
||||
## 2026-09-04 通道字段池单卡片高度补救
|
||||
|
||||
- 重新检查确认上一轮只设置左侧字段按钮`min-height: 68px`,没有阻止父级CSS Grid在字段较少时把自动行拉伸到剩余高度;添加字段导致可选数量变化后,单个字段卡片仍会重新分配高度,原验收结论不成立。
|
||||
- 补救将字段池设为`align-content: start`并固定`grid-auto-rows: 68px`,同时把单个按钮的`height/min-height/max-height`统一为68px,确保签名和引流字段弹窗在任意可选数量下都不改变单卡片高度;不修改字段选择、保存接口或业务数据。
|
||||
- 回归测试直接验证添加前两个卡片及添加后剩余卡片的高度均为68px;定向Vitest 1文件2项、前端TypeScript和`git diff --check`通过。本轮按用户补充要求仅本地提交,不推送、不部署;测试环境继续运行`bb435fb0ac1e7812fcdb59950a4b13757899ab39`,预生产未访问。
|
||||
|
||||
## 2026-09-04 导入签名审核必填字段修复与全流程验收
|
||||
|
||||
- 测试环境失败批次`cmtmt7tge003zcaledaw1atog`和`cmtmtfp3o004scale3hdm2bjp`真实记录显示,XLSX中的签名、图片及其他动态字段已进入导入暂存,其中图片已写真实`FileObject`;应用同时把短信签名、签名用途、企业名称和应用名称配置为必填动态报备字段。导出阶段会从业务实体解析这些系统身份字段,导入审核落库阶段却只检查`signatureReportValues`,因此在创建签名前误报“缺少必填签名报备资料”。
|
||||
- 修复在导入审核落库前按既有导出语义从签名、企业和应用实体物化系统身份字段,当前签名用途为空时不伪造值,用户显式映射值覆盖系统派生值,未映射旧资料继续保留。新增纯函数与导入审核回归覆盖签名、用途、企业、应用和图片引用;对应功能用例为`TC-REPORT-MATERIAL-IMPORT-004`。
|
||||
- 定向报备服务17项、API全量53套610项通过;API与前端TypeScript、API生产构建、Vite生产构建及`git diff --check`通过。直接执行不带构建配置的API `tsc --noEmit`会把测试文件纳入且因未加载Jest类型失败,已改用项目正式`tsconfig.build.json`复核通过,未把错误命令结果冒充构建失败。
|
||||
- 功能提交为`41962e7a6e6cfd34b4313c8bf52d9345d37d8e10`,包含此前仅本地的字段卡片固定高度提交`0ec386e`。推送前fetch确认相对`origin/main`为`ahead 2 / behind 0`;远端随后返回认证失败,因此本节记录时尚未推送成功,没有改写或回退远端。
|
||||
- 测试环境发布前为`bb435fb0ac1e7812fcdb59950a4b13757899ab39`。发布包大小2766696字节,本地和服务器SHA-256均为`2d3c9a022ba1b6494a51e2ee0f3eb04eabb2513af25ba3631dae5ea70ea68783`;恢复点为`/opt/cmpp-platform-backups/import-review-fix-20260904T122649Z`,包含PostgreSQL custom dump、原运行目录、系统配置及发布包,四项SHA、`pg_restore --list`和两份tar可读性通过。95项migration齐全且无待执行项,没有执行迁移;最终`.deployed-commit=41962e7a6e6cfd34b4313c8bf52d9345d37d8e10`,旧运行目录为`/opt/cmpp-platform.previous-import-review-20260904T122649Z`。
|
||||
- 使用自行生成的标准Excel Drawing工作簿`签名导入全流程测试-含图片.xlsx`走真实HTTP API:解析得到6列、1张图片,导入批次`cmtmxvjuh000xluleh38c4sjy`审核结果`approved / approvedCount=1 / failedCount=0`;生成签名`cmtmxvjyh0014lule5o2t16m9`为`approved`,四项系统身份字段、图片`FileObject`及6个通道图片材料均在PostgreSQL存在。报备批次`cmtmxvk8c0043lules3upx5f2`完成并生成6个真实通道文件,下载ZIP中的首个XLSX包含`xl/media`图片。批次生成后签名`pendingReport=false`,没有发送、补发、重投或重新入队短信。
|
||||
- API验收使用本轮临时平台管理员,登录、近期认证、导入、审核、生成和下载均经过真实HTTP会话;结束时调用登出并删除临时账号,查询剩余数为0。浏览器已读取测试环境登录页、验证码控件和页面标题,控制台无warning/error;登录后页面交互需按浏览器安全规则由用户确认验证码后继续,不将当前登录页检查冒充已完成登录后验收。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ReportFieldMappingModal } from './ReportFieldMappingModal';
|
||||
|
||||
@@ -29,4 +29,30 @@ describe('ReportFieldMappingModal', () => {
|
||||
const remove = screen.getByRole('button', { name: '移除字段' });
|
||||
expect(remove).toHaveClass('channel-remove-field-button', 'ui-button--ghost');
|
||||
});
|
||||
|
||||
it('keeps every field-pool card at a fixed height instead of stretching the grid rows', () => {
|
||||
render(
|
||||
<ReportFieldMappingModal
|
||||
fields={[]}
|
||||
commonFields={[]}
|
||||
libraryFields={[
|
||||
{ id: 'field-1', code: 'field_1', name: '字段一', fieldType: 'string', status: 'active' },
|
||||
{ id: 'field-2', code: 'field_2', name: '字段二', fieldType: 'string', status: 'active' },
|
||||
]}
|
||||
reportType="signature"
|
||||
onClose={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const pool = document.querySelector<HTMLElement>('.channel-field-pool-list');
|
||||
expect(pool).toHaveStyle({ alignContent: 'start', gridAutoRows: '68px' });
|
||||
const initialCards = document.querySelectorAll<HTMLButtonElement>('.channel-field-pool-list > button');
|
||||
expect(initialCards).toHaveLength(2);
|
||||
expect(initialCards[0]).toHaveStyle({ height: '68px', maxHeight: '68px', minHeight: '68px' });
|
||||
|
||||
fireEvent.click(initialCards[0]);
|
||||
const remainingCards = document.querySelectorAll<HTMLButtonElement>('.channel-field-pool-list > button');
|
||||
expect(remainingCards).toHaveLength(1);
|
||||
expect(remainingCards[0]).toHaveStyle({ height: '68px', maxHeight: '68px', minHeight: '68px' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,9 +201,14 @@ export function ReportFieldMappingModal({
|
||||
prefix={<Search size={16} />}
|
||||
value={search}
|
||||
/>
|
||||
<div className="channel-field-pool-list">
|
||||
<div className="channel-field-pool-list" style={{ alignContent: 'start', gridAutoRows: '68px' }}>
|
||||
{available.map((field) => (
|
||||
<button key={String(field.id)} onClick={() => addField(field)} type="button">
|
||||
<button
|
||||
key={String(field.id)}
|
||||
onClick={() => addField(field)}
|
||||
style={{ height: 68, maxHeight: 68, minHeight: 68 }}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
<strong>{String(field.name ?? field.code)}</strong>
|
||||
<Tag tone="neutral">{fieldTypeLabel[String(field.fieldType)] ?? field.fieldType}</Tag>
|
||||
|
||||
@@ -6176,6 +6176,11 @@
|
||||
padding-right: var(--space-2);
|
||||
}
|
||||
|
||||
.channel-field-pool-list {
|
||||
align-content: start;
|
||||
grid-auto-rows: 68px;
|
||||
}
|
||||
|
||||
.channel-field-pool-list > button {
|
||||
align-items: center;
|
||||
background: var(--color-surface);
|
||||
@@ -6185,10 +6190,12 @@
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 58px;
|
||||
box-sizing: border-box;
|
||||
height: 68px;
|
||||
max-height: 68px;
|
||||
min-height: 68px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
text-align: left;
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.channel-remove-field-button {
|
||||
|
||||
Reference in New Issue
Block a user