nucleus-core-ts 0.10.147 → 0.10.151
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 +48 -48
- package/dist/.build-ok +1 -1
- package/dist/fe/components/AuthorizationPage/labels.js +13 -13
- package/dist/fe/components/Captcha/labels.js +17 -17
- package/dist/fe/components/ChatPanel/helpers.js +17 -17
- package/dist/fe/components/ChatPanel/labels.js +16 -16
- package/dist/fe/components/DataTable/labels.js +16 -16
- package/dist/fe/components/DevicesPage/components/DeviceCard.js +15 -15
- package/dist/fe/components/DevicesPage/components/DevicesPage.js +9 -9
- package/dist/fe/components/DevicesPage/labels.js +12 -12
- package/dist/fe/components/ForgotPasswordPage/labels.js +8 -8
- package/dist/fe/components/FormBuilder/components/FormField.js +5 -5
- package/dist/fe/components/FormBuilder/utils/fieldMapping.js +9 -9
- package/dist/fe/components/IntegrationsPage/components/ConnectionAuthFields.js +8 -8
- package/dist/fe/components/IntegrationsPage/components/ConnectionBasicFields.js +3 -3
- package/dist/fe/components/IntegrationsPage/components/ConnectionExport.js +23 -23
- package/dist/fe/components/IntegrationsPage/components/ConnectionForm.js +22 -22
- package/dist/fe/components/IntegrationsPage/components/ConnectionList.js +7 -7
- package/dist/fe/components/IntegrationsPage/components/ConnectionOauthFields.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/ConnectionSettingsTab.js +24 -24
- package/dist/fe/components/IntegrationsPage/components/ConnectionSwitcher.js +19 -19
- package/dist/fe/components/IntegrationsPage/components/ConnectionTabs.js +15 -15
- package/dist/fe/components/IntegrationsPage/components/ConnectionTest.js +21 -21
- package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.js +18 -18
- package/dist/fe/components/IntegrationsPage/components/CredentialFields.js +19 -19
- package/dist/fe/components/IntegrationsPage/components/CredentialSlotRow.js +8 -8
- package/dist/fe/components/IntegrationsPage/components/EndpointForm.js +7 -7
- package/dist/fe/components/IntegrationsPage/components/EndpointFormFields.js +12 -12
- package/dist/fe/components/IntegrationsPage/components/EndpointList.js +7 -7
- package/dist/fe/components/IntegrationsPage/components/EndpointSample.js +36 -36
- package/dist/fe/components/IntegrationsPage/components/EndpointsTab.js +29 -29
- package/dist/fe/components/IntegrationsPage/components/FieldMappingRow.js +7 -7
- package/dist/fe/components/IntegrationsPage/components/FieldMappingRows.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.js +60 -60
- package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.js +15 -15
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +9 -9
- package/dist/fe/components/IntegrationsPage/components/MappingList.js +23 -23
- package/dist/fe/components/IntegrationsPage/components/MappingPasswordField.js +12 -12
- package/dist/fe/components/IntegrationsPage/components/MappingScheduleFields.js +23 -23
- package/dist/fe/components/IntegrationsPage/components/MappingWriteModeFields.js +31 -31
- package/dist/fe/components/IntegrationsPage/components/MappingsTab.js +56 -56
- package/dist/fe/components/IntegrationsPage/components/PaginationFields.js +11 -11
- package/dist/fe/components/IntegrationsPage/components/ParamBindingRows.js +15 -15
- package/dist/fe/components/IntegrationsPage/components/Primitives.js +28 -28
- package/dist/fe/components/IntegrationsPage/components/ReferenceCheckRows.js +9 -9
- package/dist/fe/components/IntegrationsPage/components/RunConfirmStep.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/RunHistory.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/RunHistoryRow.js +9 -9
- package/dist/fe/components/IntegrationsPage/components/RunPanel.js +40 -40
- package/dist/fe/components/IntegrationsPage/components/RunPasswordField.js +18 -18
- package/dist/fe/components/IntegrationsPage/components/RunPlanIssues.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/RunPlanReferences.js +12 -12
- package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.js +11 -11
- package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/RunProgressBar.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/RunRevertConflicts.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/Skeletons.js +7 -7
- package/dist/fe/components/IntegrationsPage/components/authOptions.js +4 -4
- package/dist/fe/components/IntegrationsPage/components/connectionDraft.js +27 -27
- package/dist/fe/components/IntegrationsPage/components/credentialSlots.js +25 -25
- package/dist/fe/components/IntegrationsPage/components/deleteIsConfirmed.js +9 -9
- package/dist/fe/components/IntegrationsPage/components/fieldMappingOptions.js +4 -4
- package/dist/fe/components/IntegrationsPage/components/formSupport.js +34 -34
- package/dist/fe/components/IntegrationsPage/components/jsonArray.js +14 -14
- package/dist/fe/components/IntegrationsPage/components/jsonObjectField.js +15 -15
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +10 -10
- package/dist/fe/components/IntegrationsPage/components/mappingStaleness.js +22 -22
- package/dist/fe/components/IntegrationsPage/components/nextRuns.js +8 -8
- package/dist/fe/components/IntegrationsPage/components/passwordIsNeeded.js +13 -13
- package/dist/fe/components/IntegrationsPage/components/pathParams.js +6 -6
- package/dist/fe/components/IntegrationsPage/components/runFormat.js +18 -18
- package/dist/fe/components/IntegrationsPage/components/settleOwnership.js +18 -18
- package/dist/fe/components/IntegrationsPage/components/useInfiniteList.js +8 -8
- package/dist/fe/components/IntegrationsPage/store/index.js +20 -20
- package/dist/fe/components/IntegrationsPage/store/upsertInto.js +12 -12
- package/dist/fe/components/IntegrationsPage/text/context.js +6 -6
- package/dist/fe/components/IntegrationsPage/text/index.js +18 -18
- package/dist/fe/components/IntegrationsPage/theme/index.js +29 -29
- package/dist/fe/components/IntegrationsPage/types/records.js +10 -10
- package/dist/fe/components/IntegrationsPage/types/results.js +3 -3
- package/dist/fe/components/NotificationCenter/labels.js +17 -17
- package/dist/fe/components/ProfilePage/labels.js +10 -10
- package/dist/fe/components/ResetPasswordPage/labels.js +4 -4
- package/dist/fe/components/SetPasswordPage/labels.js +11 -11
- package/dist/fe/components/UsersPage/labels.js +14 -14
- package/dist/fe/utils/columnUtils.js +24 -24
- package/dist/fe/utils/searchFold.js +36 -36
- package/dist/index.js +6 -6
- package/dist/server.js +6 -6
- package/dist/src/Client/Proxy/authz.js +4 -4
- package/dist/src/Client/Proxy/httpProxy.js +136 -136
- package/dist/src/Client/Proxy/largeBody.js +29 -29
- package/dist/src/Client/Proxy/utils.js +32 -32
- package/dist/src/Client/Proxy/wsProxy.js +59 -59
- package/dist/src/Managers/Redis/index.d.ts +14 -0
- package/dist/src/Nucleus/routes/auth/admin/impersonationStore.d.ts +3 -4
- package/dist/src/Nucleus/routes/auth/refresh/issuedPair.d.ts +67 -0
- package/dist/src/Nucleus/routes/auth/refresh/recordIssued.d.ts +15 -6
- package/dist/src/Nucleus/routes/auth/refresh/refreshChain.d.ts +56 -0
- package/dist/src/Services/Auth/SessionStore/utils.d.ts +2 -0
- package/dist/src/Shared/cron.js +71 -71
- package/dist/src/Shared/redactSecrets.js +156 -156
- package/dist/src/Shared/redaction/bare.js +61 -61
- package/dist/src/Shared/redaction/floor.js +48 -48
- package/dist/src/Shared/redaction/freeText.js +70 -70
- package/dist/src/Shared/redaction/json.js +30 -30
- package/dist/src/Shared/redaction/lists.js +23 -23
- package/dist/src/Shared/redaction/markers.js +28 -28
- package/dist/src/Shared/redaction/names.js +49 -49
- package/dist/src/Shared/redaction/pairs.js +54 -54
- package/dist/src/Shared/redaction/secretString.js +29 -29
- package/dist/src/Shared/redaction/structured.js +52 -52
- package/dist/src/Shared/redaction/walk.js +25 -25
- package/infra/scripts/generate-project.ts +766 -766
- package/infra/templates/README.md +57 -57
- package/infra/templates/backend/.dockerignore +9 -9
- package/infra/templates/backend/.env.example +15 -15
- package/infra/templates/backend/README.md +28 -28
- package/infra/templates/backend/azure-pipelines.yml +135 -135
- package/infra/templates/backend/k8s/Dockerfile +31 -31
- package/infra/templates/backend/k8s/configmap.yaml +26 -26
- package/infra/templates/backend/k8s/deployment.yaml +85 -85
- package/infra/templates/backend/k8s/pvc.yaml +12 -12
- package/infra/templates/backend/k8s/service.yaml +13 -13
- package/infra/templates/backend/tsconfig.json +22 -22
- package/infra/templates/frontend/.dockerignore +10 -10
- package/infra/templates/frontend/.env.example +8 -8
- package/infra/templates/frontend/.env.local.example +10 -10
- package/infra/templates/frontend/README.md +69 -69
- package/infra/templates/frontend/app/[tablename]/page.tsx +37 -37
- package/infra/templates/frontend/app/_components/AuthWrapper/index.tsx +63 -63
- package/infra/templates/frontend/app/authorization/page.tsx +43 -43
- package/infra/templates/frontend/app/change-password/page.tsx +23 -23
- package/infra/templates/frontend/app/devices/page.tsx +127 -127
- package/infra/templates/frontend/app/forgot-password/page.tsx +20 -20
- package/infra/templates/frontend/app/globals.css +28 -28
- package/infra/templates/frontend/app/layout.tsx +35 -35
- package/infra/templates/frontend/app/login/page.tsx +33 -33
- package/infra/templates/frontend/app/page.tsx +565 -565
- package/infra/templates/frontend/app/profile/page.tsx +92 -92
- package/infra/templates/frontend/app/register/page.tsx +27 -27
- package/infra/templates/frontend/app/reset-password/page.tsx +60 -60
- package/infra/templates/frontend/app/set-password/page.tsx +43 -43
- package/infra/templates/frontend/app/ui-docs/page.tsx +52 -52
- package/infra/templates/frontend/app/users/page.tsx +52 -52
- package/infra/templates/frontend/app/verify-email/page.tsx +41 -41
- package/infra/templates/frontend/app/verify-magic-link/page.tsx +46 -46
- package/infra/templates/frontend/azure-pipelines.yml +137 -137
- package/infra/templates/frontend/code-and-design-principles.md +438 -438
- package/infra/templates/frontend/k8s/Dockerfile +50 -50
- package/infra/templates/frontend/k8s/configmap.yaml +18 -18
- package/infra/templates/frontend/k8s/deployment.yaml +72 -72
- package/infra/templates/frontend/k8s/ingress.yaml +36 -36
- package/infra/templates/frontend/k8s/service.yaml +14 -14
- package/infra/templates/frontend/lib/api/endpoints.ts +37 -37
- package/infra/templates/frontend/lib/api/factory.ts +44 -44
- package/infra/templates/frontend/lib/api/hook.ts +10 -10
- package/infra/templates/frontend/next.config.ts +19 -19
- package/infra/templates/frontend/package.json +34 -34
- package/infra/templates/frontend/postcss.config.mjs +5 -5
- package/infra/templates/frontend/server.ts +114 -114
- package/infra/templates/frontend/tsconfig.json +23 -23
- package/package.json +1 -1
- package/public/components/CTA.tsx +25 -25
- package/public/components/Capabilities.tsx +67 -67
- package/public/components/Features.tsx +107 -107
- package/public/components/Footer.tsx +58 -58
- package/public/components/Hero.tsx +51 -51
- package/public/components/Managers.tsx +55 -55
- package/public/components/Nav.tsx +91 -91
- package/public/components/Stack.tsx +36 -36
- package/public/components/docs/Docs.tsx +60 -60
- package/public/components/docs/DocsContent.tsx +176 -176
- package/public/components/docs/DocsRightSidebar.tsx +53 -53
- package/public/components/docs/DocsSidebar.tsx +54 -54
- package/public/components/docs/content/authentication.ts +836 -836
- package/public/components/docs/content/authorization.ts +268 -268
- package/public/components/docs/content/backupRestore.ts +173 -173
- package/public/components/docs/content/clientModule.ts +287 -287
- package/public/components/docs/content/entityCrud.ts +682 -682
- package/public/components/docs/content/monitoring.ts +313 -313
- package/public/components/docs/content/multiTenant.ts +269 -269
- package/public/components/docs/content/oauthApiKeys.ts +325 -325
- package/public/components/docs/content/pubsub.ts +131 -131
- package/public/components/docs/content/storageCdn.ts +171 -171
- package/public/components/docs/content/verification.ts +289 -289
- package/public/components/docs/docsData.ts +113 -113
- package/public/components/docs/index.ts +6 -6
- package/public/components/index.ts +9 -9
- package/public/docs.css +629 -629
- package/public/index.html +81 -81
- package/public/index.tsx +42 -42
- package/public/store.ts +76 -76
- package/public/styles.css +858 -858
- package/schemas/config.nucleus.json +2651 -2651
- package/scripts/audit-purge-noise.ts +120 -120
- package/scripts/build.ts +582 -582
- package/scripts/generate-system-tables-types.ts +18 -18
- package/scripts/generate-types-schema.test.ts +73 -73
- package/scripts/generate-types-schema.ts +1284 -1284
- package/scripts/packagedScripts.test.ts +87 -87
- package/scripts/publish.ts +276 -276
- package/scripts/skillCitations.test.ts +107 -107
- package/scripts/verify-publishable.ts +40 -40
|
@@ -45,19 +45,19 @@ export function draftFromSource(source) {
|
|
|
45
45
|
timeoutMs: source.timeoutMs == null ? '' : String(source.timeoutMs)
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* The boxes a SAVED record should correct, and only those.
|
|
50
|
-
*
|
|
51
|
-
* After an edit the form has to show what was actually stored — the server
|
|
52
|
-
* trims, defaults and ignores things, and the boxes went on showing what was
|
|
53
|
-
* typed. But re-deriving the whole draft from the reply is not safe: a
|
|
54
|
-
* field-scoped patch claim strips columns out of the response, and
|
|
55
|
-
* {@link draftFromSource} substitutes a default for every column it does not
|
|
56
|
-
* find. A reply that legitimately omits `name` would therefore blank the name
|
|
57
|
-
* box — trading a stale screen for a destroyed one.
|
|
58
|
-
*
|
|
59
|
-
* So this reads the reply key by key and says nothing about a key that is not
|
|
60
|
-
* there. What it returns is a patch, applied over the draft the operator has.
|
|
48
|
+
/**
|
|
49
|
+
* The boxes a SAVED record should correct, and only those.
|
|
50
|
+
*
|
|
51
|
+
* After an edit the form has to show what was actually stored — the server
|
|
52
|
+
* trims, defaults and ignores things, and the boxes went on showing what was
|
|
53
|
+
* typed. But re-deriving the whole draft from the reply is not safe: a
|
|
54
|
+
* field-scoped patch claim strips columns out of the response, and
|
|
55
|
+
* {@link draftFromSource} substitutes a default for every column it does not
|
|
56
|
+
* find. A reply that legitimately omits `name` would therefore blank the name
|
|
57
|
+
* box — trading a stale screen for a destroyed one.
|
|
58
|
+
*
|
|
59
|
+
* So this reads the reply key by key and says nothing about a key that is not
|
|
60
|
+
* there. What it returns is a patch, applied over the draft the operator has.
|
|
61
61
|
*/ export function draftPatchFromSource(source) {
|
|
62
62
|
const full = draftFromSource(source);
|
|
63
63
|
const patch = {};
|
|
@@ -96,13 +96,13 @@ export function usesQueryParam(authType) {
|
|
|
96
96
|
export function usesOauth(authType) {
|
|
97
97
|
return authType === 'oauth2';
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Rebuilds the record.
|
|
101
|
-
*
|
|
102
|
-
* Fields the chosen auth type does not use are sent as null rather than left
|
|
103
|
-
* alone. Switching a connection from OAuth to a bearer token and leaving the old
|
|
104
|
-
* token URL behind is how a source ends up carrying settings nobody can see in
|
|
105
|
-
* the form but the server still reads.
|
|
99
|
+
/**
|
|
100
|
+
* Rebuilds the record.
|
|
101
|
+
*
|
|
102
|
+
* Fields the chosen auth type does not use are sent as null rather than left
|
|
103
|
+
* alone. Switching a connection from OAuth to a bearer token and leaving the old
|
|
104
|
+
* token URL behind is how a source ends up carrying settings nobody can see in
|
|
105
|
+
* the form but the server still reads.
|
|
106
106
|
*/ export function draftToSource(draft, id) {
|
|
107
107
|
const oauth = usesOauth(draft.authType);
|
|
108
108
|
const timeout = Number.parseInt(draft.timeoutMs.trim(), 10);
|
|
@@ -139,13 +139,13 @@ export function usesOauth(authType) {
|
|
|
139
139
|
}
|
|
140
140
|
const SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
141
141
|
const DIGITS_PATTERN = /^\d+$/;
|
|
142
|
-
/**
|
|
143
|
-
* What must be filled in before a save is worth attempting.
|
|
144
|
-
*
|
|
145
|
-
* The base URL is checked for a scheme and nothing else. It is deliberately not
|
|
146
|
-
* checked for https, for a public host or for a resolvable name: an in-cluster
|
|
147
|
-
* service on plain http is a normal target, and a form that rejected it would be
|
|
148
|
-
* rejecting a working setup.
|
|
142
|
+
/**
|
|
143
|
+
* What must be filled in before a save is worth attempting.
|
|
144
|
+
*
|
|
145
|
+
* The base URL is checked for a scheme and nothing else. It is deliberately not
|
|
146
|
+
* checked for https, for a public host or for a resolvable name: an in-cluster
|
|
147
|
+
* service on plain http is a normal target, and a form that rejected it would be
|
|
148
|
+
* rejecting a working setup.
|
|
149
149
|
*/ export function validateConnectionDraft(draft) {
|
|
150
150
|
const errors = {};
|
|
151
151
|
if (draft.name.trim().length === 0) errors.name = 'Give the connection a name.';
|
|
@@ -14,24 +14,24 @@
|
|
|
14
14
|
if (slot === 'oauthClientSecret') return source.oauthClientSecretHint ?? null;
|
|
15
15
|
return source.oauthPasswordHint ?? null;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Every name a server might return a preview under.
|
|
19
|
-
*
|
|
20
|
-
* This list was wrong, and the way it was wrong is worth keeping: it accepted
|
|
21
|
-
* the slot name and the hint column, on the reasoning that a server would use
|
|
22
|
-
* one or the other. Nucleus' own route uses NEITHER for the OAuth pair — it
|
|
23
|
-
* answers `clientSecret` and `password`. So a saved password came back under a
|
|
24
|
-
* name nothing looked for, {@link hintFromResult} said "the reply mentioned no
|
|
25
|
-
* such slot", and the screen went on showing the record it had loaded BEFORE
|
|
26
|
-
* the save: "Not set", under the words "Saved". Refreshing fixed it, which is
|
|
27
|
-
* the tell — the value was in the database the whole time.
|
|
28
|
-
*
|
|
29
|
-
* `token` was unaffected, because for that one slot the guess happened to
|
|
30
|
-
* match. One correct name out of three is what let this ship.
|
|
31
|
-
*
|
|
32
|
-
* The guard against it happening again is not a longer list: it is
|
|
33
|
-
* `credentialSlots.contract.test.ts`, which reads the server route and fails if
|
|
34
|
-
* it ever emits a name this does not accept.
|
|
17
|
+
/**
|
|
18
|
+
* Every name a server might return a preview under.
|
|
19
|
+
*
|
|
20
|
+
* This list was wrong, and the way it was wrong is worth keeping: it accepted
|
|
21
|
+
* the slot name and the hint column, on the reasoning that a server would use
|
|
22
|
+
* one or the other. Nucleus' own route uses NEITHER for the OAuth pair — it
|
|
23
|
+
* answers `clientSecret` and `password`. So a saved password came back under a
|
|
24
|
+
* name nothing looked for, {@link hintFromResult} said "the reply mentioned no
|
|
25
|
+
* such slot", and the screen went on showing the record it had loaded BEFORE
|
|
26
|
+
* the save: "Not set", under the words "Saved". Refreshing fixed it, which is
|
|
27
|
+
* the tell — the value was in the database the whole time.
|
|
28
|
+
*
|
|
29
|
+
* `token` was unaffected, because for that one slot the guess happened to
|
|
30
|
+
* match. One correct name out of three is what let this ship.
|
|
31
|
+
*
|
|
32
|
+
* The guard against it happening again is not a longer list: it is
|
|
33
|
+
* `credentialSlots.contract.test.ts`, which reads the server route and fails if
|
|
34
|
+
* it ever emits a name this does not accept.
|
|
35
35
|
*/ const HINT_KEYS = {
|
|
36
36
|
token: [
|
|
37
37
|
'token',
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
'oauthPasswordHint'
|
|
50
50
|
]
|
|
51
51
|
};
|
|
52
|
-
/**
|
|
53
|
-
* The record columns a set of new masks writes to.
|
|
54
|
-
*
|
|
55
|
-
* The counterpart of {@link storedHint}: that reads a hint off the record, this
|
|
56
|
-
* puts one back. Both exist because the screen answers from local state OR from
|
|
57
|
-
* the record, and if only one of the two is updated after a save the answer
|
|
58
|
-
* depends on whether the component happens to have been remounted since.
|
|
52
|
+
/**
|
|
53
|
+
* The record columns a set of new masks writes to.
|
|
54
|
+
*
|
|
55
|
+
* The counterpart of {@link storedHint}: that reads a hint off the record, this
|
|
56
|
+
* puts one back. Both exist because the screen answers from local state OR from
|
|
57
|
+
* the record, and if only one of the two is updated after a save the answer
|
|
58
|
+
* depends on whether the component happens to have been remounted since.
|
|
59
59
|
*/ export function hintColumnsFor(hints) {
|
|
60
60
|
const columns = {};
|
|
61
61
|
if ('token' in hints) columns.authTokenHint = hints.token ?? null;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether what the operator typed matches the connection they are deleting.
|
|
3
|
-
*
|
|
4
|
-
* Kept out of the component so the rule can be tested: it is the only thing
|
|
5
|
-
* standing between a stray click and a connection's calls, mappings and run
|
|
6
|
-
* history. Trimmed on both sides because a name pasted out of the list carries
|
|
7
|
-
* a trailing space often enough that refusing it reads as a bug, and compared
|
|
8
|
-
* exactly otherwise — two connections called "HR" and "HR 2" must not confirm
|
|
9
|
-
* for each other.
|
|
1
|
+
/**
|
|
2
|
+
* Whether what the operator typed matches the connection they are deleting.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component so the rule can be tested: it is the only thing
|
|
5
|
+
* standing between a stray click and a connection's calls, mappings and run
|
|
6
|
+
* history. Trimmed on both sides because a name pasted out of the list carries
|
|
7
|
+
* a trailing space often enough that refusing it reads as a bug, and compared
|
|
8
|
+
* exactly otherwise — two connections called "HR" and "HR 2" must not confirm
|
|
9
|
+
* for each other.
|
|
10
10
|
*/ export function deleteIsConfirmed(name, typed) {
|
|
11
11
|
const target = name.trim();
|
|
12
12
|
if (target.length === 0) return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The vocabulary of a field mapping row, kept out of the components so the
|
|
3
|
-
* header, the row and the editor cannot disagree about what a column is called
|
|
4
|
-
* or how wide it is.
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary of a field mapping row, kept out of the components so the
|
|
3
|
+
* header, the row and the editor cannot disagree about what a column is called
|
|
4
|
+
* or how wide it is.
|
|
5
5
|
*/ /** A source that reads from the record the PARENT endpoint produced. */ export const PARENT_PREFIX = '__parent.';
|
|
6
6
|
/** One template for the header and every row, so the columns cannot drift apart. */ export const FIELD_MAPPING_GRID = 'grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_1fr_1fr_auto]';
|
|
7
7
|
export const FIELD_MAPPING_COLUMNS = [
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The small things every form in this panel needs and none of them deserves a file.
|
|
3
|
-
*/ /**
|
|
4
|
-
* How an endpoint should read in a list someone has to choose from.
|
|
5
|
-
*
|
|
6
|
-
* The parameter-binding picker showed `tag`, which groups the endpoints of one
|
|
7
|
-
* API — so a source whose endpoints are all tagged `corporate-portal` offered
|
|
8
|
-
* three identical options and no way to tell which one supplied the value.
|
|
9
|
-
* The name is what distinguishes them; the path is what distinguishes them when
|
|
10
|
-
* nobody typed a name, since it is the one field that cannot be blank.
|
|
1
|
+
/**
|
|
2
|
+
* The small things every form in this panel needs and none of them deserves a file.
|
|
3
|
+
*/ /**
|
|
4
|
+
* How an endpoint should read in a list someone has to choose from.
|
|
5
|
+
*
|
|
6
|
+
* The parameter-binding picker showed `tag`, which groups the endpoints of one
|
|
7
|
+
* API — so a source whose endpoints are all tagged `corporate-portal` offered
|
|
8
|
+
* three identical options and no way to tell which one supplied the value.
|
|
9
|
+
* The name is what distinguishes them; the path is what distinguishes them when
|
|
10
|
+
* nobody typed a name, since it is the one field that cannot be blank.
|
|
11
11
|
*/ /** Same caps as the engine's `flattenKeys`: this feeds a dropdown, not an analysis. */ const MAX_FIELDS_LISTED = 200;
|
|
12
12
|
const MAX_FIELD_DEPTH = 4;
|
|
13
13
|
function walkKeys(value, prefix, out, depth) {
|
|
@@ -27,18 +27,18 @@ function walkKeys(value, prefix, out, depth) {
|
|
|
27
27
|
}
|
|
28
28
|
return out;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* The field names an endpoint's STORED sample reports.
|
|
32
|
-
*
|
|
33
|
-
* A sample is persisted on the endpoint precisely so the field list survives a
|
|
34
|
-
* reload, but the mapping editor was reading only the sample taken in this
|
|
35
|
-
* browser session. Come back to a connection tomorrow and it said "sample the
|
|
36
|
-
* endpoint first" about an endpoint it had already sampled and saved — so the
|
|
37
|
-
* source names had to be typed from memory, which is the one thing the sample
|
|
38
|
-
* exists to prevent.
|
|
39
|
-
*
|
|
40
|
-
* Mirrors the engine's `flattenKeys` rather than calling it: that lives in the
|
|
41
|
-
* server bundle, which this kit cannot import.
|
|
30
|
+
/**
|
|
31
|
+
* The field names an endpoint's STORED sample reports.
|
|
32
|
+
*
|
|
33
|
+
* A sample is persisted on the endpoint precisely so the field list survives a
|
|
34
|
+
* reload, but the mapping editor was reading only the sample taken in this
|
|
35
|
+
* browser session. Come back to a connection tomorrow and it said "sample the
|
|
36
|
+
* endpoint first" about an endpoint it had already sampled and saved — so the
|
|
37
|
+
* source names had to be typed from memory, which is the one thing the sample
|
|
38
|
+
* exists to prevent.
|
|
39
|
+
*
|
|
40
|
+
* Mirrors the engine's `flattenKeys` rather than calling it: that lives in the
|
|
41
|
+
* server bundle, which this kit cannot import.
|
|
42
42
|
*/ export function fieldsOfStoredSample(sample) {
|
|
43
43
|
const first = sample?.records?.[0];
|
|
44
44
|
if (!first || typeof first !== 'object') return [];
|
|
@@ -51,24 +51,24 @@ export function endpointLabel(endpoint) {
|
|
|
51
51
|
if (path) return `${endpoint.method} ${path}`;
|
|
52
52
|
return endpoint.tag;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
* The ids a control should point `aria-describedby` at.
|
|
56
|
-
*
|
|
57
|
-
* {@link Field} renders its hint as `<id>-hint` and its error as `<id>-error`,
|
|
58
|
-
* so pointing at an id that was not rendered would leave a screen reader
|
|
59
|
-
* announcing nothing — the ids are named here only when they exist.
|
|
54
|
+
/**
|
|
55
|
+
* The ids a control should point `aria-describedby` at.
|
|
56
|
+
*
|
|
57
|
+
* {@link Field} renders its hint as `<id>-hint` and its error as `<id>-error`,
|
|
58
|
+
* so pointing at an id that was not rendered would leave a screen reader
|
|
59
|
+
* announcing nothing — the ids are named here only when they exist.
|
|
60
60
|
*/ export function describedBy(id, hasHint, hasError) {
|
|
61
61
|
if (hasHint && hasError) return `${id}-hint ${id}-error`;
|
|
62
62
|
if (hasHint) return `${id}-hint`;
|
|
63
63
|
if (hasError) return `${id}-error`;
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Whatever a rejected action carries, as a sentence.
|
|
68
|
-
*
|
|
69
|
-
* The server's own wording is kept verbatim wherever there is one: it is the
|
|
70
|
-
* only text that knows WHICH setting was wrong, and a friendlier replacement
|
|
71
|
-
* would be a friendlier way of saying nothing.
|
|
66
|
+
/**
|
|
67
|
+
* Whatever a rejected action carries, as a sentence.
|
|
68
|
+
*
|
|
69
|
+
* The server's own wording is kept verbatim wherever there is one: it is the
|
|
70
|
+
* only text that knows WHICH setting was wrong, and a friendlier replacement
|
|
71
|
+
* would be a friendlier way of saying nothing.
|
|
72
72
|
*/ export function errorText(reason, fallback) {
|
|
73
73
|
if (reason instanceof Error && reason.message.length > 0) return reason.message;
|
|
74
74
|
if (typeof reason === 'string' && reason.length > 0) return reason;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading a jsonb column that is supposed to hold a list.
|
|
3
|
-
*
|
|
4
|
-
* It is not always a list. The schema generator wrote every object-shaped
|
|
5
|
-
* default — including `[]` — into Postgres as `'{}'`, which is an empty OBJECT.
|
|
6
|
-
* So every row that never had a value reads back as `{}`, and the first
|
|
7
|
-
* `for…of` over it throws "object is not iterable". That is every existing row
|
|
8
|
-
* the moment a new column is added to a table already in use, which is exactly
|
|
9
|
-
* when nobody is looking: the column is new, nothing has filled it in, and the
|
|
10
|
-
* screen that reads it dies on the next click.
|
|
11
|
-
*
|
|
12
|
-
* The generator is fixed. This exists because the rows it already wrote are
|
|
13
|
-
* still there, and because a value crossing the wire from a database is not
|
|
14
|
-
* something a component should have to trust.
|
|
1
|
+
/**
|
|
2
|
+
* Reading a jsonb column that is supposed to hold a list.
|
|
3
|
+
*
|
|
4
|
+
* It is not always a list. The schema generator wrote every object-shaped
|
|
5
|
+
* default — including `[]` — into Postgres as `'{}'`, which is an empty OBJECT.
|
|
6
|
+
* So every row that never had a value reads back as `{}`, and the first
|
|
7
|
+
* `for…of` over it throws "object is not iterable". That is every existing row
|
|
8
|
+
* the moment a new column is added to a table already in use, which is exactly
|
|
9
|
+
* when nobody is looking: the column is new, nothing has filled it in, and the
|
|
10
|
+
* screen that reads it dies on the next click.
|
|
11
|
+
*
|
|
12
|
+
* The generator is fixed. This exists because the rows it already wrote are
|
|
13
|
+
* still there, and because a value crossing the wire from a database is not
|
|
14
|
+
* something a component should have to trust.
|
|
15
15
|
*/ export function asArray(value) {
|
|
16
16
|
return Array.isArray(value) ? value : [];
|
|
17
17
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A JSON object typed into a form field.
|
|
3
|
-
*
|
|
4
|
-
* Query parameters and a request body are `Record<string, unknown>` on the wire
|
|
5
|
-
* — an endpoint may need `{"employeeTypes": ["A", "S"]}`, which no key/value
|
|
6
|
-
* grid of strings can express. So the field takes JSON, and the only question
|
|
7
|
-
* is whether a form is allowed to save what it cannot parse. It is not: a body
|
|
8
|
-
* saved as broken text fails at RUN time, against a live service, with an error
|
|
9
|
-
* from somewhere else entirely.
|
|
10
|
-
*/ /**
|
|
11
|
-
* Parses the text of a JSON-object field.
|
|
12
|
-
*
|
|
13
|
-
* Empty is `null` rather than `{}` — "nothing configured" and "an empty object
|
|
14
|
-
* configured" read the same on screen but not in a request, and the column is
|
|
15
|
-
* nullable precisely so the difference survives.
|
|
1
|
+
/**
|
|
2
|
+
* A JSON object typed into a form field.
|
|
3
|
+
*
|
|
4
|
+
* Query parameters and a request body are `Record<string, unknown>` on the wire
|
|
5
|
+
* — an endpoint may need `{"employeeTypes": ["A", "S"]}`, which no key/value
|
|
6
|
+
* grid of strings can express. So the field takes JSON, and the only question
|
|
7
|
+
* is whether a form is allowed to save what it cannot parse. It is not: a body
|
|
8
|
+
* saved as broken text fails at RUN time, against a live service, with an error
|
|
9
|
+
* from somewhere else entirely.
|
|
10
|
+
*/ /**
|
|
11
|
+
* Parses the text of a JSON-object field.
|
|
12
|
+
*
|
|
13
|
+
* Empty is `null` rather than `{}` — "nothing configured" and "an empty object
|
|
14
|
+
* configured" read the same on screen but not in a request, and the column is
|
|
15
|
+
* nullable precisely so the difference survives.
|
|
16
16
|
*/ export function parseJsonObject(text) {
|
|
17
17
|
const trimmed = text.trim();
|
|
18
18
|
if (trimmed === '') return {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { asArray } from './jsonArray';
|
|
2
2
|
import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
3
|
-
/**
|
|
4
|
-
* Upsert and ignore: the two choices that cannot lose a row by themselves.
|
|
5
|
-
*
|
|
6
|
-
* `targetEntity` is filled in when the operator arrived from the table itself —
|
|
7
|
-
* they have already said which one, and asking again is the panel forgetting.
|
|
3
|
+
/**
|
|
4
|
+
* Upsert and ignore: the two choices that cannot lose a row by themselves.
|
|
5
|
+
*
|
|
6
|
+
* `targetEntity` is filled in when the operator arrived from the table itself —
|
|
7
|
+
* they have already said which one, and asking again is the panel forgetting.
|
|
8
8
|
*/ export function blankMappingDraft(endpointId, targetEntity = '') {
|
|
9
9
|
return {
|
|
10
10
|
endpointId,
|
|
@@ -42,11 +42,11 @@ import { missingRecordFieldsIncomplete } from './MappingWriteModeFields';
|
|
|
42
42
|
enabled: mapping.enabled
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Everything standing between this draft and a run that does what it says.
|
|
47
|
-
*
|
|
48
|
-
* Reported as a list rather than a boolean because "Save is greyed out" with no
|
|
49
|
-
* reason is the single most common way a form wastes somebody's afternoon.
|
|
45
|
+
/**
|
|
46
|
+
* Everything standing between this draft and a run that does what it says.
|
|
47
|
+
*
|
|
48
|
+
* Reported as a list rather than a boolean because "Save is greyed out" with no
|
|
49
|
+
* reason is the single most common way a form wastes somebody's afternoon.
|
|
50
50
|
*/ export function mappingDraftIssues(draft) {
|
|
51
51
|
const issues = [];
|
|
52
52
|
if (draft.name.trim() === '') issues.push('Give the mapping a name.');
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether a mapping is actually keeping its table up to date.
|
|
3
|
-
*
|
|
4
|
-
* The list already said "Last run: 2026-08-02 12:51". Next to four siblings that
|
|
5
|
-
* all ran the previous night, that line is the difference between a working
|
|
6
|
-
* integration and a table two days out of date — and it takes arithmetic to see
|
|
7
|
-
* it, so nobody did. What happened instead: a chief executive was missing from
|
|
8
|
-
* the directory, a director's title was the one he had held in July, and people
|
|
9
|
-
* who had left were still listed. All four were the same fact, and the screen
|
|
10
|
-
* that knew it showed a timestamp.
|
|
11
|
-
*
|
|
12
|
-
* The cause underneath was smaller still: that mapping had no schedule. Its
|
|
13
|
-
* siblings ran nightly; it only ever ran when somebody pressed a button, and
|
|
14
|
-
* nobody had pressed it since Sunday. Nothing on the screen said so.
|
|
15
|
-
*
|
|
16
|
-
* So this answers two questions the timestamp cannot: does anything run this on
|
|
17
|
-
* its own, and is what it wrote still recent. Both are properties of the row, so
|
|
18
|
-
* they belong on the row.
|
|
1
|
+
/**
|
|
2
|
+
* Whether a mapping is actually keeping its table up to date.
|
|
3
|
+
*
|
|
4
|
+
* The list already said "Last run: 2026-08-02 12:51". Next to four siblings that
|
|
5
|
+
* all ran the previous night, that line is the difference between a working
|
|
6
|
+
* integration and a table two days out of date — and it takes arithmetic to see
|
|
7
|
+
* it, so nobody did. What happened instead: a chief executive was missing from
|
|
8
|
+
* the directory, a director's title was the one he had held in July, and people
|
|
9
|
+
* who had left were still listed. All four were the same fact, and the screen
|
|
10
|
+
* that knew it showed a timestamp.
|
|
11
|
+
*
|
|
12
|
+
* The cause underneath was smaller still: that mapping had no schedule. Its
|
|
13
|
+
* siblings ran nightly; it only ever ran when somebody pressed a button, and
|
|
14
|
+
* nobody had pressed it since Sunday. Nothing on the screen said so.
|
|
15
|
+
*
|
|
16
|
+
* So this answers two questions the timestamp cannot: does anything run this on
|
|
17
|
+
* its own, and is what it wrote still recent. Both are properties of the row, so
|
|
18
|
+
* they belong on the row.
|
|
19
19
|
*/ const HOUR = 60 * 60 * 1000;
|
|
20
20
|
export function mappingStaleness(question) {
|
|
21
21
|
const unscheduled = question.scheduleEnabled !== true;
|
|
22
22
|
const staleAfter = (question.staleAfterHours ?? 24) * HOUR;
|
|
23
23
|
const parsed = question.lastRunAt ? Date.parse(question.lastRunAt) : Number.NaN;
|
|
24
24
|
if (!Number.isFinite(parsed)) {
|
|
25
|
-
/*
|
|
26
|
-
* Never run is only worth a warning when nothing is going to run it.
|
|
27
|
-
* A mapping created five minutes ago with a schedule on it is not a
|
|
28
|
-
* problem — it is a mapping waiting for tonight.
|
|
25
|
+
/*
|
|
26
|
+
* Never run is only worth a warning when nothing is going to run it.
|
|
27
|
+
* A mapping created five minutes ago with a schedule on it is not a
|
|
28
|
+
* problem — it is a mapping waiting for tonight.
|
|
29
29
|
*/ return {
|
|
30
30
|
unscheduled,
|
|
31
31
|
neverRun: true,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { nextFireTimes } from '../../../../src/Shared/cron';
|
|
2
|
-
/**
|
|
3
|
-
* A fixed, unambiguous format rather than the reader's locale.
|
|
4
|
-
*
|
|
5
|
-
* `toLocaleString()` here would render the server's locale on the server and
|
|
6
|
-
* the browser's on the client — two different strings for one row, and a
|
|
7
|
-
* hydration tear. It would also print 8/5/2026, which in Turkey reads as the
|
|
8
|
-
* 8th of May. Year-first with the zone spelled out cannot be misread in either
|
|
9
|
-
* direction.
|
|
2
|
+
/**
|
|
3
|
+
* A fixed, unambiguous format rather than the reader's locale.
|
|
4
|
+
*
|
|
5
|
+
* `toLocaleString()` here would render the server's locale on the server and
|
|
6
|
+
* the browser's on the client — two different strings for one row, and a
|
|
7
|
+
* hydration tear. It would also print 8/5/2026, which in Turkey reads as the
|
|
8
|
+
* 8th of May. Year-first with the zone spelled out cannot be misread in either
|
|
9
|
+
* direction.
|
|
10
10
|
*/ function partsIn(when, timeZone) {
|
|
11
11
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
12
12
|
timeZone,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Does starting this import require a password?
|
|
3
|
-
*
|
|
4
|
-
* Two ways it can: the mapping writes one into a column of the record's own
|
|
5
|
-
* table, or it creates a login alongside each record. Either way the operator
|
|
6
|
-
* types it once, on the run screen, and it goes no further than that run.
|
|
7
|
-
*
|
|
8
|
-
* A function rather than three expressions, because it WAS three: the field that
|
|
9
|
-
* asks for the password, the button that starts the import, and the request that
|
|
10
|
-
* carries it each decided for themselves. Two were updated when accounts were
|
|
11
|
-
* added and one was not — so the field appeared, the button enabled, and the
|
|
12
|
-
* request sent nothing. Every import that created logins was refused for a
|
|
13
|
-
* missing password the operator had typed.
|
|
1
|
+
/**
|
|
2
|
+
* Does starting this import require a password?
|
|
3
|
+
*
|
|
4
|
+
* Two ways it can: the mapping writes one into a column of the record's own
|
|
5
|
+
* table, or it creates a login alongside each record. Either way the operator
|
|
6
|
+
* types it once, on the run screen, and it goes no further than that run.
|
|
7
|
+
*
|
|
8
|
+
* A function rather than three expressions, because it WAS three: the field that
|
|
9
|
+
* asks for the password, the button that starts the import, and the request that
|
|
10
|
+
* carries it each decided for themselves. Two were updated when accounts were
|
|
11
|
+
* added and one was not — so the field appeared, the button enabled, and the
|
|
12
|
+
* request sent nothing. Every import that created logins was refused for a
|
|
13
|
+
* missing password the operator had typed.
|
|
14
14
|
*/ export function passwordIsNeeded(mapping) {
|
|
15
15
|
return Boolean(mapping.passwordTargetColumn) || mapping.accountRule != null;
|
|
16
16
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading the parameter names out of an endpoint path.
|
|
3
|
-
*
|
|
4
|
-
* Shared by the sample screen, which asks for a value by hand, and the binding
|
|
5
|
-
* editor, which asks where the value should come from on a real run. One reader
|
|
6
|
-
* so the two screens can never disagree about what a path needs.
|
|
1
|
+
/**
|
|
2
|
+
* Reading the parameter names out of an endpoint path.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the sample screen, which asks for a value by hand, and the binding
|
|
5
|
+
* editor, which asks where the value should come from on a real run. One reader
|
|
6
|
+
* so the two screens can never disagree about what a path needs.
|
|
7
7
|
*/ const PARAM_PATTERN = /\{([^{}]+)\}|:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
8
8
|
/** `{brace}` and `:colon` are both accepted, so both are read back. */ export function pathParamNames(path) {
|
|
9
9
|
const names = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The small conversions the run screens share.
|
|
3
|
-
*
|
|
4
|
-
* Nothing here touches the server or React — it turns a record into the words an
|
|
5
|
-
* operator reads, and it does so the SAME way in every place, so a duration on a
|
|
6
|
-
* history row cannot disagree with the one on the panel above it.
|
|
1
|
+
/**
|
|
2
|
+
* The small conversions the run screens share.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here touches the server or React — it turns a record into the words an
|
|
5
|
+
* operator reads, and it does so the SAME way in every place, so a duration on a
|
|
6
|
+
* history row cannot disagree with the one on the panel above it.
|
|
7
7
|
*/ /** A run's outcome, coloured. Reverted is deliberately calm: nothing is wrong with it. */ export const RUN_STATUS_TONE = {
|
|
8
8
|
queued: 'neutral',
|
|
9
9
|
running: 'info',
|
|
@@ -18,12 +18,12 @@ export function errorText(error, fallback) {
|
|
|
18
18
|
function pad(value) {
|
|
19
19
|
return String(value).padStart(2, '0');
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* A moment, in UTC.
|
|
23
|
-
*
|
|
24
|
-
* Deliberately not locale-formatted: `toLocaleString` reads the server's zone on
|
|
25
|
-
* the server and the browser's on the client, so the two renders differ and the
|
|
26
|
-
* row tears on hydration. UTC is the same number everywhere, and it is labelled.
|
|
21
|
+
/**
|
|
22
|
+
* A moment, in UTC.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately not locale-formatted: `toLocaleString` reads the server's zone on
|
|
25
|
+
* the server and the browser's on the client, so the two renders differ and the
|
|
26
|
+
* row tears on hydration. UTC is the same number everywhere, and it is labelled.
|
|
27
27
|
*/ export function formatMoment(value) {
|
|
28
28
|
if (!value) return '—';
|
|
29
29
|
const date = new Date(value);
|
|
@@ -65,12 +65,12 @@ export function formatDuration(startedAt, finishedAt) {
|
|
|
65
65
|
if (run.errorCount > 0) parts.push(`${run.errorCount} failed`);
|
|
66
66
|
return parts.join(' · ');
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
* What a destructive run takes away, counted wherever there is a plan to count from.
|
|
70
|
-
*
|
|
71
|
-
* The mapping alone can say WHAT will happen; only the plan can say to how many
|
|
72
|
-
* rows. Both wordings exist because an operator is allowed to skip the preview,
|
|
73
|
-
* and a confirmation that says "some rows" is not a confirmation.
|
|
68
|
+
/**
|
|
69
|
+
* What a destructive run takes away, counted wherever there is a plan to count from.
|
|
70
|
+
*
|
|
71
|
+
* The mapping alone can say WHAT will happen; only the plan can say to how many
|
|
72
|
+
* rows. Both wordings exist because an operator is allowed to skip the preview,
|
|
73
|
+
* and a confirmation that says "some rows" is not a confirmation.
|
|
74
74
|
*/ export function describeLosses(mapping, plan) {
|
|
75
75
|
const lines = [];
|
|
76
76
|
const target = mapping.targetEntity;
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Who owns a busy flag when a late answer arrives.
|
|
3
|
-
*
|
|
4
|
-
* Every request in this panel sets a shared "in progress" flag on the way in and
|
|
5
|
-
* clears it on the way out. That breaks the moment an answer is DISCARDED —
|
|
6
|
-
* because the operator moved on, or because a newer request replaced this one —
|
|
7
|
-
* since a bare `return` skips the clearing too and the flag stays set with
|
|
8
|
-
* nobody left to unset it. The panel then shows a skeleton for a request that
|
|
9
|
-
* has already finished and will never be written.
|
|
10
|
-
*
|
|
11
|
-
* Two questions, and they are not the same question:
|
|
12
|
-
*
|
|
13
|
-
* superseded — a NEWER request of the same kind has started. It set the flag
|
|
14
|
-
* after this one did and will clear it when it settles, so this
|
|
15
|
-
* answer must leave the flag strictly alone.
|
|
16
|
-
* movedOn — the operator is looking at something else. Nobody else is
|
|
17
|
-
* coming: this request is the last one out and must clear the
|
|
18
|
-
* flag before dropping its answer.
|
|
1
|
+
/**
|
|
2
|
+
* Who owns a busy flag when a late answer arrives.
|
|
3
|
+
*
|
|
4
|
+
* Every request in this panel sets a shared "in progress" flag on the way in and
|
|
5
|
+
* clears it on the way out. That breaks the moment an answer is DISCARDED —
|
|
6
|
+
* because the operator moved on, or because a newer request replaced this one —
|
|
7
|
+
* since a bare `return` skips the clearing too and the flag stays set with
|
|
8
|
+
* nobody left to unset it. The panel then shows a skeleton for a request that
|
|
9
|
+
* has already finished and will never be written.
|
|
10
|
+
*
|
|
11
|
+
* Two questions, and they are not the same question:
|
|
12
|
+
*
|
|
13
|
+
* superseded — a NEWER request of the same kind has started. It set the flag
|
|
14
|
+
* after this one did and will clear it when it settles, so this
|
|
15
|
+
* answer must leave the flag strictly alone.
|
|
16
|
+
* movedOn — the operator is looking at something else. Nobody else is
|
|
17
|
+
* coming: this request is the last one out and must clear the
|
|
18
|
+
* flag before dropping its answer.
|
|
19
19
|
*/ export function settlementOf(args) {
|
|
20
20
|
const superseded = args.latest !== args.mine;
|
|
21
21
|
const movedOn = args.selectedNow !== args.requestedFor;
|
|
@@ -3,14 +3,14 @@ import { useEffect, useEffectEvent, useRef, useState } from 'react';
|
|
|
3
3
|
const DEFAULT_PAGE_SIZE = 20;
|
|
4
4
|
export function useInfiniteList(options) {
|
|
5
5
|
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
6
|
-
/**
|
|
7
|
-
* The caller's deps as ONE value.
|
|
8
|
-
*
|
|
9
|
-
* Spreading them into the dependency array made its LENGTH depend on the
|
|
10
|
-
* caller, and React requires that length to be constant across renders — an
|
|
11
|
-
* array that grows or shrinks silently stops the effect re-running when it
|
|
12
|
-
* should. Collapsing to a string keeps one slot no matter how many things the
|
|
13
|
-
* caller watches.
|
|
6
|
+
/**
|
|
7
|
+
* The caller's deps as ONE value.
|
|
8
|
+
*
|
|
9
|
+
* Spreading them into the dependency array made its LENGTH depend on the
|
|
10
|
+
* caller, and React requires that length to be constant across renders — an
|
|
11
|
+
* array that grows or shrinks silently stops the effect re-running when it
|
|
12
|
+
* should. Collapsing to a string keeps one slot no matter how many things the
|
|
13
|
+
* caller watches.
|
|
14
14
|
*/ const depsKey = JSON.stringify(options.deps);
|
|
15
15
|
const [items, setItems] = useState([]);
|
|
16
16
|
const [total, setTotal] = useState(0);
|