feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { mergeConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
import path from 'path';
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../src', '../stories'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@edenx/storybook',
options: {
bundler: 'webpack',
configPath: path.resolve(__dirname, '../edenx.config.ts'),
},
},
docs: {
autodocs: 'tag',
},
viteFinal: config =>
mergeConfig(config, {
plugins: [
svgr({
svgrOptions: {
native: false,
},
}),
],
}),
};
export default config;

View File

@@ -0,0 +1,14 @@
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-workflow/setters
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,72 @@
{
"name": "@coze-workflow/setters",
"version": "0.0.1",
"description": "workflow setters",
"license": "Apache-2.0",
"author": "zhuxiaowei.711@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "exit 0",
"dev": "storybook dev -p 6006",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@douyinfe/semi-ui": "~2.72.3",
"@flowgram-adapter/free-layout-editor": "workspace:*",
"@tanstack/react-query": "~5.13.4",
"classnames": "^2.3.2",
"nanoid": "^4.0.2"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/semi-theme-hand01": "0.0.6-alpha.346d77",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@douyinfe/semi-webpack-plugin": "2.61.0",
"@rsbuild/core": "1.1.13",
"@storybook/addon-essentials": "^7.6.7",
"@storybook/addon-interactions": "^7.6.7",
"@storybook/addon-links": "^7.6.7",
"@storybook/addon-onboarding": "^1.0.10",
"@storybook/blocks": "^7.6.7",
"@storybook/preview-api": "^7.6.7",
"@storybook/react": "^7.6.7",
"@storybook/react-vite": "^7.6.7",
"@storybook/test": "^7.6.7",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "~14.5.2",
"@types/node": "18.18.9",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"humps": "2.0.1",
"i18next": ">= 19.0.0",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-is": ">= 16.8.0",
"storybook": "^7.6.7",
"styled-components": ">= 2",
"stylelint": "^15.11.0",
"typescript": "~5.8.2",
"vite": "^4.3.9",
"vite-plugin-svgr": "~3.3.0",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "~3.0.5",
"webpack": "~5.91.0"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { pascalize } = require('humps');
const setterName = process.argv[2];
if (!setterName) {
console.error('Please provide a setter name.');
process.exit(1);
}
const componentDir = path.join(__dirname, '..', 'src', setterName);
const indexFile = path.join(componentDir, 'index.ts');
const componentFile = path.join(componentDir, `${setterName}.tsx`);
const storiesFile = path.join(componentDir, 'index.stories.tsx');
const testFile = path.join(componentDir, 'index.test.tsx');
const styleFile = path.join(componentDir, `${setterName}.module.less`);
const packageIndexFile = path.join(__dirname, '..', 'src', 'index.ts');
const componentName = pascalize(setterName);
// 创建组件目录
if (fs.existsSync(componentDir)) {
console.error('can not created because this setter existed.');
process.exit(1);
}
fs.mkdirSync(componentDir);
// 创建 index.ts 文件
const indexContent = `export { ${componentName} } from './${setterName}';
export type { ${componentName}Options } from './${setterName}';`;
fs.writeFileSync(indexFile, indexContent);
// 创建 {setterName}.tsx 文件
const componentContent = `import type { Setter } from '../types';
import styles from './${setterName}.module.less';
export interface ${componentName}Options {}
export const ${componentName}: Setter<string, ${componentName}Options> = ({value, onChange, readonly, options={}}) => {
return <div className={styles['${setterName}']}>This is ${setterName}</div>;
};
`;
fs.writeFileSync(componentFile, componentContent);
// 创建 index.stories.tsx 文件
const storiesContent = `import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { ${componentName} } from './${setterName}'
const meta: Meta<typeof ${componentName}> = {
title: 'workflow setters/${componentName}',
component: ${componentName},
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
render: args => {
const [, updateArgs] = useArgs();
return (
<${componentName}
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
);
},
}
export default meta;
type Story = StoryObj<typeof ${componentName}>;
export const Base: Story = {};`;
fs.writeFileSync(storiesFile, storiesContent);
// 创建 index.test.tsx 文件
const testContent = `import '@testing-library/jest-dom';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ${componentName} } from './${setterName}';
const mockProps = {
value: '',
onChange: vi.fn(),
readonly: false,
};
describe('${componentName} Setter', () => {
it('renders correctly with default props', () => {
const { container } = render(<${componentName} {...mockProps} />);
expect(container.firstChild).toBeInTheDocument();
});
});
`;
fs.writeFileSync(testFile, testContent);
// 创建 {setterName}.module.less 文件
const styleContent = `.${setterName} {
// Your styles here
}
`;
fs.writeFileSync(styleFile, styleContent);
// 在包入口追加setter到导出
const packageIndexAppendedExportContent = `export { ${componentName} } from './${setterName}';
export type { ${componentName}Options } from './${setterName}';`;
fs.appendFileSync(packageIndexFile, packageIndexAppendedExportContent);
console.log(`Setter component ${setterName} created successfully.`);

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext, useContext } from 'react';
interface ArraySetterContext {
currentAddIndex?: number;
currentIndex?: number;
}
const arraySetterItemContext = createContext<ArraySetterContext>({});
// eslint-disable-next-line @typescript-eslint/naming-convention
export const ArraySetterItemContextProvider = arraySetterItemContext.Provider;
export const useArraySetterItemContext = () =>
useContext(arraySetterItemContext);

