@skopon-cool/form-sdk 0.2.4 → 0.3.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/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/catalog/useSkoponBoundField.d.ts +16 -1
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/client/authStorage.d.ts +14 -1
- package/dist/client/authStorage.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +8 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/CurlSubmitBlock.d.ts +3 -1
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
- package/dist/components/SkoponFormRenderer.d.ts +1 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1579 -1107
- package/dist/runtime/formAutoControl.d.ts +15 -0
- package/dist/runtime/formAutoControl.d.ts.map +1 -0
- package/dist/runtime/formRuntimeContext.d.ts +17 -0
- package/dist/runtime/formRuntimeContext.d.ts.map +1 -0
- package/dist/runtime/formRuntimeWs.d.ts +27 -0
- package/dist/runtime/formRuntimeWs.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +20 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/submit/buildCurlStatement.d.ts +4 -3
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/submitAnswerAgent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/catalog/a2uiCustomCatalog.tsx +41 -16
- package/src/catalog/skoponCaseSelect.tsx +10 -4
- package/src/catalog/skoponResumeSelect.tsx +10 -4
- package/src/catalog/useSkoponBoundField.ts +100 -4
- package/src/client/authStorage.test.ts +74 -1
- package/src/client/authStorage.ts +45 -4
- package/src/components/AskUserFormCard.tsx +178 -31
- package/src/components/CurlSubmitBlock.tsx +14 -3
- package/src/components/SkoponFormRenderer.tsx +12 -0
- package/src/index.ts +3 -0
- package/src/runtime/formAutoControl.test.ts +71 -0
- package/src/runtime/formAutoControl.ts +109 -0
- package/src/runtime/formRuntimeContext.tsx +107 -0
- package/src/runtime/formRuntimeWs.ts +158 -0
- package/src/runtime/types.ts +26 -0
- package/src/submit/buildCurlStatement.ts +20 -3
- package/src/submit/submit.test.ts +81 -0
- package/src/submit/submitAnswerAgent.ts +22 -13
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FormAutoControlEventData, FormRuntimeEventData } from './types';
|
|
2
|
+
export interface FormAutoControlTargets {
|
|
3
|
+
setValues: (values: Record<string, unknown>) => void;
|
|
4
|
+
getValues: (fieldNames?: string[]) => Record<string, unknown>;
|
|
5
|
+
submit: () => void | Promise<void>;
|
|
6
|
+
/** 卡片根节点,用于按 data-skopon-field 定位 DOM */
|
|
7
|
+
rootElement: HTMLElement | null;
|
|
8
|
+
}
|
|
9
|
+
export interface FormAutoControlHelpers {
|
|
10
|
+
reportEvent: (event: FormRuntimeEventData) => void;
|
|
11
|
+
runAutoControl: <T>(fn: () => T) => T;
|
|
12
|
+
}
|
|
13
|
+
/** 执行下行 form_auto_control_event;缺参或未知 action 时 warn 并忽略。 */
|
|
14
|
+
export declare function executeFormAutoControl(data: FormAutoControlEventData, targets: FormAutoControlTargets, helpers: FormAutoControlHelpers): void;
|
|
15
|
+
//# sourceMappingURL=formAutoControl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formAutoControl.d.ts","sourceRoot":"","sources":["../../src/runtime/formAutoControl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAE7E,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IACpD,SAAS,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7D,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,yCAAyC;IACzC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAClD,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;CACtC;AAiCD,4DAA4D;AAC5D,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,wBAAwB,EAC9B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,sBAAsB,GAC9B,IAAI,CAyDN"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { FormRuntimeWsClient } from './formRuntimeWs';
|
|
3
|
+
import type { FormRuntimeEventData } from './types';
|
|
4
|
+
export interface FormRuntimeContextValue {
|
|
5
|
+
reportEvent: (event: FormRuntimeEventData) => void;
|
|
6
|
+
reportData: (values: Record<string, unknown>) => void;
|
|
7
|
+
runAutoControl: <T>(fn: () => T) => T;
|
|
8
|
+
isAutoControl: () => boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface FormRuntimeProviderProps {
|
|
11
|
+
client: FormRuntimeWsClient | null;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function FormRuntimeProvider({ client, children, }: FormRuntimeProviderProps): import("react").JSX.Element;
|
|
15
|
+
/** 无 Provider 时 no-op(表单管理端预览不受影响)。 */
|
|
16
|
+
export declare function useFormRuntime(): FormRuntimeContextValue;
|
|
17
|
+
//# sourceMappingURL=formRuntimeContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formRuntimeContext.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAClD,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IACrD,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IACrC,aAAa,EAAE,MAAM,OAAO,CAAA;CAC7B;AAqBD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,QAAQ,GACT,EAAE,wBAAwB,+BAwD1B;AAED,uCAAuC;AACvC,wBAAgB,cAAc,IAAI,uBAAuB,CAExD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FormAutoControlEventData, FormRuntimeEnvelope, FormRuntimeOutboundType } from './types';
|
|
2
|
+
export type FormAutoControlHandler = (data: FormAutoControlEventData, envelope: FormRuntimeEnvelope<FormAutoControlEventData>) => void;
|
|
3
|
+
export interface FormRuntimeWsClientOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
askUserMessageId: string;
|
|
6
|
+
onAutoControl?: FormAutoControlHandler;
|
|
7
|
+
}
|
|
8
|
+
export declare class FormRuntimeWsClient {
|
|
9
|
+
private readonly url;
|
|
10
|
+
private readonly askUserMessageId;
|
|
11
|
+
private readonly onAutoControl?;
|
|
12
|
+
private ws;
|
|
13
|
+
private closed;
|
|
14
|
+
private reconnectAttempt;
|
|
15
|
+
private reconnectTimer;
|
|
16
|
+
private sendQueue;
|
|
17
|
+
constructor(options: FormRuntimeWsClientOptions);
|
|
18
|
+
connect(): void;
|
|
19
|
+
send(type: FormRuntimeOutboundType, data: Record<string, unknown>): void;
|
|
20
|
+
close(): void;
|
|
21
|
+
private enqueue;
|
|
22
|
+
private flushQueue;
|
|
23
|
+
private scheduleReconnect;
|
|
24
|
+
private clearReconnectTimer;
|
|
25
|
+
private handleMessage;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=formRuntimeWs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formRuntimeWs.d.ts","sourceRoot":"","sources":["../../src/runtime/formRuntimeWs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,SAAS,CAAA;AAKhB,MAAM,MAAM,sBAAsB,GAAG,CACnC,IAAI,EAAE,wBAAwB,EAC9B,QAAQ,EAAE,mBAAmB,CAAC,wBAAwB,CAAC,KACpD,IAAI,CAAA;AAET,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,sBAAsB,CAAA;CACvC;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAwB;IACvD,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,SAAS,CAAe;gBAEpB,OAAO,EAAE,0BAA0B;IAM/C,OAAO,IAAI,IAAI;IA0Bf,IAAI,CACF,IAAI,EAAE,uBAAuB,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI;IAqBP,KAAK,IAAI,IAAI;IAkBb,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,aAAa;CAgBtB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type FormRuntimeAction = 'focus' | 'change' | 'select' | 'click' | 'submit';
|
|
2
|
+
export interface FormRuntimeEventData {
|
|
3
|
+
action: FormRuntimeAction;
|
|
4
|
+
component: string;
|
|
5
|
+
field?: string;
|
|
6
|
+
value?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface FormAutoControlEventData {
|
|
9
|
+
action: FormRuntimeAction;
|
|
10
|
+
field?: string;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
component?: string;
|
|
13
|
+
}
|
|
14
|
+
export type FormRuntimeOutboundType = 'form_runtime_data' | 'form_runtime_event' | 'form_auto_control_bac_event';
|
|
15
|
+
export interface FormRuntimeEnvelope<T = Record<string, unknown>> {
|
|
16
|
+
type: string;
|
|
17
|
+
ask_user_message_id: string;
|
|
18
|
+
data: T;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAElF,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,iBAAiB,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,iBAAiB,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,oBAAoB,GACpB,6BAA6B,CAAA;AAEjC,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,mBAAmB,EAAE,MAAM,CAAA;IAC3B,IAAI,EAAE,CAAC,CAAA;CACR"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string;
|
|
1
|
+
export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null, headers?: Record<string, string>): string;
|
|
2
2
|
/** 构建合法 JSON body(card + extra 合并,便于 curl 直接执行) */
|
|
3
3
|
export declare function buildAskUserCurlBodyJson(cardValues: Record<string, unknown>, extraValues: Record<string, unknown>): string;
|
|
4
4
|
export interface BuildAskUserCurlStatementOptions {
|
|
5
5
|
cardValues: Record<string, unknown>;
|
|
6
6
|
extraValues?: Record<string, unknown>;
|
|
7
7
|
callbackUrl?: string | null;
|
|
8
|
+
headers?: Record<string, string>;
|
|
8
9
|
}
|
|
9
|
-
export declare function buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl, }: BuildAskUserCurlStatementOptions): string;
|
|
10
|
+
export declare function buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl, headers, }: BuildAskUserCurlStatementOptions): string;
|
|
10
11
|
/** 从 ask_user payload(blocksJson)生成向 callback 提交返回字段的 curl */
|
|
11
|
-
export declare function buildAskUserSubmitCurlFromPayload(payload: unknown, callbackUrl?: string | null): string;
|
|
12
|
+
export declare function buildAskUserSubmitCurlFromPayload(payload: unknown, callbackUrl?: string | null, headers?: Record<string, string>): string;
|
|
12
13
|
//# sourceMappingURL=buildCurlStatement.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAiBA,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CASR;AAED,mDAAmD;AACnD,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAER;AAED,MAAM,WAAW,gCAAgC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACV,WAAgB,EAChB,WAAW,EACX,OAAO,GACR,EAAE,gCAAgC,GAAG,MAAM,CAS3C;AAED,8DAA8D;AAC9D,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,OAAO,EAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CAKR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submitAnswerAgent.d.ts","sourceRoot":"","sources":["../../src/submit/submitAnswerAgent.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"submitAnswerAgent.d.ts","sourceRoot":"","sources":["../../src/submit/submitAnswerAgent.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAC1B,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;IACrE,WAAW,CAAC,EAAE,wBAAwB,CAAA;IACtC,kDAAkD;IAClD,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvC;AAwDD,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,oBAAoB,GAC3B;IAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAchD;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,oBAAoB,CAAC,CAmB/B"}
|
package/package.json
CHANGED
|
@@ -242,7 +242,10 @@ const SkoponSelectApi = {
|
|
|
242
242
|
|
|
243
243
|
function SkoponSelectPreview({ context }: { context: ComponentContext }) {
|
|
244
244
|
const { interactive } = useA2uiPreviewMode()
|
|
245
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
245
|
+
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
246
|
+
component: 'SkoponSelect',
|
|
247
|
+
changeAction: 'select',
|
|
248
|
+
})
|
|
246
249
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
247
250
|
const label = readString(props.label)
|
|
248
251
|
const placeholder = readString(props.placeholder)
|
|
@@ -251,11 +254,11 @@ function SkoponSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
251
254
|
|
|
252
255
|
useEffect(() => {
|
|
253
256
|
const normalized = normalizeSelectStoredValue(value, isMultiple)
|
|
254
|
-
if (normalized !== null)
|
|
255
|
-
}, [isMultiple, value,
|
|
257
|
+
if (normalized !== null) setValueSilent(normalized)
|
|
258
|
+
}, [isMultiple, value, setValueSilent])
|
|
256
259
|
|
|
257
260
|
return (
|
|
258
|
-
<div className="form-block-preview">
|
|
261
|
+
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
259
262
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
260
263
|
<Select
|
|
261
264
|
mode={isMultiple ? 'multiple' : undefined}
|
|
@@ -279,7 +282,10 @@ const SkoponSelectImpl = createBinderlessComponentImplementation(
|
|
|
279
282
|
|
|
280
283
|
function ChoicePickerPreview({ context }: { context: ComponentContext }) {
|
|
281
284
|
const { interactive } = useA2uiPreviewMode()
|
|
282
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
285
|
+
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
286
|
+
component: 'ChoicePicker',
|
|
287
|
+
changeAction: 'select',
|
|
288
|
+
})
|
|
283
289
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
284
290
|
const label = readString(props.label)
|
|
285
291
|
const variant = readString(props.variant) || 'mutuallyExclusive'
|
|
@@ -301,11 +307,11 @@ function ChoicePickerPreview({ context }: { context: ComponentContext }) {
|
|
|
301
307
|
|
|
302
308
|
useEffect(() => {
|
|
303
309
|
const normalized = normalizeSelectStoredValue(value, isMultiplePicker)
|
|
304
|
-
if (normalized !== null)
|
|
305
|
-
}, [isMultiplePicker, value,
|
|
310
|
+
if (normalized !== null) setValueSilent(normalized)
|
|
311
|
+
}, [isMultiplePicker, value, setValueSilent])
|
|
306
312
|
|
|
307
313
|
return (
|
|
308
|
-
<div className="form-block-preview">
|
|
314
|
+
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
309
315
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
310
316
|
{effectiveType === 'select' ? (
|
|
311
317
|
<Select
|
|
@@ -370,12 +376,15 @@ const ChoicePickerImpl = createBinderlessComponentImplementation(
|
|
|
370
376
|
|
|
371
377
|
function ToggleSwitchPreview({ context }: { context: ComponentContext }) {
|
|
372
378
|
const { interactive } = useA2uiPreviewMode()
|
|
373
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
379
|
+
const { value, setValue, fieldName } = useSkoponBoundField(context, 'value', {
|
|
380
|
+
component: 'CheckBox',
|
|
381
|
+
changeAction: 'change',
|
|
382
|
+
})
|
|
374
383
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
375
384
|
const label = readString(props.label)
|
|
376
385
|
|
|
377
386
|
return (
|
|
378
|
-
<div className="form-block-preview">
|
|
387
|
+
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
379
388
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
380
389
|
<Switch
|
|
381
390
|
disabled={!interactive}
|
|
@@ -409,7 +418,10 @@ function toDateTimePickerValue(
|
|
|
409
418
|
|
|
410
419
|
function DateTimeInputPreview({ context }: { context: ComponentContext }) {
|
|
411
420
|
const { interactive } = useA2uiPreviewMode()
|
|
412
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
421
|
+
const { value, setValue, fieldName, reportFocus } = useSkoponBoundField(context, 'value', {
|
|
422
|
+
component: 'DateTimeInput',
|
|
423
|
+
changeAction: 'change',
|
|
424
|
+
})
|
|
413
425
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
414
426
|
const label = readString(props.label)
|
|
415
427
|
const placeholder = readString(props.placeholder)
|
|
@@ -434,7 +446,7 @@ function DateTimeInputPreview({ context }: { context: ComponentContext }) {
|
|
|
434
446
|
}
|
|
435
447
|
|
|
436
448
|
return (
|
|
437
|
-
<div className="form-block-preview">
|
|
449
|
+
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
438
450
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
439
451
|
{!enableDate && enableTime ? (
|
|
440
452
|
<TimePicker
|
|
@@ -444,6 +456,7 @@ function DateTimeInputPreview({ context }: { context: ComponentContext }) {
|
|
|
444
456
|
placeholder={placeholder || '选择时间'}
|
|
445
457
|
value={pickerValue}
|
|
446
458
|
onChange={handleChange}
|
|
459
|
+
onFocus={reportFocus}
|
|
447
460
|
/>
|
|
448
461
|
) : (
|
|
449
462
|
<DatePicker
|
|
@@ -457,6 +470,7 @@ function DateTimeInputPreview({ context }: { context: ComponentContext }) {
|
|
|
457
470
|
}
|
|
458
471
|
value={pickerValue}
|
|
459
472
|
onChange={handleChange}
|
|
473
|
+
onFocus={reportFocus}
|
|
460
474
|
/>
|
|
461
475
|
)}
|
|
462
476
|
</div>
|
|
@@ -498,7 +512,10 @@ const FileUploadApi = {
|
|
|
498
512
|
|
|
499
513
|
function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
500
514
|
const { interactive } = useA2uiPreviewMode()
|
|
501
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
515
|
+
const { value, setValue, fieldName } = useSkoponBoundField(context, 'value', {
|
|
516
|
+
component: 'FileUpload',
|
|
517
|
+
changeAction: 'change',
|
|
518
|
+
})
|
|
502
519
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
503
520
|
const label = readString(props.label)
|
|
504
521
|
const accept = readString(props.accept)
|
|
@@ -524,7 +541,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
524
541
|
const belowMinCount = selectedNames.length < minCount
|
|
525
542
|
|
|
526
543
|
return (
|
|
527
|
-
<div className="form-block-preview">
|
|
544
|
+
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
528
545
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
529
546
|
<Upload.Dragger
|
|
530
547
|
disabled={!interactive || atMaxLimit}
|
|
@@ -585,7 +602,11 @@ const FileUploadImpl = createBinderlessComponentImplementation(
|
|
|
585
602
|
|
|
586
603
|
function TextFieldPreview({ context }: { context: ComponentContext }) {
|
|
587
604
|
const { interactive } = useA2uiPreviewMode()
|
|
588
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
605
|
+
const { value, setValue, fieldName, reportFocus } = useSkoponBoundField(context, 'value', {
|
|
606
|
+
component: 'TextField',
|
|
607
|
+
changeAction: 'change',
|
|
608
|
+
debounceChange: true,
|
|
609
|
+
})
|
|
589
610
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
590
611
|
const label = readString(props.label)
|
|
591
612
|
const placeholder = readString(props.placeholder)
|
|
@@ -624,6 +645,7 @@ function TextFieldPreview({ context }: { context: ComponentContext }) {
|
|
|
624
645
|
setValue(next)
|
|
625
646
|
validateFormat(next)
|
|
626
647
|
}}
|
|
648
|
+
onFocus={reportFocus}
|
|
627
649
|
onBlur={(e) => validateFormat(e.target.value)}
|
|
628
650
|
/>
|
|
629
651
|
)
|
|
@@ -638,6 +660,7 @@ function TextFieldPreview({ context }: { context: ComponentContext }) {
|
|
|
638
660
|
placeholder={placeholder || '数字'}
|
|
639
661
|
value={numericValue}
|
|
640
662
|
onChange={(next) => setValue(next == null ? '' : String(next))}
|
|
663
|
+
onFocus={reportFocus}
|
|
641
664
|
/>
|
|
642
665
|
)
|
|
643
666
|
} else if (variant === 'obscured') {
|
|
@@ -648,6 +671,7 @@ function TextFieldPreview({ context }: { context: ComponentContext }) {
|
|
|
648
671
|
placeholder={placeholder}
|
|
649
672
|
value={stringValue}
|
|
650
673
|
onChange={(e) => setValue(e.target.value)}
|
|
674
|
+
onFocus={reportFocus}
|
|
651
675
|
/>
|
|
652
676
|
)
|
|
653
677
|
} else {
|
|
@@ -663,13 +687,14 @@ function TextFieldPreview({ context }: { context: ComponentContext }) {
|
|
|
663
687
|
setValue(next)
|
|
664
688
|
validateFormat(next)
|
|
665
689
|
}}
|
|
690
|
+
onFocus={reportFocus}
|
|
666
691
|
onBlur={(e) => validateFormat(e.target.value)}
|
|
667
692
|
/>
|
|
668
693
|
)
|
|
669
694
|
}
|
|
670
695
|
|
|
671
696
|
return (
|
|
672
|
-
<div className="form-block-preview">
|
|
697
|
+
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
673
698
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
674
699
|
{control}
|
|
675
700
|
{formatError ? (
|
|
@@ -110,7 +110,10 @@ function CaseCard({
|
|
|
110
110
|
function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
|
|
111
111
|
const { interactive } = useA2uiPreviewMode()
|
|
112
112
|
const caseSearch = useCaseSearch()
|
|
113
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
113
|
+
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
114
|
+
component: 'SkoponCaseSelect',
|
|
115
|
+
changeAction: 'click',
|
|
116
|
+
})
|
|
114
117
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
115
118
|
|
|
116
119
|
const label = readString(props.label)
|
|
@@ -134,8 +137,8 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
134
137
|
|
|
135
138
|
useEffect(() => {
|
|
136
139
|
const clamped = clampStringArraySelection(selectedIds, maxCount)
|
|
137
|
-
if (clamped !== null)
|
|
138
|
-
}, [maxCount, selectedIds,
|
|
140
|
+
if (clamped !== null) setValueSilent(clamped)
|
|
141
|
+
}, [maxCount, selectedIds, setValueSilent])
|
|
139
142
|
|
|
140
143
|
const emptyHint = useMemo(() => {
|
|
141
144
|
if (!caseSearch) return '未配置案例搜索服务'
|
|
@@ -202,7 +205,10 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
return (
|
|
205
|
-
<div
|
|
208
|
+
<div
|
|
209
|
+
className="form-block-preview skopon-resume-select"
|
|
210
|
+
data-skopon-field={fieldName || undefined}
|
|
211
|
+
>
|
|
206
212
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
207
213
|
{loading && items.length === 0 ? (
|
|
208
214
|
<div className="skopon-resume-select-status">
|
|
@@ -163,7 +163,10 @@ function ResumeCard({
|
|
|
163
163
|
function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
164
164
|
const { interactive } = useA2uiPreviewMode()
|
|
165
165
|
const resumeSearch = useResumeSearch()
|
|
166
|
-
const { value, setValue } = useSkoponBoundField(context
|
|
166
|
+
const { value, setValue, setValueSilent, fieldName } = useSkoponBoundField(context, 'value', {
|
|
167
|
+
component: 'SkoponResumeSelect',
|
|
168
|
+
changeAction: 'click',
|
|
169
|
+
})
|
|
167
170
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
168
171
|
|
|
169
172
|
const label = readString(props.label)
|
|
@@ -187,8 +190,8 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
187
190
|
|
|
188
191
|
useEffect(() => {
|
|
189
192
|
const clamped = clampStringArraySelection(selectedIds, maxCount)
|
|
190
|
-
if (clamped !== null)
|
|
191
|
-
}, [maxCount, selectedIds,
|
|
193
|
+
if (clamped !== null) setValueSilent(clamped)
|
|
194
|
+
}, [maxCount, selectedIds, setValueSilent])
|
|
192
195
|
|
|
193
196
|
const fetchPage = useCallback(
|
|
194
197
|
async (targetPage: number) => {
|
|
@@ -250,7 +253,10 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
250
253
|
const emptyLabel = placeholder || '暂无匹配的简历'
|
|
251
254
|
|
|
252
255
|
return (
|
|
253
|
-
<div
|
|
256
|
+
<div
|
|
257
|
+
className="form-block-preview skopon-resume-select"
|
|
258
|
+
data-skopon-field={fieldName || undefined}
|
|
259
|
+
>
|
|
254
260
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
255
261
|
{loading && items.length === 0 ? (
|
|
256
262
|
<div className="skopon-resume-select-status">
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { useCallback, useSyncExternalStore } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'
|
|
2
2
|
import type { ComponentContext } from '@a2ui/web_core/v0_9'
|
|
3
|
+
import { useFormRuntime } from '../runtime/formRuntimeContext'
|
|
4
|
+
import type { FormRuntimeAction } from '../runtime/types'
|
|
3
5
|
|
|
4
6
|
type DynamicBinding = Parameters<ComponentContext['dataContext']['resolveDynamicValue']>[0]
|
|
5
7
|
|
|
8
|
+
const TEXT_CHANGE_DEBOUNCE_MS = 300
|
|
9
|
+
|
|
6
10
|
function bindingPath(value: unknown): string | null {
|
|
7
11
|
if (value && typeof value === 'object' && 'path' in value) {
|
|
8
12
|
const path = String((value as { path: unknown }).path ?? '').trim()
|
|
@@ -11,6 +15,14 @@ function bindingPath(value: unknown): string | null {
|
|
|
11
15
|
return null
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** 从 path(如 /fieldName)提取字段名。 */
|
|
19
|
+
export function fieldNameFromBindingPath(path: string | null): string | undefined {
|
|
20
|
+
if (!path) return undefined
|
|
21
|
+
const trimmed = path.trim()
|
|
22
|
+
if (!trimmed) return undefined
|
|
23
|
+
return trimmed.startsWith('/') ? trimmed.slice(1) : trimmed
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/** 从 A2UI 组件 context 读取当前绑定字段值(不订阅更新,适合事件回调内读最新值)。 */
|
|
15
27
|
export function readBoundFieldValue(context: ComponentContext, field = 'value'): unknown {
|
|
16
28
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
@@ -20,11 +32,44 @@ export function readBoundFieldValue(context: ComponentContext, field = 'value'):
|
|
|
20
32
|
return context.dataContext.resolveDynamicValue(binding)
|
|
21
33
|
}
|
|
22
34
|
|
|
35
|
+
export interface UseSkoponBoundFieldOptions {
|
|
36
|
+
/** catalog 组件名,用于 runtime event */
|
|
37
|
+
component?: string
|
|
38
|
+
/** setValue 上报的 action,默认 change */
|
|
39
|
+
changeAction?: Extract<FormRuntimeAction, 'change' | 'select' | 'click'>
|
|
40
|
+
/** TextField 等文本输入是否对 change 防抖 */
|
|
41
|
+
debounceChange?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
/** 将 skopon 预览控件绑定到 A2UI surface dataModel(与 basicCatalog binder 行为一致)。 */
|
|
24
|
-
export function useSkoponBoundField(
|
|
45
|
+
export function useSkoponBoundField(
|
|
46
|
+
context: ComponentContext,
|
|
47
|
+
field = 'value',
|
|
48
|
+
options: UseSkoponBoundFieldOptions = {},
|
|
49
|
+
) {
|
|
25
50
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
26
51
|
const binding = props[field] as DynamicBinding | undefined
|
|
27
52
|
const path = bindingPath(binding)
|
|
53
|
+
const fieldName = fieldNameFromBindingPath(path)
|
|
54
|
+
const runtime = useFormRuntime()
|
|
55
|
+
const {
|
|
56
|
+
component = 'Unknown',
|
|
57
|
+
changeAction = 'change',
|
|
58
|
+
debounceChange = false,
|
|
59
|
+
} = options
|
|
60
|
+
|
|
61
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
62
|
+
const runtimeRef = useRef(runtime)
|
|
63
|
+
runtimeRef.current = runtime
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
if (debounceTimerRef.current != null) {
|
|
68
|
+
clearTimeout(debounceTimerRef.current)
|
|
69
|
+
debounceTimerRef.current = null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [])
|
|
28
73
|
|
|
29
74
|
const subscribe = useCallback(
|
|
30
75
|
(onStoreChange: () => void) => {
|
|
@@ -42,7 +87,33 @@ export function useSkoponBoundField(context: ComponentContext, field = 'value')
|
|
|
42
87
|
|
|
43
88
|
const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
44
89
|
|
|
45
|
-
const
|
|
90
|
+
const reportValueChange = useCallback(
|
|
91
|
+
(next: unknown) => {
|
|
92
|
+
if (!fieldName) return
|
|
93
|
+
const emit = () => {
|
|
94
|
+
runtimeRef.current.reportEvent({
|
|
95
|
+
action: changeAction,
|
|
96
|
+
component,
|
|
97
|
+
field: fieldName,
|
|
98
|
+
value: next,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
if (!debounceChange) {
|
|
102
|
+
emit()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (debounceTimerRef.current != null) {
|
|
106
|
+
clearTimeout(debounceTimerRef.current)
|
|
107
|
+
}
|
|
108
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
109
|
+
debounceTimerRef.current = null
|
|
110
|
+
emit()
|
|
111
|
+
}, TEXT_CHANGE_DEBOUNCE_MS)
|
|
112
|
+
},
|
|
113
|
+
[fieldName, changeAction, component, debounceChange],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const setValueSilent = useCallback(
|
|
46
117
|
(next: unknown) => {
|
|
47
118
|
if (!path || !binding || typeof binding !== 'object' || !('path' in binding)) return
|
|
48
119
|
context.dataContext.set(String((binding as { path: string }).path), next)
|
|
@@ -50,7 +121,32 @@ export function useSkoponBoundField(context: ComponentContext, field = 'value')
|
|
|
50
121
|
[context, binding, path],
|
|
51
122
|
)
|
|
52
123
|
|
|
53
|
-
|
|
124
|
+
const setValue = useCallback(
|
|
125
|
+
(next: unknown) => {
|
|
126
|
+
setValueSilent(next)
|
|
127
|
+
reportValueChange(next)
|
|
128
|
+
},
|
|
129
|
+
[setValueSilent, reportValueChange],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const reportFocus = useCallback(() => {
|
|
133
|
+
if (!fieldName) return
|
|
134
|
+
runtimeRef.current.reportEvent({
|
|
135
|
+
action: 'focus',
|
|
136
|
+
component,
|
|
137
|
+
field: fieldName,
|
|
138
|
+
})
|
|
139
|
+
}, [fieldName, component])
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
value,
|
|
143
|
+
setValue,
|
|
144
|
+
/** 写入 dataModel 但不上报(用于模式切换规范化等内部同步) */
|
|
145
|
+
setValueSilent,
|
|
146
|
+
hasBinding: !!path,
|
|
147
|
+
fieldName,
|
|
148
|
+
reportFocus,
|
|
149
|
+
}
|
|
54
150
|
}
|
|
55
151
|
|
|
56
152
|
export function asOptionalString(value: unknown): string | undefined {
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
|
|
7
|
+
DEFAULT_AUTH_REALTIME_WS_URL_FIELD,
|
|
7
8
|
DEFAULT_ANSWER_AGENT_CALLBACK_URL,
|
|
8
9
|
DEFAULT_AUTH_STORAGE_KEY,
|
|
9
10
|
resolveAnswerAgentCallbackUrl,
|
|
11
|
+
resolveRealtimeWsUrl,
|
|
10
12
|
resolveSkoponAuthHeaders,
|
|
13
|
+
resolveSubmitAuthHeaders,
|
|
11
14
|
} from './authStorage'
|
|
12
15
|
|
|
13
16
|
describe('resolveSkoponAuthHeaders', () => {
|
|
@@ -63,6 +66,33 @@ describe('resolveSkoponAuthHeaders', () => {
|
|
|
63
66
|
})
|
|
64
67
|
})
|
|
65
68
|
|
|
69
|
+
describe('resolveSubmitAuthHeaders', () => {
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
sessionStorage.clear()
|
|
72
|
+
localStorage.clear()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('merges session auth with explicit headers, explicit wins', () => {
|
|
76
|
+
sessionStorage.setItem(
|
|
77
|
+
DEFAULT_AUTH_STORAGE_KEY,
|
|
78
|
+
JSON.stringify({ token: 'session-tok' }),
|
|
79
|
+
)
|
|
80
|
+
expect(
|
|
81
|
+
resolveSubmitAuthHeaders({ Authorization: 'Bearer explicit-tok' }),
|
|
82
|
+
).toEqual({ Authorization: 'Bearer explicit-tok' })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('falls back to session auth when explicit headers missing', () => {
|
|
86
|
+
sessionStorage.setItem(
|
|
87
|
+
DEFAULT_AUTH_STORAGE_KEY,
|
|
88
|
+
JSON.stringify({ token: 'session-tok' }),
|
|
89
|
+
)
|
|
90
|
+
expect(resolveSubmitAuthHeaders()).toEqual({
|
|
91
|
+
Authorization: 'Bearer session-tok',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
66
96
|
describe('resolveAnswerAgentCallbackUrl', () => {
|
|
67
97
|
afterEach(() => {
|
|
68
98
|
sessionStorage.clear()
|
|
@@ -86,7 +116,50 @@ describe('resolveAnswerAgentCallbackUrl', () => {
|
|
|
86
116
|
expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
|
|
87
117
|
})
|
|
88
118
|
|
|
89
|
-
it('returns
|
|
119
|
+
it('returns empty string when session is missing', () => {
|
|
120
|
+
expect(resolveAnswerAgentCallbackUrl()).toBe('')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('returns default mock url when field is empty string', () => {
|
|
124
|
+
sessionStorage.setItem(
|
|
125
|
+
DEFAULT_AUTH_STORAGE_KEY,
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
token: 'tok',
|
|
128
|
+
[DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD]: ' ',
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
90
131
|
expect(resolveAnswerAgentCallbackUrl()).toBe(DEFAULT_ANSWER_AGENT_CALLBACK_URL)
|
|
91
132
|
})
|
|
92
133
|
})
|
|
134
|
+
|
|
135
|
+
describe('resolveRealtimeWsUrl', () => {
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
sessionStorage.clear()
|
|
138
|
+
localStorage.clear()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('reads realtime_ws_url from sessionStorage auth_user', () => {
|
|
142
|
+
sessionStorage.setItem(
|
|
143
|
+
DEFAULT_AUTH_STORAGE_KEY,
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
token: 'tok',
|
|
146
|
+
[DEFAULT_AUTH_REALTIME_WS_URL_FIELD]: 'wss://example.com/ws',
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
expect(resolveRealtimeWsUrl()).toBe('wss://example.com/ws')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('returns empty string when session is missing', () => {
|
|
153
|
+
expect(resolveRealtimeWsUrl()).toBe('')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('returns empty string when field is missing or blank', () => {
|
|
157
|
+
sessionStorage.setItem(DEFAULT_AUTH_STORAGE_KEY, JSON.stringify({ token: 'tok' }))
|
|
158
|
+
expect(resolveRealtimeWsUrl()).toBe('')
|
|
159
|
+
sessionStorage.setItem(
|
|
160
|
+
DEFAULT_AUTH_STORAGE_KEY,
|
|
161
|
+
JSON.stringify({ token: 'tok', [DEFAULT_AUTH_REALTIME_WS_URL_FIELD]: ' ' }),
|
|
162
|
+
)
|
|
163
|
+
expect(resolveRealtimeWsUrl()).toBe('')
|
|
164
|
+
})
|
|
165
|
+
})
|