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.
Files changed (96) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -0
  4. package/package.json +1 -1
  5. package/repo-docs/backend/shared-rbac-architecture.md +4 -0
  6. package/repo-docs/blogs/ai-spec-driven-development/cover-en-v1.png +0 -0
  7. package/repo-docs/blogs/ai-spec-driven-development/implementation-burndown.png +0 -0
  8. package/repo-docs/blogs/ai-spec-driven-development/implementation-gantt.png +0 -0
  9. package/repo-docs/blogs/ai-spec-driven-development/index.md +217 -0
  10. package/repo-docs/blogs/index.md +15 -0
  11. package/repo-docs/editions/overview.md +2 -0
  12. package/repo-docs/frontend/environment-config-guide.md +43 -10
  13. package/repo-docs/frontend/resource-list-page-deep-dive.md +14 -1
  14. package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +20 -0
  15. package/repo-docs/frontend/table-guide.md +42 -0
  16. package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -1
  17. package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
  18. package/repo-e2e/specs/cabloy-basic.spec.ts +249 -2
  19. package/vona/packages-cli/cli/package.json +1 -1
  20. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +17 -6
  21. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
  22. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
  23. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
  24. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +12 -1
  25. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +12 -1
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +8 -0
  27. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
  28. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +1 -0
  31. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +1 -0
  32. package/vona/packages-cli/cli-set-api/package.json +1 -1
  33. package/vona/packages-vona/vona/package.json +1 -1
  34. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +9 -0
  35. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  36. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  37. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +9 -0
  38. package/vona/src/suite/a-training/modules/training-student/src/dto/studentDeleteBulk.tsx +14 -0
  39. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +7 -1
  40. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +6 -0
  41. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +114 -2
  42. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  43. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/index.ts +24 -3
  44. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/locales.ts +18 -0
  45. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacResourceBulk.ts +50 -0
  46. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/index.ts +1 -0
  47. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/errors.ts +18 -0
  48. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/en-us.ts +6 -0
  49. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/zh-cn.ts +6 -0
  50. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/index.ts +1 -0
  51. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/resourceBulk.test.ts +128 -0
  52. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  53. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  54. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  55. package/zova/packages-cli/cli/package.json +2 -2
  56. package/zova/packages-cli/cli-set-front/cli/templates/create/component/boilerplateTableActionBulk/controller.tsx_ +2 -2
  57. package/zova/packages-cli/cli-set-front/package.json +1 -1
  58. package/zova/packages-zova/zova/package.json +2 -2
  59. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -12
  60. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +66 -7
  61. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +18 -0
  62. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +5 -0
  63. package/zova/src/suite/cabloy-basic/modules/basic-commands/src/.metadata/index.ts +16 -0
  64. package/zova/src/suite/cabloy-basic/modules/basic-commands/src/bean/command.deleteBulk.tsx +35 -0
  65. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +143 -7
  66. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -2
  67. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +76 -4
  68. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +7 -0
  69. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +6 -0
  70. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +84 -0
  71. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +99 -0
  72. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts +31 -0
  73. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +13 -0
  74. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +5 -2
  75. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +57 -0
  76. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +35 -7
  77. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +4 -0
  78. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +4 -0
  79. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  80. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/selectedIds.ts +15 -0
  81. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +57 -0
  82. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/selectedIds.test.ts +20 -0
  83. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  84. package/zova/src/suite-vendor/a-zova/modules/a-command/package.json +1 -1
  85. package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/beanCommandSelectedBase.ts +33 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/index.ts +1 -0
  87. package/zova/src/suite-vendor/a-zova/modules/a-command/src/types/command.ts +5 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-command/test/lib/beanCommandSelectedBase.test.ts +47 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  90. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +3 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +19 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  93. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +86 -26
  94. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +30 -6
  95. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +2 -0
  96. 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(page.getByText('Web: en-us')).toBeVisible();
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'] },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.145",
3
+ "version": "1.1.146",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -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.delete(':id')
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: [ZovaRender.tableActionBulk('basic-table:actionCreate')],
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
- return locale_transform({ ast, argv, resources: __resources });
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
- return locale_transform({ ast, argv, resources: __resources });
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: [ZovaRender.tableActionBulk('start-table:actionCreate')],
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
  }
@@ -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' {
@@ -7,6 +7,7 @@ const __resources = {
7
7
  Name: '名称',
8
8
  Description: '描述',
9
9
  Operations: '操作',
10
+ BulkDelete: '批量删除',
10
11
  };
11
12
 
12
13
  export default metadataCustomSnippet({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.143",
3
+ "version": "1.1.144",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.92",
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',
@@ -22,6 +22,7 @@ export default {
22
22
  LevelTitle: '阶段名称',
23
23
  DescriptionLength: '描述长度',
24
24
  ForceDelete: '强制删除',
25
+ BulkDelete: '批量删除',
25
26
  TrainingRecords: '学生培训记录',
26
27
  AddTrainingRecord: '添加学生培训记录',
27
28
  EditTrainingRecord: '编辑学生培训记录',
@@ -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()