View File

@@ -0,0 +1,43 @@
.array {
position: relative;
.add-button {
margin-top: 8px;
}
.content {
display: flex;
flex-direction: column;
gap: 8px;
}
.array-item {
display: flex;
.child {
display: flex;
flex: 1;
gap: 4px;
align-items: start;
min-width: 0;
>* {
&:last-child {
flex: 1;
}
}
}
.minus {
position: relative;
top: 5px;
padding: 4px;
color: var(--light-usage-text-color-text-3, rgba(28, 29, 35, 35%));
&:hover {
cursor: pointer;
}
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState } from 'react';
import { IconCozPlus, IconCozMinus } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import type { Setter } from '../types';
import type { Field } from './types';
import { ColumnTitles } from './column-titles';
import { ArraySetterItemContextProvider } from './array-context';
import styles from './array.module.less';
export interface ArrayOptions {
disableAdd?: boolean;
getDefaultAppendValue?: () => any;
fields?: Field[];
/** 入参最大数量,若没有提供,默认为整数最大值 */
maxItems?: number;
/** 入参最小数量,若没有提供,默认为 0 */
minItems?: number;
/** 单条是否可删除 */
disableDeleteItem?: ((value: unknown, index: number) => boolean) | boolean;
}
// eslint-disable-next-line complexity
export const Array: Setter<Array<any>, ArrayOptions> = ({
value = [],
readonly = false,
children,
onChange,
context,
disableAdd = false,
getDefaultAppendValue,
fields = [],
maxItems = Number.MAX_SAFE_INTEGER,
minItems = 0,
disableDeleteItem = () => false,
}) => {
const [currentAddIndex, setCurrentAddIndex] = useState<number | undefined>();
const { node, meta } = context || {};
// 后端返回的 value 可能为 null此时不会赋值给 [],这里重新兜底下
const originValue = value || [];
const add = () => {
const defaultValue = getDefaultAppendValue?.() || {};
setCurrentAddIndex(originValue.length);
onChange?.([...originValue, defaultValue]);
};
const remove = (index: number) => {
const newValue = [...originValue];
newValue.splice(index, 1);
onChange?.(newValue);
};
const showAddButton =
!disableAdd && !readonly && originValue?.length < maxItems;
const calcShowDeleteButton = (item: unknown, index: number) => {
const globalEnableDelete = !readonly && originValue?.length > minItems;
if (typeof disableDeleteItem === 'undefined') {
return globalEnableDelete;
}
if (typeof disableDeleteItem === 'boolean') {
return globalEnableDelete && !disableDeleteItem;
}
return globalEnableDelete && !disableDeleteItem(item, index);
};
const columns = [...fields, ...(readonly ? [] : [{ label: '', width: 24 }])];
return (
<div className={styles.array}>
<div className={styles.content}>
{fields.length > 0 && <ColumnTitles columns={columns} />}
{React.Children.toArray(children).map((child, index) => {
const showDeleteButton = calcShowDeleteButton(
originValue[index],
index,
);
return (
<ArraySetterItemContextProvider
value={{
currentAddIndex,
currentIndex: index,
}}
>
<div className={styles['array-item']}>
<div className={styles.child}>{child}</div>
{showDeleteButton ? (
<IconButton
className="!block ml-1"
icon={<IconCozMinus className="text-sm" />}
size="small"
color="secondary"
onClick={() => remove(index)}
/>
) : null}
</div>
</ArraySetterItemContextProvider>
);
})}
</div>
{showAddButton ? (
<IconButton
color="highlight"
size="small"
className="absolute -top-8 right-0"
icon={<IconCozPlus />}
onClick={() => add()}
data-testid={`playground.node.${node?.id}.${meta?.name}.addbutton`}
/>
) : null}
</div>
);
};

View File

@@ -0,0 +1,12 @@
.column-titles {
display: flex;
gap: 4px;
align-items: center;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-3, rgb(28 29 35 / 35%));
letter-spacing: 0.12px;
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import styles from './column-titles.module.less';
interface Column {
label: string;
width?: number;
required?: boolean;
style?: React.CSSProperties;
}
interface ColumnTitlesProps {
columns: Column[];
}
export const ColumnTitles: FC<ColumnTitlesProps> = ({ columns }) => (
<div className={styles['column-titles']}>
{columns.map(({ label, width, required = false, style }, index) => (
<div
key={index}
className={styles['column-title']}
style={{ width: width ? `${width}px` : 'auto', ...(style || {}) }}
>
{label}
{required ? (
<span style={{ color: '#f93920', paddingLeft: 2 }}>*</span>
) : null}
</div>
))}
</div>
);

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-empty-function */
import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { String } from '../string';
import { Number } from '../number';
import { Enum } from '../enum';
import { Array } from './array';
const meta: Meta<typeof Array> = {
title: 'workflow setters/Array',
component: Array,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
const { value = [] } = args;
const handleItemChange = (newItemValue: number, index: number) => {
const newValue = [...(args.value || [])];
newValue[index] = newItemValue;
updateArgs({ ...args, value: newValue });
};
return (
<Array
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
>
{value?.map((itemValue: number, index) => (
<Enum
value={itemValue}
options={[
{
label: '知识1',
value: 1,
},
{
label: '知识2',
value: 2,
},
]}
onChange={newValue => handleItemChange(newValue as number, index)}
/>
))}
</Array>
);
},
args: {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
value: [1, 2],
},
};
export default meta;
type Story = StoryObj<typeof Array>;
export const Base: Story = {};
export const DisableAdd: Story = {
args: {
disableAdd: true,
},
};
export const Readonly: Story = {
args: {
readonly: true,
},
};
interface WithFieldsValueItem {
paramName?: string;
paramValue?: number;
}
export const WithFields: Story = {
args: {
value: [
{ paramName: 'key1', paramValue: 100 },
{ paramName: 'key2', paramValue: 200 },
],
fields: [
{
label: '参数名',
width: 160,
},
{
label: '参数值',
},
],
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
const { value } = args;
return (
<Array
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
>
{value?.map((itemValue: WithFieldsValueItem) => (
<>
<String
value={itemValue?.paramName}
width={160}
onChange={v => {}}
/>
<Number value={itemValue?.paramValue} onChange={() => {}} />
</>
))}
</Array>
);
},
};

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Array } from './array';
export type { ArrayOptions } from './array';

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Field {
label: string;
required?: boolean;
width?: number;
}

