@geniee-inc/sfa-kit 1.6.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 +585 -0
- package/LICENSE.md +21 -0
- package/README.md +31 -0
- package/package.json +36 -0
- package/src/index.d.ts +29 -0
- package/src/index.js +21 -0
- package/src/lib/auth/DevToolbar.d.ts +38 -0
- package/src/lib/auth/DevToolbar.js +271 -0
- package/src/lib/auth/SfaLoginGate.d.ts +53 -0
- package/src/lib/auth/SfaLoginGate.js +462 -0
- package/src/lib/auth/StandalonePreviewContext.d.ts +27 -0
- package/src/lib/auth/StandalonePreviewContext.js +14 -0
- package/src/lib/auth/embed-protocol.d.ts +40 -0
- package/src/lib/auth/embed-protocol.js +56 -0
- package/src/lib/auth/exchange-code.d.ts +14 -0
- package/src/lib/auth/exchange-code.js +45 -0
- package/src/lib/auth/keystone-auth.d.ts +16 -0
- package/src/lib/auth/keystone-auth.js +84 -0
- package/src/lib/auth/session-store.d.ts +19 -0
- package/src/lib/auth/session-store.js +73 -0
- package/src/lib/auth/types.d.ts +33 -0
- package/src/lib/auth/types.js +1 -0
- package/src/lib/auth/useEmbedContext.d.ts +22 -0
- package/src/lib/auth/useEmbedContext.js +31 -0
- package/src/lib/components/EmptyState.d.ts +18 -0
- package/src/lib/components/EmptyState.js +36 -0
- package/src/lib/components/ErrorBoundary.d.ts +31 -0
- package/src/lib/components/ErrorBoundary.js +100 -0
- package/src/lib/components/FieldRenderer.d.ts +11 -0
- package/src/lib/components/FieldRenderer.js +57 -0
- package/src/lib/components/RecordList.d.ts +20 -0
- package/src/lib/components/RecordList.js +139 -0
- package/src/lib/components/RecordView.d.ts +14 -0
- package/src/lib/components/RecordView.js +153 -0
- package/src/lib/errors.d.ts +15 -0
- package/src/lib/errors.js +15 -0
- package/src/lib/hooks/useCreateRecord.d.ts +17 -0
- package/src/lib/hooks/useCreateRecord.js +48 -0
- package/src/lib/hooks/useDeleteRecord.d.ts +16 -0
- package/src/lib/hooks/useDeleteRecord.js +48 -0
- package/src/lib/hooks/useDescribe.d.ts +16 -0
- package/src/lib/hooks/useDescribe.js +40 -0
- package/src/lib/hooks/useFieldTypes.d.ts +15 -0
- package/src/lib/hooks/useFieldTypes.js +39 -0
- package/src/lib/hooks/useObjectCatalog.d.ts +15 -0
- package/src/lib/hooks/useObjectCatalog.js +39 -0
- package/src/lib/hooks/useRecord.d.ts +15 -0
- package/src/lib/hooks/useRecord.js +49 -0
- package/src/lib/hooks/useRecordList.d.ts +19 -0
- package/src/lib/hooks/useRecordList.js +82 -0
- package/src/lib/hooks/useRelatedRecords.d.ts +65 -0
- package/src/lib/hooks/useRelatedRecords.js +227 -0
- package/src/lib/hooks/useUpdateRecord.d.ts +23 -0
- package/src/lib/hooks/useUpdateRecord.js +55 -0
- package/src/lib/provider/SfaClient.d.ts +98 -0
- package/src/lib/provider/SfaClient.js +688 -0
- package/src/lib/provider/SfaProvider.d.ts +38 -0
- package/src/lib/provider/SfaProvider.js +44 -0
- package/src/lib/provider/types.d.ts +184 -0
- package/src/lib/provider/types.js +1 -0
- package/src/lib/provider/useSfaClient.d.ts +11 -0
- package/src/lib/provider/useSfaClient.js +24 -0
- package/src/lib/theme.d.ts +55 -0
- package/src/lib/theme.js +68 -0
- package/src/lib/version.d.ts +1 -0
- package/src/lib/version.js +1 -0
package/GUIDE.md
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# @geniee-inc/sfa-kit
|
|
2
|
+
|
|
3
|
+
React hooks and components for building pages that connect to GENIEE SFA/CRM data. The kit handles authentication and data fetching — you compose layouts freely using the hooks or optional convenience wrappers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @geniee-inc/sfa-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No registry configuration is required — the package is published publicly on the npm registry. `react` and `react-dom` (18 or 19) are peer dependencies.
|
|
12
|
+
|
|
13
|
+
The package ships built JavaScript and TypeScript declarations only. It contains no credentials: every API call requires a Keystone JWT obtained at runtime, so the package on its own cannot reach any data.
|
|
14
|
+
|
|
15
|
+
### Reading the installed version
|
|
16
|
+
|
|
17
|
+
`SFA_KIT_VERSION` is not a public export. The installed version is read from the package manifest, which is exposed as a subpath for exactly this purpose:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import pkg from '@geniee-inc/sfa-kit/package.json';
|
|
21
|
+
// pkg.version -> '1.6.0'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No other subpath resolves: `exports` deliberately exposes only the root entry and `./package.json`, so internal modules cannot be imported directly.
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
The kit is **hooks-first**. It provides auth, data fetching, and metadata discovery. The AI (or developer) generates the UI directly — deciding layout, field arrangement, and styling. Pre-built components exist as convenience wrappers but are not required.
|
|
29
|
+
|
|
30
|
+
## Auth
|
|
31
|
+
|
|
32
|
+
Every page uses `SfaLoginGate` as the outermost wrapper. It auto-detects the hosting context and chooses the right auth flow:
|
|
33
|
+
|
|
34
|
+
| Context | What happens | How |
|
|
35
|
+
| --------------------------- | ------------------------------------- | ------------------------------------------------------------------------------- |
|
|
36
|
+
| Standalone (Studio preview) | Login form + DevToolbar appear | Page is not in an iframe, or no embed context detected |
|
|
37
|
+
| Embedded in SFA (iframe) | Seamless code exchange, no login form | Page is in an iframe with `trustedOrigin` prop or content token (`_t`) detected |
|
|
38
|
+
|
|
39
|
+
### Props
|
|
40
|
+
|
|
41
|
+
- **`apiBase`** (optional) — Overrides the default API endpoint (`https://keystone-api.chikyu.mobi`).
|
|
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
|
+
- **`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
|
+
- **`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.
|
|
45
|
+
- **`level`** (optional) — `"record"` or `"object"`: which Chikyu tab slot this page is for. Only `"record"` gets a record ID input in the standalone DevToolbar. Defaults to `"record"` when `objectKey` is set and `"object"` otherwise, so pages written before this prop keep working — but **an object-level page that passes `objectKey` must set `level="object"` explicitly**, or it gets a record ID input it cannot use.
|
|
46
|
+
|
|
47
|
+
### Standalone mode (no `trustedOrigin`)
|
|
48
|
+
|
|
49
|
+
Shows a GENIEE SFA/CRM-branded login form. Org is auto-selected via `defaultOrganizationId` (or the first available org). After login, a DevToolbar appears at the top; on record-level pages it carries a record ID input and a fetch button, and at both levels it shows the object, org ID and sfa-kit version behind the ページ情報 tooltip. The toolbar only renders in standalone mode and will not appear when embedded.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
'use client';
|
|
53
|
+
|
|
54
|
+
import { SfaLoginGate } from '@geniee-inc/sfa-kit';
|
|
55
|
+
|
|
56
|
+
export default function Page() {
|
|
57
|
+
return (
|
|
58
|
+
<SfaLoginGate
|
|
59
|
+
defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}
|
|
60
|
+
objectKey="companies"
|
|
61
|
+
level="record">
|
|
62
|
+
{/* hooks and components go here */}
|
|
63
|
+
</SfaLoginGate>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Every component and hook in this package is a client component: they use `useState`, `useEffect` and context. The package does not emit a `'use client'` directive of its own, so **any file that imports from `@geniee-inc/sfa-kit` must declare `'use client'` itself** when it lives in a React Server Components tree (Next.js App Router, for example). Omitting it fails at build time with "You're importing a component that needs `useState`".
|
|
69
|
+
|
|
70
|
+
### Embedded mode (`trustedOrigin` + iframe)
|
|
71
|
+
|
|
72
|
+
No login form. The host sends a single-use exchange code via `postMessage`, `SfaLoginGate` exchanges it for a JWT, and proactively refreshes the token before expiry. The same page code works in both modes — `trustedOrigin` is injected at serve time, so no code changes are needed between preview and production.
|
|
73
|
+
|
|
74
|
+
**postMessage protocol:**
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
iframe -> host : { type: "sfa-kit:ready" } after mount
|
|
78
|
+
host -> iframe: { type: "sfa-kit:code", code } single-use exchange code
|
|
79
|
+
iframe -> host : { type: "sfa-kit:code-request" } pre-expiry refresh / 401 fallback
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### SfaProvider (advanced)
|
|
83
|
+
|
|
84
|
+
For cases where the host app manages auth entirely and passes a token directly as a prop. Most pages should use `SfaLoginGate` instead.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { SfaProvider } from '@geniee-inc/sfa-kit';
|
|
88
|
+
|
|
89
|
+
<SfaProvider
|
|
90
|
+
apiBase="..."
|
|
91
|
+
auth={{ accessToken: jwt }}
|
|
92
|
+
mode="direct"
|
|
93
|
+
organizationId={organizationId}>
|
|
94
|
+
{/* hooks and components go here */}
|
|
95
|
+
</SfaProvider>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Never build a custom login form.** `SfaLoginGate` is the only auth entry point. It handles login, org resolution, dev email formats, and iframe code exchange. Custom forms will break.
|
|
99
|
+
|
|
100
|
+
## Embed context
|
|
101
|
+
|
|
102
|
+
Pages use `useEmbedContext()` to read the current object and record from the hosting environment. In embedded mode, the host provides these via postMessage. In standalone mode, the DevToolbar provides them.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { useEmbedContext, useRecord } from '@geniee-inc/sfa-kit';
|
|
106
|
+
|
|
107
|
+
function CompanyDetail() {
|
|
108
|
+
const ctx = useEmbedContext();
|
|
109
|
+
const { record, isLoading } = useRecord(
|
|
110
|
+
ctx?.object_key ?? '',
|
|
111
|
+
ctx?.record_id ?? ''
|
|
112
|
+
);
|
|
113
|
+
// ...
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Returns `{ object_key: string, record_id?: string } | null`.
|
|
118
|
+
|
|
119
|
+
## Hooks
|
|
120
|
+
|
|
121
|
+
The primary way to access SFA/CRM data. Must be inside the auth wrapper.
|
|
122
|
+
|
|
123
|
+
### useRecord
|
|
124
|
+
|
|
125
|
+
Fetches a single record.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { useRecord } from '@geniee-inc/sfa-kit';
|
|
129
|
+
|
|
130
|
+
const { record, isLoading, error, refetch } = useRecord('companies', 'abc123');
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Returns `{ record: SfaRecord | null, isLoading, error, refetch }`.
|
|
134
|
+
|
|
135
|
+
### useRecordList
|
|
136
|
+
|
|
137
|
+
Fetches a paginated list of records. Supports cursor-based pagination (Keystone API).
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { useRecordList } from '@geniee-inc/sfa-kit';
|
|
141
|
+
|
|
142
|
+
const { items, totalCount, hasMore, pageIndex, goToPage, isLoading, error } =
|
|
143
|
+
useRecordList('companies');
|
|
144
|
+
|
|
145
|
+
// With params
|
|
146
|
+
const result = useRecordList('companies', {
|
|
147
|
+
limit: 10,
|
|
148
|
+
sortBy: 'company_name',
|
|
149
|
+
sortOrder: 'asc',
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Filters** must use react-querybuilder shape with `combinator` and `rules`. The backend rejects any other format with a 422.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
const result = useRecordList('companies', {
|
|
157
|
+
filter: {
|
|
158
|
+
combinator: 'and',
|
|
159
|
+
rules: [
|
|
160
|
+
{ field: 'industry', operator: 'eq', value: 'Manufacturing' },
|
|
161
|
+
{ field: 'annual_revenue', operator: 'gte', value: 1000000 },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
For cross-object queries (fetching child records of a parent), prefer `useRelatedRecords` which builds the filter automatically from describe metadata.
|
|
168
|
+
|
|
169
|
+
### useRelatedRecords
|
|
170
|
+
|
|
171
|
+
Fetches child records scoped to a parent by resolving the join automatically from describe metadata. The caller names the parent and target collections; the hook discovers which foreign key to filter on.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { useRelatedRecords } from '@geniee-inc/sfa-kit';
|
|
175
|
+
|
|
176
|
+
// Fetch opportunities belonging to a company
|
|
177
|
+
const { items, isLoading, error, relationship } = useRelatedRecords(
|
|
178
|
+
'companies', // parent collection
|
|
179
|
+
'comp-1', // parent record id
|
|
180
|
+
'opportunities' // target collection
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Internally, the hook calls `useDescribe` on the parent to find the relationship's `viaField`, builds a filter (`eq` for a single id, `in` for an array), and delegates to `useRecordList` with cursor pagination.
|
|
185
|
+
|
|
186
|
+
**Grandchild pattern** — for deeper traversals (e.g., company → negotiations → activity histories), compose two calls. The second call takes the child IDs returned by the first call and fetches their grandchildren in a single `in` query, no N+1. Because `childIds` comes from the first call's paginated `items`, this fetches grandchildren for the currently loaded page only:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const { items: negotiations } = useRelatedRecords(
|
|
190
|
+
'companies',
|
|
191
|
+
companyId,
|
|
192
|
+
'business_discussions'
|
|
193
|
+
);
|
|
194
|
+
const childIds = negotiations.map(r => r.record_id);
|
|
195
|
+
const { items: activities } = useRelatedRecords(
|
|
196
|
+
'business_discussions',
|
|
197
|
+
childIds, // string[] triggers "in" operator
|
|
198
|
+
'activity_histories',
|
|
199
|
+
{ limit: 50, sortBy: 'activity_date', sortOrder: 'desc' }
|
|
200
|
+
);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Params:** `{ limit?, sortBy?, sortOrder?, additionalFilter? }`. The `additionalFilter` is merged alongside the auto-built relationship filter. It is added as a single element of the `rules` array, so pass either one rule object or a nested react-querybuilder group with `{ combinator, rules }`:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
// Single extra rule
|
|
207
|
+
useRelatedRecords('companies', 'comp-1', 'opportunities', {
|
|
208
|
+
additionalFilter: { field: 'status', operator: 'eq', value: 'open' },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Nested rule group
|
|
212
|
+
useRelatedRecords('companies', 'comp-1', 'opportunities', {
|
|
213
|
+
additionalFilter: {
|
|
214
|
+
combinator: 'and',
|
|
215
|
+
rules: [
|
|
216
|
+
{ field: 'status', operator: 'eq', value: 'open' },
|
|
217
|
+
{ field: 'annual_revenue', operator: 'gte', value: 1000000 },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Options:** `{ enabled?: boolean }`. Set `enabled: false` to keep the hook from fetching until you are ready:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
const { items, isLoading, error } = useRelatedRecords(
|
|
227
|
+
'companies',
|
|
228
|
+
'comp-1',
|
|
229
|
+
'opportunities',
|
|
230
|
+
{}, // params
|
|
231
|
+
{ enabled: false }
|
|
232
|
+
);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Error handling:** If the relationship doesn't exist in the parent's metadata, the hook returns a `RelationNotFoundError` with an `availableTargets` list so the caller can self-correct.
|
|
236
|
+
|
|
237
|
+
Returns `{ items, totalCount, hasMore, pageIndex, goToPage, isLoading, error, refetch, relationship }`.
|
|
238
|
+
|
|
239
|
+
### useDescribe
|
|
240
|
+
|
|
241
|
+
Discovers object schema — fields, types, relationships, permissions. Use this to build dynamic UIs that adapt to any object.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { useDescribe } from '@geniee-inc/sfa-kit';
|
|
245
|
+
|
|
246
|
+
const { object, isLoading, error } = useDescribe('companies');
|
|
247
|
+
|
|
248
|
+
// object.fields — field definitions (key, label, kind, required, editable, choices)
|
|
249
|
+
// object.relationships — relationships to other objects
|
|
250
|
+
// object.permissions — { readable, creatable, updatable, deletable }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### useObjectCatalog
|
|
254
|
+
|
|
255
|
+
Lists all objects available in the org.
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { useObjectCatalog } from '@geniee-inc/sfa-kit';
|
|
259
|
+
|
|
260
|
+
const { objects, isLoading } = useObjectCatalog();
|
|
261
|
+
// objects: [{ key, label, origin, primaryDisplayField, relationships }]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### useSfaClient
|
|
265
|
+
|
|
266
|
+
Returns the raw `SfaClient` instance for advanced use cases not covered by the other hooks.
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
import { useSfaClient } from '@geniee-inc/sfa-kit';
|
|
270
|
+
|
|
271
|
+
const client = useSfaClient();
|
|
272
|
+
const record = await client.get('companies', 'abc123');
|
|
273
|
+
const list = await client.list('companies', { limit: 10 });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Record shape
|
|
277
|
+
|
|
278
|
+
All hooks return records in a normalized shape:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
interface SfaRecord {
|
|
282
|
+
record_id: string;
|
|
283
|
+
collection_name: string;
|
|
284
|
+
fields: Record<string, SfaFieldValue>;
|
|
285
|
+
titleFields: string[];
|
|
286
|
+
title?: string | null; // Pre-built display title from Keystone
|
|
287
|
+
version?: string | null; // Opaque version token (Keystone ETag) — required by useUpdateRecord and useDeleteRecord for optimistic locking
|
|
288
|
+
permissions: { canEdit: boolean; canDelete: boolean };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface SfaFieldValue {
|
|
292
|
+
kind: SfaFieldKind; // 'text' | 'number' | 'date' | 'datetime' | 'single_choice' | ...
|
|
293
|
+
value: unknown; // raw stored value
|
|
294
|
+
label: string | null; // display label (for selects, references, etc.)
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Access field values: `record.fields['company_name']?.value` for the raw value, `record.fields['company_name']?.label` for the display label.
|
|
299
|
+
|
|
300
|
+
## Error handling
|
|
301
|
+
|
|
302
|
+
Two baseline components for resilient pages:
|
|
303
|
+
|
|
304
|
+
### ErrorBoundary
|
|
305
|
+
|
|
306
|
+
Catches uncaught errors and renders a retry UI instead of a blank page.
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { ErrorBoundary } from '@geniee-inc/sfa-kit';
|
|
310
|
+
|
|
311
|
+
<ErrorBoundary>
|
|
312
|
+
<RecordView collection="companies" recordId={id} />
|
|
313
|
+
</ErrorBoundary>;
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### EmptyState
|
|
317
|
+
|
|
318
|
+
Placeholder for pages with no data.
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
import { EmptyState } from '@geniee-inc/sfa-kit';
|
|
322
|
+
|
|
323
|
+
if (!ctx?.record_id)
|
|
324
|
+
return <EmptyState message="レコードが指定されていません" />;
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Convenience wrappers (optional)
|
|
328
|
+
|
|
329
|
+
These fetch their own data. Use them for simple cases; prefer hooks when you need custom UI.
|
|
330
|
+
|
|
331
|
+
### RecordView
|
|
332
|
+
|
|
333
|
+
Displays a single record as a label-value grid.
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
<RecordView collection="companies" recordId="abc123" />
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Props:
|
|
340
|
+
|
|
341
|
+
- `collection` (required) - The object name.
|
|
342
|
+
- `recordId` (required) - The ID of the record to display.
|
|
343
|
+
- `title` (optional) - Override the display title.
|
|
344
|
+
- `fields` (optional) - Array of field names to show. If omitted, shows all fields.
|
|
345
|
+
- `fieldGroups` (optional) - Grouped field layout. See below.
|
|
346
|
+
|
|
347
|
+
**fieldGroups** organizes fields into titled groups with multi-column layouts:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
<RecordView
|
|
351
|
+
collection="companies"
|
|
352
|
+
recordId="abc123"
|
|
353
|
+
fieldGroups={[
|
|
354
|
+
{
|
|
355
|
+
title: 'Basic Info',
|
|
356
|
+
layout: '2-col',
|
|
357
|
+
fields: ['company_name', 'industry'],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
title: 'Financial',
|
|
361
|
+
layout: '2-col',
|
|
362
|
+
fields: ['annual_revenue_calc', 'founded_date'],
|
|
363
|
+
},
|
|
364
|
+
]}
|
|
365
|
+
/>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Each group: `title?` (heading), `layout?` (`"1-col"` | `"2-col"` | `"3-col"`), `fields` (field names).
|
|
369
|
+
|
|
370
|
+
### RecordList
|
|
371
|
+
|
|
372
|
+
Displays a paginated table of records.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
<RecordList collection="companies" />
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Props:
|
|
379
|
+
|
|
380
|
+
- `collection` (required) - The object name.
|
|
381
|
+
- `columns` (optional) - Array of `{ fieldName, header?, width? }`. If omitted, auto-detects up to 8 columns.
|
|
382
|
+
- `pageSize` (optional) - Records per page. Default 20.
|
|
383
|
+
- `onRowClick` (optional) - `(record) => void` callback.
|
|
384
|
+
- `showPagination` (optional) - Default true.
|
|
385
|
+
- `params` (optional) - `{ limit?, offset?, sortBy?, sortOrder?, filter?, cursor? }`.
|
|
386
|
+
|
|
387
|
+
### FieldRenderer
|
|
388
|
+
|
|
389
|
+
Renders a single field value based on its kind. Used internally by RecordView and RecordList. Use directly when building custom layouts.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
<FieldRenderer fieldValue={someField} fieldName="company_name" />
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Design tokens
|
|
396
|
+
|
|
397
|
+
All kit components use CSS custom properties with `--sfa-*` prefixes. Use these in your generated UI for visual consistency with SFA/CRM:
|
|
398
|
+
|
|
399
|
+
```css
|
|
400
|
+
--sfa-primary: #3867d1;
|
|
401
|
+
--sfa-error: #ed5c5c;
|
|
402
|
+
--sfa-success: #a3c848;
|
|
403
|
+
--sfa-warning: #f2b45e;
|
|
404
|
+
--sfa-text-primary: #313542;
|
|
405
|
+
--sfa-text-secondary: #838a98;
|
|
406
|
+
--sfa-border: #d6d9dd;
|
|
407
|
+
--sfa-bg-default: #f7f8fa;
|
|
408
|
+
--sfa-bg-surface: #ffffff;
|
|
409
|
+
--sfa-font-family: 'Noto Sans JP', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Override any token on a parent element:
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
<div style={{ '--sfa-primary': '#FF0000' } as React.CSSProperties}>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Composition examples
|
|
419
|
+
|
|
420
|
+
**Record-level page with org default and embed context:**
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
'use client';
|
|
424
|
+
import {
|
|
425
|
+
SfaLoginGate,
|
|
426
|
+
useEmbedContext,
|
|
427
|
+
useRecord,
|
|
428
|
+
ErrorBoundary,
|
|
429
|
+
EmptyState,
|
|
430
|
+
} from '@geniee-inc/sfa-kit';
|
|
431
|
+
|
|
432
|
+
function CompanyDetail() {
|
|
433
|
+
const ctx = useEmbedContext();
|
|
434
|
+
const { record, isLoading, error } = useRecord(
|
|
435
|
+
ctx?.object_key ?? '',
|
|
436
|
+
ctx?.record_id ?? ''
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
if (!ctx?.record_id)
|
|
440
|
+
return <EmptyState message="会社レコードが選択されていません" />;
|
|
441
|
+
if (isLoading) return <p>Loading...</p>;
|
|
442
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
443
|
+
if (!record) return null;
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div>
|
|
447
|
+
<h1>{record.title ?? record.record_id}</h1>
|
|
448
|
+
{Object.entries(record.fields).map(([key, field]) => (
|
|
449
|
+
<p key={key}>
|
|
450
|
+
<strong>{key}:</strong> {field.label ?? String(field.value ?? '')}
|
|
451
|
+
</p>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export default function Page() {
|
|
458
|
+
return (
|
|
459
|
+
<SfaLoginGate
|
|
460
|
+
defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}
|
|
461
|
+
objectKey="companies"
|
|
462
|
+
level="record">
|
|
463
|
+
<ErrorBoundary>
|
|
464
|
+
<CompanyDetail />
|
|
465
|
+
</ErrorBoundary>
|
|
466
|
+
</SfaLoginGate>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Object-level page (`level="object"`, no record ID input):**
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
'use client';
|
|
475
|
+
import {
|
|
476
|
+
SfaLoginGate,
|
|
477
|
+
useEmbedContext,
|
|
478
|
+
useRecordList,
|
|
479
|
+
} from '@geniee-inc/sfa-kit';
|
|
480
|
+
|
|
481
|
+
function ObjectList() {
|
|
482
|
+
const ctx = useEmbedContext();
|
|
483
|
+
const { items, isLoading } = useRecordList(ctx?.object_key ?? '');
|
|
484
|
+
// ...
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export default function Page() {
|
|
488
|
+
return (
|
|
489
|
+
<SfaLoginGate
|
|
490
|
+
defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}
|
|
491
|
+
objectKey="companies"
|
|
492
|
+
level="object">
|
|
493
|
+
<ObjectList />
|
|
494
|
+
</SfaLoginGate>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Cross-object page (company timeline with current-page grandchildren):**
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
'use client';
|
|
503
|
+
import {
|
|
504
|
+
SfaLoginGate,
|
|
505
|
+
useEmbedContext,
|
|
506
|
+
useRelatedRecords,
|
|
507
|
+
ErrorBoundary,
|
|
508
|
+
EmptyState,
|
|
509
|
+
} from '@geniee-inc/sfa-kit';
|
|
510
|
+
|
|
511
|
+
function CompanyTimeline() {
|
|
512
|
+
const ctx = useEmbedContext();
|
|
513
|
+
const companyId = ctx?.record_id ?? '';
|
|
514
|
+
|
|
515
|
+
const {
|
|
516
|
+
items: negotiations,
|
|
517
|
+
isLoading: loadingNeg,
|
|
518
|
+
error: negotiationsError,
|
|
519
|
+
} = useRelatedRecords('companies', companyId, 'business_discussions', {
|
|
520
|
+
limit: 50,
|
|
521
|
+
sortBy: 'created_date',
|
|
522
|
+
sortOrder: 'desc',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const childIds = negotiations.map(r => r.record_id);
|
|
526
|
+
// `childIds` is from the first call's paginated `items`, so this fetches
|
|
527
|
+
// grandchildren for the currently loaded page of negotiations only.
|
|
528
|
+
const {
|
|
529
|
+
items: activities,
|
|
530
|
+
isLoading: loadingAct,
|
|
531
|
+
error: activitiesError,
|
|
532
|
+
} = useRelatedRecords(
|
|
533
|
+
'business_discussions',
|
|
534
|
+
childIds,
|
|
535
|
+
'activity_histories',
|
|
536
|
+
{ limit: 50, sortBy: 'activity_date', sortOrder: 'desc' }
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
if (!companyId)
|
|
540
|
+
return <EmptyState message="会社レコードが選択されていません" />;
|
|
541
|
+
if (negotiationsError || activitiesError)
|
|
542
|
+
return <p>Error: {(negotiationsError ?? activitiesError).message}</p>;
|
|
543
|
+
if (loadingNeg || loadingAct) return <p>Loading...</p>;
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<div>
|
|
547
|
+
<h2>商談 ({negotiations.length})</h2>
|
|
548
|
+
{negotiations.map(n => (
|
|
549
|
+
<p key={n.record_id}>{n.title ?? n.record_id}</p>
|
|
550
|
+
))}
|
|
551
|
+
<h2>活動履歴 ({activities.length})</h2>
|
|
552
|
+
{activities.map(a => (
|
|
553
|
+
<p key={a.record_id}>{a.title ?? a.record_id}</p>
|
|
554
|
+
))}
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export default function Page() {
|
|
560
|
+
return (
|
|
561
|
+
<SfaLoginGate
|
|
562
|
+
defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}
|
|
563
|
+
objectKey="companies"
|
|
564
|
+
level="record">
|
|
565
|
+
<ErrorBoundary>
|
|
566
|
+
<CompanyTimeline />
|
|
567
|
+
</ErrorBoundary>
|
|
568
|
+
</SfaLoginGate>
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## Do NOT
|
|
574
|
+
|
|
575
|
+
- Do NOT create your own authentication, login forms, or auth providers. `SfaLoginGate` handles everything.
|
|
576
|
+
- Do NOT hand-wire foreign key filters for cross-object queries. Use `useRelatedRecords` which resolves the join from describe metadata automatically.
|
|
577
|
+
- Do NOT hardcode `defaultOrganizationId` as a number literal in JSX. Use `Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)` and set the value in `.env`.
|
|
578
|
+
- Do NOT embed service account credentials in generated page source. Discovery credentials (used to query the schema at build time) must never appear in deployed code.
|
|
579
|
+
- Do NOT call the API directly with fetch. Use the provided hooks or `useSfaClient()`.
|
|
580
|
+
- Do NOT add Route Handlers (app/api/) for SFA data. The kit calls the API from the browser.
|
|
581
|
+
- Do NOT hardcode API base URLs, API keys, or credentials anywhere.
|
|
582
|
+
- Do NOT import from `@mui/material`, `@emotion`, or `@ag-grid`. This package has zero external UI dependencies.
|
|
583
|
+
- Do NOT nest SfaLoginGate inside another SfaLoginGate or SfaProvider.
|
|
584
|
+
- Do NOT hardcode a list of available objects. Discover from the user's request or `useDescribe`.
|
|
585
|
+
- Do NOT write `.npmrc` or embed npm registry tokens in any file. sfa-kit is a public package and installs without authentication.
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Geniee, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @geniee-inc/sfa-kit
|
|
2
|
+
|
|
3
|
+
React hooks and components for building pages that connect to GENIEE SFA/CRM data. The kit handles authentication and data fetching — you compose the layout.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @geniee-inc/sfa-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No registry configuration and no authentication are required. `react` and `react-dom` (18 or 19) are peer dependencies.
|
|
12
|
+
|
|
13
|
+
The package ships ES modules built for a bundler — Next.js, Vite, webpack, Bun. Relative specifiers inside it are extensionless, so plain Node ESM resolution will not load it directly, and there is no CommonJS entry point: `require()` is not supported.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
This package is published so that it can be installed by GENIEE SFA/CRM (Chikyu) pages and by the JapanAI Studio page builder. It is not a general-purpose library: every request needs a Keystone access token issued at runtime by GENIEE's API, and without one all calls fail with 401. The package carries no credentials and cannot reach any data on its own.
|
|
18
|
+
|
|
19
|
+
## What is published
|
|
20
|
+
|
|
21
|
+
Built JavaScript and TypeScript declarations only. Source, tests and git history stay in GENIEE's private repository.
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
The full API reference — every hook, component, prop and the composition rules — ships inside this package as `GUIDE.md`. After installing, read `node_modules/@geniee-inc/sfa-kit/GUIDE.md`, or extract it from the published tarball.
|
|
26
|
+
|
|
27
|
+
It is the authoritative reference for the version you installed. Cross-check it against the `.d.ts` declarations in the same package before relying on a specific prop.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT — see `LICENSE.md`.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geniee-inc/sfa-kit",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "React hooks and components for building pages that connect to GENIEE SFA/CRM data.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE.md",
|
|
22
|
+
"GUIDE.md"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"registry": "https://registry.npmjs.org",
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
31
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"tslib": "^2.3.0"
|
|
35
|
+
}
|
|
36
|
+
}
|