Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00934a7c0c | ||
|
|
cd6ef1c120 |
No files matched your search
@@ -118,3 +118,20 @@ Consequences: Every application of the store works on the same rows and refuses
|
|||||||
conflicts, and changes of the tables reach them with a release of the toolkit. The objects' keys
|
conflicts, and changes of the tables reach them with a release of the toolkit. The objects' keys
|
||||||
are in `jsonb`'s order. The store is tested against a real PostgreSQL server, which the release
|
are in `jsonb`'s order. The store is tested against a real PostgreSQL server, which the release
|
||||||
workflow does not have, so `npm test` runs on the developer's machine.
|
workflow does not have, so `npm test` runs on the developer's machine.
|
||||||
|
|
||||||
|
## ADR-6: A fourth entry point, /shared, exports the shared types and constants
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context: An application keeps the types of its API in one file that its server and its web
|
||||||
|
application both import. When those types name types of the toolkit, such as `TfModule`, the file
|
||||||
|
must import them from an entry point both sides can resolve: `/server` brings Fastify and `pg`
|
||||||
|
into the web application's type check, `/web` brings React and MUI into the server's.
|
||||||
|
|
||||||
|
Decision: `shared/index.ts` is the entry point `/shared`, built and exported like the other three.
|
||||||
|
It exports the types and constants of `shared/`, the credential types and the types and name
|
||||||
|
patterns of the tfvars store, and no code that needs a peer dependency. `/server` and `/web` keep
|
||||||
|
exporting them too.
|
||||||
|
|
||||||
|
Consequences: An application's shared API types import from `/shared` without the code of either
|
||||||
|
side. A type added to `shared/` is exported from `/shared` as well as from the side that uses it.
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
feature: RowDialog validates a row as part of a larger value, such as a whole document with the row put in.
|
||||||
|
|
||||||
|
- `RowDialog` takes `body`, the value posted to `validatePath` for the row, and `select`, the
|
||||||
|
row's problems by field and message taken from the answer.
|
||||||
|
- `useValidation` and `RowDialog` take an answer of any shape.
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
feature: the entry point /shared exports the types and constants a server and its web application share.
|
||||||
|
|
||||||
|
- `@koszewscy/sk-react-toolkit/shared` exports the credential types, `TfModule`, `TfInstance`,
|
||||||
|
`TfvarsFileStatus` and the name patterns of the tfvars store, for the API types of an
|
||||||
|
application imported by both its server and its web application.
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
feature: the tfvars store keeps Terraform modules, their instances and the instances' variable files in PostgreSQL.
|
feature: the tfvars store keeps Terraform modules, their instances and the instances' variable files in PostgreSQL.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ source: Node 26 runs the server helpers, and the application's Vite build compil
|
|||||||
|
|
||||||
The package is `@koszewscy/sk-react-toolkit`, published to the npm registry of Gitea,
|
The package is `@koszewscy/sk-react-toolkit`, published to the npm registry of Gitea,
|
||||||
`https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/`. An application depends on a
|
`https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/`. An application depends on a
|
||||||
published version and imports `@koszewscy/sk-react-toolkit/server`, `/web` and `/vite`. Its
|
published version and imports `@koszewscy/sk-react-toolkit/server`, `/shared`, `/web` and `/vite`. Its
|
||||||
`.npmrc` names the registry of the scope:
|
`.npmrc` names the registry of the scope:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
@@ -66,6 +66,13 @@ code; `vite` for the Vite helpers.
|
|||||||
| `CredentialStore`, `credentialProblems`, `credentialValues`, `registerCredentialRoutes`, `POSTGRESQL_KIND` | the credential store; see [Credential store](#credential-store) |
|
| `CredentialStore`, `credentialProblems`, `credentialValues`, `registerCredentialRoutes`, `POSTGRESQL_KIND` | the credential store; see [Credential store](#credential-store) |
|
||||||
| `createTfvarsStore`, `versionOf`, `tfvarsText`, `TfModule`, `TfInstance`, `TfvarsFileStatus`, `TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN`, `STATE_SCHEMA_PATTERN` | the Terraform modules, their instances and their variable files in PostgreSQL; see [Tfvars store](#tfvars-store) |
|
| `createTfvarsStore`, `versionOf`, `tfvarsText`, `TfModule`, `TfInstance`, `TfvarsFileStatus`, `TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN`, `STATE_SCHEMA_PATTERN` | the Terraform modules, their instances and their variable files in PostgreSQL; see [Tfvars store](#tfvars-store) |
|
||||||
|
|
||||||
|
## sk-react-toolkit/shared
|
||||||
|
|
||||||
|
The types and constants of `/server` and `/web` that a server and its web application share, for
|
||||||
|
API types imported by both, without the code of either: `CredentialField`, `CredentialKind`,
|
||||||
|
`CredentialProfile`, `CredentialsDocument`, `TfModule`, `TfInstance`, `TfvarsFileStatus`,
|
||||||
|
`TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN` and `STATE_SCHEMA_PATTERN`.
|
||||||
|
|
||||||
## sk-react-toolkit/web
|
## sk-react-toolkit/web
|
||||||
|
|
||||||
| Export | Purpose |
|
| Export | Purpose |
|
||||||
@@ -77,7 +84,7 @@ code; `vite` for the Vite helpers.
|
|||||||
| `ThemeMenu` | the button cycling light, dark and system theme |
|
| `ThemeMenu` | the button cycling light, dark and system theme |
|
||||||
| `SwitcherMenu`, `SwitcherItem` | a button naming the item chosen, whose menu chooses another, such as a page or a connection |
|
| `SwitcherMenu`, `SwitcherItem` | a button naming the item chosen, whose menu chooses another, such as a page or a connection |
|
||||||
| `TextInput` | a text field with clear and show/hide buttons |
|
| `TextInput` | a text field with clear and show/hide buttons |
|
||||||
| `RowDialog`, `useValidation` | a dialog that edits a value validated by the server as it changes, and the validation hook |
|
| `RowDialog`, `useValidation` | a dialog that edits a value validated by the server as it changes, and the validation hook; with `body` and `select` the dialog posts another value, such as a whole document with the row put in, and takes the row's problems from the answer |
|
||||||
| `createBrowserSettings(keys, event)` | switches kept in localStorage and followed across tabs |
|
| `createBrowserSettings(keys, event)` | switches kept in localStorage and followed across tabs |
|
||||||
| `CredentialsButton` | the dialog of a credential store; see [Credential store](#credential-store) |
|
| `CredentialsButton` | the dialog of a credential store; see [Credential store](#credential-store) |
|
||||||
| `TfModule`, `TfInstance`, `TfvarsFileStatus`, `TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN`, `STATE_SCHEMA_PATTERN` | the types and name patterns of the [Tfvars store](#tfvars-store) |
|
| `TfModule`, `TfInstance`, `TfvarsFileStatus`, `TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN`, `STATE_SCHEMA_PATTERN` | the types and name patterns of the [Tfvars store](#tfvars-store) |
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|||||||
|
|
||||||
const ROOT = import.meta.dirname;
|
const ROOT = import.meta.dirname;
|
||||||
const DIST = join(ROOT, 'dist');
|
const DIST = join(ROOT, 'dist');
|
||||||
const ENTRIES = ['server', 'web', 'vite'];
|
const ENTRIES = ['server', 'shared', 'web', 'vite'];
|
||||||
|
|
||||||
function fail(message) {
|
function fail(message) {
|
||||||
console.error(`build: ${message}`);
|
console.error(`build: ${message}`);
|
||||||
@@ -23,7 +23,7 @@ if (tsc.error) fail(`tsc: ${tsc.error.message}; run npm ci first`);
|
|||||||
if (tsc.status !== 0) process.exit(tsc.status ?? 1);
|
if (tsc.status !== 0) process.exit(tsc.status ?? 1);
|
||||||
|
|
||||||
// The declarations import the other declarations by their .js names, as the compiled JavaScript does.
|
// The declarations import the other declarations by their .js names, as the compiled JavaScript does.
|
||||||
for (const entry of [...ENTRIES, 'shared']) {
|
for (const entry of ENTRIES) {
|
||||||
for (const file of readdirSync(join(DIST, entry)).filter((name) => name.endsWith('.d.ts'))) {
|
for (const file of readdirSync(join(DIST, entry)).filter((name) => name.endsWith('.d.ts'))) {
|
||||||
const path = join(DIST, entry, file);
|
const path = join(DIST, entry, file);
|
||||||
writeFileSync(path, readFileSync(path, 'utf8').replace(/(from\s+|import\s+)(['"])(\.{1,2}\/[^'"]+)\.tsx?\2/g, '$1$2$3.js$2'));
|
writeFileSync(path, readFileSync(path, 'utf8').replace(/(from\s+|import\s+)(['"])(\.{1,2}\/[^'"]+)\.tsx?\2/g, '$1$2$3.js$2'));
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@koszewscy/sk-react-toolkit",
|
"name": "@koszewscy/sk-react-toolkit",
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@koszewscy/sk-react-toolkit",
|
"name": "@koszewscy/sk-react-toolkit",
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@koszewscy/sk-react-toolkit",
|
"name": "@koszewscy/sk-react-toolkit",
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.",
|
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./server": "./server/index.ts",
|
"./server": "./server/index.ts",
|
||||||
|
"./shared": "./shared/index.ts",
|
||||||
"./web": "./web/index.ts",
|
"./web": "./web/index.ts",
|
||||||
"./vite": "./vite/index.ts"
|
"./vite": "./vite/index.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type { CredentialField, CredentialKind, CredentialProfile, CredentialsDocument } from './credentials.ts'
|
||||||
|
export type { TfInstance, TfModule, TfvarsFileStatus } from './tfvars.ts'
|
||||||
|
export { STATE_SCHEMA_PATTERN, TFVARS_FILE_PATTERN, TF_NAME_PATTERN } from './tfvars.ts'
|
||||||
+10
-5
@@ -6,12 +6,16 @@ import { useValidation } from './useValidation.ts'
|
|||||||
/**
|
/**
|
||||||
* A dialog that edits one row. The server validates the value as it changes (validatePath); onSave writes it and
|
* A dialog that edits one row. The server validates the value as it changes (validatePath); onSave writes it and
|
||||||
* the dialog closes when it succeeds. fields renders the inputs from the value, a setter and the problems by field;
|
* the dialog closes when it succeeds. fields renders the inputs from the value, a setter and the problems by field;
|
||||||
* below shows extra information from the validation, such as subnet placements.
|
* below shows extra information from the validation, such as subnet placements. body gives the value posted to
|
||||||
|
* validatePath, such as a whole document with the row put in, and select the problems by field and the message of
|
||||||
|
* the row taken from the answer; without them the row itself is posted and the answer is its validation.
|
||||||
*/
|
*/
|
||||||
export default function RowDialog<T, V extends Validation = Validation>(props: {
|
export default function RowDialog<T, V = Validation>(props: {
|
||||||
title: string
|
title: string
|
||||||
initial: T
|
initial: T
|
||||||
validatePath: string
|
validatePath: string
|
||||||
|
body?: (value: T) => unknown
|
||||||
|
select?: (result: V) => Validation
|
||||||
saveLabel: string
|
saveLabel: string
|
||||||
onSave: (value: T) => Promise<void>
|
onSave: (value: T) => Promise<void>
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
@@ -21,9 +25,10 @@ export default function RowDialog<T, V extends Validation = Validation>(props: {
|
|||||||
const [value, setValue] = useState<T>(props.initial)
|
const [value, setValue] = useState<T>(props.initial)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const validation = useValidation<V>(props.validatePath, value)
|
const validation = useValidation<V>(props.validatePath, props.body ? props.body(value) : value)
|
||||||
const problems = validation.result?.problems ?? {}
|
const selected = validation.result === null ? null : props.select ? props.select(validation.result) : validation.result as Validation
|
||||||
const message = validation.result?.message ?? ''
|
const problems = selected?.problems ?? {}
|
||||||
|
const message = selected?.message ?? ''
|
||||||
const blocked = validation.pending || Object.keys(problems).length > 0 || message !== ''
|
const blocked = validation.pending || Object.keys(problems).length > 0 || message !== ''
|
||||||
|
|
||||||
async function submit(event: FormEvent) {
|
async function submit(event: FormEvent) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { request, type Validation } from './api.ts'
|
|||||||
* Asks the server to validate value at path (a .../validate endpoint) 250 ms after it last
|
* Asks the server to validate value at path (a .../validate endpoint) 250 ms after it last
|
||||||
* changed. pending is true until the answer for the current value arrives; until then the previous result stays.
|
* changed. pending is true until the answer for the current value arrives; until then the previous result stays.
|
||||||
*/
|
*/
|
||||||
export function useValidation<T extends Validation>(path: string | null, value: unknown): { result: T | null; error: string; pending: boolean } {
|
export function useValidation<T = Validation>(path: string | null, value: unknown): { result: T | null; error: string; pending: boolean } {
|
||||||
const key = path === null ? '' : `${path}\n${JSON.stringify(value)}`
|
const key = path === null ? '' : `${path}\n${JSON.stringify(value)}`
|
||||||
const [answer, setAnswer] = useState<{ key: string; result: T | null; error: string } | null>(null)
|
const [answer, setAnswer] = useState<{ key: string; result: T | null; error: string } | null>(null)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user