cabloy 5.1.162 → 5.1.164
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 +16 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/repo-docs/backend/shared-rbac-architecture.md +4 -0
- package/repo-docs/blogs/ai-spec-driven-development/cover-en-v1.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/implementation-burndown.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/implementation-gantt.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/index.md +217 -0
- package/repo-docs/blogs/index.md +15 -0
- package/repo-docs/editions/overview.md +2 -0
- package/repo-docs/frontend/environment-config-guide.md +43 -10
- package/repo-docs/frontend/resource-list-page-deep-dive.md +14 -1
- package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +20 -0
- package/repo-docs/frontend/table-guide.md +42 -0
- package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -1
- package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
- package/repo-e2e/specs/cabloy-basic.spec.ts +249 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +17 -6
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +12 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +12 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +8 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +1 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +1 -0
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +9 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +9 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentDeleteBulk.tsx +14 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +7 -1
- package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +6 -0
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +114 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/index.ts +24 -3
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/locales.ts +18 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacResourceBulk.ts +50 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/errors.ts +18 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/en-us.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/zh-cn.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/resourceBulk.test.ts +128 -0
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/create/component/boilerplateTableActionBulk/controller.tsx_ +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -12
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +66 -7
- package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +18 -0
- package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-commands/src/.metadata/index.ts +16 -0
- package/zova/src/suite/cabloy-basic/modules/basic-commands/src/bean/command.deleteBulk.tsx +35 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +143 -7
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -2
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +76 -4
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +6 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +84 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +99 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts +31 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +5 -2
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +57 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +35 -7
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +4 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +4 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/selectedIds.ts +15 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +57 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/selectedIds.test.ts +20 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-command/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/beanCommandSelectedBase.ts +33 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/types/command.ts +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/test/lib/beanCommandSelectedBase.test.ts +47 -0
- 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/action.ts +3 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +19 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +86 -26
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +30 -6
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +2 -0
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Locator, Page, Route } from '@playwright/test';
|
|
1
|
+
import type { Locator, Page, Request, Route } from '@playwright/test';
|
|
2
2
|
|
|
3
3
|
import { expect, test } from '@playwright/test';
|
|
4
4
|
|
|
@@ -171,7 +171,9 @@ test(
|
|
|
171
171
|
expect(documentResponse?.ok()).toBeTruthy();
|
|
172
172
|
await expect(page).toHaveURL(/\/$/);
|
|
173
173
|
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
174
|
-
await expect(
|
|
174
|
+
await expect(
|
|
175
|
+
page.getByRole('heading', { name: 'Vona integrated SSR', exact: true }),
|
|
176
|
+
).toBeVisible();
|
|
175
177
|
await expect(page.getByText('Dashboard')).toHaveCount(0);
|
|
176
178
|
await expect(page.locator('body')).toBeVisible();
|
|
177
179
|
await expect(page).not.toHaveTitle(/error/i);
|
|
@@ -516,6 +518,251 @@ test(
|
|
|
516
518
|
},
|
|
517
519
|
);
|
|
518
520
|
|
|
521
|
+
test(
|
|
522
|
+
'ATP-BASIC-TABLE-02: Training Student selection mode preserves page state and bulk deletes selected rows',
|
|
523
|
+
{ tag: ['@admin', '@flow'] },
|
|
524
|
+
async ({ page }, testInfo) => {
|
|
525
|
+
testInfo.setTimeout(120_000);
|
|
526
|
+
page.setDefaultTimeout(10_000);
|
|
527
|
+
await page.setViewportSize({ width: 1440, height: 900 });
|
|
528
|
+
const pageErrors = collectPageErrors(page);
|
|
529
|
+
await loginAsAdmin(page);
|
|
530
|
+
|
|
531
|
+
const fixturePrefix = `Selection E2E ${Date.now()}`;
|
|
532
|
+
const fixtureNames = Array.from(
|
|
533
|
+
{ length: 21 },
|
|
534
|
+
(_, index) => `${fixturePrefix} ${String(index + 1).padStart(2, '0')}`,
|
|
535
|
+
);
|
|
536
|
+
const visibleFixtureNames = fixtureNames.toReversed();
|
|
537
|
+
const fixtureRecords: Array<{ id: string | number; name: string }> = [];
|
|
538
|
+
const accessToken = (await page.context().cookies()).find(
|
|
539
|
+
cookie => cookie.name === 'token',
|
|
540
|
+
)?.value;
|
|
541
|
+
expect(accessToken).toBeTruthy();
|
|
542
|
+
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
543
|
+
|
|
544
|
+
const allPageCheckbox = page.getByRole('checkbox', {
|
|
545
|
+
name: 'Select all rows on this page',
|
|
546
|
+
exact: true,
|
|
547
|
+
});
|
|
548
|
+
const selectionStatus = page.getByRole('status').filter({ hasText: /^Selected \d+ items$/ });
|
|
549
|
+
const select = page.getByRole('button', { name: 'Select', exact: true });
|
|
550
|
+
const done = page.getByRole('button', { name: 'Done', exact: true });
|
|
551
|
+
const create = page.getByRole('button', { name: 'Create', exact: true });
|
|
552
|
+
const deleteBulk = page.getByRole('button', { name: 'Bulk Delete', exact: true });
|
|
553
|
+
const rowFor = (name: string) => page.locator('tbody tr').filter({ hasText: name });
|
|
554
|
+
const rowCheckboxFor = (name: string) =>
|
|
555
|
+
rowFor(name).getByRole('checkbox', { name: /^Select row / });
|
|
556
|
+
const selectPage = async () => {
|
|
557
|
+
const response = waitForStudentSelect(page);
|
|
558
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
559
|
+
return await response;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
for (const name of fixtureNames) {
|
|
564
|
+
const createResponse = await page.request.post('/api/training/student', {
|
|
565
|
+
data: { name, mobile: '13812345678', level: 1 },
|
|
566
|
+
headers,
|
|
567
|
+
});
|
|
568
|
+
expect(createResponse.ok()).toBeTruthy();
|
|
569
|
+
const id = (await createResponse.json()).data;
|
|
570
|
+
expect(['string', 'number']).toContain(typeof id);
|
|
571
|
+
fixtureRecords.unshift({ id, name });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const initialSelect = waitForStudentSelect(page);
|
|
575
|
+
await page.getByRole('link', { name: 'Student', exact: true }).click();
|
|
576
|
+
await expect(page).toHaveURL(studentResourceUrl);
|
|
577
|
+
await initialSelect;
|
|
578
|
+
|
|
579
|
+
await page.getByLabel('Student Name').fill(fixturePrefix);
|
|
580
|
+
const filteredResponse = await selectPage();
|
|
581
|
+
const filteredUrl = new URL(filteredResponse.url());
|
|
582
|
+
expect(filteredUrl.searchParams.get('name')).toBe(fixturePrefix);
|
|
583
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveCount(1);
|
|
584
|
+
await expect(rowFor(visibleFixtureNames[20])).toHaveCount(0);
|
|
585
|
+
|
|
586
|
+
await expect(create).toBeVisible();
|
|
587
|
+
await expect(create).toBeEnabled();
|
|
588
|
+
await expect(select).toBeVisible();
|
|
589
|
+
await expect(allPageCheckbox).toHaveCount(0);
|
|
590
|
+
await expect(deleteBulk).toBeDisabled();
|
|
591
|
+
|
|
592
|
+
await select.click();
|
|
593
|
+
await expect(done).toBeVisible();
|
|
594
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
595
|
+
await expect(allPageCheckbox).toBeVisible();
|
|
596
|
+
await expect(rowCheckboxFor(visibleFixtureNames[0])).toBeVisible();
|
|
597
|
+
|
|
598
|
+
await rowCheckboxFor(visibleFixtureNames[0]).check();
|
|
599
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveAttribute('aria-selected', 'true');
|
|
600
|
+
await expect(selectionStatus).toHaveText('Selected 1 items');
|
|
601
|
+
await expect(allPageCheckbox).toHaveAttribute('aria-checked', 'mixed');
|
|
602
|
+
await expect(allPageCheckbox).toHaveJSProperty('indeterminate', true);
|
|
603
|
+
|
|
604
|
+
await done.click();
|
|
605
|
+
await expect(select).toBeVisible();
|
|
606
|
+
await expect(allPageCheckbox).toHaveCount(0);
|
|
607
|
+
await expect(create).toBeEnabled();
|
|
608
|
+
|
|
609
|
+
await select.click();
|
|
610
|
+
await rowCheckboxFor(visibleFixtureNames[0]).check();
|
|
611
|
+
await allPageCheckbox.check();
|
|
612
|
+
await expect(selectionStatus).toHaveText('Selected 20 items');
|
|
613
|
+
await expect(allPageCheckbox).toBeChecked();
|
|
614
|
+
await expect(allPageCheckbox).toHaveJSProperty('indeterminate', false);
|
|
615
|
+
|
|
616
|
+
const pageTwoResponse = waitForStudentSelect(page);
|
|
617
|
+
await page.getByRole('button', { name: '»', exact: true }).click();
|
|
618
|
+
const pageTwoUrl = new URL((await pageTwoResponse).url());
|
|
619
|
+
expect(pageTwoUrl.searchParams.get('pageNo')).toBe('2');
|
|
620
|
+
await expect(selectionStatus).toHaveText('Selected 20 items');
|
|
621
|
+
await expect(rowFor(visibleFixtureNames[20])).toHaveCount(1);
|
|
622
|
+
await expect(rowCheckboxFor(visibleFixtureNames[20])).not.toBeChecked();
|
|
623
|
+
|
|
624
|
+
await rowCheckboxFor(visibleFixtureNames[20]).check();
|
|
625
|
+
await expect(rowFor(visibleFixtureNames[20])).toHaveAttribute('aria-selected', 'true');
|
|
626
|
+
await expect(selectionStatus).toHaveText('Selected 21 items');
|
|
627
|
+
|
|
628
|
+
await page.getByRole('button', { name: '«', exact: true }).click();
|
|
629
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveCount(1);
|
|
630
|
+
await expect(allPageCheckbox).toBeChecked();
|
|
631
|
+
await allPageCheckbox.uncheck();
|
|
632
|
+
await expect(selectionStatus).toHaveText('Selected 1 items');
|
|
633
|
+
|
|
634
|
+
await rowCheckboxFor(visibleFixtureNames[9]).check();
|
|
635
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
636
|
+
await page.getByLabel('Student Name').fill(`${fixturePrefix} 1`);
|
|
637
|
+
await selectPage();
|
|
638
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
639
|
+
await expect(rowFor(visibleFixtureNames[9])).toHaveAttribute('aria-selected', 'false');
|
|
640
|
+
|
|
641
|
+
await rowCheckboxFor(visibleFixtureNames[9]).check();
|
|
642
|
+
await expect(selectionStatus).toHaveText('Selected 1 items');
|
|
643
|
+
await page.getByRole('button', { name: 'Reset', exact: true }).click();
|
|
644
|
+
await expect(page.getByLabel('Student Name')).toHaveValue('');
|
|
645
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
646
|
+
await page.getByLabel('Student Name').fill(fixturePrefix);
|
|
647
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
648
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveCount(1);
|
|
649
|
+
|
|
650
|
+
await rowCheckboxFor(visibleFixtureNames[0]).check();
|
|
651
|
+
await expect(selectionStatus).toHaveText('Selected 1 items');
|
|
652
|
+
const nameSort = page.getByRole('button', { name: 'Sort by name', exact: true });
|
|
653
|
+
const sortResponse = waitForStudentSelect(page);
|
|
654
|
+
await nameSort.click();
|
|
655
|
+
await sortResponse;
|
|
656
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
657
|
+
|
|
658
|
+
await nameSort.click();
|
|
659
|
+
const selectedRows = page.locator('tbody tr').filter({ hasText: fixturePrefix });
|
|
660
|
+
await expect(selectedRows).toHaveCount(20);
|
|
661
|
+
const selectedNames = await selectedRows.evaluateAll(rows =>
|
|
662
|
+
rows.slice(0, 2).map(row => row.querySelector('td:nth-child(3) a')?.textContent?.trim()),
|
|
663
|
+
);
|
|
664
|
+
const selectedIds = await selectedRows.evaluateAll(rows =>
|
|
665
|
+
rows
|
|
666
|
+
.slice(0, 2)
|
|
667
|
+
.map(row => Number(row.querySelector('td:nth-child(2)')?.textContent?.trim())),
|
|
668
|
+
);
|
|
669
|
+
expect(selectedNames.every((name): name is string => Boolean(name))).toBeTruthy();
|
|
670
|
+
expect(selectedIds.every(Number.isInteger)).toBeTruthy();
|
|
671
|
+
await rowCheckboxFor(selectedNames[0]).check();
|
|
672
|
+
await rowCheckboxFor(selectedNames[1]).check();
|
|
673
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
674
|
+
await expect(deleteBulk).toBeEnabled();
|
|
675
|
+
|
|
676
|
+
let cancelledRequestCount = 0;
|
|
677
|
+
const cancelledRequestListener = (request: Request) => {
|
|
678
|
+
if (
|
|
679
|
+
request.method() === 'POST' &&
|
|
680
|
+
new URL(request.url()).pathname === '/api/training/student/bulk/delete'
|
|
681
|
+
) {
|
|
682
|
+
cancelledRequestCount++;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
page.on('request', cancelledRequestListener);
|
|
686
|
+
try {
|
|
687
|
+
await deleteBulk.click();
|
|
688
|
+
await expect(page.getByRole('dialog')).toContainText('2 selected items');
|
|
689
|
+
await page.getByRole('button', { name: 'No', exact: true }).click();
|
|
690
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
691
|
+
expect(cancelledRequestCount).toBe(0);
|
|
692
|
+
} finally {
|
|
693
|
+
page.off('request', cancelledRequestListener);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let failureRequestCount = 0;
|
|
697
|
+
const bulkDeleteRoute = (url: URL) => url.pathname === '/api/training/student/bulk/delete';
|
|
698
|
+
const bulkDeleteFailureHandler = async (route: Route) => {
|
|
699
|
+
failureRequestCount++;
|
|
700
|
+
await route.fulfill({
|
|
701
|
+
status: 500,
|
|
702
|
+
contentType: 'application/json',
|
|
703
|
+
body: JSON.stringify({ code: 500, message: 'Bulk deletion temporarily unavailable' }),
|
|
704
|
+
});
|
|
705
|
+
};
|
|
706
|
+
await page.route(bulkDeleteRoute, bulkDeleteFailureHandler);
|
|
707
|
+
try {
|
|
708
|
+
await deleteBulk.click();
|
|
709
|
+
await page.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
710
|
+
await expect.poll(() => failureRequestCount).toBe(1);
|
|
711
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
712
|
+
await expect(rowFor(selectedNames[0])).toHaveAttribute('aria-selected', 'true');
|
|
713
|
+
await expect(rowFor(selectedNames[1])).toHaveAttribute('aria-selected', 'true');
|
|
714
|
+
} finally {
|
|
715
|
+
await page.unroute(bulkDeleteRoute, bulkDeleteFailureHandler);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const bulkDeleteRequest = page.waitForRequest(request => {
|
|
719
|
+
const url = new URL(request.url());
|
|
720
|
+
return request.method() === 'POST' && url.pathname === '/api/training/student/bulk/delete';
|
|
721
|
+
});
|
|
722
|
+
const bulkDeleteResponse = page.waitForResponse(response => {
|
|
723
|
+
const url = new URL(response.url());
|
|
724
|
+
return (
|
|
725
|
+
response.request().method() === 'POST' &&
|
|
726
|
+
response.ok() &&
|
|
727
|
+
url.pathname === '/api/training/student/bulk/delete'
|
|
728
|
+
);
|
|
729
|
+
});
|
|
730
|
+
const refetchResponse = waitForStudentSelect(page);
|
|
731
|
+
await deleteBulk.click();
|
|
732
|
+
await page.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
733
|
+
const [request, response, refetch] = await Promise.all([
|
|
734
|
+
bulkDeleteRequest,
|
|
735
|
+
bulkDeleteResponse,
|
|
736
|
+
refetchResponse,
|
|
737
|
+
]);
|
|
738
|
+
expect(response.ok()).toBeTruthy();
|
|
739
|
+
expect(request.postDataJSON()).toEqual({ ids: selectedIds });
|
|
740
|
+
expect(refetch.ok()).toBeTruthy();
|
|
741
|
+
|
|
742
|
+
await expect(rowFor(selectedNames[0])).toHaveCount(0);
|
|
743
|
+
await expect(rowFor(selectedNames[1])).toHaveCount(0);
|
|
744
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
745
|
+
await expect(page.locator('tbody tr[aria-selected="true"]')).toHaveCount(0);
|
|
746
|
+
await expect(deleteBulk).toBeDisabled();
|
|
747
|
+
expect(pageErrors).toEqual([]);
|
|
748
|
+
} finally {
|
|
749
|
+
for (const { id, name } of fixtureRecords) {
|
|
750
|
+
const deleteResponse = await page.request.delete(
|
|
751
|
+
`/api/training/student/deleteForce/${id}`,
|
|
752
|
+
{
|
|
753
|
+
headers,
|
|
754
|
+
timeout: 10_000,
|
|
755
|
+
},
|
|
756
|
+
);
|
|
757
|
+
expect(
|
|
758
|
+
deleteResponse.ok() || deleteResponse.status() === 404,
|
|
759
|
+
`Failed to clean up Training Student ${name} (${id}): ${deleteResponse.status()}`,
|
|
760
|
+
).toBeTruthy();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
);
|
|
765
|
+
|
|
519
766
|
test(
|
|
520
767
|
'ATP-BASIC-FORM-01: Training Student content remains in the Basic Information tab',
|
|
521
768
|
{ tag: ['@admin', '@flow'] },
|
|
@@ -10,7 +10,9 @@ import { Arg, Controller, Web } from 'vona-module-a-web';
|
|
|
10
10
|
|
|
11
11
|
import type { Model<%=argv.resourceNameCapitalize%> } from '../model/<%=argv.resourceName%>.ts';
|
|
12
12
|
|
|
13
|
+
import { $locale } from '../.metadata/locales.ts';
|
|
13
14
|
import { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
|
|
15
|
+
import { Dto<%=argv.resourceNameCapitalize%>DeleteBulk } from '../dto/<%=argv.resourceName%>DeleteBulk.tsx';
|
|
14
16
|
import { Dto<%=argv.resourceNameCapitalize%>SelectReq } from '../dto/<%=argv.resourceName%>SelectReq.tsx';
|
|
15
17
|
import { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
|
|
16
18
|
import { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.resourceName%>Update.tsx';
|
|
@@ -18,38 +20,47 @@ import { Dto<%=argv.resourceNameCapitalize%>View } from '../dto/<%=argv.resource
|
|
|
18
20
|
|
|
19
21
|
export interface IControllerOptions<%=argv.resourceNameCapitalize%> extends IDecoratorControllerOptions {}
|
|
20
22
|
|
|
21
|
-
@Controller<IControllerOptions<%=argv.resourceNameCapitalize%>>('<%=argv.resourceName%>'
|
|
23
|
+
@Controller<IControllerOptions<%=argv.resourceNameCapitalize%>>('<%=argv.resourceName%>', {
|
|
24
|
+
summary: $locale('<%=argv.resourceNameCapitalize%>Controller'),
|
|
25
|
+
})
|
|
22
26
|
@Resource()
|
|
23
27
|
export class Controller<%=argv.resourceNameCapitalize%> extends BeanBase {
|
|
24
|
-
@Web.post()
|
|
28
|
+
@Web.post('', { summary: $locale('<%=argv.resourceNameCapitalize%>Create') })
|
|
25
29
|
@Api.body(v.tableIdentity())
|
|
26
30
|
@Passport.systemAdmin()
|
|
27
31
|
async create(@Arg.body() <%=argv.resourceName%>: Dto<%=argv.resourceNameCapitalize%>Create): Promise<TableIdentity> {
|
|
28
32
|
return (await this.scope.service.<%=argv.resourceName%>.create(<%=argv.resourceName%>)).id;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
@Web.get()
|
|
35
|
+
@Web.get('', { summary: $locale('<%=argv.resourceNameCapitalize%>Select') })
|
|
32
36
|
@Api.body(Dto<%=argv.resourceNameCapitalize%>SelectRes)
|
|
33
37
|
@Passport.systemAdmin()
|
|
34
38
|
async select(@Arg.filter(Dto<%=argv.resourceNameCapitalize%>SelectReq) params: IQueryParams<Model<%=argv.resourceNameCapitalize%>>): Promise<Dto<%=argv.resourceNameCapitalize%>SelectRes> {
|
|
35
39
|
return await this.scope.service.<%=argv.resourceName%>.select(params);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
@Web.get(':id')
|
|
42
|
+
@Web.get(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>View') })
|
|
39
43
|
@Api.body(v.optional(), v.object(Dto<%=argv.resourceNameCapitalize%>View))
|
|
40
44
|
@Passport.systemAdmin()
|
|
41
45
|
async view(@Arg.param('id', v.tableIdentity()) id: TableIdentity): Promise<Dto<%=argv.resourceNameCapitalize%>View | undefined> {
|
|
42
46
|
return await this.scope.service.<%=argv.resourceName%>.view(id);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
@Web.patch(':id')
|
|
49
|
+
@Web.patch(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>Update') })
|
|
46
50
|
@Api.body(z.null())
|
|
47
51
|
@Passport.systemAdmin()
|
|
48
52
|
async update(@Arg.param('id', v.tableIdentity()) id: TableIdentity, @Arg.body() <%=argv.resourceName%>: Dto<%=argv.resourceNameCapitalize%>Update): Promise<void> {
|
|
49
53
|
await this.scope.service.<%=argv.resourceName%>.update(id, <%=argv.resourceName%>);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
@Web.
|
|
56
|
+
@Web.post('bulk/delete', { summary: $locale('BulkDelete') })
|
|
57
|
+
@Api.body(z.null())
|
|
58
|
+
@Passport.systemAdmin()
|
|
59
|
+
async deleteBulk(@Arg.body() command: Dto<%=argv.resourceNameCapitalize%>DeleteBulk): Promise<void> {
|
|
60
|
+
await this.scope.service.<%=argv.resourceName%>.deleteBulk(command.ids);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Web.delete(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>Delete') })
|
|
53
64
|
@Api.body(z.null())
|
|
54
65
|
@Passport.systemAdmin()
|
|
55
66
|
async delete(@Arg.param('id', v.tableIdentity()) id: TableIdentity): Promise<void> {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
3
|
+
|
|
4
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
5
|
+
import { Dto } from 'vona-module-a-web';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
export interface IDtoOptions<%=argv.resourceNameCapitalize%>DeleteBulk extends IDecoratorDtoOptions {}
|
|
9
|
+
|
|
10
|
+
@Dto<IDtoOptions<%=argv.resourceNameCapitalize%>DeleteBulk>()
|
|
11
|
+
export class Dto<%=argv.resourceNameCapitalize%>DeleteBulk {
|
|
12
|
+
@Api.field(v.required(), z.array(v.tableIdentity()()).min(1).max(100))
|
|
13
|
+
ids: TableIdentity[];
|
|
14
|
+
}
|
|
@@ -38,7 +38,13 @@ export interface IDtoOptions<%=argv.resourceNameCapitalize%>SelectResItem extend
|
|
|
38
38
|
],
|
|
39
39
|
}),
|
|
40
40
|
ZovaRender.block('basic-page:blockToolbarBulk', {
|
|
41
|
-
actions: [
|
|
41
|
+
actions: [
|
|
42
|
+
ZovaRender.tableActionBulk('basic-table:actionCreate'),
|
|
43
|
+
ZovaRender.tableActionBulk('basic-table:actionDeleteBulk', {
|
|
44
|
+
requiresSelection: true,
|
|
45
|
+
selectedMaxIds: 100,
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
42
48
|
}),
|
|
43
49
|
ZovaRender.block('basic-page:blockTable'),
|
|
44
50
|
ZovaRender.block('basic-page:blockPager'),
|
|
@@ -3,6 +3,7 @@ import type { IQueryParams } from 'vona-module-a-orm';
|
|
|
3
3
|
|
|
4
4
|
import { BeanBase } from 'vona';
|
|
5
5
|
import { Service } from 'vona-module-a-bean';
|
|
6
|
+
import { Core } from 'vona-module-a-core';
|
|
6
7
|
|
|
7
8
|
import type { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
|
|
8
9
|
import type { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
|
|
@@ -32,4 +33,9 @@ export class Service<%=argv.resourceNameCapitalize%> extends BeanBase {
|
|
|
32
33
|
async delete(id: TableIdentity) {
|
|
33
34
|
return await this.scope.model.<%=argv.resourceName%>.deleteById(id);
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
@Core.transaction()
|
|
38
|
+
async deleteBulk(ids: TableIdentity[]): Promise<void> {
|
|
39
|
+
await this.scope.model.<%=argv.resourceName%>.deleteBulk(ids);
|
|
40
|
+
}
|
|
35
41
|
}
|
|
@@ -9,6 +9,7 @@ const __resources = {
|
|
|
9
9
|
Name: 'Name',
|
|
10
10
|
Description: 'Description',
|
|
11
11
|
Operations: 'Operations',
|
|
12
|
+
BulkDelete: 'Bulk Delete',
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
declare module '@cabloy/cli' {
|
|
@@ -30,6 +31,16 @@ export default metadataCustomSnippet({
|
|
|
30
31
|
return fs.readFileSync(targetFile).toString('utf8');
|
|
31
32
|
},
|
|
32
33
|
async transform({ ast, argv }) {
|
|
33
|
-
|
|
34
|
+
const resource = argv.resourceNameCapitalize;
|
|
35
|
+
const resources = {
|
|
36
|
+
...__resources,
|
|
37
|
+
[`${resource}Controller`]: `${resource} Management`,
|
|
38
|
+
[`${resource}Create`]: `Create ${resource}`,
|
|
39
|
+
[`${resource}Select`]: `Query ${resource} List`,
|
|
40
|
+
[`${resource}View`]: `View ${resource}`,
|
|
41
|
+
[`${resource}Update`]: `Update ${resource}`,
|
|
42
|
+
[`${resource}Delete`]: `Delete ${resource}`,
|
|
43
|
+
};
|
|
44
|
+
return locale_transform({ ast, argv, resources });
|
|
34
45
|
},
|
|
35
46
|
});
|
|
@@ -7,12 +7,23 @@ const __resources = {
|
|
|
7
7
|
Name: '名称',
|
|
8
8
|
Description: '描述',
|
|
9
9
|
Operations: '操作',
|
|
10
|
+
BulkDelete: '批量删除',
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export default metadataCustomSnippet({
|
|
13
14
|
file: 'src/config/locale/zh-cn.ts',
|
|
14
15
|
language: 'plain',
|
|
15
16
|
async transform({ ast, argv }) {
|
|
16
|
-
|
|
17
|
+
const resource = argv.resourceNameCapitalize;
|
|
18
|
+
const resources = {
|
|
19
|
+
...__resources,
|
|
20
|
+
[`${resource}Controller`]: `${resource}管理`,
|
|
21
|
+
[`${resource}Create`]: `创建${resource}`,
|
|
22
|
+
[`${resource}Select`]: `查询${resource}列表`,
|
|
23
|
+
[`${resource}View`]: `查看${resource}`,
|
|
24
|
+
[`${resource}Update`]: `更新${resource}`,
|
|
25
|
+
[`${resource}Delete`]: `删除${resource}`,
|
|
26
|
+
};
|
|
27
|
+
return locale_transform({ ast, argv, resources });
|
|
17
28
|
},
|
|
18
29
|
});
|
|
@@ -12,6 +12,7 @@ import type { Model<%=argv.resourceNameCapitalize%> } from '../model/<%=argv.res
|
|
|
12
12
|
|
|
13
13
|
import { $locale } from '../.metadata/locales.ts';
|
|
14
14
|
import { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
|
|
15
|
+
import { Dto<%=argv.resourceNameCapitalize%>DeleteBulk } from '../dto/<%=argv.resourceName%>DeleteBulk.tsx';
|
|
15
16
|
import { Dto<%=argv.resourceNameCapitalize%>SelectReq } from '../dto/<%=argv.resourceName%>SelectReq.tsx';
|
|
16
17
|
import { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
|
|
17
18
|
import { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.resourceName%>Update.tsx';
|
|
@@ -52,6 +53,13 @@ export class Controller<%=argv.resourceNameCapitalize%> extends BeanBase {
|
|
|
52
53
|
await this.scope.service.<%=argv.resourceName%>.update(id, <%=argv.resourceName%>);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
@Web.post('bulk/delete', { summary: $locale('BulkDelete') })
|
|
57
|
+
@Api.body(z.null())
|
|
58
|
+
@Passport.rbac()
|
|
59
|
+
async deleteBulk(@Arg.body() command: Dto<%=argv.resourceNameCapitalize%>DeleteBulk): Promise<void> {
|
|
60
|
+
await this.scope.service.<%=argv.resourceName%>.deleteBulk(command.ids);
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
@Web.delete(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>Delete') })
|
|
56
64
|
@Api.body(z.null())
|
|
57
65
|
@Passport.rbac()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
3
|
+
|
|
4
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
5
|
+
import { Dto } from 'vona-module-a-web';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
export interface IDtoOptions<%=argv.resourceNameCapitalize%>DeleteBulk extends IDecoratorDtoOptions {}
|
|
9
|
+
|
|
10
|
+
@Dto<IDtoOptions<%=argv.resourceNameCapitalize%>DeleteBulk>()
|
|
11
|
+
export class Dto<%=argv.resourceNameCapitalize%>DeleteBulk {
|
|
12
|
+
@Api.field(v.required(), z.array(v.tableIdentity()()).min(1).max(100))
|
|
13
|
+
ids: TableIdentity[];
|
|
14
|
+
}
|
|
@@ -38,7 +38,13 @@ export interface IDtoOptions<%=argv.resourceNameCapitalize%>SelectResItem extend
|
|
|
38
38
|
],
|
|
39
39
|
}),
|
|
40
40
|
ZovaRender.block('start-page:blockToolbarBulk', {
|
|
41
|
-
actions: [
|
|
41
|
+
actions: [
|
|
42
|
+
ZovaRender.tableActionBulk('start-table:actionCreate'),
|
|
43
|
+
ZovaRender.tableActionBulk('start-table:actionDeleteBulk', {
|
|
44
|
+
requiresSelection: true,
|
|
45
|
+
selectedMaxIds: 100,
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
42
48
|
}),
|
|
43
49
|
ZovaRender.block('start-page:blockTable'),
|
|
44
50
|
],
|
|
@@ -3,6 +3,7 @@ import type { IQueryParams } from 'vona-module-a-orm';
|
|
|
3
3
|
|
|
4
4
|
import { BeanBase } from 'vona';
|
|
5
5
|
import { Service } from 'vona-module-a-bean';
|
|
6
|
+
import { Core } from 'vona-module-a-core';
|
|
6
7
|
|
|
7
8
|
import type { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
|
|
8
9
|
import type { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
|
|
@@ -32,4 +33,9 @@ export class Service<%=argv.resourceNameCapitalize%> extends BeanBase {
|
|
|
32
33
|
async delete(id: TableIdentity) {
|
|
33
34
|
return await this.scope.model.<%=argv.resourceName%>.deleteById(id);
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
@Core.transaction()
|
|
38
|
+
async deleteBulk(ids: TableIdentity[]): Promise<void> {
|
|
39
|
+
await this.scope.model.<%=argv.resourceName%>.deleteBulk(ids);
|
|
40
|
+
}
|
|
35
41
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.93",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
|
|
6
6
|
"keywords": [
|
|
@@ -217,6 +217,7 @@ export * from '../dto/detailRecordMutate.tsx';
|
|
|
217
217
|
export * from '../dto/detailRecordResItem.tsx';
|
|
218
218
|
export * from '../dto/detailRecordView.tsx';
|
|
219
219
|
export * from '../dto/studentCreate.tsx';
|
|
220
|
+
export * from '../dto/studentDeleteBulk.tsx';
|
|
220
221
|
export * from '../dto/studentSelectReq.tsx';
|
|
221
222
|
export * from '../dto/studentSelectRes.tsx';
|
|
222
223
|
export * from '../dto/studentSelectResItem.tsx';
|
|
@@ -228,6 +229,7 @@ import type { IDtoOptionsDetailRecordMutate } from '../dto/detailRecordMutate.ts
|
|
|
228
229
|
import type { IDtoOptionsDetailRecordResItem } from '../dto/detailRecordResItem.tsx';
|
|
229
230
|
import type { IDtoOptionsDetailRecordView } from '../dto/detailRecordView.tsx';
|
|
230
231
|
import type { IDtoOptionsStudentCreate } from '../dto/studentCreate.tsx';
|
|
232
|
+
import type { IDtoOptionsStudentDeleteBulk } from '../dto/studentDeleteBulk.tsx';
|
|
231
233
|
import type { IDtoOptionsStudentSelectReq } from '../dto/studentSelectReq.tsx';
|
|
232
234
|
import type { IDtoOptionsStudentSelectRes } from '../dto/studentSelectRes.tsx';
|
|
233
235
|
import type { IDtoOptionsStudentSelectResItem } from '../dto/studentSelectResItem.tsx';
|
|
@@ -243,6 +245,7 @@ declare module 'vona-module-a-web' {
|
|
|
243
245
|
'training-student:detailRecordResItem': IDtoOptionsDetailRecordResItem;
|
|
244
246
|
'training-student:detailRecordView': IDtoOptionsDetailRecordView;
|
|
245
247
|
'training-student:studentCreate': IDtoOptionsStudentCreate;
|
|
248
|
+
'training-student:studentDeleteBulk': IDtoOptionsStudentDeleteBulk;
|
|
246
249
|
'training-student:studentSelectReq': IDtoOptionsStudentSelectReq;
|
|
247
250
|
'training-student:studentSelectRes': IDtoOptionsStudentSelectRes;
|
|
248
251
|
'training-student:studentSelectResItem': IDtoOptionsStudentSelectResItem;
|
|
@@ -263,6 +266,7 @@ import type { DtoDetailRecordMutate } from '../dto/detailRecordMutate.tsx';
|
|
|
263
266
|
import type { DtoDetailRecordResItem } from '../dto/detailRecordResItem.tsx';
|
|
264
267
|
import type { DtoDetailRecordView } from '../dto/detailRecordView.tsx';
|
|
265
268
|
import type { DtoStudentCreate } from '../dto/studentCreate.tsx';
|
|
269
|
+
import type { DtoStudentDeleteBulk } from '../dto/studentDeleteBulk.tsx';
|
|
266
270
|
import type { DtoStudentSelectReq } from '../dto/studentSelectReq.tsx';
|
|
267
271
|
import type { DtoStudentSelectRes } from '../dto/studentSelectRes.tsx';
|
|
268
272
|
import type { DtoStudentSelectResItem } from '../dto/studentSelectResItem.tsx';
|
|
@@ -291,6 +295,10 @@ declare module 'vona-module-training-student' {
|
|
|
291
295
|
fields?: TypeEntityOptionsFields<DtoStudentCreate, IDtoOptionsStudentCreate[TypeSymbolKeyFieldsMore]>;
|
|
292
296
|
}
|
|
293
297
|
|
|
298
|
+
export interface IDtoOptionsStudentDeleteBulk {
|
|
299
|
+
fields?: TypeEntityOptionsFields<DtoStudentDeleteBulk, IDtoOptionsStudentDeleteBulk[TypeSymbolKeyFieldsMore]>;
|
|
300
|
+
}
|
|
301
|
+
|
|
294
302
|
export interface IDtoOptionsStudentSelectReq {
|
|
295
303
|
fields?: TypeEntityOptionsFields<DtoStudentSelectReq, IDtoOptionsStudentSelectReq[TypeSymbolKeyFieldsMore]>;
|
|
296
304
|
}
|
|
@@ -354,6 +362,7 @@ declare module 'vona-module-training-student' {
|
|
|
354
362
|
declare module 'vona-module-a-web' {
|
|
355
363
|
export interface IApiPathPostRecord{
|
|
356
364
|
'/training/student': undefined;
|
|
365
|
+
'/training/student/bulk/delete': undefined;
|
|
357
366
|
}
|
|
358
367
|
export interface IApiPathGetRecord{
|
|
359
368
|
'/training/student': undefined;
|
|
@@ -22,6 +22,7 @@ export default {
|
|
|
22
22
|
LevelTitle: 'Level Title',
|
|
23
23
|
DescriptionLength: 'Description Length',
|
|
24
24
|
ForceDelete: 'Force Delete',
|
|
25
|
+
BulkDelete: 'Bulk Delete',
|
|
25
26
|
TrainingRecords: 'Student Training Records',
|
|
26
27
|
AddTrainingRecord: 'Add Student Training Record',
|
|
27
28
|
EditTrainingRecord: 'Edit Student Training Record',
|
|
@@ -11,7 +11,9 @@ import { z } from 'zod';
|
|
|
11
11
|
|
|
12
12
|
import type { ModelStudent } from '../model/student.ts';
|
|
13
13
|
|
|
14
|
+
import { $locale } from '../.metadata/locales.ts';
|
|
14
15
|
import { DtoStudentCreate } from '../dto/studentCreate.tsx';
|
|
16
|
+
import { DtoStudentDeleteBulk } from '../dto/studentDeleteBulk.tsx';
|
|
15
17
|
import { DtoStudentSelectReq } from '../dto/studentSelectReq.tsx';
|
|
16
18
|
import { DtoStudentSelectRes } from '../dto/studentSelectRes.tsx';
|
|
17
19
|
import { DtoStudentSummary } from '../dto/studentSummary.tsx';
|
|
@@ -70,6 +72,13 @@ export class ControllerStudent extends BeanBase {
|
|
|
70
72
|
return await this.scope.service.student.summary(id);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
@Web.post('bulk/delete', { summary: $locale('BulkDelete') })
|
|
76
|
+
@Api.body(z.null())
|
|
77
|
+
@Passport.systemAdmin()
|
|
78
|
+
async deleteBulk(@Arg.body() command: DtoStudentDeleteBulk): Promise<void> {
|
|
79
|
+
await this.scope.service.student.deleteBulk(command.ids);
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
@Web.delete(':id')
|
|
74
83
|
@Api.body(z.null())
|
|
75
84
|
@Passport.systemAdmin()
|