@geniee-inc/sfa-kit 1.19.0 → 1.19.2

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
@@ -287,7 +287,7 @@ const result = useRecordList('companies', {
287
287
 
288
288
  Disabling the hook, or clearing `collection`, discards any request still in flight: a response that lands after the caller stopped asking never becomes the result, and the `'loading'` a discarded refetch set does not outlive it — the hook goes back to how the query last ended.
289
289
 
290
- Returns `{ items, totalCount, hasMore, pageIndex, status, isLoading, error, refetch, goToPage }`.
290
+ Returns `{ items, totalCount, hasMore, pageIndex, status, isLoading, error, refetch, goToPage }`. `totalCount` is `null` when it is not known: the backend does not report it — Keystone pages with cursors and returns no total — or no fetch has succeeded since the query last changed (an earlier answer to the same query does not count). Read it together with `status`, and use `hasMore` to tell whether another page exists.
291
291
 
292
292
  For cross-object queries (fetching child records of a parent), prefer `useRelatedRecords` which builds the filter automatically from describe metadata.
293
293
 
@@ -361,7 +361,7 @@ const { items, isLoading, error } = useRelatedRecords(
361
361
 
362
362
  Check `status` for **every** hook on the page, not just the first one. Each related-records hook fails independently: with several fetches firing on mount, one can hit a rate limit or a timeout while the others succeed, and the failed one renders as an empty section unless its own status is read.
363
363
 
364
- Returns `{ items, totalCount, hasMore, pageIndex, status, goToPage, isLoading, error, refetch, relationship }`.
364
+ Returns `{ items, totalCount, hasMore, pageIndex, status, goToPage, isLoading, error, refetch, relationship }`. As in `useRecordList`, `totalCount` is `null` when it is not known.
365
365
 
366
366
  ### Bulk writes
367
367
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
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",
@@ -63,16 +63,13 @@ export function DevToolbar({ organizationId, preview, level, objectKey, onLogout
63
63
  };
64
64
  }, [client, hasObjectKey]);
65
65
  const [localRecordId, setLocalRecordId] = useState((_b = (_a = preview.state) === null || _a === void 0 ? void 0 : _a.record_id) !== null && _b !== void 0 ? _b : '');
66
- // NOTE: フォームは record レベル かつ objectKey があるときだけ描くため、
67
- // ここに来る時点で objectKey はある。型を絞るためのガードとして残す
66
+ // NOTE: フォームは record レベル かつ objectKey があるときだけ描く。対象の
67
+ // オブジェクトは StandaloneFlow が objectKey から決めるので、ここではレコード ID だけを渡す
68
68
  const handleSubmit = useCallback((e) => {
69
69
  e.preventDefault();
70
- if (!hasObjectKey || objectKey == null)
71
- return;
72
- preview.setObjectKey(objectKey);
73
70
  preview.setRecordId(localRecordId);
74
71
  preview.triggerFetch();
75
- }, [hasObjectKey, objectKey, localRecordId, preview]);
72
+ }, [localRecordId, preview]);
76
73
  // NOTE: カタログ取得前は key をそのまま表示名に使う。objectKey が無いページは
77
74
  // undefined のままにして、表示側で出し分ける
78
75
  const objectLabel = hasObjectKey
@@ -166,22 +166,31 @@ function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, chil
166
166
  // NOTE: level を渡さない既存ページのために、objectKey の有無から補う。
167
167
  // 以前は objectKey があること自体が「record レベル」を意味していた
168
168
  const resolvedLevel = level !== null && level !== void 0 ? level : (objectKey != null && objectKey !== '' ? 'record' : 'object');
169
- // Preview context state managed by DevToolbar.
170
- // When objectKey is provided, pre-populate it.
171
- const [previewObjectKey, setPreviewObjectKey] = useState(objectKey !== null && objectKey !== void 0 ? objectKey : '');
169
+ // Preview context state managed by DevToolbar. The object comes straight
170
+ // from the objectKey prop: nothing in the toolbar changes it.
172
171
  const [previewRecordId, setPreviewRecordId] = useState('');
172
+ // NOTE: a record id belongs to the object it was submitted for. objectKey can
173
+ // change without remounting this flow, so forget the id whenever it does —
174
+ // also when it changes back, or the old id would come back with it. This is
175
+ // React's "adjust state when a prop changes" pattern: it runs during render,
176
+ // so no frame pairs the new object with the old id
177
+ const [recordIdObjectKey, setRecordIdObjectKey] = useState(objectKey);
178
+ if (recordIdObjectKey !== objectKey) {
179
+ setRecordIdObjectKey(objectKey);
180
+ setPreviewRecordId('');
181
+ }
173
182
  const [fetchGeneration, setFetchGeneration] = useState(0);
