cabloy 5.1.120 → 5.1.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +17 -0
- package/cabloy-docs/frontend/form-layout-guide.md +51 -20
- package/cabloy-docs/frontend/table-resource-crud-cookbook.md +31 -4
- package/cabloy-docs/frontend/zova-form-source-reading-map.md +7 -5
- package/cabloy-docs/frontend/zova-form-under-the-hood.md +5 -2
- package/cabloy-docs/fullstack/backend-metadata-to-frontend-table-actions.md +2 -2
- package/e2e/specs/cabloy-basic/basic.spec.ts +9 -0
- package/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +27 -2
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +27 -2
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/pnpm-lock.yaml +39 -39
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productSelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +24 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectResItem.tsx +23 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipmentRefundRace.test.ts +377 -24
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +24 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +4 -1
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +10 -3
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +3 -3
- package/zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx +25 -4
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +21 -4
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formLayout.ts +13 -4
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formLayout.ts +14 -4
- package/zova/src/suite-vendor/a-zova/package.json +3 -3
|
@@ -31,8 +31,8 @@ async function cleanup(fixture: IFixture) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function createPaidOrder(suffix: string): Promise<
|
|
35
|
-
|
|
34
|
+
async function createPaidOrder(fixture: IFixture, suffix: string): Promise<void> {
|
|
35
|
+
fixture.customerName = `shipment-refund-${suffix}`;
|
|
36
36
|
const customer = await app.bean.user.register({ name: fixture.customerName }, true);
|
|
37
37
|
fixture.userId = customer.id as number;
|
|
38
38
|
await app.bean.passport.signinMock(fixture.customerName as any);
|
|
@@ -61,7 +61,111 @@ async function createPaidOrder(suffix: string): Promise<IFixture> {
|
|
|
61
61
|
} finally {
|
|
62
62
|
await app.bean.passport.signout();
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runInMockCtx<TResult>(
|
|
67
|
+
customerName: string | undefined,
|
|
68
|
+
operation: () => Promise<TResult>,
|
|
69
|
+
): Promise<TResult> {
|
|
70
|
+
return await app.bean.executor.mockCtx(async () => {
|
|
71
|
+
if (customerName === undefined) {
|
|
72
|
+
await app.bean.passport.signinMock();
|
|
73
|
+
} else {
|
|
74
|
+
await app.bean.passport.signinMock(customerName as any);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return await operation();
|
|
78
|
+
} finally {
|
|
79
|
+
await app.bean.passport.signout();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function withTimeout<TResult>(promise: Promise<TResult>, message: string): Promise<TResult> {
|
|
85
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
86
|
+
try {
|
|
87
|
+
return await Promise.race([
|
|
88
|
+
promise,
|
|
89
|
+
new Promise<never>((_, reject) => {
|
|
90
|
+
timeout = setTimeout(() => reject(new Error(message)), 5_000);
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
} finally {
|
|
94
|
+
if (timeout) clearTimeout(timeout);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function waitForHolderStage(
|
|
99
|
+
entered: Promise<void>,
|
|
100
|
+
holder: Promise<unknown>,
|
|
101
|
+
label: string,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
return await withTimeout(
|
|
104
|
+
Promise.race([
|
|
105
|
+
entered,
|
|
106
|
+
holder.then(
|
|
107
|
+
() =>
|
|
108
|
+
Promise.reject(new Error(`${label} completed before reaching its lock-holding stage`)),
|
|
109
|
+
error => Promise.reject(error),
|
|
110
|
+
),
|
|
111
|
+
]),
|
|
112
|
+
`${label} did not reach its lock-holding stage`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitForPostgresWaiter(waiterPid: number, holderPid: number): Promise<void> {
|
|
117
|
+
const deadline = Date.now() + 5_000;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
const result = await app.bean.database.current.connection.raw(
|
|
120
|
+
`
|
|
121
|
+
select wait_event_type
|
|
122
|
+
from pg_stat_activity
|
|
123
|
+
where pid = ?
|
|
124
|
+
and ? = any(pg_blocking_pids(pid))
|
|
125
|
+
`,
|
|
126
|
+
[waiterPid, holderPid],
|
|
127
|
+
);
|
|
128
|
+
if (result.rows[0]) {
|
|
129
|
+
assert.equal(result.rows[0].wait_event_type, 'Lock');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
133
|
+
}
|
|
134
|
+
assert.fail(`PostgreSQL backend ${waiterPid} was not blocked by backend ${holderPid}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function assertPersistedRefundOutcome(
|
|
138
|
+
orderId: number,
|
|
139
|
+
expected: {
|
|
140
|
+
orderState: string;
|
|
141
|
+
refundState: string;
|
|
142
|
+
refundAttemptState: string;
|
|
143
|
+
orderAuditOperations: readonly string[];
|
|
144
|
+
refundAuditStates: readonly string[];
|
|
145
|
+
},
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const trade = app.scope('commerce-trade');
|
|
148
|
+
const payment = app.scope('commerce-payment');
|
|
149
|
+
const order = await trade.model.order.getById(orderId);
|
|
150
|
+
const shipments = await trade.model.shipment.select({ where: { orderId } });
|
|
151
|
+
const requests = await payment.model.refundRequest.select({ where: { orderId } });
|
|
152
|
+
const attempts = await payment.model.refundAttempt.select({
|
|
153
|
+
where: { refundRequestId: requests[0]?.id },
|
|
154
|
+
});
|
|
155
|
+
const orderAudits = await trade.model.orderAudit.select({ where: { orderId } });
|
|
156
|
+
const refundAudits = await payment.model.refundAudit.select({ where: { orderId } });
|
|
157
|
+
|
|
158
|
+
assert.equal(order?.state, expected.orderState);
|
|
159
|
+
assert.equal(shipments.length, 0);
|
|
160
|
+
assert.equal(requests.length, 1);
|
|
161
|
+
assert.equal(requests[0]?.state, expected.refundState);
|
|
162
|
+
assert.equal(attempts.length, 1);
|
|
163
|
+
assert.equal(attempts[0]?.state, expected.refundAttemptState);
|
|
164
|
+
for (const operation of expected.orderAuditOperations) {
|
|
165
|
+
assert.equal(orderAudits.filter(item => item.operation === operation).length, 1);
|
|
166
|
+
}
|
|
167
|
+
assert.equal(orderAudits.filter(item => item.operation === 'shipped').length, 0);
|
|
168
|
+
assert.deepEqual(refundAudits.map(item => item.toRefundState).sort(), expected.refundAuditStates);
|
|
65
169
|
}
|
|
66
170
|
|
|
67
171
|
describe('shipmentRefundRace.test.ts', { concurrency: false, sequential: true }, () => {
|
|
@@ -83,31 +187,65 @@ describe('shipmentRefundRace.test.ts', { concurrency: false, sequential: true },
|
|
|
83
187
|
const fixture: IFixture = {};
|
|
84
188
|
try {
|
|
85
189
|
await app.bean.executor.mockCtx(async () => {
|
|
86
|
-
|
|
190
|
+
await createPaidOrder(fixture, randomUUID().slice(0, 12));
|
|
87
191
|
});
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
192
|
+
const requestEntered = Promise.withResolvers<void>();
|
|
193
|
+
const releaseRequest = Promise.withResolvers<void>();
|
|
194
|
+
let requestPid: number | undefined;
|
|
195
|
+
const requestRefund = runInMockCtx(fixture.customerName, async () => {
|
|
196
|
+
return await app.scope('commerce-trade').service.order.requestRefundForTest(
|
|
197
|
+
fixture.orderId!,
|
|
198
|
+
{
|
|
92
199
|
reason: 'race request',
|
|
93
200
|
idempotencyKey: 'shipment-refund-race-request-1',
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
201
|
+
},
|
|
202
|
+
async stage => {
|
|
203
|
+
if (stage !== 'afterRefundRequestOrderState') return;
|
|
204
|
+
const result = await app.bean.database.current.connection.raw(
|
|
205
|
+
'select pg_backend_pid() as pid',
|
|
206
|
+
);
|
|
207
|
+
requestPid = result.rows[0].pid;
|
|
208
|
+
requestEntered.resolve();
|
|
209
|
+
await releaseRequest.promise;
|
|
210
|
+
},
|
|
211
|
+
);
|
|
98
212
|
});
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await app.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
213
|
+
const shipmentReady = Promise.withResolvers<void>();
|
|
214
|
+
const releaseShipment = Promise.withResolvers<void>();
|
|
215
|
+
let shipmentPid: number | undefined;
|
|
216
|
+
let ship: Promise<unknown> | undefined;
|
|
217
|
+
try {
|
|
218
|
+
await waitForHolderStage(requestEntered.promise, requestRefund, 'refund request');
|
|
219
|
+
assert.notEqual(requestPid, undefined);
|
|
220
|
+
ship = runInMockCtx(undefined, async () => {
|
|
221
|
+
return await app.scope('commerce-trade').service.order.shipForTest(
|
|
222
|
+
fixture.orderId!,
|
|
223
|
+
{
|
|
224
|
+
carrier: 'Cabloy Express',
|
|
225
|
+
trackingNumber: 'CAB-RFD-1',
|
|
226
|
+
},
|
|
227
|
+
async stage => {
|
|
228
|
+
if (stage !== 'beforeOrderLock') return;
|
|
229
|
+
const result = await app.bean.database.current.connection.raw(
|
|
230
|
+
'select pg_backend_pid() as pid',
|
|
231
|
+
);
|
|
232
|
+
shipmentPid = result.rows[0].pid;
|
|
233
|
+
shipmentReady.resolve();
|
|
234
|
+
await releaseShipment.promise;
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
await waitForHolderStage(shipmentReady.promise, ship, 'shipment');
|
|
239
|
+
assert.notEqual(shipmentPid, undefined);
|
|
240
|
+
releaseShipment.resolve();
|
|
241
|
+
await app.bean.executor.mockCtx(async () => {
|
|
242
|
+
await waitForPostgresWaiter(shipmentPid!, requestPid!);
|
|
243
|
+
});
|
|
244
|
+
} finally {
|
|
245
|
+
releaseShipment.resolve();
|
|
246
|
+
releaseRequest.resolve();
|
|
247
|
+
await Promise.allSettled([requestRefund, ship].filter(Boolean));
|
|
248
|
+
}
|
|
111
249
|
await app.bean.executor.mockCtx(async () => {
|
|
112
250
|
const trade = app.scope('commerce-trade');
|
|
113
251
|
const payment = app.scope('commerce-payment');
|
|
@@ -140,4 +278,219 @@ describe('shipmentRefundRace.test.ts', { concurrency: false, sequential: true },
|
|
|
140
278
|
});
|
|
141
279
|
}
|
|
142
280
|
});
|
|
281
|
+
|
|
282
|
+
it('rejects shipment when refund approval contends for the locked refund-requested order', async t => {
|
|
283
|
+
if (process.env.DATABASE_DEFAULT_CLIENT !== 'pg') {
|
|
284
|
+
t.skip('requires PostgreSQL row-lock contention');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const fixture: IFixture = {};
|
|
288
|
+
try {
|
|
289
|
+
await app.bean.executor.mockCtx(async () => {
|
|
290
|
+
await createPaidOrder(fixture, randomUUID().slice(0, 12));
|
|
291
|
+
});
|
|
292
|
+
await runInMockCtx(fixture.customerName, async () => {
|
|
293
|
+
await app.scope('commerce-trade').service.order.requestRefund(fixture.orderId!, {
|
|
294
|
+
reason: 'race approval request',
|
|
295
|
+
idempotencyKey: 'shipment-refund-race-approval-request-1',
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
const entered = Promise.withResolvers<void>();
|
|
299
|
+
const release = Promise.withResolvers<void>();
|
|
300
|
+
let holderPid: number | undefined;
|
|
301
|
+
const approveRefund = runInMockCtx(undefined, async () => {
|
|
302
|
+
return await app.scope('commerce-trade').service.order.reviewRefundForTest(
|
|
303
|
+
fixture.orderId!,
|
|
304
|
+
{
|
|
305
|
+
reason: 'race approval',
|
|
306
|
+
idempotencyKey: 'shipment-refund-race-approval-1',
|
|
307
|
+
},
|
|
308
|
+
'approved',
|
|
309
|
+
async stage => {
|
|
310
|
+
if (stage !== 'afterRefundReviewOrderState') return;
|
|
311
|
+
const result = await app.bean.database.current.connection.raw(
|
|
312
|
+
'select pg_backend_pid() as pid',
|
|
313
|
+
);
|
|
314
|
+
holderPid = result.rows[0].pid;
|
|
315
|
+
entered.resolve();
|
|
316
|
+
await release.promise;
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
const shipmentReady = Promise.withResolvers<void>();
|
|
321
|
+
const releaseShipment = Promise.withResolvers<void>();
|
|
322
|
+
let shipmentPid: number | undefined;
|
|
323
|
+
let ship: Promise<unknown> | undefined;
|
|
324
|
+
let results: PromiseSettledResult<unknown>[] | undefined;
|
|
325
|
+
try {
|
|
326
|
+
await waitForHolderStage(entered.promise, approveRefund, 'refund approval');
|
|
327
|
+
assert.notEqual(holderPid, undefined);
|
|
328
|
+
ship = runInMockCtx(undefined, async () => {
|
|
329
|
+
return await app.scope('commerce-trade').service.order.shipForTest(
|
|
330
|
+
fixture.orderId!,
|
|
331
|
+
{
|
|
332
|
+
carrier: 'Cabloy Express',
|
|
333
|
+
trackingNumber: 'CAB-RFD-APPROVAL-1',
|
|
334
|
+
},
|
|
335
|
+
async stage => {
|
|
336
|
+
if (stage !== 'beforeOrderLock') return;
|
|
337
|
+
const result = await app.bean.database.current.connection.raw(
|
|
338
|
+
'select pg_backend_pid() as pid',
|
|
339
|
+
);
|
|
340
|
+
shipmentPid = result.rows[0].pid;
|
|
341
|
+
shipmentReady.resolve();
|
|
342
|
+
await releaseShipment.promise;
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
await waitForHolderStage(shipmentReady.promise, ship, 'shipment');
|
|
347
|
+
assert.notEqual(shipmentPid, undefined);
|
|
348
|
+
releaseShipment.resolve();
|
|
349
|
+
await app.bean.executor.mockCtx(async () => {
|
|
350
|
+
await waitForPostgresWaiter(shipmentPid!, holderPid!);
|
|
351
|
+
});
|
|
352
|
+
} finally {
|
|
353
|
+
releaseShipment.resolve();
|
|
354
|
+
release.resolve();
|
|
355
|
+
results = await Promise.allSettled([approveRefund, ship].filter(Boolean));
|
|
356
|
+
}
|
|
357
|
+
const [approvalResult, shipmentResult] = results!;
|
|
358
|
+
const message = JSON.stringify(results);
|
|
359
|
+
assert.equal(approvalResult.status, 'fulfilled', message);
|
|
360
|
+
assert.equal(shipmentResult.status, 'rejected', message);
|
|
361
|
+
if (approvalResult.status !== 'fulfilled' || shipmentResult.status !== 'rejected') return;
|
|
362
|
+
assert.deepEqual(
|
|
363
|
+
[
|
|
364
|
+
approvalResult.value.orderState,
|
|
365
|
+
approvalResult.value.refundState,
|
|
366
|
+
approvalResult.value.refundAttemptState,
|
|
367
|
+
],
|
|
368
|
+
['refund_approved', 'approved', 'created'],
|
|
369
|
+
);
|
|
370
|
+
assert.equal(shipmentResult.reason?.code, 409);
|
|
371
|
+
await app.bean.executor.mockCtx(async () => {
|
|
372
|
+
await assertPersistedRefundOutcome(fixture.orderId!, {
|
|
373
|
+
orderState: 'refund_approved',
|
|
374
|
+
refundState: 'approved',
|
|
375
|
+
refundAttemptState: 'created',
|
|
376
|
+
orderAuditOperations: ['refund_requested', 'refund_approved'],
|
|
377
|
+
refundAuditStates: ['approved', 'requested'],
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
} finally {
|
|
381
|
+
await app.bean.executor.mockCtx(async () => {
|
|
382
|
+
await cleanup(fixture);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('rejects shipment when successful refund execution contends for the locked approved order', async t => {
|
|
388
|
+
if (process.env.DATABASE_DEFAULT_CLIENT !== 'pg') {
|
|
389
|
+
t.skip('requires PostgreSQL row-lock contention');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const fixture: IFixture = {};
|
|
393
|
+
try {
|
|
394
|
+
await app.bean.executor.mockCtx(async () => {
|
|
395
|
+
await createPaidOrder(fixture, randomUUID().slice(0, 12));
|
|
396
|
+
});
|
|
397
|
+
await runInMockCtx(fixture.customerName, async () => {
|
|
398
|
+
await app.scope('commerce-trade').service.order.requestRefund(fixture.orderId!, {
|
|
399
|
+
reason: 'race execution request',
|
|
400
|
+
idempotencyKey: 'shipment-refund-race-execution-request-1',
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
await runInMockCtx(undefined, async () => {
|
|
404
|
+
await app.scope('commerce-trade').service.order.approveRefund(fixture.orderId!, {
|
|
405
|
+
reason: 'race execution approval',
|
|
406
|
+
idempotencyKey: 'shipment-refund-race-execution-approval-1',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
const entered = Promise.withResolvers<void>();
|
|
410
|
+
const release = Promise.withResolvers<void>();
|
|
411
|
+
let holderPid: number | undefined;
|
|
412
|
+
const executeRefund = runInMockCtx(undefined, async () => {
|
|
413
|
+
return await app.scope('commerce-trade').service.order.applyRefundOutcomeForTest(
|
|
414
|
+
fixture.orderId!,
|
|
415
|
+
{
|
|
416
|
+
outcome: 'succeeded',
|
|
417
|
+
idempotencyKey: 'shipment-refund-race-execution-outcome-1',
|
|
418
|
+
},
|
|
419
|
+
async stage => {
|
|
420
|
+
if (stage !== 'afterRefundOutcomeOrderState') return;
|
|
421
|
+
const result = await app.bean.database.current.connection.raw(
|
|
422
|
+
'select pg_backend_pid() as pid',
|
|
423
|
+
);
|
|
424
|
+
holderPid = result.rows[0].pid;
|
|
425
|
+
entered.resolve();
|
|
426
|
+
await release.promise;
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
const shipmentReady = Promise.withResolvers<void>();
|
|
431
|
+
const releaseShipment = Promise.withResolvers<void>();
|
|
432
|
+
let shipmentPid: number | undefined;
|
|
433
|
+
let ship: Promise<unknown> | undefined;
|
|
434
|
+
let results: PromiseSettledResult<unknown>[] | undefined;
|
|
435
|
+
try {
|
|
436
|
+
await waitForHolderStage(entered.promise, executeRefund, 'refund execution');
|
|
437
|
+
assert.notEqual(holderPid, undefined);
|
|
438
|
+
ship = runInMockCtx(undefined, async () => {
|
|
439
|
+
return await app.scope('commerce-trade').service.order.shipForTest(
|
|
440
|
+
fixture.orderId!,
|
|
441
|
+
{
|
|
442
|
+
carrier: 'Cabloy Express',
|
|
443
|
+
trackingNumber: 'CAB-RFD-EXECUTION-1',
|
|
444
|
+
},
|
|
445
|
+
async stage => {
|
|
446
|
+
if (stage !== 'beforeOrderLock') return;
|
|
447
|
+
const result = await app.bean.database.current.connection.raw(
|
|
448
|
+
'select pg_backend_pid() as pid',
|
|
449
|
+
);
|
|
450
|
+
shipmentPid = result.rows[0].pid;
|
|
451
|
+
shipmentReady.resolve();
|
|
452
|
+
await releaseShipment.promise;
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
await waitForHolderStage(shipmentReady.promise, ship, 'shipment');
|
|
457
|
+
assert.notEqual(shipmentPid, undefined);
|
|
458
|
+
releaseShipment.resolve();
|
|
459
|
+
await app.bean.executor.mockCtx(async () => {
|
|
460
|
+
await waitForPostgresWaiter(shipmentPid!, holderPid!);
|
|
461
|
+
});
|
|
462
|
+
} finally {
|
|
463
|
+
releaseShipment.resolve();
|
|
464
|
+
release.resolve();
|
|
465
|
+
results = await Promise.allSettled([executeRefund, ship].filter(Boolean));
|
|
466
|
+
}
|
|
467
|
+
const [refundResult, shipmentResult] = results!;
|
|
468
|
+
const message = JSON.stringify(results);
|
|
469
|
+
assert.equal(refundResult.status, 'fulfilled', message);
|
|
470
|
+
assert.equal(shipmentResult.status, 'rejected', message);
|
|
471
|
+
if (refundResult.status !== 'fulfilled' || shipmentResult.status !== 'rejected') return;
|
|
472
|
+
assert.deepEqual(
|
|
473
|
+
[
|
|
474
|
+
refundResult.value.orderState,
|
|
475
|
+
refundResult.value.refundState,
|
|
476
|
+
refundResult.value.refundAttemptState,
|
|
477
|
+
],
|
|
478
|
+
['refunded', 'refunded', 'succeeded'],
|
|
479
|
+
);
|
|
480
|
+
assert.equal(shipmentResult.reason?.code, 409);
|
|
481
|
+
await app.bean.executor.mockCtx(async () => {
|
|
482
|
+
await assertPersistedRefundOutcome(fixture.orderId!, {
|
|
483
|
+
orderState: 'refunded',
|
|
484
|
+
refundState: 'refunded',
|
|
485
|
+
refundAttemptState: 'succeeded',
|
|
486
|
+
orderAuditOperations: ['refund_requested', 'refund_approved', 'refunded'],
|
|
487
|
+
refundAuditStates: ['approved', 'refunded', 'requested'],
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
} finally {
|
|
491
|
+
await app.bean.executor.mockCtx(async () => {
|
|
492
|
+
await cleanup(fixture);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
});
|
|
143
496
|
});
|
|
@@ -16,7 +16,30 @@ export interface IDtoOptionsRecordSelectResItem extends IDecoratorDtoOptions {}
|
|
|
16
16
|
blocks: [
|
|
17
17
|
ZovaRender.block('basic-page:blockPage', {
|
|
18
18
|
blocks: [
|
|
19
|
-
ZovaRender.block('basic-page:blockFilter'
|
|
19
|
+
ZovaRender.block('basic-page:blockFilter', {
|
|
20
|
+
formFieldLayout: { inline: true },
|
|
21
|
+
blocks: [
|
|
22
|
+
ZovaRender.block('basic-form:blockFormLayout', {
|
|
23
|
+
formLayout: {
|
|
24
|
+
children: [
|
|
25
|
+
{
|
|
26
|
+
type: 'section',
|
|
27
|
+
layout: 'flow',
|
|
28
|
+
children: [
|
|
29
|
+
{ type: 'field', name: 'name' },
|
|
30
|
+
{ type: 'field', name: 'studentId' },
|
|
31
|
+
{ type: 'field', name: 'createdAt' },
|
|
32
|
+
{
|
|
33
|
+
type: 'block',
|
|
34
|
+
block: ZovaRender.block('basic-page:blockFilterActions'),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
20
43
|
ZovaRender.block('basic-page:blockToolbarBulk', {
|
|
21
44
|
actions: [ZovaRender.tableActionBulk('basic-table:actionCreate')],
|
|
22
45
|
}),
|
|
@@ -28,12 +28,15 @@ export interface IDtoOptionsStudentSelectResItem extends IDecoratorDtoOptions {}
|
|
|
28
28
|
{ type: 'field', name: 'name' },
|
|
29
29
|
{ type: 'field', name: 'level' },
|
|
30
30
|
{ type: 'field', name: 'createdAt' },
|
|
31
|
+
{
|
|
32
|
+
type: 'block',
|
|
33
|
+
block: ZovaRender.block('basic-page:blockFilterActions'),
|
|
34
|
+
},
|
|
31
35
|
],
|
|
32
36
|
},
|
|
33
37
|
],
|
|
34
38
|
},
|
|
35
39
|
}),
|
|
36
|
-
ZovaRender.block('basic-page:blockFilterActions'),
|
|
37
40
|
],
|
|
38
41
|
}),
|
|
39
42
|
ZovaRender.block('basic-page:blockToolbarBulk', {
|
|
@@ -57,16 +57,23 @@ describe('student.test.ts', () => {
|
|
|
57
57
|
assert.equal(filterBlock?.options?.formFieldLayout?.inline, true);
|
|
58
58
|
assert.deepEqual(
|
|
59
59
|
filterBlock?.options?.blocks?.map(item => item.render),
|
|
60
|
-
['basic-form:blockFormLayout'
|
|
60
|
+
['basic-form:blockFormLayout'],
|
|
61
61
|
);
|
|
62
62
|
const formLayout = filterBlock?.options?.blocks?.[0]?.options?.formLayout;
|
|
63
|
+
const filterLayoutChildren = formLayout?.children[0]?.children;
|
|
63
64
|
assert.equal(formLayout?.children[0]?.layout, 'flow');
|
|
64
65
|
assert.equal(formLayout?.children[0]?.columns, undefined);
|
|
66
|
+
assert.equal(filterLayoutChildren?.length, 4);
|
|
65
67
|
assert.deepEqual(
|
|
66
|
-
|
|
68
|
+
filterLayoutChildren?.map(item => item.type),
|
|
69
|
+
['field', 'field', 'field', 'block'],
|
|
70
|
+
);
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
filterLayoutChildren?.slice(0, 3).map(item => item.name),
|
|
67
73
|
['name', 'level', 'createdAt'],
|
|
68
74
|
);
|
|
69
|
-
assert.equal(
|
|
75
|
+
assert.equal(filterLayoutChildren?.[2]?.span, undefined);
|
|
76
|
+
assert.equal(filterLayoutChildren?.[3]?.block?.render, 'basic-page:blockFilterActions');
|
|
70
77
|
});
|
|
71
78
|
});
|
|
72
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.142",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "A vue3 framework with ioc",
|
|
6
6
|
"keywords": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"zova-core": "^5.1.79",
|
|
49
|
-
"zova-suite-a-zova": "^5.1.
|
|
49
|
+
"zova-suite-a-zova": "^5.1.141"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"clean-package": "^2.2.0",
|
package/zova/pnpm-lock.yaml
CHANGED
|
@@ -849,7 +849,7 @@ importers:
|
|
|
849
849
|
specifier: ^5.1.79
|
|
850
850
|
version: link:../zova-core
|
|
851
851
|
zova-suite-a-zova:
|
|
852
|
-
specifier: ^5.1.
|
|
852
|
+
specifier: ^5.1.141
|
|
853
853
|
version: link:../../src/suite-vendor/a-zova
|
|
854
854
|
devDependencies:
|
|
855
855
|
clean-package:
|
|
@@ -986,7 +986,7 @@ importers:
|
|
|
986
986
|
specifier: ^5.1.24
|
|
987
987
|
version: link:modules/a-fetch
|
|
988
988
|
zova-module-a-form:
|
|
989
|
-
specifier: ^5.1.
|
|
989
|
+
specifier: ^5.1.46
|
|
990
990
|
version: link:modules/a-form
|
|
991
991
|
zova-module-a-icon:
|
|
992
992
|
specifier: ^5.1.26
|
|
@@ -1004,7 +1004,7 @@ importers:
|
|
|
1004
1004
|
specifier: ^5.1.33
|
|
1005
1005
|
version: link:modules/a-model
|
|
1006
1006
|
zova-module-a-openapi:
|
|
1007
|
-
specifier: ^5.1.
|
|
1007
|
+
specifier: ^5.1.47
|
|
1008
1008
|
version: link:modules/a-openapi
|
|
1009
1009
|
zova-module-a-router:
|
|
1010
1010
|
specifier: ^5.1.31
|
package/zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx
CHANGED
|
@@ -2,7 +2,9 @@ import type { IComponentOptions } from 'zova';
|
|
|
2
2
|
import type {
|
|
3
3
|
IJsxRenderContextForm,
|
|
4
4
|
IResolvedFormLayout,
|
|
5
|
+
IResolvedFormLayoutBlock,
|
|
5
6
|
IResolvedFormLayoutField,
|
|
7
|
+
IResolvedFormLayoutLeaf,
|
|
6
8
|
IResolvedFormLayoutGroup,
|
|
7
9
|
IResolvedFormLayoutNode,
|
|
8
10
|
IResolvedFormLayoutSection,
|
|
@@ -78,6 +80,8 @@ export class ControllerBlockFormLayout extends BeanControllerBase {
|
|
|
78
80
|
switch (node.type) {
|
|
79
81
|
case 'field':
|
|
80
82
|
return this._renderField(node);
|
|
83
|
+
case 'block':
|
|
84
|
+
return this._renderBlock(node);
|
|
81
85
|
case 'group':
|
|
82
86
|
return this._renderGroup(node);
|
|
83
87
|
case 'section':
|
|
@@ -87,11 +91,28 @@ export class ControllerBlockFormLayout extends BeanControllerBase {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
private
|
|
91
|
-
const { $$form } = this.$$renderContext;
|
|
94
|
+
private _renderLeaf(node: IResolvedFormLayoutLeaf, sectionLayout?: 'grid' | 'flow') {
|
|
92
95
|
const className =
|
|
93
96
|
sectionLayout === 'flow' ? 'min-w-0 max-w-full' : this._gridClasses('col-span', node.span);
|
|
94
|
-
return <div class={className}>{
|
|
97
|
+
return <div class={className}>{this._renderLeafContent(node)}</div>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private _renderLeafContent(node: IResolvedFormLayoutLeaf) {
|
|
101
|
+
if (node.type === 'field') return this.$$renderContext.$$form.renderField(node.name);
|
|
102
|
+
return this._renderBlockContent(node);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private _renderField(node: IResolvedFormLayoutField) {
|
|
106
|
+
return this._renderLeaf(node);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _renderBlock(node: IResolvedFormLayoutBlock) {
|
|
110
|
+
return this._renderLeaf(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private _renderBlockContent(node: IResolvedFormLayoutBlock) {
|
|
114
|
+
const { $celScope, $jsx } = this.$$renderContext;
|
|
115
|
+
return $jsx.render(node.block.render!, node.block.options, $celScope, this.$$renderContext);
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
private _renderGroup(node: IResolvedFormLayoutGroup) {
|
|
@@ -114,7 +135,7 @@ export class ControllerBlockFormLayout extends BeanControllerBase {
|
|
|
114
135
|
<section>
|
|
115
136
|
{!!node.title && <h3 class="mb-1 text-lg font-semibold">{node.title}</h3>}
|
|
116
137
|
{!!node.description && <p class="mb-4 text-sm text-base-content/70">{node.description}</p>}
|
|
117
|
-
<div class={className}>{node.children.map(child => this.
|
|
138
|
+
<div class={className}>{node.children.map(child => this._renderLeaf(child, layout))}</div>
|
|
118
139
|
</section>
|
|
119
140
|
);
|
|
120
141
|
}
|
|
@@ -411,6 +411,7 @@ export class ControllerForm<
|
|
|
411
411
|
if (node.type === 'field') {
|
|
412
412
|
return this.formState.fieldMeta[node.name]?.errors?.length ? 1 : 0;
|
|
413
413
|
}
|
|
414
|
+
if (node.type === 'block') return 0;
|
|
414
415
|
return node.children.reduce((count, child) => {
|
|
415
416
|
return count + this._getFormLayoutErrorFieldCount(child);
|
|
416
417
|
}, 0);
|