View File

@@ -0,0 +1,3 @@
.boolean {
// Your styles here
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Switch } from '@coze-arch/coze-design';
import type { Setter } from '../types';
export const Boolean: Setter<boolean> = ({ value, onChange, readonly }) => (
<Switch checked={value} onChange={onChange} disabled={readonly} />
);

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { Boolean } from './boolean';
const meta: Meta<typeof Boolean> = {
title: 'workflow setters/Boolean',
component: Boolean,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
return (
<Boolean
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof Boolean>;
export const Base: Story = {};
export const Readonly: Story = {
args: {
value: true,
readonly: true,
},
};

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import { Boolean } from './boolean';
const mockProps = {
value: false,
onChange: vi.fn(),
};
describe('Boolean Setter', () => {
it('renders correctly with default props', () => {
const { container } = render(<Boolean {...mockProps} />);
expect(container.firstChild).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Boolean } from './boolean';

View File

@@ -0,0 +1,35 @@
/* stylelint-disable no-descending-specificity, no-duplicate-selectors */
.label {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-right: 8px;
:global {
.semi-select-content-wrapper {
& {
width: auto;
.icon {
display: none;
}
}
}
}
.thumbnail {
flex-shrink: 0;
margin-right: 8px;
margin-left: 8px;
}
.content {
margin-right: auto;
}
.icon {
margin-left: auto;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { Tooltip, Avatar } from '@coze-arch/coze-design';
import { type EnumImageModelLabelProps } from './types';
import styles from './enum-image-model-label.module.less';
export function EnumImageModelLabel({
thumbnail,
label,
tooltip,
disabled = false,
disabledTooltip,
}: EnumImageModelLabelProps) {
let content = (
<div className={styles.label}>
<Avatar
className={classNames(
styles.thumbnail,
'wf-enum-image-model-thumbnail',
)}
style={{ width: 16, height: 16 }}
shape="square"
src={thumbnail}
/>
<span className={styles.content}>{label}</span>
</div>
);
if (disabled && disabledTooltip) {
tooltip = disabledTooltip;
}
if (tooltip) {
content = (
<Tooltip content={tooltip} position="left" spacing={40}>
{content}
</Tooltip>
);
}
return content;
}

View File

@@ -0,0 +1,14 @@
.readonly {
pointer-events: none;
}
.select {
:global(.option-prefix-icon) {
margin-right: 8px;
}
:global(.wf-enum-image-model-thumbnail) {
margin-right: 4px;
margin-left: 0;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cx from 'classnames';
import { Select } from '@coze-arch/coze-design';
import { type Setter } from '../types';
import { type Value, type EnumImageModelOptions } from './types';
import { EnumImageModelLabel } from './enum-image-model-label';
import styles from './enum-image-model.module.less';
export const EnumImageModel: Setter<Value, EnumImageModelOptions> = ({
value,
onChange,
readonly = false,
width = '100%',
showClear = false,
placeholder = '',
options,
validateStatus,
}) => (
<Select
size="small"
className={cx('flex', {
[styles.select]: true,
[styles.readonly]: readonly,
})}
optionList={options.map(
({ label, value: optionValue, thumbnail, disabled, tooltip }) => ({
label: (
<EnumImageModelLabel
thumbnail={thumbnail}
label={label}
tooltip={tooltip}
disabled={disabled}
/>
),
value: optionValue,
disabled,
}),
)}
style={{ width }}
value={value}
onChange={v => onChange?.(v as Value)}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
renderSelectedItem={({ value: selectedValue }: any) => {
const option = options.find(item => item.value === selectedValue);
if (option) {
const { thumbnail, label } = option;
return <EnumImageModelLabel thumbnail={thumbnail} label={label} />;
}
return null;
}}
showClear={showClear}
onClear={() => {
onChange?.(undefined);
}}
placeholder={placeholder}
validateStatus={validateStatus}
/>
);

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { type EnumImageModelOptions } from './types';
export { EnumImageModel } from './enum-image-model';

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type SelectProps } from '@coze-arch/coze-design';
export type Value = string | number | undefined;
export interface EnumImageModelOptionsOption {
thumbnail: string;
tooltip?: string;
value: Value;
label: string;
disabled?: boolean;
}
export interface EnumImageModelOptions {
width?: string | number;
showClear?: boolean;
placeholder?: string;
options: EnumImageModelOptionsOption[];
validateStatus?: SelectProps['validateStatus'];
}
export interface EnumImageModelLabelProps {
thumbnail: string;
label: string;
tooltip?: string;
disabledTooltip?: string;
disabled?: boolean;
}

View File

@@ -0,0 +1,3 @@
.readonly {
pointer-events: none;
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cx from 'classnames';
import { Select } from '@coze-arch/coze-design';
import type { Setter } from '../types';
import type { Options, EnumValue } from './types';
import styles from './enum.module.less';
export interface EnumOptions {
width?: number | string;
placeholder?: string;
options: Options;
}
export const Enum: Setter<EnumValue, EnumOptions> = ({
value,
onChange,
readonly,
options = [],
placeholder,
width = '100%',
}) => (
<Select
placeholder={placeholder}
className={cx({ [styles.readonly]: readonly })}
optionList={options}
style={{ width }}
value={value}
onChange={v => onChange?.(v as EnumValue)}
/>
);

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { Enum } from './enum';
const meta: Meta<typeof Enum> = {
title: 'workflow setters/Enum',
component: Enum,
tags: ['autodocs'],
args: {
value: '1',
options: [
{
value: '1',
label: 'single',
},
{
value: '2',
label: 'batch',
},
],
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
return (
<Enum
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof Enum>;
export const Base: Story = {};
export const Readonly: Story = {
args: {
value: '1',
options: [
{
value: '1',
label: 'single',
},
{
value: '2',
label: 'batch',
},
],
readonly: true,
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
const buttonArgs = JSON.parse(JSON.stringify(args));
buttonArgs.options.mode = 'button';
return (
<>
<div style={{ marginBottom: 10 }}>
<Enum
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
</div>
<div>
<Enum
{...buttonArgs}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
</div>
</>
);
},
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import { Enum } from './enum';
const mockProps = {
value: '',
onChange: vi.fn(),
options: [
{ label: '选项一', value: 1 },
{ label: '选项一', value: 2 },
],
};
describe('Enum Setter', () => {
it('renders correctly with default props', () => {
const { container } = render(<Enum {...mockProps} />);
expect(container.firstChild).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Enum } from './enum';
export type { EnumOptions } from './enum';

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type EnumValue = string | number;
export type Options = {
label: string;
value: EnumValue;
disabled?: boolean;
}[];

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { String } from './string';
export type { StringOptions } from './string';
export { Number } from './number';
export type { NumberOptions } from './number';
export { Text } from './text';
export type { TextOptions } from './text';
export { Boolean } from './boolean';
export { Enum } from './enum';
export type { EnumOptions } from './enum';
export { Array } from './array';
export { useArraySetterItemContext } from './array/array-context';
export type { ArrayOptions } from './array';
export { EnumImageModel } from './enum-image-model';
export type { EnumImageModelOptions } from './enum-image-model';
export { Setter } from './types';

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { Number } from './number';
const meta: Meta<typeof Number> = {
title: 'workflow setters/Number',
component: Number,
parameters: {
layout: 'centered',
},
args: {
value: 10,
},
tags: ['autodocs'],
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
return (
<Number
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof Number>;
export const Base: Story = {};
export const Placeholder: Story = {
args: {
value: undefined,
placeholder: '请输入数字',
},
};
export const Width: Story = {
args: {
width: 100,
},
};
export const MaxMinStep: Story = {
args: {
max: 100,
min: 10,
step: 10,
},
};
export const Readonly: Story = {
args: {
readonly: true,
},
};
export const Slider: Story = {
args: {
mode: 'slider',
width: 200,
min: 1,
max: 10,
step: 1,
},
};

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Number } from './number';
const mockProps = {
value: 0,
onChange: vi.fn(),
};
async function clickNumberButtonDown(container: HTMLElement) {
await clickNumberButton(container, 'down');
}
async function clickNumberButtonUp(container: HTMLElement) {
await clickNumberButton(container, 'up');
}
async function clickNumberButton(container: HTMLElement, arrow: 'up' | 'down') {
// 先触发 hover
const numberContainer = container.firstChild as HTMLElement;
fireEvent.mouseEnter(numberContainer);
// 等待下一个事件循环
await Promise.resolve();
const upButton = container.querySelector(
'.semi-input-number-button-up',
) as HTMLElement;
const downButton = container.querySelector(
'.semi-input-number-button-down',
) as HTMLElement;
if (arrow === 'up') {
fireEvent.mouseDown(upButton);
fireEvent.mouseUp(upButton);
} else {
fireEvent.mouseDown(downButton);
fireEvent.mouseUp(downButton);
}
}
function inputValue(container: HTMLElement, value: number) {
const inputElement = container.querySelector('input') as HTMLElement;
fireEvent.input(inputElement, { target: { value } });
}
describe('Number Setter', () => {
it('renders correctly with default props', () => {
const { container } = render(
// @ts-expect-error -- mock
<Number {...mockProps} value={0} onChange={vi.fn} />,
);
expect(container.firstChild).toBeInTheDocument();
});
it('displays the correct placeholder text', () => {
const placeholderText = 'Enter a number';
render(<Number {...mockProps} value={0} placeholder={placeholderText} />);
const inputElement = screen.getByPlaceholderText(placeholderText);
expect(inputElement).toBeInTheDocument();
});
it('calls onChange when value is changed', () => {
const newValue = 5;
const handleChange = vi.fn();
const { container } = render(
<Number {...mockProps} value={0} onChange={handleChange} />,
);
inputValue(container, newValue);
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(newValue);
});
it('applies custom width when provided', () => {
const customWidth = '50%';
const { container } = render(
<Number {...mockProps} value={0} width={customWidth} />,
);
expect(container.firstChild).toHaveStyle(`width: ${customWidth}`);
});
it('is readonly when readonly prop is true', () => {
const handleChange = vi.fn();
const { container } = render(
<Number {...mockProps} value={0} onChange={handleChange} readonly />,
);
inputValue(container, 1);
expect(handleChange).not.toHaveBeenCalled();
});
it('does not allow values less than min', async () => {
const handleChange = vi.fn();
const min = 0;
const { container } = render(
<Number {...mockProps} value={min} onChange={handleChange} min={min} />,
);
await clickNumberButtonDown(container);
expect(handleChange).not.toHaveBeenCalled();
});
it('does not allow values greater than max', async () => {
const handleChange = vi.fn();
const max = 10;
const { container } = render(
<Number {...mockProps} value={max} onChange={handleChange} max={max} />,
);
await clickNumberButtonUp(container);
expect(handleChange).not.toHaveBeenCalled();
});
it('increments value by step when using arrow up', async () => {
const handleChange = vi.fn();
const step = 2;
const { container } = render(
<Number {...mockProps} value={1} onChange={handleChange} step={step} />,
);
await clickNumberButtonUp(container);
expect(handleChange).toBeCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(3);
});
it('decrements value by step when using arrow down', async () => {
const handleChange = vi.fn();
const step = 2;
const { container } = render(
<Number {...mockProps} value={3} onChange={handleChange} step={step} />,
);
await clickNumberButtonDown(container);
expect(handleChange).toBeCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(1);
});
});

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Number } from './number';
export type { NumberOptions } from './number';

View File

@@ -0,0 +1,20 @@
.readonly {
pointer-events: none;
}
.slider {
position: relative;
:global {
// semi-slider放到flex布局会无法拖动 这里样式可以修复这个问题
.semi-slider {
position: relative;
width: 100%;
.semi-slider-wrapper {
width: 100%;
}
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cx from 'classnames';
import { CozInputNumber, Slider } from '@coze-arch/coze-design';
import type { Setter } from '../types';
import styles from './number.module.less';
export interface NumberOptions {
placeholder?: string;
width?: number | string;
step?: number;
max?: number;
min?: number;
mode?: 'input' | 'slider';
size?: 'small' | 'default';
style?: React.CSSProperties;
}
export const Number: Setter<number, NumberOptions> = ({
value,
onChange,
width = '100%',
readonly = false,
mode = 'input',
max,
min,
step,
placeholder,
size = 'default',
style = {},
}) => {
const handleChange = (newValue: number | string) => {
if (typeof newValue === 'number' && !readonly) {
onChange?.(newValue);
}
};
const handleSliderChange = (newValue?: number | number[]) => {
if (typeof newValue === 'number' && !readonly) {
onChange?.(newValue);
}
};
if (mode === 'slider') {
return (
<div className={styles.slider} style={{ width, ...style }}>
<Slider
className={cx({ [styles.readonly]: readonly })}
value={value}
min={min}
max={max}
step={step}
onChange={handleSliderChange}
/>
</div>
);
}
return (
<CozInputNumber
value={value}
onChange={handleChange}
className={cx({ [styles.readonly]: readonly })}
style={{ width, ...style }}
max={max}
min={min}
step={step}
placeholder={placeholder}
size={size}
/>
);
};

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { String } from './string';
const meta: Meta<typeof String> = {
title: 'workflow setters/String',
component: String,
args: {
value: '文本',
},
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
return (
<String
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof String>;
export const Base: Story = {};
export const Placeholder: Story = {
args: {
value: '',
placeholder: '请输入文字',
},
};
export const Width: Story = {
args: {
value: '文本',
placeholder: '请输入文字',
width: 100,
},
};
export const MaxCount: Story = {
args: {
value: '文本',
maxCount: 20,
},
};
export const Readonly: Story = {
args: {
readonly: true,
},
};
export const TextMode: Story = {
args: {
textMode: true,
},
};

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { String } from './string';
const mockProps = {
value: '',
onChange: vi.fn(),
};
function inputValue(container: HTMLElement, value: string) {
const inputElement = container.querySelector('input') as HTMLElement;
fireEvent.input(inputElement, { target: { value } });
}
describe('String Setter', () => {
it('renders correctly with default props', () => {
const { container } = render(<String {...mockProps} />);
expect(container.firstChild).toBeInTheDocument();
});
it('displays the correct placeholder text', () => {
const placeholderText = 'Enter some text';
render(<String {...mockProps} placeholder={placeholderText} />);
const inputElement = screen.getByPlaceholderText(placeholderText);
expect(inputElement).toBeInTheDocument();
});
it('calls onChange when text is entered', () => {
const handleChange = vi.fn();
const { container } = render(
<String {...mockProps} onChange={handleChange} />,
);
const newValue = 'new text';
inputValue(container, newValue);
screen.logTestingPlaygroundURL();
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(newValue);
});
it('applies custom width when provided', () => {
const customWidth = '50%';
const { container } = render(<String {...mockProps} width={customWidth} />);
expect(container.firstChild).toHaveStyle(`width: ${customWidth}`);
});
it('does not allow input when readonly is true', () => {
const handleChange = vi.fn();
const { container } = render(<String {...mockProps} readonly />);
const newValue = 'new text';
inputValue(container, newValue);
expect(handleChange).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { String } from './string';
export type { StringOptions } from './string';

View File

@@ -0,0 +1,21 @@
.suffix {
overflow: hidden;
padding-right: 12px;
padding-left: 8px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 35%));
}
.readonly {
pointer-events: none;
}
.text-mode {
font-size: 12px;
color: var(--coz-fg-primary);
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cx from 'classnames';
import { Input } from '@coze-arch/coze-design';
import { type Setter } from '../types';
import styles from './string.module.less';
export interface StringOptions {
placeholder?: string;
width?: number | string;
maxCount?: number;
// 新增这个配置的原因是readonly样式带有输入框 有些场景需要只展示文本
textMode?: boolean;
testId?: string;
}
export const String: Setter<string, StringOptions> = ({
value,
onChange,
readonly = false,
width = 'auto',
placeholder,
maxCount,
textMode = false,
testId,
}) => {
const handleChange = (newValue: string) => {
onChange?.(newValue);
};
if (textMode) {
return (
<div style={{ width }} className={styles['text-mode']}>
{value}
</div>
);
}
return (
<Input
size="small"
data-testid={testId}
className={cx({
[styles.readonly]: readonly,
})}
style={{
width,
}}
value={value}
onChange={handleChange}
readonly={readonly}
placeholder={placeholder}
maxLength={maxCount}
suffix={
maxCount === undefined ? null : (
<span className={styles.suffix}>
{`${value?.length || 0}/${maxCount}`}
</span>
)
}
/>
);
};

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { StoryObj, Meta } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { Text } from './text';
const meta: Meta<typeof Text> = {
title: 'workflow setters/Text',
component: Text,
args: {
value: '长文本',
},
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const [, updateArgs] = useArgs();
return (
<Text
{...args}
onChange={newValue => {
updateArgs({ ...args, value: newValue });
}}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof Text>;
export const Base: Story = {};
export const Placeholder: Story = {
args: {
value: '',
placeholder: '请输入文字',
},
};
export const Width: Story = {
args: {
value: '长文本',
placeholder: '请输入文字',
width: 100,
},
};
export const MaxCount: Story = {
args: {
value: '长文本',
maxCount: 100,
},
};
export const Readonly: Story = {
args: {
readonly: true,
},
};

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Text } from './text';
const mockProps = {
value: '',
onChange: vi.fn(),
};
function inputValue(container: HTMLElement, value: string) {
const inputElement = container.querySelector('textarea') as HTMLElement;
fireEvent.input(inputElement, { target: { value } });
}
describe('Text Setter', () => {
it('renders correctly with default props', () => {
const { container } = render(<Text {...mockProps} />);
expect(container.firstChild).toBeInTheDocument();
});
it('displays the correct placeholder text', () => {
const placeholderText = 'Enter some text';
render(<Text {...mockProps} placeholder={placeholderText} />);
const inputElement = screen.getByPlaceholderText(placeholderText);
expect(inputElement).toBeInTheDocument();
});
it('calls onChange when text is entered', () => {
const handleChange = vi.fn();
const { container } = render(
<Text {...mockProps} onChange={handleChange} />,
);
const newValue = 'new text';
inputValue(container, newValue);
screen.logTestingPlaygroundURL();
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(newValue);
});
it('does not allow input when readonly is true', () => {
const handleChange = vi.fn();
const { container } = render(<Text {...mockProps} readonly />);
const newValue = 'new text';
inputValue(container, newValue);
expect(handleChange).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Text } from './text';
export type { TextOptions } from './text';

View File

@@ -0,0 +1,3 @@
.readonly {
pointer-events: none;
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cx from 'classnames';
import { TextArea } from '@coze-arch/coze-design';
import { type Setter } from '../types';
import styles from './text.module.less';
export interface TextOptions {
placeholder?: string;
width?: number | string;
maxCount?: number;
}
export const Text: Setter<string, TextOptions> = ({
value,
onChange,
readonly = false,
width = '100%',
placeholder,
maxCount,
}) => {
const handleChange = (newValue: string) => {
onChange?.(newValue);
};
return (
<TextArea
className={cx({ [styles.readonly]: readonly })}
style={{
width,
}}
value={value}
onChange={handleChange}
placeholder={placeholder}
maxLength={maxCount}
maxCount={maxCount}
/>
);
};

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type SetterComponentProps } from '@flowgram-adapter/free-layout-editor';
type SetterProps<Value, CustomProps> = {
value?: Value;
onChange?: (value: Value) => void;
readonly?: boolean;
children?: React.ReactNode;
context?: SetterComponentProps['context'];
testId?: string;
} & CustomProps;
export type Setter<
Value = unknown,
CustomOptions = NonNullable<unknown>,
> = React.FC<SetterProps<Value, CustomOptions>>;

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,39 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": [],
"strictNullChecks": true,
"noImplicitAny": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../common/flowgram-adapter/free-layout-editor/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,24 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": [
"__tests__",
"stories",
"vitest.config.ts",
"tailwind.config.ts",
"edenx.config.ts"
],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"strictNullChecks": true,
"noImplicitAny": true
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig(
{
plugins: [],
dirname: __dirname,
preset: 'web',
ssr: {
noExternal: ['@coze-arch/coze-design', '@douyinfe/semi-ui'],
},
},
{
fixSemi: true,
},
);