@mpfrontend/nx-plugin 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +87 -2
- package/dist/feature-generator.d.ts +7 -0
- package/dist/feature-generator.js +9 -0
- package/dist/feature-generator.js.map +1 -0
- package/dist/generator.d.ts +6 -0
- package/dist/generator.js +14 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/nx-tree.d.ts +5 -0
- package/dist/nx-tree.js +12 -0
- package/dist/nx-tree.js.map +1 -0
- package/dist/package-generator.d.ts +8 -0
- package/dist/package-generator.js +9 -0
- package/dist/package-generator.js.map +1 -0
- package/dist/route-generator.d.ts +8 -0
- package/dist/route-generator.js +9 -0
- package/dist/route-generator.js.map +1 -0
- package/dist/scaffold.d.ts +96 -0
- package/dist/scaffold.js +227 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/templates.d.ts +21 -0
- package/dist/templates.js +83 -0
- package/dist/templates.js.map +1 -0
- package/dist/workspace.d.ts +41 -0
- package/dist/workspace.js +213 -0
- package/dist/workspace.js.map +1 -0
- package/generators.json +24 -0
- package/package.json +37 -3
- package/schema.json +18 -0
- package/schemas/feature-schema.json +25 -0
- package/schemas/package-schema.json +28 -0
- package/schemas/route-schema.json +31 -0
- package/templates/feature/list/api/server/__resource__.ts.template +24 -0
- package/templates/feature/list/app/api/__resource__/route.ts.template +24 -0
- package/templates/feature/list/entities/__resource__/index.ts.template +12 -0
- package/templates/feature/list/entities/__resource__/model/__resource__-list.test.ts.template +23 -0
- package/templates/feature/list/entities/__resource__/model/__resource__-list.ts.template +24 -0
- package/templates/feature/list/entities/__resource__/model/__resource__.test.ts.template +17 -0
- package/templates/feature/list/entities/__resource__/model/__resource__.ts.template +19 -0
- package/templates/feature/list/features/__feature__/api/__feature__-read.ts.template +12 -0
- package/templates/feature/list/features/__feature__/api/__feature__.ts.template +28 -0
- package/templates/feature/list/features/__feature__/hooks/use-__feature__-list.ts.template +22 -0
- package/templates/feature/list/features/__feature__/index.ts.template +3 -0
- package/templates/feature/list/features/__feature__/model/__feature__-fields.ts.template +4 -0
- package/templates/feature/list/features/__feature__/model/overrides/index.ts.template +3 -0
- package/templates/feature/list/features/__feature__/ui/__feature__-detail-screen.tsx.template +16 -0
- package/templates/feature/list/features/__feature__/ui/__feature__-detail.test.tsx.template +28 -0
- package/templates/feature/list/features/__feature__/ui/__feature__-detail.tsx.template +44 -0
- package/templates/feature/list/features/__feature__/ui/__feature__-list.tsx.template +75 -0
- package/templates/feature/list/features/__feature__/ui/__feature__-screen.tsx.template +31 -0
- package/templates/feature/list/features/__feature__/utils/__feature__-href.test.ts.template +7 -0
- package/templates/feature/list/features/__feature__/utils/__feature__-href.ts.template +4 -0
- package/templates/feature/screen/features/__feature__/api/__feature__.ts.template +13 -0
- package/templates/feature/screen/features/__feature__/hooks/use-__feature__.ts.template +27 -0
- package/templates/feature/screen/features/__feature__/index.ts.template +3 -0
- package/templates/feature/screen/features/__feature__/model/__feature__.test.ts.template +12 -0
- package/templates/feature/screen/features/__feature__/model/__feature__.ts.template +12 -0
- package/templates/feature/screen/features/__feature__/ui/__feature__-panel.tsx.template +22 -0
- package/templates/feature/screen/features/__feature__/ui/__feature__-screen.tsx.template +15 -0
- package/templates/feature/screen/features/__feature__/ui/__feature__-view.test.tsx.template +25 -0
- package/templates/feature/screen/features/__feature__/ui/__feature__-view.tsx.template +38 -0
- package/templates/feature/screen/features/__feature__/utils/__feature__.test.ts.template +9 -0
- package/templates/feature/screen/features/__feature__/utils/__feature__.ts.template +6 -0
- package/templates/next/README.md.template +10 -0
- package/templates/next/docs/api-contracts.md.template +22 -0
- package/templates/next/docs/overview.md.template +67 -0
- package/templates/next/eslint.config.mjs.template +4 -0
- package/templates/next/ftg.config.json.template +11 -0
- package/templates/next/next-env.d.ts.template +2 -0
- package/templates/next/next.config.mjs.template +9 -0
- package/templates/next/package.json.template +27 -0
- package/templates/next/postcss.config.mjs.template +2 -0
- package/templates/next/prepare-runtime.mjs.template +11 -0
- package/templates/next/project.json.template +113 -0
- package/templates/next/src/api/server/transfer.test.ts.template +153 -0
- package/templates/next/src/api/server/transfer.ts.template +25 -0
- package/templates/next/src/app/api/access/route.ts.template +6 -0
- package/templates/next/src/app/api/docs/assets/[file]/route.ts.template +23 -0
- package/templates/next/src/app/api/docs/route.ts.template +8 -0
- package/templates/next/src/app/api/openapi/route.ts.template +12 -0
- package/templates/next/src/app/api/transfer/[...path]/route.ts.template +22 -0
- package/templates/next/src/app/components/page.tsx.template +32 -0
- package/templates/next/src/app/globals.css.template +32 -0
- package/templates/next/src/app/layout.tsx.template +53 -0
- package/templates/next/src/app/page.tsx.template +6 -0
- package/templates/next/src/app/providers.tsx.template +31 -0
- package/templates/next/src/config/access.ts.template +21 -0
- package/templates/next/src/config/app.ts.template +33 -0
- package/templates/next/src/config/server.ts.template +13 -0
- package/templates/next/src/features/preferences/api/preference-cookie.ts.template +14 -0
- package/templates/next/src/features/preferences/hooks/use-preferences.ts.template +30 -0
- package/templates/next/src/features/preferences/index.ts.template +3 -0
- package/templates/next/src/features/preferences/model/messages/en.ts.template +10 -0
- package/templates/next/src/features/preferences/model/messages.ts.template +13 -0
- package/templates/next/src/features/preferences/model/preferences.test.ts.template +32 -0
- package/templates/next/src/features/preferences/model/preferences.ts.template +22 -0
- package/templates/next/src/features/preferences/ui/preference-controls.tsx.template +50 -0
- package/templates/next/src/i18n/index.ts.template +13 -0
- package/templates/next/src/i18n/messages/en.ts.template +18 -0
- package/templates/next/src/i18n/server.ts.template +8 -0
- package/templates/next/src/theme/config.test.ts.template +9 -0
- package/templates/next/src/theme/config.ts.template +7 -0
- package/templates/next/src/theme/theme.css.template +8 -0
- package/templates/next/tsconfig.json.template +11 -0
- package/templates/package/README.md.template +10 -0
- package/templates/package/eslint.config.mjs.template +4 -0
- package/templates/package/package.json.template +15 -0
- package/templates/package/project.json.template +51 -0
- package/templates/package/src/index.ts.template +2 -0
- package/templates/package/src/package-name.test.ts.template +7 -0
- package/templates/package/src/package-name.ts.template +2 -0
- package/templates/package/tsconfig.json.template +7 -0
- package/templates/package/tsconfig.lib.json.template +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|
package/generators.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generators": {
|
|
3
|
+
"application": {
|
|
4
|
+
"factory": "./dist/generator.js",
|
|
5
|
+
"schema": "./schema.json",
|
|
6
|
+
"description": "Create a neutral Next.js consumer using MP Frontend public packages"
|
|
7
|
+
},
|
|
8
|
+
"feature": {
|
|
9
|
+
"factory": "./dist/feature-generator.js",
|
|
10
|
+
"schema": "./schemas/feature-schema.json",
|
|
11
|
+
"description": "Create a feature with ui, model, hooks, api, utils and an index.ts public entry; with a resource, a URL-driven list and detail over that generated read"
|
|
12
|
+
},
|
|
13
|
+
"route": {
|
|
14
|
+
"factory": "./dist/route-generator.js",
|
|
15
|
+
"schema": "./schemas/route-schema.json",
|
|
16
|
+
"description": "Create one Next.js route that renders a feature screen"
|
|
17
|
+
},
|
|
18
|
+
"package": {
|
|
19
|
+
"factory": "./dist/package-generator.js",
|
|
20
|
+
"schema": "./schemas/package-schema.json",
|
|
21
|
+
"description": "Create a shared package with boundary tags, a public entry and the quality targets"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpfrontend/nx-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/panahister/mpfrontend.git",
|
|
8
|
+
"directory": "packages/nx-plugin"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
"./generators.json": "./generators.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"templates",
|
|
25
|
+
"generators.json",
|
|
26
|
+
"schema.json",
|
|
27
|
+
"schemas"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"provenance": true
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@nx/devkit": "23.2.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@mpfrontend/workspace-config": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"generators": "./generators.json"
|
|
6
40
|
}
|
package/schema.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"name": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"pattern": "^[a-z][a-z0-9-]{1,48}$"
|
|
8
|
+
},
|
|
9
|
+
"directory": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"required": [
|
|
14
|
+
"name",
|
|
15
|
+
"directory"
|
|
16
|
+
],
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"app": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The app directory, relative to the workspace root, for example apps/customer"
|
|
8
|
+
},
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^[a-z][a-z0-9-]{1,48}$",
|
|
12
|
+
"description": "The feature name, kebab case"
|
|
13
|
+
},
|
|
14
|
+
"resource": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-z][a-z0-9-]{1,48}$",
|
|
17
|
+
"description": "A read resource selected in the app's ftg.config.json; makes a list feature"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": [
|
|
21
|
+
"app",
|
|
22
|
+
"name"
|
|
23
|
+
],
|
|
24
|
+
"additionalProperties": false
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"name": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"pattern": "^[a-z][a-z0-9-]{1,48}$",
|
|
8
|
+
"description": "The package name, kebab case; the workspace scope is added"
|
|
9
|
+
},
|
|
10
|
+
"directory": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^packages/[a-z][a-z0-9-]{1,48}$",
|
|
13
|
+
"description": "Defaults to packages/<name>"
|
|
14
|
+
},
|
|
15
|
+
"runtime": {
|
|
16
|
+
"enum": [
|
|
17
|
+
"universal",
|
|
18
|
+
"client",
|
|
19
|
+
"server"
|
|
20
|
+
],
|
|
21
|
+
"description": "The runtime tag; defaults to universal"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": [
|
|
25
|
+
"name"
|
|
26
|
+
],
|
|
27
|
+
"additionalProperties": false
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"app": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The app directory, relative to the workspace root"
|
|
8
|
+
},
|
|
9
|
+
"path": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^[a-z0-9\\[][a-zA-Z0-9\\-\\[\\]/]*$",
|
|
12
|
+
"description": "The route path under src/app, for example orders or orders/[position]"
|
|
13
|
+
},
|
|
14
|
+
"feature": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-z][a-z0-9-]{1,48}$",
|
|
17
|
+
"description": "The feature whose screen the route renders"
|
|
18
|
+
},
|
|
19
|
+
"screen": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^[A-Z][A-Za-z0-9]{0,79}$",
|
|
22
|
+
"description": "The screen export of the feature; defaults to <Feature>Screen"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"app",
|
|
27
|
+
"path",
|
|
28
|
+
"feature"
|
|
29
|
+
],
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { listSearchParams, type ListState } from '@mpfrontend/ftg-core';
|
|
3
|
+
import { bffOrigin, appId } from '../../config/server';
|
|
4
|
+
import { localeRegistry } from '../../config/app';
|
|
5
|
+
import { __RESOURCE_CONSTANT___LIST, type __Resource__Page } from '../../entities/__resource__';
|
|
6
|
+
import { parseRead } from '__SERVER_TO_OUTPUT__/read-models.gen';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The trusted server boundary of the resource: a list state in, only the parameters that the list protocol declares
|
|
10
|
+
* sent to the BFF, a validated response out.
|
|
11
|
+
*/
|
|
12
|
+
export async function read__Resource__(
|
|
13
|
+
state: ListState,
|
|
14
|
+
language: string = localeRegistry.defaultLocale,
|
|
15
|
+
): Promise<__Resource__Page> {
|
|
16
|
+
const path = '/' + appId + '/__resource__?' + listSearchParams(__RESOURCE_CONSTANT___LIST, state);
|
|
17
|
+
const response = await fetch(bffOrigin + path, {
|
|
18
|
+
cache: 'no-store',
|
|
19
|
+
headers: { 'Accept-Language': language },
|
|
20
|
+
signal: AbortSignal.timeout(10000),
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) throw new Error('__RESOURCE_CONSTANT___UNAVAILABLE');
|
|
23
|
+
return parseRead('__resource__', await response.json());
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { read__Resource__ } from '../../../api/server/__resource__';
|
|
2
|
+
import { localeRegistry } from '../../../config/app';
|
|
3
|
+
import { __RESOURCE_CONSTANT___URL_STATE } from '../../../entities/__resource__';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The app's own read route of the resource: the browser reads the list here, never the backend. The query is read
|
|
7
|
+
* with the list's URL state, so an invalid value is its default and only declared parameters reach the BFF.
|
|
8
|
+
*/
|
|
9
|
+
export async function GET(request: Request) {
|
|
10
|
+
try {
|
|
11
|
+
return Response.json(
|
|
12
|
+
await read__Resource__(
|
|
13
|
+
__RESOURCE_CONSTANT___URL_STATE.read(new URL(request.url).searchParams),
|
|
14
|
+
localeRegistry.negotiate(request.headers.get('Accept-Language')),
|
|
15
|
+
),
|
|
16
|
+
{ headers: { 'Cache-Control': 'no-store' } },
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
return Response.json(
|
|
20
|
+
{ code: '__RESOURCE_CONSTANT___UNAVAILABLE' },
|
|
21
|
+
{ status: 503, headers: { 'Cache-Control': 'no-store' } },
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The public entry of the entity. Features and server adapters import only from here.
|
|
2
|
+
export {
|
|
3
|
+
itemPosition,
|
|
4
|
+
locateItem,
|
|
5
|
+
type __Resource__Item,
|
|
6
|
+
type __Resource__Page,
|
|
7
|
+
} from './model/__resource__';
|
|
8
|
+
export {
|
|
9
|
+
__RESOURCE_CONSTANT___LIST,
|
|
10
|
+
__RESOURCE_CONSTANT___PAGE_SIZE,
|
|
11
|
+
__RESOURCE_CONSTANT___URL_STATE,
|
|
12
|
+
} from './model/__resource__-list';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { checkListProtocol, type OpenApiDocument } from '@mpfrontend/ftg-core';
|
|
5
|
+
import { __RESOURCE_CONSTANT___LIST, __RESOURCE_CONSTANT___URL_STATE } from './__resource__-list';
|
|
6
|
+
|
|
7
|
+
test('the list protocol sends only parameters that the captured contract declares', async () => {
|
|
8
|
+
const app = new URL('../../../../', import.meta.url);
|
|
9
|
+
const config = JSON.parse(await readFile(new URL('ftg.config.json', app), 'utf8')) as { openapi: string };
|
|
10
|
+
const contract = JSON.parse(await readFile(new URL(config.openapi, app), 'utf8')) as OpenApiDocument;
|
|
11
|
+
assert.deepEqual(checkListProtocol(contract, __RESOURCE_CONSTANT___LIST), []);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('the URL state restores a linked list and falls back to its defaults for an invalid value', () => {
|
|
15
|
+
const state = __RESOURCE_CONSTANT___URL_STATE.read('page=3&search=term');
|
|
16
|
+
assert.equal(state.page, 3);
|
|
17
|
+
assert.equal(state.filters?.search, 'term');
|
|
18
|
+
assert.equal(__RESOURCE_CONSTANT___URL_STATE.serialize(state), '?page=3&search=term');
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
__RESOURCE_CONSTANT___URL_STATE.read('page=0&size=1000&search=' + 'x'.repeat(101)),
|
|
21
|
+
__RESOURCE_CONSTANT___URL_STATE.defaults,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ListProtocol } from '@mpfrontend/ftg-core';
|
|
2
|
+
import { listUrlState } from '@mpfrontend/data-react/shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How the list operation of the resource pages and filters, as data: the parameters a request sends and where the
|
|
6
|
+
* answer holds the rows and the total. The test beside it checks it against the captured contract, so a request
|
|
7
|
+
* sends only parameters the operation declares. Add a sort or a filter here only when the contract declares it.
|
|
8
|
+
*/
|
|
9
|
+
export const __RESOURCE_CONSTANT___LIST: ListProtocol = {
|
|
10
|
+
operationId: '__OPERATION_ID__',
|
|
11
|
+
page: { param: 'page', encoding: 'number', first: 1 },
|
|
12
|
+
size: { param: 'size', max: 100 },
|
|
13
|
+
filters: { search: { param: 'search', maxLength: 100 } },
|
|
14
|
+
rows: ['items'],
|
|
15
|
+
total: ['total'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The page size when the URL names none. */
|
|
19
|
+
export const __RESOURCE_CONSTANT___PAGE_SIZE = 12;
|
|
20
|
+
|
|
21
|
+
/** The list state that the URL owns, read the same way on the server and in the browser. It never holds the scope. */
|
|
22
|
+
export const __RESOURCE_CONSTANT___URL_STATE = listUrlState(__RESOURCE_CONSTANT___LIST, {
|
|
23
|
+
size: __RESOURCE_CONSTANT___PAGE_SIZE,
|
|
24
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { itemPosition, locateItem } from './__resource__';
|
|
4
|
+
|
|
5
|
+
test('a list position and its page and index convert both ways', () => {
|
|
6
|
+
for (const [page, index] of [
|
|
7
|
+
[1, 0],
|
|
8
|
+
[1, 11],
|
|
9
|
+
[2, 0],
|
|
10
|
+
[9, 5],
|
|
11
|
+
] as const) {
|
|
12
|
+
assert.deepEqual(locateItem(itemPosition(page, 12, index), 12), { page, index });
|
|
13
|
+
}
|
|
14
|
+
for (const position of [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1]) {
|
|
15
|
+
assert.equal(locateItem(position, 12), undefined);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReadModels } from '__ENTITY_TO_OUTPUT__/read-models.gen';
|
|
2
|
+
|
|
3
|
+
/** One page of the resource, as the generated contract validates it. */
|
|
4
|
+
export type __Resource__Page = ReadModels['__resource__'];
|
|
5
|
+
export type __Resource__Item = __Resource__Page['items'][number];
|
|
6
|
+
|
|
7
|
+
/** An item's position in the list, counted from 1; the identity that a detail route links to. */
|
|
8
|
+
export function itemPosition(page: number, size: number, index: number): number {
|
|
9
|
+
return (page - 1) * size + index + 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The page and index of a list position, or undefined for a position that cannot exist. */
|
|
13
|
+
export function locateItem(
|
|
14
|
+
position: number,
|
|
15
|
+
size: number,
|
|
16
|
+
): { page: number; index: number } | undefined {
|
|
17
|
+
if (!Number.isSafeInteger(position) || position < 1) return undefined;
|
|
18
|
+
return { page: Math.floor((position - 1) / size) + 1, index: (position - 1) % size };
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { __Resource__Page } from '../../../entities/__resource__';
|
|
2
|
+
import { parseRead } from '__FEATURE_TO_OUTPUT__/read-models.gen';
|
|
3
|
+
|
|
4
|
+
/** Reads one page in the browser through the app's own route, never the backend directly, and validates it. */
|
|
5
|
+
export async function read__Feature__Page(search: string, signal: AbortSignal): Promise<__Resource__Page> {
|
|
6
|
+
const response = await fetch('/api/__resource__' + search, {
|
|
7
|
+
signal,
|
|
8
|
+
headers: { Accept: 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) throw new Error('__RESOURCE_CONSTANT___UNAVAILABLE');
|
|
11
|
+
return parseRead('__resource__', await response.json());
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import type { ListState } from '@mpfrontend/ftg-core';
|
|
3
|
+
import { read__Resource__ } from '../../../api/server/__resource__';
|
|
4
|
+
import {
|
|
5
|
+
locateItem,
|
|
6
|
+
__RESOURCE_CONSTANT___PAGE_SIZE,
|
|
7
|
+
type __Resource__Item,
|
|
8
|
+
type __Resource__Page,
|
|
9
|
+
} from '../../../entities/__resource__';
|
|
10
|
+
|
|
11
|
+
/** The first page of the list on the server, for the URL state of the request. */
|
|
12
|
+
export function load__Feature__Page(state: ListState, language: string): Promise<__Resource__Page> {
|
|
13
|
+
return read__Resource__(state, language);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The item at a list position, or undefined when the position is outside the list. */
|
|
17
|
+
export async function load__Feature__Item(
|
|
18
|
+
position: number,
|
|
19
|
+
language: string,
|
|
20
|
+
): Promise<__Resource__Item | undefined> {
|
|
21
|
+
const location = locateItem(position, __RESOURCE_CONSTANT___PAGE_SIZE);
|
|
22
|
+
if (!location) return undefined;
|
|
23
|
+
const page = await read__Resource__(
|
|
24
|
+
{ page: location.page, size: __RESOURCE_CONSTANT___PAGE_SIZE },
|
|
25
|
+
language,
|
|
26
|
+
);
|
|
27
|
+
return page.items[location.index];
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useScopedQuery, useTableUrlState } from '@mpfrontend/data-react';
|
|
3
|
+
import { __RESOURCE_CONSTANT___URL_STATE, type __Resource__Page } from '../../../entities/__resource__';
|
|
4
|
+
import { read__Feature__Page } from '../api/__feature__-read';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The list state of the URL and the page it selects. The page is read under the access scope through the app's own
|
|
8
|
+
* route; the server's first page is used while the URL still asks for it. `null` means the server could not read it.
|
|
9
|
+
*/
|
|
10
|
+
export function use__Feature__List(initial: Readonly<{ search: string; page: __Resource__Page | null }>) {
|
|
11
|
+
const table = useTableUrlState(__RESOURCE_CONSTANT___URL_STATE);
|
|
12
|
+
const search = __RESOURCE_CONSTANT___URL_STATE.serialize(table);
|
|
13
|
+
const first = search === initial.search;
|
|
14
|
+
const query = useScopedQuery({
|
|
15
|
+
operation: '__resource__.list',
|
|
16
|
+
params: search,
|
|
17
|
+
read: read__Feature__Page,
|
|
18
|
+
...(first && initial.page ? { initialData: initial.page } : {}),
|
|
19
|
+
});
|
|
20
|
+
const unavailable = query.isError || (first && initial.page === null && query.data === undefined);
|
|
21
|
+
return { table, page: query.data, unavailable };
|
|
22
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Authored product formatting and behavior belong here; generators never overwrite this file. Visible text
|
|
2
|
+
// comes from the catalogs in ../messages, for example a column label: labels = { name: t('nameColumn') }.
|
|
3
|
+
export const labels: Readonly<Record<string, string>> = {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
import { preferenceCookie, visitorLocale } from '../../../config/app';
|
|
4
|
+
import { load__Feature__Item } from '../api/__feature__';
|
|
5
|
+
import { fields } from '../model/__feature__-fields';
|
|
6
|
+
import type { __Feature__ScreenProps } from './__feature__-screen';
|
|
7
|
+
import { __Feature__Detail } from './__feature__-detail';
|
|
8
|
+
|
|
9
|
+
/** The detail screen: a child route of the list, so an item can be linked and refreshed. */
|
|
10
|
+
export async function __Feature__DetailScreen({ params, basePath }: __Feature__ScreenProps) {
|
|
11
|
+
const position = Number((await params)?.position);
|
|
12
|
+
const language = visitorLocale((await cookies()).get(preferenceCookie.name)?.value);
|
|
13
|
+
const item = await load__Feature__Item(position, language).catch(() => null);
|
|
14
|
+
if (item === undefined) notFound();
|
|
15
|
+
return <__Feature__Detail item={item} fields={fields} language={language} basePath={basePath} />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createElement } from 'react';
|
|
4
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
5
|
+
import { __Feature__Detail } from './__feature__-detail';
|
|
6
|
+
|
|
7
|
+
const fields = [{ key: 'name', type: 'string', required: true, editable: false, nullable: false }];
|
|
8
|
+
|
|
9
|
+
test('the detail view renders the generated fields of one item and a way back to the list', () => {
|
|
10
|
+
const html = renderToStaticMarkup(
|
|
11
|
+
createElement(__Feature__Detail, {
|
|
12
|
+
item: { name: 'Example' } as never,
|
|
13
|
+
fields,
|
|
14
|
+
language: 'en',
|
|
15
|
+
basePath: '/list',
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
assert.match(html, /<dt[^>]*>name<\/dt><dd[^>]*>Example<\/dd>/);
|
|
19
|
+
assert.match(html, /href="\/list"/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('an unreadable item is announced instead of rendering an empty record', () => {
|
|
23
|
+
const html = renderToStaticMarkup(
|
|
24
|
+
createElement(__Feature__Detail, { item: null, fields, language: 'en', basePath: '/list' }),
|
|
25
|
+
);
|
|
26
|
+
assert.match(html, /role="alert"/);
|
|
27
|
+
assert.doesNotMatch(html, /<dl/);
|
|
28
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Fragment } from 'react';
|
|
2
|
+
import { PageFrame } from '@mpfrontend/app-layout';
|
|
3
|
+
import { Card, type DisplayField } from '@mpfrontend/ui';
|
|
4
|
+
import { formatValue } from '@mpfrontend/i18n';
|
|
5
|
+
import { localeRegistry } from '../../../config/app';
|
|
6
|
+
import type { __Resource__Item } from '../../../entities/__resource__';
|
|
7
|
+
import { messages } from '../model/messages';
|
|
8
|
+
import { labels } from '../model/overrides';
|
|
9
|
+
|
|
10
|
+
type Props = Readonly<{
|
|
11
|
+
item: __Resource__Item | null;
|
|
12
|
+
fields: readonly DisplayField[];
|
|
13
|
+
language: string;
|
|
14
|
+
basePath: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** The detail view. `null` means the server boundary could not read the item. */
|
|
18
|
+
export function __Feature__Detail({ item, fields, language, basePath }: Props) {
|
|
19
|
+
const t = messages.translator(language);
|
|
20
|
+
const values = item as Readonly<Record<string, unknown>> | null;
|
|
21
|
+
return (
|
|
22
|
+
<PageFrame title={t('title')} actions={<a href={basePath}>{t('back')}</a>}>
|
|
23
|
+
{values === null ? (
|
|
24
|
+
<p role="alert">{t('unavailable')}</p>
|
|
25
|
+
) : (
|
|
26
|
+
<Card>
|
|
27
|
+
<dl className="grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2">
|
|
28
|
+
{fields.map((field) => (
|
|
29
|
+
<Fragment key={field.key}>
|
|
30
|
+
<dt className="font-medium">{labels[field.key] ?? field.key}</dt>
|
|
31
|
+
<dd className="m-0">
|
|
32
|
+
{formatValue(values[field.key], language, localeRegistry, {
|
|
33
|
+
true: t('yes'),
|
|
34
|
+
false: t('no'),
|
|
35
|
+
})}
|
|
36
|
+
</dd>
|
|
37
|
+
</Fragment>
|
|
38
|
+
))}
|
|
39
|
+
</dl>
|
|
40
|
+
</Card>
|
|
41
|
+
)}
|
|
42
|
+
</PageFrame>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { PageFrame } from '@mpfrontend/app-layout';
|
|
3
|
+
import { Card, ResourceTable, type DisplayField } from '@mpfrontend/ui';
|
|
4
|
+
import { formatValue } from '@mpfrontend/i18n';
|
|
5
|
+
import { localeRegistry } from '../../../config/app';
|
|
6
|
+
import { itemPosition, type __Resource__Page } from '../../../entities/__resource__';
|
|
7
|
+
import { use__Feature__List } from '../hooks/use-__feature__-list';
|
|
8
|
+
import { messages } from '../model/messages';
|
|
9
|
+
import { labels } from '../model/overrides';
|
|
10
|
+
import { detailHref } from '../utils/__feature__-href';
|
|
11
|
+
|
|
12
|
+
type Props = Readonly<{
|
|
13
|
+
initialPage: __Resource__Page | null;
|
|
14
|
+
initialSearch: string;
|
|
15
|
+
fields: readonly DisplayField[];
|
|
16
|
+
language: string;
|
|
17
|
+
basePath: string;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
/** The list view: it renders the page that the URL state selects and emits paging and filter intent. */
|
|
21
|
+
export function __Feature__List({ initialPage, initialSearch, fields, language, basePath }: Props) {
|
|
22
|
+
const t = messages.translator(language);
|
|
23
|
+
const { table, page, unavailable } = use__Feature__List({ search: initialSearch, page: initialPage });
|
|
24
|
+
return (
|
|
25
|
+
<PageFrame title={t('title')}>
|
|
26
|
+
{unavailable ? (
|
|
27
|
+
<p
|
|
28
|
+
role="alert"
|
|
29
|
+
className="border-s-4 border-[color:var(--mp-status-warning)] bg-[color:var(--mp-status-warningsurface)] p-4"
|
|
30
|
+
>
|
|
31
|
+
{t('unavailable')}
|
|
32
|
+
</p>
|
|
33
|
+
) : page === undefined ? (
|
|
34
|
+
<p role="status">{t('loading')}</p>
|
|
35
|
+
) : (
|
|
36
|
+
<Card>
|
|
37
|
+
<ResourceTable
|
|
38
|
+
fields={fields}
|
|
39
|
+
rows={page.items}
|
|
40
|
+
caption={t('title')}
|
|
41
|
+
format={(value) =>
|
|
42
|
+
formatValue(value, language, localeRegistry, { true: t('yes'), false: t('no') })
|
|
43
|
+
}
|
|
44
|
+
labels={{ ...labels, detail: t('detail'), search: t('search') }}
|
|
45
|
+
detail={(row) => (
|
|
46
|
+
<a
|
|
47
|
+
href={detailHref(
|
|
48
|
+
basePath,
|
|
49
|
+
itemPosition(page.number, table.size, page.items.indexOf(row as never)),
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{t('detail')}
|
|
53
|
+
</a>
|
|
54
|
+
)}
|
|
55
|
+
control={{
|
|
56
|
+
mode: 'server',
|
|
57
|
+
...table,
|
|
58
|
+
total: page.total,
|
|
59
|
+
filterable: ['search'],
|
|
60
|
+
text: {
|
|
61
|
+
pagination: t('pagination'),
|
|
62
|
+
previous: t('previous'),
|
|
63
|
+
next: t('next'),
|
|
64
|
+
page: (number, count) => t('pageOf', { page: number, count: count ?? number }),
|
|
65
|
+
apply: t('apply'),
|
|
66
|
+
filters: t('search'),
|
|
67
|
+
},
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
{page.items.length === 0 && <p>{t('empty')}</p>}
|
|
71
|
+
</Card>
|
|
72
|
+
)}
|
|
73
|
+
</PageFrame>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
import { preferenceCookie, visitorLocale } from '../../../config/app';
|
|
3
|
+
import { __RESOURCE_CONSTANT___URL_STATE } from '../../../entities/__resource__';
|
|
4
|
+
import { load__Feature__Page } from '../api/__feature__';
|
|
5
|
+
import { fields } from '../model/__feature__-fields';
|
|
6
|
+
import { __Feature__List } from './__feature__-list';
|
|
7
|
+
|
|
8
|
+
export type __Feature__ScreenProps = Readonly<{
|
|
9
|
+
params?: Promise<Readonly<Record<string, string>>>;
|
|
10
|
+
searchParams?: Promise<Readonly<Record<string, string | string[] | undefined>>>;
|
|
11
|
+
basePath: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The list screen. The server reads the URL state the way the browser does and renders the first page; the browser
|
|
16
|
+
* then reads each further page through the app's own route.
|
|
17
|
+
*/
|
|
18
|
+
export async function __Feature__Screen({ searchParams, basePath }: __Feature__ScreenProps) {
|
|
19
|
+
const state = __RESOURCE_CONSTANT___URL_STATE.read((await searchParams) ?? {});
|
|
20
|
+
const language = visitorLocale((await cookies()).get(preferenceCookie.name)?.value);
|
|
21
|
+
const page = await load__Feature__Page(state, language).catch(() => null);
|
|
22
|
+
return (
|
|
23
|
+
<__Feature__List
|
|
24
|
+
initialPage={page}
|
|
25
|
+
initialSearch={__RESOURCE_CONSTANT___URL_STATE.serialize(state)}
|
|
26
|
+
fields={fields}
|
|
27
|
+
language={language}
|
|
28
|
+
basePath={basePath}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|