chore: replace all cn comments of fe to en version by volc api (#320)

This commit is contained in:
tecvan
2025-07-31 10:32:15 +08:00
committed by GitHub
parent 716ec0cba8
commit 71f6245a01
2960 changed files with 15545 additions and 15545 deletions

View File

@@ -120,7 +120,7 @@ export function UIEmpty({
);
}
// 无图场景下的原生用法
// Native usage in non-graphic scenarios
UIEmpty.Semi = Empty;
export default UIEmpty;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FC, useRef } from 'react';
import cs from 'classnames';
@@ -35,7 +35,7 @@ export const UIFormInput: FC<CommonFieldProps & InputProps> = ({
return (
<div
style={{
// @ts-expect-error ts 无法识别 css 自定义变量
// @ts-expect-error ts cannot recognize css custom variable
'--var-error-msg-offset': props.addonBefore
? `${inputRef.current?.offsetLeft ?? 0}px`
: '0px',

View File

@@ -22,7 +22,7 @@ import { withField } from '@douyinfe/semi-ui';
import { UISelect } from '../../ui-select';
// UISelect 的 label 属性是提供给 borderless 主题使用的 表单场景下没有此主题,去掉这个属性避免和 form label 混合
// The label attribute of UISelect is provided for the borderless theme. There is no such theme in the form scene. Remove this attribute to avoid mixing with the form label.
const SelectInner: React.FC<
Omit<ComponentProps<typeof UISelect>, 'label'>
> = props => <UISelect {...props} />;

View File

@@ -25,12 +25,12 @@ import s from './index.module.less';
export interface UIIconButtonProps extends ButtonProps {
wrapperClass?: string;
/**
* iconSize: 带hover大小small:18default:24large:32
* iconSize: with hover size, small: 18, default: 24, large: 32
*/
iconSize?: 'small' | 'default' | 'large';
}
//图标按钮组件
//icon button component
export const UIIconButton = forwardRef(
(
{

View File

@@ -42,7 +42,7 @@ export type UIDragModalType =
export type UIDragModalProps = ComponentProps<typeof Modal> & {
type?: UIDragModalType;
focusKey?: string;
onWindowFocus?: (v: string) => void; // 当前窗口被点击时的回调
onWindowFocus?: (v: string) => void; // Callback when the current window is clicked
};
export const UIDragModal: FC<PropsWithChildren<UIDragModalProps>> = props => {

View File

@@ -59,7 +59,7 @@ export const UIMobileModal = forwardRef(
) => (
<Modal
{...props}
// 对齐 UX 规范,点击半透明背景默认不关闭
// Align the UX specification, click on the translucent background and do not close by default
maskClosable={false}
ref={ref}
centered={centered}

View File

@@ -56,7 +56,7 @@ export class UIModal extends Modal {
return (
<Modal
{...props}
// 对齐 UX 规范,点击半透明背景默认不关闭
// Align the UX specification, click on the translucent background and do not close by default
maskClosable={false}
centered={centered}
cancelButtonProps={{

View File

@@ -27,7 +27,7 @@ export type UISearchInputProps = InputProps & {
type InputRefType = HTMLInputElement | null;
/**
* 搜索场景下的 Input 组件 结合 composition api 优化了中文输入场景
* Input component in the search scene, combined with the composition API to optimize the Chinese input scene
* @returns Input
*/
export const UISearchInput = forwardRef(

View File

@@ -156,7 +156,7 @@ export const UISelect: ForwardRefExoticComponent<
Omit<FilterProps & Omit<SelectProps, 'clickToHide'>, 'ref'> &
RefAttributes<SemiSelectActions>
> & {
// follow Semi 组件命名
// Name the following Semi components
OptGroup: typeof Select.OptGroup;

View File

@@ -45,7 +45,7 @@ export const UITabBar: React.FC<PropsWithChildren<UITabBarProps>> = ({
<div className={cs(s.header, containerClass)}>
<Node {...innerProps} />
{/* 右侧工具栏没有可不传children */}
{/* Right toolbar, no children can be passed on. */}
<div className={s['tool-bar']}>{children}</div>
</div>
)}

View File

@@ -25,14 +25,14 @@ import s from './index.module.less';
export interface UITableMetaProps {
className?: string;
avatarClassName?: string;
icon_url?: string; //icon图标
icon?: ReactElement; //支持传入icon标签
name?: string; //名称
description?: string; //描述
suffix?: ReactElement; //额外元素
icon_url?: string; //icon icon
icon?: ReactElement; //Support for incoming icon tags
name?: string; //name
description?: string; //describe
suffix?: ReactElement; //Extra Element
}
//Table表头详情组件
//Table header details component
export const UITableMeta: React.FC<UITableMetaProps> = ({
className,
avatarClassName,

View File

@@ -92,7 +92,7 @@ export const UITable = forwardRef<UITableMethods, UITableProps>(
const showTable = initialized && !!dataSource?.length;
/**
* TODO:处理触底加载,一坨💩,待优化
* TODO: handle bottom loading, a lump of 💩, to be optimized
*/
const IndicatorRoot = useRef<Root>();
@@ -133,7 +133,7 @@ export const UITable = forwardRef<UITableMethods, UITableProps>(
useEffect(() => {
if (tableRef.current && enableLoad && needRenderIndicator) {
const tableContainer =
// FIXME: 这段代码这么多层 ref需要优化
// FIXME: This code has so many layers of refs and needs to be optimized
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(tableRef.current as any).tableRef.current.bodyWrapRef.current;
@@ -196,7 +196,7 @@ export const UITable = forwardRef<UITableMethods, UITableProps>(
/>
) : null}
{/* 空状态 */}
{/* empty state */}
{initialized && !tableProps?.loading && !dataSource?.length ? (
<div className={styles['empty-content']}>{empty}</div>
) : null}
@@ -205,7 +205,7 @@ export const UITable = forwardRef<UITableMethods, UITableProps>(
},
);
// Indicatore 组件
// Indicatore component
interface IndicatorState {
done: boolean;
}

View File

@@ -29,7 +29,7 @@ import { ToastFactory, Toast } from '@douyinfe/semi-ui';
import styles from './index.module.less';
// Toast展示位置离top 80px
// Toast display 80px from the top
Toast.config({
top: 80,
});
@@ -61,9 +61,9 @@ function rewriteToastCreate(opts: ToastReactProps) {
className: classNames(styles.container, className),
icon: getIcon(),
theme: 'light',
// 默认不展示close图标
// Default does not display close icon
showClose: false,
// Toast展示位置离top 80px
// Toast display 80px from the top
top: 80,
...rest,
}),
@@ -83,7 +83,7 @@ function rewriteToastCreateAlias(type?: RequiredToastType) {
return rewriteToastCreate(merge({}, opts, { type }));
};
}
/* 重写Toast的方法 */
/* How to Override Toast */
UIToast.create = rewriteToastCreateAlias();
UIToast.info = rewriteToastCreateAlias('info');
UIToast.error = rewriteToastCreateAlias('error');

View File

@@ -66,15 +66,15 @@ const useShouldUpdateAnchor = () => {
export interface GrabProps {
/**
* 拖拽的锚点
* Drag anchor
* @default grabTarget
*/
grabAnchor?: RefObject<HTMLDivElement>;
/** 被拖拽移动的目标 */
/** Dragged moving target */
grabTarget: RefObject<HTMLDivElement>;
/** 是否直接修改 style 达到移动的效果 */
/** Whether to directly modify the style to achieve the effect of moving */
isModifyStyle: boolean;
/** 位置改变时的回调 */
/** Callback when position changes */
onPositionChange?: (param: { left: number; top: number }) => void;
}
@@ -113,8 +113,8 @@ export const useGrab = ({
e.preventDefault();
/**
* 计算页面宽度和目标宽度, 和拖拽的位置进行比较
* 避免将弹窗拖离页面
* Calculate the page width and target width, and compare with the drag position
* Avoid dragging pop-ups off the page
*/
const currentLeft = Math.min(
Math.max(e.clientX - anchorOffsetX.current, 0),
@@ -157,8 +157,8 @@ export const useGrab = ({
return;
}
/**
* 计算鼠标在拖拽目标 dom 中的相对位置
* 用于后续和全局鼠标位置进行差值计算得到拖拽位置
* Calculate the relative position of the mouse in the drag target dom
* Use for subsequent and global mouse position difference calculations to get the drag position
*/
const left = target.offsetLeft;
const top = target.offsetTop;
@@ -176,7 +176,7 @@ export const useGrab = ({
const handleAnchorMouseDown = (e: MouseEvent) => {
/**
* https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/buttons
* 只识别鼠标主按键(左键)按下
* Only the main mouse button (left button) is recognized and pressed.
*/
if (e.button !== 0) {
return;
@@ -188,8 +188,8 @@ export const useGrab = ({
};
/**
* 当弹窗到达边界/由于 resize 导致弹窗超出边界后
* 鼠标移动过程中需要更新 anchor 位置, 否则会出现鼠标移动但是弹窗位置不更新的问题
* When the pop-up window reaches the boundary/after the pop-up window exceeds the boundary due to resize
* The anchor position needs to be updated during the mouse movement, otherwise there will be a problem that the mouse moves but the pop-up window position is not updated.
*/
const handleAnchorMouseMove = (e: MouseEvent) => {
if (!isGrabbingRef.current) {
@@ -203,8 +203,8 @@ export const useGrab = ({
updateGrabAnchorBuffer();
/**
* 避免正常拖拽时 anchor 位置和全局鼠标位置同时变化
* 导致差值计算后位置不更新
* Avoid changing anchor position and global mouse position simultaneously during normal dragging
* The position is not updated after the difference is calculated
*/
if (!getGrabAnchorBufferExceed()) {
return;

View File

@@ -15,7 +15,7 @@
*/
// re-export everything from semi ui
// 这里保留具名导出,未来那些 component 需要做修改的时候,补充文件修改即可
// Keep the named export here. When those components need to be modified in the future, you can modify the supplementary file.
export {
Anchor,
AutoComplete,
@@ -25,7 +25,7 @@ export {
Badge,
Banner,
Breadcrumb,
// FIXME: Button 这个名字已经被上层占用,需要先修复上层所有被调用的地方,之后才能导出这个组件
// The name FIXME: Button has been taken by the upper layer. You need to fix all the places where the upper layer is called before exporting this component.
// Button,
ButtonGroup,
Calendar,
@@ -53,7 +53,7 @@ export {
List,
IconButton,
Icon,
// FIXME: Input 这个名字已经被上层占用,需要先修复上层所有被调用的地方,之后才能导出这个组件
// FIXME: The name Input is already occupied by the upper layer. You need to fix all the places where the upper layer is called before exporting this component.
// Input,
InputGroup,
TextArea,

View File

@@ -29,7 +29,7 @@ import { LogObjSpecialKey, LogValueStyleType } from '../../constants';
import styles from './json-field.module.less';
/* JSON 类型数据渲染 */
/* JSON type data rendering */
const FieldValue: React.FC<{
value: Field['value'];
}> = ({ value }) => {

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
/** 预置的特殊的 key */
/** Preset special key */
export enum LogObjSpecialKey {
Error = '$error',
Warning = '$warning',
}
/** log 中 value 的显示样式类型 */
/** Display style type of value in log */
export enum LogValueStyleType {
Default,
Number,

View File

@@ -36,7 +36,7 @@ interface JsonViewerProviderProps {
}
/**
* 根只有一项且其可以下钻时,默认展开它
* When the root has only one item and it can be drilled down, it is expanded by default
*/
const generateInitialExpandValue = (fields: Field[], expandAll?: boolean) => {
if (expandAll) {
@@ -68,7 +68,7 @@ export const JsonViewerContext = createContext<JsonViewerContextType>({
export const JsonViewerProvider: React.FC<
PropsWithChildren<JsonViewerProviderProps>
> = ({ fields, children, defaultExpandAllFields }) => {
/** 因为存在不属于单项的逻辑,所以集中管理展开折叠的状态 */
/** Because there is logic that does not belong to a single item, the state of unfolding and folding is managed centrally */
const [expand, setExpand] = useState<JsonViewerContextType['expand'] | null>(
null,
);
@@ -78,8 +78,8 @@ export const JsonViewerProvider: React.FC<
);
/**
* fields 是动态更新的,这里要注意固化 expand 数据,因为 fields 总是由少增多
* 由于存在自动展开逻辑,所以从 0 => 1 变化时需要赋值
* Fields are dynamically updated, pay attention to solidifying expand data here, because fields are always increased by less
* Due to the existence of automatic expansion logic, assignment is required when changing from 0 = > 1
*/
useEffect(() => {
if (!expand) {

View File

@@ -32,7 +32,7 @@ export const useValue = (value: Field['value']) => {
type: LogValueStyleType.Default,
};
} else if (isObject(value)) {
// 大数字返回数字类型,值用字符串
// Large number Returns the numeric type, and the value is a string.
if (isBigNumber(value)) {
return {
value: bigNumbertoString(value),
@@ -64,7 +64,7 @@ export const useValue = (value: Field['value']) => {
}
return {
value: generateStrAvoidEscape(value),
// value: generateStr2Link(value, avoidEscape), 先取消做 link 解析
// Value: generateStr2Link (value, avoidEscape), cancel the link parsing first
type: LogValueStyleType.Default,
};
} else if (isNumber(value)) {

View File

@@ -6,7 +6,7 @@
width: 100%;
min-height: 32px;
/** 高度限制 */
/** height limit */
max-height: 272px;
padding: 6px 12px;

View File

@@ -30,10 +30,10 @@ import styles from './index.module.less';
export type { JsonValueType };
export interface JsonViewerProps {
/** 支持对象或者纯文本渲染 */
/** Supports object or plain text rendering */
data: JsonValueType;
className?: React.HTMLAttributes<HTMLDivElement>['className'];
/** 默认展开所有字段 */
/** Expand all fields by default */
defaultExpandAllFields?: boolean;
}
@@ -43,7 +43,7 @@ export const JsonViewer: React.FC<JsonViewerProps> = ({
defaultExpandAllFields,
}) => {
const render = () => {
// 兜底展示 null
// Bottom display null
if (isNil(data)) {
return (
<JsonField
@@ -59,13 +59,13 @@ export const JsonViewer: React.FC<JsonViewerProps> = ({
);
}
// 文本类结果展示
// Text class result display
const isStr = isString(data);
if (isStr) {
return <TextField text={data} />;
}
// 其他json类型数据展示
// Other JSON data display
const fields = generateFields(data);
return (
<JsonViewerProvider

View File

@@ -15,21 +15,21 @@
*/
/*******************************************************************************
* log 相关的类型
* Log related types
*/
/** 线可能存在的几种状态 */
/** The possible states of a line */
export enum LineStatus {
/** 完全隐藏,最后一个父属性嵌套的子属性同列将不会有线 */
/** Completely hidden, the last parent attribute nested child attribute will not be wired in the same column */
Hidden,
/** 完全显示,仅出现在属性相邻的线 */
/** Full display, appearing only on adjacent lines of properties */
Visible,
/** 半显示,非相邻的线 */
/** Semi-display, non-adjacent lines */
Half,
/** 最后属性的相邻线 */
/** Adjacent line of last property */
Last,
}
/** JsonViewer 中的 value 可能值 */
/** Possible values in JsonViewer */
export type JsonValueType =
| string
| null
@@ -39,12 +39,12 @@ export type JsonValueType =
| undefined;
export interface Field {
/** 使用数组而不是 'a.b.c' 是因为可能存在 key='a.b' 会产生错误嵌套 */
/** The use of arrays instead of'a.b.c 'is due to the possibility that key =' a.b 'will generate false nesting */
path: string[];
lines: LineStatus[];
/** 这里 value 可能是任意值,这里是不完全枚举 */
/** Here value can be any value, here is an incomplete enumeration */
value: JsonValueType;
children: Field[];
/** 是否是可下钻的对象(包含数组) */
/** Whether it is a drillable object (containing an array) */
isObj: boolean;
}

View File

@@ -17,7 +17,7 @@
import BigNumber from 'bignumber.js';
/**
* 是不是大数字
* Is it a big number?
* @param value
* @returns
*/
@@ -26,7 +26,7 @@ export function isBigNumber(value: unknown): value is BigNumber {
}
/**
* 大数字转字符串
* Large number to string
* @param value
* @returns
*/

View File

@@ -20,29 +20,29 @@ import { LineStatus, type JsonValueType, type Field } from '../types';
import { isBigNumber } from './big-number';
/**
* 通过父元素的线条状态推到子元素的线条状态
* Push through the line state of the parent element to the line state of the child element
*/
const getLineByParent2Child = (pLine: LineStatus): LineStatus => {
switch (pLine) {
/** 表示父节点也是从父父节点下钻而来,此处的子节点只需要把线延续下去即可 */
/** It means that the parent node is also drilled from the parent node, and the sub-node here only needs to continue the line. */
case LineStatus.Visible:
return LineStatus.Half;
/** 表示父节点是父父节点的最后一个节点,子节点无需再延续,渲染空白即可 */
/** Indicates that the parent node is the last node of the parent node, and the sub-node does not need to continue, just render blank. */
case LineStatus.Last:
return LineStatus.Hidden;
/** 其他的情况完全继承父节点的线 */
/** Other cases fully inherit the line of the parent node */
default:
return pLine;
}
};
/**
* object 解析成可以循环渲染的 fields
* 1. object 非复杂类型,则返回长度为 1 的 fields 只渲染一项
* 2. object = {},则返回长度为 0 的 fields渲染层需要做好兜底
* Parse objects into fields that can be cycled
* 1. If object is not a complex type, fields of length 1 are returned to render only one item
* 2. If object = {}, fields of length 0 are returned, and the rendering layer needs to be well covered
*/
const generateFields = (object: JsonValueType): Field[] => {
/** object 非复杂类型 */
/** If the object is not a complex type */
if (!isObject(object) || isBigNumber(object)) {
return [
{
@@ -55,17 +55,17 @@ const generateFields = (object: JsonValueType): Field[] => {
];
}
/** 递归计算时缓存一下计算好的线,没别的意义,降低一些时间复杂度 */
/** Cache the calculated line during recursive calculation, which is meaningless and reduces some time complexity */
const lineMap = new Map<string[], LineStatus[]>();
/** 递归解析 object fields */
/** Recursive parsing of object as fields */
const dfs = ($object: object, $parentPath: string[] = []): Field[] => {
// 如果不是对象,直接返回空数组,兜底异常情况
// If it is not an object, return an empty array directly to cover the exception
if (!isObject($object)) {
return [];
}
// 如果是大数字,直接返回空数组
// If it is a large number, return an empty array directly
if (isBigNumber($object)) {
return [];
}
@@ -79,12 +79,12 @@ const generateFields = (object: JsonValueType): Field[] => {
const path = $parentPath.concat(key);
const last = idx === keys.length - 1;
/**
* 根据父节点的线推导子节点的线
* Derive the sub-node's line from the parent's line
*/
const lines = parentLines
.map<LineStatus>(getLineByParent2Child)
/**
* 最后拼接上子节点自己的线,最后一个节点和普通的节点有样式区分
* Finally, splice the sub-node's own line, and the last node is distinguished from the ordinary node by style.
*/
.concat(last ? LineStatus.Last : LineStatus.Visible);
lineMap.set(path, lines);

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
// 对象中的文本,避免字符被转译
// Text in the object to avoid characters being translated
export const generateStrAvoidEscape = (str: string) => {
const characters = {
'\\': '\\\\',

View File

@@ -33,11 +33,11 @@ export const generateStr2Link = (str: string, avoidEscape?: boolean) => {
}
/**
* 更严格的 url 匹配规则,防止过度匹配
* 协议:httphttps
* 域名:允许使用 -、a-zA-Z0-9,其中 - 不能开头,每一级域名长度不会超过 63
* 端口:支持带端口 0 - 65535
* URL:严格型不匹配中文等转译前的文字,否则一旦命中将会识别整段字符串
* Stricter URL matching rules to prevent over-matching
* Protocol: http, https
* Domain names: -, a-z, A-Z, 0-9 are allowed, where - cannot start, and the length of each level of domain name will not exceed 63.
* Port: Support with port 0 - 65535
* URL: Strict does not match the text before translation such as Chinese, otherwise the entire string will be recognized once hit
*/
const urlReg = new RegExp(
'http(s)?://' +
@@ -48,12 +48,12 @@ export const generateStr2Link = (str: string, avoidEscape?: boolean) => {
);
const matches = [...str.matchAll(urlReg)];
/**
* 切割字符串url 嵌套为 link 的样式,切割步骤:
* 1. 匹配字符串中所有的 url
* 2. 倒序 matches,从末尾开始切,原因是 match.index 是从头开始计数,从头切增加计算量
* 3. 每一个 match 切两刀成三段,头尾是普通字符串,中间为 url
* 4. 按照 end、url、start 的顺序 push 到栈中,下次 match 会直接取 start 继续切
* 5. 切割完成后做一次倒序
* Cut string, url nested as link style, cutting steps:
* 1. Match all URLs in the string
* 2. Reverse matches, cut from the end, the reason is that match.index counts from scratch, and cut from scratch increases the amount of calculation
* 3. Each match is cut into three sections, the head and tail are ordinary strings, and the middle is url.
* 4. Push to the stack in the order of end, url, and start, and the next match will directly take start and continue to cut
* 5. Do a reverse sequence after the cutting is completed
*/
return matches
.reverse()

View File

@@ -22,7 +22,7 @@ import { type ButtonProps } from '@coze-arch/bot-semi/Button';
import { UIButton, Toast, Spin } from '@coze-arch/bot-semi';
export type LoadingButtonProps = ButtonProps & {
/** 加载中的 toast 文案 */
/** Loading toast copy */
loadingToast?: string | Omit<ToastReactProps, 'type'>;
};

View File

@@ -30,7 +30,7 @@ export const getPreferInteractiveType = () => {
return IS_MAC_OS ? InteractiveType.Pad : InteractiveType.Mouse;
};
/** 记录选择的交互模式 */
/** Record the selected interaction mode */
export const setPreferInteractiveType = (type: InteractiveType) => {
localStorage.setItem(CACHE_KEY, type);
};

View File

@@ -98,7 +98,7 @@ export const GuidingPopover = (
const [visible, setVisible] = useState(() => needShowGuidingPopover());
const onButtonClick = () => setVisible(false);
// gotIt 方法先不暴露到上层了,后续需要使用再暴露出来
// The gotIt method is not exposed to the upper layer first, and it needs to be used later before being exposed.
const handleGotIt = () => {
hideGuidingPopover();
setVisible(false);

View File

@@ -49,7 +49,7 @@ export const Icon = ({ type, icon }: { type: NodeType; icon?: string }) => {
if (type === NodeType.DATABASE) {
return <IconDatabase />;
}
// 插件来自商店和资源库场景默认图标不同
// Plugin from store and repository scenes have different default icons
if (type === NodeType.PLUGIN) {
return <IconPlugin />;
}

View File

@@ -44,7 +44,7 @@ const { Text } = Typography;
export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
const nodeRender = useCustomNodeRender();
const treeService = useService<TreeService>(TreeService);
// schema 信息存储
// Schema Information Store
const extraInfo = nodeRender.getExtInfo();
const [hoverNode, setHoverNode] = useState(false);
const { renderLinkNode } = useContext(TreeContext);
@@ -86,16 +86,16 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
}, []);
const handleClick = (e: React.MouseEvent) => {
// 1. 取消其他选中的线条
// 1. Uncheck other selected lines
hoverService.backgroundClick();
// 2. 选中节点,重新计算需要选中的线条
// 2. Select the node and recalculate the lines that need to be selected.
hoverService.selectNode(node);
e.stopPropagation();
};
const handleMouseEnter = () => {
setHoverNode(true);
// 判断节点类型
// Determine the node type
if (
node.flowNodeType !== 'custom' ||
edges.some(edge => edge.from === node.id)
@@ -172,7 +172,7 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
/>
</div>
) : null}
{/* 不同版本的 tag */}
{/* Different versions of tags */}
{isOtherVersion && !activated ? (
<div className={s['diff-tag']}>
{activatedVersion === extraInfo.version

View File

@@ -50,8 +50,8 @@ export function Collapse(props: Omit<CollapseProps, 'collapsed'>): JSX.Element {
const { edges: originEdges } = treeService;
const edges = treeService.getUnCollapsedEdges();
// 如果没有子元素,就是折叠了。
// 还要判断是否有线条连线
// If there are no child elements, it is collapsed.
// Also determine whether there are lines connecting
const collapsed =
!collapseNode?.children?.length &&
!collapseNode.next &&
@@ -77,9 +77,9 @@ export function Collapse(props: Omit<CollapseProps, 'collapsed'>): JSX.Element {
}
});
}
// 节点重绘
// Node redraw
treeService.treeToFlowNodeJson();
// 线条重绘
// Line redrawing
rerenderLines();
};
@@ -95,12 +95,12 @@ export function Collapse(props: Omit<CollapseProps, 'collapsed'>): JSX.Element {
}
});
}
// 节点重绘
// Node redraw
treeService.treeToFlowNodeJson();
rerenderLines();
};
// flow-labels-layer 不更新
// flow-labels-layer is not updated
useEffect(() => {
const disposable = entityManager.onEntityChange(() => {
refresh();

View File

@@ -38,7 +38,7 @@ export const LinesRenderer = ({
isViewportVisible: (bounds: Rectangle) => boolean;
version: number;
}) => {
// 单线条选中
// Single line selection
const [activeLine, setActiveLine] = useState<string[]>([]);
const renderState = useConfigEntity<CustomRenderStateConfigEntity>(
@@ -50,7 +50,7 @@ export const LinesRenderer = ({
setActiveLine(renderState.activeLines);
}, [renderState.activeLines]);
// semi 弹窗偏移计算有误,这里线条直接全部展示
// The offset calculation of the semi pop-up window is wrong, and the lines here are directly displayed.
const visibleLines = lines.filter(line => {
const bounds = Rectangle.createRectangleWithTwoPoints(
line.fromPoint,
@@ -75,7 +75,7 @@ export const LinesRenderer = ({
overflow="visible"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
// 确保线条数量变化的时候,强制刷新
// Make sure to force a refresh when the number of lines changes
key={nanoid(5)}
>
{allLines.map(line => {

View File

@@ -41,7 +41,7 @@ function getPath(params: {
const { controls } = params;
// 渲染端点位置计算
// Render endpoint position calculation
const renderToPos: IPoint = { x: toPos.x, y: toPos.y };
const getPathData = (): string => {

View File

@@ -26,9 +26,9 @@ export enum BezierControlType {
const CONTROL_MAX = 300;
/**
* 获取贝塞尔曲线垂直方向的控制节点
* @param fromPos 起始点
* @param toPos 终点
* Get the control node for the vertical direction of the Bézier curve
* @param fromPos starting point
* @param toPos destination
*/
export function getBezierVerticalControlPoints(
fromPos: IPoint,

View File

@@ -60,12 +60,12 @@ export const Interactive = () => {
function handleUpdateInteractiveType(interType: InteractiveType) {
if (interType === InteractiveType.Mouse) {
// 鼠标优先交互模式:更新状态 & 设置小手
// Mouse First Interactive Mode: Update Status & Set Small Hands
playground.editorState.changeState(
EditorState.STATE_MOUSE_FRIENDLY_SELECT.id,
);
} else if (interType === InteractiveType.Pad) {
// 触控板优先交互模式:更新状态 & 设置箭头
// Trackpad Priority Interaction Mode: Update Status & Settings Arrows
playground.editorState.changeState(EditorState.STATE_SELECT.id);
}
setInteractiveType(interType);
@@ -75,7 +75,7 @@ export const Interactive = () => {
useEffect(() => {
handleUpdateMouseScrollDelta(zoom => zoom / 20);
// 从缓存读取交互模式,应用生效
// Read interactive mode from cache, application takes effect
const preferInteractiveType = getPreferInteractiveType();
handleUpdateInteractiveType(preferInteractiveType as InteractiveType);
// eslint-disable-next-line react-hooks/exhaustive-deps -- init
@@ -92,7 +92,7 @@ export const Interactive = () => {
value={interactiveType}
onChange={value => {
setInteractiveType(value);
// 目前逻辑是,只从画布读取设置。
// The current logic is to only read settings from the canvas.
// setPreferInteractiveType(value);
handleUpdateInteractiveType(value as unknown as InteractiveType);
}}

View File

@@ -33,7 +33,7 @@ export const ZoomSelect = () => {
const zoomValue = tools.zoom * 100;
return zoomValue.toFixed(0);
}, [tools.zoom]);
// 为了覆盖 coze design 的样式,不能用 tailwind css
// In order to override the style of the cozed design, you cannot use tailwind css.
const zoomOptionStyle: CSSProperties = {
padding: '8px',
lineHeight: '16px',

View File

@@ -17,4 +17,4 @@
export const STROKE_WIDTH = 2;
export const ARROW_HEIGHT = 7;
export const LINE_CLASS_NAME = 'resource-tree-custom-line';
export const LINE_HOVER_DISTANCE = 8; // 线条 hover 的最小检测距离
export const LINE_HOVER_DISTANCE = 8; // Minimum detection distance of line hover

View File

@@ -26,7 +26,7 @@ interface NodeRenderState {
activatedNode?: FlowNodeEntity;
}
/**
* 渲染相关的全局状态管理
* Rendering-related global state management
*/
export class CustomRenderStateConfigEntity extends ConfigEntity<
NodeRenderState,

View File

@@ -20,7 +20,7 @@ interface CustomRenderState {
version: number;
}
/**
* 渲染相关的全局状态管理
* Rendering-related global state management
*/
export class CustomRenderStateEntity extends ConfigEntity<CustomRenderState> {
static type = 'CustomRenderStateEntity';

View File

@@ -43,11 +43,11 @@ export function createFixedLayoutPreset(
opts = { ...DEFAULT, ...opts };
let plugins: Plugin[] = [createOperationPlugin(opts)];
/**
* 加载默认编辑器配置
* Load default editor configuration
*/
plugins = createDefaultPreset(opts, plugins)(ctx);
/*
* 加载固定布局画布模块
* Load fixed layout canvas module
* */
plugins.push(
createPlaygroundPlugin<FixedLayoutPluginContext>({
@@ -66,15 +66,15 @@ export function createFixedLayoutPreset(
},
onInit: _ctx => {
_ctx.playground.registerLayers(
FlowNodesContentLayer, // 节点内容渲染
FlowNodesTransformLayer, // 节点位置偏移计算
FlowNodesContentLayer, // Node content rendering
FlowNodesTransformLayer, // Node position offset calculation
);
if (!opts.scroll?.disableScrollLimit) {
// 控制滚动范围
// Control scroll range
_ctx.playground.registerLayer(FlowScrollLimitLayer);
}
if (!opts.scroll?.disableScrollBar) {
// 控制条
// control bar
_ctx.playground.registerLayer(FlowScrollBarLayer);
}
if (opts.nodeRegistries) {

View File

@@ -30,7 +30,7 @@ import {
export interface FixedLayoutPluginContext extends EditorPluginContext {
document: FlowDocument;
/**
* 提供对画布节点相关操作方法, 并 支持 redo/undo
* Provide operation methods related to canvas nodes, and support redo/undo
*/
operation: FlowOperationService;
clipboard: ClipboardService;
@@ -39,14 +39,14 @@ export interface FixedLayoutPluginContext extends EditorPluginContext {
}
/**
* 固定布局配置
* fixed layout configuration
*/
export interface FixedLayoutProps
extends EditorProps<FixedLayoutPluginContext, FlowDocumentJSON> {
history?: FixedHistoryPluginOptions<FixedLayoutPluginContext> & {
disableShortcuts?: boolean;
};
defaultLayout?: FlowLayoutDefault | string; // 默认布局
defaultLayout?: FlowLayoutDefault | string; // default layout
}
export const DEFAULT: FixedLayoutProps =

View File

@@ -38,50 +38,50 @@ export class FlowRegisters
implements FlowDocumentContribution, FlowRendererContribution
{
/**
* 注册数据层
* registration data layer
* @param document
*/
registerDocument(document: FlowDocument) {
/**
* 注册节点 (ECS - Entity)
* Registered Node (ECS - Entity)
*/
document.registerFlowNodes(
// 等待简化
FixedLayoutRegistries.RootRegistry, // 根节点
FixedLayoutRegistries.StartRegistry, // 开始节点
FixedLayoutRegistries.DynamicSplitRegistry, // 动态分支(并行、排他)
FixedLayoutRegistries.BlockRegistry, // 单条 block 注册
FixedLayoutRegistries.InlineBlocksRegistry, // 多个 block 组成的 block 列表
FixedLayoutRegistries.BlockIconRegistry, // icon 节点,如条件分支的菱形图标
// FixedLayoutRegistries.EndRegistry, // 结束节点
FixedLayoutRegistries.EmptyRegistry, // 占位节点
// Wait for Simplification
FixedLayoutRegistries.RootRegistry, // root node
FixedLayoutRegistries.StartRegistry, // start node
FixedLayoutRegistries.DynamicSplitRegistry, // Dynamic branching (parallel, exclusive)
FixedLayoutRegistries.BlockRegistry, // Single block registration
FixedLayoutRegistries.InlineBlocksRegistry, // A list of multiple blocks
FixedLayoutRegistries.BlockIconRegistry, // Icon nodes, such as diamond icons for conditional branches
// FixedLayoutRegistries. EndRegistry,//End Node
FixedLayoutRegistries.EmptyRegistry, // placeholder node
);
/**
* 注册节点数据 (ECS - Component)
* Registered Node Data (ECS - Components)
*/
document.registerNodeDatas(
FlowNodeRenderData, // 渲染节点相关数据
FlowNodeTransitionData, // 线条绘制数据
FlowNodeTransformData, // 坐标计算数据
FlowNodeRenderData, // Render node related data
FlowNodeTransitionData, // line drawing data
FlowNodeTransformData, // coordinate calculation data
);
}
/**
* 注册渲染层
* Register the render layer
* @param renderer
*/
registerRenderer(renderer: FlowRendererRegistry) {
/**
* 注册 layer (ECS - System)
* Registration layer (ECS - System)
*/
renderer.registerLayers(
FlowNodesTransformLayer, // 节点位置渲染
FlowNodesContentLayer, // 节点内容渲染
FlowLinesLayer, // 线条渲染
// FlowLabelsLayer, // Label 渲染
PlaygroundLayer, // 画布基础层,提供缩放、手势等能力
FlowScrollLimitLayer, // 控制滚动范围
FlowScrollBarLayer, // 滚动条
FlowNodesTransformLayer, // Node position rendering
FlowNodesContentLayer, // Node content rendering
FlowLinesLayer, // line rendering
// FlowLabelsLayer,//Label rendering
PlaygroundLayer, // Canvas base layer, providing zoom, gesture, and more
FlowScrollLimitLayer, // Control scroll range
FlowScrollBarLayer, // scroll bar
);
}
}

View File

@@ -30,56 +30,56 @@ import { getStoreNode } from '../utils';
export interface NodeRenderReturnType {
/**
* 当前节点 (如果是 icon 则会返回它的父节点)
* The current node (if it is an icon, it will return its parent node)
*/
node: FlowNodeEntity;
/**
* 节点是否激活
* Is the node active?
*/
activated: boolean;
/**
* 节点是否展开
* Is the node expanded?
*/
expanded: boolean;
/**
* 鼠标进入, 主要用于控制 activated 状态
* Mouse entry, mainly used to control the activated state
*/
onMouseEnter: (e: React.MouseEvent) => void;
/**
* 鼠标离开, 主要用于控制 activated 状态
* The mouse leaves, mainly used to control the activated state
*/
onMouseLeave: (e: React.MouseEvent) => void;
/**
* 渲染表单,只有节点引擎开启才能使用
* Render the form, which can only be used if the node engine is turned on
*/
form: NodeFormProps<any> | undefined;
/**
* 获取节点的扩展数据
* Get the extended data of the node
*/
getExtInfo<T = any>(): T;
/**
* 更新节点的扩展数据
* Update the extended data of the node
* @param extInfo
*/
updateExtInfo<T = any>(extInfo: T): void;
/**
* 展开/收起节点
* Expand/Collapse Nodes
* @param expanded
*/
toggleExpand(): void;
/**
* 全局 readonly 状态
* Global readonly state
*/
readonly: boolean;
}
/**
* 自定义 useNodeRender
* 不区分 blockIcon inlineBlocks
* Custom useNodeRender
* Do not distinguish between blockIcon and inlineBlocks
*/
export function useCustomNodeRender(
nodeFromProps?: FlowNodeEntity,

View File

@@ -50,13 +50,13 @@ export const useEditorProps = (data?: any, json?: any) =>
[FlowRendererKey.BRANCH_ADDER]: () => null,
[FlowRendererKey.DRAG_NODE]: () => null,
},
renderDefaultNode: BaseNode, // 节点渲染
renderDefaultNode: BaseNode, // Node rendering
},
onReady(ctx) {
const treeService = ctx.get<TreeService>(TreeService);
treeService.transformSchema(json);
treeService.treeToFlowNodeJson();
// 强制 resize 生效
// Forced resizing takes effect
setTimeout(() => {
ctx.playground.resize();
}, 100);
@@ -67,12 +67,12 @@ export const useEditorProps = (data?: any, json?: any) =>
},
plugins: () => [
/**
* 自定义线条插件
* Custom Line Plugin
*/
createCustomLinesPlugin({}),
/**
* Minimap plugin
* 缩略图插件
* Thumbnail plugin
*/
createMinimapPlugin({
disableLayer: true,

View File

@@ -18,7 +18,7 @@ import { type FlowDocumentJSON } from '@flowgram-adapter/fixed-layout-editor';
// only for test
export const initialData: FlowDocumentJSON = {
nodes: [
// 开始节点
// start node
{
id: 'start_0',
type: 'split',
@@ -31,7 +31,7 @@ export const initialData: FlowDocumentJSON = {
id: 'noop',
type: 'block',
blocks: [
// 分支节点
// branch node
{
id: 'condition_0',
type: 'split',

View File

@@ -48,7 +48,7 @@ export class FlowLinesLayer extends Layer {
protected declare renderState: CustomRenderStateEntity;
/**
* 可视区域变化
* Visual area change
*/
onViewportChange = throttle(() => {
this.render();
@@ -85,7 +85,7 @@ export class FlowLinesLayer extends Layer {
render(): JSX.Element {
const isViewportVisible = this.config.isViewportVisible.bind(this.config);
// 还没初始化
// Not initialized yet
if (this.documentTransformer?.loading) {
return <></>;
}

View File

@@ -21,7 +21,7 @@ import {
type FlowNodeRegister,
} from '@flowgram-adapter/fixed-layout-editor';
/**
* BlockOrderIcon 的分支节点
* BlockOrderIcon-free branch nodes
*/
export const Split: FlowNodeRegister = {
type: 'split',
@@ -29,11 +29,11 @@ export const Split: FlowNodeRegister = {
onBlockChildCreate(
originParent: FlowNodeEntity,
blockData: FlowNodeJSON,
addedNodes: FlowNodeEntity[] = [], // 新创建的节点都要存在这里
addedNodes: FlowNodeEntity[] = [], // All newly created nodes must exist here
) {
const { document } = originParent;
const parent = document.getNode(`$inlineBlocks$${originParent.id}`);
// 块节点会生成一个空的 Block 节点用来切割 Block
// The block node will generate an empty block node to cut the block.
const proxyBlock = document.addNode({
id: `$block$${blockData.id}`,
type: FlowNodeBaseType.BLOCK,

View File

@@ -76,7 +76,7 @@ export class CustomHoverService {
}
/**
* 根据鼠标位置计算和线条的间距
* Calculate and line spacing based on mouse position
*/
getCloseInLineFromMousePos(
mousePos: IPoint,
@@ -123,7 +123,7 @@ export class CustomHoverService {
}
}
// 折叠策略:访问所有连线的元素,同时访问其父元素,执行 collapse
// Collapse strategy: access all connected elements and access their parent elements simultaneously, performing collapse.
hoverCollapse(from?: FlowNodeEntity) {
this.onHoverCollapseEmitter.fire(from);
}
@@ -143,7 +143,7 @@ export class CustomHoverService {
const selectNodes = this.getRelatedNodes(node);
this.renderStateEntity.setSelectNodes(selectNodes);
this.renderStateEntity.setActivatedNode(node);
// 高亮所有相关的线条
// Highlight all relevant lines
const activeLines: string[] = [];
this.linesManager.lines.map(line => {
const fromInclude = selectNodes.includes(line.from.id);
@@ -164,9 +164,9 @@ export class CustomHoverService {
}
this.edges.forEach(edge => {
if (edge.to === getTreeIdFromNodeId(node.id)) {
// push 额外线条
// Push extra lines
ancArr.push(edge.from);
// 遍历之前的节点信息
// Traverse the previous node information
const fromNode = this.document.getNode(getNodeIdFromTreeId(edge.from));
if (fromNode) {
const arr = this.traverseAncestors(fromNode);
@@ -195,7 +195,7 @@ export class CustomHoverService {
};
/**
* 根据当前树结构,遍历节点并且选中
* According to the current tree structure, iterate through the nodes and select
*/
getRelatedNodes = (node: FlowNodeEntity) => {
const parentRelated = this.traverseAncestors(node);

View File

@@ -36,7 +36,7 @@ export class CustomLinesManager {
@inject(EntityManager) declare entityManager: EntityManager;
// 额外的连线
// Additional connection
@inject(TreeService) declare treeService: TreeService;
get edges(): EdgeItem[] {
@@ -58,7 +58,7 @@ export class CustomLinesManager {
}
/**
* 过滤其他的非渲染节点
* Filter other non-rendering nodes
*/
filterTreeNode(nodes: FlowNodeEntity[]) {
return nodes.filter(
@@ -94,7 +94,7 @@ export class CustomLinesManager {
if (node.flowNodeType === 'root') {
queue.push(node.children[0]);
} else if (node.flowNodeType === 'split') {
// 分支逻辑特殊处理
// Branch logic special handling
const inlineBlocksChildren = node.children[1]?.children || [];
const branchChildren =
inlineBlocksChildren
@@ -138,7 +138,7 @@ export class CustomLinesManager {
}
renderLines() {
// 下一帧渲染,保证线条数据最新
// The next frame is rendered to ensure the latest line data
requestAnimationFrame(() => {
const lines = this.bfsAddLine(this.document.originTree.root);
const extraLines: CustomLine[] = (this.edges || [])

View File

@@ -53,9 +53,9 @@ export class TreeService {
edges: EdgeItem[] = [];
treeHistory: {
// 唯一标识符
// unique device identifier
id: string;
// 判断是否已经存在的两个条件
// To determine whether two conditions already exist
resourceId: string;
version?: string;
depth: number;
@@ -73,7 +73,7 @@ export class TreeService {
}
/**
* 用于检查是否有重复项
* Used to check for duplicates
*/
getNodeDuplicateFromTree = (id: string, version?: string) => {
let duplicate = false;
@@ -122,7 +122,7 @@ export class TreeService {
}
/**
* 处理 knowledgeplugintable 的逻辑
* Handling knowledge, plugins, table logic
*/
transformDuplicateInfo = (
id: string,
@@ -144,13 +144,13 @@ export class TreeService {
} else {
newSchema = transformTable(depth + 1, info);
}
// 如果重复,判断添加线还是节点
// If repeated, determine whether to add a line or a node
if (duplicate) {
for (const [i, d] of dupDepth.entries()) {
// 只要有一个匹配,就加线。
// 兜底走加节点的逻辑
// As long as there is a match, add the line.
// The logic of adding nodes at the bottom
if (depth + 1 === d) {
// 存到线条中
// Save in line
this.edges.push({
from: fromId,
to: dupId[i],
@@ -160,7 +160,7 @@ export class TreeService {
}
return newSchema;
}
// 否则,正常往 blocks 里添加数据
// Otherwise, add data to blocks normally
return newSchema;
};
@@ -202,7 +202,7 @@ export class TreeService {
}
}
const loop = node.id && isLoop(node.id, this.globalJson);
// 重复节点,停止继续往下
// Repeat node, stop, continue down
const endData = {
id: nodeId,
type: 'custom',
@@ -315,7 +315,7 @@ export class TreeService {
undefined,
nodeId,
);
// 检测 workflow 的重复
// Detect duplication of workflow
if (subWorkflowSchema) {
this.treeHistory.push({
id: subWorkflowSchema.id,
@@ -353,7 +353,7 @@ export class TreeService {
};
};
// 展开所有的子元素
// Expand all child elements
dfsCloneCollapsedOpen(node: TreeNode): TreeNode {
const children = node.children?.map(c => this.dfsCloneCollapsedOpen(c));
return {
@@ -366,14 +366,14 @@ export class TreeService {
};
}
// 根据 edges移动节点到另一个 TreeNode 的 children 下。
// 需要将内部的 collapsed 全部变成 open
// According to edges, move the node to the children of another TreeNode.
// It needs to be collapsed and opened
cloneNode(node: TreeNode) {
return this.dfsCloneCollapsedOpen(node);
}
/**
* 绑定父元素 parent
* Bind parent element parent
*/
bindTreeParent(node: TreeNode, parent?: TreeNode) {
if (parent) {
@@ -393,7 +393,7 @@ export class TreeService {
}
/**
* 初始化数据,将数据从后端数据 json 变成 tree 结构
* Initialize the data and turn the data from the back-end data json into a tree structure
*/
transformSchema(json: DependencyTree) {
this.globalJson = json;
@@ -465,7 +465,7 @@ export class TreeService {
this.addNode(block, addItem);
});
}
// 可能因此原本设置为结束的节点现在有 child 了
// Perhaps because of this, the node that was originally set to end now has children
if (json.blocks?.length) {
if (json.type === 'custom') {
json.type = 'split';

View File

@@ -28,23 +28,23 @@ export interface CustomLine {
}
/**
* 资源 icon 类型
* Resource icon type
*/
export enum NodeType {
WORKFLOW, // 工作流
CHAT_FLOW, // 对话流
KNOWLEDGE, // 知识库
PLUGIN, // 插件
DATABASE, // 数据库
WORKFLOW, // Workflow
CHAT_FLOW, // conversation flow
KNOWLEDGE, // Knowledge Base
PLUGIN, // plugin
DATABASE, // database
}
/**
* 资源来源
* source of resources
*/
export enum DependencyOrigin {
LIBRARY, // 资源库
LIBRARY, // resource library
APP, // App / Project
SHOP, // 商店
SHOP, // store
}
export interface EdgeItem {

View File

@@ -20,7 +20,7 @@ export interface TreeNode {
id: string;
type: string;
meta?: FlowNodeMeta;
// collapseddepth 放在 data
// Collapsed, depth in data
data: Record<string, any>;
parent: TreeNode[];
children?: TreeNode[];

View File

@@ -125,18 +125,18 @@ const findNode = (
};
/**
* 判断是否循环
* Determine whether to cycle
*/
export const isLoop = (id: string, json: DependencyTree) => {
const node = findNode(json, id);
// 只有 workflow 会存在循环,因此只需要关注 workflow_version
// Only workflow has loops, so just focus on workflow_version
if (node?.dependency?.workflow_version?.length) {
return hasSameId(id, node.dependency.workflow_version, json);
}
return false;
};
// 判断循环
// judgment loop
const hasSameId = (
id: string,
arr: WorkflowVersionInfo[],

View File

@@ -29,18 +29,18 @@ const SUPPORT_NEGATIVE_SCROLL_TOP = supportNegativeScrollTop();
import styles from './index.module.less';
export interface UseScrollViewControllerAndStateParams {
/** 滚动方向 */
/** scroll direction */
reverse: boolean;
/** 滚动状态,自动吸顶/吸底时依赖当Top时自动吸顶Bottom时自动吸底 */
/** Scroll state, automatic ceiling/bottom dependence, automatic ceiling when Top, automatic bottom when Bottom */
scrollStatusRef?: RefObject<ScrollStatus>;
}
export interface UseScrollViewControllerAndStateReturnValue {
/** 注入到滚动容器的引用 */
/** Reference injected into the scrolling container */
ref: RefObject<HTMLDivElement>;
/** 滚动容器外层 dom 的引用 */
/** Reference to the outer dom of a rolling container */
wrapperRef: RefObject<HTMLDivElement>;
/** 控制器 */
/** controller */
controller: ScrollViewController;
}
@@ -141,7 +141,7 @@ export const useScrollViewControllerAndState = ({
const { offsetHeight, scrollHeight } = container;
/** 当当前不是滚动状态时,不调整滚动进度 */
/** When the current state is not scrolling, the scrolling progress is not adjusted */
if (scrollHeight <= offsetHeight) {
return;
}
@@ -261,18 +261,18 @@ export const useScrollViewControllerAndState = ({
};
export interface UseAutoAnchorWhenPrependOnSafariParams {
/** 滚动方法 */
/** rolling method */
scrollTo: ScrollViewController['scrollTo'];
/** 获取当前滚动距底部距离 */
/** Get the current scroll distance to the bottom */
getScrollBottom: ScrollViewController['getScrollTop'];
/** 滚动方向 */
/** scroll direction */
reverse: boolean;
/** 启用锚定时离边界的最小值,默认为10 */
/** The minimum value from the boundary when anchoring is enabled, defaults to 10 */
enableThreshold?: number;
}
/**
* 处理y-reverse在Safari下向下插入元素时自动锚定的问题safari不支持overflow-anchor属性)
* Handle the issue of automatic anchoring when y-reverse inserts elements down in Safari (Safari does not support overflow-anchor attribute)
*/
export const useAutoAnchorWhenAppendOnSafari = ({
scrollTo,
@@ -300,7 +300,7 @@ export const useAutoAnchorWhenAppendOnSafari = ({
const currentContainerHeight = container.getBoundingClientRect().height;
if (prevLastChild && isNumber(prevContainerHeight)) {
/** 末尾元素变了,同时高度变了,那就断定为末尾元素插入,但是仅当超过阈值时才锚定 */
/** The end element has changed, and the height has changed at the same time, so it is concluded that the end element is inserted, but it is only anchored when the threshold is exceeded */
if (
prevContainerHeight !== currentContainerHeight &&
currentLastChild !== prevLastChild &&

View File

@@ -68,7 +68,7 @@ export const ScrollView = forwardRef<ScrollViewController, ScrollViewProps>(
},
outerRef,
) {
/** 在最开始的时候,默认的滚动状态 */
/** At the very beginning, the default scrolling state */
const defaultScrollStatus = reverse
? ScrollStatus.Bottom
: ScrollStatus.Top;
@@ -113,7 +113,7 @@ export const ScrollView = forwardRef<ScrollViewController, ScrollViewProps>(
const anchorThreshold = 0;
/** 滚动至事件上边界 */
/** Scroll to the upper boundary of the event */
if (getScrollTop() < topThreshold) {
if (!isReachTopRef.current) {
isReachTopRef.current = true;
@@ -126,7 +126,7 @@ export const ScrollView = forwardRef<ScrollViewController, ScrollViewProps>(
}
}
/** 滚动至事件下边界 */
/** Scroll to the lower boundary of the event */
if (getScrollBottom() < bottomThreshold) {
if (!isReachBottomRef.current) {
isReachBottomRef.current = true;
@@ -139,7 +139,7 @@ export const ScrollView = forwardRef<ScrollViewController, ScrollViewProps>(
}
}
/** 滚动至自动贴边anchor边界先释放再延迟更新贴边态防止还未滚出贴边阈值时自动贴边和滚动冲突 */
/** Scroll to the automatic welt (anchor) boundary, release first and then delay updating the welt state to prevent automatic welt and scroll conflicts when the welt threshold has not been rolled out */
scrollStatusRef.current = ScrollStatus.Inner;
if (
getScrollTop() <= anchorThreshold &&

View File

@@ -17,31 +17,31 @@
import { type RefObject } from 'react';
export interface ScrollViewController {
/** 滚动到 */
/** Scroll to */
scrollTo: (update: (prev: number) => number) => void;
/** 滚动到可滚动高度的指定百分比的位置以容器顶部为参考基线当滚动完毕后回调callback */
/** Scroll to a specified percentage of the scrollable height, using the top of the container as a reference baseline; callback when scrolling is complete */
scrollToPercentage: (ratio: number) => Promise<void> | void;
/** 获取当前滚动百分比 */
/** Get the current scroll percentage */
getScrollPercentage: () => number;
/** 获取当前滚动状态距离顶部的距离适配y方向和y-reverse方向的情况 */
/** Get the distance from the top of the current scrolling state, adapt the y-direction and y-reverse direction */
getScrollTop: () => number;
/** 获取原始的 scroll top 值,不做换算 */
/** Get the original scroll top value without conversion */
getOriginScrollInfo: () => {
scrollHeight: number;
scrollTop: number;
rect: null | DOMRect;
};
/** 获取当前滚动状态距离顶部的距离适配y方向和y-reverse方向的情况 */
/** Get the distance from the top of the current scrolling state, adapt the y-direction and y-reverse direction */
getScrollBottom: () => number;
/** 更新吸顶/吸底状态当数据有更新时主动调用此API */
/** Update the ceiling/bottom suction status, and actively call this API when the data is updated */
refreshAnchor: () => void;
/** 禁止容器滚动 */
/** Disable container scrolling */
disableScroll: () => void;
/** 使得容器可滚动 */
/** Makes the container scrollable */
enableScroll: () => void;
/** 检查内容充满容器(用于初始状态高度较小的情况,防止无法触发 scroll 事件) */
/** Check that the content fills the container (used when the initial state height is small to prevent the scroll event from being triggered) */
checkContentIsFull: () => boolean;
/** 获取 scroll 外层容器的引用 */
/** Get a reference to the scroll outer container */
getScrollViewWrapper: () => RefObject<HTMLDivElement>;
}
@@ -63,35 +63,35 @@ export interface ScrollViewProps
innerBefore?:
| ((controller: ScrollViewController) => JSX.Element)
| JSX.Element;
/** 是否反转,从下往上滚动 */
/** Whether to reverse, scroll from bottom to top */
reverse?: boolean;
/** 剩余滚动至顶部距离小于多少时触发,默认为offsetHeight */
/** Triggered when the remaining scroll to the top is less than the distance, the default is offsetHeight */
reachTopThreshold?: number;
/* 滚动到达顶部阈值 */
/* Scroll to the top threshold */
onReachTop?: () => unknown;
/* 滚动离开顶部阈值 */
/* Scroll away from the top threshold */
onLeaveTop?: () => unknown;
/** 剩余滚动至底部距离小于多少时触发,默认为offsetHeight */
/** Triggered when the remaining scroll to the bottom distance is less than, the default is offsetHeight */
reachBottomThreshold?: number;
/** 滚动到达底部阈值 */
/** Scroll to the bottom threshold */
onReachBottom?: () => unknown;
/** 滚动离开底部阈值 */
/** Scroll away from the bottom threshold */
onLeaveBottom?: () => unknown;
/** 不管内容是否超出 container 都展示 scrollBar */
/** Show scrollBar regardless of whether the content exceeds the container. */
showScrollbar?: boolean;
/** 内容超出 container 时才展示 scrollBar若未超出不展示 scrollBar */
/** The scrollBar is only displayed when the content exceeds the container. If it does not exceed, the scrollBar is not displayed. */
autoShowScrollbar?: boolean;
/** 完全隐藏 scrollbar */
/** Completely hide scrollbar */
scrollbarWidthNone?: boolean;
}
/** 滚动状态 */
/** scrolling state */
export enum ScrollStatus {
/** 吸顶 */
/** Ceiling */
Top = 'top',
/** 吸底 */
/** bottom suction */
Bottom = 'bottom',
/** 中间可双向滚动 */
/** Two-way scrolling in the middle */
Inner = 'inner',
}

View File

@@ -15,7 +15,7 @@
*/
/**
* 出自:https://stackoverflow.com/questions/4900436/how-to-detect-the-installed-chrome-version
* From: https://stackoverflow.com/questions/4900436/how-to-detect-the-installed-chrome-version
*/
export const getChromeVersion = () => {
const pieces = navigator.userAgent.match(
@@ -38,13 +38,13 @@ export const getChromeVersion = () => {
};
/**
* 是否支持在column-reverse模式下为负数的scrollTopchromium最低支持版本83.0.4086.1(上一个版本为82.0.4082.0
* Whether to support scrollTop with negative numbers in column-reverse mode, chromium minimum supported version 83.0.4086 (previous version was 82.0.4082)
*/
export const supportNegativeScrollTop = () => {
const chromeVersion = getChromeVersion();
if (!chromeVersion) {
/** 假设非chromium系浏览器均支持 */
/** Suppose all non-chromium browsers support it */
return true;
}

View File

@@ -15,8 +15,8 @@
*/
/**
* 是否是在苹果平台的Webkit内核浏览器下
* 这个判断条件不等于是在苹果设备下因为部分苹果设备例如Mac可以运行非原生Webkit引擎的浏览器例如Chromium(Blink)
* Is it under the Webkit kernel browser of the Apple platform?
* Note: This judgment condition is not equal to under Apple devices, because some Apple devices (such as Mac) can run non-native Webkit engine browsers, such as Chromium (Blink)
*/
export const isAppleWebkit = () =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -22,7 +22,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ImageRender } from '../../../src/components/renders/image-render';
// 模拟依赖
// simulated dependency
vi.mock('@coze-arch/bot-semi', () => ({
Image: ({ src, fallback, placeholder, onClick, preview, ...props }: any) => {
if (!src) {
@@ -55,7 +55,7 @@ vi.mock('@coze-arch/bot-icons', () => ({
),
}));
// 模拟useImagePreview钩子
// Simulation useImagePreview hook
vi.mock(
'../../../src/components/renders/image-render/use-image-preview',
() => ({
@@ -84,7 +84,7 @@ describe('ImageRender', () => {
render(<ImageRender srcList={srcList} />);
// 验证图片容器被渲染
// Verify that the image container is rendered
const images = screen.getAllByTestId('image');
expect(images).toHaveLength(2);
expect(images[0]).toHaveAttribute('src', srcList[0]);
@@ -94,11 +94,11 @@ describe('ImageRender', () => {
test('应该处理空的图片列表', () => {
render(<ImageRender srcList={[]} />);
// 验证没有图片被渲染
// Verify that no images are being rendered
const images = screen.queryAllByTestId('image');
expect(images).toHaveLength(0);
// 验证空状态容器存在
// Verify that an empty state container exists
const emptyContainer = screen.getByTestId('image-preview-modal');
expect(emptyContainer).toBeInTheDocument();
});
@@ -112,7 +112,7 @@ describe('ImageRender', () => {
render(<ImageRender srcList={[]} customEmpty={customEmpty} />);
// 验证自定义空状态被渲染
// Verify that the custom empty state is rendered
const customEmptyElement = screen.getByTestId('custom-empty');
expect(customEmptyElement).toBeInTheDocument();
expect(customEmptyElement).toHaveTextContent('自定义空状态');
@@ -126,8 +126,8 @@ describe('ImageRender', () => {
/>,
);
// 验证自定义className被应用
// 由于组件结构复杂,我们直接查找包含custom-class的元素
// Verify that the custom className is applied
// Due to the complex structure of components, we directly look for elements containing custom-classes
const container = document.querySelector('.custom-class');
expect(container).toBeInTheDocument();
});
@@ -137,11 +137,11 @@ describe('ImageRender', () => {
render(<ImageRender srcList={srcList} />);
// 点击图片
// Click on the picture.
const image = screen.getByTestId('image');
fireEvent.click(image);
// 验证预览模态框存在
// Verify that the preview modal box exists
const previewModal = screen.getByTestId('image-preview-modal');
expect(previewModal).toBeInTheDocument();
expect(previewModal).toHaveAttribute('data-src', srcList[0]);
@@ -155,7 +155,7 @@ describe('ImageRender', () => {
/>,
);
// 验证editable属性被传递给预览模态框
// Verify that the editable property is passed to the preview modal box
const previewModal = screen.getByTestId('image-preview-modal');
expect(previewModal).toHaveAttribute('data-editable', 'false');
});
@@ -169,7 +169,7 @@ describe('ImageRender', () => {
/>,
);
// 验证onChange属性被传递给预览模态框
// Verify that the onChange property is passed to the preview modal box
const previewModal = screen.getByTestId('image-preview-modal');
expect(previewModal).toBeInTheDocument();
});

View File

@@ -22,7 +22,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useImagePreview } from '../../../src/components/renders/image-render/use-image-preview';
// 模拟依赖
// simulated dependency
vi.mock('@coze-arch/coze-design', () => ({
Upload: function Upload({
children,
@@ -167,7 +167,7 @@ vi.mock('../../../src/components/renders/image-render/utils', () => ({
isValidSize: vi.fn().mockReturnValue(true),
}));
// 模拟 CustomError
// Simulate CustomError
vi.mock('@coze-arch/bot-error', () => ({
CustomError: class CustomError extends Error {
constructor(event: string, message: string) {
@@ -179,7 +179,7 @@ vi.mock('@coze-arch/bot-error', () => ({
describe('useImagePreview 基本功能测试', () => {
test('测试图片URL输入框更新', () => {
// 创建一个简单的测试组件
// Create a simple test component
const TestComponent = () => {
const [src, setSrc] = React.useState('https://example.com/image.jpg');
const onChange = vi.fn();
@@ -201,16 +201,16 @@ describe('useImagePreview 基本功能测试', () => {
render(<TestComponent />);
// 验证初始URL正确显示
// Verify that the initial URL is displayed correctly
const urlInput = screen.getByTestId('image-url-input');
expect(urlInput).toHaveValue('https://example.com/image.jpg');
// 修改URL
// Modify URL
fireEvent.change(urlInput, {
target: { value: 'https://example.com/new-image.jpg' },
});
// 验证URL已更新
// Verify that the URL has been updated
expect(
screen.getByText('当前图片URL: https://example.com/new-image.jpg'),
).toBeInTheDocument();
@@ -219,7 +219,7 @@ describe('useImagePreview 基本功能测试', () => {
test('测试确认按钮调用onChange', () => {
const onChange = vi.fn();
// 创建一个简单的测试组件
// Create a simple test component
const TestComponent = () => {
const [src, setSrc] = React.useState('https://example.com/image.jpg');
@@ -235,16 +235,16 @@ describe('useImagePreview 基本功能测试', () => {
render(<TestComponent />);
// 点击确认按钮
// Click the confirm button.
const okButton = screen.getByTestId('modal-ok');
fireEvent.click(okButton);
// 验证onChange被调用
// Verify that onChange is invoked
expect(onChange).toHaveBeenCalledWith('https://example.com/image.jpg', '');
});
test('测试editable属性', () => {
// 创建一个简单的测试组件
// Create a simple test component
const TestComponent = () => {
const [src, setSrc] = React.useState('https://example.com/image.jpg');
const onChange = vi.fn();
@@ -261,7 +261,7 @@ describe('useImagePreview 基本功能测试', () => {
render(<TestComponent />);
// 验证输入框被禁用
// Verify text box is disabled
const urlInput = screen.getByTestId('image-url-input');
expect(urlInput).toBeDisabled();
});

View File

@@ -23,7 +23,7 @@ import {
isValidSize,
} from '../../../src/components/renders/image-render/utils';
// 模拟 CustomError
// Simulate CustomError
vi.mock('@coze-arch/bot-error', () => ({
CustomError: class CustomError extends Error {
constructor(event: string, message: string) {
@@ -48,7 +48,7 @@ describe('getFileExtension', () => {
describe('isValidSize', () => {
test('文件大小小于限制时应返回true', () => {
// 20MB限制
// 20MB limit
const validSize = 10 * 1024 * 1024; // 10MB
expect(isValidSize(validSize)).toBe(true);
});
@@ -66,10 +66,10 @@ describe('isValidSize', () => {
describe('getBase64', () => {
test('应该正确转换文件为base64字符串', async () => {
// 创建一个模拟的Blob对象
// Create a simulated blob object
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
// 模拟FileReader
// Analog FileReader
const mockFileReader = {
readAsDataURL: vi.fn(),
onload: null as any,
@@ -77,36 +77,36 @@ describe('getBase64', () => {
onabort: null as any,
};
// 保存原始的FileReader
// Save the original FileReader
const originalFileReader = global.FileReader;
// 模拟FileReader构造函数
// Mock FileReader constructor
global.FileReader = vi.fn(() => mockFileReader) as any;
// 调用getBase64
// Call getBase64
const promise = getBase64(mockBlob);
// 触发onload事件
// Trigger onload event
mockFileReader.onload({
target: {
result: 'data:text/plain;base64,dGVzdCBjb250ZW50',
},
} as any);
// 验证结果
// validation result
const result = await promise;
expect(result).toBe('dGVzdCBjb250ZW50');
expect(mockFileReader.readAsDataURL).toHaveBeenCalledWith(mockBlob);
// 恢复原始的FileReader
// Restore the original FileReader
global.FileReader = originalFileReader;
});
test('当FileReader.onload返回非字符串结果时应拒绝Promise', async () => {
// 创建一个模拟的Blob对象
// Create a simulated blob object
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
// 模拟FileReader
// Analog FileReader
const mockFileReader = {
readAsDataURL: vi.fn(),
onload: null as any,
@@ -114,34 +114,34 @@ describe('getBase64', () => {
onabort: null as any,
};
// 保存原始的FileReader
// Save the original FileReader
const originalFileReader = global.FileReader;
// 模拟FileReader构造函数
// Mock FileReader constructor
global.FileReader = vi.fn(() => mockFileReader) as any;
// 调用getBase64
// Call getBase64
const promise = getBase64(mockBlob);
// 触发onload事件但返回非字符串结果
// Fires the onload event, but returns a non-string result
mockFileReader.onload({
target: {
result: null,
},
} as any);
// 验证Promise被拒绝
// Verification Promise Rejected
await expect(promise).rejects.toThrow('file read invalid');
// 恢复原始的FileReader
// Restore the original FileReader
global.FileReader = originalFileReader;
});
test('当FileReader.onerror触发时应拒绝Promise', async () => {
// 创建一个模拟的Blob对象
// Create a simulated blob object
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
// 模拟FileReader
// Analog FileReader
const mockFileReader = {
readAsDataURL: vi.fn(),
onload: null as any,
@@ -149,30 +149,30 @@ describe('getBase64', () => {
onabort: null as any,
};
// 保存原始的FileReader
// Save the original FileReader
const originalFileReader = global.FileReader;
// 模拟FileReader构造函数
// Mock FileReader constructor
global.FileReader = vi.fn(() => mockFileReader) as any;
// 调用getBase64
// Call getBase64
const promise = getBase64(mockBlob);
// 触发onerror事件
// Trigger the onerror event
mockFileReader.onerror();
// 验证Promise被拒绝
// Verification Promise Rejected
await expect(promise).rejects.toThrow('file read fail');
// 恢复原始的FileReader
// Restore the original FileReader
global.FileReader = originalFileReader;
});
test('当FileReader.onabort触发时应拒绝Promise', async () => {
// 创建一个模拟的Blob对象
// Create a simulated blob object
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
// 模拟FileReader
// Analog FileReader
const mockFileReader = {
readAsDataURL: vi.fn(),
onload: null as any,
@@ -180,100 +180,100 @@ describe('getBase64', () => {
onabort: null as any,
};
// 保存原始的FileReader
// Save the original FileReader
const originalFileReader = global.FileReader;
// 模拟FileReader构造函数
// Mock FileReader constructor
global.FileReader = vi.fn(() => mockFileReader) as any;
// 调用getBase64
// Call getBase64
const promise = getBase64(mockBlob);
// 触发onabort事件
// Trigger onabort event
mockFileReader.onabort();
// 验证Promise被拒绝
// Verification Promise Rejected
await expect(promise).rejects.toThrow('file read abort');
// 恢复原始的FileReader
// Restore the original FileReader
global.FileReader = originalFileReader;
});
});
describe('getUint8Array', () => {
test('应该正确转换文件为Uint8Array', async () => {
// 创建一个模拟的Blob对象
// Create a simulated blob object
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
// 创建一个模拟的ArrayBuffer
const mockArrayBuffer = new ArrayBuffer(12); // 'test content' 的长度
// Create a simulated ArrayBuffer
const mockArrayBuffer = new ArrayBuffer(12); // Length of'test content '
const uint8Array = new Uint8Array(mockArrayBuffer);
for (let i = 0; i < 12; i++) {
uint8Array[i] = 'test content'.charCodeAt(i);
}
// 模拟FileReader
// Analog FileReader
const mockFileReader = {
readAsArrayBuffer: vi.fn(),
onload: null as any,
};
// 保存原始的FileReader
// Save the original FileReader
const originalFileReader = global.FileReader;
// 模拟FileReader构造函数
// Mock FileReader constructor
global.FileReader = vi.fn(() => mockFileReader) as any;
// 调用getUint8Array
// Call getUint8Array
const promise = getUint8Array(mockBlob);
// 触发onload事件
// Trigger onload event
mockFileReader.onload({
target: {
result: mockArrayBuffer,
},
} as any);
// 验证结果
// validation result
const result = await promise;
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(12);
expect(mockFileReader.readAsArrayBuffer).toHaveBeenCalledWith(mockBlob);
// 恢复原始的FileReader
// Restore the original FileReader
global.FileReader = originalFileReader;
});
test('当FileReader.onload返回无效结果时应拒绝Promise', async () => {
// 创建一个模拟的Blob对象
// Create a simulated blob object
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
// 模拟FileReader
// Analog FileReader
const mockFileReader = {
readAsArrayBuffer: vi.fn(),
onload: null as any,
};
// 保存原始的FileReader
// Save the original FileReader
const originalFileReader = global.FileReader;
// 模拟FileReader构造函数
// Mock FileReader constructor
global.FileReader = vi.fn(() => mockFileReader) as any;
// 调用getUint8Array
// Call getUint8Array
const promise = getUint8Array(mockBlob);
// 触发onload事件,但返回无效结果
// The onload event is fired, but an invalid result is returned
mockFileReader.onload({
target: {
result: null,
},
} as any);
// 验证Promise被拒绝
// Verification Promise Rejected
await expect(promise).rejects.toThrow('file read invalid');
// 恢复原始的FileReader
// Restore the original FileReader
global.FileReader = originalFileReader;
});
});

View File

@@ -22,7 +22,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ActionsRender } from '../../../src/components/renders/actions-render';
// 使用vi.mock的回调函数形式来避免linter错误
// Use the callback function form of vi.mock to avoid linter errors
vi.mock('@coze-arch/coze-design/icons', () => ({
IconCozEdit: () => <div data-testid="edit-icon" />,
IconCozTrashCan: () => <div data-testid="delete-icon" />,
@@ -49,9 +49,9 @@ describe('ActionsRender', () => {
render(<ActionsRender record={mockRecord} index={mockIndex} />);
// 验证按钮被渲染
// Verify button is rendered
const buttons = screen.getAllByTestId('button');
expect(buttons).toHaveLength(2); // 编辑和删除按钮
expect(buttons).toHaveLength(2); // Edit and delete buttons
});
test('应该在点击编辑按钮时调用onEdit回调', () => {
@@ -67,11 +67,11 @@ describe('ActionsRender', () => {
/>,
);
// 点击编辑按钮
// Click the Edit button
const buttons = screen.getAllByTestId('button');
fireEvent.click(buttons[0]); // 第一个按钮是编辑按钮
fireEvent.click(buttons[0]); // The first button is the edit button
// 验证编辑回调被调用
// Verify edit callback is invoked
expect(mockOnEdit).toHaveBeenCalledWith(mockRecord, mockIndex);
});
@@ -88,11 +88,11 @@ describe('ActionsRender', () => {
/>,
);
// 点击删除按钮
// Click the Delete button.
const buttons = screen.getAllByTestId('button');
fireEvent.click(buttons[1]); // 第二个按钮是删除按钮
fireEvent.click(buttons[1]); // The second button is the delete button.
// 验证删除回调被调用
// Verify that the delete callback is invoked
expect(mockOnDelete).toHaveBeenCalledWith(mockIndex);
});
@@ -108,7 +108,7 @@ describe('ActionsRender', () => {
/>,
);
// 验证只有一个按钮(删除按钮)
// Verify that there is only one button (delete button)
const buttons = screen.getAllByTestId('button');
expect(buttons).toHaveLength(1);
});
@@ -125,7 +125,7 @@ describe('ActionsRender', () => {
/>,
);
// 验证只有一个按钮(编辑按钮)
// Verify that there is only one button (edit button)
const buttons = screen.getAllByTestId('button');
expect(buttons).toHaveLength(1);
});

View File

@@ -22,7 +22,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { EditHeaderRender } from '../../../src/components/renders/edit-header-render';
// 模拟依赖
// simulated dependency
vi.mock('@coze-arch/bot-semi', () => {
const uiButton = ({ children, onClick, ...props }: any) => (
<button data-testid="button" onClick={onClick} {...props}>
@@ -82,7 +82,7 @@ describe('EditHeaderRender', () => {
<EditHeaderRender value="测试标题" onBlur={mockOnBlur} validator={{}} />,
);
// 验证预览模式显示正确的值
// Verify that the preview mode displays the correct value
const previewElement = screen.getByText('测试标题');
expect(previewElement).toBeInTheDocument();
});
@@ -94,11 +94,11 @@ describe('EditHeaderRender', () => {
<EditHeaderRender value="测试标题" onBlur={mockOnBlur} validator={{}} />,
);
// 点击预览文本
// Click to preview text
const previewElement = screen.getByText('测试标题');
fireEvent.click(previewElement);
// 验证输入框出现
// Verify text box appears
const inputElement = screen.getByTestId('input');
expect(inputElement).toBeInTheDocument();
expect(inputElement).toHaveValue('测试标题');
@@ -108,7 +108,7 @@ describe('EditHeaderRender', () => {
const mockOnBlur = vi.fn();
const mockEditPropsOnBlur = vi.fn();
// 渲染组件
// rendering component
render(
<EditHeaderRender
value="测试标题"
@@ -120,17 +120,17 @@ describe('EditHeaderRender', () => {
/>,
);
// 点击预览文本进入编辑模式
// Click on the preview text to enter edit mode
const previewElement = screen.getByText('测试标题');
fireEvent.click(previewElement);
// 获取输入框
// Get text box
const inputElement = screen.getByTestId('input');
// 触发 blur 事件,让组件内部的 onBlurFn 函数被调用
// The blur event is triggered, causing the onBlurFn function inside the component to be called
fireEvent.blur(inputElement);
// 验证 editProps.onBlur 被调用,并且传递了正确的参数
// Verify that editProps.onBlur is called and the correct parameters are passed
expect(mockEditPropsOnBlur).toHaveBeenCalledWith('测试标题');
});
@@ -149,15 +149,15 @@ describe('EditHeaderRender', () => {
/>,
);
// 点击预览文本进入编辑模式
// Click on the preview text to enter edit mode
const previewElement = screen.getByText('测试标题');
fireEvent.click(previewElement);
// 获取输入框并修改值
// Get the text box and modify the value
const inputElement = screen.getByTestId('input');
fireEvent.change(inputElement, { target: { value: '新标题' } });
// 验证 onChange 回调被调用
// Verify that the onChange callback is invoked
expect(mockOnChange).toHaveBeenCalledWith('新标题');
});
@@ -177,11 +177,11 @@ describe('EditHeaderRender', () => {
/>,
);
// 点击删除按钮
// Click the Delete button.
const deleteButton = screen.getByTestId('button');
fireEvent.click(deleteButton);
// 验证 onDelete 回调被调用
// Verify that the onDelete callback is invoked
expect(mockOnDelete).toHaveBeenCalledWith('测试标题');
});
@@ -201,11 +201,11 @@ describe('EditHeaderRender', () => {
/>,
);
// 验证删除按钮被禁用
// Verify that the delete button is disabled
const deleteButton = screen.getByTestId('button');
expect(deleteButton).toHaveAttribute('disabled');
// 点击删除按钮不应调用回调
// Clicking the delete button should not invoke a callback
fireEvent.click(deleteButton);
expect(mockOnDelete).not.toHaveBeenCalled();
});
@@ -222,7 +222,7 @@ describe('EditHeaderRender', () => {
/>,
);
// 验证删除按钮不存在
// Verify that the delete button does not exist
expect(screen.queryByTestId('button')).not.toBeInTheDocument();
});
@@ -241,12 +241,12 @@ describe('EditHeaderRender', () => {
/>,
);
// 点击预览文本进入编辑模式
// Click on the preview text to enter edit mode
const previewElement = screen.getByText('测试标题');
fireEvent.click(previewElement);
// 由于我们的模拟实现中,错误图标和提示是通过 suffix 属性传递的
// 所以我们需要检查 tooltip error-icon 是否存在于文档中
// Since in our simulation implementation, error icons and hints are passed through the suffix attribute
// So we need to check if tooltip and error-icon exist in the document
expect(screen.getByTestId('error-icon')).toBeInTheDocument();
expect(screen.getByTestId('tooltip')).toHaveAttribute(
'data-content',

View File

@@ -39,7 +39,7 @@ describe('TagRender', () => {
<TagRender value="标签文本" record={mockRecord} index={mockIndex} />,
);
// 验证标签内容被正确渲染
// Verify that the tag content is rendered correctly
const tag = screen.getByTestId('tag');
expect(tag).toBeInTheDocument();
expect(tag).toHaveTextContent('标签文本');
@@ -53,7 +53,7 @@ describe('TagRender', () => {
<TagRender value="标签文本" record={mockRecord} index={mockIndex} />,
);
// 验证标签使用默认颜色
// Verify that the label uses the default color
const tag = screen.getByTestId('tag');
expect(tag).toHaveAttribute('data-color', 'primary');
});
@@ -71,7 +71,7 @@ describe('TagRender', () => {
/>,
);
// 验证标签使用自定义颜色
// Verify labels with custom colors
const tag = screen.getByTestId('tag');
expect(tag).toHaveAttribute('data-color', 'red');
});
@@ -84,7 +84,7 @@ describe('TagRender', () => {
<TagRender value={undefined} record={mockRecord} index={mockIndex} />,
);
// 验证标签内容为空字符串
// Verify tag content is empty string
const tag = screen.getByTestId('tag');
expect(tag).toHaveTextContent('');
});

View File

@@ -22,7 +22,7 @@ import '@testing-library/jest-dom';
import { TextRender } from '../../../src/components/renders/text-render';
// 模拟依赖
// simulated dependency
vi.mock('@coze-arch/coze-design', () => ({
TextArea: ({
value,
@@ -80,10 +80,10 @@ describe('TextRender', () => {
/>,
);
// 验证文本内容被正确渲染
// Verify that the text content is rendered correctly
expect(screen.getByText('测试文本')).toBeInTheDocument();
// 验证 TextArea 不可见
// Verify that the TextArea is not visible
expect(screen.queryByTestId('text-area')).not.toBeInTheDocument();
});
@@ -99,13 +99,13 @@ describe('TextRender', () => {
/>,
);
// 验证文本内容被正确渲染
// Verify that the text content is rendered correctly
expect(screen.getByText('测试文本')).toBeInTheDocument();
// 点击文本进入编辑模式
// Click on the text to enter edit mode
fireEvent.click(screen.getByText('测试文本'));
// 验证 TextArea 可见
// Verify that the TextArea is visible
expect(screen.getByTestId('text-area')).toBeInTheDocument();
expect(screen.getByTestId('text-area')).toHaveValue('测试文本');
});
@@ -122,20 +122,20 @@ describe('TextRender', () => {
/>,
);
// 点击文本进入编辑模式
// Click on the text to enter edit mode
fireEvent.click(screen.getByText('测试文本'));
// 修改输入值
// Modify input value
fireEvent.change(screen.getByTestId('text-area'), {
target: { value: '新文本' },
});
// 验证 onChange 被调用
// Verify that onChange is invoked
expect(mockOnChange).toHaveBeenCalledWith('新文本', mockRecord, mockIndex);
});
test('应该在失去焦点时调用 onBlur', async () => {
// 使用 dataIndex 属性
// Using the dataIndex property
render(
<TextRender
value="测试文本"
@@ -144,33 +144,33 @@ describe('TextRender', () => {
onBlur={mockOnBlur}
onChange={mockOnChange}
editable={true}
dataIndex="name" // 添加 dataIndex 属性
dataIndex="name" // Add dataIndex property
/>,
);
// 点击文本进入编辑模式
// Click on the text to enter edit mode
fireEvent.click(screen.getByText('测试文本'));
// 修改输入值
// Modify input value
fireEvent.change(screen.getByTestId('text-area'), {
target: { value: '新文本' },
});
// 失去焦点
// Lost focus
fireEvent.blur(screen.getByTestId('text-area'));
// 验证 onBlur 被调用,并且传递了正确的参数
// 根据组件实现onBlur 会被调用,参数是 inputValue, updateRecord, index
// 其中 updateRecord { ...record, [dataIndex]: inputValue } 并且删除了 tableViewKey
// Verify that onBlur is called and the correct parameters are passed
// Depending on the component implementation, onBlur will be called with the parameters inputValue, updateRecord, index
// Where updateRecord is {... record, [dataIndex]: inputValue} and removed tableViewKey
await waitFor(() => {
expect(mockOnBlur).toHaveBeenCalledWith(
'新文本',
{ id: '1', name: '新文本' }, // 更新后的 record
{ id: '1', name: '新文本' }, // Updated records
mockIndex,
);
});
// 验证组件回到只读模式
// Verify that the component returns to read-only mode
await waitFor(() => {
expect(screen.queryByTestId('text-area')).not.toBeInTheDocument();
expect(screen.getByText('新文本')).toBeInTheDocument();
@@ -179,7 +179,7 @@ describe('TextRender', () => {
test('应该在验证失败时显示错误提示', () => {
const mockValidator = {
validate: vi.fn().mockReturnValue(true), // 返回 true 表示验证失败
validate: vi.fn().mockReturnValue(true), // Returning true indicates that the verification failed.
errorMsg: '输入不合法',
};
@@ -192,32 +192,32 @@ describe('TextRender', () => {
onChange={mockOnChange}
editable={true}
validator={mockValidator}
isEditing={true} // 直接进入编辑模式
isEditing={true} // Go straight to edit mode
/>,
);
// 验证 TextArea 可见
// Verify that the TextArea is visible
expect(screen.getByTestId('text-area')).toBeInTheDocument();
// 修改输入值
// Modify input value
fireEvent.change(screen.getByTestId('text-area'), {
target: { value: '新文本' },
});
// 验证验证函数被调用
// The validation function is called
expect(mockValidator.validate).toHaveBeenCalledWith(
'新文本',
mockRecord,
mockIndex,
);
// 验证错误状态
// validation error status
expect(screen.getByTestId('text-area')).toHaveAttribute(
'data-validate-status',
'error',
);
// 验证错误图标和提示被显示
// Validation error icons and prompts are displayed
expect(screen.getByTestId('error-icon')).toBeInTheDocument();
expect(screen.getByTestId('tooltip')).toHaveAttribute(
'data-content',
@@ -238,7 +238,7 @@ describe('TextRender', () => {
/>,
);
// 验证 TextArea 直接可见
// Verify that the TextArea is directly visible
expect(screen.getByTestId('text-area')).toBeInTheDocument();
expect(screen.getByTestId('text-area')).toHaveValue('测试文本');
});
@@ -256,10 +256,10 @@ describe('TextRender', () => {
/>,
);
// 验证 TextArea 直接可见
// Verify that the TextArea is directly visible
expect(screen.getByTestId('text-area')).toBeInTheDocument();
// 重新渲染组件,isEditing undefined
// Render the component again, isEditing is undefined
rerender(
<TextRender
value="测试文本"
@@ -271,7 +271,7 @@ describe('TextRender', () => {
/>,
);
// 验证组件回到只读模式
// Verify that the component returns to read-only mode
await waitFor(() => {
expect(screen.queryByTestId('text-area')).not.toBeInTheDocument();
expect(screen.getByText('测试文本')).toBeInTheDocument();

View File

@@ -26,7 +26,7 @@ import {
EditToolBar,
} from '../../../src/components/table-view/edit-menu';
// 模拟依赖
// simulated dependency
vi.mock('@coze-arch/i18n', () => ({
I18n: {
t: (key: string, options?: any) => {
@@ -82,7 +82,7 @@ vi.mock('@coze-arch/coze-design/icons', () => ({
IconCozTrashCan: () => <div data-testid="icon-trash"></div>,
}));
// 模拟样式
// simulation style
vi.mock('../../../src/components/table-view/index.module.less', () => ({
default: {
'table-edit-menu': 'table-edit-menu-class',
@@ -207,11 +207,11 @@ describe('EditMenu 组件', () => {
expect.any(Function),
);
// 触发点击事件
// trigger click event
window.dispatchEvent(new Event('click'));
expect(mockOnExit).toHaveBeenCalled();
// 卸载组件
// uninstall components
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'click',
@@ -260,7 +260,7 @@ describe('EditToolBar 组件', () => {
expect(screen.getByTestId('button-group')).toBeInTheDocument();
expect(screen.getByText('已选择 2 项')).toBeInTheDocument();
expect(screen.getAllByTestId('button')).toHaveLength(3); // 编辑、删除和关闭按钮
expect(screen.getAllByTestId('button')).toHaveLength(3); // Edit, delete, and close buttons
});
test('点击编辑按钮应调用onEdit回调', () => {
@@ -352,7 +352,7 @@ describe('EditToolBar 组件', () => {
const toolbar = screen.getByTestId('button-group').parentElement;
expect(toolbar).toHaveStyle('margin-left: -145px');
// 重新渲染,只选择一个项目
// Re-render, select only one item
rerender(
<EditToolBar
configs={[]}

View File

@@ -22,7 +22,7 @@ import '@testing-library/jest-dom';
import { TableView } from '../../../src/components/table-view';
// 模拟依赖
// simulated dependency
vi.mock('ahooks', () => ({
useDebounceFn: fn => ({
run: fn,
@@ -125,7 +125,7 @@ vi.mock('../../../src/components/table-view/edit-menu', () => ({
) : null,
}));
// 模拟样式
// simulation style
vi.mock('../../../src/components/table-view/index.module.less', () => ({
default: {
'data-table-view': 'data-table-view-class',
@@ -196,7 +196,7 @@ describe('TableView 组件', () => {
test('当启用虚拟滚动时应渲染AutoSizer', () => {
render(<TableView {...defaultProps} isVirtualized={true} />);
// 由于我们模拟了AutoSizer我们可以检查UITable是否接收了正确的props
// Since we simulate AutoSizer, we can check if UITable receives the correct props.
const uiTable = screen.getByTestId('ui-table');
expect(uiTable).toBeInTheDocument();
});
@@ -204,23 +204,23 @@ describe('TableView 组件', () => {
test('当启用行选择时应传递rowSelection属性', () => {
render(<TableView {...defaultProps} rowSelect={true} />);
// 由于我们模拟了UITable我们无法直接检查rowSelection属性
// 但我们可以检查表格是否正确渲染
// Since we simulate UITable, we cannot directly check the rowSelection property
// But we can check if the table is rendered correctly.
expect(screen.getByTestId('ui-table')).toBeInTheDocument();
});
test('当启用列伸缩时应传递resizable属性', () => {
render(<TableView {...defaultProps} resizable={true} />);
// 由于我们模拟了UITable我们无法直接检查resizable属性
// 但我们可以检查表格是否正确渲染
// Since we simulate UITable, we cannot directly check the resizable property
// But we can check if the table is rendered correctly.
expect(screen.getByTestId('ui-table')).toBeInTheDocument();
});
test('当滚动到底部时应调用scrollToBottom回调', () => {
render(<TableView {...defaultProps} isVirtualized={true} />);
// 模拟滚动事件
// simulated rolling event
act(() => {
const onScrollProp = vi.fn();
onScrollProp({
@@ -231,25 +231,25 @@ describe('TableView 组件', () => {
});
});
// 由于我们模拟了useDebounceFnscrollToBottom会被立即调用
// 但由于我们无法直接触发onScroll回调这个测试实际上并不能验证scrollToBottom是否被调用
// 这里只是为了测试代码覆盖率
// Since we emulated useDebounceFn, scrollToBottom will be called immediately
// But since we can't directly trigger the onScroll callback, this test doesn't actually verify that scrollToBottom was called
// This is just to test code coverage
});
test('应该正确处理右键菜单', () => {
render(<TableView {...defaultProps} rowOperation={true} />);
// 模拟右键点击
// Simulated right click
const firstRow = screen.getByTestId('row-0');
const firstCell = firstRow.querySelector('td');
if (firstCell) {
// 模拟右键点击
// Simulated right click
fireEvent.contextMenu(firstCell);
// 检查菜单是否显示
// 注意由于我们无法直接触发onCell.onMouseDown这个测试实际上并不能验证菜单是否显示
// 这里只是为了测试代码覆盖率
// Check if the menu is displayed
// Note: Since we cannot directly trigger onCell.onMouseDown, this test does not actually verify that the menu is displayed
// This is just to test code coverage
}
});
@@ -258,14 +258,14 @@ describe('TableView 组件', () => {
<TableView {...defaultProps} rowSelect={true} />,
);
// 初始状态下工具栏不应显示
// The toolbar should not be displayed in the initial state
expect(screen.queryByTestId('edit-toolbar')).not.toBeInTheDocument();
// 模拟选择行
// 注意由于我们无法直接设置selected状态这个测试实际上并不能验证工具栏是否显示
// 这里只是为了测试代码覆盖率
// Simulate select row
// Note: Since we can't set the selected state directly, this test doesn't actually verify that the toolbar is displayed
// This is just to test code coverage
// 重新渲染组件
// Rerender component
rerender(<TableView {...defaultProps} rowSelect={true} />);
});
@@ -273,11 +273,11 @@ describe('TableView 组件', () => {
const ref = React.createRef();
render(<TableView {...defaultProps} ref={ref} />);
// 检查ref是否包含正确的方法
// Check if the ref contains the correct method
expect(ref.current).toHaveProperty('resetSelected');
expect(ref.current).toHaveProperty('getTableHeight');
// 调用ref方法
// Call the ref method
act(() => {
ref.current.resetSelected();
});
@@ -287,8 +287,8 @@ describe('TableView 组件', () => {
height = ref.current.getTableHeight();
});
// 验证getTableHeight返回正确的高度
// 行高56 * 3行 + 表头高41 = 209
// Verify that getTableHeight returns the correct height
// Line height 56 * 3 lines + header height 41 = 209
expect(height).toBe(209);
});
});

View File

@@ -19,7 +19,7 @@ import { CustomError } from '@coze-arch/bot-error';
import { colWidthCacheService } from '../../../src/components/table-view/service';
// 模拟 localStorage
// simulated localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
@@ -27,12 +27,12 @@ const localStorageMock = {
clear: vi.fn(),
};
// 模拟 window.localStorage
// Emulate window.localStorage
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});
// 模拟 CustomError
// Simulate CustomError
vi.mock('@coze-arch/bot-error', () => {
const mockCustomError = vi.fn().mockImplementation((event, message) => {
const error = new Error(message);
@@ -59,22 +59,22 @@ describe('ColWidthCacheService', () => {
describe('initWidthMap', () => {
test('当 localStorage 中不存在缓存时应该初始化一个空 Map', () => {
// 模拟 localStorage.getItem 返回 null
// Simulate localStorage.getItem returns null
localStorageMock.getItem.mockReturnValueOnce(null);
colWidthCacheService.initWidthMap();
// 验证 localStorage.setItem 被调用,并且参数是一个空 Map 的字符串表示
// Verify that localStorage.setItem is called and that the argument is a string representation of an empty Map
expect(localStorageMock.setItem).toHaveBeenCalledWith(mapName, '[]');
});
test('当 localStorage 中已存在缓存时不应该重新初始化', () => {
// 模拟 localStorage.getItem 返回一个已存在的缓存
// Simulate localStorage.getItem to return an existing cache
localStorageMock.getItem.mockReturnValueOnce('[["table1",{"col1":100}]]');
colWidthCacheService.initWidthMap();
// 验证 localStorage.setItem 没有被调用
// Verify that localStorage.setItem is not called
expect(localStorageMock.setItem).not.toHaveBeenCalled();
});
});
@@ -83,18 +83,18 @@ describe('ColWidthCacheService', () => {
test('当 tableKey 为空时不应该设置缓存', () => {
colWidthCacheService.setWidthMap({ col1: 100 }, undefined);
// 验证 localStorage.getItem localStorage.setItem 都没有被调用
// Verify that neither localStorage.getItem nor localStorage.setItem is called
expect(localStorageMock.getItem).not.toHaveBeenCalled();
expect(localStorageMock.setItem).not.toHaveBeenCalled();
});
test('当缓存中已存在相同 tableKey 时应该更新缓存', () => {
// 模拟 localStorage.getItem 返回一个已存在的缓存
// Simulate localStorage.getItem to return an existing cache
localStorageMock.getItem.mockReturnValueOnce('[["table1",{"col1":100}]]');
colWidthCacheService.setWidthMap({ col1: 200 }, 'table1');
// 验证 localStorage.setItem 被调用,并且参数是更新后的缓存
// Verify that localStorage.setItem is called and that the parameter is the updated cache
expect(localStorageMock.setItem).toHaveBeenCalledWith(
mapName,
'[["table1",{"col1":200}]]',
@@ -102,12 +102,12 @@ describe('ColWidthCacheService', () => {
});
test('当缓存中不存在相同 tableKey 且缓存未满时应该添加新缓存', () => {
// 模拟 localStorage.getItem 返回一个已存在的缓存
// Simulate localStorage.getItem to return an existing cache
localStorageMock.getItem.mockReturnValueOnce('[["table1",{"col1":100}]]');
colWidthCacheService.setWidthMap({ col1: 200 }, 'table2');
// 验证 localStorage.setItem 被调用,并且参数是添加新缓存后的结果
// Verify that localStorage.setItem is called and that the argument is the result of adding a new cache
expect(localStorageMock.setItem).toHaveBeenCalledWith(
mapName,
'[["table1",{"col1":100}],["table2",{"col1":200}]]',
@@ -115,37 +115,37 @@ describe('ColWidthCacheService', () => {
});
test('当缓存中不存在相同 tableKey 且缓存已满时应该移除最旧的缓存并添加新缓存', () => {
// 创建一个已满的缓存(容量为 20
// Create a full cache (capacity 20)
const fullCache = new Map();
for (let i = 0; i < colWidthCacheService.capacity; i++) {
fullCache.set(`table${i}`, { col1: 100 });
}
// 模拟 localStorage.getItem 返回已满的缓存
// Simulate localStorage.getItem returns a full cache
localStorageMock.getItem.mockReturnValueOnce(
JSON.stringify(Array.from(fullCache)),
);
colWidthCacheService.setWidthMap({ col1: 200 }, 'tableNew');
// 验证 localStorage.setItem 被调用
// Verify that localStorage.setItem is called
expect(localStorageMock.setItem).toHaveBeenCalled();
// 解析设置的新缓存
// New cache for parsing settings
const setItemCall = localStorageMock.setItem.mock.calls[0];
const newCacheStr = setItemCall[1];
const newCache = JSON.parse(newCacheStr);
// 验证新缓存的大小仍然是容量限制
// Verify that the size of the new cache is still the capacity limit
expect(newCache.length).toBe(colWidthCacheService.capacity);
// 验证最旧的缓存table0被移除
// Verify that the oldest cache (table0) is removed
const hasOldestCache = newCache.some(
([key]: [string, any]) => key === 'table0',
);
expect(hasOldestCache).toBe(false);
// 验证新缓存被添加
// Verify that the new cache is added
const hasNewCache = newCache.some(
([key]: [string, any]) => key === 'tableNew',
);
@@ -153,12 +153,12 @@ describe('ColWidthCacheService', () => {
});
test('当 localStorage 操作抛出异常时应该抛出 CustomError', () => {
// 模拟 localStorage.getItem 抛出异常
// Simulate localStorage.getItem throwing an exception
localStorageMock.getItem.mockImplementationOnce(() => {
throw new Error('localStorage error');
});
// 验证调用 setWidthMap 会抛出 CustomError
// Verify that calling setWidthMap throws a CustomError
expect(() =>
colWidthCacheService.setWidthMap({ col1: 100 }, 'table1'),
).toThrow(CustomError);
@@ -167,27 +167,27 @@ describe('ColWidthCacheService', () => {
describe('getTableWidthMap', () => {
test('当缓存中存在 tableKey 时应该返回对应的缓存并更新其位置', () => {
// 模拟 localStorage.getItem 返回一个已存在的缓存
// Simulate localStorage.getItem to return an existing cache
localStorageMock.getItem.mockReturnValueOnce(
'[["table1",{"col1":100}],["table2",{"col1":200}]]',
);
const result = colWidthCacheService.getTableWidthMap('table1');
// 验证返回正确的缓存
// Verify that the correct cache is returned
expect(result).toEqual({ col1: 100 });
// 注意:实际实现中并没有调用 localStorage.setItem所以移除这个期望
// 只验证返回的缓存数据是否正确
// Note: The actual implementation does not call localStorage.setItem, so remove this expectation
// Only verify that the cached data returned is correct
});
test('当 localStorage 操作抛出异常时应该抛出 CustomError', () => {
// 模拟 localStorage.getItem 抛出异常
// Simulate localStorage.getItem throwing an exception
localStorageMock.getItem.mockImplementationOnce(() => {
throw new Error('localStorage error');
});
// 验证调用 getTableWidthMap 会抛出 CustomError
// Verify that calling getTableWidthMap throws a CustomError
expect(() => colWidthCacheService.getTableWidthMap('table1')).toThrow(
CustomError,
);

View File

@@ -127,34 +127,34 @@ describe('utils', () => {
onDelete,
});
// 验证返回的配置对象包含正确的菜单项
// Verify that the returned configuration object contains the correct menu items
expect(result).toHaveProperty(EditMenuItem.EDIT);
expect(result).toHaveProperty(EditMenuItem.DELETE);
expect(result).toHaveProperty(EditMenuItem.DELETEALL);
// 验证编辑菜单项
// Verify edit menu item
expect(result[EditMenuItem.EDIT].text).toBe('knowledge_tableview_01');
expect(result[EditMenuItem.EDIT].icon).toBeDefined();
// 验证删除菜单项
// Verify deletion of menu items
expect(result[EditMenuItem.DELETE].text).toBe('knowledge_tableview_02');
expect(result[EditMenuItem.DELETE].icon).toBeDefined();
// 验证批量删除菜单项
// Verify bulk deletion of menu items
expect(result[EditMenuItem.DELETEALL].text).toBe(
'knowledge_tableview_02',
);
expect(result[EditMenuItem.DELETEALL].icon).toBeDefined();
// 测试点击编辑菜单项
// Test click edit menu item
result[EditMenuItem.EDIT].onClick();
expect(onEdit).toHaveBeenCalledWith(record, indexs[0]);
// 测试点击删除菜单项
// Test Click Delete Menu Item
result[EditMenuItem.DELETE].onClick();
expect(onDelete).toHaveBeenCalledWith(indexs);
// 测试点击批量删除菜单项
// Test click to delete menu items in bulk
result[EditMenuItem.DELETEALL].onClick();
expect(onDelete).toHaveBeenCalledWith(indexs);
});
@@ -167,18 +167,18 @@ describe('utils', () => {
selected: { record, indexs },
});
// 验证返回的配置对象包含正确的菜单项
// Verify that the returned configuration object contains the correct menu items
expect(result).toHaveProperty(EditMenuItem.EDIT);
expect(result).toHaveProperty(EditMenuItem.DELETE);
expect(result).toHaveProperty(EditMenuItem.DELETEALL);
// 测试点击编辑菜单项不应该抛出错误
// Tests that clicking on the edit menu item should not throw an error
expect(() => result[EditMenuItem.EDIT].onClick()).not.toThrow();
// 测试点击删除菜单项不应该抛出错误
// Test that clicking on the delete menu item should not throw an error
expect(() => result[EditMenuItem.DELETE].onClick()).not.toThrow();
// 测试点击批量删除菜单项不应该抛出错误
// Test clicking to delete menu items in bulk should not throw an error
expect(() => result[EditMenuItem.DELETEALL].onClick()).not.toThrow();
});
});

View File

@@ -26,12 +26,12 @@ export interface ActionsRenderProps {
index: number;
editProps?: {
disabled: boolean;
// 编辑回调
// edit callback
onEdit?: (record: TableViewRecord, index: number) => void;
};
deleteProps?: {
disabled: boolean;
// 删除回调
// Delete callback
onDelete?: (index: number) => void;
};
className?: string;

View File

@@ -25,20 +25,20 @@ import styles from './index.module.less';
export interface EditHeaderRenderProps {
value: string;
deleteProps?: {
// 禁用删除
// disable deletion
disabled: boolean;
// 删除回调
// Delete callback
onDelete?: (v: string) => void;
};
editProps?: {
// 编辑回调
// edit callback
onChange?: (v: string) => void;
// 失焦回调
// out of focus callback
onBlur?: (v: string) => void;
};
// 失焦回调
// out of focus callback
onBlur: (v: string) => void;
// 表头校验逻辑
// header check logic
validator: ValidatorProps;
editable?: boolean;
}
@@ -75,7 +75,7 @@ export const EditHeaderRender = ({
const isError = useMemo(() => validate && validate(value), [inputValue]);
return (
<div className={styles['edit-header-render']}>
{/* 编辑态组件 */}
{/* edit state component */}
{isEditCom && (
<UIInput
autoFocus
@@ -100,7 +100,7 @@ export const EditHeaderRender = ({
/>
)}
{/* 预览态组件 */}
{/* preview component */}
{!isEditCom && (
<div
className={styles['header-preview']}
@@ -110,7 +110,7 @@ export const EditHeaderRender = ({
</div>
)}
{/* 列删除按钮 */}
{/* column delete button */}
{editable && (
<UIButton
disabled={deleteDisabled}

View File

@@ -23,7 +23,7 @@ import styles from '../index.module.less';
import { useImagePreview } from './use-image-preview';
export interface ImageRenderProps {
srcList: string[];
// 图片是否可编辑,默认为false
// Whether the picture can be edited, the default is false
editable?: boolean;
onChange?: (tosKey: string, src: string) => void;
dataIndex?: string;
@@ -60,7 +60,7 @@ const ImageContainer = ({
}}
preview={false}
src={src}
// 失败时兜底图
// bottom line on failure
fallback={
<IconImageFailOutlined
className={styles['image-failed']}
@@ -70,7 +70,7 @@ const ImageContainer = ({
}}
/>
}
// 图片加载时的占位图,主要用于大图加载
// The placeholder map when the picture is loaded, mainly used for large image loading
placeholder={<div className="image-skeleton" onClick={onClick} />}
/>
))}

View File

@@ -74,7 +74,7 @@ export const useImagePreview = ({
return;
}
try {
// 业务
// business
const { name, fileInstance, url } = file;
setUploading(true);
if (fileInstance) {

View File

@@ -70,7 +70,7 @@ export const TextRender = ({
try {
await onBlur(inputValue, updateRecord, index);
} catch (e) {
// 更新失败,恢复原值
// Update failed, restore original value
console.log('update table content error', e);
setInputValue(String(value));
}
@@ -86,7 +86,7 @@ export const TextRender = ({
}
setInputValue(v);
};
// 校验状态
// check state
const isError = useMemo(
() => !!validate?.(String(inputValue), record, index),
[inputValue, validate],
@@ -113,7 +113,7 @@ export const TextRender = ({
className={`${styles['cell-text-render']} text-render-wrapper`}
data-testid={CommonE2e.CommonTableViewTextRender}
>
{/* 编辑态组件 */}
{/* edit state component */}
{isEditCom ? (
<span
className={`${styles['cell-text-edit']} ${
@@ -141,7 +141,7 @@ export const TextRender = ({
</span>
) : null}
{/* 预览态组件 */}
{/* preview component */}
{!isEditCom && (
<div
className={`${styles['cell-text-preview']} text-content`}

View File

@@ -42,7 +42,7 @@ export interface EditMenuProps {
};
onExit?: () => void | Promise<void>;
onDelete?: (indexs: (string | number)[]) => void | Promise<void>;
// 行操作编辑行的回调
// Line operations edit line callbacks
onEdit?: (
record: TableViewRecord,
index: string | number,

View File

@@ -9,9 +9,9 @@
.table-wrapper {
:global {
/** 公共样式 **/
/** Common Style **/
/** 重置table背景色 */
/** Reset table background color */
.semi-table-tbody>.semi-table-row,
.semi-table-thead>.semi-table-row>.semi-table-row-head,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left,
@@ -37,7 +37,7 @@
}
/** table header样式 **/
/** Table header style **/
.semi-table-thead {
// 拖拽列宽度的图标样式
&:hover {
@@ -85,13 +85,13 @@
}
}
/** table body部分样式 **/
/** Table body part style **/
.semi-table-tbody {
.semi-table-row {
>.semi-table-row-cell {
/**
* table 开启虚拟滚动后 单元格会加上 overflow: hidden
* 未开启虚拟滚动情况下正常展示溢出内容
* After table turns on virtual scrolling, the cell will be added with overflow: hidden.
* Display overflow content normally without virtual scrolling enabled
*/
overflow: visible;
border-top: 1px solid var(--coz-stroke-primary);

View File

@@ -52,40 +52,40 @@ import { EditMenu, EditToolBar } from './edit-menu';
import styles from './index.module.less';
export interface TableViewProps {
// 唯一标识表,且会作为列宽缓存map中的key值
// Uniquely identifies the table and is used as the key value in the column width cache map
tableKey?: string;
// 类名,用于样式覆盖
// Class name for style overrides
className?: string;
// 编辑配置
// Edit Configuration
editProps?: {
// 数据删除的回调,支持批量
// Callback for data deletion, batch support
onDelete?: (indexs: (string | number)[]) => void;
// 行操作编辑行的回调
// Line operations edit line callbacks
onEdit?: (record: TableViewRecord, index: string | number) => void;
};
// 滚动到底部的回调
// Scroll to the bottom of the callback
scrollToBottom?: () => void | Promise<void>;
// 拖拽钩子
// Drag hook
onResize?: (col: TableViewColumns) => void;
// 是否开启虚拟滚动,默认为false
// Whether to enable virtual scrolling, the default is false
isVirtualized?: boolean;
// 是否开启伸缩列,默认为false
// Whether to enable scaled columns, the default is false
resizable?: boolean;
// 是否开启行选择,默认为false
// Whether to enable line selection, the default is false
rowSelect?: boolean;
// 是否支持行操作,默认为false
// Whether line operations are supported, the default is false
rowOperation?: boolean;
// 数据
// data
dataSource: TableViewRecord[];
// 表头项
// header item
columns: TableViewColumns[];
// 数据为空的兜底展示
// The data is empty.
empty?: ReactNode;
// loading
loading?: boolean;
// 不消费仅用于触发渲染的state需优化
// No consumption, only used to trigger the rendered state, which needs to be optimized
resizeTriState?: number;
// 额外 tableProps
// Additional tableProps
tableProps?: TableProps;
}
export interface TableViewMethods {
@@ -204,7 +204,7 @@ export const TableView = forwardRef<TableViewMethods, TableViewProps>(
if (e.button === MOUSE_RIGHT_BTN && rowOperation) {
e.preventDefault();
const { offsetWidth, offsetHeight } = document.body;
// 如果右键位置非选中项,取消选中
// If the right-click position is not selected, uncheck it
if (
rowIndex &&
selected?.length &&
@@ -212,7 +212,7 @@ export const TableView = forwardRef<TableViewMethods, TableViewProps>(
) {
setSelected([]);
}
// 右键展示菜单
// right-click to display the menu
setFocusRow(rowIndex);
setMenuVisible(true);
setMenuStyle({
@@ -283,8 +283,8 @@ export const TableView = forwardRef<TableViewMethods, TableViewProps>(
scrollDirection === 'forward' &&
scrollOffset &&
/**
* 这一行一点余量都没留 可能在不同浏览器渲染下会有 bad case 导致无法满足条件
* 如果有遇到类似反馈可以优先排查这里
* This line has no margin at all, and there may be bad cases in different browsers that cannot meet the conditions.
* If you encounter similar feedback, you can give priority to checking here.
*/
scrollOffset + height - HEADER_SIZE >= tableData.length * ITEM_SIZE &&
!scrollUpdateWasRequested &&
@@ -330,7 +330,7 @@ export const TableView = forwardRef<TableViewMethods, TableViewProps>(
onResize: col =>
onResize ? onResize(col) : resizeFn(col),
onResizeStop: col => {
// resize完后缓存列宽
// Cache column width after resizing
const resizedCols = newColumns.map(oCol => {
if (oCol.dataIndex === col.dataIndex) {
return col;

View File

@@ -18,7 +18,7 @@ import { REPORT_EVENTS } from '@coze-arch/report-events';
import { CustomError } from '@coze-arch/bot-error';
/**
* 缓存列宽的方法类
* Method class with cache column width
*/
class ColWidthCacheService {
@@ -46,18 +46,18 @@ class ColWidthCacheService {
}
/**
* 初始化伸缩列缓存
* Initializing the scaled column cache
*/
initWidthMap() {
const widthMap = window.localStorage.getItem(this.mapName);
if (!widthMap) {
// 利用Map可记录键值对顺序的特性完成一个简易的LRU
// Completing a simple LRU using the characteristic that Map can record the order of key-value pairs
window.localStorage.setItem(this.mapName, this.mapToString(new Map()));
}
}
/**
* 设置列宽缓存,若超过缓存个数删除map中最近未使用的值
* Set the column width cache, if it exceeds the number of caches, delete the most recently unused value in the map
*/
setWidthMap(widthMap: Record<string, number>, tableKey?: string) {
if (!tableKey) {
@@ -68,11 +68,11 @@ class ColWidthCacheService {
window.localStorage.getItem(this.mapName) || '',
);
if (cacheWidthMap.has(tableKey)) {
// 存在即更新(删除后加入)
// Exist and update (join after deletion)
cacheWidthMap.delete(tableKey);
} else if (cacheWidthMap.size >= this.capacity) {
// 不存在即加入
// 缓存超过最大值,则移除最近没有使用的
// Join if you don't exist
// If the cache exceeds the maximum value, remove the recently unused
cacheWidthMap.delete(cacheWidthMap.keys().next().value);
}
cacheWidthMap.set(tableKey, widthMap);
@@ -89,7 +89,7 @@ class ColWidthCacheService {
}
/**
* 以表维度查询列宽缓存信息
* Query column width cache information in table dimension
* @param tableKey
*/
getTableWidthMap(tableKey: string) {
@@ -97,7 +97,7 @@ class ColWidthCacheService {
const cacheWidthMap = this.stringToMap(
window.localStorage.getItem(this.mapName) || '',
);
// 存在即更新
// exist and update
const temp = cacheWidthMap.get(tableKey);
cacheWidthMap.delete(tableKey);
cacheWidthMap.set(tableKey, temp);

View File

@@ -39,7 +39,7 @@ export interface GetRowOpConfig {
}
/**
* 表格列伸缩时的回调,用于限制伸缩边界
* Callbacks when table columns are scaled to limit the scaling boundaries
* @param column
* @returns
*/
@@ -64,7 +64,7 @@ export const getRowKey: RowKey<TableViewRecord> = (record?: TableViewRecord) =>
record?.tableViewKey || '';
/**
* 获取行操作配置
* Get row operation configuration
* @param record
* @param indexs
* @param onEdit

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
// 集中管理coze中使用的虚拟列表依赖
// Centralized management of virtual list dependencies used in coze
export {
FixedSizeList,