@skopon-cool/form-sdk 0.4.8 → 0.5.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/README.md +26 -1
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/adapter/formWeb.d.ts +14 -0
- package/dist/adapter/formWeb.d.ts.map +1 -0
- package/dist/blocks/registry.d.ts.map +1 -1
- package/dist/blocks/web/adapter.d.ts +8 -0
- package/dist/blocks/web/adapter.d.ts.map +1 -0
- package/dist/blocks/web/catalog.d.ts +9 -0
- package/dist/blocks/web/catalog.d.ts.map +1 -0
- package/dist/blocks/web/index.d.ts +3 -0
- package/dist/blocks/web/index.d.ts.map +1 -0
- package/dist/client/authStorage.d.ts +1 -1
- package/dist/client/authStorage.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1364 -1182
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts +2 -2
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
- package/dist/types/index.d.ts +7 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +39 -0
- package/src/adapter/formSchema.test.ts +38 -0
- package/src/adapter/formSchema.ts +16 -1
- package/src/adapter/formWeb.test.ts +38 -0
- package/src/adapter/formWeb.ts +41 -0
- package/src/blocks/registry.ts +6 -1
- package/src/blocks/web/adapter.ts +36 -0
- package/src/blocks/web/catalog.test.tsx +95 -0
- package/src/blocks/web/catalog.tsx +158 -0
- package/src/blocks/web/index.ts +11 -0
- package/src/client/authStorage.ts +1 -1
- package/src/components/AskUserFormCard.tsx +2 -1
- package/src/index.ts +9 -0
- package/src/styles/index.css +87 -0
- package/src/submit/intersectPayloadBlocksWithForm.ts +6 -5
- package/src/submit/submit.test.ts +14 -0
- package/src/submit/validateFormSubmit.ts +2 -2
- package/src/types/index.ts +12 -2
|
@@ -9,7 +9,7 @@ export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' |
|
|
|
9
9
|
export interface PayloadBlocksIntersection {
|
|
10
10
|
matchedBlocks: FormBlock[];
|
|
11
11
|
extraBlocks: FormBlock[];
|
|
12
|
-
/** 按 payload 顺序:layout + matched input(不含 extra input) */
|
|
12
|
+
/** 按 payload 顺序:layout/media + matched input(不含 extra input) */
|
|
13
13
|
renderBlocks: FormBlock[];
|
|
14
14
|
title: string;
|
|
15
15
|
description: string;
|
|
@@ -25,7 +25,7 @@ export declare function intersectPayloadBlocksWithForm(payloadDef: PayloadBlocks
|
|
|
25
25
|
export declare function extractExtraBlockValues(extraBlocks: FormBlock[]): Record<string, unknown>;
|
|
26
26
|
/** payload 是否含至少一个具 name 的 input 块(可用于卡片 fallback) */
|
|
27
27
|
export declare function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): boolean;
|
|
28
|
-
/** 交集为空时 fallback:payload 中可上屏的 blocks(layout + 非 hidden input) */
|
|
28
|
+
/** 交集为空时 fallback:payload 中可上屏的 blocks(layout/media + 非 hidden input) */
|
|
29
29
|
export declare function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[];
|
|
30
30
|
/** payload 中所有 input 块 field name(fallback 提交用,含 hidden) */
|
|
31
31
|
export declare function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intersectPayloadBlocksWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadBlocksWithForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAC,GAAG;IAC3F;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;CACxB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,
|
|
1
|
+
{"version":3,"file":"intersectPayloadBlocksWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadBlocksWithForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAC,GAAG;IAC3F;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;CACxB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,gEAAgE;IAChE,YAAY,EAAE,SAAS,EAAE,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AA+ED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAmBvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CA+B3B;AA+BD,2CAA2C;AAC3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzF;AAED,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAIlF;AAED,yEAAyE;AACzE,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAO3F;AAED,4DAA4D;AAC5D,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,uBAAuB,GAAG,MAAM,EAAE,CAIvF;AAED,6DAA6D;AAC7D,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,uBAAuB,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export type A2uiBinding<T> = T | {
|
|
5
5
|
path: string;
|
|
6
6
|
};
|
|
7
|
-
export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
|
|
7
|
+
export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponWeb' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
|
|
8
8
|
export interface A2uiComponentNode {
|
|
9
9
|
id: string;
|
|
10
10
|
component: A2uiComponentName | string;
|
|
@@ -23,7 +23,7 @@ export declare const A2UI_PROTOCOL_VERSION: "v0.9";
|
|
|
23
23
|
export type FormJsonSchema = Record<string, unknown>;
|
|
24
24
|
export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'resume_multiselect' | 'case_multiselect'
|
|
25
25
|
/** @deprecated 加载时自动迁移为 case_multiselect */
|
|
26
|
-
| 'case_singleselect' | 'image' | 'video' | 'audio';
|
|
26
|
+
| 'case_singleselect' | 'image' | 'video' | 'audio' | 'web';
|
|
27
27
|
export type FormMediaSize = 'huge' | 'large' | 'medium' | 'small' | 'icon';
|
|
28
28
|
export type FormFilePlaceholderIcon = 'video' | 'audio' | 'image' | 'file' | 'spreadsheet' | 'document';
|
|
29
29
|
export declare const FORM_MEDIA_SIZES: FormMediaSize[];
|
|
@@ -72,6 +72,10 @@ export interface FormBlock {
|
|
|
72
72
|
mediaUrl?: string;
|
|
73
73
|
mediaUrls?: string[];
|
|
74
74
|
mediaSize?: FormMediaSize;
|
|
75
|
+
/** 网页展示块:iframe 地址,仅支持 HTTP(S) 或同源相对地址 */
|
|
76
|
+
webUrl?: string;
|
|
77
|
+
/** 网页展示块:行内 iframe 高度(px) */
|
|
78
|
+
webHeight?: number;
|
|
75
79
|
fileAcceptTypes?: string[];
|
|
76
80
|
fileAcceptExtensions?: string[];
|
|
77
81
|
filePlaceholderIcon?: FormFilePlaceholderIcon;
|
|
@@ -109,6 +113,7 @@ export interface FormSchema {
|
|
|
109
113
|
jsonSchema: FormJsonSchema;
|
|
110
114
|
}
|
|
111
115
|
export declare function isMediaBlockType(type: FormBlockType): boolean;
|
|
116
|
+
export declare function isWebBlockType(type: FormBlockType): boolean;
|
|
112
117
|
export declare function isLayoutBlockType(type: FormBlockType): boolean;
|
|
113
118
|
export declare function isInputBlockType(type: FormBlockType): boolean;
|
|
114
119
|
export declare function isDisplayBlockType(type: FormBlockType): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,KAAK,GACL,MAAM,GACN,OAAO,GACP,OAAO,GACP,aAAa,GACb,WAAW,GACX,UAAU,GACV,cAAc,GACd,eAAe,GACf,YAAY,GACZ,aAAa,GACb,cAAc,GACd,oBAAoB,GACpB,kBAAkB,CAAA;AAEtB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,iBAAiB,GAAG,MAAM,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,iBAAiB,EAAE,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAC1E;AAED,eAAO,MAAM,qBAAqB,EAAG,MAAe,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEpD,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,WAAW,GACX,MAAM,GACN,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,aAAa,GACb,OAAO,GACP,UAAU,GACV,QAAQ,GACR,KAAK,GACL,KAAK,GACL,UAAU,GACV,MAAM,GACN,MAAM,GACN,oBAAoB,GACpB,kBAAkB;AACpB,4CAA4C;GAC1C,mBAAmB,GACnB,OAAO,GACP,OAAO,GACP,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -88,6 +88,45 @@ describe('a2uiAdapter blocks <-> surface', () => {
|
|
|
88
88
|
expect(imgNode?.mediaSize).toBe('large')
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
it('round-trips web blocks through SkoponWeb', () => {
|
|
92
|
+
const doc = blocksToA2ui(
|
|
93
|
+
{
|
|
94
|
+
title: '',
|
|
95
|
+
description: '',
|
|
96
|
+
blocks: [
|
|
97
|
+
{
|
|
98
|
+
id: 'b-web',
|
|
99
|
+
type: 'web',
|
|
100
|
+
label: '官网',
|
|
101
|
+
help: '可扩展查看',
|
|
102
|
+
webUrl: 'https://example.com/page',
|
|
103
|
+
webHeight: 480,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{ includeHeader: false },
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const node = doc.components.find((component) => component.id === 'b-web')
|
|
111
|
+
expect(node).toMatchObject({
|
|
112
|
+
component: 'SkoponWeb',
|
|
113
|
+
url: 'https://example.com/page',
|
|
114
|
+
height: 480,
|
|
115
|
+
label: '官网',
|
|
116
|
+
help: '可扩展查看',
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const back = a2uiToBlocks(doc)
|
|
120
|
+
expect(back.blocks[0]).toMatchObject({
|
|
121
|
+
id: 'b-web',
|
|
122
|
+
type: 'web',
|
|
123
|
+
label: '官网',
|
|
124
|
+
help: '可扩展查看',
|
|
125
|
+
webUrl: 'https://example.com/page',
|
|
126
|
+
webHeight: 480,
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
91
130
|
it('blocksToA2ui omits hidden text controls but keeps dataModel defaults', () => {
|
|
92
131
|
const doc = blocksToA2ui(
|
|
93
132
|
{
|
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
import { describe, it, expect } from 'vitest'
|
|
5
5
|
import {
|
|
6
6
|
coerceToggleValue,
|
|
7
|
+
createEmptyBlock,
|
|
7
8
|
normalizeFormDefinition,
|
|
8
9
|
blocksToJsonSchema,
|
|
9
10
|
} from './formSchema'
|
|
10
11
|
import { mapFormDefinitionFromDto } from './resolveSurface'
|
|
12
|
+
import {
|
|
13
|
+
isDisplayBlockType,
|
|
14
|
+
isInputBlockType,
|
|
15
|
+
isMediaBlockType,
|
|
16
|
+
isWebBlockType,
|
|
17
|
+
} from '../types/index'
|
|
11
18
|
|
|
12
19
|
describe('coerceToggleValue', () => {
|
|
13
20
|
it('accepts boolean, string, and numeric truthy/falsy values', () => {
|
|
@@ -128,6 +135,37 @@ describe('normalizeFormDefinition preserves needLLM and editor fields', () => {
|
|
|
128
135
|
})
|
|
129
136
|
})
|
|
130
137
|
|
|
138
|
+
describe('web display block', () => {
|
|
139
|
+
it('creates and normalizes web defaults without treating web as input or media', () => {
|
|
140
|
+
const empty = createEmptyBlock('web')
|
|
141
|
+
expect(empty.webUrl).toBe('')
|
|
142
|
+
expect(empty.webHeight).toBe(360)
|
|
143
|
+
expect(isWebBlockType(empty.type)).toBe(true)
|
|
144
|
+
expect(isDisplayBlockType(empty.type)).toBe(true)
|
|
145
|
+
expect(isInputBlockType(empty.type)).toBe(false)
|
|
146
|
+
expect(isMediaBlockType(empty.type)).toBe(false)
|
|
147
|
+
|
|
148
|
+
const normalized = normalizeFormDefinition({
|
|
149
|
+
title: '',
|
|
150
|
+
description: '',
|
|
151
|
+
blocks: [
|
|
152
|
+
{
|
|
153
|
+
id: 'web-1',
|
|
154
|
+
type: 'web',
|
|
155
|
+
name: 'must_not_submit',
|
|
156
|
+
label: '官网',
|
|
157
|
+
webUrl: ' javascript:alert(1) ',
|
|
158
|
+
webHeight: 100,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
jsonSchema: {},
|
|
162
|
+
})
|
|
163
|
+
expect(normalized.blocks[0]?.webUrl).toBe('javascript:alert(1)')
|
|
164
|
+
expect(normalized.blocks[0]?.webHeight).toBe(200)
|
|
165
|
+
expect(normalized.jsonSchema).toMatchObject({ properties: {}, required: [] })
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
131
169
|
describe('mapFormDefinitionFromDto jsonSchema', () => {
|
|
132
170
|
it('regenerates jsonSchema from DTO blocks', () => {
|
|
133
171
|
const def = mapFormDefinitionFromDto({
|
|
@@ -6,7 +6,12 @@ import type {
|
|
|
6
6
|
FormJsonSchema,
|
|
7
7
|
FormSchema,
|
|
8
8
|
} from '../types/index'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isInputBlockType,
|
|
11
|
+
isMediaBlockType,
|
|
12
|
+
isOptionsBlockType,
|
|
13
|
+
isWebBlockType,
|
|
14
|
+
} from '../types/index'
|
|
10
15
|
import { generateId } from './id'
|
|
11
16
|
import {
|
|
12
17
|
filterFileAcceptExtensionsForTypes,
|
|
@@ -17,6 +22,7 @@ import {
|
|
|
17
22
|
normalizeMediaSize,
|
|
18
23
|
normalizeMediaUrls,
|
|
19
24
|
} from './formMedia'
|
|
25
|
+
import { FORM_WEB_DEFAULT_HEIGHT, normalizeWebHeight } from './formWeb'
|
|
20
26
|
|
|
21
27
|
const DEFAULT_MEDIA_URL_MAX = 10
|
|
22
28
|
|
|
@@ -162,6 +168,8 @@ function normalizeBlock(raw: FormBlock): FormBlock {
|
|
|
162
168
|
? normalizeMediaUrls(migrated, DEFAULT_MEDIA_URL_MAX)
|
|
163
169
|
: undefined,
|
|
164
170
|
mediaSize: isMediaBlockType(type) ? normalizeMediaSize(migrated.mediaSize) : undefined,
|
|
171
|
+
webUrl: isWebBlockType(type) ? migrated.webUrl?.trim() ?? '' : undefined,
|
|
172
|
+
webHeight: isWebBlockType(type) ? normalizeWebHeight(migrated.webHeight) : undefined,
|
|
165
173
|
fileAcceptTypes,
|
|
166
174
|
fileAcceptExtensions: filterFileAcceptExtensionsForTypes(
|
|
167
175
|
fileAcceptTypes,
|
|
@@ -425,6 +433,13 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
|
|
|
425
433
|
block.mediaSize = 'medium'
|
|
426
434
|
}
|
|
427
435
|
|
|
436
|
+
if (isWebBlockType(type)) {
|
|
437
|
+
block.label = ''
|
|
438
|
+
block.help = ''
|
|
439
|
+
block.webUrl = ''
|
|
440
|
+
block.webHeight = FORM_WEB_DEFAULT_HEIGHT
|
|
441
|
+
}
|
|
442
|
+
|
|
428
443
|
if (isOptionsBlockType(type)) {
|
|
429
444
|
block.options = [
|
|
430
445
|
{ value: 'option_1', label: '选项 1' },
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
FORM_WEB_DEFAULT_HEIGHT,
|
|
7
|
+
FORM_WEB_MAX_HEIGHT,
|
|
8
|
+
FORM_WEB_MIN_HEIGHT,
|
|
9
|
+
isSupportedWebUrl,
|
|
10
|
+
normalizeWebHeight,
|
|
11
|
+
} from './formWeb'
|
|
12
|
+
|
|
13
|
+
describe('formWeb', () => {
|
|
14
|
+
it('normalizes iframe height to the supported integer range', () => {
|
|
15
|
+
expect(normalizeWebHeight(undefined)).toBe(FORM_WEB_DEFAULT_HEIGHT)
|
|
16
|
+
expect(normalizeWebHeight('bad')).toBe(FORM_WEB_DEFAULT_HEIGHT)
|
|
17
|
+
expect(normalizeWebHeight(199)).toBe(FORM_WEB_MIN_HEIGHT)
|
|
18
|
+
expect(normalizeWebHeight(480.9)).toBe(480)
|
|
19
|
+
expect(normalizeWebHeight(1201)).toBe(FORM_WEB_MAX_HEIGHT)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('accepts HTTP(S) and explicit same-origin relative paths', () => {
|
|
23
|
+
expect(isSupportedWebUrl('https://example.com/page')).toBe(true)
|
|
24
|
+
expect(isSupportedWebUrl('http://localhost:3000/preview')).toBe(true)
|
|
25
|
+
expect(isSupportedWebUrl('/preview')).toBe(true)
|
|
26
|
+
expect(isSupportedWebUrl('./preview')).toBe(true)
|
|
27
|
+
expect(isSupportedWebUrl('../preview')).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('rejects empty, dangerous, protocol-relative, and ambiguous paths', () => {
|
|
31
|
+
expect(isSupportedWebUrl('')).toBe(false)
|
|
32
|
+
expect(isSupportedWebUrl('javascript:alert(1)')).toBe(false)
|
|
33
|
+
expect(isSupportedWebUrl('data:text/html,test')).toBe(false)
|
|
34
|
+
expect(isSupportedWebUrl('//example.com/page')).toBe(false)
|
|
35
|
+
expect(isSupportedWebUrl('preview.html')).toBe(false)
|
|
36
|
+
expect(isSupportedWebUrl('https://')).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const FORM_WEB_DEFAULT_HEIGHT = 360
|
|
2
|
+
export const FORM_WEB_MIN_HEIGHT = 200
|
|
3
|
+
export const FORM_WEB_MAX_HEIGHT = 1200
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 网页展示高度统一持久化为整数像素。
|
|
7
|
+
* 空值或非数字回退默认值,越界值限制到允许范围。
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeWebHeight(raw: unknown): number {
|
|
10
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
11
|
+
return FORM_WEB_DEFAULT_HEIGHT
|
|
12
|
+
}
|
|
13
|
+
const numeric = typeof raw === 'number' ? raw : Number(raw)
|
|
14
|
+
if (!Number.isFinite(numeric)) return FORM_WEB_DEFAULT_HEIGHT
|
|
15
|
+
return Math.min(
|
|
16
|
+
FORM_WEB_MAX_HEIGHT,
|
|
17
|
+
Math.max(FORM_WEB_MIN_HEIGHT, Math.floor(numeric)),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* iframe 仅允许 HTTP(S) 绝对地址,或明确的同源相对路径。
|
|
23
|
+
* 不接受 protocol-relative、data、javascript 等地址。
|
|
24
|
+
*/
|
|
25
|
+
export function isSupportedWebUrl(raw: unknown): raw is string {
|
|
26
|
+
if (typeof raw !== 'string') return false
|
|
27
|
+
const value = raw.trim()
|
|
28
|
+
if (!value) return false
|
|
29
|
+
|
|
30
|
+
if (/^\/(?!\/)/.test(value) || /^\.\.?\//.test(value)) {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
if (!/^https?:\/\//i.test(value)) return false
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value)
|
|
37
|
+
return (url.protocol === 'http:' || url.protocol === 'https:') && Boolean(url.hostname)
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/blocks/registry.ts
CHANGED
|
@@ -2,8 +2,13 @@ import type { FormBlockType } from '../types/index'
|
|
|
2
2
|
import type { BlockAdapterPlugin } from './types'
|
|
3
3
|
import { caseMultiselectAdapter } from './case_multiselect'
|
|
4
4
|
import { resumeMultiselectAdapter } from './resume_multiselect'
|
|
5
|
+
import { webAdapter } from './web'
|
|
5
6
|
|
|
6
|
-
const adapters: BlockAdapterPlugin[] = [
|
|
7
|
+
const adapters: BlockAdapterPlugin[] = [
|
|
8
|
+
resumeMultiselectAdapter,
|
|
9
|
+
caseMultiselectAdapter,
|
|
10
|
+
webAdapter,
|
|
11
|
+
]
|
|
7
12
|
|
|
8
13
|
const adapterByType = new Map<FormBlockType, BlockAdapterPlugin>(
|
|
9
14
|
adapters.map((adapter) => [adapter.type, adapter]),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { A2uiComponentNode, FormBlock } from '../../types/index'
|
|
2
|
+
import { normalizeWebHeight } from '../../adapter/formWeb'
|
|
3
|
+
import type { BlockAdapterContext, BlockToComponentResult } from '../types'
|
|
4
|
+
|
|
5
|
+
export function webToComponent(
|
|
6
|
+
block: FormBlock,
|
|
7
|
+
ctx: BlockAdapterContext,
|
|
8
|
+
): BlockToComponentResult {
|
|
9
|
+
return {
|
|
10
|
+
node: {
|
|
11
|
+
id: ctx.id,
|
|
12
|
+
component: 'SkoponWeb',
|
|
13
|
+
url: block.webUrl?.trim() ?? '',
|
|
14
|
+
height: normalizeWebHeight(block.webHeight),
|
|
15
|
+
label: ctx.label,
|
|
16
|
+
help: block.help ?? '',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function webFromComponent(
|
|
22
|
+
node: A2uiComponentNode,
|
|
23
|
+
base: (type: 'web') => FormBlock,
|
|
24
|
+
helpers: {
|
|
25
|
+
asLiteral: (value: unknown) => string | undefined
|
|
26
|
+
nameFromValue: (value: unknown) => string
|
|
27
|
+
},
|
|
28
|
+
): FormBlock {
|
|
29
|
+
const help = helpers.asLiteral(node.help)?.trim()
|
|
30
|
+
return {
|
|
31
|
+
...base('web'),
|
|
32
|
+
webUrl: helpers.asLiteral(node.url)?.trim() ?? '',
|
|
33
|
+
webHeight: normalizeWebHeight(node.height),
|
|
34
|
+
...(help ? { help } : {}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { cleanup, fireEvent, render } from '@testing-library/react'
|
|
4
|
+
import { buildSkoponCatalog } from '../../catalog/a2uiCustomCatalog'
|
|
5
|
+
import { SkoponWebPreview } from './catalog'
|
|
6
|
+
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
const getComputedStyle = window.getComputedStyle.bind(window)
|
|
9
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((element) =>
|
|
10
|
+
getComputedStyle(element),
|
|
11
|
+
)
|
|
12
|
+
class ResizeObserverMock {
|
|
13
|
+
observe() {}
|
|
14
|
+
unobserve() {}
|
|
15
|
+
disconnect() {}
|
|
16
|
+
}
|
|
17
|
+
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
|
18
|
+
vi.stubGlobal(
|
|
19
|
+
'matchMedia',
|
|
20
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
21
|
+
matches: false,
|
|
22
|
+
media: query,
|
|
23
|
+
onchange: null,
|
|
24
|
+
addListener: vi.fn(),
|
|
25
|
+
removeListener: vi.fn(),
|
|
26
|
+
addEventListener: vi.fn(),
|
|
27
|
+
removeEventListener: vi.fn(),
|
|
28
|
+
dispatchEvent: vi.fn(),
|
|
29
|
+
})),
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
cleanup()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function activeFrames(url: string): HTMLIFrameElement[] {
|
|
38
|
+
return Array.from(document.querySelectorAll<HTMLIFrameElement>('iframe')).filter(
|
|
39
|
+
(frame) => frame.getAttribute('src') === url,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('SkoponWebPreview', () => {
|
|
44
|
+
it('is registered in the merged Skopon A2UI catalog', () => {
|
|
45
|
+
const catalog = buildSkoponCatalog()
|
|
46
|
+
expect(
|
|
47
|
+
[...catalog.components.values()].some((component) => component.name === 'SkoponWeb'),
|
|
48
|
+
).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders one inline iframe and moves the active URL into the modal', () => {
|
|
52
|
+
const url = 'https://example.com/page'
|
|
53
|
+
const view = render(
|
|
54
|
+
<SkoponWebPreview
|
|
55
|
+
url={url}
|
|
56
|
+
height={480}
|
|
57
|
+
label="官网"
|
|
58
|
+
help="可扩展查看"
|
|
59
|
+
/>,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const inlineFrame = view.getByTitle('官网') as HTMLIFrameElement
|
|
63
|
+
expect(inlineFrame.style.height).toBe('480px')
|
|
64
|
+
expect(inlineFrame.getAttribute('loading')).toBe('lazy')
|
|
65
|
+
expect(activeFrames(url)).toHaveLength(1)
|
|
66
|
+
expect(view.getByText('可扩展查看')).toBeTruthy()
|
|
67
|
+
|
|
68
|
+
fireEvent.click(view.getByRole('button', { name: '扩展显示' }))
|
|
69
|
+
|
|
70
|
+
expect(document.querySelector('.skopon-web-modal')).toBeTruthy()
|
|
71
|
+
expect(view.queryByTitle('官网')).toBeNull()
|
|
72
|
+
expect(document.querySelector('iframe[title="官网(扩展显示)"]')).toBeTruthy()
|
|
73
|
+
expect(activeFrames(url)).toHaveLength(1)
|
|
74
|
+
|
|
75
|
+
const closeButton = document.querySelector<HTMLButtonElement>('.ant-modal-close')
|
|
76
|
+
expect(closeButton).toBeTruthy()
|
|
77
|
+
if (closeButton) fireEvent.click(closeButton)
|
|
78
|
+
|
|
79
|
+
expect(activeFrames(url)).toHaveLength(1)
|
|
80
|
+
expect(view.getByTitle('官网')).toBeTruthy()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('shows distinct placeholders and never passes unsupported URLs to iframe', () => {
|
|
84
|
+
const emptyView = render(<SkoponWebPreview url="" />)
|
|
85
|
+
expect(emptyView.getByText('未设置网页 URL')).toBeTruthy()
|
|
86
|
+
expect(document.querySelector('iframe')).toBeNull()
|
|
87
|
+
expect(emptyView.queryByRole('button', { name: '扩展显示' })).toBeNull()
|
|
88
|
+
emptyView.unmount()
|
|
89
|
+
|
|
90
|
+
const invalidView = render(<SkoponWebPreview url="javascript:alert(1)" />)
|
|
91
|
+
expect(invalidView.getByText('网页 URL 无效')).toBeTruthy()
|
|
92
|
+
expect(document.querySelector('iframe')).toBeNull()
|
|
93
|
+
expect(invalidView.queryByRole('button', { name: '扩展显示' })).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { FullscreenOutlined } from '@ant-design/icons'
|
|
2
|
+
import { Button, Modal, Tooltip, Typography } from 'antd'
|
|
3
|
+
import { useRef, useState, type Ref } from 'react'
|
|
4
|
+
import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { isSupportedWebUrl, normalizeWebHeight } from '../../adapter/formWeb'
|
|
7
|
+
|
|
8
|
+
export interface SkoponWebPreviewProps {
|
|
9
|
+
url?: string
|
|
10
|
+
height?: number
|
|
11
|
+
label?: string
|
|
12
|
+
help?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function WebFrame({
|
|
16
|
+
url,
|
|
17
|
+
title,
|
|
18
|
+
className,
|
|
19
|
+
height,
|
|
20
|
+
frameRef,
|
|
21
|
+
}: {
|
|
22
|
+
url: string
|
|
23
|
+
title: string
|
|
24
|
+
className: string
|
|
25
|
+
height?: number
|
|
26
|
+
frameRef?: Ref<HTMLIFrameElement>
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<iframe
|
|
30
|
+
ref={frameRef}
|
|
31
|
+
src={url}
|
|
32
|
+
title={title}
|
|
33
|
+
className={className}
|
|
34
|
+
style={height === undefined ? undefined : { height }}
|
|
35
|
+
loading="lazy"
|
|
36
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
37
|
+
allowFullScreen
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SkoponWebPreview({
|
|
43
|
+
url = '',
|
|
44
|
+
height,
|
|
45
|
+
label,
|
|
46
|
+
help,
|
|
47
|
+
}: SkoponWebPreviewProps) {
|
|
48
|
+
const [expanded, setExpanded] = useState(false)
|
|
49
|
+
const [inlineActive, setInlineActive] = useState(true)
|
|
50
|
+
const modalFrameRef = useRef<HTMLIFrameElement>(null)
|
|
51
|
+
const trimmedUrl = url.trim()
|
|
52
|
+
const supportedUrl = isSupportedWebUrl(trimmedUrl) ? trimmedUrl : null
|
|
53
|
+
const resolvedHeight = normalizeWebHeight(height)
|
|
54
|
+
const title = label?.trim() || '网页展示'
|
|
55
|
+
const helpText = help?.trim()
|
|
56
|
+
const emptyText = trimmedUrl ? '网页 URL 无效' : '未设置网页 URL'
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="form-block-preview skopon-web-preview">
|
|
60
|
+
{label?.trim() ? <div className="form-block-preview-label">{label.trim()}</div> : null}
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
className={`skopon-web-frame-shell${supportedUrl ? '' : ' skopon-web-frame-shell--empty'}`}
|
|
64
|
+
style={{ height: resolvedHeight }}
|
|
65
|
+
>
|
|
66
|
+
{supportedUrl && inlineActive ? (
|
|
67
|
+
<WebFrame
|
|
68
|
+
url={supportedUrl}
|
|
69
|
+
title={title}
|
|
70
|
+
className="skopon-web-frame skopon-web-frame--inline"
|
|
71
|
+
height={resolvedHeight}
|
|
72
|
+
/>
|
|
73
|
+
) : null}
|
|
74
|
+
|
|
75
|
+
{!supportedUrl ? (
|
|
76
|
+
<div className="skopon-web-empty" role="status">
|
|
77
|
+
{emptyText}
|
|
78
|
+
</div>
|
|
79
|
+
) : null}
|
|
80
|
+
|
|
81
|
+
{supportedUrl ? (
|
|
82
|
+
<Tooltip title="扩展显示">
|
|
83
|
+
<Button
|
|
84
|
+
type="default"
|
|
85
|
+
shape="circle"
|
|
86
|
+
className="skopon-web-expand-button"
|
|
87
|
+
icon={<FullscreenOutlined />}
|
|
88
|
+
aria-label="扩展显示"
|
|
89
|
+
onClick={() => {
|
|
90
|
+
setInlineActive(false)
|
|
91
|
+
setExpanded(true)
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</Tooltip>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{helpText ? (
|
|
99
|
+
<Typography.Text type="secondary" className="form-block-preview-help">
|
|
100
|
+
{helpText}
|
|
101
|
+
</Typography.Text>
|
|
102
|
+
) : null}
|
|
103
|
+
|
|
104
|
+
<Modal
|
|
105
|
+
open={expanded}
|
|
106
|
+
title={title}
|
|
107
|
+
footer={null}
|
|
108
|
+
width="min(1200px, calc(100vw - 32px))"
|
|
109
|
+
className="skopon-web-modal"
|
|
110
|
+
destroyOnHidden
|
|
111
|
+
mask={{ closable: true }}
|
|
112
|
+
onCancel={() => {
|
|
113
|
+
// rc-dialog 在关闭动画期间会缓存子树;先清掉 src,避免行内 iframe
|
|
114
|
+
// 恢复后与缓存中的弹窗 iframe 同时继续运行。
|
|
115
|
+
modalFrameRef.current?.removeAttribute('src')
|
|
116
|
+
setExpanded(false)
|
|
117
|
+
setInlineActive(true)
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{supportedUrl && !inlineActive ? (
|
|
121
|
+
<WebFrame
|
|
122
|
+
url={supportedUrl}
|
|
123
|
+
title={`${title}(扩展显示)`}
|
|
124
|
+
className="skopon-web-frame skopon-web-frame--modal"
|
|
125
|
+
frameRef={modalFrameRef}
|
|
126
|
+
/>
|
|
127
|
+
) : null}
|
|
128
|
+
</Modal>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const SkoponWebApi = {
|
|
134
|
+
name: 'SkoponWeb',
|
|
135
|
+
schema: z
|
|
136
|
+
.object({
|
|
137
|
+
url: z.any().optional(),
|
|
138
|
+
height: z.any().optional(),
|
|
139
|
+
label: z.any().optional(),
|
|
140
|
+
help: z.any().optional(),
|
|
141
|
+
})
|
|
142
|
+
.passthrough(),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const SkoponWebImpl = createBinderlessComponentImplementation(
|
|
146
|
+
SkoponWebApi,
|
|
147
|
+
({ context }) => {
|
|
148
|
+
const props = context.componentModel.properties as Record<string, unknown>
|
|
149
|
+
return (
|
|
150
|
+
<SkoponWebPreview
|
|
151
|
+
url={typeof props.url === 'string' ? props.url : ''}
|
|
152
|
+
height={normalizeWebHeight(props.height)}
|
|
153
|
+
label={typeof props.label === 'string' ? props.label : ''}
|
|
154
|
+
help={typeof props.help === 'string' ? props.help : ''}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
},
|
|
158
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BlockAdapterPlugin } from '../types'
|
|
2
|
+
import { webFromComponent, webToComponent } from './adapter'
|
|
3
|
+
import { SkoponWebImpl } from './catalog'
|
|
4
|
+
|
|
5
|
+
export const webAdapter: BlockAdapterPlugin = {
|
|
6
|
+
type: 'web',
|
|
7
|
+
componentName: 'SkoponWeb',
|
|
8
|
+
toComponent: webToComponent,
|
|
9
|
+
fromComponent: webFromComponent,
|
|
10
|
+
catalogComponents: [SkoponWebImpl],
|
|
11
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Button, Spin, Tag, Tooltip } from 'antd'
|
|
3
3
|
import type { FormDetailResult, FormSchema, SubmitMode } from '../types/index'
|
|
4
|
+
import { isInputBlockType } from '../types/index'
|
|
4
5
|
import {
|
|
5
6
|
resolveRealtimeWsUrl,
|
|
6
7
|
resolveSubmitAuthHeaders,
|
|
@@ -213,7 +214,7 @@ function AskUserFormCardBody({
|
|
|
213
214
|
const fieldNames = useMemo(
|
|
214
215
|
() =>
|
|
215
216
|
renderBlocks
|
|
216
|
-
.filter((block) => block.name?.trim())
|
|
217
|
+
.filter((block) => isInputBlockType(block.type) && block.name?.trim())
|
|
217
218
|
.map((block) => block.name!.trim()),
|
|
218
219
|
[renderBlocks],
|
|
219
220
|
)
|