@flowgram.ai/form-materials 0.1.0-alpha.10

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.
Files changed (100) hide show
  1. package/bin/index.ts +99 -0
  2. package/bin/materials.ts +156 -0
  3. package/bin/project.ts +91 -0
  4. package/dist/esm/index.js +3303 -0
  5. package/dist/esm/index.js.map +1 -0
  6. package/dist/index.d.mts +495 -0
  7. package/dist/index.d.ts +495 -0
  8. package/dist/index.js +3299 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +75 -0
  11. package/src/components/batch-outputs/config.json +12 -0
  12. package/src/components/batch-outputs/index.tsx +61 -0
  13. package/src/components/batch-outputs/styles.tsx +19 -0
  14. package/src/components/batch-outputs/types.ts +22 -0
  15. package/src/components/batch-outputs/use-list.ts +86 -0
  16. package/src/components/batch-variable-selector/config.json +5 -0
  17. package/src/components/batch-variable-selector/index.tsx +24 -0
  18. package/src/components/code-editor/config.json +9 -0
  19. package/src/components/code-editor/index.tsx +74 -0
  20. package/src/components/code-editor/language-features.ts +24 -0
  21. package/src/components/code-editor/theme/dark.ts +119 -0
  22. package/src/components/code-editor/theme/index.ts +12 -0
  23. package/src/components/code-editor/theme/light.ts +119 -0
  24. package/src/components/code-editor/utils.ts +20 -0
  25. package/src/components/condition-row/config.json +5 -0
  26. package/src/components/condition-row/constants.ts +128 -0
  27. package/src/components/condition-row/hooks/useOp.tsx +50 -0
  28. package/src/components/condition-row/hooks/useRule.ts +31 -0
  29. package/src/components/condition-row/index.tsx +81 -0
  30. package/src/components/condition-row/styles.tsx +30 -0
  31. package/src/components/condition-row/types.ts +42 -0
  32. package/src/components/constant-input/config.json +6 -0
  33. package/src/components/constant-input/index.tsx +86 -0
  34. package/src/components/constant-input/types.ts +23 -0
  35. package/src/components/dynamic-value-input/config.json +5 -0
  36. package/src/components/dynamic-value-input/index.tsx +92 -0
  37. package/src/components/dynamic-value-input/styles.tsx +26 -0
  38. package/src/components/index.ts +18 -0
  39. package/src/components/json-editor-with-variables/config.json +13 -0
  40. package/src/components/json-editor-with-variables/extensions/variable-tag.tsx +173 -0
  41. package/src/components/json-editor-with-variables/extensions/variable-tree.tsx +83 -0
  42. package/src/components/json-editor-with-variables/index.tsx +19 -0
  43. package/src/components/json-editor-with-variables/styles.tsx +44 -0
  44. package/src/components/json-schema-editor/components/blur-input.tsx +27 -0
  45. package/src/components/json-schema-editor/config.json +13 -0
  46. package/src/components/json-schema-editor/default-value.tsx +135 -0
  47. package/src/components/json-schema-editor/hooks.tsx +166 -0
  48. package/src/components/json-schema-editor/index.tsx +267 -0
  49. package/src/components/json-schema-editor/styles.tsx +240 -0
  50. package/src/components/json-schema-editor/types.ts +26 -0
  51. package/src/components/json-schema-editor/utils.ts +29 -0
  52. package/src/components/prompt-editor/config.json +9 -0
  53. package/src/components/prompt-editor/extensions/jinja.tsx +58 -0
  54. package/src/components/prompt-editor/extensions/language-support.tsx +19 -0
  55. package/src/components/prompt-editor/extensions/markdown.tsx +75 -0
  56. package/src/components/prompt-editor/index.tsx +68 -0
  57. package/src/components/prompt-editor/styles.tsx +18 -0
  58. package/src/components/prompt-editor/types.tsx +18 -0
  59. package/src/components/prompt-editor-with-inputs/config.json +13 -0
  60. package/src/components/prompt-editor-with-inputs/extensions/inputs-tree.tsx +82 -0
  61. package/src/components/prompt-editor-with-inputs/index.tsx +22 -0
  62. package/src/components/prompt-editor-with-inputs/inputs-picker.tsx +100 -0
  63. package/src/components/prompt-editor-with-variables/config.json +13 -0
  64. package/src/components/prompt-editor-with-variables/extensions/variable-tag.tsx +179 -0
  65. package/src/components/prompt-editor-with-variables/extensions/variable-tree.tsx +83 -0
  66. package/src/components/prompt-editor-with-variables/index.tsx +19 -0
  67. package/src/components/prompt-editor-with-variables/styles.tsx +44 -0
  68. package/src/components/type-selector/config.json +5 -0
  69. package/src/components/type-selector/constants.tsx +364 -0
  70. package/src/components/type-selector/index.tsx +62 -0
  71. package/src/components/variable-selector/config.json +5 -0
  72. package/src/components/variable-selector/index.tsx +116 -0
  73. package/src/components/variable-selector/styles.tsx +59 -0
  74. package/src/components/variable-selector/use-variable-tree.tsx +103 -0
  75. package/src/effects/auto-rename-ref/config.json +5 -0
  76. package/src/effects/auto-rename-ref/index.ts +109 -0
  77. package/src/effects/index.ts +10 -0
  78. package/src/effects/provide-batch-input/config.json +5 -0
  79. package/src/effects/provide-batch-input/index.ts +43 -0
  80. package/src/effects/provide-batch-outputs/config.json +5 -0
  81. package/src/effects/provide-batch-outputs/index.ts +38 -0
  82. package/src/effects/provide-json-schema-outputs/config.json +8 -0
  83. package/src/effects/provide-json-schema-outputs/index.ts +28 -0
  84. package/src/effects/sync-variable-title/config.json +5 -0
  85. package/src/effects/sync-variable-title/index.ts +28 -0
  86. package/src/form-plugins/batch-outputs-plugin/config.json +7 -0
  87. package/src/form-plugins/batch-outputs-plugin/index.ts +104 -0
  88. package/src/form-plugins/index.ts +6 -0
  89. package/src/index.ts +10 -0
  90. package/src/typings/flow-value/config.json +5 -0
  91. package/src/typings/flow-value/index.ts +32 -0
  92. package/src/typings/index.ts +7 -0
  93. package/src/typings/json-schema/config.json +5 -0
  94. package/src/typings/json-schema/index.ts +36 -0
  95. package/src/utils/format-legacy-refs/config.json +5 -0
  96. package/src/utils/format-legacy-refs/index.ts +158 -0
  97. package/src/utils/format-legacy-refs/readme.md +38 -0
  98. package/src/utils/index.ts +7 -0
  99. package/src/utils/json-schema/config.json +5 -0
  100. package/src/utils/json-schema/index.ts +180 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import React, { useLayoutEffect } from 'react';
