cabloy 5.1.163 → 5.1.165
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/.claude/scheduled_tasks.lock +1 -0
- package/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/repo-docs/backend/shared-rbac-architecture.md +26 -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-docs/fullstack/backend-metadata-to-frontend-table-actions.md +36 -1
- package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
- package/repo-e2e/specs/cabloy-basic.spec.ts +248 -1
- 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_ +21 -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/pnpm-lock.yaml +91 -11
- 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-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +1 -1
- package/zova/packages-zova/zova-core/src/types/interface/component.ts +26 -20
- package/zova/pnpm-lock.yaml +6 -6
- 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/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
|
@@ -25,6 +25,7 @@ interface IArticleGeometry {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const blogArticlePaths = [
|
|
28
|
+
'/blogs/ai-spec-driven-development/',
|
|
28
29
|
'/blogs/ai-react-nextjs-enterprise-architecture-cabloy/',
|
|
29
30
|
'/blogs/cabloy-fullstack-resource-addressing/',
|
|
30
31
|
'/blogs/nextjs-integrated-fullstack-cabloy-contract-loop/',
|
|
@@ -118,29 +119,46 @@ test(
|
|
|
118
119
|
await expect(page.locator('.VPDoc.has-aside')).toHaveCount(0);
|
|
119
120
|
|
|
120
121
|
const cards = page.locator('.cabloy-blog-card');
|
|
121
|
-
await expect(cards).toHaveCount(
|
|
122
|
-
for (let index = 0; index <
|
|
122
|
+
await expect(cards).toHaveCount(5);
|
|
123
|
+
for (let index = 0; index < 5; index++) {
|
|
123
124
|
await expect(cards.nth(index)).toBeVisible();
|
|
124
125
|
await expect(cards.nth(index).locator('.cabloy-blog-card__cover img')).toBeVisible();
|
|
125
126
|
}
|
|
127
|
+
await expect(
|
|
128
|
+
cards.nth(0).getByRole('link', {
|
|
129
|
+
name: 'CabloyJS AI Spec-Driven Development Automatically Generates Gantt and Burndown Charts',
|
|
130
|
+
exact: true,
|
|
131
|
+
}),
|
|
132
|
+
).toBeVisible();
|
|
133
|
+
await expect(cards.nth(0).locator('.cabloy-blog-card__date')).toHaveText('September 11, 2026');
|
|
134
|
+
await expect(cards.nth(0).locator('.cabloy-blog-card__cover')).toHaveAttribute(
|
|
135
|
+
'href',
|
|
136
|
+
'/blogs/ai-spec-driven-development/',
|
|
137
|
+
);
|
|
126
138
|
|
|
127
139
|
const geometry = await getBlogsGeometry(page);
|
|
128
140
|
expect(geometry.grid.display).toBe('grid');
|
|
129
|
-
expect(geometry.cards).toHaveLength(
|
|
141
|
+
expect(geometry.cards).toHaveLength(5);
|
|
130
142
|
|
|
131
|
-
const [firstCard,
|
|
143
|
+
const [firstCard, secondCard, thirdCard, fourthCard, fifthCard] = geometry.cards;
|
|
144
|
+
const firstRow = [firstCard, secondCard, thirdCard, fourthCard];
|
|
132
145
|
expect(firstCard.width).toBeGreaterThanOrEqual(279);
|
|
133
|
-
for (const card of
|
|
146
|
+
for (const card of firstRow) {
|
|
134
147
|
expect(Math.abs(card.top - firstCard.top)).toBeLessThanOrEqual(1);
|
|
135
148
|
expect(Math.abs(card.width - firstCard.width)).toBeLessThanOrEqual(1);
|
|
136
|
-
expect(card.left).toBeGreaterThan(firstCard.left);
|
|
137
149
|
expect(card.left).toBeGreaterThanOrEqual(geometry.grid.left - 1);
|
|
138
150
|
expect(card.right).toBeLessThanOrEqual(geometry.grid.right + 1);
|
|
139
151
|
expect(card.right).toBeLessThanOrEqual(1441);
|
|
140
152
|
}
|
|
141
|
-
expect(
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
153
|
+
expect(secondCard.left).toBeGreaterThan(firstCard.left);
|
|
154
|
+
expect(thirdCard.left).toBeGreaterThan(secondCard.left);
|
|
155
|
+
expect(fourthCard.left).toBeGreaterThan(thirdCard.left);
|
|
156
|
+
expect(fifthCard.top).toBeGreaterThan(firstCard.bottom);
|
|
157
|
+
expect(fifthCard.left).toBeCloseTo(firstCard.left, 0);
|
|
158
|
+
expect(fifthCard.width).toBeCloseTo(firstCard.width, 0);
|
|
159
|
+
expect(fifthCard.left).toBeGreaterThanOrEqual(geometry.grid.left - 1);
|
|
160
|
+
expect(fifthCard.right).toBeLessThanOrEqual(geometry.grid.right + 1);
|
|
161
|
+
expect(fifthCard.right).toBeLessThanOrEqual(1441);
|
|
144
162
|
|
|
145
163
|
await expect.poll(() => getDocumentHorizontalOverflow(page)).toBeLessThanOrEqual(1);
|
|
146
164
|
expect(pageErrors).toEqual([]);
|
|
@@ -163,6 +181,24 @@ test(
|
|
|
163
181
|
await expect(page.locator('.VPDoc.has-sidebar')).toHaveCount(0);
|
|
164
182
|
await expect(page.locator('.VPDoc.has-aside')).toHaveCount(1);
|
|
165
183
|
await expect(page.locator('.aside')).toBeVisible();
|
|
184
|
+
if (path === '/blogs/ai-spec-driven-development/') {
|
|
185
|
+
await expect(
|
|
186
|
+
page.getByRole('heading', {
|
|
187
|
+
name: 'CabloyJS AI Spec-Driven Development Automatically Generates Gantt and Burndown Charts',
|
|
188
|
+
level: 1,
|
|
189
|
+
}),
|
|
190
|
+
).toBeVisible();
|
|
191
|
+
await expect(
|
|
192
|
+
page.getByAltText(
|
|
193
|
+
'A-Commerce implementation roadmap: WBS phases, dependencies, and derived status',
|
|
194
|
+
),
|
|
195
|
+
).toBeVisible();
|
|
196
|
+
await expect(
|
|
197
|
+
page.getByAltText(
|
|
198
|
+
'A-Commerce WBS scope-count burndown: approved scope and remaining-item counts',
|
|
199
|
+
),
|
|
200
|
+
).toBeVisible();
|
|
201
|
+
}
|
|
166
202
|
|
|
167
203
|
const geometry = await getArticleGeometry(page);
|
|
168
204
|
expect(geometry.container.width).toBeCloseTo(1216, 0);
|
|
@@ -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
|
|
|
@@ -518,6 +518,253 @@ test(
|
|
|
518
518
|
},
|
|
519
519
|
);
|
|
520
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
|
|
549
|
+
.getByRole('status')
|
|
550
|
+
.filter({ hasText: /^Selected (?:\d+ items|one item)$/ });
|
|
551
|
+
const select = page.getByRole('button', { name: 'Select', exact: true });
|
|
552
|
+
const done = page.getByRole('button', { name: 'Done', exact: true });
|
|
553
|
+
const create = page.getByRole('button', { name: 'Create', exact: true });
|
|
554
|
+
const deleteBulk = page.getByRole('button', { name: 'Bulk Delete', exact: true });
|
|
555
|
+
const rowFor = (name: string) => page.locator('tbody tr').filter({ hasText: name });
|
|
556
|
+
const rowCheckboxFor = (name: string) =>
|
|
557
|
+
rowFor(name).getByRole('checkbox', { name: /^Select row / });
|
|
558
|
+
const selectPage = async () => {
|
|
559
|
+
const response = waitForStudentSelect(page);
|
|
560
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
561
|
+
return await response;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
for (const name of fixtureNames) {
|
|
566
|
+
const createResponse = await page.request.post('/api/training/student', {
|
|
567
|
+
data: { name, mobile: '13812345678', level: 1 },
|
|
568
|
+
headers,
|
|
569
|
+
});
|
|
570
|
+
expect(createResponse.ok()).toBeTruthy();
|
|
571
|
+
const id = (await createResponse.json()).data;
|
|
572
|
+
expect(['string', 'number']).toContain(typeof id);
|
|
573
|
+
fixtureRecords.unshift({ id, name });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const initialSelect = waitForStudentSelect(page);
|
|
577
|
+
await page.getByRole('link', { name: 'Student', exact: true }).click();
|
|
578
|
+
await expect(page).toHaveURL(studentResourceUrl);
|
|
579
|
+
await initialSelect;
|
|
580
|
+
|
|
581
|
+
await page.getByLabel('Student Name').fill(fixturePrefix);
|
|
582
|
+
const filteredResponse = await selectPage();
|
|
583
|
+
const filteredUrl = new URL(filteredResponse.url());
|
|
584
|
+
expect(filteredUrl.searchParams.get('name')).toBe(fixturePrefix);
|
|
585
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveCount(1);
|
|
586
|
+
await expect(rowFor(visibleFixtureNames[20])).toHaveCount(0);
|
|
587
|
+
|
|
588
|
+
await expect(create).toBeVisible();
|
|
589
|
+
await expect(create).toBeEnabled();
|
|
590
|
+
await expect(select).toBeVisible();
|
|
591
|
+
await expect(allPageCheckbox).toHaveCount(0);
|
|
592
|
+
await expect(deleteBulk).toBeDisabled();
|
|
593
|
+
|
|
594
|
+
await select.click();
|
|
595
|
+
await expect(done).toBeVisible();
|
|
596
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
597
|
+
await expect(allPageCheckbox).toBeVisible();
|
|
598
|
+
await expect(rowCheckboxFor(visibleFixtureNames[0])).toBeVisible();
|
|
599
|
+
|
|
600
|
+
await rowCheckboxFor(visibleFixtureNames[0]).check();
|
|
601
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveAttribute('aria-selected', 'true');
|
|
602
|
+
await expect(selectionStatus).toHaveText('Selected one item');
|
|
603
|
+
await expect(allPageCheckbox).toHaveAttribute('aria-checked', 'mixed');
|
|
604
|
+
await expect(allPageCheckbox).toHaveJSProperty('indeterminate', true);
|
|
605
|
+
|
|
606
|
+
await done.click();
|
|
607
|
+
await expect(select).toBeVisible();
|
|
608
|
+
await expect(allPageCheckbox).toHaveCount(0);
|
|
609
|
+
await expect(create).toBeEnabled();
|
|
610
|
+
|
|
611
|
+
await select.click();
|
|
612
|
+
await rowCheckboxFor(visibleFixtureNames[0]).check();
|
|
613
|
+
await allPageCheckbox.check();
|
|
614
|
+
await expect(selectionStatus).toHaveText('Selected 20 items');
|
|
615
|
+
await expect(allPageCheckbox).toBeChecked();
|
|
616
|
+
await expect(allPageCheckbox).toHaveJSProperty('indeterminate', false);
|
|
617
|
+
|
|
618
|
+
const pageTwoResponse = waitForStudentSelect(page);
|
|
619
|
+
await page.getByRole('button', { name: '»', exact: true }).click();
|
|
620
|
+
const pageTwoUrl = new URL((await pageTwoResponse).url());
|
|
621
|
+
expect(pageTwoUrl.searchParams.get('pageNo')).toBe('2');
|
|
622
|
+
await expect(selectionStatus).toHaveText('Selected 20 items');
|
|
623
|
+
await expect(rowFor(visibleFixtureNames[20])).toHaveCount(1);
|
|
624
|
+
await expect(rowCheckboxFor(visibleFixtureNames[20])).not.toBeChecked();
|
|
625
|
+
|
|
626
|
+
await rowCheckboxFor(visibleFixtureNames[20]).check();
|
|
627
|
+
await expect(rowFor(visibleFixtureNames[20])).toHaveAttribute('aria-selected', 'true');
|
|
628
|
+
await expect(selectionStatus).toHaveText('Selected 21 items');
|
|
629
|
+
|
|
630
|
+
await page.getByRole('button', { name: '«', exact: true }).click();
|
|
631
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveCount(1);
|
|
632
|
+
await expect(allPageCheckbox).toBeChecked();
|
|
633
|
+
await allPageCheckbox.uncheck();
|
|
634
|
+
await expect(selectionStatus).toHaveText('Selected one item');
|
|
635
|
+
|
|
636
|
+
await rowCheckboxFor(visibleFixtureNames[9]).check();
|
|
637
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
638
|
+
await page.getByLabel('Student Name').fill(`${fixturePrefix} 1`);
|
|
639
|
+
await selectPage();
|
|
640
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
641
|
+
await expect(rowFor(visibleFixtureNames[9])).toHaveAttribute('aria-selected', 'false');
|
|
642
|
+
|
|
643
|
+
await rowCheckboxFor(visibleFixtureNames[9]).check();
|
|
644
|
+
await expect(selectionStatus).toHaveText('Selected one item');
|
|
645
|
+
await page.getByRole('button', { name: 'Reset', exact: true }).click();
|
|
646
|
+
await expect(page.getByLabel('Student Name')).toHaveValue('');
|
|
647
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
648
|
+
await page.getByLabel('Student Name').fill(fixturePrefix);
|
|
649
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
650
|
+
await expect(rowFor(visibleFixtureNames[0])).toHaveCount(1);
|
|
651
|
+
|
|
652
|
+
await rowCheckboxFor(visibleFixtureNames[0]).check();
|
|
653
|
+
await expect(selectionStatus).toHaveText('Selected one item');
|
|
654
|
+
const nameSort = page.getByRole('button', { name: 'Sort by name', exact: true });
|
|
655
|
+
const sortResponse = waitForStudentSelect(page);
|
|
656
|
+
await nameSort.click();
|
|
657
|
+
await sortResponse;
|
|
658
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
659
|
+
|
|
660
|
+
await nameSort.click();
|
|
661
|
+
const selectedRows = page.locator('tbody tr').filter({ hasText: fixturePrefix });
|
|
662
|
+
await expect(selectedRows).toHaveCount(20);
|
|
663
|
+
const selectedNames = await selectedRows.evaluateAll(rows =>
|
|
664
|
+
rows.slice(0, 2).map(row => row.querySelector('td:nth-child(3) a')?.textContent?.trim()),
|
|
665
|
+
);
|
|
666
|
+
const selectedIds = await selectedRows.evaluateAll(rows =>
|
|
667
|
+
rows
|
|
668
|
+
.slice(0, 2)
|
|
669
|
+
.map(row => Number(row.querySelector('td:nth-child(2)')?.textContent?.trim())),
|
|
670
|
+
);
|
|
671
|
+
expect(selectedNames.every((name): name is string => Boolean(name))).toBeTruthy();
|
|
672
|
+
expect(selectedIds.every(Number.isInteger)).toBeTruthy();
|
|
673
|
+
await rowCheckboxFor(selectedNames[0]).check();
|
|
674
|
+
await rowCheckboxFor(selectedNames[1]).check();
|
|
675
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
676
|
+
await expect(deleteBulk).toBeEnabled();
|
|
677
|
+
|
|
678
|
+
let cancelledRequestCount = 0;
|
|
679
|
+
const cancelledRequestListener = (request: Request) => {
|
|
680
|
+
if (
|
|
681
|
+
request.method() === 'POST' &&
|
|
682
|
+
new URL(request.url()).pathname === '/api/training/student/bulk/delete'
|
|
683
|
+
) {
|
|
684
|
+
cancelledRequestCount++;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
page.on('request', cancelledRequestListener);
|
|
688
|
+
try {
|
|
689
|
+
await deleteBulk.click();
|
|
690
|
+
await expect(page.getByRole('dialog')).toContainText('2 selected items');
|
|
691
|
+
await page.getByRole('button', { name: 'No', exact: true }).click();
|
|
692
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
693
|
+
expect(cancelledRequestCount).toBe(0);
|
|
694
|
+
} finally {
|
|
695
|
+
page.off('request', cancelledRequestListener);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let failureRequestCount = 0;
|
|
699
|
+
const bulkDeleteRoute = (url: URL) => url.pathname === '/api/training/student/bulk/delete';
|
|
700
|
+
const bulkDeleteFailureHandler = async (route: Route) => {
|
|
701
|
+
failureRequestCount++;
|
|
702
|
+
await route.fulfill({
|
|
703
|
+
status: 500,
|
|
704
|
+
contentType: 'application/json',
|
|
705
|
+
body: JSON.stringify({ code: 500, message: 'Bulk deletion temporarily unavailable' }),
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
await page.route(bulkDeleteRoute, bulkDeleteFailureHandler);
|
|
709
|
+
try {
|
|
710
|
+
await deleteBulk.click();
|
|
711
|
+
await page.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
712
|
+
await expect.poll(() => failureRequestCount).toBe(1);
|
|
713
|
+
await expect(selectionStatus).toHaveText('Selected 2 items');
|
|
714
|
+
await expect(rowFor(selectedNames[0])).toHaveAttribute('aria-selected', 'true');
|
|
715
|
+
await expect(rowFor(selectedNames[1])).toHaveAttribute('aria-selected', 'true');
|
|
716
|
+
} finally {
|
|
717
|
+
await page.unroute(bulkDeleteRoute, bulkDeleteFailureHandler);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const bulkDeleteRequest = page.waitForRequest(request => {
|
|
721
|
+
const url = new URL(request.url());
|
|
722
|
+
return request.method() === 'POST' && url.pathname === '/api/training/student/bulk/delete';
|
|
723
|
+
});
|
|
724
|
+
const bulkDeleteResponse = page.waitForResponse(response => {
|
|
725
|
+
const url = new URL(response.url());
|
|
726
|
+
return (
|
|
727
|
+
response.request().method() === 'POST' &&
|
|
728
|
+
response.ok() &&
|
|
729
|
+
url.pathname === '/api/training/student/bulk/delete'
|
|
730
|
+
);
|
|
731
|
+
});
|
|
732
|
+
const refetchResponse = waitForStudentSelect(page);
|
|
733
|
+
await deleteBulk.click();
|
|
734
|
+
await page.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
735
|
+
const [request, response, refetch] = await Promise.all([
|
|
736
|
+
bulkDeleteRequest,
|
|
737
|
+
bulkDeleteResponse,
|
|
738
|
+
refetchResponse,
|
|
739
|
+
]);
|
|
740
|
+
expect(response.ok()).toBeTruthy();
|
|
741
|
+
expect(request.postDataJSON()).toEqual({ ids: selectedIds });
|
|
742
|
+
expect(refetch.ok()).toBeTruthy();
|
|
743
|
+
|
|
744
|
+
await expect(rowFor(selectedNames[0])).toHaveCount(0);
|
|
745
|
+
await expect(rowFor(selectedNames[1])).toHaveCount(0);
|
|
746
|
+
await expect(selectionStatus).toHaveText('Selected 0 items');
|
|
747
|
+
await expect(page.locator('tbody tr[aria-selected="true"]')).toHaveCount(0);
|
|
748
|
+
await expect(deleteBulk).toBeDisabled();
|
|
749
|
+
expect(pageErrors).toEqual([]);
|
|
750
|
+
} finally {
|
|
751
|
+
for (const { id, name } of fixtureRecords) {
|
|
752
|
+
const deleteResponse = await page.request.delete(
|
|
753
|
+
`/api/training/student/deleteForce/${id}`,
|
|
754
|
+
{
|
|
755
|
+
headers,
|
|
756
|
+
timeout: 10_000,
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
expect(
|
|
760
|
+
deleteResponse.ok() || deleteResponse.status() === 404,
|
|
761
|
+
`Failed to clean up Training Student ${name} (${id}): ${deleteResponse.status()}`,
|
|
762
|
+
).toBeTruthy();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
|
|
521
768
|
test(
|
|
522
769
|
'ATP-BASIC-FORM-01: Training Student content remains in the Basic Information tab',
|
|
523
770
|
{ 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,41 +20,50 @@ 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.delete(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>Delete') })
|
|
53
57
|
@Api.body(z.null())
|
|
54
58
|
@Passport.systemAdmin()
|
|
55
59
|
async delete(@Arg.param('id', v.tableIdentity()) id: TableIdentity): Promise<void> {
|
|
56
60
|
await this.scope.service.<%=argv.resourceName%>.delete(id);
|
|
57
61
|
}
|
|
62
|
+
|
|
63
|
+
@Web.post('bulk/delete', { summary: $locale('BulkDelete') })
|
|
64
|
+
@Api.body(z.null())
|
|
65
|
+
@Passport.systemAdmin()
|
|
66
|
+
async deleteBulk(@Arg.body() command: Dto<%=argv.resourceNameCapitalize%>DeleteBulk): Promise<void> {
|
|
67
|
+
await this.scope.service.<%=argv.resourceName%>.deleteBulk(command.ids);
|
|
68
|
+
}
|
|
58
69
|
}
|
|
@@ -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
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TableIdentity } from 'table-identity';
|
|
2
2
|
import type { IQueryParams } from 'vona-module-a-orm';
|
|
3
|
+
import type { IRbacScopeAccess } from 'vona-module-a-rbac';
|
|
3
4
|
import type { IDecoratorControllerOptions } from 'vona-module-a-web';
|
|
4
5
|
|
|
5
6
|
import { BeanBase } from 'vona';
|
|
@@ -12,6 +13,7 @@ import type { Model<%=argv.resourceNameCapitalize%> } from '../model/<%=argv.res
|
|
|
12
13
|
|
|
13
14
|
import { $locale } from '../.metadata/locales.ts';
|
|
14
15
|
import { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
|
|
16
|
+
import { Dto<%=argv.resourceNameCapitalize%>DeleteBulk } from '../dto/<%=argv.resourceName%>DeleteBulk.tsx';
|
|
15
17
|
import { Dto<%=argv.resourceNameCapitalize%>SelectReq } from '../dto/<%=argv.resourceName%>SelectReq.tsx';
|
|
16
18
|
import { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
|
|
17
19
|
import { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.resourceName%>Update.tsx';
|
|
@@ -58,4 +60,23 @@ export class Controller<%=argv.resourceNameCapitalize%> extends BeanBase {
|
|
|
58
60
|
async delete(@Arg.param('id', v.tableIdentity()) id: TableIdentity): Promise<void> {
|
|
59
61
|
await this.scope.service.<%=argv.resourceName%>.delete(id);
|
|
60
62
|
}
|
|
63
|
+
|
|
64
|
+
@Web.post('bulk/delete', { summary: $locale('BulkDelete') })
|
|
65
|
+
@Api.body(z.null())
|
|
66
|
+
@Passport.rbac({ actionInherit: 'delete' })
|
|
67
|
+
async deleteBulk(
|
|
68
|
+
@Arg.body() command: Dto<%=argv.resourceNameCapitalize%>DeleteBulk,
|
|
69
|
+
@Arg.rbacScopeCurrent() rbacScopeCurrent: IRbacScopeAccess,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const entries = await this.bean.rbacResourceBulk.entries(
|
|
72
|
+
command.ids,
|
|
73
|
+
async ids => {
|
|
74
|
+
return await this.scope.model.<%=argv.resourceName%>.select({
|
|
75
|
+
where: rbacScopeCurrent.where({ id: ids }),
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
rbacScopeCurrent,
|
|
79
|
+
);
|
|
80
|
+
await this.scope.service.<%=argv.resourceName%>.deleteBulk(entries.map(entry => entry.id));
|
|
81
|
+
}
|
|
61
82
|
}
|
|
@@ -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": [
|