174
183
  const triggerFetch = useCallback(() => {
175
184
  setFetchGeneration(prev => prev + 1);
176
185
  }, []);
177
186
  const previewControls = useMemo(() => ({
178
- state: previewObjectKey !== ''
179
- ? Object.assign({ object_key: previewObjectKey }, (previewRecordId !== '' ? { record_id: previewRecordId } : {})) : null,
180
- setObjectKey: setPreviewObjectKey,
187
+ state: objectKey == null || objectKey === ''
188
+ ? null
189
+ : Object.assign({ object_key: objectKey }, (previewRecordId === '' ? {} : { record_id: previewRecordId })),
181
190
  setRecordId: setPreviewRecordId,
182
191
  triggerFetch,
183
192
  fetchGeneration,
184
- }), [previewObjectKey, previewRecordId, triggerFetch, fetchGeneration]);
193
+ }), [objectKey, previewRecordId, triggerFetch, fetchGeneration]);
185
194
  const completeLogin = useCallback((newSession, normalizedBase) => {
186
195
  const sessionWithBase = Object.assign(Object.assign({}, newSession), { apiBase: normalizedBase });
187
196
  saveSession(sessionWithBase);
@@ -242,7 +251,7 @@ function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, chil
242
251
  if (!session || !auth) {
243
252
  return (_jsx(LoginForm, { defaultApiBase: apiBase, defaultOrganizationId: defaultOrganizationId, onSubmit: handleLogin, status: status, error: error }));
244
253
  }
245
- return (_jsx(SfaProvider, { apiBase: activeBase, auth: auth, mode: "direct", organizationId: session.organizationId, onAuthError: handleLogout, children: _jsxs(StandalonePreviewCtx.Provider, { value: previewControls, children: [_jsx(DevToolbar, { organizationId: session.organizationId, preview: previewControls, objectKey: objectKey, level: resolvedLevel, onLogout: handleLogout }), objectKey ? (_jsx(ObjectKeyGuard, { objectKey: objectKey, children: children })) : (children)] }) }));
254
+ return (_jsx(SfaProvider, { apiBase: activeBase, auth: auth, mode: "direct", organizationId: session.organizationId, onAuthError: handleLogout, children: _jsxs(StandalonePreviewCtx.Provider, { value: previewControls, children: [_jsx(DevToolbar, { organizationId: session.organizationId, preview: previewControls, objectKey: objectKey, level: resolvedLevel, onLogout: handleLogout }, objectKey), objectKey ? (_jsx(ObjectKeyGuard, { objectKey: objectKey, children: children })) : (children)] }) }));
246
255
  }
247
256
  // ---------------------------------------------------------------------------
248
257
  // GENIEE SFA/CRM-branded login form
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @description State managed by the DevToolbar in standalone (non-embedded) mode.
2
+ * @description Preview state in standalone (non-embedded) mode. `object_key`
3
+ * is SfaLoginGate's `objectKey`; `record_id` is what the DevToolbar submits.
3
4
  * Mirrors the shape of EmbedContext so useEmbedContext() can fall back to it.
4
5
  */
5
6
  export interface StandalonePreviewState {
@@ -9,7 +10,6 @@ export interface StandalonePreviewState {
9
10
  }
10
11
  export interface StandalonePreviewControls {
11
12
  readonly state: StandalonePreviewState | null;
12
- readonly setObjectKey: (key: string) => void;
13
13
  readonly setRecordId: (id: string) => void;
14
14
  readonly triggerFetch: () => void;
15
15
  /** Increments on each triggerFetch call so consumers can react. */
@@ -17,7 +17,7 @@ export interface StandalonePreviewControls {
17
17
  }
18
18
  /**
19
19
  * @description React context carrying standalone preview state.
20
- * Populated only in standalone mode by StandaloneFlow's DevToolbar.
20
+ * Populated only in standalone mode by StandaloneFlow, for its DevToolbar.
21
21
  * useEmbedContext() checks this as a fallback when EmbedContext is null.
22
22
  */
23
23
  export declare const StandalonePreviewCtx: import("react").Context<StandalonePreviewControls | null>;
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from 'react';
2
2
  /**
3
3
  * @description React context carrying standalone preview state.
4
- * Populated only in standalone mode by StandaloneFlow's DevToolbar.
4
+ * Populated only in standalone mode by StandaloneFlow, for its DevToolbar.
5
5
  * useEmbedContext() checks this as a fallback when EmbedContext is null.
6
6
  */
7
7
  export const StandalonePreviewCtx = createContext(null);
@@ -10,7 +10,9 @@ export declare const EmbedContextValue: import("react").Context<EmbedContext | n
10
10
  * active runtime environment. Checks two sources in priority order:
11
11
  *
12
12
  * 1. Embed context (iframe postMessage flow) - set by the SFA/CRM host.
13
- * 2. Standalone preview context (DevToolbar) - set by the user in preview mode.
13
+ * 2. Standalone preview context - set by StandaloneFlow: `object_key` is
14
+ * SfaLoginGate's `objectKey`, `record_id` is what the user submits in the
15
+ * DevToolbar. It is null when no `objectKey` is passed.
14
16
  *
15
17
  * Returns null only when neither context is available.
16
18
  *
@@ -11,7 +11,9 @@ export const EmbedContextValue = createContext(null);
11
11
  * active runtime environment. Checks two sources in priority order:
12
12
  *
13
13
  * 1. Embed context (iframe postMessage flow) - set by the SFA/CRM host.
14
- * 2. Standalone preview context (DevToolbar) - set by the user in preview mode.
14
+ * 2. Standalone preview context - set by StandaloneFlow: `object_key` is
15
+ * SfaLoginGate's `objectKey`, `record_id` is what the user submits in the
16
+ * DevToolbar. It is null when no `objectKey` is passed.
15
17
  *
16
18
  * Returns null only when neither context is available.
17
19
  *
@@ -37,10 +37,28 @@ export function RecordList({ collection, columns, params, pageSize = 20, showPag
37
37
  return (_jsxs("div", { style: styles.container, children: [_jsx(EmptyState, { message: "\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" }), _jsxs("div", { style: styles.versionFooter, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }));
38
38
  }
39
39
  const resolvedColumns = columns !== null && columns !== void 0 ? columns : autoDetectColumns(items);
40
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
40
+ // NOTE: totalCount is null when it is not known — the backend reports no
41
+ // total (Keystone), or the count is not in yet. Then show the page number and
42
+ // the range on screen without a total
41
43
  const rangeStart = pageIndex * pageSize + 1;
42
- const rangeEnd = Math.min((pageIndex + 1) * pageSize, totalCount);
43
- return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: styles.collectionName, children: collection }), !isLoading && _jsxs("span", { style: styles.count, children: [totalCount, " records"] })] }), _jsx("div", { style: styles.tableWrapper, children: _jsxs("table", { style: styles.table, children: [_jsx("thead", { children: _jsx("tr", { children: resolvedColumns.map(col => {
44
+ const rangeEnd = totalCount == null
45
+ ? pageIndex * pageSize + items.length
46
+ : Math.min((pageIndex + 1) * pageSize, totalCount);
47
+ const pageLabel = totalCount == null
48
+ ? `Page ${pageIndex + 1}`
49
+ : `Page ${pageIndex + 1} of ${Math.max(1, Math.ceil(totalCount / pageSize))}`;
50
+ const rangeLabel = totalCount == null
51
+ ? `Showing ${rangeStart}-${rangeEnd}`
52
+ : `Showing ${rangeStart}-${rangeEnd} of ${totalCount}`;
53
+ // NOTE: a later page that came back empty (records removed between page
54
+ // turns) keeps the pager so Prev can lead back, also while that page turn
55
+ // loads. No page position is stated there — see hasRecordsOnScreen
56
+ const hasPages = (totalCount !== null && totalCount !== void 0 ? totalCount : items.length) > 0 || pageIndex > 0;
57
+ // NOTE: with no records on screen there is no range or page position to state.
58
+ // While loading the table shows the loading row instead of the items, and
59
+ // pageIndex already names the page being fetched
60
+ const hasRecordsOnScreen = !isLoading && items.length > 0;
61
+ return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: styles.collectionName, children: collection }), !isLoading && totalCount != null && (_jsxs("span", { style: styles.count, children: [totalCount, " records"] }))] }), _jsx("div", { style: styles.tableWrapper, children: _jsxs("table", { style: styles.table, children: [_jsx("thead", { children: _jsx("tr", { children: resolvedColumns.map(col => {
44
62
  var _a;
45
63
  return (_jsx("th", { style: Object.assign(Object.assign({}, styles.th), { width: col.width }), children: (_a = col.header) !== null && _a !== void 0 ? _a : col.fieldName }, col.fieldName));
46
64
  }) }) }), _jsxs("tbody", { children: [isLoading && (_jsx("tr", { children: _jsx("td", { colSpan: resolvedColumns.length, style: styles.loadingCell, children: "Loading..." }) })), !isLoading && items.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: resolvedColumns.length, style: styles.emptyCell, children: "No records found" }) })), !isLoading &&
@@ -48,7 +66,7 @@ export function RecordList({ collection, columns, params, pageSize = 20, showPag
48
66
  items.map(record => (_jsx("tr", { style: onRowClick ? styles.clickableRow : styles.row, onClick: onRowClick ? () => onRowClick(record) : undefined, children: resolvedColumns.map(col => {
49
67
  const fieldValue = record.fields[col.fieldName];
50
68
  return (_jsx("td", { style: styles.td, children: fieldValue ? (_jsx(FieldRenderer, { fieldValue: fieldValue, fieldName: col.fieldName })) : (_jsx("span", { style: styles.emptyValue, children: "\u2014" })) }, col.fieldName));
51
- }) }, record.record_id)))] })] }) }), showPagination && totalCount > 0 && (_jsxs("div", { style: styles.pagination, children: [_jsx("button", { style: styles.pageButton, disabled: pageIndex === 0 || isLoading, onClick: () => goToPage(pageIndex - 1), type: "button", children: "Prev" }), _jsxs("span", { style: styles.pageInfo, children: ["Page ", pageIndex + 1, " of ", totalPages] }), _jsx("button", { style: styles.pageButton, disabled: !hasMore || isLoading, onClick: () => goToPage(pageIndex + 1), type: "button", children: "Next" }), _jsxs("span", { style: styles.rangeInfo, children: ["Showing ", rangeStart, "-", rangeEnd, " of ", totalCount] })] })), _jsxs("div", { style: styles.versionFooter, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }));
69
+ }) }, record.record_id)))] })] }) }), showPagination && hasPages && (_jsxs("div", { style: styles.pagination, children: [_jsx("button", { style: styles.pageButton, disabled: pageIndex === 0 || isLoading, onClick: () => goToPage(pageIndex - 1), type: "button", children: "Prev" }), hasRecordsOnScreen && (_jsx("span", { style: styles.pageInfo, children: pageLabel })), _jsx("button", { style: styles.pageButton, disabled: !hasMore || isLoading, onClick: () => goToPage(pageIndex + 1), type: "button", children: "Next" }), hasRecordsOnScreen && (_jsx("span", { style: styles.rangeInfo, children: rangeLabel }))] })), _jsxs("div", { style: styles.versionFooter, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }));
52
70
  }
