feat: expose readable submission choices
This commit is contained in:
@@ -62,6 +62,13 @@ function findRecordByKey(value: unknown, key: string): Record<string, unknown> |
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findRecordsByKey(value: unknown, key: string): Array<Record<string, unknown>> {
|
||||
if (Array.isArray(value)) return value.flatMap((item) => findRecordsByKey(item, key));
|
||||
if (!value || typeof value !== "object") return [];
|
||||
const record = value as Record<string, unknown>;
|
||||
return [...(record.key === key ? [record] : []), ...Object.values(record).flatMap((item) => findRecordsByKey(item, key))];
|
||||
}
|
||||
|
||||
test("Xft OpenAPI maps the authenticated DingTalk user and signs encrypted requests", async () => {
|
||||
const calls: Array<{ path: string; body: Record<string, unknown>; headers: Headers; url: URL }> = [];
|
||||
const fakeFetch: typeof fetch = async (input, init) => {
|
||||
@@ -477,10 +484,91 @@ test("all formal Xft expense fields declare a user-readable expense type option
|
||||
const field = findRecordByKey(schema.inputContract, fieldKey);
|
||||
assert.equal(field?.label, "费用类型");
|
||||
assert.deepEqual(field?.optionSource, { tool: "approval_get_submission_options", kind: "expense_type" });
|
||||
assert.deepEqual(field?.selectionCapability, {
|
||||
status: "supported", userInput: "label", submitValue: "code", exposeInternalValue: false,
|
||||
});
|
||||
assert.doesNotMatch(String(field?.description ?? ""), /要求用户.*编码/);
|
||||
}
|
||||
});
|
||||
|
||||
test("all formal Xft city and enum choices expose readable labels while unresolved references fail closed", async () => {
|
||||
const records = [
|
||||
{ eventSeqId: 21, eventNumber: "EXP-21", eventName: "费用申请", eventStatus: "1", eventType: "EXPENSE_APPLY" },
|
||||
{ eventSeqId: 22, eventNumber: "ENT-22", eventName: "招待申请", eventStatus: "1", eventType: "ENTERTAINMENT_APPLY" },
|
||||
{ eventSeqId: 23, eventNumber: "TRIP-23", eventName: "差旅申请", eventStatus: "1", eventType: "TRAVEL_APPLY" },
|
||||
{ eventSeqId: 24, eventNumber: "REIM-24", eventName: "费用报销", eventStatus: "1", eventType: "COMMON_REIM" },
|
||||
{ eventSeqId: 25, eventNumber: "TR-25", eventName: "差旅报销", eventStatus: "1", eventType: "TRAVEL_REIM" },
|
||||
];
|
||||
const client = new XftOpenApiClient(config(), async (input) => {
|
||||
const path = new URL(String(input)).pathname;
|
||||
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
|
||||
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: records } });
|
||||
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [] });
|
||||
throw new Error(`unexpected ${path}`);
|
||||
});
|
||||
const templates = await client.listSubmissionTemplates({ userId: "ding-u1" }, 20);
|
||||
const schemas = await Promise.all(templates.map((template) => client.getSubmissionSchema({ userId: "ding-u1" }, template)));
|
||||
const combined = schemas.map((schema) => schema.inputContract);
|
||||
for (const key of ["payCityCode", "entertainmentCityCode", "departCityCode", "destinationCityCode", "departureCode", "destinationCode"]) {
|
||||
const matches = findRecordsByKey(combined, key);
|
||||
assert.ok(matches.length > 0, `${key} should exist`);
|
||||
for (const field of matches) {
|
||||
assert.deepEqual(field.optionSource, { tool: "approval_get_submission_options", kind: "city" });
|
||||
assert.equal((field.selectionCapability as Record<string, unknown>)?.exposeInternalValue, false);
|
||||
assert.doesNotMatch(String(field.label), /编码/);
|
||||
}
|
||||
}
|
||||
const travelMode = findRecordByKey(combined, "travelMode");
|
||||
assert.equal((travelMode?.enumLabels as Record<string, string>)?.AirPlane, "飞机");
|
||||
assert.equal((travelMode?.enumLabels as Record<string, string>)?.RideHailing, "网约车");
|
||||
assert.equal((findRecordByKey(combined, "roundTrip")?.enumLabels as Record<string, string>)?.Y, "往返");
|
||||
assert.equal((findRecordByKey(combined, "beginTimePrecision")?.enumLabels as Record<string, string>)?.AM, "上午");
|
||||
assert.equal((findRecordByKey(combined, "currencyCode")?.enumLabels as Record<string, string>)?.CNY, "人民币");
|
||||
assert.equal((findRecordByKey(combined, "hotelStar")?.enumLabels as Record<string, string>)?.["1"], "五星");
|
||||
for (const key of ["peerEmpNumbers", "participants", "customArchiveOptionNumbers", "aircraftCabin", "trainCabin", "shipCabin", "relatedBillId"]) {
|
||||
const field = findRecordByKey(combined, key);
|
||||
assert.equal((field?.selectionCapability as Record<string, unknown>)?.status, "client_only", key);
|
||||
assert.doesNotMatch(String(field?.label ?? ""), /工号|编码| ID/);
|
||||
}
|
||||
});
|
||||
|
||||
test("live Xft custom choices normalize labels recursively and unsupported selectors are explicit", async () => {
|
||||
const templateRecord = {
|
||||
eventSeqId: 31, eventNumber: "CUSTOM-31", eventName: "费用申请", eventStatus: "1", eventType: "EXPENSE_APPLY",
|
||||
};
|
||||
const client = new XftOpenApiClient(config(), async (input) => {
|
||||
const path = new URL(String(input)).pathname;
|
||||
if (path.includes("user-mapping")) return encrypted({ code: "0", data: { records: [{ thirdStaffId: "ding-u1", enterpriseUserId: "EMP-1" }] } });
|
||||
if (path.endsWith("form/query-event-list")) return encrypted({ code: "0", data: { list: [templateRecord] } });
|
||||
if (path.includes("form/customFields/eventSeqId")) return encrypted({ code: "0", data: [
|
||||
{ fieldNumber: "S1", name: "餐别", type: "SELECT", required: true, options: [
|
||||
{ optionCode: "LUNCH", optionName: "午餐" }, { optionCode: "DINNER", optionName: "晚餐" },
|
||||
] },
|
||||
{ fieldNumber: "S2", name: "无名称映射", type: "SELECT", required: false, options: ["1", "2"] },
|
||||
{ fieldNumber: "P1", name: "参与人员", type: "PERSON", required: true },
|
||||
{ fieldNumber: "D1", name: "成本部门", type: "ADV_DEPARTMENT", required: true },
|
||||
{ fieldNumber: "T1", name: "明细", type: "TABLE", required: true, columns: [
|
||||
{ titleCode: "TS", name: "用餐类型", type: "SELECT", required: true, options: [{ code: "TEAM", label: "团队餐" }] },
|
||||
{ titleCode: "TP", name: "同行人", type: "PERSON", required: false },
|
||||
] },
|
||||
] });
|
||||
throw new Error(`unexpected ${path}`);
|
||||
});
|
||||
const template = (await client.listSubmissionTemplates({ userId: "ding-u1" }, 10))[0]!;
|
||||
const fields = (await client.getSubmissionSchema({ userId: "ding-u1" }, template)).fields;
|
||||
const select = fields.find((field) => field.fieldNumber === "S1")!;
|
||||
assert.deepEqual(select.options, [{ code: "LUNCH", label: "午餐" }, { code: "DINNER", label: "晚餐" }]);
|
||||
assert.equal(select.selectionCapability?.status, "supported");
|
||||
assert.equal(fields.find((field) => field.fieldNumber === "S2")?.selectionCapability?.status, "client_only");
|
||||
assert.equal(fields.find((field) => field.fieldNumber === "P1")?.selectionCapability?.status, "client_only");
|
||||
assert.deepEqual(fields.find((field) => field.fieldNumber === "D1")?.optionSource, {
|
||||
tool: "approval_get_submission_options", kind: "department",
|
||||
});
|
||||
const tableFields = fields.find((field) => field.fieldNumber === "T1")?.itemFields ?? [];
|
||||
assert.deepEqual(tableFields.find((field) => field.titleCode === "TS")?.options, [{ code: "TEAM", label: "团队餐" }]);
|
||||
assert.equal((tableFields.find((field) => field.titleCode === "TP")?.selectionCapability as Record<string, unknown>)?.status, "client_only");
|
||||
});
|
||||
|
||||
test("Xft expense types prefer user-readable live template options and fail closed when unavailable", async () => {
|
||||
const templateRecord = {
|
||||
eventSeqId: 8, eventNumber: "EXP-8", eventName: "招待费用申请单", eventStatus: "1", eventType: "EXPENSE_APPLY",
|
||||
|
||||
Reference in New Issue
Block a user