@geniee-inc/sfa-kit 1.12.0 → 1.13.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 CHANGED
@@ -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 tell the reader
307
- the operation is unavailable for this object instead of offering a retry. Read
308
- it from `SfaApiError.status`.
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
- Two baseline components for resilient pages:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "React hooks and components for building pages that connect to GENIEE SFA/CRM data.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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';
@@ -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 batch without it, and kept next to the
18
- * check that enforces it so the two cannot disagree.
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 batch without it, and kept next to the
36
- * check that enforces it so the two cannot disagree.
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: the API rejects the whole call with 422 when the array is empty or
864
- * longer than the limit, so a caller that over-sends loses every row. Saying
865
- * so here names the limit instead of surfacing a generic validation failure.
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 Error('A bulk write needs at least one row.');
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.
@@ -1 +1 @@
1
- export declare const SFA_KIT_VERSION = "1.12.0";
1
+ export declare const SFA_KIT_VERSION = "1.13.0";
@@ -1,3 +1,3 @@
1
1
  // このファイルは libs/sfa-kit/scripts/sync-version.cjs が生成する。
2
2
  // 手で編集しないこと。バージョンを上げるときは libs/sfa-kit/package.json を変更する。
3
- export const SFA_KIT_VERSION = '1.12.0';
3
+ export const SFA_KIT_VERSION = '1.13.0';