53
71
  function autoDetectColumns(items) {
54
72
  const first = items[0];
@@ -26,7 +26,13 @@ interface UseRecordListResult {
26
26
  * it without checking `status` turns a failed fetch into a confident zero.
27
27
  */
28
28
  readonly items: readonly SfaRecord[];
29
- readonly totalCount: number;
29
+ /**
30
+ * @description Records across all pages, or null when not known: the backend
31
+ * does not report it (Keystone), or no fetch has succeeded since the query
32
+ * last changed — an earlier answer to the same query does not count. Like
33
+ * `items`, read it together with `status`
34
+ */
35
+ readonly totalCount: number | null;
30
36
  readonly hasMore: boolean;
31
37
  readonly pageIndex: number;
32
38
  readonly status: SfaListStatus;
@@ -14,7 +14,7 @@ export function useRecordList(collection, params, options) {
14
14
  const listId = (_a = params === null || params === void 0 ? void 0 : params.listId) !== null && _a !== void 0 ? _a : embedCtx === null || embedCtx === void 0 ? void 0 : embedCtx.list_id;
15
15
  const limit = (_b = params === null || params === void 0 ? void 0 : params.limit) !== null && _b !== void 0 ? _b : 20;
16
16
  const [items, setItems] = useState([]);
17
- const [totalCount, setTotalCount] = useState(0);
17
+ const [totalCount, setTotalCount] = useState(null);
18
18
  const [hasMore, setHasMore] = useState(false);
19
19
  /**
20
20
  * Where the caller asked the listing to start.
@@ -161,7 +161,7 @@ export function useRecordList(collection, params, options) {
161
161
  // would show one record's children — or one organization's data —
162
162
  // under another, so drop them and let status report them unknown.
163
163
  setItems([]);
164
- setTotalCount(0);
164
+ setTotalCount(null);
165
165
  setHasMore(false);
166
166
  loadedRef.current = null;
167
167
  }
@@ -245,7 +245,7 @@ export function useRecordList(collection, params, options) {
245
245
  ? { items, totalCount, hasMore, status, error }
246
246
  : {
247
247
  items: NO_ITEMS,
248
- totalCount: 0,
248
+ totalCount: null,
249
249
  hasMore: false,
250
250
  status: willFetch ? 'loading' : 'idle',
251
251
  error: null,
@@ -22,7 +22,11 @@ export interface UseRelatedRecordsResult {
22
22
  * only meaningful when `status` is `'success'` — see useRecordList.
23
23
  */
24
24
  readonly items: readonly SfaRecord[];
25
- readonly totalCount: number;
25
+ /**
26
+ * @description Records across all pages, or null when not known — see
27
+ * useRecordList
28
+ */
29
+ readonly totalCount: number | null;
26
30
  readonly hasMore: boolean;
27
31
  readonly pageIndex: number;
28
32
  /**
@@ -132,7 +132,7 @@ export class SfaClient {
132
132
  }
133
133
  const path = `/v1/objects/${enc(collection)}/records/query`;
134
134
  const raw = yield this.postJson(path, body);
135
- return normalizeKeystoneListResponse(raw, pageIndex, pageSize);
135
+ return normalizeKeystoneListResponse(raw, pageIndex);
136
136
  }
137
137
  const query = new URLSearchParams();
138
138
  if ((params === null || params === void 0 ? void 0 : params.limit) != null)
@@ -660,7 +660,7 @@ function inferScalarKind(value) {
660
660
  }
661
661
  return 'text';
662
662
  }
663
- function normalizeKeystoneListResponse(raw, pageIndex, pageSize) {
663
+ function normalizeKeystoneListResponse(raw, pageIndex) {
664
664
  var _a, _b, _c;
665
665
  const data = raw;
666
666
  const rawRecords = (_a = data['records']) !== null && _a !== void 0 ? _a : [];
@@ -668,12 +668,17 @@ function normalizeKeystoneListResponse(raw, pageIndex, pageSize) {
668
668
  const nextCursor = (_b = data['next_cursor']) !== null && _b !== void 0 ? _b : null;
669
669
  const prevCursor = (_c = data['prev_cursor']) !== null && _c !== void 0 ? _c : null;
670
670
  const hasMore = nextCursor != null;
671
- // Keystone uses cursor pagination without a total count.
672
- // Estimate a totalCount that keeps the hasMore logic correct.
673
- const totalCount = hasMore
674
- ? (pageIndex + 1) * pageSize + 1
675
- : pageIndex * pageSize + items.length;
676
- return { items, totalCount, hasMore, pageIndex, nextCursor, prevCursor };
671
+ // NOTE: Keystone pages with cursors and returns no total count. Report it as
672
+ // unknown rather than estimating one: an estimate reads as the real total
673
+ // ("21 records" for 500). Whether another page exists comes from hasMore
674
+ return {
675
+ items,
676
+ totalCount: null,
677
+ hasMore,
678
+ pageIndex,
679
+ nextCursor,
680
+ prevCursor,
681
+ };
677
682
  }
678
683
  // ── Legacy Chikyu normalization (proxy mode) ──
679
684
  const CHIKYU_KIND_MAP = {
@@ -53,7 +53,13 @@ export interface SfaRecord {
53
53
  }
54
54
  export interface SfaListResponse {
55
55
  readonly items: readonly SfaRecord[];
56
- readonly totalCount: number;
56
+ /**
57
+ * Records across all pages, or null when the backend does not report it.
58
+ *
59
+ * NOTE: Keystone pages with cursors and returns no total, so it is null in
60
+ * direct mode. Use `hasMore` to tell whether another page exists
61
+ */
62
+ readonly totalCount: number | null;
57
63
  readonly hasMore: boolean;
58
64
  readonly pageIndex: number;
59
65
  /** Cursor for fetching the next page (keystone cursor-based pagination). */
@@ -1 +1 @@
1
- export declare const SFA_KIT_VERSION = "1.19.0";
1
+ export declare const SFA_KIT_VERSION = "1.19.2";
@@ -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.19.0';
3
+ export const SFA_KIT_VERSION = '1.19.2';