@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuthResult, SfaSession } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* @description Returns a 30s timeout AbortSignal for auth fetch calls, or
|
|
4
|
+
* undefined in runtimes that lack AbortSignal.timeout (e.g. jsdom in tests).
|
|
5
|
+
*/
|
|
6
|
+
export declare function authTimeoutSignal(): AbortSignal | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* @description Authenticate against Keystone's POST /v1/auth/token.
|
|
9
|
+
* When organizationId is omitted and the account belongs to multiple
|
|
10
|
+
* organizations, the API responds 409 with available_organizations.
|
|
11
|
+
* In that case we return a 'choose_organization' result so the caller
|
|
12
|
+
* can present a picker and retry with the chosen org.
|
|
13
|
+
*/
|
|
14
|
+
export declare function authenticate(apiBase: string, email: string, password: string, organizationId?: number): Promise<AuthResult>;
|
|
15
|
+
export declare function isSessionExpired(session: SfaSession): boolean;
|
|
16
|
+
export declare function decodeJwtPayload(token: string): Record<string, unknown>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
const AUTH_TIMEOUT_MS = 30000;
|
|
3
|
+
/**
|
|
4
|
+
* @description Returns a 30s timeout AbortSignal for auth fetch calls, or
|
|
5
|
+
* undefined in runtimes that lack AbortSignal.timeout (e.g. jsdom in tests).
|
|
6
|
+
*/
|
|
7
|
+
export function authTimeoutSignal() {
|
|
8
|
+
if (typeof AbortSignal.timeout === 'function') {
|
|
9
|
+
return AbortSignal.timeout(AUTH_TIMEOUT_MS);
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @description Authenticate against Keystone's POST /v1/auth/token.
|
|
15
|
+
* When organizationId is omitted and the account belongs to multiple
|
|
16
|
+
* organizations, the API responds 409 with available_organizations.
|
|
17
|
+
* In that case we return a 'choose_organization' result so the caller
|
|
18
|
+
* can present a picker and retry with the chosen org.
|
|
19
|
+
*/
|
|
20
|
+
export function authenticate(apiBase, email, password, organizationId) {
|
|
21
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
22
|
+
var _a, _b, _c;
|
|
23
|
+
const authUrl = `${apiBase}/v1/auth/token`;
|
|
24
|
+
const body = { email, password };
|
|
25
|
+
if (organizationId != null) {
|
|
26
|
+
body['organization_id'] = organizationId;
|
|
27
|
+
}
|
|
28
|
+
const response = yield globalThis.fetch(authUrl, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
signal: authTimeoutSignal(),
|
|
33
|
+
});
|
|
34
|
+
if (response.status === 409) {
|
|
35
|
+
const data = (yield response.json());
|
|
36
|
+
const organizations = (_a = data.available_organizations) !== null && _a !== void 0 ? _a : [];
|
|
37
|
+
if (organizations.length === 0) {
|
|
38
|
+
throw new Error('Auth returned 409 but no available organizations');
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
status: 'choose_organization',
|
|
42
|
+
availableOrganizations: organizations,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const text = yield response.text().catch(() => '');
|
|
47
|
+
throw new Error(`Auth failed: ${response.status} ${response.statusText}${text ? ` — ${text}` : ''}`);
|
|
48
|
+
}
|
|
49
|
+
const data = (yield response.json());
|
|
50
|
+
if (!data.access_token || typeof data.expires_in !== 'number') {
|
|
51
|
+
throw new Error('Auth response missing access_token or expires_in');
|
|
52
|
+
}
|
|
53
|
+
const claims = decodeJwtPayload(data.access_token);
|
|
54
|
+
return {
|
|
55
|
+
status: 'authenticated',
|
|
56
|
+
session: {
|
|
57
|
+
accessToken: data.access_token,
|
|
58
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
59
|
+
organizationId: String((_c = (_b = claims['organization_id']) !== null && _b !== void 0 ? _b : organizationId) !== null && _c !== void 0 ? _c : ''),
|
|
60
|
+
email,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function isSessionExpired(session) {
|
|
66
|
+
// NOTE: expiresAt が NaN/undefined だとこの比較は常に false を返し、
|
|
67
|
+
// セッションが永久に有効と判定されてしまう。安全側に倒して期限切れとする
|
|
68
|
+
if (!Number.isFinite(session.expiresAt))
|
|
69
|
+
return true;
|
|
70
|
+
return Date.now() >= session.expiresAt - 60000;
|
|
71
|
+
}
|
|
72
|
+
export function decodeJwtPayload(token) {
|
|
73
|
+
const parts = token.split('.');
|
|
74
|
+
const payload = parts[1];
|
|
75
|
+
if (!payload)
|
|
76
|
+
return {};
|
|
77
|
+
try {
|
|
78
|
+
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
79
|
+
return JSON.parse(decoded);
|
|
80
|
+
}
|
|
81
|
+
catch (_a) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SfaSession } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* @description Loads the saved session from sessionStorage.
|
|
4
|
+
* Returns null if no session exists or if the token has expired.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadSession(): SfaSession | null;
|
|
7
|
+
export declare function saveSession(session: SfaSession): void;
|
|
8
|
+
export declare function clearSession(): void;
|
|
9
|
+
/**
|
|
10
|
+
* @description Persists the last successfully authenticated org ID to
|
|
11
|
+
* localStorage. Used to pre-fill the org ID field and auto-select the
|
|
12
|
+
* correct org on subsequent logins, even after the session expires.
|
|
13
|
+
*/
|
|
14
|
+
export declare function saveLastOrgId(orgId: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* @description Loads the last successfully authenticated org ID from
|
|
17
|
+
* localStorage. Returns null if none is stored.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadLastOrgId(): string | null;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isSessionExpired } from './keystone-auth';
|
|
2
|
+
const STORAGE_KEY = 'sfa-kit-session';
|
|
3
|
+
const LAST_ORG_KEY = 'sfa-kit-last-org';
|
|
4
|
+
/**
|
|
5
|
+
* @description Loads the saved session from sessionStorage.
|
|
6
|
+
* Returns null if no session exists or if the token has expired.
|
|
7
|
+
*/
|
|
8
|
+
export function loadSession() {
|
|
9
|
+
var _a;
|
|
10
|
+
try {
|
|
11
|
+
const stored = (_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.getItem(STORAGE_KEY);
|
|
12
|
+
if (!stored)
|
|
13
|
+
return null;
|
|
14
|
+
const session = JSON.parse(stored);
|
|
15
|
+
if (!session.accessToken || isSessionExpired(session)) {
|
|
16
|
+
clearSession();
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return session;
|
|
20
|
+
}
|
|
21
|
+
catch (_b) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function saveSession(session) {
|
|
26
|
+
var _a;
|
|
27
|
+
try {
|
|
28
|
+
(_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
29
|
+
}
|
|
30
|
+
catch (_b) {
|
|
31
|
+
// sessionStorage not available
|
|
32
|
+
}
|
|
33
|
+
// Persist org ID to localStorage so it survives across sessions
|
|
34
|
+
if (session.organizationId) {
|
|
35
|
+
saveLastOrgId(session.organizationId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function clearSession() {
|
|
39
|
+
var _a;
|
|
40
|
+
try {
|
|
41
|
+
(_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.removeItem(STORAGE_KEY);
|
|
42
|
+
}
|
|
43
|
+
catch (_b) {
|
|
44
|
+
// sessionStorage not available
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @description Persists the last successfully authenticated org ID to
|
|
49
|
+
* localStorage. Used to pre-fill the org ID field and auto-select the
|
|
50
|
+
* correct org on subsequent logins, even after the session expires.
|
|
51
|
+
*/
|
|
52
|
+
export function saveLastOrgId(orgId) {
|
|
53
|
+
var _a;
|
|
54
|
+
try {
|
|
55
|
+
(_a = globalThis.localStorage) === null || _a === void 0 ? void 0 : _a.setItem(LAST_ORG_KEY, orgId);
|
|
56
|
+
}
|
|
57
|
+
catch (_b) {
|
|
58
|
+
// localStorage not available
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @description Loads the last successfully authenticated org ID from
|
|
63
|
+
* localStorage. Returns null if none is stored.
|
|
64
|
+
*/
|
|
65
|
+
export function loadLastOrgId() {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
try {
|
|
68
|
+
return (_b = (_a = globalThis.localStorage) === null || _a === void 0 ? void 0 : _a.getItem(LAST_ORG_KEY)) !== null && _b !== void 0 ? _b : null;
|
|
69
|
+
}
|
|
70
|
+
catch (_c) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface KeystoneTokenResponse {
|
|
2
|
+
readonly access_token: string;
|
|
3
|
+
readonly token_type: string;
|
|
4
|
+
readonly expires_in: number;
|
|
5
|
+
}
|
|
6
|
+
export interface OrganizationChoice {
|
|
7
|
+
readonly organization_id: number;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @description Persisted session state (sessionStorage).
|
|
12
|
+
* The JWT itself carries org_id/user_id in its claims.
|
|
13
|
+
*/
|
|
14
|
+
export interface SfaSession {
|
|
15
|
+
readonly accessToken: string;
|
|
16
|
+
readonly expiresAt: number;
|
|
17
|
+
readonly organizationId: string;
|
|
18
|
+
readonly email?: string;
|
|
19
|
+
readonly apiBase?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @description Discriminated result from authenticate().
|
|
23
|
+
* 'authenticated' means a token was issued.
|
|
24
|
+
* 'choose_organization' means the account spans multiple orgs
|
|
25
|
+
* and the caller must retry with one of the listed choices.
|
|
26
|
+
*/
|
|
27
|
+
export type AuthResult = {
|
|
28
|
+
readonly status: 'authenticated';
|
|
29
|
+
readonly session: SfaSession;
|
|
30
|
+
} | {
|
|
31
|
+
readonly status: 'choose_organization';
|
|
32
|
+
readonly availableOrganizations: readonly OrganizationChoice[];
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EmbedContext } from './embed-protocol';
|
|
2
|
+
/**
|
|
3
|
+
* @description React context carrying the runtime embed context
|
|
4
|
+
* (object_key and optional record_id) injected by the SFA/CRM host.
|
|
5
|
+
* Only populated when running inside an iframe via the postMessage flow.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EmbedContextValue: import("react").Context<EmbedContext | null>;
|
|
8
|
+
/**
|
|
9
|
+
* @description Returns the embed context (object_key, record_id) from the
|
|
10
|
+
* active runtime environment. Checks two sources in priority order:
|
|
11
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* Returns null only when neither context is available.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const ctx = useEmbedContext();
|
|
19
|
+
* const { record } = useRecord(ctx?.object_key ?? '', ctx?.record_id ?? '');
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function useEmbedContext(): EmbedContext | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { StandalonePreviewCtx } from './StandalonePreviewContext';
|
|
3
|
+
/**
|
|
4
|
+
* @description React context carrying the runtime embed context
|
|
5
|
+
* (object_key and optional record_id) injected by the SFA/CRM host.
|
|
6
|
+
* Only populated when running inside an iframe via the postMessage flow.
|
|
7
|
+
*/
|
|
8
|
+
export const EmbedContextValue = createContext(null);
|
|
9
|
+
/**
|
|
10
|
+
* @description Returns the embed context (object_key, record_id) from the
|
|
11
|
+
* active runtime environment. Checks two sources in priority order:
|
|
12
|
+
*
|
|
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.
|
|
15
|
+
*
|
|
16
|
+
* Returns null only when neither context is available.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const ctx = useEmbedContext();
|
|
20
|
+
* const { record } = useRecord(ctx?.object_key ?? '', ctx?.record_id ?? '');
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useEmbedContext() {
|
|
24
|
+
const embed = useContext(EmbedContextValue);
|
|
25
|
+
const standalone = useContext(StandalonePreviewCtx);
|
|
26
|
+
if (embed)
|
|
27
|
+
return embed;
|
|
28
|
+
if (standalone === null || standalone === void 0 ? void 0 : standalone.state)
|
|
29
|
+
return standalone.state;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface EmptyStateProps {
|
|
2
|
+
/** Primary message displayed to the user. */
|
|
3
|
+
readonly message?: string;
|
|
4
|
+
/** Secondary hint or instructions. */
|
|
5
|
+
readonly hint?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* @description Baseline placeholder for pages that have no data to display.
|
|
9
|
+
* Renders a centered, non-alarming message. Use this as the default when
|
|
10
|
+
* a record ID is missing, a list returns zero results, or a page hasn't
|
|
11
|
+
* been configured yet.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* if (!ctx?.record_id) return <EmptyState message="レコードが指定されていません" />;
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function EmptyState({ message, hint, }: EmptyStateProps): JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { color, font, space, baseContainerStyle } from '../theme';
|
|
3
|
+
/**
|
|
4
|
+
* @description Baseline placeholder for pages that have no data to display.
|
|
5
|
+
* Renders a centered, non-alarming message. Use this as the default when
|
|
6
|
+
* a record ID is missing, a list returns zero results, or a page hasn't
|
|
7
|
+
* been configured yet.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* if (!ctx?.record_id) return <EmptyState message="レコードが指定されていません" />;
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function EmptyState({ message = 'データがありません', hint, }) {
|
|
14
|
+
return (_jsxs("div", { style: styles.container, children: [_jsx("div", { style: styles.icon, children: "\uD83D\uDCCB" }), _jsx("p", { style: styles.message, children: message }), hint && _jsx("p", { style: styles.hint, children: hint })] }));
|
|
15
|
+
}
|
|
16
|
+
const styles = {
|
|
17
|
+
container: Object.assign(Object.assign({}, baseContainerStyle), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: `${space.xl * 2}px ${space.xl}px`, textAlign: 'center', minHeight: 200 }),
|
|
18
|
+
icon: {
|
|
19
|
+
fontSize: 32,
|
|
20
|
+
marginBottom: space.md,
|
|
21
|
+
opacity: 0.6,
|
|
22
|
+
},
|
|
23
|
+
message: {
|
|
24
|
+
fontSize: font.sizeXl,
|
|
25
|
+
fontWeight: font.weightMedium,
|
|
26
|
+
color: color.textSecondary,
|
|
27
|
+
margin: `0 0 ${space.xs}px`,
|
|
28
|
+
},
|
|
29
|
+
hint: {
|
|
30
|
+
fontSize: font.sizeMd,
|
|
31
|
+
color: color.textMuted,
|
|
32
|
+
margin: 0,
|
|
33
|
+
maxWidth: 400,
|
|
34
|
+
lineHeight: 1.5,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import type { ErrorInfo, ReactNode } from 'react';
|
|
3
|
+
interface ErrorBoundaryProps {
|
|
4
|
+
/** Content to render when no error has occurred. */
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
/** Optional custom fallback. Receives the error for display. */
|
|
7
|
+
readonly fallback?: (error: Error, reset: () => void) => ReactNode;
|
|
8
|
+
}
|
|
9
|
+
interface ErrorBoundaryState {
|
|
10
|
+
readonly error: Error | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* @description Catches uncaught errors in the component tree below it and
|
|
14
|
+
* renders a friendly fallback instead of a blank page. Provides a "再読み込み"
|
|
15
|
+
* (reload) button so the user can retry without losing the DevToolbar.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <ErrorBoundary>
|
|
20
|
+
* <RecordView collection="companies" recordId={id} />
|
|
21
|
+
* </ErrorBoundary>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
25
|
+
constructor(props: ErrorBoundaryProps);
|
|
26
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
27
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
28
|
+
private handleReset;
|
|
29
|
+
render(): string | number | boolean | import("react").ReactFragment | JSX.Element | null | undefined;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { color, font, radius, space, baseContainerStyle } from '../theme';
|
|
4
|
+
import { SFA_KIT_VERSION } from '../version';
|
|
5
|
+
/**
|
|
6
|
+
* @description Catches uncaught errors in the component tree below it and
|
|
7
|
+
* renders a friendly fallback instead of a blank page. Provides a "再読み込み"
|
|
8
|
+
* (reload) button so the user can retry without losing the DevToolbar.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <ErrorBoundary>
|
|
13
|
+
* <RecordView collection="companies" recordId={id} />
|
|
14
|
+
* </ErrorBoundary>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class ErrorBoundary extends Component {
|
|
18
|
+
constructor(props) {
|
|
19
|
+
super(props);
|
|
20
|
+
this.handleReset = () => {
|
|
21
|
+
this.setState({ error: null });
|
|
22
|
+
};
|
|
23
|
+
this.state = { error: null };
|
|
24
|
+
}
|
|
25
|
+
static getDerivedStateFromError(error) {
|
|
26
|
+
return { error };
|
|
27
|
+
}
|
|
28
|
+
componentDidCatch(error, info) {
|
|
29
|
+
// Log to console so Sentry or other monitors can pick it up.
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.error('[sfa-kit] ErrorBoundary caught:', error, info.componentStack);
|
|
32
|
+
}
|
|
33
|
+
render() {
|
|
34
|
+
const { error } = this.state;
|
|
35
|
+
const { children, fallback } = this.props;
|
|
36
|
+
if (error) {
|
|
37
|
+
if (fallback)
|
|
38
|
+
return fallback(error, this.handleReset);
|
|
39
|
+
return _jsx(DefaultErrorFallback, { error: error, onReset: this.handleReset });
|
|
40
|
+
}
|
|
41
|
+
return children;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function DefaultErrorFallback({ error, onReset, }) {
|
|
45
|
+
return (_jsx("div", { style: styles.container, children: _jsxs("div", { style: styles.card, children: [_jsx("h3", { style: styles.title, children: "\u8868\u793A\u30A8\u30E9\u30FC" }), _jsx("p", { style: styles.message, children: "\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u8868\u793A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u518D\u8AAD\u307F\u8FBC\u307F\u3092\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002" }), _jsx("pre", { style: styles.detail, children: error.message }), _jsx("button", { type: "button", style: styles.retryButton, onClick: onReset, children: "\u518D\u8AAD\u307F\u8FBC\u307F" }), _jsxs("div", { style: styles.footer, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }) }));
|
|
46
|
+
}
|
|
47
|
+
const styles = {
|
|
48
|
+
container: Object.assign(Object.assign({}, baseContainerStyle), { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: space.xl, minHeight: 200 }),
|
|
49
|
+
card: {
|
|
50
|
+
maxWidth: 520,
|
|
51
|
+
width: '100%',
|
|
52
|
+
padding: space.lg,
|
|
53
|
+
backgroundColor: color.bgSurface,
|
|
54
|
+
border: `1px solid ${color.border}`,
|
|
55
|
+
borderRadius: radius.sm,
|
|
56
|
+
textAlign: 'center',
|
|
57
|
+
},
|
|
58
|
+
title: {
|
|
59
|
+
fontSize: font.sizeXl,
|
|
60
|
+
fontWeight: font.weightMedium,
|
|
61
|
+
color: color.error,
|
|
62
|
+
margin: `0 0 ${space.sm}px`,
|
|
63
|
+
},
|
|
64
|
+
message: {
|
|
65
|
+
fontSize: font.sizeMd,
|
|
66
|
+
color: color.textSecondary,
|
|
67
|
+
margin: `0 0 ${space.md}px`,
|
|
68
|
+
lineHeight: 1.5,
|
|
69
|
+
},
|
|
70
|
+
detail: {
|
|
71
|
+
fontSize: font.sizeSm,
|
|
72
|
+
fontFamily: 'monospace',
|
|
73
|
+
color: color.textMuted,
|
|
74
|
+
backgroundColor: color.bgDefault,
|
|
75
|
+
padding: space.sm,
|
|
76
|
+
borderRadius: radius.sm,
|
|
77
|
+
overflow: 'auto',
|
|
78
|
+
maxHeight: 120,
|
|
79
|
+
textAlign: 'left',
|
|
80
|
+
margin: `0 0 ${space.md}px`,
|
|
81
|
+
whiteSpace: 'pre-wrap',
|
|
82
|
+
wordBreak: 'break-all',
|
|
83
|
+
},
|
|
84
|
+
retryButton: {
|
|
85
|
+
padding: '8px 20px',
|
|
86
|
+
fontSize: font.sizeMd,
|
|
87
|
+
fontFamily: font.family,
|
|
88
|
+
fontWeight: font.weightMedium,
|
|
89
|
+
color: color.white,
|
|
90
|
+
backgroundColor: color.primary,
|
|
91
|
+
border: 'none',
|
|
92
|
+
borderRadius: radius.sm,
|
|
93
|
+
cursor: 'pointer',
|
|
94
|
+
},
|
|
95
|
+
footer: {
|
|
96
|
+
fontSize: font.sizeXs,
|
|
97
|
+
color: color.textMuted,
|
|
98
|
+
marginTop: space.sm,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SfaFieldValue } from '../provider/types';
|
|
2
|
+
interface FieldRendererProps {
|
|
3
|
+
readonly fieldValue: SfaFieldValue;
|
|
4
|
+
readonly fieldName?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* @description Renders a single field value based on its normalized kind.
|
|
8
|
+
* Uses `label` for display (pre-formatted by the API), falls back to `value`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function FieldRenderer({ fieldValue, fieldName }: FieldRendererProps): JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @description Renders a single field value based on its normalized kind.
|
|
4
|
+
* Uses `label` for display (pre-formatted by the API), falls back to `value`.
|
|
5
|
+
*/
|
|
6
|
+
export function FieldRenderer({ fieldValue, fieldName }) {
|
|
7
|
+
var _a;
|
|
8
|
+
const display = (_a = fieldValue.label) !== null && _a !== void 0 ? _a : formatFallback(fieldValue);
|
|
9
|
+
if (display == null || display === '') {
|
|
10
|
+
return _jsx("span", { style: styles.empty, children: "\u2014" });
|
|
11
|
+
}
|
|
12
|
+
if (fieldValue.kind === 'text' && typeof fieldValue.value === 'string') {
|
|
13
|
+
if (isMultiline(fieldValue.value)) {
|
|
14
|
+
return _jsx("span", { style: styles.preWrap, children: display });
|
|
15
|
+
}
|
|
16
|
+
const link = detectLink(fieldValue.value);
|
|
17
|
+
if (link) {
|
|
18
|
+
return (_jsx("a", { href: link.href, style: styles.link, target: "_blank", rel: "noopener noreferrer", children: display }));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return _jsx("span", { "data-field": fieldName, children: display });
|
|
22
|
+
}
|
|
23
|
+
function formatFallback(field) {
|
|
24
|
+
if (field.value == null)
|
|
25
|
+
return null;
|
|
26
|
+
if (typeof field.value === 'number')
|
|
27
|
+
return field.value.toLocaleString('ja-JP');
|
|
28
|
+
if (typeof field.value === 'string')
|
|
29
|
+
return field.value;
|
|
30
|
+
if (Array.isArray(field.value))
|
|
31
|
+
return field.value.join(', ');
|
|
32
|
+
return String(field.value);
|
|
33
|
+
}
|
|
34
|
+
function isMultiline(value) {
|
|
35
|
+
return value.includes('\n');
|
|
36
|
+
}
|
|
37
|
+
function detectLink(value) {
|
|
38
|
+
if (/^https?:\/\//.test(value))
|
|
39
|
+
return { href: value };
|
|
40
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
41
|
+
return { href: `mailto:${value}` };
|
|
42
|
+
if (/^[\d+][\d\-().+\s]{7,}$/.test(value))
|
|
43
|
+
return { href: `tel:${value.replace(/[\s()-]/g, '')}` };
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const styles = {
|
|
47
|
+
empty: {
|
|
48
|
+
color: '#94a3b8',
|
|
49
|
+
},
|
|
50
|
+
preWrap: {
|
|
51
|
+
whiteSpace: 'pre-wrap',
|
|
52
|
+
},
|
|
53
|
+
link: {
|
|
54
|
+
color: '#3867D1',
|
|
55
|
+
textDecoration: 'none',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SfaListParams, SfaRecord } from '../provider/types';
|
|
2
|
+
interface RecordListColumn {
|
|
3
|
+
readonly fieldName: string;
|
|
4
|
+
readonly header?: string;
|
|
5
|
+
readonly width?: string;
|
|
6
|
+
}
|
|
7
|
+
interface RecordListProps {
|
|
8
|
+
readonly collection: string;
|
|
9
|
+
readonly columns?: readonly RecordListColumn[];
|
|
10
|
+
readonly params?: SfaListParams;
|
|
11
|
+
readonly pageSize?: number;
|
|
12
|
+
readonly showPagination?: boolean;
|
|
13
|
+
readonly onRowClick?: (record: SfaRecord) => void;
|
|
14
|
+
readonly onError?: (error: Error) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @description Fetches and displays a paginated list of SFA/CRM records as a table.
|
|
18
|
+
*/
|
|
19
|
+
export declare function RecordList({ collection, columns, params, pageSize, showPagination, onRowClick, onError, }: RecordListProps): JSX.Element;
|
|
20
|
+
export {};
|