@geniee-inc/sfa-kit 1.12.0 → 1.14.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/GUIDE.md +89 -6
- package/package.json +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.js +2 -1
- package/src/lib/auth/SfaLoginGate.js +35 -7
- package/src/lib/auth/resolve-api-base.d.ts +19 -0
- package/src/lib/auth/resolve-api-base.js +65 -0
- package/src/lib/components/ErrorSnackbar.d.ts +53 -0
- package/src/lib/components/ErrorSnackbar.js +112 -0
- package/src/lib/provider/SfaClient.d.ts +44 -2
- package/src/lib/provider/SfaClient.js +74 -11
- package/src/lib/version.d.ts +1 -1
- package/src/lib/version.js +1 -1
package/GUIDE.md
CHANGED
|
@@ -18,7 +18,7 @@ The package ships built JavaScript and TypeScript declarations only. It contains
|
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import pkg from '@geniee-inc/sfa-kit/package.json';
|
|
21
|
-
// pkg.version -> '1.
|
|
21
|
+
// pkg.version -> '1.14.0'
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Two additional subpaths are available: `@geniee-inc/sfa-kit/signing` exposes bundle-signing utilities (`computeContentHash`, `computeSignature`, `createManifest`, `createManifestFromDirectory`) for Node.js environments, and `./package.json` exposes the manifest. Internal modules cannot be imported directly.
|
|
@@ -38,7 +38,7 @@ Every page uses `SfaLoginGate` as the outermost wrapper. It auto-detects the hos
|
|
|
38
38
|
|
|
39
39
|
### Props
|
|
40
40
|
|
|
41
|
-
- **`apiBase`** (optional) — Overrides the
|
|
41
|
+
- **`apiBase`** (optional) — Overrides the auto-detected Keystone API endpoint. When omitted, the endpoint is resolved from a hostname: the embedding SFA/CRM host's when the page runs inside the iframe, the page's own when it runs standalone. Non-production environments (`stg.chikyu.mobi`, `dev01`-`dev09`, `devdc`, `deva0`, `dev-docker`, `dev.chikyu.local`, `localhost`, `127.0.0.1`, `[::1]`, and every Amplify host of the new frontend under `*.amplify.axiom.chikyu.mobi`: PR previews plus the `devNN.waf.` / `stg.waf.` record-detail hosts) use `https://keystone-api-staging.chikyu.mobi`; everything else (`app.chikyu.net`, any hostname not in that list) uses `https://keystone-api-production.chikyu.mobi`. Production is the fallback on purpose so a page that reaches a customer never silently talks to staging.
|
|
42
42
|
- **`trustedOrigin`** (optional) — The origin of the SFA/CRM host page (e.g. `"https://app.chikyu.net"`). When provided and the page runs inside an iframe, `SfaLoginGate` uses the postMessage code-exchange flow (`POST /v1/auth/exchange`) instead of a login form. Injected automatically by Keystone's content endpoint at serve time.
|
|
43
43
|
- **`defaultOrganizationId`** (optional) — The org ID to auto-select when the user's account belongs to multiple organizations. **Set this to the org that generated the page's metadata** (the org used in Step 0 schema discovery). Without this prop, multi-org accounts auto-select the first available org, which may not be the intended one. Use an env var, not a literal: `defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}`.
|
|
44
44
|
- **`objectKey`** (optional) — The object this page is built for (e.g. `"companies"`). The standalone DevToolbar shows it in the page-info tooltip and in the record ID placeholder. Pass it at **both** levels; omit it only if the page genuinely has no object.
|
|
@@ -303,9 +303,11 @@ that did land, and those are not recoverable anywhere else.
|
|
|
303
303
|
- `reset` — clears results and error
|
|
304
304
|
|
|
305
305
|
A `501` inside that error is not a hiccup: the object does not offer bulk
|
|
306
|
-
writes at all. Waiting or retrying will never make it work, so
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
writes at all. Waiting or retrying will never make it work, so it arrives as
|
|
307
|
+
`SfaBulkUnsupportedError` — a `SfaApiError` carrying `status` 501 and a
|
|
308
|
+
`userMessage` that says the operation is unavailable rather than asking for
|
|
309
|
+
another try. `ErrorSnackbar` shows that text as it stands; a page writing its
|
|
310
|
+
own wording can tell the case apart with `instanceof` or `status`.
|
|
309
311
|
|
|
310
312
|
Everything the hook reports describes the object it was written for, on the
|
|
311
313
|
visit it was written on. Pass a different collection, or let the provider
|
|
@@ -332,6 +334,60 @@ batch the way you treat `unresolved`: the server may have written some of it
|
|
|
332
334
|
and the answer is gone, so re-read the records rather than sending them
|
|
333
335
|
again.
|
|
334
336
|
|
|
337
|
+
An oversized selection is refused with `SfaBulkRowCountError`, which carries
|
|
338
|
+
`limit`, `submitted` and a ready `userMessage` in Japanese. Show that message
|
|
339
|
+
rather than writing the sentence again in the page, where it would drift from
|
|
340
|
+
the limit the kit enforces. It lands in the hook's `error` like any whole-call
|
|
341
|
+
failure, so showing it takes no state of your own — hand `error` to
|
|
342
|
+
`ErrorSnackbar` and clear it with `reset`:
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
import { ErrorSnackbar, useBulkUpdateRecords } from '@geniee-inc/sfa-kit';
|
|
346
|
+
|
|
347
|
+
const { bulkUpdate, error, reset } = useBulkUpdateRecords('opportunity');
|
|
348
|
+
|
|
349
|
+
// the call rejects as well as setting `error`, so catch it to keep the
|
|
350
|
+
// rejection from reaching the console
|
|
351
|
+
const save = () => void bulkUpdate(edited).catch(() => undefined);
|
|
352
|
+
|
|
353
|
+
<ErrorSnackbar error={error} onClose={reset} />;
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`ErrorSnackbar` shows the error's `userMessage` when there is one and a general
|
|
357
|
+
line otherwise; an error's `message` is never shown, being English wording
|
|
358
|
+
written for the log. Pass `message` to say something more specific.
|
|
359
|
+
|
|
360
|
+
The same bar reports how a partly successful batch ended — the counts, and the
|
|
361
|
+
action left to take:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
return (
|
|
365
|
+
<>
|
|
366
|
+
<OpportunityGrid results={results} />
|
|
367
|
+
{/* `error` is null here: the call worked, some rows did not. The bar
|
|
368
|
+
appears because there is a message to show.
|
|
369
|
+
|
|
370
|
+
Not while the retry runs: `results` are kept during a call, so the bar
|
|
371
|
+
and its action would stay on screen, and a second press would send the
|
|
372
|
+
rows again. Nothing de-duplicates them — a repeated create writes
|
|
373
|
+
twice. */}
|
|
374
|
+
{!isLoading && (
|
|
375
|
+
<ErrorSnackbar
|
|
376
|
+
error={null}
|
|
377
|
+
message={`${results.length} 件中 ${updated.length} 件を保存しました。${failed.length} 件は保存されていません。`}
|
|
378
|
+
onRetry={() => void bulkUpdate(rowsFor(failed)).catch(() => undefined)}
|
|
379
|
+
onClose={reset}
|
|
380
|
+
/>
|
|
381
|
+
)}
|
|
382
|
+
</>
|
|
383
|
+
);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Counts only: which row failed and why belongs in the grid, where the reader
|
|
387
|
+
acts on it — `results` carries each row's `index` and its per-field errors.
|
|
388
|
+
Offer `onRetry` for `failed` rows; `unresolved` rows are re-read instead, so
|
|
389
|
+
label the action accordingly (`retryLabel`).
|
|
390
|
+
|
|
335
391
|
A row counts as written only when it carries what a write produces: the record
|
|
336
392
|
for a create or an update, the id for a delete. Anything short of that is
|
|
337
393
|
`failed` or `unresolved`, and which one is decided by what the answer says,
|
|
@@ -544,7 +600,34 @@ The server's error text is deliberately not shown: it is wording the reader cann
|
|
|
544
600
|
|
|
545
601
|
`refetch` retries whichever step failed. On `useRelatedRecords` that includes a failed describe: retrying only the list would do nothing, because without relationship metadata the list never ran.
|
|
546
602
|
|
|
547
|
-
|
|
603
|
+
Three baseline components for resilient pages:
|
|
604
|
+
|
|
605
|
+
### ErrorSnackbar
|
|
606
|
+
|
|
607
|
+
A bar at the bottom of the page saying why a write did not happen, and how a
|
|
608
|
+
partly successful one ended. Renders nothing until there is something to say —
|
|
609
|
+
an error or a `message` — so a page can hand it the state it keeps the failure
|
|
610
|
+
in.
|
|
611
|
+
|
|
612
|
+
```tsx
|
|
613
|
+
<ErrorSnackbar error={failure} onClose={reset} />
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
- `error` — the error as caught. Its `userMessage` is shown when it has one
|
|
617
|
+
(the row count refusal and the 501 do), a general line otherwise — and that
|
|
618
|
+
line says to try again later, which is why the failures where waiting cannot
|
|
619
|
+
help carry their own text. An error's `message` is never shown: it is
|
|
620
|
+
English wording written for the log
|
|
621
|
+
- `message` — overrides both, for when the page can be more specific. A bulk
|
|
622
|
+
write passes its counts here
|
|
623
|
+
- `onRetry`, `retryLabel` — an optional action beside the text, for the rows
|
|
624
|
+
in `failed`. Name it for what it does: `unresolved` rows are re-read, not
|
|
625
|
+
retried. Take the bar down while the retry runs: a second press sends the
|
|
626
|
+
same rows again, and nothing de-duplicates them
|
|
627
|
+
- `onClose` — clears it. The bar stays until dismissed: a reader told to
|
|
628
|
+
reduce a selection has to read a number before acting on it
|
|
629
|
+
|
|
630
|
+
Use `FetchErrorState` for a failed read and this for a failed write.
|
|
548
631
|
|
|
549
632
|
### ErrorBoundary
|
|
550
633
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SfaApiError, SfaConflictError, SfaValidationError, } from './lib/provider/SfaClient';
|
|
1
|
+
export { SfaApiError, SfaBulkRowCountError, SfaBulkUnsupportedError, SfaConflictError, SfaValidationError, } from './lib/provider/SfaClient';
|
|
2
2
|
export { SfaProvider } from './lib/provider/SfaProvider';
|
|
3
3
|
export { useSfaClient } from './lib/provider/useSfaClient';
|
|
4
4
|
export { SfaLoginGate } from './lib/auth/SfaLoginGate';
|
|
@@ -33,6 +33,7 @@ export { RecordList } from './lib/components/RecordList';
|
|
|
33
33
|
export { ErrorBoundary } from './lib/components/ErrorBoundary';
|
|
34
34
|
export { EmptyState } from './lib/components/EmptyState';
|
|
35
35
|
export { FetchErrorState } from './lib/components/FetchErrorState';
|
|
36
|
+
export { ErrorSnackbar } from './lib/components/ErrorSnackbar';
|
|
36
37
|
export type { SfaAuth, SfaApiMode, SfaClientConfig, SfaRecord, SfaListResponse, SfaListParams, SfaFieldValue, SfaFieldKind, SfaFieldValueMapping, FieldGroupSpec, FieldGroupLayout, SfaFieldDescriptor, SfaFieldOption, SfaRelationDescriptor, SfaObjectDetail, SfaObjectSummary, SfaObjectCatalog, SfaObjectPermissions, SfaObjectOrigin, SfaRelationshipKind, SfaCardinality, SfaDescribeResult, SfaFieldTypeDescriptor, SfaFieldTypeCatalog, SfaFilterOperator, SfaCreateOptions, SfaFieldError, SfaBulkRowStatus, SfaBulkRowResult, SfaBulkRowError, SfaBulkUpdateRow, SfaBulkDeleteRow, } from './lib/provider/types';
|
|
37
38
|
export { SFA_BULK_MAX_ROWS } from './lib/provider/SfaClient';
|
|
38
39
|
export type { SfaSession, AuthResult, OrganizationChoice, } from './lib/auth/types';
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SfaApiError, SfaConflictError, SfaValidationError, } from './lib/provider/SfaClient';
|
|
1
|
+
export { SfaApiError, SfaBulkRowCountError, SfaBulkUnsupportedError, SfaConflictError, SfaValidationError, } from './lib/provider/SfaClient';
|
|
2
2
|
export { SfaProvider } from './lib/provider/SfaProvider';
|
|
3
3
|
export { useSfaClient } from './lib/provider/useSfaClient';
|
|
4
4
|
export { SfaLoginGate } from './lib/auth/SfaLoginGate';
|
|
@@ -23,6 +23,7 @@ export { RecordList } from './lib/components/RecordList';
|
|
|
23
23
|
export { ErrorBoundary } from './lib/components/ErrorBoundary';
|
|
24
24
|
export { EmptyState } from './lib/components/EmptyState';
|
|
25
25
|
export { FetchErrorState } from './lib/components/FetchErrorState';
|
|
26
|
+
export { ErrorSnackbar } from './lib/components/ErrorSnackbar';
|
|
26
27
|
export { SFA_BULK_MAX_ROWS } from './lib/provider/SfaClient';
|
|
27
28
|
export { buildRecordDetailUrl, resolveHostOrigin, } from './lib/navigation/record-detail-url';
|
|
28
29
|
export { useRecordDetailUrl } from './lib/hooks/useRecordDetailUrl';
|
|
@@ -8,10 +8,10 @@ import { DevToolbar } from './DevToolbar';
|
|
|
8
8
|
import { parseInboundMessage, sendToHost } from './embed-protocol';
|
|
9
9
|
import { exchangeCode } from './exchange-code';
|
|
10
10
|
import { authenticate } from './keystone-auth';
|
|
11
|
+
import { KEYSTONE_PRODUCTION_API_BASE, resolveApiBase, } from './resolve-api-base';
|
|
11
12
|
import { loadSession, saveSession, clearSession, loadLastOrgId, } from './session-store';
|
|
12
13
|
import { StandalonePreviewCtx } from './StandalonePreviewContext';
|
|
13
14
|
import { EmbedContextValue } from './useEmbedContext';
|
|
14
|
-
const DEFAULT_API_BASE = 'https://keystone-api-staging.chikyu.mobi';
|
|
15
15
|
const REFRESH_BUFFER_SECONDS = 120;
|
|
16
16
|
/** After this many consecutive exchange failures, stop retrying and show session_expired. */
|
|
17
17
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
@@ -62,6 +62,21 @@ function resolveTrustedOrigin(explicitOrigin) {
|
|
|
62
62
|
}
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* @description The origin this page is being served from.
|
|
67
|
+
*
|
|
68
|
+
* Used to pick the Keystone API when the page runs standalone: a preview
|
|
69
|
+
* opened directly, or a Studio page on a developer's machine, has no
|
|
70
|
+
* embedding host to judge by, so its own hostname is the only signal.
|
|
71
|
+
*/
|
|
72
|
+
function resolvePageOrigin() {
|
|
73
|
+
try {
|
|
74
|
+
return window.location.origin;
|
|
75
|
+
}
|
|
76
|
+
catch (_a) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
/**
|
|
66
81
|
* @description Universal auth wrapper for sfa-kit pages.
|
|
67
82
|
*
|
|
@@ -75,13 +90,15 @@ function resolveTrustedOrigin(explicitOrigin) {
|
|
|
75
90
|
* automatically from the API (auto-selected for multi-org accounts when
|
|
76
91
|
* defaultOrganizationId is set, or the first org is used by default).
|
|
77
92
|
*/
|
|
78
|
-
export function SfaLoginGate({ apiBase
|
|
93
|
+
export function SfaLoginGate({ apiBase, trustedOrigin, defaultOrganizationId, objectKey, level, children, }) {
|
|
79
94
|
const shouldEmbed = isEmbedded() && (trustedOrigin != null || hasContentToken());
|
|
80
95
|
const resolvedOrigin = resolveTrustedOrigin(trustedOrigin);
|
|
81
96
|
if (shouldEmbed && resolvedOrigin != null) {
|
|
82
|
-
|
|
97
|
+
// Embedded: the SFA/CRM host that framed us decides the environment.
|
|
98
|
+
return (_jsx(EmbedFlow, { trustedOrigin: resolvedOrigin, apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvedOrigin), children: children }));
|
|
83
99
|
}
|
|
84
|
-
|
|
100
|
+
// Standalone: there is no host, so the page's own origin decides.
|
|
101
|
+
return (_jsx(StandaloneFlow, { apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvePageOrigin()), defaultOrganizationId: defaultOrganizationId, objectKey: objectKey, level: level, children: children }));
|
|
85
102
|
}
|
|
86
103
|
function EmbedFlow({ trustedOrigin, apiBase, children, }) {
|
|
87
104
|
const [phase, setPhase] = useState({ step: 'waiting_for_code' });
|
|
@@ -201,8 +218,19 @@ function EmbedStatus({ title, message, showReload, }) {
|
|
|
201
218
|
// Standalone flow (login form + DevToolbar)
|
|
202
219
|
// ---------------------------------------------------------------------------
|
|
203
220
|
function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, children, }) {
|
|
204
|
-
const
|
|
205
|
-
const [
|
|
221
|
+
const normalizedBase = useMemo(() => apiBase.replace(/\/+$/, ''), [apiBase]);
|
|
222
|
+
const [session, setSession] = useState(() => {
|
|
223
|
+
const saved = loadSession();
|
|
224
|
+
// Discard a saved session whose apiBase does not match the current
|
|
225
|
+
// environment. Without this check a staging session could be reused
|
|
226
|
+
// when the page is opened from a production host (or vice-versa).
|
|
227
|
+
if ((saved === null || saved === void 0 ? void 0 : saved.apiBase) != null && saved.apiBase !== normalizedBase) {
|
|
228
|
+
clearSession();
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return saved;
|
|
232
|
+
});
|
|
233
|
+
const [activeBase, setActiveBase] = useState(() => { var _a; return (_a = session === null || session === void 0 ? void 0 : session.apiBase) !== null && _a !== void 0 ? _a : normalizedBase; });
|
|
206
234
|
const [status, setStatus] = useState('idle');
|
|
207
235
|
const [error, setError] = useState(null);
|
|
208
236
|
// NOTE: level を渡さない既存ページのために、objectKey の有無から補う。
|
|
@@ -308,7 +336,7 @@ function LoginForm({ defaultApiBase, defaultOrganizationId, onSubmit, status, er
|
|
|
308
336
|
: undefined;
|
|
309
337
|
onSubmit(email.trim(), password, apiBase.trim(), Number.isFinite(orgId) ? orgId : undefined);
|
|
310
338
|
};
|
|
311
|
-
return (_jsx("div", { style: loginStyles.backdrop, children: _jsxs("form", { style: loginStyles.card, onSubmit: handleSubmit, children: [_jsxs("div", { style: loginStyles.logoGroup, children: [_jsx(GenieeLogoMark, {}), _jsxs("div", { style: loginStyles.logoText, children: [_jsx("span", { style: loginStyles.logoGeniee, children: "GENIEE" }), _jsx("span", { style: loginStyles.logoProduct, children: "SFA/CRM" })] })] }), error && (_jsx("div", { role: "alert", style: styles.error, children: error })), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.labelText, children: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9*" }), _jsx("input", { style: loginStyles.input, type: "text", value: email, onChange: e => setEmail(e.target.value), autoComplete: "email", required: true, disabled: status === 'loading' })] }), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.labelText, children: "\u30D1\u30B9\u30EF\u30FC\u30C9*" }), _jsx("input", { style: loginStyles.input, type: "password", value: password, onChange: e => setPassword(e.target.value), autoComplete: "current-password", required: true, disabled: status === 'loading' })] }), _jsx("button", { style: Object.assign(Object.assign({}, loginStyles.submitButton), { opacity: status === 'loading' ? 0.7 : 1 }), type: "submit", disabled: status === 'loading', children: status === 'loading' ? 'ログイン中...' : 'ログイン' }), _jsx("button", { type: "button", style: loginStyles.configToggle, onClick: () => setShowConfig(prev => !prev), children: showConfig ? '▾ 接続設定を隠す' : '▸ 接続設定' }), showConfig && (_jsxs("div", { style: loginStyles.configSection, children: [_jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.configLabel, children: "API Base" }), _jsx("input", { style: loginStyles.input, type: "text", value: apiBase, onChange: e => setApiBase(e.target.value), placeholder:
|
|
339
|
+
return (_jsx("div", { style: loginStyles.backdrop, children: _jsxs("form", { style: loginStyles.card, onSubmit: handleSubmit, children: [_jsxs("div", { style: loginStyles.logoGroup, children: [_jsx(GenieeLogoMark, {}), _jsxs("div", { style: loginStyles.logoText, children: [_jsx("span", { style: loginStyles.logoGeniee, children: "GENIEE" }), _jsx("span", { style: loginStyles.logoProduct, children: "SFA/CRM" })] })] }), error && (_jsx("div", { role: "alert", style: styles.error, children: error })), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.labelText, children: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9*" }), _jsx("input", { style: loginStyles.input, type: "text", value: email, onChange: e => setEmail(e.target.value), autoComplete: "email", required: true, disabled: status === 'loading' })] }), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.labelText, children: "\u30D1\u30B9\u30EF\u30FC\u30C9*" }), _jsx("input", { style: loginStyles.input, type: "password", value: password, onChange: e => setPassword(e.target.value), autoComplete: "current-password", required: true, disabled: status === 'loading' })] }), _jsx("button", { style: Object.assign(Object.assign({}, loginStyles.submitButton), { opacity: status === 'loading' ? 0.7 : 1 }), type: "submit", disabled: status === 'loading', children: status === 'loading' ? 'ログイン中...' : 'ログイン' }), _jsx("button", { type: "button", style: loginStyles.configToggle, onClick: () => setShowConfig(prev => !prev), children: showConfig ? '▾ 接続設定を隠す' : '▸ 接続設定' }), showConfig && (_jsxs("div", { style: loginStyles.configSection, children: [_jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.configLabel, children: "API Base" }), _jsx("input", { style: loginStyles.input, type: "text", value: apiBase, onChange: e => setApiBase(e.target.value), placeholder: KEYSTONE_PRODUCTION_API_BASE, disabled: status === 'loading' })] }), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.configLabel, children: "Organization ID" }), _jsx("input", { style: loginStyles.input, type: "text", value: orgIdInput, onChange: e => setOrgIdInput(e.target.value), placeholder: "\u81EA\u52D5\u9078\u629E (\u7A7A\u6B04)", disabled: status === 'loading' })] })] })), _jsxs("div", { style: styles.versionFooter, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }) }));
|
|
312
340
|
}
|
|
313
341
|
// ---------------------------------------------------------------------------
|
|
314
342
|
// Geniee logo mark (inline SVG)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const KEYSTONE_PRODUCTION_API_BASE = "https://keystone-api-production.chikyu.mobi";
|
|
2
|
+
export declare const KEYSTONE_STAGING_API_BASE = "https://keystone-api-staging.chikyu.mobi";
|
|
3
|
+
/**
|
|
4
|
+
* @description Resolves the Keystone API base URL from an origin.
|
|
5
|
+
*
|
|
6
|
+
* Every non-production chikyu environment — `stg.chikyu.mobi`, the dev tier,
|
|
7
|
+
* local development, and Amplify PR previews — uses the staging Keystone API.
|
|
8
|
+
* Everything else — `app.chikyu.net`, an origin this table does not know, or
|
|
9
|
+
* no origin at all — uses production.
|
|
10
|
+
*
|
|
11
|
+
* Production is the fallback on purpose: a page that reaches a real customer
|
|
12
|
+
* must never silently talk to staging. A developer on an unrecognised host
|
|
13
|
+
* can still pass `apiBase` explicitly or type it into the standalone login form.
|
|
14
|
+
*
|
|
15
|
+
* Callers decide which origin to pass: the embedding host's origin when the
|
|
16
|
+
* page runs inside the SFA/CRM iframe, and the page's own origin when it runs
|
|
17
|
+
* standalone. A malformed origin is treated the same as no origin.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveApiBase(origin: string | undefined): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const KEYSTONE_PRODUCTION_API_BASE = 'https://keystone-api-production.chikyu.mobi';
|
|
2
|
+
export const KEYSTONE_STAGING_API_BASE = 'https://keystone-api-staging.chikyu.mobi';
|
|
3
|
+
/** Dev-tier chikyu (legacy frontend) hosts: dev01-dev09, devdc, deva0, and the like. */
|
|
4
|
+
const DEV_HOST_PATTERN = /^dev\w+\.chikyu\.mobi$/;
|
|
5
|
+
/**
|
|
6
|
+
* Amplify-hosted new frontend. Covers PR previews
|
|
7
|
+
* (`pr-{id}.amplify.axiom.chikyu.mobi`, see amplify.yml) AND the dev / stg
|
|
8
|
+
* record-detail hosts that frame sfa-kit pages
|
|
9
|
+
* (`dev01.waf.amplify.axiom.chikyu.mobi`, `stg.waf.amplify.axiom.chikyu.mobi`).
|
|
10
|
+
* Keep this a suffix match: narrowing it to `pr-` would send every dev
|
|
11
|
+
* environment to production.
|
|
12
|
+
*/
|
|
13
|
+
const AMPLIFY_PATTERN = /\.amplify\.axiom\.chikyu\.mobi$/;
|
|
14
|
+
/** Hostnames that are always a developer's own machine. */
|
|
15
|
+
const LOCAL_HOSTNAMES = new Set([
|
|
16
|
+
'localhost',
|
|
17
|
+
'127.0.0.1',
|
|
18
|
+
'[::1]',
|
|
19
|
+
'dev.chikyu.local',
|
|
20
|
+
'dev-docker.chikyu.mobi',
|
|
21
|
+
]);
|
|
22
|
+
/** Shared pre-production chikyu environments. */
|
|
23
|
+
const STAGING_HOSTNAMES = new Set(['stg.chikyu.mobi']);
|
|
24
|
+
/**
|
|
25
|
+
* @description Resolves the Keystone API base URL from an origin.
|
|
26
|
+
*
|
|
27
|
+
* Every non-production chikyu environment — `stg.chikyu.mobi`, the dev tier,
|
|
28
|
+
* local development, and Amplify PR previews — uses the staging Keystone API.
|
|
29
|
+
* Everything else — `app.chikyu.net`, an origin this table does not know, or
|
|
30
|
+
* no origin at all — uses production.
|
|
31
|
+
*
|
|
32
|
+
* Production is the fallback on purpose: a page that reaches a real customer
|
|
33
|
+
* must never silently talk to staging. A developer on an unrecognised host
|
|
34
|
+
* can still pass `apiBase` explicitly or type it into the standalone login form.
|
|
35
|
+
*
|
|
36
|
+
* Callers decide which origin to pass: the embedding host's origin when the
|
|
37
|
+
* page runs inside the SFA/CRM iframe, and the page's own origin when it runs
|
|
38
|
+
* standalone. A malformed origin is treated the same as no origin.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveApiBase(origin) {
|
|
41
|
+
const hostname = hostnameOf(origin);
|
|
42
|
+
if (hostname == null)
|
|
43
|
+
return KEYSTONE_PRODUCTION_API_BASE;
|
|
44
|
+
const isNonProduction = STAGING_HOSTNAMES.has(hostname) ||
|
|
45
|
+
LOCAL_HOSTNAMES.has(hostname) ||
|
|
46
|
+
DEV_HOST_PATTERN.test(hostname) ||
|
|
47
|
+
AMPLIFY_PATTERN.test(hostname);
|
|
48
|
+
return isNonProduction
|
|
49
|
+
? KEYSTONE_STAGING_API_BASE
|
|
50
|
+
: KEYSTONE_PRODUCTION_API_BASE;
|
|
51
|
+
}
|
|
52
|
+
/** The hostname of an origin, or undefined when there is no usable origin. */
|
|
53
|
+
function hostnameOf(origin) {
|
|
54
|
+
if (origin == null)
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
// WHATWG URL preserves a trailing dot on FQDN hostnames (e.g.
|
|
58
|
+
// "stg.chikyu.mobi." stays as-is). Strip it so the allowlist and
|
|
59
|
+
// regex checks match regardless of whether the dot is present.
|
|
60
|
+
return new URL(origin).hostname.replace(/\.$/, '');
|
|
61
|
+
}
|
|
62
|
+
catch (_a) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
interface ErrorSnackbarProps {
|
|
2
|
+
/**
|
|
3
|
+
* The error to announce, as caught. A page can hand this the same state it
|
|
4
|
+
* stores the failure in; pass null when there is none and say what happened
|
|
5
|
+
* through `message` instead, as a partly successful batch does.
|
|
6
|
+
*/
|
|
7
|
+
readonly error: unknown;
|
|
8
|
+
/** Dismisses it. Clear the error the page is holding. */
|
|
9
|
+
readonly onClose: () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Overrides the text. Use when the page can say something more specific
|
|
12
|
+
* than the kit can — which object was being saved, or how a bulk write
|
|
13
|
+
* ended: `8 件中 5 件を保存しました。2 件は保存されていません。` An empty
|
|
14
|
+
* string counts as nothing said, and the error is used instead.
|
|
15
|
+
*/
|
|
16
|
+
readonly message?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Adds an action next to the text. A bulk write hands this the retry of
|
|
19
|
+
* its `failed` rows; omit it when there is nothing to offer.
|
|
20
|
+
*/
|
|
21
|
+
readonly onRetry?: () => void;
|
|
22
|
+
/** Label for that action. */
|
|
23
|
+
readonly retryLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @description A bar that tells the reader why a write did not happen.
|
|
27
|
+
*
|
|
28
|
+
* The text comes from the error's `userMessage` when it has one — the row
|
|
29
|
+
* count refusal and the 501 that says an object has no bulk write do, since
|
|
30
|
+
* the general line asks for another try and neither is worth retrying — and
|
|
31
|
+
* falls back to that general line otherwise. An error's
|
|
32
|
+
* `message` is never shown: it is written for the log, in English, and names
|
|
33
|
+
* things the reader cannot act on.
|
|
34
|
+
*
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const [failure, setFailure] = useState<unknown>(null);
|
|
37
|
+
*
|
|
38
|
+
* const save = async () => {
|
|
39
|
+
* try {
|
|
40
|
+
* await bulkUpdate(edited);
|
|
41
|
+
* } catch (error) {
|
|
42
|
+
* setFailure(error);
|
|
43
|
+
* }
|
|
44
|
+
* };
|
|
45
|
+
*
|
|
46
|
+
* <ErrorSnackbar error={failure} onClose={() => setFailure(null)} />;
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* NOTE: it stays until dismissed. A reader told to reduce a selection has to
|
|
50
|
+
* read a number and then act on it, which a bar that times out takes away.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ErrorSnackbar({ error, onClose, message, onRetry, retryLabel, }: ErrorSnackbarProps): JSX.Element | null;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { color, font, radius, space } from '../theme';
|
|
3
|
+
/** Shown when the error carries nothing a reader can act on. */
|
|
4
|
+
const FALLBACK_MESSAGE = '保存できませんでした。時間をおいて再度お試しください。';
|
|
5
|
+
/**
|
|
6
|
+
* @description A bar that tells the reader why a write did not happen.
|
|
7
|
+
*
|
|
8
|
+
* The text comes from the error's `userMessage` when it has one — the row
|
|
9
|
+
* count refusal and the 501 that says an object has no bulk write do, since
|
|
10
|
+
* the general line asks for another try and neither is worth retrying — and
|
|
11
|
+
* falls back to that general line otherwise. An error's
|
|
12
|
+
* `message` is never shown: it is written for the log, in English, and names
|
|
13
|
+
* things the reader cannot act on.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const [failure, setFailure] = useState<unknown>(null);
|
|
17
|
+
*
|
|
18
|
+
* const save = async () => {
|
|
19
|
+
* try {
|
|
20
|
+
* await bulkUpdate(edited);
|
|
21
|
+
* } catch (error) {
|
|
22
|
+
* setFailure(error);
|
|
23
|
+
* }
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* <ErrorSnackbar error={failure} onClose={() => setFailure(null)} />;
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* NOTE: it stays until dismissed. A reader told to reduce a selection has to
|
|
30
|
+
* read a number and then act on it, which a bar that times out takes away.
|
|
31
|
+
*/
|
|
32
|
+
export function ErrorSnackbar({ error, onClose, message, onRetry, retryLabel = '再試行', }) {
|
|
33
|
+
// NOTE: a partly successful batch has no error at all — the call worked,
|
|
34
|
+
// some rows did not. The bar still has something to say, so what decides
|
|
35
|
+
// whether it appears is the text, not the error.
|
|
36
|
+
//
|
|
37
|
+
// NOTE: an empty string is a page with nothing to say — a summary built from
|
|
38
|
+
// a list that turned out empty. Taken as given it would draw a bar carrying
|
|
39
|
+
// a warning sign and no words, and announce nothing to a screen reader
|
|
40
|
+
const pageText = message != null && message.trim() !== '' ? message : null;
|
|
41
|
+
const text = pageText !== null && pageText !== void 0 ? pageText : (error == null ? null : readUserMessage(error));
|
|
42
|
+
if (text == null)
|
|
43
|
+
return null;
|
|
44
|
+
return (_jsxs("div", { style: styles.bar, children: [_jsxs("span", { style: styles.text, role: "alert", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0" }), " ", text] }), onRetry && (_jsx("button", { type: "button", style: styles.retry, onClick: onRetry, children: retryLabel })), _jsx("button", { type: "button", style: styles.close, onClick: onClose, "aria-label": "\u9589\u3058\u308B", children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @description The reader-facing text of an error, if it has one.
|
|
48
|
+
*
|
|
49
|
+
* NOTE: reads the field rather than testing for a class, so an error type
|
|
50
|
+
* added later carries its own wording here without changing this component —
|
|
51
|
+
* and this stays a presentational component with nothing to know about the
|
|
52
|
+
* provider layer.
|
|
53
|
+
*/
|
|
54
|
+
function readUserMessage(error) {
|
|
55
|
+
if (error != null && typeof error === 'object' && 'userMessage' in error) {
|
|
56
|
+
const carried = error.userMessage;
|
|
57
|
+
if (typeof carried === 'string' && carried !== '')
|
|
58
|
+
return carried;
|
|
59
|
+
}
|
|
60
|
+
return FALLBACK_MESSAGE;
|
|
61
|
+
}
|
|
62
|
+
const styles = {
|
|
63
|
+
bar: {
|
|
64
|
+
position: 'fixed',
|
|
65
|
+
left: '50%',
|
|
66
|
+
bottom: space.lg,
|
|
67
|
+
transform: 'translateX(-50%)',
|
|
68
|
+
zIndex: 1000,
|
|
69
|
+
display: 'flex',
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
gap: space.md,
|
|
72
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
73
|
+
padding: `${space.sm}px ${space.md}px`,
|
|
74
|
+
fontFamily: font.family,
|
|
75
|
+
fontSize: font.sizeMd,
|
|
76
|
+
color: color.textPrimary,
|
|
77
|
+
backgroundColor: color.errorLight,
|
|
78
|
+
border: `1px solid ${color.error}`,
|
|
79
|
+
borderRadius: radius.sm,
|
|
80
|
+
},
|
|
81
|
+
text: {
|
|
82
|
+
fontWeight: font.weightMedium,
|
|
83
|
+
},
|
|
84
|
+
retry: {
|
|
85
|
+
flexShrink: 0,
|
|
86
|
+
padding: 0,
|
|
87
|
+
fontFamily: font.family,
|
|
88
|
+
fontSize: font.sizeMd,
|
|
89
|
+
color: color.primary,
|
|
90
|
+
backgroundColor: 'transparent',
|
|
91
|
+
border: 'none',
|
|
92
|
+
textDecoration: 'underline',
|
|
93
|
+
cursor: 'pointer',
|
|
94
|
+
},
|
|
95
|
+
close: {
|
|
96
|
+
flexShrink: 0,
|
|
97
|
+
// NOTE: 記号は小さいので、押せる範囲は文字サイズではなく箱で確保する
|
|
98
|
+
display: 'flex',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
justifyContent: 'center',
|
|
101
|
+
width: space.lg,
|
|
102
|
+
height: space.lg,
|
|
103
|
+
padding: 0,
|
|
104
|
+
fontFamily: font.family,
|
|
105
|
+
fontSize: font.sizeXl,
|
|
106
|
+
lineHeight: 1,
|
|
107
|
+
color: color.textSecondary,
|
|
108
|
+
backgroundColor: 'transparent',
|
|
109
|
+
border: 'none',
|
|
110
|
+
cursor: 'pointer',
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -14,8 +14,9 @@ import type { SfaApiMode, SfaBulkDeleteRow, SfaBulkRowResult, SfaBulkUpdateRow,
|
|
|
14
14
|
* nothing about which rows had been written. It can go up only once a row
|
|
15
15
|
* costs less, or those limits change.
|
|
16
16
|
*
|
|
17
|
-
* NOTE: public because a caller cannot
|
|
18
|
-
* check that enforces it so the two cannot
|
|
17
|
+
* NOTE: public because a caller cannot size its batches or its own warning
|
|
18
|
+
* without it, and kept next to the check that enforces it so the two cannot
|
|
19
|
+
* disagree.
|
|
19
20
|
*/
|
|
20
21
|
export declare const SFA_BULK_MAX_ROWS = 25;
|
|
21
22
|
/**
|
|
@@ -166,6 +167,47 @@ export declare class SfaConflictError extends SfaApiError {
|
|
|
166
167
|
constructor(status: number, statusText: string, body: string, url: string, currentRecord: SfaRecord | null);
|
|
167
168
|
static fromBody(status: number, statusText: string, body: string, url: string): SfaConflictError;
|
|
168
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* @description A bulk write refused before a request went out: nothing was
|
|
172
|
+
* selected, or more rows than the kit sends in one call.
|
|
173
|
+
*
|
|
174
|
+
* NOTE: `message` is for the log, like the rest of the kit's errors, and
|
|
175
|
+
* `userMessage` is what a page shows the reader — the same split the kit
|
|
176
|
+
* already makes between an error and the text in `FetchErrorState`. The page
|
|
177
|
+
* decides where to show it; the kit has no UI of its own.
|
|
178
|
+
*
|
|
179
|
+
* NOTE: a distinct class so a page recognises this without matching on the
|
|
180
|
+
* message. It is the one write failure a reader causes and can fix.
|
|
181
|
+
*/
|
|
182
|
+
export declare class SfaBulkRowCountError extends Error {
|
|
183
|
+
readonly submitted: number;
|
|
184
|
+
/** What a page shows the reader. `message` is the English text for the log. */
|
|
185
|
+
readonly userMessage: string;
|
|
186
|
+
/** The most rows one call sends, so a page can say it without importing it. */
|
|
187
|
+
readonly limit = 25;
|
|
188
|
+
/** @param submitted how many rows the caller tried to send */
|
|
189
|
+
constructor(submitted: number);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* @description Thrown when the object offers no bulk writes at all: the
|
|
193
|
+
* endpoint answers 501.
|
|
194
|
+
*
|
|
195
|
+
* NOTE: told apart from other failures because waiting changes nothing. A
|
|
196
|
+
* page with no wording of its own says to try again later, which is the one
|
|
197
|
+
* thing that cannot work here — the operation is not implemented for this
|
|
198
|
+
* object, today or in an hour.
|
|
199
|
+
*/
|
|
200
|
+
export declare class SfaBulkUnsupportedError extends SfaApiError {
|
|
201
|
+
/** What a page shows the reader. `message` is the English text for the log. */
|
|
202
|
+
readonly userMessage: string;
|
|
203
|
+
/**
|
|
204
|
+
* NOTE: takes the response as it arrived, like every other error here, so
|
|
205
|
+
* the server's `detail` and `code` are read from the whole body. Building
|
|
206
|
+
* it from an `SfaApiError` instead would re-read a body already cut to
|
|
207
|
+
* `ERROR_BODY_MAX_LENGTH`, and a cut inside the JSON leaves nothing to read.
|
|
208
|
+
*/
|
|
209
|
+
constructor(status: number, statusText: string, body: string, url: string);
|
|
210
|
+
}
|
|
169
211
|
/**
|
|
170
212
|
* @description Thrown on 422 with code VALIDATION_FAILED. Exposes per-field
|
|
171
213
|
* errors so the UI can highlight which fields need correction.
|
|
@@ -32,8 +32,9 @@ const BULK_REQUEST_TIMEOUT_MS = 60000;
|
|
|
32
32
|
* nothing about which rows had been written. It can go up only once a row
|
|
33
33
|
* costs less, or those limits change.
|
|
34
34
|
*
|
|
35
|
-
* NOTE: public because a caller cannot
|
|
36
|
-
* check that enforces it so the two cannot
|
|
35
|
+
* NOTE: public because a caller cannot size its batches or its own warning
|
|
36
|
+
* without it, and kept next to the check that enforces it so the two cannot
|
|
37
|
+
* disagree.
|
|
37
38
|
*/
|
|
38
39
|
export const SFA_BULK_MAX_ROWS = 25;
|
|
39
40
|
/**
|
|
@@ -310,6 +311,7 @@ export class SfaClient {
|
|
|
310
311
|
const path = `/v1/objects/${enc(collection)}/records/${operation}`;
|
|
311
312
|
const response = yield this.fetchWithBody('POST', path, { records }, {
|
|
312
313
|
timeoutMs: BULK_REQUEST_TIMEOUT_MS,
|
|
314
|
+
bulkUnsupportedOn501: true,
|
|
313
315
|
});
|
|
314
316
|
return normalizeBulkResults(yield response.json(), records.length);
|
|
315
317
|
});
|
|
@@ -464,6 +466,11 @@ export class SfaClient {
|
|
|
464
466
|
if (response.status === 422) {
|
|
465
467
|
throw SfaValidationError.fromBody(response.status, response.statusText, text, url);
|
|
466
468
|
}
|
|
469
|
+
// NOTE: only the bulk endpoints answer 501, and only they can say what
|
|
470
|
+
// it means to the reader. A 501 elsewhere stays a plain SfaApiError
|
|
471
|
+
if (response.status === 501 && (options === null || options === void 0 ? void 0 : options.bulkUnsupportedOn501)) {
|
|
472
|
+
throw new SfaBulkUnsupportedError(response.status, response.statusText, text, url);
|
|
473
|
+
}
|
|
467
474
|
throw new SfaApiError(response.status, response.statusText, text, url);
|
|
468
475
|
}
|
|
469
476
|
return response;
|
|
@@ -860,17 +867,14 @@ function parseConflictRecord(body) {
|
|
|
860
867
|
/**
|
|
861
868
|
* @description Guards the row count before a request goes out.
|
|
862
869
|
*
|
|
863
|
-
* NOTE:
|
|
864
|
-
*
|
|
865
|
-
* so
|
|
870
|
+
* NOTE: an empty array is rejected by the API with 422 for the whole call, and
|
|
871
|
+
* a selection over the kit's limit is refused before it can be sent at all.
|
|
872
|
+
* Either way the caller loses every row, so this names the limit and the count
|
|
873
|
+
* instead of leaving a generic validation failure to explain it.
|
|
866
874
|
*/
|
|
867
875
|
function assertBulkRowCount(count) {
|
|
868
|
-
if (count === 0) {
|
|
869
|
-
throw new
|
|
870
|
-
}
|
|
871
|
-
if (count > SFA_BULK_MAX_ROWS) {
|
|
872
|
-
throw new Error(`A bulk write accepts at most ${SFA_BULK_MAX_ROWS} rows, got ${count}. ` +
|
|
873
|
-
'Split the selection into batches.');
|
|
876
|
+
if (count === 0 || count > SFA_BULK_MAX_ROWS) {
|
|
877
|
+
throw new SfaBulkRowCountError(count);
|
|
874
878
|
}
|
|
875
879
|
}
|
|
876
880
|
/**
|
|
@@ -1112,6 +1116,65 @@ export class SfaConflictError extends SfaApiError {
|
|
|
1112
1116
|
return new SfaConflictError(status, statusText, body, url, currentRecord);
|
|
1113
1117
|
}
|
|
1114
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* @description A bulk write refused before a request went out: nothing was
|
|
1121
|
+
* selected, or more rows than the kit sends in one call.
|
|
1122
|
+
*
|
|
1123
|
+
* NOTE: `message` is for the log, like the rest of the kit's errors, and
|
|
1124
|
+
* `userMessage` is what a page shows the reader — the same split the kit
|
|
1125
|
+
* already makes between an error and the text in `FetchErrorState`. The page
|
|
1126
|
+
* decides where to show it; the kit has no UI of its own.
|
|
1127
|
+
*
|
|
1128
|
+
* NOTE: a distinct class so a page recognises this without matching on the
|
|
1129
|
+
* message. It is the one write failure a reader causes and can fix.
|
|
1130
|
+
*/
|
|
1131
|
+
export class SfaBulkRowCountError extends Error {
|
|
1132
|
+
/** @param submitted how many rows the caller tried to send */
|
|
1133
|
+
constructor(submitted) {
|
|
1134
|
+
super(rowCountLogText(submitted));
|
|
1135
|
+
this.submitted = submitted;
|
|
1136
|
+
/** The most rows one call sends, so a page can say it without importing it. */
|
|
1137
|
+
this.limit = SFA_BULK_MAX_ROWS;
|
|
1138
|
+
this.name = 'SfaBulkRowCountError';
|
|
1139
|
+
this.userMessage = rowCountReaderText(submitted);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* @description Thrown when the object offers no bulk writes at all: the
|
|
1144
|
+
* endpoint answers 501.
|
|
1145
|
+
*
|
|
1146
|
+
* NOTE: told apart from other failures because waiting changes nothing. A
|
|
1147
|
+
* page with no wording of its own says to try again later, which is the one
|
|
1148
|
+
* thing that cannot work here — the operation is not implemented for this
|
|
1149
|
+
* object, today or in an hour.
|
|
1150
|
+
*/
|
|
1151
|
+
export class SfaBulkUnsupportedError extends SfaApiError {
|
|
1152
|
+
/**
|
|
1153
|
+
* NOTE: takes the response as it arrived, like every other error here, so
|
|
1154
|
+
* the server's `detail` and `code` are read from the whole body. Building
|
|
1155
|
+
* it from an `SfaApiError` instead would re-read a body already cut to
|
|
1156
|
+
* `ERROR_BODY_MAX_LENGTH`, and a cut inside the JSON leaves nothing to read.
|
|
1157
|
+
*/
|
|
1158
|
+
constructor(status, statusText, body, url) {
|
|
1159
|
+
super(status, statusText, body, url);
|
|
1160
|
+
/** What a page shows the reader. `message` is the English text for the log. */
|
|
1161
|
+
this.userMessage = 'このオブジェクトでは一括操作を利用できません。';
|
|
1162
|
+
this.name = 'SfaBulkUnsupportedError';
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function rowCountLogText(submitted) {
|
|
1166
|
+
if (submitted === 0)
|
|
1167
|
+
return 'A bulk write needs at least one row.';
|
|
1168
|
+
return (`A bulk write accepts at most ${SFA_BULK_MAX_ROWS} rows, ` +
|
|
1169
|
+
`got ${submitted}. Split the selection into batches.`);
|
|
1170
|
+
}
|
|
1171
|
+
function rowCountReaderText(submitted) {
|
|
1172
|
+
if (submitted === 0)
|
|
1173
|
+
return '保存する行が選択されていません。';
|
|
1174
|
+
return (`一度に保存できるのは ${SFA_BULK_MAX_ROWS} 件までです。` +
|
|
1175
|
+
`選択されているのは ${submitted} 件です。` +
|
|
1176
|
+
`選択を ${SFA_BULK_MAX_ROWS} 件以下に減らしてから保存してください。`);
|
|
1177
|
+
}
|
|
1115
1178
|
/**
|
|
1116
1179
|
* @description Thrown on 422 with code VALIDATION_FAILED. Exposes per-field
|
|
1117
1180
|
* errors so the UI can highlight which fields need correction.
|
package/src/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SFA_KIT_VERSION = "1.
|
|
1
|
+
export declare const SFA_KIT_VERSION = "1.14.0";
|
package/src/lib/version.js
CHANGED