7
+
8
+ import { createRoot, Root } from 'react-dom/client';
9
+ import { isEqual, last } from 'lodash';
10
+ import {
11
+ BaseVariableField,
12
+ Disposable,
13
+ DisposableCollection,
14
+ Scope,
15
+ useCurrentScope,
16
+ } from '@flowgram.ai/editor';
17
+ import { Popover } from '@douyinfe/semi-ui';
18
+ import { IconIssueStroked } from '@douyinfe/semi-icons';
19
+ import { useInjector } from '@coze-editor/editor/react';
20
+ import {
21
+ Decoration,
22
+ DecorationSet,
23
+ EditorView,
24
+ MatchDecorator,
25
+ ViewPlugin,
26
+ WidgetType,
27
+ } from '@codemirror/view';
28
+
29
+ import { UIPopoverContent, UIRootTitle, UITag, UIVarName } from '../styles';
30
+
31
+ class VariableTagWidget extends WidgetType {
32
+ keyPath?: string[];
33
+
34
+ toDispose = new DisposableCollection();
35
+
36
+ scope: Scope;
37
+
38
+ root: Root;
39
+
40
+ constructor({ keyPath, scope }: { keyPath?: string[]; scope: Scope }) {
41
+ super();
42
+
43
+ this.keyPath = keyPath;
44
+ this.scope = scope;
45
+ }
46
+
47
+ renderIcon = (icon: string | JSX.Element) => {
48
+ if (typeof icon === 'string') {
49
+ return <img style={{ marginRight: 8 }} width={12} height={12} src={icon} />;
50
+ }
51
+
52
+ return icon;
53
+ };
54
+
55
+ renderVariable(v?: BaseVariableField) {
56
+ if (!v) {
57
+ this.root.render(
58
+ <UITag prefixIcon={<IconIssueStroked />} color="amber">
59
+ Unknown
60
+ </UITag>
61
+ );
62
+ return;
63
+ }
64
+
65
+ const rootField = last(v.parentFields);
66
+
67
+ const rootTitle = (
68
+ <UIRootTitle>{rootField?.meta.title ? `${rootField.meta.title} -` : ''}</UIRootTitle>
69
+ );
70
+ const rootIcon = this.renderIcon(rootField?.meta.icon);
71
+
72
+ this.root.render(
73
+ <Popover
74
+ content={
75
+ <UIPopoverContent>
76
+ {rootIcon}
77
+ {rootTitle}
78
+ <UIVarName>{v?.keyPath.slice(1).join('.')}</UIVarName>
79
+ </UIPopoverContent>
80
+ }
81
+ >
82
+ <UITag prefixIcon={rootIcon}>
83
+ {rootTitle}
84
+ <UIVarName>{v?.key}</UIVarName>
85
+ </UITag>
86
+ </Popover>
87
+ );
88
+ }
89
+
90
+ toDOM(view: EditorView): HTMLElement {
91
+ const dom = document.createElement('span');
92
+
93
+ this.root = createRoot(dom);
94
+
95
+ this.toDispose.push(
96
+ Disposable.create(() => {
97
+ this.root.unmount();
98
+ })
99
+ );
100
+
101
+ this.toDispose.push(
102
+ this.scope.available.trackByKeyPath(
103
+ this.keyPath,
104
+ (v) => {
105
+ this.renderVariable(v);
106
+ },
107
+ { triggerOnInit: false }
108
+ )
109
+ );
110
+
111
+ this.renderVariable(this.scope.available.getByKeyPath(this.keyPath));
112
+
113
+ return dom;
114
+ }
115
+
116
+ eq(other: VariableTagWidget) {
117
+ return isEqual(this.keyPath, other.keyPath);
118
+ }
119
+
120
+ ignoreEvent(): boolean {
121
+ return false;
122
+ }
123
+
124
+ destroy(dom: HTMLElement): void {
125
+ this.toDispose.dispose();
126
+ }
127
+ }
128
+
129
+ export function VariableTagInject() {
130
+ const injector = useInjector();
131
+
132
+ const scope = useCurrentScope();
133
+
134
+ // 基于 {{var}} 的正则进行匹配,匹配后进行自定义渲染
135
+ useLayoutEffect(() => {
136
+ const atMatcher = new MatchDecorator({
137
+ regexp: /\{\{([^\}]+)\}\}/g,
138
+ decoration: (match) =>
139
+ Decoration.replace({
140
+ widget: new VariableTagWidget({
141
+ keyPath: match[1]?.split('.') ?? [],
142
+ scope,
143
+ }),
144
+ }),
145
+ });
146
+
147
+ return injector.inject([
148
+ ViewPlugin.fromClass(
149
+ class {
150
+ decorations: DecorationSet;
151
+
152
+ constructor(private view: EditorView) {
153
+ this.decorations = atMatcher.createDeco(view);
154
+ }
155
+
156
+ update() {
157
+ this.decorations = atMatcher.createDeco(this.view);
158
+ }
159
+ },
160
+ {
161
+ decorations: (p) => p.decorations,
162
+ provide(p) {
163
+ return EditorView.atomicRanges.of(
164
+ (view) => view.plugin(p)?.decorations ?? Decoration.none
165
+ );
166
+ },
167
+ }
168
+ ),
169
+ ]);
170
+ }, [injector]);
171
+
172
+ return null;
173
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ import { Popover, Tree } from '@douyinfe/semi-ui';
9
+ import {
10
+ Mention,
11
+ MentionOpenChangeEvent,
12
+ getCurrentMentionReplaceRange,
13
+ useEditor,
14
+ PositionMirror,
15
+ } from '@coze-editor/editor/react';
16
+ import { EditorAPI } from '@coze-editor/editor/preset-prompt';
17
+
18
+ import { useVariableTree } from '../../variable-selector';
19
+
20
+ export function VariableTree() {
21
+ const [posKey, setPosKey] = useState('');
22
+ const [visible, setVisible] = useState(false);
23
+ const [position, setPosition] = useState(-1);
24
+ const editor = useEditor<EditorAPI>();
25
+
26
+ function insert(variablePath: string) {
27
+ const range = getCurrentMentionReplaceRange(editor.$view.state);
28
+
29
+ if (!range) {
30
+ return;
31
+ }
32
+
33
+ editor.replaceText({
34
+ ...range,
35
+ text: '{{' + variablePath + '}}',
36
+ });
37
+
38
+ setVisible(false);
39
+ }
40
+
41
+ function handleOpenChange(e: MentionOpenChangeEvent) {
42
+ setPosition(e.state.selection.main.head);
43
+ setVisible(e.value);
44
+ }
45
+
46
+ useEffect(() => {
47
+ if (!editor) {
48
+ return;
49
+ }
50
+ }, [editor, visible]);
51
+
52
+ const treeData = useVariableTree({});
53
+
54
+ return (
55
+ <>
56
+ <Mention triggerCharacters={['@']} onOpenChange={handleOpenChange} />
57
+
58
+ <Popover
59
+ visible={visible}
60
+ trigger="custom"
61
+ position="topLeft"
62
+ rePosKey={posKey}
63
+ content={
64
+ <div style={{ width: 300 }}>
65
+ <Tree
66
+ treeData={treeData}
67
+ onSelect={(v) => {
68
+ insert(v);
69
+ }}
70
+ />
71
+ </div>
72
+ }
73
+ >
74
+ {/* PositionMirror allows the Popover to appear at the specified cursor position */}
75
+ <PositionMirror
76
+ position={position}
77
+ // When Doc scroll, update position
78
+ onChange={() => setPosKey(String(Math.random()))}
79
+ />
80
+ </Popover>
81
+ </>
82
+ );
83
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import React from 'react';
7
+
8
+ import { VariableTree } from './extensions/variable-tree';
9
+ import { VariableTagInject } from './extensions/variable-tag';
10
+ import { CodeEditor, type CodeEditorPropsType } from '../code-editor';
11
+
12
+ export function JsonEditorWithVariables(props: Omit<CodeEditorPropsType, 'languageId'>) {
13
+ return (
14
+ <CodeEditor languageId="json" activeLinePlaceholder="Press '@' to Select variable" {...props}>
15
+ <VariableTree />
16
+ <VariableTagInject />
17
+ </CodeEditor>
18
+ );
19
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import styled from 'styled-components';
7
+ import { Tag } from '@douyinfe/semi-ui';
8
+
9
+ export const UIRootTitle = styled.div`
10
+ margin-right: 4px;
11
+ min-width: 20px;
12
+ overflow: hidden;
13
+ text-overflow: ellipsis;
14
+ white-space: nowrap;
15
+ color: var(--semi-color-text-2);
16
+ `;
17
+
18
+ export const UIVarName = styled.div`
19
+ overflow: hidden;
20
+ text-overflow: ellipsis;
21
+ white-space: nowrap;
22
+ `;
23
+
24
+ export const UITag = styled(Tag)`
25
+ display: inline-flex;
26
+ align-items: center;
27
+ justify-content: flex-start;
28
+ max-width: 300px;
29
+
30
+ & .semi-tag-content-center {
31
+ justify-content: flex-start;
32
+ }
33
+
34
+ &.semi-tag {
35
+ margin: 0 5px;
36
+ }
37
+ `;
38
+
39
+ export const UIPopoverContent = styled.div`
40
+ padding: 10px;
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: flex-start;
44
+ `;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ import Input, { InputProps } from '@douyinfe/semi-ui/lib/es/input';
9
+
10
+ export function BlurInput(props: InputProps) {
11
+ const [value, setValue] = useState('');
12
+
13
+ useEffect(() => {
14
+ setValue(props.value as string);
15
+ }, [props.value]);
16
+
17
+ return (
18
+ <Input
19
+ {...props}
20
+ value={value}
21
+ onChange={(value) => {
22
+ setValue(value);
23
+ }}
24
+ onBlur={(e) => props.onChange?.(value, e)}
25
+ />
26
+ );
27
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "json-schema-editor",
3
+ "depMaterials": [
4
+ "type-selector",
5
+ "typings/json-schema",
6
+ "constant-inputs"
7
+ ],
8
+ "depPackages": [
9
+ "@douyinfe/semi-ui",
10
+ "@douyinfe/semi-icons",
11
+ "styled-components"
12
+ ]
13
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import React, { useRef, useState, useCallback } from 'react';
7
+
8
+ import { IconButton, JsonViewer, Tooltip } from '@douyinfe/semi-ui';
9
+ import { IconBrackets } from '@douyinfe/semi-icons';
10
+
11
+ import { getValueType } from './utils';
12
+ import {
13
+ ConstantInputWrapper,
14
+ JSONHeader,
15
+ JSONHeaderLeft,
16
+ JSONHeaderRight,
17
+ JSONViewerWrapper,
18
+ } from './styles';
19
+ import { ConstantInput } from '../constant-input';
20
+ import { IJsonSchema } from '../../typings';
21
+
22
+ /**
23
+ * 根据不同的数据类型渲染对应的默认值输入组件。
24
+ * @param props - 组件属性,包括 value, type, placeholder, onChange。
25
+ * @returns 返回对应类型的输入组件或 null。
26
+ */
27
+ export function DefaultValue(props: {
28
+ value: any;
29
+ schema?: IJsonSchema;
30
+ name?: string;
31
+ type?: string;
32
+ placeholder?: string;
33
+ jsonFormatText?: string;
34
+ onChange: (value: any) => void;
35
+ }) {
36
+ const { value, schema, type, onChange, placeholder, jsonFormatText } = props;
37
+
38
+ const wrapperRef = useRef<HTMLDivElement>(null);
39
+ const JsonViewerRef = useRef<JsonViewer>(null);
40
+
41
+ // 为 JsonViewer 添加状态管理
42
+ const [internalJsonValue, setInternalJsonValue] = useState<string>(
43
+ getValueType(value) === 'string' ? value : ''
44
+ );
45
+
46
+ // 使用 useCallback 创建稳定的回调函数
47
+ const handleJsonChange = useCallback((val: string) => {
48
+ // 只在值真正改变时才更新状态
49
+ if (val !== internalJsonValue) {
50
+ setInternalJsonValue(val);
51
+ }
52
+ }, []);
53
+
54
+ // 处理编辑完成事件
55
+ const handleEditComplete = useCallback(() => {
56
+ // 只有当存在key,编辑完成时才触发父组件的 onChange
57
+ onChange(internalJsonValue);
58
+ // 确保在更新后移除焦点
59
+ requestAnimationFrame(() => {
60
+ // JsonViewerRef.current?.format();
61
+ wrapperRef.current?.blur();
62
+ });
63
+ setJsonReadOnly(true);
64
+ }, [internalJsonValue, onChange]);
65
+
66
+ const [jsonReadOnly, setJsonReadOnly] = useState<boolean>(true);
67
+
68
+ const handleFormatJson = useCallback(() => {
69
+ try {
70
+ const parsed = JSON.parse(internalJsonValue);
71
+ const formatted = JSON.stringify(parsed, null, 4);
72
+ setInternalJsonValue(formatted);
73
+ onChange(formatted);
74
+ } catch (error) {
75
+ console.error('Invalid JSON:', error);
76
+ }
77
+ }, [internalJsonValue, onChange]);
78
+
79
+ return type === 'object' ? (
80
+ <>
81
+ <JSONHeader>
82
+ <JSONHeaderLeft>json</JSONHeaderLeft>
83
+ <JSONHeaderRight>
84
+ <Tooltip content={jsonFormatText ?? 'Format'}>
85
+ <IconButton
86
+ icon={<IconBrackets style={{ color: 'var(--semi-color-primary)' }} />}
87
+ size="small"
88
+ type="tertiary"
89
+ theme="borderless"
90
+ onClick={handleFormatJson}
91
+ />
92
+ </Tooltip>
93
+ </JSONHeaderRight>
94
+ </JSONHeader>
95
+
96
+ <JSONViewerWrapper
97
+ ref={wrapperRef}
98
+ tabIndex={-1}
99
+ onBlur={(e) => {
100
+ if (wrapperRef.current && !wrapperRef.current?.contains(e.relatedTarget as Node)) {
101
+ handleEditComplete();
102
+ }
103
+ }}
104
+ onClick={(e: React.MouseEvent) => {
105
+ setJsonReadOnly(false);
106
+ }}
107
+ >
108
+ <JsonViewer
109
+ ref={JsonViewerRef}
110
+ value={getValueType(value) === 'string' ? value : ''}
111
+ height={120}
112
+ width="100%"
113
+ showSearch={false}
114
+ options={{
115
+ readOnly: jsonReadOnly,
116
+ formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' },
117
+ }}
118
+ style={{
119
+ padding: 0,
120
+ }}
121
+ onChange={handleJsonChange}
122
+ />
123
+ </JSONViewerWrapper>
124
+ </>
125
+ ) : (
126
+ <ConstantInputWrapper>
127
+ <ConstantInput
128
+ value={value}
129
+ onChange={(_v) => onChange(_v)}
130
+ schema={schema || { type: 'string' }}
131
+ placeholder={placeholder ?? 'Default value if parameter is not provided'}
132
+ />
133
+ </ConstantInputWrapper>
134
+ );
135
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { useEffect, useMemo, useRef, useState } from 'react';
7
+
8
+ import { IJsonSchema } from '../../typings';
9
+ import { PropertyValueType } from './types';
10
+
11
+ let _id = 0;
12
+ function genId() {
13
+ return _id++;
14
+ }
15
+
16
+ function getDrilldownSchema(
17
+ value?: PropertyValueType,
18
+ path?: (keyof PropertyValueType)[]
19
+ ): { schema?: PropertyValueType | null; path?: (keyof PropertyValueType)[] } {
20
+ if (!value) {
21
+ return {};
22
+ }
23
+
24
+ if (value.type === 'array' && value.items) {
25
+ return getDrilldownSchema(value.items, [...(path || []), 'items']);
26
+ }
27
+
28
+ return { schema: value, path };
29
+ }
30
+
31
+ export function usePropertiesEdit(
32
+ value?: PropertyValueType,
33
+ onChange?: (value: PropertyValueType) => void
34
+ ) {
35
+ // Get drilldown (array.items.items...)
36
+ const drilldown = useMemo(() => getDrilldownSchema(value), [value, value?.type, value?.items]);
37
+
38
+ const isDrilldownObject = drilldown.schema?.type === 'object';
39
+
40
+ // Generate Init Property List
41
+ const initPropertyList = useMemo(
42
+ () =>
43
+ isDrilldownObject
44
+ ? Object.entries(drilldown.schema?.properties || {})
45
+ .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))
46
+ .map(
47
+ ([name, _value], index) =>
48
+ ({
49
+ key: genId(),
50
+ name,
51
+ isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
52
+ ..._value,
53
+ extra: {
54
+ ...(_value.extra || {}),
55
+ index,
56
+ },
57
+ } as PropertyValueType)
58
+ )
59
+ : [],
60
+ [isDrilldownObject]
61
+ );
62
+
63
+ const [propertyList, setPropertyList] = useState<PropertyValueType[]>(initPropertyList);
64
+
65
+ const mountRef = useRef(false);
66
+
67
+ useEffect(() => {
68
+ // If initRef is true, it means the component has been mounted
69
+ if (mountRef.current) {
70
+ // If the value is changed, update the property list
71
+ setPropertyList((_list) => {
72
+ const nameMap = new Map<string, PropertyValueType>();
73
+
74
+ for (const _property of _list) {
75
+ if (_property.name) {
76
+ nameMap.set(_property.name, _property);
77
+ }
78
+ }
79
+ return Object.entries(drilldown.schema?.properties || {})
80
+ .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))
81
+ .map(([name, _value]) => {
82
+ const _property = nameMap.get(name);
83
+ if (_property) {
84
+ return {
85
+ key: _property.key,
86
+ name,
87
+ isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
88
+ ..._value,
89
+ };
90
+ }
91
+ return {
92
+ key: genId(),
93
+ name,
94
+ isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
95
+ ..._value,
96
+ };
97
+ });
98
+ });
99
+ }
100
+ mountRef.current = true;
101
+ }, [drilldown.schema]);
102
+
103
+ const updatePropertyList = (updater: (list: PropertyValueType[]) => PropertyValueType[]) => {
104
+ setPropertyList((_list) => {
105
+ const next = updater(_list);
106
+
107
+ // onChange to parent
108
+ const nextProperties: Record<string, IJsonSchema> = {};
109
+ const nextRequired: string[] = [];
110
+
111
+ for (const _property of next) {
112
+ if (!_property.name) {
113
+ continue;
114
+ }
115
+
116
+ nextProperties[_property.name] = _property;
117
+
118
+ if (_property.isPropertyRequired) {
119
+ nextRequired.push(_property.name);
120
+ }
121
+ }
122
+
123
+ let drilldownSchema = value || {};
124
+ if (drilldown.path) {
125
+ drilldownSchema = drilldown.path.reduce((acc, key) => acc[key], value || {});
126
+ }
127
+ drilldownSchema.properties = nextProperties;
128
+ drilldownSchema.required = nextRequired;
129
+
130
+ onChange?.(value || {});
131
+
132
+ return next;
133
+ });
134
+ };
135
+
136
+ const onAddProperty = () => {
137
+ updatePropertyList((_list) => [
138
+ ..._list,
139
+ { key: genId(), name: '', type: 'string', extra: { index: _list.length + 1 } },
140
+ ]);
141
+ };
142
+
143
+ const onRemoveProperty = (key: number) => {
144
+ updatePropertyList((_list) => _list.filter((_property) => _property.key !== key));
145
+ };
146
+
147
+ const onEditProperty = (key: number, nextValue: PropertyValueType) => {
148
+ updatePropertyList((_list) =>
149
+ _list.map((_property) => (_property.key === key ? nextValue : _property))
150
+ );
151
+ };
152
+
153
+ useEffect(() => {
154
+ if (!isDrilldownObject) {
155
+ setPropertyList([]);
156
+ }
157
+ }, [isDrilldownObject]);
158
+
159
+ return {
160
+ propertyList,
161
+ isDrilldownObject,
162
+ onAddProperty,
163
+ onRemoveProperty,
164
+ onEditProperty,
165
+ };
166
+ }