feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { OrderBy, WorkFlowListStatus } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import { WORKFLOW_LIST_STATUS_ALL } from '@/workflow-modal/type';
/** 流程所有者选项, 全部/我的 */
export const scopeOptions = [
{
label: I18n.t('workflow_list_scope_all'),
value: 'all',
},
{
label: I18n.t('workflow_list_scope_mine'),
value: 'me',
},
];
/** 流程状态选项, 全部/已发布/未发布 */
export const statusOptions = [
{
label: I18n.t('workflow_list_status_all'),
value: WORKFLOW_LIST_STATUS_ALL,
},
{
label: I18n.t('workflow_list_status_published'),
value: WorkFlowListStatus.HadPublished,
},
{
label: I18n.t('workflow_list_status_unpublished'),
value: WorkFlowListStatus.UnPublished,
},
];
/** 流程排序选项, 创建时间/更新时间 */
export const sortOptions = [
{
label: I18n.t('workflow_list_sort_create_time'),
value: OrderBy.CreateTime,
},
{
label: I18n.t('workflow_list_sort_edit_time'),
value: OrderBy.UpdateTime,
},
];

View File

@@ -0,0 +1,15 @@
/* stylelint-disable selector-class-pattern */
button.button {
min-width: 76px;
&.moreLevel {
color: var(--light-usage-primary-color-primary-disabled, #b4baf6);
background: var(--light-usage-bg-color-bg-0, #fff);
border: none;
}
&.mouseIn {
color: #fff;
background-color: rgba(var(--coze-red-5), 1) !important;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import classNames from 'classnames';
import { useBoolean } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { Button, type ButtonProps } from '@coze-arch/coze-design';
import { useI18nText } from '@/workflow-modal/hooks/use-i18n-text';
import styles from './index.module.less';
type WorkflowAddedButtonProps = ButtonProps;
export const WorkflowAddedButton: FC<
WorkflowAddedButtonProps
> = buttonProps => {
const [isMouseIn, { setFalse, setTrue }] = useBoolean(false);
const onMouseEnter = () => {
setTrue();
};
const onMouseLeave = () => {
setFalse();
};
const { i18nText, ModalI18nKey } = useI18nText();
return (
<Button
{...buttonProps}
color={isMouseIn ? 'red' : 'primary'}
className={classNames({
[styles.button]: true,
[styles.moreLevel]: true,
})}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
data-testid="workflow.modal.button.added"
>
{isMouseIn
? i18nText(ModalI18nKey.ListItemRemove)
: I18n.t('workflow_add_list_added')}
</Button>
);
};

View File

@@ -0,0 +1,46 @@
/* stylelint-disable selector-class-pattern */
.font-normal {
cursor: text;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
}
.container {
margin-left: 8px;
.button {
cursor: pointer;
width: 76px;
}
}
.not_publish_tooltip {
padding: 12px;
border-radius: 6px;
.content {
.font-normal();
line-height: 20px;
color: #fff;
}
}
.workflow_count_span {
display: inline-block;
width: 16px;
height: 16px;
margin-left: 6px;
font-size: 10px;
line-height: 17px;
color: #fff;
vertical-align: 1px;
background-color: rgba(77, 83, 232, 100%);
border-radius: 8px;
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, useContext, useMemo, useState } from 'react';
import classNames from 'classnames';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { LoadingButton } from '@coze-arch/coze-design';
import { Popconfirm, Tooltip } from '@coze-arch/bot-semi';
import { CheckType, type CheckResult } from '@coze-arch/bot-api/workflow_api';
import { type WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import WorkflowModalContext from '../../../workflow-modal-context';
import { isSelectProjectCategory } from '../../../utils';
import { type WorkflowInfo, WorkflowModalFrom } from '../../../type';
import { useI18nText } from '../../../hooks/use-i18n-text';
import { WorkflowAddedButton } from './added-button';
import styles from './index.module.less';
interface WorkflowBotButtonProps {
data?: WorkflowInfo;
isAdded?: boolean;
from?: WorkflowModalFrom;
loading?: boolean;
workflowNodes?: WorkflowNodeJSON[];
onAdd: () => Promise<boolean>;
onRemove: () => void;
className?: string;
style?: React.CSSProperties;
}
export const WorkflowBotButton: FC<WorkflowBotButtonProps> = ({
data,
style,
isAdded,
onAdd,
onRemove,
className,
from,
workflowNodes,
loading,
}) => {
const { plugin_id } = data || {};
const isPublished = plugin_id !== '0';
const isFromWorkflow =
from === WorkflowModalFrom.WorkflowAddNode ||
from === WorkflowModalFrom.ProjectWorkflowAddNode;
const context = useContext(WorkflowModalContext);
const isAddProjectWorkflow = isSelectProjectCategory(context?.modalState);
const canAdd = isPublished || isAddProjectWorkflow;
const isFromSocialScene = from === WorkflowModalFrom.SocialSceneHost;
const [count, setCount] = useState((workflowNodes || []).length);
const isFromWorkflowAgent = from === WorkflowModalFrom.WorkflowAgent;
const botAgentCheckResult = useMemo<CheckResult | undefined>(
() => data?.check_result?.find(check => check.type === CheckType.BotAgent),
[data],
);
const { i18nText, ModalI18nKey } = useI18nText();
const renderContent = () => {
if (isFromWorkflowAgent) {
if (botAgentCheckResult && !botAgentCheckResult.is_pass) {
return (
<Tooltip
position="top"
className={styles.not_publish_tooltip}
content={
<span className={styles.content}>
{botAgentCheckResult.reason}
</span>
}
>
<LoadingButton
disabled
color="primary"
className={styles.button}
data-testid="workflow.modal.add"
>
{I18n.t('workflow_add_list_add')}
</LoadingButton>
</Tooltip>
);
}
}
// 如果已添加,展示已添加按钮
if (isAdded) {
return (
<Popconfirm
title={i18nText(ModalI18nKey.ListItemRemoveConfirmTitle)}
content={i18nText(ModalI18nKey.ListItemRemoveConfirmDescription)}
okType="danger"
position="topRight"
onConfirm={onRemove}
zIndex={9999}
okText={I18n.t('neutral_age_gate_confirm', {}, 'Confirm')}
cancelText={I18n.t('workflow_240218_17', {}, 'Cancel')}
>
<div>
<WorkflowAddedButton />
</div>
</Popconfirm>
);
}
// 未添加,判断发布状态
// 未发布,展示下面的按钮
if (!canAdd) {
let key: I18nKeysNoOptionsType = 'workflow_add_not_allow_before_publish';
if (isFromWorkflow) {
key = 'wf_node_add_wf_modal_tip_must_publish_to_add';
} else if (isFromSocialScene) {
key = 'scene_workflow_popup_add_forbidden';
}
return (
<Tooltip
position="top"
className={styles.not_publish_tooltip}
content={<span className={styles.content}>{I18n.t(key)}</span>}
>
<LoadingButton
disabled
color="primary"
className={styles.button}
data-testid="workflow.modal.add"
>
{I18n.t('workflow_add_list_add')}
</LoadingButton>
</Tooltip>
);
}
// 已发布并且未添加,展示添加按钮
if (!isAdded) {
return (
<LoadingButton
onClick={async () => {
const isSuccess = await onAdd?.();
if (isSuccess) {
setCount(prev => prev + 1);
}
}}
color="primary"
className={styles.button}
data-testid="workflow.modal.add"
>
{I18n.t('workflow_add_list_add')}
{isFromWorkflow && count !== 0 ? (
<span className={styles.workflow_count_span}>{count}</span>
) : null}
</LoadingButton>
);
}
return null;
};
return (
<div
className={classNames(styles.container, className)}
style={style}
onClick={e => e.stopPropagation()}
>
{renderContent()}
</div>
);
};

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type MouseEvent } from 'react';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { IconButton, Popconfirm } from '@coze-arch/coze-design';
export const DeleteButton = ({
className,
onDelete,
}: {
className?: string;
onDelete?: () => Promise<void>;
}) => {
const [modalVisible, setModalVisible] = useState(false);
const handleClose = () => setModalVisible(false);
const showDeleteConfirm = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setModalVisible(true);
};
const handleDelete = () =>
// 使用 promise 让按钮出现 loading 的效果,参见
// https://semi.design/zh-CN/feedback/popconfirm
new Promise((resolve, reject) => {
onDelete?.()
.then(() => {
handleClose();
resolve(true);
})
.catch(error => {
// 处理错误
logger.error({
error: error as Error,
eventName: 'delete workflow error',
});
reject(error);
});
});
return (
<div className={className} onClick={e => e.stopPropagation()}>
<Popconfirm
visible={modalVisible}
title={I18n.t('scene_workflow_popup_delete_confirm_title')}
content={I18n.t('scene_workflow_popup_delete_confirm_subtitle')}
okText={I18n.t('shortcut_modal_confirm')}
cancelText={I18n.t('shortcut_modal_cancel')}
trigger="click"
position="bottomRight"
onConfirm={handleDelete}
onCancel={handleClose}
okButtonColor="red"
>
<IconButton
icon={<IconCozTrashCan />}
type="primary"
onClick={showDeleteConfirm}
/>
</Popconfirm>
</div>
);
};

View File

@@ -0,0 +1,201 @@
/* stylelint-disable max-nesting-depth */
.font-normal {
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
}
.container {
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 16px 14px;
border-top: 1px solid
var(--light-usage-border-color-border, rgba(28, 29, 35, 12%));
&:first-child {
border-top: 1px solid transparent;
}
&:hover {
background: var(--light-usage-fill-color-fill-0, rgba(46, 47, 56, 4%));
border-top: 1px solid transparent;
border-radius: 8px;
& + div {
border-top: 1px solid transparent;
}
}
.left {
margin-right: 16px;
.icon {
width: 36px;
height: 36px;
img {
width: 36px;
height: 36px;
}
}
}
.center {
display: flex;
flex: 1;
flex-direction: column;
width: 0;
.header {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
padding-bottom: 2px;
.title_wrapper {
cursor: pointer;
display: flex;
flex: 1;
align-items: center;
align-self: stretch;
width: 0;
.title {
.font-normal();
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--coz-fg-primary, #060709);
}
.status {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 8px;
.text {
margin-left: 4px;
color: var(--coz-fg-primary, #060709);
.font-normal();
}
}
}
}
.content {
cursor: pointer;
display: flex;
flex-direction: column;
width: 100%;
margin-top: 4px;
.desc {
.font-normal();
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary, #06070980);
}
}
.footer {
display: flex;
flex-direction: column;
width: 100%;
.creator {
cursor: pointer;
display: flex;
align-items: center;
border-radius: 3px;
.avatar {
display: flex;
width: 12px;
height: 12px;
& img {
width: 12px;
height: 12px;
}
}
.name {
.font-normal();
max-width: 106px;
margin-left: 4px;
color: var(--coz-fg-secondary, #06070980);
word-break: break-word;
}
}
.info {
cursor: pointer;
display: inline-flex;
align-items: center;
.creator {
background: unset;
&-avatar {
width: 12px;
height: 12px;
}
&-name {
max-width: 70px;
margin-left: 4px;
.font-normal();
color: var(--coz-fg-secondary, #06070980);
}
}
.symbol {
.font-normal();
margin: 0 8px;
line-height: 16px;
color: var(--coz-stroke-primary);
// color: var(--coz-fg-dim, #06070966);
}
.date {
.font-normal();
line-height: 16px;
color: var(--coz-fg-dim, #06070966);
}
}
}
}
.right {
display: flex;
flex-direction: column;
margin-left: 16px;
.buttons {
display: flex;
align-items: center;
align-self: stretch;
}
}
}

View File

@@ -0,0 +1,346 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import React, { type FC, useContext } from 'react';
import isNil from 'lodash-es/isNil';
import { unix } from 'dayjs';
import classNames from 'classnames';
import { OrderBy, WorkFlowListStatus } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import {
IconCozClockFill,
IconCozCheckMarkCircleFill,
} from '@coze-arch/coze-design/icons';
import { Typography, LoadingButton } from '@coze-arch/coze-design';
import { Avatar, Image, Tooltip } from '@coze-arch/bot-semi';
import { CheckType } from '@coze-arch/bot-api/workflow_api';
import { type Int64, SpaceType } from '@coze-arch/bot-api/developer_api';
import { LibButton } from '@/workflow-modal/content/card/lib-button';
import WorkflowModalContext from '../../workflow-modal-context';
import { isSelectProjectCategory } from '../../utils';
import {
DataSourceType,
MineActiveEnum,
type ProductInfo,
WorkflowCategory,
type WorkflowInfo,
WorkflowModalFrom,
} from '../../type';
import {
useWorkflowAction,
type WorkflowCardProps,
} from '../../hooks/use-workflow-action';
import { WorkflowParameters } from './parameters';
import { DeleteButton } from './delete-button';
import { WorkflowBotButton } from './bot-button';
import styles from './index.module.less';
const { Text } = Typography;
const formatTime = (time?: Int64) => unix(Number(time)).format('YYYY-MM-DD');
const defaultWorkFlowList = [];
export const WorkflowCard: FC<WorkflowCardProps> = props => {
const {
data,
workFlowList = defaultWorkFlowList,
from,
workflowNodes,
dupText,
itemShowDelete,
} = props;
const context = useContext(WorkflowModalContext);
const isProfessionalTemplate = (data as ProductInfo)?.meta_info
?.is_professional;
const {
dupWorkflowTpl,
addWorkflow,
removeWorkflow,
deleteWorkflow,
itemClick,
} = useWorkflowAction({ ...props, isProfessionalTemplate });
if (!context) {
return null;
}
const StatusMap = {
unpublished: {
label: I18n.t('workflow_add_status_unpublished'),
icon: <IconCozClockFill className="coz-fg-dim text-xs" />,
},
published: {
label: I18n.t('workflow_add_status_published'),
icon: (
<IconCozCheckMarkCircleFill className="text-xs coz-fg-hglt-green" />
),
},
};
const { orderBy, spaceType } = context;
const {
creator: creator,
status,
isSpaceWorkflow,
workflowCategory,
} = context.modalState;
const isTeam = spaceType === SpaceType.Team;
function isTypeWorkflow(
target: WorkflowInfo | ProductInfo,
): target is WorkflowInfo {
return context?.modalState.dataSourceType === DataSourceType.Workflow;
}
const pluginId = isTypeWorkflow(data) ? data.plugin_id : '';
const statusValue =
!isNil(pluginId) && isSpaceWorkflow
? StatusMap[pluginId === '0' ? 'unpublished' : 'published']
: undefined;
const renderStatusValue = () => {
// 添加项目里的工作流节点、官方示例不展示发布状态
if (
isSelectProjectCategory(context?.modalState) ||
workflowCategory === WorkflowCategory.Example
) {
return null;
}
if (statusValue) {
return (
<div className={classNames(styles.status)}>
{statusValue.icon}
<span className={styles.text}>{statusValue.label}</span>
</div>
);
}
return null;
};
const renderBottomLeftDesc = () => {
// 商品底部
if (!isTypeWorkflow(data)) {
const timeRender = `${I18n.t('workflow_add_list_updated')} ${formatTime(
data.meta_info.listed_at,
)}`;
return (
<div className={styles.info}>
<div className={styles.creator}>
<Avatar
className={styles['creator-avatar']}
src={data.meta_info.user_info?.avatar_url}
/>
<Text
ellipsis={{ showTooltip: true }}
className={styles['creator-name']}
>
{data.meta_info.user_info?.name ??
I18n.t('workflow_add_list_unknown')}
</Text>
<span className={styles.symbol}>|</span>
</div>
<span className={styles.date}>{timeRender}</span>
{(Number(data?.workflow_extra?.duplicate_count) || 0) > 0 ? (
<>
<span className={styles.symbol}>|</span>
<Text className={styles.date}>
{Number(data?.workflow_extra?.duplicate_count) || 0}{' '}
{I18n.t('workflowstore_card_duplicate')}
</Text>
</>
) : null}
</div>
);
}
// 用户创建的,展示修改时间
if (isSpaceWorkflow || workflowCategory === WorkflowCategory.Example) {
const showCreator =
(creator !== MineActiveEnum.Mine && isTeam) ||
from === WorkflowModalFrom.ProjectImportLibrary;
const timeRender =
orderBy === OrderBy.CreateTime
? `${I18n.t('workflow_add_list_created')} ${formatTime(
data.create_time,
)}`
: status === WorkFlowListStatus.HadPublished
? `${I18n.t('workflow_add_list_publised')} ${formatTime(
data.update_time,
)}`
: `${I18n.t('workflow_add_list_updated')} ${formatTime(
data.update_time,
)}`;
return (
<div className={styles.info}>
{showCreator ? (
<div className={styles.creator}>
<Avatar
className={styles['creator-avatar']}
src={data.creator?.avatar_url}
/>
<Text
ellipsis={{ showTooltip: true }}
className={styles['creator-name']}
>
{data.creator?.name ?? I18n.t('workflow_add_list_unknown')}
</Text>
<span className={styles.symbol}>|</span>
</div>
) : null}
<span className={styles.date}>{timeRender}</span>
</div>
);
}
// 官方模板,展示创作者
if (!isSpaceWorkflow) {
return (
<div className={styles.creator}>
<Image
preview={false}
src={data.template_author_picture_url}
className={styles.avatar}
/>
<Text ellipsis={{ showTooltip: true }} className={styles.name}>
{data.template_author_name || '-'}
</Text>
</div>
);
}
return null;
};
const renderBotButton = () => {
if (workflowCategory === WorkflowCategory.Example && isTypeWorkflow(data)) {
const botAgentCheckResult = data?.check_result?.find(
check => check.type === CheckType.BotAgent,
);
const ButtonContent = (
<LoadingButton
color="primary"
data-testid="workflow.modal.add"
disabled={botAgentCheckResult && !botAgentCheckResult?.is_pass}
onClick={async e => {
e.stopPropagation();
await dupWorkflowTpl();
}}
>
{dupText || I18n.t('workflowstore_duplicate_and_add')}
</LoadingButton>
);
if (
botAgentCheckResult &&
!botAgentCheckResult.is_pass &&
botAgentCheckResult.reason
) {
return (
<Tooltip content={botAgentCheckResult.reason}>
{ButtonContent}
</Tooltip>
);
}
return ButtonContent;
}
if (from === WorkflowModalFrom.ProjectImportLibrary) {
return (
<LibButton data={data as WorkflowInfo} onImport={props.onImport} />
);
}
return (
<>
<WorkflowBotButton
isAdded={workFlowList.some(
workflow =>
workflow.workflow_id === (data as WorkflowInfo)?.workflow_id,
)}
workflowNodes={workflowNodes}
from={from}
data={data as WorkflowInfo}
onAdd={() => addWorkflow()}
onRemove={() => {
removeWorkflow();
}}
/>
{itemShowDelete ? (
<DeleteButton className="ml-[4px]" onDelete={deleteWorkflow} />
) : null}
</>
);
};
return (
<div
className={styles.container}
onClick={() => {
itemClick();
}}
>
<div className={styles.left}>
<div className={styles.icon}>
<Image
preview={false}
src={isTypeWorkflow(data) ? data.url : data.meta_info.icon_url}
/>
</div>
</div>
<div className={styles.center}>
<div className={styles.header}>
<div className={styles.title_wrapper}>
<Text ellipsis={{ showTooltip: true }} className={styles.title}>
{isTypeWorkflow(data) ? data.name : data.meta_info.name}
</Text>
{renderStatusValue()}
</div>
</div>
<div className={styles.content}>
<Text
ellipsis={{
showTooltip: {
opts: {
style: {
maxWidth: 600,
wordBreak: 'break-word',
},
},
},
}}
className={styles.desc}
>
{(isTypeWorkflow(data) ? data.desc : data.meta_info.description) ||
''}
</Text>
</div>
<div className={styles.footer}>
<WorkflowParameters data={data} />
{renderBottomLeftDesc()}
</div>
</div>
<div className={styles.right}>
<div className={styles.buttons}>{renderBotButton()}</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { Button, Tooltip } from '@coze-arch/coze-design';
import { type WorkFlowModalModeProps, type WorkflowInfo } from '../../type';
export type LibButtonProps = Pick<WorkFlowModalModeProps, 'onImport'> & {
data?: WorkflowInfo;
};
export const LibButton: React.FC<LibButtonProps> = ({ data, onImport }) => {
const isPublished = data?.plugin_id && data?.plugin_id !== '0';
const content = (
<div onClick={e => e.stopPropagation()}>
<Button
disabled={!isPublished}
color="primary"
data-testid="workflow.modal.add"
onClick={event => {
event.stopPropagation();
data?.workflow_id &&
onImport?.({
workflow_id: data.workflow_id,
name: data.name || '',
});
}}
>
{I18n.t('project_resource_modal_copy_to_project')}
</Button>
</div>
);
if (isPublished) {
return content;
}
return (
<Tooltip
position="top"
content={I18n.t('project_toast_only_published_resources_can_be_imported')}
>
{content}
</Tooltip>
);
};

View File

@@ -0,0 +1,87 @@
/* stylelint-disable selector-class-pattern */
.font-normal {
/* Paragraph/small/EN-Regular */
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: rgba(28, 31, 35, 80%);
}
.container {
width: 100%;
margin: 4px 0 8px;
.wrapper {
cursor: pointer;
position: relative;
display: flex;
flex-direction: column;
.popover_help_block {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
}
.popover {
overflow: auto;
width: 260px;
max-height: 400px;
padding: 12px;
border-radius: 6px;
box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 10%), 0 0 1px 0 rgba(0, 0, 0, 30%);
.item {
display: flex;
flex-direction: column;
margin-top: 4px;
&:first-child {
margin-top: 0;
}
.header {
display: flex;
align-items: center;
.name {
.font-normal();
font-weight: 700;
color: #1c1f23;
word-break: break-word;
}
.type {
.font-normal();
margin-left: 12px;
}
.required {
.font-normal();
margin-left: 12px;
color: #fc8800;
}
}
.footer {
.font-normal();
width: 100%;
margin-top: 4px;
color: rgba(28, 31, 35, 60%);
word-break: break-word;
}
}
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, type FC } from 'react';
import classNames from 'classnames';
import {
PARAM_TYPE_LABEL_MAP,
STRING_ASSIST_TYPE_LABEL_MAP,
} from '@coze-workflow/base/types';
import { InputType, PluginParamTypeFormat } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import { Tag, Typography } from '@coze-arch/coze-design';
import { type OverflowListProps } from '@coze-arch/bot-semi/OverflowList';
import { Popover, OverflowList } from '@coze-arch/bot-semi';
import WorkflowModalContext from '../../../workflow-modal-context';
import {
DataSourceType,
type ProductInfo,
type WorkflowInfo,
} from '../../../type';
import { type WorkflowCardProps } from '../../../hooks/use-workflow-action';
import styles from './index.module.less';
const { Paragraph, Text } = Typography;
const getInputType = ({ type, format, assist_type }) => {
let inputType = '';
if (type) {
if (
type === InputType.String &&
format === PluginParamTypeFormat.ImageUrl
) {
inputType = 'Image';
} else if (type === InputType.String && assist_type) {
inputType = STRING_ASSIST_TYPE_LABEL_MAP[assist_type];
} else {
inputType = PARAM_TYPE_LABEL_MAP[type as InputType];
}
}
return inputType;
};
export interface WorkflowParameterItem {
name?: string;
desc?: string;
required?: boolean;
type?: string;
}
type WorkflowParametersProps = Pick<WorkflowCardProps, 'data'> & {
className?: string;
style?: React.CSSProperties;
};
interface CustomParameterPopoverProps {
items: WorkflowParameterItem[];
children: React.ReactNode;
}
const CustomParameterPopover: FC<CustomParameterPopoverProps> = ({
children,
items,
}) => (
<Popover
stopPropagation
position="top"
spacing={0}
content={
<div className={styles.popover}>
{items.map((item, index) => (
<div key={`item${index}`} className={styles.item}>
<div className={styles.header}>
<Text
ellipsis={{
showTooltip: {
opts: {
content: item.name || '',
position: 'top',
style: {
wordBreak: 'break-word',
},
},
},
}}
>
<span className={styles.name}>{item.name || '-'}</span>
</Text>
<span className={styles.type}>{item.type || '-'}</span>
{Boolean(item.required) && (
<span className={styles.required}>
{I18n.t('workflow_add_parameter_required')}
</span>
)}
</div>
<div className={styles.footer}>
<Paragraph
ellipsis={{
rows: 2,
showTooltip: {
opts: {
content: item.desc || '',
position: 'top',
style: {
wordBreak: 'break-word',
},
},
},
}}
>
<span className={styles.footer}>{item.desc || '-'}</span>
</Paragraph>
</div>
</div>
))}
</div>
}
>
{children}
</Popover>
);
export const WorkflowParameters: FC<WorkflowParametersProps> = ({
data,
style,
className,
}) => {
const context = useContext(WorkflowModalContext);
function isTypeWorkflow(
target: WorkflowInfo | ProductInfo,
): target is WorkflowInfo {
return context?.modalState.dataSourceType === DataSourceType.Workflow;
}
const getParameters = (): Array<WorkflowParameterItem> => {
// 这么拆分虽然有点冗余, 但是可以正确进行类型推导
if (isTypeWorkflow(data)) {
return (
data.start_node?.node_param?.input_parameters?.map(item => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inputType = getInputType(item as any);
return {
name: item.name,
desc: item.desc,
required: item.required,
type: inputType,
};
}) || []
);
}
return (
data?.workflow_extra?.start_node?.node_param?.input_parameters?.map(
item => {
const inputType = getInputType({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(item as any),
type: item.input_type,
});
return {
name: item.name,
desc: item.desc,
required: item.is_required,
type: inputType,
};
},
) || []
);
};
const items = getParameters();
const overflowRenderer: OverflowListProps['overflowRenderer'] = (
overflowItems: Array<WorkflowParameterItem>,
) => {
const slicedItems = overflowItems.slice(overflowItems.length * -1);
return slicedItems.length ? (
<CustomParameterPopover items={items}>
<div>
<Tag style={{ flex: '0 0 auto' }} size="mini" color="primary">
+{slicedItems.length}
</Tag>
</div>
</CustomParameterPopover>
) : null;
};
const visibleItemRenderer: OverflowListProps['visibleItemRenderer'] = (
item: WorkflowParameterItem,
) => (
<CustomParameterPopover items={items}>
<div style={{ marginRight: 8 }}>
<Tag size="mini" color="primary">
{item.name}
</Tag>
</div>
</CustomParameterPopover>
);
return (
<div className={classNames(styles.container, className)} style={style}>
<div className={styles.wrapper}>
<OverflowList
items={items}
overflowRenderer={overflowRenderer}
visibleItemRenderer={visibleItemRenderer}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,37 @@
/* stylelint-disable selector-class-pattern */
.container {
width: 100%;
height: 100%;
padding: 0 24px;
}
.scroll_load_more {
padding: 8px;
&.empty {
height: 0;
padding: 0;
}
}
.workflow-content {
overflow: auto;
width: 100%;
height: 100%;
padding-bottom: 12px;
.loading-more {
text-align: center;
}
}
.spin {
width: 100%;
height: 100%;
:global {
.semi-spin-children {
height: 100%;
}
}
}

View File

@@ -0,0 +1,404 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
/* eslint-disable @coze-arch/max-line-per-function */
import { type FC, useContext, useEffect, useMemo, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import groupBy from 'lodash-es/groupBy';
import { useInViewport, useUpdateEffect } from 'ahooks';
import { StandardNodeType } from '@coze-workflow/base/types';
import { useWorkflowStore } from '@coze-workflow/base/store';
import {
WorkflowMode,
WorkFlowType,
Tag,
BindBizType,
} from '@coze-workflow/base/api';
import { isGeneralWorkflow, workflowApi } from '@coze-workflow/base';
import { SearchNoResult } from '@coze-studio/components/search-no-result';
import { I18n } from '@coze-arch/i18n';
import { IconCozLoading } from '@coze-arch/coze-design/icons';
import { Spin } from '@coze-arch/coze-design';
import { UICompositionModalMain, UIEmpty } from '@coze-arch/bot-semi';
import {
ProductEntityType,
ProductListSource,
} from '@coze-arch/bot-api/product_api';
import { useWorkflowList } from '@/hooks/use-workflow-list';
import WorkflowModalContext from '../workflow-modal-context';
import { isSelectProjectCategory } from '../utils';
import {
DataSourceType,
MineActiveEnum,
type ProductInfo,
WORKFLOW_LIST_STATUS_ALL,
WorkflowCategory,
type WorkflowInfo,
WorkflowModalFrom,
type WorkFlowModalModeProps,
type WorkflowModalState,
} from '../type';
import { useWorkflowProductList } from '../hooks/use-workflow-product-list';
import { useI18nText } from '../hooks/use-i18n-text';
import { WorkflowCard } from './card';
import s from './index.module.less';
// eslint-disable-next-line max-lines-per-function
const WorkflowModalContent: FC<WorkFlowModalModeProps> = props => {
const { excludedWorkflowIds, from, projectId } = props;
const context = useContext(WorkflowModalContext);
const { i18nText, ModalI18nKey } = useI18nText();
const {
updatePageParam: updateWorkflowPageParam,
isFetching,
workflowList,
fetchNextPage,
loadingStatus,
refetch,
hasNextPage,
handleDelete,
} = useWorkflowList({
pageSize: 10,
enabled: context?.modalState.dataSourceType === DataSourceType.Workflow,
from,
fetchWorkflowListApi:
context?.modalState?.workflowCategory !== WorkflowCategory.Example
? workflowApi.GetWorkFlowList.bind(workflowApi)
: workflowApi.GetExampleWorkFlowList.bind(workflowApi),
});
const {
workflowProductList,
updatePageParam: updateProductPageParam,
fetchNextPage: fetchNextProductPage,
isFetching: productIsFetching,
loadingStatus: productLoadingStatus,
hasNextPage: productHasNextPage,
copyProduct,
} = useWorkflowProductList({
pageSize: 10,
enabled: context?.modalState.dataSourceType === DataSourceType.Product,
});
// 转换筛选参数
useEffect(() => {
if (!context) {
return;
}
const { modalState, flowMode } = context;
if (modalState.dataSourceType === DataSourceType.Workflow) {
const isAddProjectWorkflow = isSelectProjectCategory(modalState);
let targetTags;
if (!modalState.isSpaceWorkflow) {
if (modalState.query) {
targetTags = 1;
} else {
targetTags = modalState.workflowTag;
}
}
let type: WorkFlowType;
if (modalState.workflowCategory === WorkflowCategory.Example) {
targetTags = Tag.All;
type = WorkFlowType.GuanFang;
} else {
type = modalState.isSpaceWorkflow
? WorkFlowType.User
: WorkFlowType.GuanFang;
}
let status: WorkflowModalState['status'] | undefined = undefined;
if (modalState.isSpaceWorkflow) {
status =
// isAddProjectWorkflow项目里添加子工作流没有发布状态概念筛选状态传 undefined
modalState.status === WORKFLOW_LIST_STATUS_ALL || isAddProjectWorkflow
? undefined
: modalState.status;
}
updateWorkflowPageParam({
space_id: context.spaceId,
flow_mode: modalState.listFlowMode,
name: modalState.query,
order_by: modalState.isSpaceWorkflow ? context.orderBy : undefined,
status,
type,
project_id: isSelectProjectCategory(modalState) ? projectId : undefined,
login_user_create: modalState.isSpaceWorkflow
? modalState.creator === MineActiveEnum.Mine
: undefined,
tags: targetTags,
bind_biz_type: context.bindBizType,
bind_biz_id: context.bindBizId,
});
} else {
if (modalState.productCategory === 'recommend') {
updateProductPageParam({
keyword: modalState.query,
sort_type: modalState.sortType,
category_id: undefined,
source: ProductListSource.Recommend,
entity_type: isGeneralWorkflow(flowMode)
? ProductEntityType.WorkflowTemplateV2
: ProductEntityType.ImageflowTemplateV2,
});
} else if (modalState.productCategory === 'all') {
updateProductPageParam({
keyword: modalState.query,
sort_type: modalState.sortType,
category_id: undefined,
source: undefined,
entity_type: isGeneralWorkflow(flowMode)
? ProductEntityType.WorkflowTemplateV2
: ProductEntityType.ImageflowTemplateV2,
});
} else {
updateProductPageParam({
keyword: modalState.query,
sort_type: modalState.sortType,
category_id: modalState.productCategory,
source: undefined,
entity_type: isGeneralWorkflow(flowMode)
? ProductEntityType.WorkflowTemplateV2
: ProductEntityType.ImageflowTemplateV2,
});
}
}
}, [context]);
const { nodes } = useWorkflowStore(
useShallow(state => ({
nodes: state.nodes,
})),
);
// 子流程节点 map例如 { 'workflowId': [node1, node2, ...] }
const workflowNodesMap = useMemo(() => {
const subFlowNodes = nodes.filter(
v => v.type === StandardNodeType.SubWorkflow,
);
const groups = groupBy(
subFlowNodes,
item => item?.data?.inputs?.workflowId,
);
return groups;
}, [nodes]);
const targetWorkflowList = useMemo(() => {
if (!excludedWorkflowIds) {
return workflowList;
}
return workflowList.filter(
v => !excludedWorkflowIds.includes(v.workflow_id || ''),
);
}, [excludedWorkflowIds, workflowList]);
/** scroll的container */
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
/** 监听触底的observer */
const intersectionObserverDom = useRef<HTMLDivElement>(null);
// 是否触底
const [inViewPort] = useInViewport(intersectionObserverDom, {
root: () => scrollContainerRef.current,
threshold: 0.8,
});
// 首次effect不执行这个是切换状态的effect
useUpdateEffect(() => {
// 当筛选项改变时,回到顶部
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
top: 0,
});
}
// 只要是query中非page改变就执行此effect
}, [context?.modalState]);
// 获取下一页逻辑
useEffect(() => {
if (!inViewPort) {
return;
}
if (dataSourceType === DataSourceType.Workflow) {
if (loadingStatus !== 'success' || isFetching || !hasNextPage) {
return;
}
fetchNextPage();
} else {
if (
productLoadingStatus !== 'success' ||
productIsFetching ||
!productHasNextPage
) {
return;
}
fetchNextProductPage();
}
}, [
inViewPort,
loadingStatus,
isFetching,
hasNextPage,
productLoadingStatus,
productIsFetching,
productHasNextPage,
]);
useEffect(() => {
if (!context?.modalState.isSpaceWorkflow) {
return;
}
const visibilityChangeHandler = () => {
const needRefresh = document.visibilityState === 'visible';
if (needRefresh) {
refetch();
}
};
document.addEventListener('visibilitychange', visibilityChangeHandler);
return () => {
document.removeEventListener('visibilitychange', visibilityChangeHandler);
};
}, [context?.modalState.isSpaceWorkflow, refetch]);
if (!context) {
return null;
}
function isTypeWorkflow(
_target: WorkflowInfo | ProductInfo,
): _target is WorkflowInfo {
return context?.modalState.dataSourceType === DataSourceType.Workflow;
}
const { modalState, flowMode } = context;
const { dataSourceType } = context.modalState;
const targetLoadingStatus =
dataSourceType === DataSourceType.Workflow
? loadingStatus
: productLoadingStatus;
const targetHasNextPage =
dataSourceType === DataSourceType.Workflow
? hasNextPage
: productHasNextPage;
const targetList =
dataSourceType === DataSourceType.Workflow
? targetWorkflowList
: workflowProductList;
const isAgentWorkflow = from === WorkflowModalFrom.WorkflowAgent;
const renderEmpty = () => {
const isNotFound = Boolean(modalState.query);
if (flowMode === WorkflowMode.SceneFlow) {
return (
<SearchNoResult
title={i18nText(ModalI18nKey.CreatedListEmptyTitle)}
type={'social-scene-flow'}
isNotFound={isNotFound}
notFound={isNotFound ? i18nText(ModalI18nKey.ListEmptyTitle) : ''}
/>
);
} else {
return (
<UIEmpty
isNotFound={isNotFound}
notFound={{
title: i18nText(ModalI18nKey.ListEmptyTitle),
}}
empty={{
title: i18nText(ModalI18nKey.CreatedListEmptyTitle),
description: i18nText(ModalI18nKey.CreatedListEmptyDescription),
}}
/>
);
}
};
return (
<UICompositionModalMain>
<Spin
spinning={targetLoadingStatus === 'pending'}
wrapperClassName={s.spin}
style={{ height: '100%', width: '100%' }}
>
{/* Workflow as agent 支持添加带自定义入参的对话流 */}
{/* {isAgentWorkflow ? (
<div className="coz-mg-hglt px-[36px] py-[8px] mx-[24px] my-[0] rounded-[8px]">
{I18n.t('wf_chatflow_133')}
</div>
) : null} */}
<div
className={`${s['workflow-content']} new-workflow-modal-content`}
ref={scrollContainerRef}
>
{/* 内容渲染 */}
{targetLoadingStatus !== 'pending' && targetList.length > 0 && (
<UICompositionModalMain.Content
style={{
minHeight: '100%',
paddingBottom: isAgentWorkflow ? '60px' : 0,
}}
>
{/* 数据呈现样式, 列表样式/卡片样式. 展示图像流商品列表时使用卡片样式 */}
<>
{targetList.map((item: WorkflowInfo | ProductInfo) => (
<WorkflowCard
key={
isTypeWorkflow(item)
? item.workflow_id
: item.meta_info.entity_id
}
data={item}
itemShowDelete={
context?.bindBizType === BindBizType.DouYinBot
}
workflowNodes={
isTypeWorkflow(item)
? (workflowNodesMap[item.workflow_id || ''] ?? [])
: []
}
handleDeleteWorkflow={handleDelete}
copyProductHandle={copyProduct}
{...props}
/>
))}
</>
{targetHasNextPage ? (
<div ref={intersectionObserverDom}>
<div className={s['loading-more']}>
<IconCozLoading className="animate-spin coz-fg-dim mr-[4px]" />
<div>{I18n.t('Loading')}</div>
</div>
</div>
) : null}
</UICompositionModalMain.Content>
)}
{targetLoadingStatus === 'success' &&
targetList.length === 0 &&
renderEmpty()}
</div>
</Spin>
</UICompositionModalMain>
);
};
export { WorkflowModalContent };

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useContext } from 'react';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { Banner, Select } from '@coze-arch/coze-design';
import WorkflowModalContext from '../workflow-modal-context';
import {
WORKFLOW_LIST_STATUS_ALL,
type WorkFlowModalModeProps,
type WorkflowModalState,
} from '../type';
import { CreateWorkflowBtn } from '../sider/create-workflow-btn';
import styles from '../index.module.less';
import { useWorkflowSearch } from '../hooks/use-workflow-search';
import {
ModalI18nKey,
WORKFLOW_MODAL_I18N_KEY_MAP,
} from '../hooks/use-i18n-text';
import { statusOptions } from '../constants';
const getStatusOptions = (showAll?: boolean) =>
showAll
? statusOptions
: statusOptions.filter(item => item.value !== WORKFLOW_LIST_STATUS_ALL);
const WorkflowModalFilterForDouyin: FC<WorkFlowModalModeProps> = props => {
const context = useContext(WorkflowModalContext);
const searchNode = useWorkflowSearch();
if (!context) {
return null;
}
const { updateModalState, flowMode } = context;
const { status } = context.modalState;
const { filterOptionShowAll = false } = props;
const title = I18n.t(
WORKFLOW_MODAL_I18N_KEY_MAP[flowMode]?.[
ModalI18nKey.Title
] as I18nKeysNoOptionsType,
);
return (
<div className="w-full">
<div className="flex items-center w-full justify-between mt-[-4px]">
<div className="flex items-center gap-[24px]">
<div className={styles.titleForAvatar}>{title}</div>
<Select
insetLabel={I18n.t('publish_list_header_status')}
showClear={false}
value={status}
optionList={getStatusOptions(filterOptionShowAll)}
onChange={value => {
updateModalState({
status: value as WorkflowModalState['status'],
});
}}
/>
</div>
<div className="flex items-center gap-[12px] mr-[12px]">
<div className="w-[208px]">{searchNode}</div>
<div className="flex items-center">
<CreateWorkflowBtn
onCreateSuccess={props.onCreateSuccess}
nameValidators={props.nameValidators}
/>
</div>
</div>
</div>
<Banner
type="info"
className="mt-[16px] pt-[7px] pb-[7px] rounded-lg"
description={I18n.t('dy_avatar_add_workflow_limit')}
closeIcon={null}
/>
</div>
);
};
export { WorkflowModalFilterForDouyin };

View File

@@ -0,0 +1,32 @@
/* stylelint-disable selector-class-pattern */
.header {
display: flex;
gap: 12px;
align-items: center;
}
.workflow-status-radio {
:global(.semi-radio-buttonRadioGroup) {
padding: 0;
}
:global(.semi-radio-buttonRadioGroup:first-child .semi-radio-addon-buttonRadio) {
border-radius: 0;
border-right: 1px solid var(--semi-color-fill-2);
}
:global(.semi-radio-addon-buttonRadio) {
font-size: 14px;
padding: 0 16px;
color: rgb(28 31 35 / 40%);
}
:global(.semi-radio-addon-buttonRadio-hover) {
background-color: transparent;
}
:global(.semi-radio-addon-buttonRadio-checked) {
background-color: transparent;
color: var(--light-usage-text-color-text-0, #1c1d23)
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useContext, useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { UISelect } from '@coze-arch/bot-semi';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { SpaceType } from '@coze-arch/bot-api/playground_api';
import WorkflowModalContext from '../workflow-modal-context';
import { isSelectProjectCategory } from '../utils';
import {
DataSourceType,
MineActiveEnum,
WORKFLOW_LIST_STATUS_ALL,
type WorkFlowModalModeProps,
type WorkflowModalState,
WorkflowCategory,
} from '../type';
import { CreateWorkflowBtn } from '../sider/create-workflow-btn';
import { useI18nText } from '../hooks/use-i18n-text';
import { statusOptions } from '../constants';
import { SortTypeSelect } from './sort-type-select';
import styles from './index.module.less';
const getStatusOptions = (showAll?: boolean) =>
showAll
? statusOptions
: statusOptions.filter(item => item.value !== WORKFLOW_LIST_STATUS_ALL);
const flowModeOptions = [
{
label: I18n.t('filter_all'),
value: WorkflowMode.All,
},
{
label: I18n.t('library_resource_type_workflow'),
value: WorkflowMode.Workflow,
},
{
label: I18n.t('wf_chatflow_76'),
value: WorkflowMode.ChatFlow,
},
].filter(item => {
// 社区版本暂不支持对话流
if (item.value === WorkflowMode.ChatFlow && IS_OPEN_SOURCE) {
return false;
}
return true;
});
const WorkflowModalFilter: FC<WorkFlowModalModeProps> = props => {
const context = useContext(WorkflowModalContext);
const { i18nText, ModalI18nKey } = useI18nText();
const scopeOptions = useMemo(() => {
if (!context) {
return [];
}
return [
{
label: i18nText(ModalI18nKey.TabAll),
value: MineActiveEnum.All,
},
{
label: i18nText(ModalI18nKey.TabMine),
value: MineActiveEnum.Mine,
},
];
}, []);
if (!context) {
return null;
}
const { spaceType, updateModalState, modalState } = context;
const {
dataSourceType,
isSpaceWorkflow,
status,
creator,
listFlowMode,
workflowCategory,
} = context.modalState;
const {
hideSider,
hiddenCreate,
filterOptionShowAll = false,
hideCreatorSelect = false,
hiddenListFlowModeFilter = false,
} = props;
const isExampleWorkflow = workflowCategory === WorkflowCategory.Example;
const isAddProjectWorkflow = isSelectProjectCategory(modalState);
return (
<div
className={`${styles.header} ${
hideSider ? 'w-full justify-between' : ''
}`}
>
{(isSpaceWorkflow || isExampleWorkflow) &&
dataSourceType === DataSourceType.Workflow ? (
<>
{!hiddenListFlowModeFilter ? (
<UISelect
label={I18n.t('Type')}
showClear={false}
value={listFlowMode}
optionList={flowModeOptions}
onChange={value => {
updateModalState({
listFlowMode: value as WorkflowMode,
});
}}
/>
) : null}
{isAddProjectWorkflow || isExampleWorkflow ? null : (
<UISelect
label={I18n.t('publish_list_header_status')}
showClear={false}
value={status}
optionList={getStatusOptions(filterOptionShowAll)}
onChange={value => {
updateModalState({
status: value as WorkflowModalState['status'],
});
}}
/>
)}
{spaceType === SpaceType.Team &&
!hideCreatorSelect &&
!isExampleWorkflow && (
<UISelect
label={I18n.t('Creator')}
showClear={false}
value={creator}
onChange={value => {
updateModalState({ creator: value as MineActiveEnum });
}}
optionList={scopeOptions}
/>
)}
</>
) : null}
{hideSider ? (
<div className="flex items-center mr-[-24px]">
{!hiddenCreate && (
<CreateWorkflowBtn
className="ml-12px"
onCreateSuccess={props.onCreateSuccess}
nameValidators={props.nameValidators}
/>
)}
</div>
) : null}
{dataSourceType === DataSourceType.Product ? <SortTypeSelect /> : null}
</div>
);
};
export { WorkflowModalFilter };

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { SortType } from '@coze-arch/idl/product_api';
import { I18n } from '@coze-arch/i18n';
import { UISelect } from '@coze-arch/bot-semi';
import WorkflowModalContext, {
type WorkflowModalContextValue,
} from '../workflow-modal-context';
const defaultDataSource = [
{
label: I18n.t('Popular', {}, '最受欢迎'),
value: SortType.Heat,
},
{
label: I18n.t('mkpl_published', {}, '最近发布'),
value: SortType.Newest,
},
];
const queryDataSource = [
{
label: I18n.t('store_search_rank_default', {}, '相关性'),
value: SortType.Relative,
},
].concat(defaultDataSource);
export const SortTypeSelect = () => {
const context = useContext(WorkflowModalContext);
const { updateModalState } = context as WorkflowModalContextValue;
const { query, sortType } = context?.modalState || {};
const handleOnChange = value => {
updateModalState({ sortType: value as SortType });
};
return (
<UISelect
label={I18n.t('Sort')}
value={sortType}
optionList={query ? queryDataSource : defaultDataSource}
onChange={handleOnChange}
/>
);
};

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext, type ReactNode } from 'react';
import { WorkflowMode } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import WorkflowModalContext from '../workflow-modal-context';
export enum ModalI18nKey {
Title = 'title',
NavigationMy = 'navigation_my',
NavigationTeam = 'navigation_team',
NavigationExplore = 'navigation_explore',
TabAll = 'tab_all',
TabMine = 'tab_mine',
ListEmptyTitle = 'list_empty_title',
CreatedListEmptyTitle = 'created_list_empty_title',
CreatedListEmptyDescription = 'created_list_empty_description',
NavigationCreate = 'navigation_create',
ListError = 'list_error',
ListItemRemove = 'list_item_remove',
ListItemRemoveConfirmTitle = 'list_item_remove_confirm_title',
ListItemRemoveConfirmDescription = 'list_item_remove_confirm_description',
}
/**
* i18n 文案有变量时使用这个结构
*/
interface I18nKeyWithOptions {
/* i18n 文案的 key */
key: string;
/* 变量参数对象 */
options?: Record<string, ReactNode>;
}
export type I18nKey = string | I18nKeyWithOptions;
// 用于存放 workflow 和 imageflow 的各个 i18n 文案的 key
export const WORKFLOW_MODAL_I18N_KEY_MAP: {
[WorkflowMode.Workflow]: Record<ModalI18nKey, I18nKey>;
[WorkflowMode.Imageflow]: Record<ModalI18nKey, I18nKey>;
[WorkflowMode.SceneFlow]: Record<ModalI18nKey, I18nKey>;
} = {
[WorkflowMode.Workflow]: {
[ModalI18nKey.Title]: 'workflow_add_title',
[ModalI18nKey.NavigationMy]: 'workflow_add_navigation_my',
[ModalI18nKey.NavigationTeam]: 'workflow_add_navigation_team',
[ModalI18nKey.NavigationExplore]: 'workflow_add_navigation_explore',
[ModalI18nKey.TabAll]: 'workflow_add_created_tab_all',
[ModalI18nKey.TabMine]: 'workflow_add_created_tab_mine',
[ModalI18nKey.ListEmptyTitle]: 'workflow_add_list_empty_title',
[ModalI18nKey.CreatedListEmptyTitle]:
'workflow_add_created_list_empty_title',
[ModalI18nKey.CreatedListEmptyDescription]:
'workflow_add_created_list_empty_description',
[ModalI18nKey.NavigationCreate]: 'workflow_add_create_library',
[ModalI18nKey.ListError]: 'workflow_add_list_added_id_empty',
[ModalI18nKey.ListItemRemove]: 'workflow_add_list_remove',
[ModalI18nKey.ListItemRemoveConfirmTitle]:
'workflow_add_remove_confirm_title',
[ModalI18nKey.ListItemRemoveConfirmDescription]:
'workflow_add_remove_confirm_content',
},
[WorkflowMode.Imageflow]: {
[ModalI18nKey.Title]: 'imageflow_add',
[ModalI18nKey.NavigationMy]: 'workflow_add_navigation_my',
[ModalI18nKey.NavigationTeam]: 'imageflow_workspace2',
[ModalI18nKey.NavigationExplore]: 'imageflow_explore',
[ModalI18nKey.TabAll]: 'workflow_add_created_tab_all',
[ModalI18nKey.TabMine]: 'workflow_add_created_tab_mine',
[ModalI18nKey.ListEmptyTitle]: 'imageflow_detail_no_search_result',
[ModalI18nKey.CreatedListEmptyTitle]: 'imageflow_title',
[ModalI18nKey.CreatedListEmptyDescription]: 'imageflow_title_description',
[ModalI18nKey.NavigationCreate]: 'imageflow_create',
[ModalI18nKey.ListError]: 'imageflow_add_toast_error',
[ModalI18nKey.ListItemRemove]: 'workflow_add_list_remove',
[ModalI18nKey.ListItemRemoveConfirmTitle]:
'workflow_add_remove_confirm_title',
[ModalI18nKey.ListItemRemoveConfirmDescription]:
'workflow_add_remove_confirm_content',
},
[WorkflowMode.SceneFlow]: {
[ModalI18nKey.Title]: 'scene_workflow_popup_title',
[ModalI18nKey.NavigationMy]: 'workflow_add_navigation_my',
[ModalI18nKey.NavigationTeam]: 'workflow_add_navigation_team',
[ModalI18nKey.NavigationExplore]: 'workflow_add_navigation_explore',
[ModalI18nKey.TabAll]: 'workflow_add_created_tab_all',
[ModalI18nKey.TabMine]: 'workflow_add_created_tab_mine',
[ModalI18nKey.ListEmptyTitle]: 'scene_workflow_popup_search_empty',
[ModalI18nKey.CreatedListEmptyTitle]: 'scene_workflow_popup_list_empty',
// 场景工作流没有描述
[ModalI18nKey.CreatedListEmptyDescription]: '',
[ModalI18nKey.NavigationCreate]: 'workflow_add_navigation_create',
[ModalI18nKey.ListError]: 'workflow_add_list_added_id_empty',
[ModalI18nKey.ListItemRemove]: {
key: 'scene_workflow_delete_workflow_button',
options: { source: I18n.t('scene_mkpl_search_title') },
},
[ModalI18nKey.ListItemRemoveConfirmTitle]: {
key: 'scene_workflow_delete_workflow_popup_title',
options: { source: I18n.t('scene_mkpl_search_title') },
},
[ModalI18nKey.ListItemRemoveConfirmDescription]: {
key: 'scene_workflow_delete_workflow_popup_subtitle',
options: { source: I18n.t('scene_mkpl_search_title') },
},
},
};
/**
* 自动根据 flowMode 返回对应国际化文案
*/
export function useI18nText() {
const context = useContext(WorkflowModalContext);
const flowMode = context?.flowMode ?? WorkflowMode.Workflow;
const i18nText = (key: ModalI18nKey) => {
const i18nKey =
context?.i18nMap?.[key] ||
WORKFLOW_MODAL_I18N_KEY_MAP[flowMode]?.[key] ||
WORKFLOW_MODAL_I18N_KEY_MAP[WorkflowMode.Workflow]?.[key];
const finalKey = typeof i18nKey === 'string' ? i18nKey : i18nKey.key;
return I18n.t(finalKey || '', i18nKey?.options);
};
return { i18nText, ModalI18nKey };
}

View File

@@ -0,0 +1,568 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { useNavigate, useParams } from 'react-router-dom';
import { useContext, useEffect } from 'react';
import { isBoolean } from 'lodash-es';
import { type WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import {
useCozeProRightsStore,
getIsCozePro,
} from '@coze-workflow/resources-adapter';
import {
type DeleteType,
type Workflow,
workflowApi,
WorkflowMode,
} from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { CustomError } from '@coze-arch/bot-error';
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
import { PluginType } from '@coze-arch/bot-api/developer_api';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { Button, Space, Toast, Typography } from '@coze-arch/coze-design';
import WorkflowModalContext from '../workflow-modal-context';
import { isSelectProjectCategory } from '../utils';
import {
type BotPluginWorkFlowItem,
DataSourceType,
type ProductInfo,
type WorkflowInfo,
type WorkflowItemType,
type WorkFlowModalModeProps,
} from '../type';
import { reporter, wait } from '../../utils';
/**
* 特殊错误码
* - 788664021: 由于模型原因,暂不支持复制商店中的工作流
* - 788664024: 模板未购买,请前往模板详情页购买后再复制
*/
const copyProductErrorCodes = ['788664021', '788664024'];
const { Text } = Typography;
export interface WorkflowCardProps extends WorkFlowModalModeProps {
data: WorkflowInfo | ProductInfo;
workflowNodes?: WorkflowNodeJSON[];
copyProductHandle: (
item: ProductInfo,
targetSpaceId: string,
) => Promise<{
workflowId: string;
pluginId: string;
}>;
/**
* workflow 删除按钮点击时触发的 handler
* @param row
*/
handleDeleteWorkflow?: (row: WorkflowInfo) => Promise<{
canDelete: boolean;
deleteType: DeleteType;
handleDelete:
| ((params?: { needDeleteBlockwise: boolean }) => Promise<void>)
| undefined;
}>;
/**
* 是否为专业版特供
*/
isProfessionalTemplate?: boolean;
}
interface UseWorkflowActionReturn {
/** 复制官方流程模板 */
dupWorkflowTpl: () => Promise<void>;
/** 复制流程商品 */
dupProduct: () => Promise<void>;
/** 添加流程 */
addWorkflow: () => Promise<boolean>;
/** 移除流程 */
removeWorkflow: () => void;
/**
* 删除流程
*/
deleteWorkflow: () => Promise<void>;
/** 流程项点击 */
itemClick: () => void;
}
// eslint-disable-next-line max-lines-per-function
export function useWorkflowAction({
data,
workFlowList,
copyProductHandle,
onWorkFlowListChange,
onAdd,
onRemove,
onItemClick,
onDupSuccess,
onDelete,
handleDeleteWorkflow,
isProfessionalTemplate,
}: WorkflowCardProps): UseWorkflowActionReturn {
const context = useContext(WorkflowModalContext);
const { bot_id: botId } = useParams<DynamicParams>();
const navigate = useNavigate();
const isCozePro = useCozeProRightsStore(state =>
getIsCozePro(state?.rightsInfo),
);
useEffect(() => {
useCozeProRightsStore.getState().getRights();
}, []);
async function getWorkflowItem(config: {
spaceId?: string;
workflowId?: string;
pluginId?: string;
isImageflow: boolean;
flowMode?: WorkflowMode;
}): Promise<BotPluginWorkFlowItem> {
if (isSelectProjectCategory(context?.modalState)) {
return getProjectWorkflow(config);
}
return getWorkflowItemByPluginId(config);
}
async function getProjectWorkflow(config: {
workflowId?: string;
spaceId?: string;
}): Promise<BotPluginWorkFlowItem> {
if (!config.spaceId || !config.workflowId) {
throw new CustomError('normal_error', 'getProjectWorkflow: empty id');
}
const resp = await workflowApi.GetWorkflowDetail(
{
space_id: config.spaceId,
workflow_ids: [config.workflowId],
},
{
__disableErrorToast: true,
},
);
// 先获取工作流的信息
const workflowInfos = resp.data ?? [];
if (!workflowInfos?.length) {
Toast.error(I18n.t('workflow_add_list_added_id_empty'));
throw new CustomError('normal_error', 'project workflow list no item');
}
return workflowInfos.at(0) as BotPluginWorkFlowItem;
}
/**
* 通过插件 ID 构造新 workflowItem
*/
// eslint-disable-next-line complexity
async function getWorkflowItemByPluginId(config: {
spaceId?: string;
workflowId?: string;
pluginId?: string;
isImageflow: boolean;
flowMode?: WorkflowMode;
}) {
if (!config.spaceId || !config.workflowId || !config.pluginId) {
throw new CustomError(
'normal_error',
'getWorkflowItemByPluginId: empty id',
);
}
const resp = await PluginDevelopApi.GetPlaygroundPluginList(
{
space_id: config.spaceId,
page: 1,
size: 1,
plugin_ids: [config.pluginId || ''],
plugin_types: [
config.isImageflow ? PluginType.IMAGEFLOW : PluginType.WORKFLOW,
],
},
{
__disableErrorToast: true,
},
);
// 先获取工作流的信息
const pluginInfos = resp.data?.plugin_list ?? [];
if (!pluginInfos?.length) {
Toast.error(
I18n.t(
config.isImageflow
? 'imageflow_add_toast_error'
: 'workflow_add_list_added_id_empty',
),
);
throw new CustomError('normal_error', 'plugin_list no item');
}
const target = pluginInfos.at(0);
const newWorkflow: BotPluginWorkFlowItem = {
workflow_id: config.workflowId || '',
plugin_id: config.pluginId || '',
name: target?.name || '',
desc: target?.desc_for_human || '',
parameters: target?.plugin_apis?.at(0)?.parameters ?? [],
plugin_icon: target?.plugin_icon || '',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
version_name: (target as any)?.version_name,
flow_mode:
target?.plugin_type === PluginType.IMAGEFLOW
? WorkflowMode.Imageflow
: (config.flowMode ?? WorkflowMode.Workflow),
};
return newWorkflow;
}
function isTypeWorkflow(
target: WorkflowInfo | ProductInfo,
): target is WorkflowInfo {
return context?.modalState.dataSourceType === DataSourceType.Workflow;
}
/** 打开流程详情页 */
function openWorkflowDetailPage(workflow: WorkflowInfo | ProductInfo) {
const flowData = workflow as Workflow;
const wId = (flowData as WorkflowInfo).workflow_id ?? '';
const query = new URLSearchParams();
botId && query.append('bot_id', botId);
query.append('space_id', context?.spaceId ?? '');
query.append('workflow_id', wId);
window.open(`/work_flow?${query.toString()}`, '_blank');
}
const dupProduct = async () => {
if (!context) {
return;
}
if (isTypeWorkflow(data)) {
return;
}
if (isProfessionalTemplate && !isCozePro) {
// 跳转到专业版登录
navigate(
`/sign/oauth?redirect=${encodeURIComponent(
'/store/bot',
)}&platform=volcano&page_from=coze_pro_sign_in`,
);
return;
}
reporter.info({ message: 'workflow_modal: dupProduct' });
let newPluginId = '';
let newWorkflowId = '';
try {
const resp = await copyProductHandle(data, context.spaceId);
newPluginId = resp.pluginId;
newWorkflowId = resp.workflowId;
} catch (e) {
if (copyProductErrorCodes.includes(e?.code)) {
Toast.error(e.message);
} else {
Toast.error(I18n.t('copy_failed'));
reporter.error({
message: 'dupProduct: copyProductHandle error',
error: e,
});
}
return;
}
// 延迟刷新列表, 兜底服务端主从延迟导致问题
await wait(100);
try {
const newWorkflow = await getWorkflowItemByPluginId({
spaceId: context.spaceId,
workflowId: newWorkflowId,
pluginId: newPluginId,
isImageflow:
data?.meta_info?.entity_type ===
ProductEntityType.ImageflowTemplateV2,
});
// 构造新的绑定的工作流列表
onWorkFlowListChange?.([...(workFlowList ?? []), newWorkflow]);
onAdd?.(newWorkflow, { isDup: true, spaceId: context.spaceId });
if (onDupSuccess) {
onDupSuccess(newWorkflow);
} else {
// 复制商品成功
Toast.success({
content: (
<Space spacing={6}>
<Text>{I18n.t('workflowstore_workflow_copy_successful')}</Text>
<Button
color="primary"
onClick={() => {
window.open(
`/work_flow?space_id=${context.spaceId}&workflow_id=${newWorkflow.workflow_id}&from=dupSuccess`,
);
}}
>
{I18n.t('workflowstore_continue_editing')}
</Button>
</Space>
),
});
}
} catch (e) {
Toast.error(I18n.t('workflow_add_list_added_fail'));
reporter.error({
message: 'dupProduct: getWorkflowItemByPluginId error',
error: e,
});
}
};
const dupWorkflowTpl = async () => {
if (!context) {
return;
}
if (!isTypeWorkflow(data)) {
return;
}
reporter.info({ message: 'workflow_modal: dupWorkflowTpl' });
let newPluginId = '';
let newWorkflowId = '';
try {
const resp = await workflowApi.CopyWkTemplateApi(
{
workflow_ids: [data.workflow_id || ''],
target_space_id: context.spaceId,
},
{
__disableErrorToast: true,
},
);
newWorkflowId = resp.data[data.workflow_id ?? '']?.workflow_id || '';
newPluginId = resp.data[data.workflow_id ?? '']?.plugin_id || '0';
} catch (e) {
Toast.error(I18n.t('copy_failed'));
reporter.error({
message: 'dupWorkflowTpl: CopyWkTemplateApi error',
error: e,
});
return;
}
if (!newWorkflowId || newPluginId === '0') {
Toast.error(I18n.t('copy_failed'));
reporter.error({
message: 'dupWorkflowTpl: CopyWkTemplateApi error',
error: new CustomError(
'normal_error',
`CopyWkTemplateApi: plugin_id is ${newPluginId}, workflow_id is ${newWorkflowId}`,
),
});
return;
}
// 延迟刷新列表, 兜底服务端主从延迟导致问题
await wait(100);
try {
const newWorkflow = await getWorkflowItemByPluginId({
spaceId: context.spaceId,
workflowId: newWorkflowId,
pluginId: newPluginId,
isImageflow: context.flowMode === WorkflowMode.Imageflow,
flowMode: data.flow_mode,
});
const sourceFlowMode = data?.flow_mode ?? context?.flowMode;
if (typeof sourceFlowMode !== 'undefined') {
newWorkflow.flow_mode = sourceFlowMode;
}
// 构造新的绑定的工作流列表
onWorkFlowListChange?.([...(workFlowList ?? []), newWorkflow]);
onAdd?.(newWorkflow, { isDup: true, spaceId: context.spaceId });
if (onDupSuccess) {
onDupSuccess(newWorkflow);
} else {
Toast.success({
content: (
<Space spacing={6}>
<Text>{I18n.t('workflowstore_workflow_copy_successful')}</Text>
<Button
color="primary"
onClick={() => {
window.open(
`/work_flow?space_id=${context.spaceId}&workflow_id=${newWorkflow.workflow_id}`,
);
}}
>
{I18n.t('workflowstore_continue_editing')}
</Button>
</Space>
),
});
}
} catch (e) {
Toast.error(e.message || I18n.t('workflow_add_list_added_fail'));
reporter.error({
message: 'dupWorkflowTpl: getWorkflowItemByPluginId error',
error: e,
});
}
};
const removeWorkflow = () => {
if (!workFlowList || !isTypeWorkflow(data)) {
return;
}
reporter.info({ message: 'workflow_modal: removeWorkflow' });
const target = workFlowList.find(
item => item.workflow_id === data.workflow_id,
);
if (!target) {
return;
}
onRemove?.(target);
onWorkFlowListChange?.(
workFlowList.filter(item => item.workflow_id !== data.workflow_id),
);
};
const addWorkflow = async () => {
if (!context || !isTypeWorkflow(data)) {
return false;
}
reporter.info({ message: 'workflow_modal: addWorkflow' });
try {
const newWorkflow = await getWorkflowItem({
spaceId: context.spaceId,
workflowId: data.workflow_id,
pluginId: data.plugin_id,
isImageflow: data?.flow_mode === WorkflowMode.Imageflow,
flowMode: data?.flow_mode,
});
if (typeof data?.flow_mode !== 'undefined') {
newWorkflow.flow_mode = data?.flow_mode;
}
// 构造新的绑定的工作流列表
onWorkFlowListChange?.([...(workFlowList ?? []), newWorkflow]);
const addResult = await onAdd?.(newWorkflow, {
isDup: false,
spaceId: context.spaceId,
});
/**
* 允许外部业务逻辑添加失败
*/
if (isBoolean(addResult)) {
return addResult as unknown as boolean;
}
return true;
} catch (e) {
Toast.error(e.message || I18n.t('workflow_add_list_added_fail'));
reporter.error({
message: 'addWorkflow: getWorkflowItemByPluginId error',
error: e,
});
return false;
}
};
const deleteWorkflow = async () => {
if (!isTypeWorkflow(data)) {
return;
}
if (!handleDeleteWorkflow) {
return;
}
reporter.info({ message: 'workflow_modal: deleteWorkflow' });
// delete api
const deleteConfig = await handleDeleteWorkflow?.(data);
if (deleteConfig?.canDelete) {
await deleteConfig?.handleDelete?.();
}
if (!workFlowList) {
return;
}
const target = workFlowList.find(
item => item.workflow_id === data.workflow_id,
);
if (!target) {
return;
}
onDelete?.(target);
onWorkFlowListChange?.(
workFlowList.filter(item => item.workflow_id !== data.workflow_id),
);
};
const itemClick = () => {
if (!context) {
return;
}
reporter.info({ message: 'workflow_modal: itemClick' });
if (onItemClick) {
// @ts-expect-error 符合预期
const item: WorkflowItemType = {
item: data,
type: context.modalState.dataSourceType,
};
const ret = onItemClick(item, context.getModalState(context));
if (!ret || ret.handled) {
return;
}
}
if (isTypeWorkflow(data)) {
openWorkflowDetailPage(data);
} else {
window.open(
`/template/workflow/${data.meta_info.id}?entity_id=${ProductEntityType.WorkflowTemplateV2}`,
'_blank',
);
}
};
return {
dupWorkflowTpl,
dupProduct,
addWorkflow,
removeWorkflow,
deleteWorkflow,
itemClick,
};
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useRef, useState } from 'react';
import { useMount } from 'ahooks';
import { QueryClientProvider } from '@tanstack/react-query';
import {
BindBizType,
OrderBy,
WorkFlowListStatus,
WorkflowMode,
workflowQueryClient,
} from '@coze-workflow/base/api';
import { isGeneralWorkflow } from '@coze-workflow/base';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
EVENT_NAMES,
sendTeaEvent,
FlowResourceFrom,
FlowStoreType,
FlowDuplicateType,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { CustomError } from '@coze-arch/bot-error';
import { SpaceType } from '@coze-arch/bot-api/playground_api';
import WorkflowModalContext, {
type WorkflowModalContextValue,
} from '../workflow-modal-context';
import {
type WorkFlowModalModeProps,
DataSourceType,
MineActiveEnum,
type WorkflowModalState,
WorkflowModalFrom,
WorkflowCategory,
} from '../type';
import { type WorkflowFilterRef } from '../sider/workflow-filter';
import { WorkflowModalSider } from '../sider';
import styles from '../index.module.less';
import { WorkflowModalFilterForDouyin } from '../filter-douyin';
import { WorkflowModalFilter } from '../filter';
import { WorkflowModalContent } from '../content';
import { reporter } from '../../utils';
import { ModalI18nKey, WORKFLOW_MODAL_I18N_KEY_MAP } from './use-i18n-text';
/**
* 返回流程弹窗的各部分组件, 内容,侧边,筛选组件, 拆分组件可用于不同布局
* 本用于流程选择
*/
// eslint-disable-next-line @coze-arch/max-line-per-function
export const useWorkflowModalParts = (props: WorkFlowModalModeProps) => {
const {
flowMode = WorkflowMode.Workflow,
initState,
hideSider = false,
bindBizId,
bindBizType,
projectId,
i18nMap,
from,
} = props;
const { space_type: spaceType, id: spaceId } = useSpaceStore(
state => state.space,
);
const sideRef = useRef<WorkflowFilterRef>(null);
const [modalState, setModalState] = useState<WorkflowModalState>({
status: initState?.status ?? WorkFlowListStatus.HadPublished,
dataSourceType: initState?.dataSourceType ?? DataSourceType.Workflow,
creator: initState?.creator ?? MineActiveEnum.All,
workflowTag: initState?.workflowTag ?? 0,
productCategory: initState?.productCategory ?? '',
query: initState?.query ?? '',
isSpaceWorkflow: initState?.isSpaceWorkflow ?? true,
workflowCategory:
from === WorkflowModalFrom.ProjectWorkflowAddNode
? WorkflowCategory.Project
: WorkflowCategory.Library,
listFlowMode: initState?.listFlowMode ?? WorkflowMode.All,
});
const updateModalState = useCallback(
(newState: Partial<WorkflowModalState>) => {
setModalState({
...modalState,
...newState,
});
},
[modalState],
);
// 排序规则(流程数据源)
const [orderBy, setOrderBy] = useState(OrderBy.UpdateTime);
const [createModalVisible, setCreateModalVisible] = useState(false);
useMount(() => {
setModalState({
status: initState?.status ?? WorkFlowListStatus.HadPublished,
dataSourceType: initState?.dataSourceType ?? DataSourceType.Workflow,
creator: initState?.creator ?? MineActiveEnum.All,
workflowTag: initState?.workflowTag ?? 0,
productCategory: initState?.productCategory ?? '',
query: initState?.query ?? '',
isSpaceWorkflow: initState?.isSpaceWorkflow ?? true,
workflowCategory:
from === WorkflowModalFrom.ProjectWorkflowAddNode
? WorkflowCategory.Project
: WorkflowCategory.Library,
listFlowMode: initState?.listFlowMode ?? WorkflowMode.All,
});
reporter.info({
message: 'useWorkflowModalParts mounted',
meta: { from: props.from },
});
});
const contextValue: WorkflowModalContextValue = {
spaceId: spaceId ?? '',
spaceType: spaceType ?? SpaceType.Team,
bindBizId,
bindBizType,
projectId,
flowMode,
modalState,
updateModalState,
orderBy,
setOrderBy,
createModalVisible,
setCreateModalVisible,
getModalState: ctx => ({
...ctx.modalState,
}),
i18nMap,
};
if (!spaceType || !spaceId) {
reporter.errorEvent({
eventName: 'workflow_modal_in_bot_no_spaceId',
error: new CustomError('normal_error', 'no spaceId'),
});
return {
sider: null,
content: null,
filter: null,
} as const;
}
const isBindDouyin = bindBizType === BindBizType.DouYinBot;
const hideSidebar = hideSider || isBindDouyin;
/** 侧边栏组件 */
const sider = hideSidebar ? null : (
<QueryClientProvider client={workflowQueryClient}>
<WorkflowModalContext.Provider value={contextValue}>
<WorkflowModalSider ref={sideRef} {...props} />
</WorkflowModalContext.Provider>
</QueryClientProvider>
);
/** 流程列表组件 */
const content = (
<QueryClientProvider client={workflowQueryClient}>
<WorkflowModalContext.Provider value={contextValue}>
<WorkflowModalContent
{...props}
onDupSuccess={val => {
if (!props.onDupSuccess) {
return;
}
if (modalState.dataSourceType === DataSourceType.Product) {
const resourceMap: Record<string, FlowResourceFrom> = {
[WorkflowModalFrom.SpaceWorkflowList]:
FlowResourceFrom.template,
[WorkflowModalFrom.WorkflowAddNode]: FlowResourceFrom.flowIde,
[WorkflowModalFrom.BotSkills]: FlowResourceFrom.botIde,
[WorkflowModalFrom.BotMultiSkills]: FlowResourceFrom.botIde,
[WorkflowModalFrom.BotTrigger]: FlowResourceFrom.botIde,
[WorkflowModalFrom.BotShortcut]: FlowResourceFrom.botIde,
[WorkflowModalFrom.WorkflowAgent]: FlowResourceFrom.botIde,
};
const resource =
resourceMap[props.from || ''] ?? FlowResourceFrom.botIde;
sendTeaEvent(EVENT_NAMES.flow_duplicate_click, {
store_type: isGeneralWorkflow(flowMode)
? FlowStoreType.workflow
: FlowStoreType.imageflow,
resource,
category_name: sideRef.current?.getCurrent()?.name || '',
duplicate_type:
props.from === WorkflowModalFrom.BotSkills
? FlowDuplicateType.toBot
: FlowDuplicateType.toWorkspace,
});
}
props.onDupSuccess(val);
}}
/>
</WorkflowModalContext.Provider>
</QueryClientProvider>
);
/** 筛选组件 */
let filter = (
<QueryClientProvider client={workflowQueryClient}>
<WorkflowModalContext.Provider value={contextValue}>
{isBindDouyin ? (
<WorkflowModalFilterForDouyin {...props} />
) : (
<WorkflowModalFilter {...props} />
)}
</WorkflowModalContext.Provider>
</QueryClientProvider>
);
// 隐藏 sider 后,把 title 放到 filter 上边
if (hideSidebar && !isBindDouyin) {
const title = I18n.t(
WORKFLOW_MODAL_I18N_KEY_MAP[flowMode]?.[
ModalI18nKey.Title
] as I18nKeysNoOptionsType,
);
filter = (
<div className="flex flex-col items-start flex-grow flex-shrink-0">
<div className={styles.title}>{title}</div>
{filter}
</div>
);
}
return {
sider,
filter,
content,
} as const;
};

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo, useState } from 'react';
import {
useInfiniteQuery,
type UseInfiniteQueryResult,
} from '@tanstack/react-query';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { CustomError } from '@coze-arch/bot-error';
import {
ProductEntityType,
type ProductInfo,
type public_api,
SortType,
} from '@coze-arch/bot-api/product_api';
import { ProductApi } from '@coze-arch/bot-api';
export type GetProductListRequest = public_api.GetProductListRequest;
interface WorkflowProductListReturn {
updatePageParam: (newParam: Partial<GetProductListRequest>) => void;
workflowProductList: ProductInfo[];
queryError: UseInfiniteQueryResult['error'];
loadingStatus: UseInfiniteQueryResult['status'];
refetch: UseInfiniteQueryResult['refetch'];
fetchNextPage: UseInfiniteQueryResult['fetchNextPage'];
isFetching: UseInfiniteQueryResult['isFetching'];
isFetchingNextPage: UseInfiniteQueryResult['isFetchingNextPage'];
hasNextPage: UseInfiniteQueryResult['hasNextPage'];
copyProduct: (
item: ProductInfo,
targetSpaceId: string,
) => Promise<{
workflowId: string;
pluginId: string;
}>;
}
// eslint-disable-next-line max-lines-per-function,@coze-arch/max-line-per-function
export function useWorkflowProductList({
pageSize = 12,
enabled = false,
}: {
pageSize?: number;
enabled?: boolean;
} = {}): Readonly<WorkflowProductListReturn> {
const [keyword, setKeyword] = useState<string>();
const [sortType, setSortType] = useState<SortType>(SortType.Heat);
const [categoryId, setCategoryId] = useState<string>();
const [source, setSource] = useState<GetProductListRequest['source']>();
const initialPageParam = useMemo<GetProductListRequest>(
() => ({
entity_types: [
ProductEntityType.WorkflowTemplateV2,
ProductEntityType.ImageflowTemplateV2,
],
page_num: 1,
page_size: pageSize,
category_id: categoryId,
sort_type: sortType,
source,
keyword,
}),
[keyword, sortType, categoryId, source],
);
const updatePageParam = (newParam: Partial<GetProductListRequest>) => {
if ('category_id' in newParam) {
setCategoryId(newParam.category_id);
}
if ('sort_type' in newParam) {
setSortType(newParam.sort_type ?? SortType.Newest);
}
if ('keyword' in newParam) {
setKeyword(newParam.keyword);
}
if ('source' in newParam) {
setSource(newParam.source);
}
};
const queryKey = useMemo(
() => ['workflow_product', JSON.stringify(initialPageParam)],
[initialPageParam],
);
const fetchProductList = async (params: GetProductListRequest) => {
const resp = await ProductApi.PublicGetProductList(params);
return resp.data;
};
const {
data: pageData,
error: queryError,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status: loadingStatus,
refetch,
} = useInfiniteQuery({
enabled,
queryKey,
queryFn: ({ pageParam }) => fetchProductList(pageParam),
initialPageParam,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (!lastPage?.has_more) {
return null;
}
return {
...lastPageParam,
page_num: (lastPageParam.page_num ?? 1) + 1,
};
},
});
const workflowProductList = useMemo(() => {
const result: ProductInfo[] = [];
const idMap: Record<string, boolean> = {};
pageData?.pages.forEach(page => {
page?.products?.forEach(product => {
if (!product.meta_info.id) {
return;
}
if (!idMap[product.meta_info.id]) {
result.push(product);
}
idMap[product.meta_info.id] = true;
});
});
return result;
}, [pageData]);
const copyProduct = async (item: ProductInfo, targetSpaceId: string) => {
if (!item?.meta_info?.id) {
throw new CustomError('normal_error', 'no productId');
}
const res = await ProductApi.PublicDuplicateProduct(
{
product_id: item?.meta_info?.id,
space_id: targetSpaceId,
entity_type: item.meta_info.entity_type as ProductEntityType,
},
{
__disableErrorToast: true,
},
);
const workflowId = res.data?.new_entity_id;
const pluginId = res.data?.new_plugin_id;
if (!workflowId || !pluginId) {
throw new CustomError(
'normal_error',
'copyProduct fail, no new_entity_id',
);
}
sendTeaEvent(EVENT_NAMES.template_action_front, {
template_id: item.meta_info.id ?? '',
template_name: item.meta_info.name ?? '',
template_type:
item.meta_info.entity_type === ProductEntityType.WorkflowTemplateV2
? 'workflow'
: 'imageflow',
template_tag_professional: item.meta_info.is_professional
? 'professional'
: 'basic',
entity_id: item.meta_info.entity_id ?? '',
...(item?.meta_info?.is_free
? ({
template_tag_prize: 'free',
} as const)
: ({
template_tag_prize: 'paid',
template_prize_detail: Number(item?.meta_info?.price?.amount) || 0,
} as const)),
action: 'duplicate',
after_id: workflowId,
});
return { workflowId, pluginId };
};
return {
// 筛选条件
updatePageParam,
//
workflowProductList,
queryError,
loadingStatus,
refetch,
fetchNextPage,
isFetching,
isFetchingNextPage,
hasNextPage,
// 操作
copyProduct,
} as const;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react';
import { useDebounceFn } from 'ahooks';
import { UISearch } from '@coze-studio/components';
import { SortType } from '@coze-arch/idl/product_api';
import { I18n } from '@coze-arch/i18n';
import WorkflowModalContext from '../workflow-modal-context';
import { DataSourceType, type WorkflowModalState } from '../type';
export function useWorkflowSearch() {
const context = useContext(WorkflowModalContext);
const { run: debounceChangeSearch, cancel } = useDebounceFn(
(search: string) => {
/** 搜索最大字符数 */
const maxCount = 100;
if (search.length > maxCount) {
updateSearchQuery(search.substring(0, maxCount));
} else {
updateSearchQuery(search);
}
},
{ wait: 300 },
);
if (!context) {
return null;
}
const { dataSourceType, query, isSpaceWorkflow, sortType } =
context.modalState;
const updateSearchQuery = (search?: string) => {
const newState: Partial<WorkflowModalState> = { query: search ?? '' };
if (dataSourceType === DataSourceType.Workflow) {
// 搜索时如果有标签, 重置全部
newState.workflowTag = isSpaceWorkflow ? 0 : 1;
newState.sortType = undefined;
}
if (dataSourceType === DataSourceType.Product) {
if (!search && sortType === SortType.Relative) {
newState.sortType = SortType.Heat;
}
if (search && !context.modalState.query) {
newState.sortType = newState.sortType = SortType.Relative;
}
}
context.updateModalState(newState);
};
return (
<UISearch
tabIndex={-1}
value={query}
placeholder={I18n.t('workflow_add_search_placeholder')}
data-testid="workflow.modal.search"
onSearch={search => {
if (!search) {
// 如果search清空了那么立即更新query
cancel();
updateSearchQuery('');
} else {
// 如果search有值那么防抖更新
debounceChangeSearch(search);
}
}}
/>
);
}

View File

@@ -0,0 +1,19 @@
.workflow-modal {
height: 100%;
.title {
@apply mb-24px coz-fg-plus font-medium text-20px leading-7;
}
.title-for-avatar {
@apply coz-fg-plus font-medium text-20px leading-7;
}
}
.douyin-workflow-modal {
:global {
.semi-modal {
width: 800px;
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import classNames from 'classnames';
import { BindBizType, WorkflowMode } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import { UICompositionModal } from '@coze-arch/bot-semi';
import {
DataSourceType,
MineActiveEnum,
WorkFlowModalModeProps,
WorkflowModalFrom,
WorkflowModalState,
type WorkflowModalProps,
WORKFLOW_LIST_STATUS_ALL,
BotPluginWorkFlowItem,
} from './type';
import { useWorkflowModalParts } from './hooks/use-workflow-modal-parts';
import {
WORKFLOW_MODAL_I18N_KEY_MAP,
ModalI18nKey,
} from './hooks/use-i18n-text';
import styles from './index.module.less';
export { ModalI18nKey };
const WorkflowModal: FC<WorkflowModalProps> = ({
className,
visible,
onClose,
...props
}) => {
const { sider, filter, content } = useWorkflowModalParts(props);
const flowMode = props.flowMode ?? WorkflowMode.Workflow;
const isDouyinBot = props.bindBizType === BindBizType.DouYinBot;
return (
<UICompositionModal
visible={visible}
onCancel={onClose}
siderWrapperClassName={props.hideSider || isDouyinBot ? 'hidden' : ''}
header={I18n.t(
WORKFLOW_MODAL_I18N_KEY_MAP[flowMode]?.[ModalI18nKey.Title],
)}
className={classNames(
styles['workflow-modal'],
className,
'new-workflow-modal',
isDouyinBot ? styles['douyin-workflow-modal'] : '',
)}
sider={sider}
filter={filter}
content={content}
/>
);
};
export default WorkflowModal;
export {
useWorkflowModalParts,
DataSourceType,
MineActiveEnum,
WorkflowModalFrom,
WorkflowModalProps,
WorkFlowModalModeProps,
WorkflowModalState,
WORKFLOW_LIST_STATUS_ALL,
BotPluginWorkFlowItem,
};
export { isSelectProjectCategory } from './utils';
export { WorkflowCategory } from './type';

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, useContext, useState } from 'react';
import { WorkflowMode, BindBizType } from '@coze-arch/idl/workflow_api';
import { I18n } from '@coze-arch/i18n';
import { CustomError } from '@coze-arch/bot-error';
import {
IconCozWorkflow,
IconCozChat,
IconCozArrowDown,
} from '@coze-arch/coze-design/icons';
import { Menu, Button } from '@coze-arch/coze-design';
import WorkflowModalContext from '../workflow-modal-context';
import { WorkflowModalFrom, type WorkFlowModalModeProps } from '../type';
import { useI18nText } from '../hooks/use-i18n-text';
import { CreateWorkflowModal } from '../../workflow-edit';
import { wait } from '../../utils';
import { useOpenWorkflowDetail } from '../../hooks/use-open-workflow-detail';
export const CreateWorkflowBtn: FC<
Pick<
WorkFlowModalModeProps,
'onCreateSuccess' | 'nameValidators' | 'from'
> & {
className?: string;
}
> = ({ className, onCreateSuccess, nameValidators, from }) => {
const context = useContext(WorkflowModalContext);
const { i18nText, ModalI18nKey } = useI18nText();
const openWorkflowDetailPage = useOpenWorkflowDetail();
const [createFlowMode, setCreateFlowMode] = useState(
context?.flowMode ?? WorkflowMode.Workflow,
);
if (!context) {
return null;
}
const { createModalVisible, setCreateModalVisible, bindBizType } = context;
// 如果是抖音分身场景,此时只展示一个【创建工作流】按钮
const showSingleButton =
bindBizType === BindBizType.DouYinBot ||
from === WorkflowModalFrom.WorkflowAgent;
/** 打开流程详情页 */
const menuConfig = [
{
label: I18n.t('workflow_add_navigation_create'),
handler: () => {
setCreateFlowMode(WorkflowMode.Workflow);
setCreateModalVisible(true);
},
icon: <IconCozWorkflow />,
},
{
label: I18n.t('wf_chatflow_81'),
handler: () => {
setCreateFlowMode(WorkflowMode.ChatFlow);
setCreateModalVisible(true);
},
icon: <IconCozChat />,
},
];
return (
<>
{/* The community version does not currently support chatflow, for future expansion */}
{showSingleButton || IS_OPEN_SOURCE ? (
<Button
className={className}
color="hgltplus"
onClick={() => {
if (from === WorkflowModalFrom.WorkflowAgent) {
setCreateFlowMode(WorkflowMode.ChatFlow);
} else {
setCreateFlowMode(WorkflowMode.Workflow);
}
setCreateModalVisible(true);
}}
>
{from === WorkflowModalFrom.WorkflowAgent
? I18n.t('wf_chatflow_81')
: I18n.t('workflow_add_navigation_create')}
</Button>
) : (
<Menu
trigger="click"
position="bottom"
render={
<Menu.SubMenu className={'w-[198px]'} mode="menu">
{menuConfig.map(item => (
<Menu.Item
key={item.label}
onClick={(value, event) => {
event.stopPropagation();
item.handler();
}}
icon={item.icon}
>
{item.label}
</Menu.Item>
))}
</Menu.SubMenu>
}
>
<Button
className={className}
color="hgltplus"
icon={<IconCozArrowDown />}
iconPosition="right"
>
{context.projectId
? I18n.t('wf_chatflow_03')
: i18nText(ModalI18nKey.NavigationCreate)}
</Button>
</Menu>
)}
<CreateWorkflowModal
initConfirmDisabled
mode="add"
flowMode={createFlowMode}
bindBizType={context.bindBizType}
bindBizId={context.bindBizId}
projectId={context.projectId}
visible={createModalVisible}
onCancel={() => setCreateModalVisible(false)}
onSuccess={async ({ workflowId, flowMode }) => {
setCreateModalVisible(false);
if (!workflowId) {
throw new CustomError(
'[Workflow] create failed',
'create workflow failed, no workflow id',
);
}
// 由于服务端创建 workflow 的主备数据同步有延迟,所以在创建完后如果直接跳转,有可能查不到 workflowId所以前端延迟下降低问题触发的概率
await wait(500);
if (onCreateSuccess) {
onCreateSuccess?.({
spaceId: context.spaceId,
workflowId,
flowMode: flowMode || WorkflowMode.Workflow,
});
} else {
openWorkflowDetailPage({
workflowId,
spaceId: context.spaceId ?? '',
});
}
}}
nameValidators={nameValidators}
/>
</>
);
};

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { forwardRef, useContext } from 'react';
import { UICompositionModalSider } from '@coze-arch/bot-semi';
import WorkflowModalContext from '../workflow-modal-context';
import { type WorkFlowModalModeProps } from '../type';
import { useWorkflowSearch } from '../hooks/use-workflow-search';
import { WorkflowFilter, type WorkflowFilterRef } from './workflow-filter';
import { CreateWorkflowBtn } from './create-workflow-btn';
export const WorkflowModalSider = forwardRef<
WorkflowFilterRef,
WorkFlowModalModeProps
>((props, ref) => {
const context = useContext(WorkflowModalContext);
const { hiddenCreate, hiddenExplore, from } = props;
const searchNode = useWorkflowSearch();
if (!context) {
return null;
}
return (
<UICompositionModalSider style={{ paddingTop: 16 }}>
<UICompositionModalSider.Header>
{searchNode}
{!hiddenCreate && (
<CreateWorkflowBtn
className="!mt-6 w-full"
onCreateSuccess={props.onCreateSuccess}
nameValidators={props.nameValidators}
from={from}
/>
)}
</UICompositionModalSider.Header>
<UICompositionModalSider.Content>
<WorkflowFilter
ref={ref}
from={props.from}
hiddenExplore={hiddenExplore}
hiddenSpaceList={props.hiddenSpaceList}
hiddenLibrary={props.hiddenLibrary}
hiddenWorkflowCategories={props.hiddenWorkflowCategories}
/>
</UICompositionModalSider.Content>
</UICompositionModalSider>
);
});
WorkflowModalSider.displayName = 'WorkflowModalSider';

View File

@@ -0,0 +1,83 @@
@import '@coze-common/assets/style/common.less';
@import '@coze-common/assets/style/mixins.less';
.tool-tag-list {
overflow: auto;
flex: 1;
flex-shrink: 0;
padding-top: 16px;
white-space: nowrap;
&-label {
height: 40px;
margin-bottom: 8px;
padding: 0 12px;
font-size: 12px;
font-weight: 600;
line-height: 40px;
color: var(--light-usage-text-color-text-3, rgba(28, 29, 35, 35%));
}
&-cell {
cursor: pointer;
position: relative;
display: flex;
align-items: center;
height: 32px;
margin-bottom: 4px;
padding: 0 10px 0 12px;
font-size: 14px;
line-height: 32px;
color: #1d1c23;
border-radius: 3px;
&-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
margin-right: 8px;
.common-svg-icon(20px, #1d1c23);
>img {
width: 24px;
height: 24px;
padding: 4px;
}
}
&-divider {
width: calc(100% - 24px);
margin: 12px;
background: rgba(28, 29, 35, 12%);
}
&:hover {
color: var(--light-usage-text-color-text-0, #1c1f23);
background: var(--light-usage-fill-color-fill-0, rgba(46, 50, 56, 5%));
border-radius: 8px;
}
&.active {
font-size: 14px;
font-weight: 600;
color: var(--light-usage-text-color-text-0, #1c1d23);
background: var(--light-usage-fill-color-fill-0, rgba(46, 47, 56, 5%));
border-radius: 8px;
.tool-tag-list-cell-icon {
.common-svg-icon(20px, #4d53e8);
}
}
}
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import {
forwardRef,
type ReactNode,
useContext,
useImperativeHandle,
useMemo,
} from 'react';
import classNames from 'classnames';
import { useQuery } from '@tanstack/react-query';
import { WorkflowMode } from '@coze-workflow/base/api';
import { workflowApi, isGeneralWorkflow } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { useFlags } from '@coze-arch/bot-flags';
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
import { ProductApi } from '@coze-arch/bot-api';
import {
IconCozAllFill,
IconCozFireFill,
IconCozKnowledgeFill,
} from '@coze-arch/coze-design/icons';
import WorkflowModalContext from '../workflow-modal-context';
import {
DataSourceType,
WorkflowCategory,
WorkflowModalFrom,
type WorkFlowModalModeProps,
} from '../type';
import s from './workflow-filter.module.less';
interface PluginTag {
type?: string | number;
name?: string;
icon?: string | React.ReactElement;
active_icon?: string | React.ReactElement;
}
export interface WorkflowFilterRef {
getCurrent: () => PluginTag | undefined;
}
const WorkflowFilter = forwardRef<
WorkflowFilterRef,
Pick<
WorkFlowModalModeProps,
| 'from'
| 'hiddenSpaceList'
| 'hiddenExplore'
| 'hiddenLibrary'
| 'hiddenWorkflowCategories'
>
>(
(
{
from,
hiddenSpaceList,
hiddenExplore,
hiddenLibrary,
hiddenWorkflowCategories = [],
},
ref,
) => {
const context = useContext(WorkflowModalContext);
const [FLAGS] = useFlags();
const getWorkflowTags = async (): Promise<{
type: 'WorkFlowTemplateTag' | 'PublicGetProductCategoryList';
data: PluginTag[];
}> => {
if (hiddenExplore) {
return {
type: 'WorkFlowTemplateTag',
data: [],
};
}
if (
FLAGS['bot.community.store_imageflow'] ||
isGeneralWorkflow(context?.flowMode || WorkflowMode.Workflow)
) {
// The community version does not currently support workflow template tags for future expansion
if (IS_OPEN_SOURCE) {
return {
type: 'PublicGetProductCategoryList',
data: [],
};
}
const resp = await ProductApi.PublicGetProductCategoryList({
// 模版分类对于 工作流 / 图像流 通用
entity_type: ProductEntityType.TemplateCommon,
need_empty_category: false,
});
const targetList: PluginTag[] = (resp.data?.categories ?? []).map(
item => ({
type: item.id,
name: item.name ?? '',
icon: item.icon_url,
active_icon: item.active_icon_url,
}),
);
targetList.unshift({
type: 'recommend',
name: I18n.t('workflowstore_category1'),
icon: <IconCozFireFill />,
active_icon: <IconCozFireFill />,
});
targetList.unshift({
type: 'all',
name: I18n.t('All'),
icon: <IconCozAllFill />,
active_icon: <IconCozAllFill />,
});
return {
type: 'PublicGetProductCategoryList',
data: targetList,
};
}
const res = await workflowApi.WorkFlowTemplateTag({
flow_mode: context?.flowMode,
});
return {
type: 'WorkFlowTemplateTag',
data: res.data?.tags ?? [],
};
};
const currentValue = useMemo(() => {
if (!context?.modalState) {
return '';
}
return context.modalState.dataSourceType === DataSourceType.Product
? context.modalState.productCategory
: context.modalState.workflowTag;
}, [context?.modalState]);
const queryKey = useMemo(() => {
const result = ['workflow-modal-side'];
result.push(`flowMode-${context?.flowMode}`);
return result;
}, [context]);
const { data: tags } = useQuery({
enabled: Boolean(context),
queryKey,
queryFn: getWorkflowTags,
});
useImperativeHandle(ref, () => ({
getCurrent: () => tags?.data.find(item => item.type === currentValue),
}));
/** 展示空间流程, 我的/团队的 */
const clickSpaceContent = (category?: WorkflowCategory) => {
context?.updateModalState({
isSpaceWorkflow: category !== WorkflowCategory.Example,
workflowCategory: category,
workflowTag: 0,
query: '',
dataSourceType: DataSourceType.Workflow,
productCategory: category,
sortType: undefined,
});
};
const nodeDataList = useMemo<
Array<{
title?: string;
icon?: ReactNode;
testId?: string;
category?: WorkflowCategory;
}>
>(() => {
const tempList = hiddenLibrary
? []
: [
{
title: I18n.t('project_resource_modal_library_resources', {
resource: I18n.t('library_resource_type_workflow'),
}),
icon: (
<IconCozKnowledgeFill
className={s['tool-tag-list-cell-icon']}
/>
),
testId: 'workflow.modal.search.option.library',
category: WorkflowCategory.Library,
},
];
if (from === WorkflowModalFrom.ProjectWorkflowAddNode) {
tempList.push({
title: I18n.t('project_resource_modal_project_resources', {
resource: I18n.t('library_resource_type_workflow'),
}),
icon: (
<IconCozKnowledgeFill className={s['tool-tag-list-cell-icon']} />
),
testId: 'workflow.modal.search.option.project',
category: WorkflowCategory.Project,
});
}
tempList.push({
title: I18n.t('workflow_add_example'),
icon: <IconCozKnowledgeFill className={s['tool-tag-list-cell-icon']} />,
testId: 'workflow.modal.search.option.example',
category: WorkflowCategory.Example,
});
return tempList.filter(
item => !hiddenWorkflowCategories.includes(item.category),
);
}, [from, hiddenLibrary, hiddenWorkflowCategories, FLAGS]);
if (!context) {
return null;
}
return (
<div className={`tool-tag-list ${s['tool-tag-list']}`}>
{!hiddenSpaceList && (
<div>
{nodeDataList.map(nodeData => {
const active =
context?.modalState.workflowCategory === nodeData.category;
return (
<div
key={nodeData.testId}
data-testid={nodeData.testId}
className={classNames(s['tool-tag-list-cell'], {
[s.active]: active,
})}
onClick={() => clickSpaceContent(nodeData.category)}
>
{nodeData.icon}
{nodeData.title}
</div>
);
})}
</div>
)}
</div>
);
},
);
WorkflowFilter.displayName = 'WorkflowFilter';
export { WorkflowFilter };

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type BindBizType,
type WorkFlowListStatus,
type WorkflowMode,
} from '@coze-workflow/base/api';
import { type SortType, type public_api } from '@coze-arch/idl/product_api';
import { type PluginParameter } from '@coze-arch/idl/developer_api';
import { type WorkflowDetailData } from '@coze-arch/bot-api/workflow_api';
import { type RuleItem } from '../workflow-edit';
import { type WorkflowInfo, WorkflowModalFrom } from '../types';
import { type I18nKey, type ModalI18nKey } from './hooks/use-i18n-text';
export { WorkflowModalFrom };
export interface BotPluginWorkFlowItem extends WorkflowDetailData {
workflow_id: string;
plugin_id: string;
name: string;
desc: string;
parameters: Array<PluginParameter>;
plugin_icon: string;
flow_mode?: WorkflowMode;
version_name?: string;
}
export type GetProductListRequest = public_api.GetProductListRequest;
/**
* 商品类型
*
* 由于类型同名问题, 直接导出 ProductInfo 指向的是后台的类型不是目标类型,需要使用本方法转一下
*/
export type ProductInfo = public_api.ProductInfo;
export enum MineActiveEnum {
All = '1',
Mine = '2',
}
/** 数据来源 */
export enum DataSourceType {
/** 流程 */
Workflow = 'workflow',
/** @deprecated 流程商店 */
Product = 'product',
}
export type WorkflowItemType =
| { type: DataSourceType.Workflow; item: WorkflowInfo }
| { type: DataSourceType.Product; item: ProductInfo };
export const WORKFLOW_LIST_STATUS_ALL = 'all';
/**
* 项目内的工作流添加子流程时的分类中,资源库/项目工作流分类
*/
export enum WorkflowCategory {
/**
* 项目工作流
*/
Project = 'project',
/**
* 资源库工作流
*/
Library = 'library',
/**
* 官方示例
*/
Example = 'example',
}
/** 流程弹窗状态 */
export interface WorkflowModalState {
/** 流程状态 */
status: WorkFlowListStatus | typeof WORKFLOW_LIST_STATUS_ALL;
/** @deprecated 数据类型, 当前请求的是流程数据还是商店数据 */
dataSourceType: DataSourceType;
/** 创建者 */
creator: MineActiveEnum;
/** @deprecated 工作流模板标签 */
workflowTag: number;
/** @deprecated 商品标签 */
productCategory: string;
/** 搜索关键字 */
query: string;
/** @deprecated 是否请求当前空间流程 */
isSpaceWorkflow: boolean;
/** 选中的 workflow 分类 */
workflowCategory?: WorkflowCategory;
/** @deprecated 商店产品下的排序方式 */
sortType?: SortType;
/** 弹窗内列表筛选的工作流类型,可以的值是 All、Workflow、Chatflow。用于列表里工作流类型筛选此时 Imageflow 已经合并到 Workflow 类型中了 */
listFlowMode: WorkflowMode;
}
/** 流程弹窗 */
export interface WorkFlowModalModeProps {
/** 当前弹窗来源,默认不传 */
from?: WorkflowModalFrom;
/** 流程类型, 工作流还是图像流, 默认工作流 */
flowMode?: WorkflowMode;
/** 隐藏的流程 */
excludedWorkflowIds?: string[];
/**
* filter 状态筛选组件是否显示全部状态选项,默认为 false
*/
filterOptionShowAll?: boolean;
/**
* 是否隐藏侧边栏,默认 false。用于场景详情页选择 workflow。
*/
hideSider?: boolean;
/* 是否隐藏作者筛选菜单 */
hideCreatorSelect?: boolean;
/**
* workflow item 是否显示删除按钮,默认 false用于场景 workflow 以及抖音分身工作流
*/
itemShowDelete?: boolean;
/** @deprecated 是否隐藏空间下 Workflow 列表模块 */
hiddenSpaceList?: boolean;
/**
* @deprecated 使用 hiddenWorkflowCategories
* 是否隐藏资源库模块
*/
hiddenLibrary?: boolean;
/** 是否隐藏创建工作流入口 */
hiddenCreate?: boolean;
/**
* @deprecated 探索分类已改为官方示例,使用 hiddenWorkflowCategories
* 隐藏探索分类
*/
hiddenExplore?: boolean;
/**
* 隐藏的工作流分类,用法同 hiddenLibrary、hiddenExplore
*/
hiddenWorkflowCategories?: WorkflowCategory[];
/**
* 隐藏工作流列表类型筛选
*/
hiddenListFlowModeFilter?: boolean;
/** 复制按钮文案, 默认「复制并添加」 */
dupText?: string;
/** 初始状态, 配置各筛选项 */
initState?: Partial<WorkflowModalState>;
/** 已选流程列表 */
workFlowList?: BotPluginWorkFlowItem[];
/** 已选流程列表变化 */
onWorkFlowListChange?: (newList: BotPluginWorkFlowItem[]) => void;
/** 选择流程 */
onAdd?: (
item: BotPluginWorkFlowItem,
config: {
/** 是否来源于复制 */
isDup: boolean;
/** 目标空间 */
spaceId: string;
},
) => void;
/** 移除流程 */
onRemove?: (item: BotPluginWorkFlowItem) => void;
/**
* 删除流程后的回调 hooks同时会执行 removeWorkflow 移除和 bot/场景 的关联
* @param item
*/
onDelete?: (item: BotPluginWorkFlowItem) => void;
/**
* 列表项点击
*
* 配置可覆盖默认行为: 新开页面打开详情页
* @returns 返回 { handled: true } 或 undefined 不执行默认操作,否则执行内部默认的点击事件
*/
onItemClick?:
| ((
item: WorkflowItemType,
/** 弹窗状态, 可用于初始化弹窗 */
modalState: WorkflowModalState,
) => { handled: boolean })
| ((
item: WorkflowItemType,
/** 弹窗状态, 可用于初始化弹窗 */
modalState: WorkflowModalState,
) => void);
/**
* 创建流程成功
*
* 配置可覆盖默认行为: 新页面打开复制后的流程详情, 带参数 from=createSuccess
*/
onCreateSuccess?: (info: {
spaceId: string;
workflowId: string;
flowMode: WorkflowMode;
}) => void;
/**
* 复制流程成功
*
* 配置可覆盖默认行为: Toast 提示复制成功, 继续编辑
*/
onDupSuccess?: (item: BotPluginWorkFlowItem) => void;
/** 项目内引入资源库文件触发的回调 */
onImport?: (
item: Pick<BotPluginWorkFlowItem, 'workflow_id' | 'name'>,
) => void;
bindBizId?: string;
bindBizType?: BindBizType;
projectId?: string;
onClose?: () => void;
/**
* 创建 workflow 弹窗内命名校验
*/
nameValidators?: RuleItem[];
/** 自定义 i18n 文案 */
i18nMap?: Partial<Record<ModalI18nKey, I18nKey>>;
}
export type WorkflowModalProps = {
className?: string;
visible?: boolean;
} & WorkFlowModalModeProps;
export { WorkflowInfo };

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WorkflowModalState, WorkflowCategory } from './type';
/**
* workflow modal 当前是否选中了 project 工具流分类
* @param modalState
*/
export const isSelectProjectCategory = (modalState?: WorkflowModalState) =>
modalState?.workflowCategory === WorkflowCategory.Project;

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
type BindBizType,
type OrderBy,
type WorkflowMode,
} from '@coze-workflow/base/api';
import { type SpaceType } from '@coze-arch/bot-api/playground_api';
import { type WorkflowModalState } from './type';
import { type I18nKey, type ModalI18nKey } from './hooks/use-i18n-text';
export interface WorkflowModalContextValue {
spaceId: string;
spaceType: SpaceType;
bindBizId?: string;
bindBizType?: BindBizType;
/** 当前项目 id只在项目内的 workflow 有该字段 */
projectId?: string;
/** 工作流类型,此参数由 WorkflowModal 弹窗创建时由 props 传进来,可能的值是 Workflow、Imageflow。用于区分添加哪种工作流 */
flowMode: WorkflowMode;
modalState: WorkflowModalState;
/** 更新弹窗状态, merge 的模式 */
updateModalState: (newState: Partial<WorkflowModalState>) => void;
orderBy: OrderBy;
setOrderBy: React.Dispatch<React.SetStateAction<OrderBy>>;
createModalVisible: boolean;
setCreateModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
/** 获取当前弹窗状态, 可用于恢复弹窗状态 */
getModalState: (ctx: WorkflowModalContextValue) => WorkflowModalState;
/** 自定义 i18n 文案 */
i18nMap?: Partial<Record<ModalI18nKey, I18nKey>>;
}
const WorkflowModalContext =
React.createContext<WorkflowModalContextValue | null>(null);
export default WorkflowModalContext;