@nocobase/app-template-default 1.0.0-beta.82 → 1.0.0-beta.84
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/.agents/rules/build-and-deploy.md +1 -1
- package/.agents/rules/shell.md +4 -0
- package/AGENTS.md +9 -1
- package/README.MD +10 -2
- package/client/components/back-button.tsx +3 -5
- package/client/components/default-access-editor.tsx +13 -2
- package/client/components/feedback-form.tsx +474 -0
- package/client/components/lifecycle-diagram.tsx +10 -10
- package/client/components/permission-editor.tsx +396 -210
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +17 -6
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +24 -7
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +19 -7
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +15 -7
- package/client/extensions/nocobase-device-approval/device-approval.tsx +7 -2
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +16 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +7 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +20 -17
- package/client/hooks/use-list-data.ts +77 -0
- package/client/hooks/use-list-location.ts +23 -0
- package/client/hooks/use-list-tabs.ts +42 -0
- package/client/hooks/use-table-url-state.ts +143 -30
- package/client/locales/system/en-US.ts +10 -0
- package/client/locales/system/settings-en-US.ts +8 -0
- package/client/locales/system/settings-zh-CN.ts +8 -0
- package/client/locales/system/zh-CN.ts +8 -0
- package/client/pages/settings/default-access-form.tsx +44 -26
- package/client/pages/settings/default-access.tsx +56 -27
- package/client/pages/settings/hooks.tsx +4 -59
- package/client/pages/settings/page-permissions.ts +17 -0
- package/client/pages/settings/permissions-model.ts +54 -58
- package/client/pages/settings/role-form.tsx +98 -56
- package/client/pages/settings/roles.tsx +11 -3
- package/client/pages/settings/shared.tsx +43 -33
- package/client/pages/settings/user-form.tsx +51 -30
- package/client/pages/settings/users.tsx +39 -27
- package/client/routing/session-preparation.tsx +10 -3
- package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
- package/package.json +13 -12
- package/server/providers/index.ts +4 -1
- package/server/providers/submission.ts +61 -0
- package/tests/client/auth-i18n.test.tsx +9 -0
- package/tests/client/back-button.test.tsx +3 -3
- package/tests/client/default-access.test.tsx +60 -4
- package/tests/client/form-feedback.test.tsx +589 -0
- package/tests/client/list-recovery.test.tsx +373 -0
- package/tests/client/list-tabs.test.tsx +121 -0
- package/tests/client/page-permission-navigation.test.ts +8 -0
- package/tests/client/permission-editor.test.tsx +191 -21
- package/tests/client/permission-filter-binding.test.tsx +1 -1
- package/tests/client/role-return.test.tsx +2 -1
- package/tests/client/session-preparation.test.tsx +24 -1
- package/tests/client/settings-continuity.test.tsx +5 -4
- package/tests/client/settings-feedback.test.tsx +25 -0
- package/tests/client/settings-permissions.test.ts +35 -0
- package/tests/client/settings.test.tsx +2 -1
- package/tests/client/table-url-state.test.tsx +117 -3
- package/tests/client/user-drawer.test.tsx +2 -1
- package/tests/client/user-permissions.test.tsx +30 -5
- package/tests/database/mutation-receipts.test.ts +45 -0
- package/tests/fixtures/form-feedback/README.md +33 -0
- package/tests/fixtures/form-feedback/equipment-detail.tsx +11 -0
- package/tests/fixtures/form-feedback/equipment-layout.tsx +15 -0
- package/tests/fixtures/form-feedback/equipment-migration.ts +27 -0
- package/tests/fixtures/form-feedback/equipment-server.ts +197 -0
- package/tests/fixtures/form-feedback/equipment-validation.tsx +94 -0
- package/tests/fixtures/form-feedback/equipment.tsx +369 -0
- package/tests/fixtures/form-feedback/verify-inline.mjs +174 -0
- package/tests/fixtures/form-feedback/verify.mjs +373 -0
- package/tests/fixtures/submission/README.md +35 -0
- package/tests/fixtures/submission/browser.test.ts +478 -0
- package/tests/fixtures/submission/migration.ts +61 -0
- package/tests/fixtures/submission/page.tsx +138 -0
- package/tests/fixtures/submission/playwright.config.ts +11 -0
- package/tests/fixtures/submission/routes.ts +208 -0
- package/tests/helpers/settings-client.tsx +31 -0
- package/tests/server/member-role.test.ts +7 -1
- package/tests/server/submission-settings.test.ts +241 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderSettingsClient as renderWithApp } from '../helpers/settings-client.js';
|
|
2
|
+
import { answerApi } from '@nocobase/app-testing/client';
|
|
2
3
|
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
4
|
import {
|
|
4
5
|
AuthorizationClient,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
defineAppRoutes,
|
|
10
11
|
defineClientPlugin,
|
|
11
12
|
} from '@nocobase/app-client/plugins';
|
|
12
|
-
import { screen, waitFor } from '@testing-library/react';
|
|
13
|
+
import { screen, waitFor, within } from '@testing-library/react';
|
|
13
14
|
import userEvent from '@testing-library/user-event';
|
|
14
15
|
import { Routes, Route } from 'react-router';
|
|
15
16
|
import { describe, expect, it, vi } from 'vitest';
|
|
@@ -138,12 +139,36 @@ describe('user permission configuration', () => {
|
|
|
138
139
|
expect(
|
|
139
140
|
screen.getByRole('heading', { name: words.roleAssignment }),
|
|
140
141
|
).toBeVisible();
|
|
141
|
-
|
|
142
|
+
const permissionNames = [
|
|
143
|
+
words.readUsers,
|
|
144
|
+
words.createUsers,
|
|
145
|
+
words.updateUsers,
|
|
146
|
+
words.disable,
|
|
147
|
+
words.enable,
|
|
148
|
+
words.revokeSessions,
|
|
149
|
+
words.assignRole,
|
|
150
|
+
];
|
|
151
|
+
const permissionCheckboxes = permissionNames.map((name) =>
|
|
152
|
+
screen.getByRole('checkbox', { name, exact: true }),
|
|
153
|
+
);
|
|
154
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(
|
|
155
|
+
permissionNames.length,
|
|
156
|
+
);
|
|
142
157
|
expect(
|
|
143
158
|
screen.queryByRole('checkbox', { name: words.inviteUsers }),
|
|
144
159
|
).not.toBeInTheDocument();
|
|
145
|
-
const rowIds =
|
|
146
|
-
expect(
|
|
160
|
+
const rowIds = permissionCheckboxes.map((node) => node.id);
|
|
161
|
+
expect(rowIds).not.toContain('');
|
|
162
|
+
expect(new Set(rowIds).size).toBe(permissionNames.length);
|
|
163
|
+
const status = within(
|
|
164
|
+
screen.getByRole('tablist', { name: words.permissionStatus }),
|
|
165
|
+
);
|
|
166
|
+
expect(status.getAllByRole('tab')).toHaveLength(3);
|
|
167
|
+
expect(
|
|
168
|
+
status.getByRole('tab', { name: words.permissionAll, exact: true }),
|
|
169
|
+
).toHaveAttribute('aria-selected', 'true');
|
|
170
|
+
for (const name of [words.permissionGranted, words.permissionUngranted])
|
|
171
|
+
expect(status.getByRole('tab', { name, exact: true })).toBeVisible();
|
|
147
172
|
await user.click(
|
|
148
173
|
screen.getByRole('button', { name: words.save, exact: true }),
|
|
149
174
|
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { describeMigration } from '@nocobase/app-testing/server';
|
|
3
|
+
import { expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describeMigration('202611020002_create_mutation_receipts', {
|
|
6
|
+
sources: [
|
|
7
|
+
{
|
|
8
|
+
packageName: 'operation-receipts-test',
|
|
9
|
+
directory: path.resolve(
|
|
10
|
+
import.meta.dirname,
|
|
11
|
+
'../../database/main/migrations',
|
|
12
|
+
),
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
up: async ({ connection, expectCollection }) => {
|
|
16
|
+
const receipts = await expectCollection('mutationReceipts').toExist();
|
|
17
|
+
expect(receipts.primaryKey).toEqual(['id']);
|
|
18
|
+
await expectCollection('mutationReceipts').toHaveField('expiresAt', {
|
|
19
|
+
nullable: false,
|
|
20
|
+
});
|
|
21
|
+
await connection.query
|
|
22
|
+
.insertInto('mutationReceipts')
|
|
23
|
+
.values({
|
|
24
|
+
id: 'sample',
|
|
25
|
+
digest: 'digest',
|
|
26
|
+
result: '{}',
|
|
27
|
+
expiresAt: new Date(),
|
|
28
|
+
})
|
|
29
|
+
.execute();
|
|
30
|
+
await expect(
|
|
31
|
+
connection.query
|
|
32
|
+
.insertInto('mutationReceipts')
|
|
33
|
+
.values({
|
|
34
|
+
id: 'sample',
|
|
35
|
+
digest: 'other',
|
|
36
|
+
result: '{}',
|
|
37
|
+
expiresAt: new Date(),
|
|
38
|
+
})
|
|
39
|
+
.execute(),
|
|
40
|
+
).rejects.toThrow();
|
|
41
|
+
},
|
|
42
|
+
down: async ({ expectCollection }) => {
|
|
43
|
+
await expectCollection('mutationReceipts').not.toExist();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Isolated form feedback verification
|
|
2
|
+
|
|
3
|
+
This test-only workload checks newly generated Default and Examples applications. It is not registered in either template runtime. Use independent databases and storage, native sign-in and the existing administrator action; never install it in a management or production application.
|
|
4
|
+
|
|
5
|
+
## Prepare two new applications
|
|
6
|
+
|
|
7
|
+
Create one application from each selected target template with `pnpm create @nocobase/app@beta <name> --template <package>@<exact-version> --no-install --json`. Use actual released packages or immutable, explicitly versioned candidates in an isolated registry. Read each generated application's instructions, install dependencies and record exact template/runtime versions, lockfiles, source commits and package integrity. Keep the target's feedback component and auth/settings sources unchanged.
|
|
8
|
+
|
|
9
|
+
Copy `equipment-layout.tsx`, `equipment-detail.tsx`, `equipment.tsx` and `equipment-validation.tsx` into the application's `client/pages/`; copy `equipment-server.ts` as `server/routes/equipment-fixture.ts` and `equipment-migration.ts` as `database/main/migrations/209901010001_equipment_fixture.ts`. Keep an executed migration unchanged. Register `equipmentRoutes` in the server route contributions.
|
|
10
|
+
|
|
11
|
+
Declare `/equipment` with required native authentication, `authz: { resource: { type: 'user', id: '*' }, action: 'create' }` and the equipment layout. Its `dialog` and `drawer` children load the equipment editor; its `:equipmentId` detail child has an `edit` child loading that editor. The layout renders the page editor at its own path and the outlet on child paths. Keep the actual RouteDialog, RouteDrawer and RouteChildPage. Declare `/equipment-validation` loading `equipment-validation.tsx` with the same authentication and administrator action. The server independently checks that native permission.
|
|
12
|
+
|
|
13
|
+
Add the `equipment` application translation group: title, name, help, department, departmentError, attachment, retainedAttachment, longForm, response, save, saved, itemError, customCopy. English values are “Equipment borrowing”, “Equipment name”, “Keep this equipment identifier.”, “Department”, “Department must be Operations.”, “Attachment”, “Retained attachment”, “Additional equipment information.”, “Response”, “Save request”, “Saved; workflow submitted.”, “Check this borrowed item.” and “Customized equipment workflow and asset fields are preserved.” respectively. Add corresponding Chinese application overrides and preserve existing system groups.
|
|
14
|
+
|
|
15
|
+
Initialize and check configuration through the native CLI, using a separate SQLite database and storage for each app. Start one fixture at a time with `NOCOBASE_STRICT_STARTUP=true APP_SERVER_PORT=<port> pnpm dev`, record its printed server Local URL, and verify the served `client/runtime.ts` identifies the intended application. Strict startup isolates the backend port while several development tasks share a host. Never copy configuration secrets or native sessions between applications or into evidence.
|
|
16
|
+
|
|
17
|
+
Run `pnpm typecheck` after wiring the workload. Install the application's matching Playwright Chromium shell in a task-local directory and set `PLAYWRIGHT_BROWSERS_PATH`.
|
|
18
|
+
|
|
19
|
+
## Inline feedback regression
|
|
20
|
+
|
|
21
|
+
From each generated application run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node tests/fixtures/form-feedback/verify-inline.mjs <printed-server-entry-url> <evidence-directory> <sample-name>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The verifier accepts only an explicitly supplied loopback `/main/` entry. It uses the isolated initializer's default native administrator or `TEST_ADMIN_USERNAME`/`TEST_ADMIN_PASSWORD` from the environment; it saves no session.
|
|
28
|
+
|
|
29
|
+
Mapped field and array-level reasons appear only beside their controls, without a top summary or navigation list. Submission still opens required regions, scrolls and focuses the first editable error. Repeated submission does not accumulate errors; corrections and successful retry clear obsolete marks while preserving input, selected files, existing attachment references and help/error associations. Unknown paths, permission/network failures and unavailable editable focus targets retain a safe accessible form message. The script checks these cases in a real routed drawer and saves English screenshots and machine-readable results.
|
|
30
|
+
|
|
31
|
+
The broader `verify.mjs` fixture remains available for layout order, row identities, long forms, dialog/drawer/detail routes, native permission checks and the fictional submitted/approved process. Its failure modes are explicit test inputs, not external providers. This fixture does not verify resumable uploads or a commercial approval provider.
|
|
32
|
+
|
|
33
|
+
Existing source reconciliation support is documented in the installed upgrade Skill's `references/form-feedback-upgrade.md`. Preserve application-owned business code and executed migrations when using that support; creating old applications or replaying upgrades is not part of this inline regression.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Outlet } from 'react-router';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
|
|
4
|
+
export default function EquipmentDetail(): ReactElement {
|
|
5
|
+
return (
|
|
6
|
+
<section aria-label='Equipment record detail'>
|
|
7
|
+
<h2>Equipment record detail</h2>
|
|
8
|
+
<Outlet />
|
|
9
|
+
</section>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Outlet, useLocation } from 'react-router';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
import EquipmentFixture from './equipment.js';
|
|
4
|
+
|
|
5
|
+
export default function EquipmentLayout(): ReactElement {
|
|
6
|
+
const { pathname } = useLocation();
|
|
7
|
+
return (
|
|
8
|
+
<div className='relative h-full'>
|
|
9
|
+
<section aria-label='Equipment requests'>
|
|
10
|
+
Equipment borrowing requests
|
|
11
|
+
</section>
|
|
12
|
+
{pathname === '/equipment' ? <EquipmentFixture /> : <Outlet />}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Test-only schema, installed before the customized application's source upgrade.
|
|
2
|
+
import { defineMigration } from '@nocobase/db';
|
|
3
|
+
export default defineMigration({
|
|
4
|
+
name: '209901010001_equipment_fixture',
|
|
5
|
+
async up({ builder }) {
|
|
6
|
+
await builder.createCollection('equipmentFixture', (collection) => {
|
|
7
|
+
collection.string('id').primary().notNull();
|
|
8
|
+
for (const key of [
|
|
9
|
+
'name',
|
|
10
|
+
'department',
|
|
11
|
+
'attachment',
|
|
12
|
+
'workflow',
|
|
13
|
+
'customField',
|
|
14
|
+
])
|
|
15
|
+
collection.string(key, { nullable: false });
|
|
16
|
+
});
|
|
17
|
+
await builder.createCollection('equipmentFixtureHistory', (collection) => {
|
|
18
|
+
collection.string('id').primary().notNull();
|
|
19
|
+
for (const key of ['recordId', 'event', 'actor'])
|
|
20
|
+
collection.string(key, { nullable: false });
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
async down({ builder }) {
|
|
24
|
+
await builder.dropCollection('equipmentFixtureHistory');
|
|
25
|
+
await builder.dropCollection('equipmentFixture');
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Application } from '@nocobase/app-server/application';
|
|
3
|
+
// Test-only routes for an isolated application; authentication and authorization remain native.
|
|
4
|
+
import { authenticationToken } from '@nocobase/app-plugin-authentication';
|
|
5
|
+
import { databaseManagerToken } from '@nocobase/db';
|
|
6
|
+
import {
|
|
7
|
+
ApiError,
|
|
8
|
+
apiErrorHandler,
|
|
9
|
+
apiValidator,
|
|
10
|
+
apiErrorResponse,
|
|
11
|
+
dataResponse,
|
|
12
|
+
describeRoute,
|
|
13
|
+
defineApiRoutes,
|
|
14
|
+
} from '@nocobase/app-server/router';
|
|
15
|
+
import {
|
|
16
|
+
authorizationToken,
|
|
17
|
+
type AuthorizationEnv,
|
|
18
|
+
} from '@nocobase/app-plugin-authorization';
|
|
19
|
+
import { Hono } from 'hono';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
export const equipmentRoutes = defineApiRoutes<Application>((app) => {
|
|
23
|
+
const routes = new Hono<AuthorizationEnv>();
|
|
24
|
+
const auth = app.container.resolve(authenticationToken);
|
|
25
|
+
const db = app.container.resolve(databaseManagerToken);
|
|
26
|
+
routes.use(
|
|
27
|
+
'/equipmentFixture',
|
|
28
|
+
auth.required(),
|
|
29
|
+
app.container.resolve(authorizationToken).middleware(),
|
|
30
|
+
);
|
|
31
|
+
routes.onError(apiErrorHandler);
|
|
32
|
+
const schema = z.object({
|
|
33
|
+
mode: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
department: z.string(),
|
|
36
|
+
items: z.array(z.object({ key: z.string(), name: z.string() })),
|
|
37
|
+
attachment: z.string(),
|
|
38
|
+
recordId: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
const description = {
|
|
41
|
+
tags: ['Equipment fixture'],
|
|
42
|
+
summary: 'Equipment verification fixture',
|
|
43
|
+
responses: {
|
|
44
|
+
200: dataResponse(z.unknown()),
|
|
45
|
+
400: apiErrorResponse(400),
|
|
46
|
+
401: apiErrorResponse(401),
|
|
47
|
+
403: apiErrorResponse(403),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
routes.get(
|
|
51
|
+
'/equipmentFixture',
|
|
52
|
+
describeRoute({ ...description, operationId: 'equipmentFixtureGet' }),
|
|
53
|
+
async (c) => {
|
|
54
|
+
await c.get('authz').require({
|
|
55
|
+
resource: { type: 'user', id: '*' },
|
|
56
|
+
action: 'create',
|
|
57
|
+
});
|
|
58
|
+
const repository = db.repository('equipmentFixture');
|
|
59
|
+
if (!(await repository.findOne({ filter: { id: 'retained' } })))
|
|
60
|
+
await repository.createOne({
|
|
61
|
+
values: {
|
|
62
|
+
id: 'retained',
|
|
63
|
+
name: 'Legacy camera',
|
|
64
|
+
department: 'Operations',
|
|
65
|
+
attachment: 'safety-check.pdf',
|
|
66
|
+
workflow: 'approved',
|
|
67
|
+
customField: 'asset-714',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return c.json({
|
|
71
|
+
data: await repository.findOne({ filter: { id: 'retained' } }),
|
|
72
|
+
requests: await repository.findMany({}),
|
|
73
|
+
history: await db.repository('equipmentFixtureHistory').findMany({}),
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
routes.post(
|
|
78
|
+
'/equipmentFixture',
|
|
79
|
+
describeRoute({ ...description, operationId: 'equipmentFixtureSave' }),
|
|
80
|
+
apiValidator('json', schema),
|
|
81
|
+
async (c) => {
|
|
82
|
+
await c.get('authz').require({
|
|
83
|
+
resource: { type: 'user', id: '*' },
|
|
84
|
+
action: 'create',
|
|
85
|
+
});
|
|
86
|
+
const input = c.req.valid('json');
|
|
87
|
+
if (input.mode === 'denied')
|
|
88
|
+
throw new ApiError({
|
|
89
|
+
status: 'PERMISSION_DENIED',
|
|
90
|
+
reason: 'RESTRICTED',
|
|
91
|
+
domain: 'equipment-fixture',
|
|
92
|
+
message: 'Private equipment details',
|
|
93
|
+
});
|
|
94
|
+
if (input.mode !== 'success' && input.mode !== 'approve')
|
|
95
|
+
throw new ApiError({
|
|
96
|
+
status: 'INVALID_ARGUMENT',
|
|
97
|
+
reason: 'EQUIPMENT_INVALID',
|
|
98
|
+
domain: 'equipment-fixture',
|
|
99
|
+
message: 'Private developer stack',
|
|
100
|
+
fieldViolations:
|
|
101
|
+
input.mode === 'structured'
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
field: 'items[1].name',
|
|
105
|
+
description: 'Private item value',
|
|
106
|
+
reason: 'ITEM',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
field: 'items.0.name',
|
|
110
|
+
description: 'Private item value',
|
|
111
|
+
reason: 'ITEM',
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
: input.mode === 'business'
|
|
115
|
+
? [
|
|
116
|
+
{
|
|
117
|
+
field: 'department',
|
|
118
|
+
reason: 'DEPARTMENT',
|
|
119
|
+
description: 'Private business detail',
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
: [
|
|
123
|
+
{
|
|
124
|
+
field: 'privateSalary',
|
|
125
|
+
description: 'Secret salary 123',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
return db.transaction(async (connection) => {
|
|
130
|
+
const repository = connection.repository('equipmentFixture');
|
|
131
|
+
const history = connection.repository('equipmentFixtureHistory');
|
|
132
|
+
if (input.mode === 'approve') {
|
|
133
|
+
const request = await repository.findOne({
|
|
134
|
+
filter: { id: input.recordId, workflow: 'submitted' },
|
|
135
|
+
});
|
|
136
|
+
if (!request)
|
|
137
|
+
throw new ApiError({
|
|
138
|
+
status: 'INVALID_ARGUMENT',
|
|
139
|
+
reason: 'NOT_SUBMITTED',
|
|
140
|
+
domain: 'equipment-fixture',
|
|
141
|
+
message: 'Only submitted requests can be approved.',
|
|
142
|
+
});
|
|
143
|
+
await repository.updateMany({
|
|
144
|
+
filter: { id: input.recordId, workflow: 'submitted' },
|
|
145
|
+
values: { workflow: 'approved' },
|
|
146
|
+
});
|
|
147
|
+
await history.createOne({
|
|
148
|
+
values: {
|
|
149
|
+
id: randomUUID(),
|
|
150
|
+
recordId: input.recordId,
|
|
151
|
+
event: 'approved',
|
|
152
|
+
actor: 'native-administrator',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return c.json({
|
|
156
|
+
data: await repository.findOne({ filter: { id: input.recordId } }),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (input.department !== 'Operations')
|
|
160
|
+
throw new ApiError({
|
|
161
|
+
status: 'INVALID_ARGUMENT',
|
|
162
|
+
reason: 'DEPARTMENT',
|
|
163
|
+
domain: 'equipment-fixture',
|
|
164
|
+
message: 'Private business detail',
|
|
165
|
+
fieldViolations: [
|
|
166
|
+
{
|
|
167
|
+
field: 'department',
|
|
168
|
+
reason: 'DEPARTMENT',
|
|
169
|
+
description: 'Private business detail',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
const id = randomUUID();
|
|
174
|
+
await repository.createOne({
|
|
175
|
+
values: {
|
|
176
|
+
id,
|
|
177
|
+
name: input.name,
|
|
178
|
+
department: input.department,
|
|
179
|
+
attachment: input.attachment,
|
|
180
|
+
workflow: 'submitted',
|
|
181
|
+
customField: 'asset-714',
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
await history.createOne({
|
|
185
|
+
values: {
|
|
186
|
+
id: randomUUID(),
|
|
187
|
+
recordId: id,
|
|
188
|
+
event: 'submitted',
|
|
189
|
+
actor: 'native-administrator',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return c.json({ data: await repository.findOne({ filter: { id } }) });
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
return new Hono().route('/', routes);
|
|
197
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState, type ReactElement } from 'react';
|
|
2
|
+
import { useForm, useFieldArray } from 'react-hook-form';
|
|
3
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
4
|
+
import { PageContainer } from '#components/page-container';
|
|
5
|
+
import { Button } from '#components/ui/button';
|
|
6
|
+
import { Input } from '#components/ui/input';
|
|
7
|
+
|
|
8
|
+
// Isolated browser regression workload; all feedback behavior comes from the selected template component.
|
|
9
|
+
export default function EquipmentRegressions(): ReactElement {
|
|
10
|
+
const form = useForm<{ items: { name: string }[] }>({
|
|
11
|
+
defaultValues: { items: [{ name: 'Camera' }] },
|
|
12
|
+
shouldFocusError: false,
|
|
13
|
+
});
|
|
14
|
+
const rows = useFieldArray({
|
|
15
|
+
control: form.control,
|
|
16
|
+
name: 'items',
|
|
17
|
+
rules: {
|
|
18
|
+
minLength: { value: 2, message: 'Add at least two equipment items.' },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const [saved, setSaved] = useState(false);
|
|
22
|
+
const [invalid, setInvalid] = useState(false);
|
|
23
|
+
const [corrected, setCorrected] = useState(false);
|
|
24
|
+
return (
|
|
25
|
+
<PageContainer>
|
|
26
|
+
<h1 className='text-xl font-semibold'>Equipment validation</h1>
|
|
27
|
+
<div className='grid gap-8 max-w-2xl'>
|
|
28
|
+
<FeedbackForm
|
|
29
|
+
aria-label='Equipment group'
|
|
30
|
+
className='space-y-4'
|
|
31
|
+
noValidate
|
|
32
|
+
validationErrors={form.formState.errors}
|
|
33
|
+
submitting={form.formState.isSubmitting}
|
|
34
|
+
onSubmit={form.handleSubmit(() => setSaved(true))}
|
|
35
|
+
>
|
|
36
|
+
<h2 id='equipment-group-label' className='text-lg font-semibold'>
|
|
37
|
+
Equipment group
|
|
38
|
+
</h2>
|
|
39
|
+
<div>
|
|
40
|
+
<Button
|
|
41
|
+
type='button'
|
|
42
|
+
data-field-path='items'
|
|
43
|
+
aria-labelledby='equipment-group-label'
|
|
44
|
+
onClick={() => rows.append({ name: 'Projector' })}
|
|
45
|
+
>
|
|
46
|
+
Add equipment
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
{rows.fields.map((row, i) => (
|
|
50
|
+
<div key={row.id}>
|
|
51
|
+
<label htmlFor={'group-' + row.id}>Equipment item {i + 1}</label>
|
|
52
|
+
<Input
|
|
53
|
+
id={'group-' + row.id}
|
|
54
|
+
{...form.register(`items.${i}.name`)}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
<Button type='submit'>Save equipment group</Button>
|
|
59
|
+
{saved && <p role='status'>Equipment group saved.</p>}
|
|
60
|
+
</FeedbackForm>
|
|
61
|
+
<FeedbackForm
|
|
62
|
+
aria-label='Unique equipment'
|
|
63
|
+
className='space-y-4'
|
|
64
|
+
noValidate
|
|
65
|
+
onSubmit={() => setInvalid(true)}
|
|
66
|
+
>
|
|
67
|
+
<h2 className='text-lg'>Unique equipment name</h2>
|
|
68
|
+
<div data-slot='field'>
|
|
69
|
+
<label htmlFor='unique-equipment'>Unique name</label>
|
|
70
|
+
<Input
|
|
71
|
+
id='unique-equipment'
|
|
72
|
+
name='uniqueName'
|
|
73
|
+
defaultValue='Existing camera'
|
|
74
|
+
aria-invalid={invalid}
|
|
75
|
+
aria-describedby={corrected ? 'updated-help' : 'original-help'}
|
|
76
|
+
onChange={() => {
|
|
77
|
+
setInvalid(false);
|
|
78
|
+
setCorrected(true);
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
<p id='original-help'>Use a unique inventory name.</p>
|
|
82
|
+
<p id='updated-help'>Corrected input is retained.</p>
|
|
83
|
+
{invalid && (
|
|
84
|
+
<p data-slot='field-error' className='text-destructive'>
|
|
85
|
+
Choose a unique equipment name.
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
<Button type='submit'>Save unique name</Button>
|
|
90
|
+
</FeedbackForm>
|
|
91
|
+
</div>
|
|
92
|
+
</PageContainer>
|
|
93
|
+
);
|
|
94
|
+
}
|